/** * 左栏 · 规则目录 * 显示文件名、分数进度条、搜索、评查点分组列表 */ import { useState, useMemo } from 'react'; import type { ReviewPoint } from '../ReviewPointsList'; interface Statistics { total: number; success: number; warning: number; error: number; notApplicable: number; score: number; } interface RulesDirectoryProps { reviewPoints: ReviewPoint[]; statistics: Statistics; activeReviewPointResultId: string | number | null; fileName: string; onRuleSelect: (id: string | number) => void; onBack: () => void; } type PointStatus = 'pass' | 'warn' | 'fail' | 'skipped'; function classifyPoint(p: ReviewPoint): PointStatus { if (p.status === 'notApplicable' || p.status === 'not_applicable') return 'skipped'; if (p.result === true || (p.result === undefined && p.status === 'success')) return 'pass'; if (p.result === false) { if (p.status === 'error') return 'fail'; if (p.status === 'warning' || p.status === 'info') return 'warn'; } return 'pass'; } const STATUS_ICON: Record = { pass: { icon: 'ri-checkbox-circle-fill', color: 'text-emerald-500' }, warn: { icon: 'ri-lightbulb-flash-fill', color: 'text-amber-500' }, fail: { icon: 'ri-close-circle-fill', color: 'text-red-500' }, skipped: { icon: 'ri-forbid-2-line', color: 'text-slate-400' }, }; function RuleListItem({ point, isActive, showCategory, onClick, }: { point: ReviewPoint; isActive: boolean; showCategory?: boolean; onClick: () => void; }) { const cls = classifyPoint(point); const s = STATUS_ICON[cls]; return ( ); } export function RulesDirectory({ reviewPoints, statistics, activeReviewPointResultId, fileName, onRuleSelect, onBack, }: RulesDirectoryProps) { const [searchText, setSearchText] = useState(''); const [passOpen, setPassOpen] = useState(true); const [openCategories, setOpenCategories] = useState>(new Set()); const q = searchText.toLowerCase(); const matchSearch = (p: ReviewPoint) => !q || (p.pointName?.toLowerCase().includes(q)) || (p.pointCode?.toLowerCase().includes(q)) || (p.groupName?.toLowerCase().includes(q)); const { needAttention, passed, passedByGroup } = useMemo(() => { const filtered = reviewPoints.filter(matchSearch); const needAttention = filtered.filter( (p) => classifyPoint(p) !== 'pass' && classifyPoint(p) !== 'skipped' ); const passed = filtered.filter((p) => classifyPoint(p) === 'pass'); const passedByGroup: Record = {}; passed.forEach((p) => { const key = p.groupName || '未分组'; (passedByGroup[key] = passedByGroup[key] || []).push(p); }); return { needAttention, passed, passedByGroup }; }, [reviewPoints, searchText]); // 计算进度条百分比 const total = statistics.total || reviewPoints.length; const passPct = total > 0 ? (statistics.success / total) * 100 : 0; const warnPct = total > 0 ? (statistics.warning / total) * 100 : 0; const errPct = total > 0 ? (statistics.error / total) * 100 : 0; const naPct = total > 0 ? ((statistics.notApplicable || 0) / total) * 100 : 0; const attentionCount = needAttention.length; const passedCount = passed.length; const toggleCategory = (cat: string) => { setOpenCategories((prev) => { const next = new Set(prev); if (next.has(cat)) next.delete(cat); else next.add(cat); return next; }); }; return ( ); }