/** * 左栏 / 规则目录 * 展示文件信息、分数进度、搜索与评查点列表。 */ import { useMemo, useState } 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'; type StatusFilter = 'all' | PointStatus; function classifyPoint(point: ReviewPoint): PointStatus { if (point.status === 'notApplicable' || point.status === 'not_applicable') return 'skipped'; if (point.result === true || (point.result === undefined && point.status === 'success')) return 'pass'; if (point.result === false) { if (point.status === 'error') return 'fail'; if (point.status === 'warning' || point.status === 'info') return 'warn'; } if (point.status === 'error') return 'fail'; if (point.status === 'warning' || point.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' }, }; const FILTER_CHIPS: Array<{ key: StatusFilter; label: string; getClassName: (active: boolean) => string; }> = [ { key: 'all', label: '全部', getClassName: (active) => active ? 'bg-slate-700 text-white border-slate-700' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-50', }, { key: 'warn', label: '提醒', getClassName: (active) => active ? 'bg-amber-500 text-white border-amber-500' : 'bg-white text-amber-700 border-amber-200 hover:bg-amber-50', }, { key: 'fail', label: '问题', getClassName: (active) => active ? 'bg-red-500 text-white border-red-500' : 'bg-white text-red-700 border-red-200 hover:bg-red-50', }, { key: 'pass', label: '通过', getClassName: (active) => active ? 'bg-emerald-500 text-white border-emerald-500' : 'bg-white text-emerald-700 border-emerald-200 hover:bg-emerald-50', }, { key: 'skipped', label: '跳过', getClassName: (active) => active ? 'bg-slate-500 text-white border-slate-500' : 'bg-white text-slate-600 border-slate-200 hover:bg-slate-100', }, ]; function RuleListItem({ point, isActive, showCategory, onClick, }: { point: ReviewPoint; isActive: boolean; showCategory?: boolean; onClick: () => void; }) { const status = classifyPoint(point); const statusMeta = STATUS_ICON[status]; const trailingLabel = showCategory ? point.groupName : point.pointCode || point.pointId || ''; return ( ); } export function RulesDirectory({ reviewPoints, statistics, activeReviewPointResultId, fileName, onRuleSelect, onBack, }: RulesDirectoryProps) { const [searchText, setSearchText] = useState(''); const [statusFilter, setStatusFilter] = useState('all'); const [passOpen, setPassOpen] = useState(true); const [openCategories, setOpenCategories] = useState>( () => new Set(reviewPoints.map((point) => point.groupName).filter(Boolean)) ); const q = searchText.toLowerCase(); const matchSearch = (point: ReviewPoint) => !q || point.pointName?.toLowerCase().includes(q) || String(point.pointId ?? '').toLowerCase().includes(q) || String(point.pointCode ?? '').toLowerCase().includes(q) || point.groupName?.toLowerCase().includes(q); const matchStatus = (point: ReviewPoint) => statusFilter === 'all' || classifyPoint(point) === statusFilter; const statusCounts = useMemo(() => { return reviewPoints.reduce>( (acc, point) => { const status = classifyPoint(point); acc.all += 1; acc[status] += 1; return acc; }, { all: 0, pass: 0, warn: 0, fail: 0, skipped: 0 } ); }, [reviewPoints]); const { needAttention, passed, passedByGroup, hasSearchResult } = useMemo(() => { const filtered = reviewPoints.filter((point) => matchSearch(point) && matchStatus(point)); const needAttention = filtered.filter((point) => classifyPoint(point) !== 'pass'); const passed = filtered.filter((point) => classifyPoint(point) === 'pass'); const passedByGroup: Record = {}; passed.forEach((point) => { const key = point.groupName || '未分组'; (passedByGroup[key] = passedByGroup[key] || []).push(point); }); return { needAttention, passed, passedByGroup, hasSearchResult: filtered.length > 0, }; }, [reviewPoints, q, statusFilter]); 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 = reviewPoints.filter((point) => classifyPoint(point) !== 'pass').length; const passedCount = reviewPoints.filter((point) => classifyPoint(point) === 'pass').length; const toggleCategory = (category: string) => { setOpenCategories((prev) => { const next = new Set(prev); if (next.has(category)) next.delete(category); else next.add(category); return next; }); }; return ( ); }