/** * 文档评审结果统计显示组件 * 显示通过、警告、错误、人工四个维度的统计数据及版本差异 */ import { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; interface StatItem { label: string; current: number | null; previous?: number | null; color: string; icon: string; reverseColors?: boolean; messages?: string[]; onClick?: () => void; isClickable?: boolean; } interface ResultStatsProps { passCount: number | null; warningCount: number | null; errorCount: number | null; manualCount: number | null; previousPassCount?: number | null; previousWarningCount?: number | null; previousErrorCount?: number | null; previousManualCount?: number | null; warningMessages?: string[]; errorMessages?: string[]; manualMessages?: string[]; className?: string; } /** * 计算差异类型和差异值 */ function calculateDiff(current: number | null, previous: number | null | undefined): { diff: number; type: 'increase' | 'decrease' | 'same'; } | null { if (current === null || previous === null || previous === undefined) { return null; } const diff = current - previous; if (diff > 0) { return { diff, type: 'increase' }; } else if (diff < 0) { return { diff: Math.abs(diff), type: 'decrease' }; } else { return { diff: 0, type: 'same' }; } } /** * 单个统计项组件 */ function StatItemComponent({ label, current, previous, color, icon, reverseColors = false, messages = [], onClick, isClickable = false }: StatItem) { const diffResult = calculateDiff(current, previous); const [showPopover, setShowPopover] = useState(false); const [popoverPosition, setPopoverPosition] = useState({ top: 0, left: 0 }); const itemRef = useRef(null); const popoverRef = useRef(null); // 定义图标颜色映射 const iconColorMap: Record = { green: '#16a34a', yellow: '#f59e0b', red: '#dc2626', blue: '#3b82f6' }; const iconColor = iconColorMap[color] || '#6b7280'; // 根据是否反转颜色来确定差异显示的颜色 const getDiffColor = (type: 'increase' | 'decrease' | 'same'): string => { if (type === 'same') return '#9ca3af'; if (reverseColors) { // 对于通过数量:增加是好的(绿色),减少是坏的(红色) return type === 'increase' ? '#059669' : '#dc2626'; } else { // 对于错误/警告:增加是坏的(红色),减少是好的(绿色) return type === 'increase' ? '#dc2626' : '#059669'; } }; // 计算气泡框位置 const updatePopoverPosition = () => { if (itemRef.current) { const rect = itemRef.current.getBoundingClientRect(); const scrollTop = window.pageYOffset || document.documentElement.scrollTop; const scrollLeft = window.pageXOffset || document.documentElement.scrollLeft; setPopoverPosition({ top: rect.bottom + scrollTop + 8, left: rect.left + scrollLeft + rect.width / 2 }); } }; // 点击外部关闭气泡框 useEffect(() => { if (!showPopover) return; const handleClickOutside = (event: MouseEvent) => { if ( popoverRef.current && !popoverRef.current.contains(event.target as Node) && itemRef.current && !itemRef.current.contains(event.target as Node) ) { setShowPopover(false); } }; const handleScroll = () => { updatePopoverPosition(); }; document.addEventListener('mousedown', handleClickOutside); window.addEventListener('scroll', handleScroll, true); window.addEventListener('resize', handleScroll); return () => { document.removeEventListener('mousedown', handleClickOutside); window.removeEventListener('scroll', handleScroll, true); window.removeEventListener('resize', handleScroll); }; }, [showPopover]); const handleClick = () => { if (isClickable && messages.length > 0) { updatePopoverPosition(); setShowPopover(!showPopover); onClick?.(); } }; // 如果当前值为 null,显示 "-" if (current === null) { return (
{label} -
); } // 去重并计算重复次数 const deduplicateMessages = (msgs: string[]): Array<{ text: string; count: number }> => { const messageMap = new Map(); msgs.forEach(msg => { const count = messageMap.get(msg) || 0; messageMap.set(msg, count + 1); }); return Array.from(messageMap.entries()).map(([text, count]) => ({ text, count })); }; const popoverContent = showPopover && messages.length > 0 && (
e.stopPropagation()} >
{label}详情
{deduplicateMessages(messages).map((item, index) => (
{item.text} {item.count > 1 && (×{item.count})}
))}
); return ( <>
0 ? 'clickable' : ''}`} ref={itemRef} onClick={handleClick} > {label}
{current} {diffResult && ( {diffResult.type === 'increase' && ( <> +{diffResult.diff} )} {diffResult.type === 'decrease' && ( <> -{diffResult.diff} )} {diffResult.type === 'same' && ( <> 0 )} )}
{/* 使用 Portal 将气泡框渲染到 body */} {typeof document !== 'undefined' && popoverContent && createPortal(popoverContent, document.body)} ); } /** * 结果统计组件 */ export function ResultStats({ passCount, warningCount, errorCount, manualCount, previousPassCount, previousWarningCount, previousErrorCount, previousManualCount, warningMessages = [], errorMessages = [], manualMessages = [], className = '' }: ResultStatsProps) { const stats: StatItem[] = [ { label: '通过', current: passCount, previous: previousPassCount, color: 'green', icon: 'ri-check-line', reverseColors: true, // 通过数量增加是好的,减少是坏的 messages: [], isClickable: false }, { label: '警告', current: warningCount, previous: previousWarningCount, color: 'yellow', icon: 'ri-alert-line', messages: warningMessages, isClickable: warningMessages.length > 0 }, { label: '错误', current: errorCount, previous: previousErrorCount, color: 'red', icon: 'ri-close-circle-line', messages: errorMessages, isClickable: errorMessages.length > 0 }, { label: '人工', current: manualCount, previous: previousManualCount, color: 'blue', icon: 'ri-user-line', messages: manualMessages, isClickable: manualMessages.length > 0 } ]; return (
{stats.map((stat, index) => ( ))}
); }