/** * 评查点列表组件 * * 功能概述: * - 展示评查结果统计信息(总计、通过、警告、错误数量) * - 提供评查点过滤功能(按状态和搜索文本) * - 显示评查点详细信息(标题、状态、内容、建议修改等) * - 支持评查点操作(一键替换、人工审核等) * * 组件结构: * - 统计区域: 显示评查点数量统计 * - 搜索区域: 提供文本搜索功能 * - 评查点列表: 展示所有评查点 * - 评查点卡片: 展示单个评查点详情 * - 评查点头部: 显示标题和状态 * - 评查点内容: 显示当前内容和问题 * - 建议修改区域: 显示建议的修改内容 * - 操作按钮: 提供一键替换和人工审核功能 */ import { useState, useEffect, useRef } from 'react'; import { toastService } from '../ui/Toast'; import { createPortal } from 'react-dom'; // 导入React Portal API,用于将组件渲染到DOM树的不同位置 import { Tooltip } from '../ui/Tooltip'; // import '../../styles/components/TooltipStyles.css'; /** * 比较方法映射 * 将后端返回的比较方法英文值映射为友好的中文显示 */ const compareMethodMap: Record = { 'exact': '精确匹配', 'contains': '包含关系', 'semantic': '大模型语义匹配', // 可以根据需要添加更多映射 }; /** * 获取比较方法的中文显示 * @param method 比较方法的原始值 * @returns 映射后的中文显示文本 */ const getCompareMethodText = (method?: string): string => { if (!method) return '相等'; const text = compareMethodMap[method] || method; // 确保返回的是字符串类型 return typeof text === 'string' ? text : String(text); }; /** * 规则类型映射 * 将后端返回的规则类型英文值映射为友好的中文显示 */ const ruleTypeMap: Record = { 'exists': '有无判断', 'format': '格式判断', 'logic': '逻辑判断', 'regex': '正则表达式', // 可以根据需要添加更多映射 }; /** * 获取规则类型的中文显示 * @param type 规则类型的原始值 * @returns 映射后的中文显示文本 */ const getRuleTypeText = (type?: string): string => { if (!type) return ''; return ruleTypeMap[type] || type; }; /** * 评查点类型定义 * 用于展示单个评查结果 */ export interface ReviewPoint { id: string; documentId?: string; pointId?: string; editAuditStatusId?: string | number; editAuditStatus: number; editAuditStatusMessage?: string; // 添加审核意见字段 pointName: string; title: string; groupName: string; status: string; content: Record; suggestion: string; needsHumanReview?: boolean; humanReviewNote?: string; humanReviewBy?: string; humanReviewTime?: string; contentPage?: Record; position?: { section: string; index: number; }; result?: boolean; legalBasis?: { name?: string; content?: string; articles?: Array; [key: string]: unknown; }; postAction?: string; actionContent?: string; evaluationConfig?: { rules?: Array<{ type: string; config?: { fields?: string[]; pairs?: Array<{ sourceField?: string; targetField?: string }>; logic?: string; }; }>; }; evaluatedPointResultsLog?: { rules: Array<{ id: string; type: string; res?: boolean; config: Record; }>; }; } // 统计数据类型 interface Statistics { total: number; success: number; warning: number; error: number; score: number; } interface ReviewPointsListProps { reviewPoints: ReviewPoint[]; statistics: Statistics; activeReviewPointResultId: string | null; onReviewPointSelect: (id: string, page?: number) => void; onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void; } /** * 全局状态对象,存储当前活动的提示框信息 * 这种方式避免了复杂的状态提升或Context API的使用 */ let activeTooltip = { show: false, // 控制提示框是否显示 content: null as React.ReactNode, // 提示框内容(React节点) position: { top: 0, left: 0 }, // 提示框在屏幕上的位置 ready: false // 新增:控制是否已准备好显示 }; /** * 提示框Portal组件 * * 使用React Portal将提示框渲染到document.body下, * 这样可以确保提示框不受任何父元素overflow或z-index限制 */ function TooltipPortal() { // 使用本地状态保存提示框信息的副本 const [tooltip, setTooltip] = useState(activeTooltip); useEffect(() => { // 通过自定义事件机制监听全局tooltip状态更新 const updateTooltip = () => { // 使用扩展运算符创建对象副本,确保状态更新被React检测到 setTooltip({...activeTooltip}); }; // 添加事件监听器 window.addEventListener('tooltip-update', updateTooltip); // 组件卸载时清理事件监听器 return () => { window.removeEventListener('tooltip-update', updateTooltip); }; }, []); // 如果不显示或没有内容,则不渲染任何东西 if (!tooltip.show || !tooltip.content) return null; // 使用createPortal将提示框内容渲染到document.body return createPortal(
{tooltip.content} {/* 添加小三角形指向提示框指向的元素 */}
, document.body // 将内容挂载到body元素,完全脱离原组件DOM结构 ); } /** * 显示提示框的辅助函数 * @param content 要显示的React节点内容 * @param position 显示位置坐标 */ function showTooltip(content: React.ReactNode, position: { top: number; left: number }): void { // 先设置内容和位置,但不立即显示 activeTooltip = { show: true, content, position, ready: false // 初始设为未准备好 }; // 触发事件,让TooltipPortal渲染tooltip(但不可见) window.dispatchEvent(new Event('tooltip-update')); // 使用RAF确保tooltip已渲染到DOM后再计算最终位置 requestAnimationFrame(() => { // 查找刚创建的tooltip元素 const tooltipElement = document.querySelector('.fixed.bg-white.shadow-lg.rounded-md') as HTMLElement; if (tooltipElement) { // 获取tooltip的实际尺寸 const tooltipRect = tooltipElement.getBoundingClientRect(); // 重新计算位置,确保tooltip不会超出视口 let adjustedTop = position.top; let adjustedLeft = position.left; // 检查是否超出右边界 if (adjustedLeft - tooltipRect.width < 0) { adjustedLeft = tooltipRect.width + 10; // 留一些边距 } // 检查是否超出上边界 if (adjustedTop - tooltipRect.height / 2 < 0) { adjustedTop = tooltipRect.height / 2 + 10; } // 检查是否超出下边界 if (adjustedTop + tooltipRect.height / 2 > window.innerHeight) { adjustedTop = window.innerHeight - tooltipRect.height / 2 - 10; } // 更新位置并设为准备好显示 activeTooltip.position = { top: adjustedTop, left: adjustedLeft }; activeTooltip.ready = true; // 再次触发事件更新显示状态 window.dispatchEvent(new Event('tooltip-update')); } else { // 如果找不到tooltip元素,直接显示 activeTooltip.ready = true; window.dispatchEvent(new Event('tooltip-update')); } }); } /** * 隐藏提示框的辅助函数 */ function hideTooltip(): void { // 设置为不显示状态并重置ready状态 activeTooltip.show = false; activeTooltip.ready = false; // 触发自定义事件,通知TooltipPortal组件更新状态 window.dispatchEvent(new Event('tooltip-update')); } /** * React组件表格Tooltip * 将文本数据解析为表格并使用React组件渲染 * 条件性Tooltip组件 * 只有当内容超过2行时才显示tooltip */ const ReactTableTooltip = ({ content }: { content: string }) => { const [showTooltip, setShowTooltip] = useState(false); const [renderedContent, setRenderedContent] = useState(null); const textRef = useRef(null); const isTableLike = content.includes('\t') && content.includes('\n'); useEffect(() => { const checkTextOverflow = () => { const element = textRef.current; if (element) { // 如果是表格格式,总是显示tooltip;否则只在文本溢出时显示 setShowTooltip(isTableLike || element.scrollHeight > element.clientHeight); } }; // 预渲染内容并缓存 if (isTableLike) { setRenderedContent(renderReactTable(content)); } else { setRenderedContent(content); } requestAnimationFrame(checkTextOverflow); window.addEventListener('resize', checkTextOverflow); return () => { window.removeEventListener('resize', checkTextOverflow); }; }, [content, isTableLike]); // 解析表格数据 const parseTableData = (text: string) => { const rows = text.split('\n').map(row => row.split('\t')); return rows; }; // 渲染React表格 const renderReactTable = (text: string) => { try { const tableData = parseTableData(text); const hasHeader = tableData.length > 0; return (
{hasHeader && ( {tableData[0].map((cell, cellIndex) => ( ))} )} {tableData.slice(1).map((row, rowIndex) => ( {row.map((cell, cellIndex) => ( ))} ))}
{cell || ' '}
{cell || ' '}
); } catch (error) { console.error('表格渲染错误:', error); return
表格渲染错误
; } }; return (
{showTooltip ? (
{content}
) : (
{content}
)}
); }; export function ReviewPointsList({ reviewPoints, statistics, activeReviewPointResultId, onReviewPointSelect, onStatusChange }: ReviewPointsListProps) { // 状态管理 const [editingReviewPoint, setEditingReviewPoint] = useState(null); // 当前正在编辑的评查点ID const [searchText, setSearchText] = useState(''); // 搜索文本 const [statusFilter, setStatusFilter] = useState(null); // 状态过滤 // const [suggestionTexts, setSuggestionTexts] = useState>({}); // 存储每个评查点的建议文本 // 添加重新审核意见的状态/ 用户输入的修改内容 / 用户提前写好的修改内容 const [manualReviewNotes, setManualReviewNotes] = useState>({}); // 存放评查点ID与有效页码的映射 const [effectivePages, setEffectivePages] = useState>({}); // 初始化建议文本 useEffect(() => { // 使用函数式更新,不再需要外部 manualReviewNotes 变量 setManualReviewNotes(prev => { const notes = { ...prev }; reviewPoints.forEach(point => { // 优先使用editAuditStatusMessage,如果为空则使用actionContent或suggestion notes[point.id] = point.editAuditStatusMessage || point.actionContent || point.suggestion || ''; }); return notes; }); }, [reviewPoints]); // 处理建议文本变更 // const handleSuggestionChange = (reviewPointId: string, text: string) => { // setSuggestionTexts(prev => ({ // ...prev, // [reviewPointId]: text // })); // }; /** * 处理评查点审核操作 * @param reviewPointResultId 评查点结果ID * @param editAuditStatusId 审核状态记录ID * @param action 操作类型: 'approve' 通过 / 'reject' 不通过 / 'review' 重新审核 * @param message 用户输入的审核内容 */ const handleReviewAction = (reviewPointResultId: string, editAuditStatusId: string | number | undefined, action: 'approve' | 'reject' | 'review', message: string) => { // 通过/不通过时,必须有输入内容 if(action !== 'review' && message.trim() === ''){ toastService.error('请输入审核意见'); return; } if (action === 'review') { // 重新审核时,不更新结果状态,也不更新审核意见和审核状态 onStatusChange(reviewPointResultId, editAuditStatusId || '', 'review', message); // 找到当前评查点并更新其editAuditStatus为0,使其立即显示通过/不通过按钮 const updatedReviewPoint = reviewPoints.find(point => point.id === reviewPointResultId); if (updatedReviewPoint) { updatedReviewPoint.editAuditStatus = 0; // 重新审核时不更新输入框内容 } } else { // 通过/不通过时,更新结果状态和审核意见 onStatusChange(reviewPointResultId, editAuditStatusId || '', action === 'approve' ? 'true' : 'false', message); // 找到当前评查点并立即更新其editAuditStatusMessage const updatedReviewPoint = reviewPoints.find(point => point.id === reviewPointResultId); if (updatedReviewPoint) { updatedReviewPoint.editAuditStatusMessage = message; } } // 清除编辑状态 setEditingReviewPoint(null); }; /** * 过滤评查点 * 根据搜索文本和状态过滤条件筛选评查点 */ const filteredReviewPoints = reviewPoints.filter(point => { // 匹配搜索文本 const matchesSearch = searchText === '' || point.pointName.toLowerCase().includes(searchText.toLowerCase()) || point.title.toLowerCase().includes(searchText.toLowerCase()) || // point.groupName.toLowerCase().includes(searchText.toLowerCase()) || JSON.stringify(point.content).toLowerCase().includes(searchText.toLowerCase()) // 处理状态过滤 let matchesStatus = false; if (statusFilter === null) { // 未选择过滤条件时显示所有 matchesStatus = true; } else if (statusFilter === 'success') { // 过滤"通过"状态 matchesStatus = point.result === true; } else if (statusFilter === 'warning') { // 过滤"警告"状态 matchesStatus = point.result === false && (point.status === 'warning' || point.status === 'info'); } else if (statusFilter === 'error') { // 过滤"错误"状态 matchesStatus = point.result === false && point.status === 'error'; } // console.log('筛选point', point); return matchesSearch && matchesStatus; }); // console.log('筛选filteredReviewPoints', filteredReviewPoints); /** * 处理一键替换操作 * @param reviewPointId 评查点ID */ const handleReplace = (reviewPointId: string) => { // 在实际应用中,这里应该调用API进行内容替换 // 模拟替换操作 alert(`将为评查点 ${reviewPointId} 执行一键替换操作`); // 更新评查点状态为成功 // onStatusChange(reviewPointId, 'success'); }; /** * 渲染评查统计信息 * 显示总计、通过、警告、错误数量 */ const renderStatistics = () => { // 确保传入的statistics存在,否则使用计算值 const statsToUse = statistics || { total: reviewPoints.length, success: 0, warning: 0, error: 0, score: 0 }; // 计算各个状态的评查点数量 const successCount = reviewPoints.filter( point => point.result === true || (point.result === undefined && point.status === 'success') ).length; const warningCount = reviewPoints.filter( point => point.result === false && (point.status === 'warning' || point.status === 'info') ).length; const errorCount = reviewPoints.filter( point => point.result === false && point.status === 'error' ).length; // 如果没有计算值,则使用传入的统计值 const totalToShow = statsToUse.total === 0 ? reviewPoints.length : statsToUse.total; const successToShow = successCount || statsToUse.success; const warningToShow = warningCount || statsToUse.warning; const errorToShow = errorCount || statsToUse.error; return (
{/* 总计数量 */}
{/* 通过数量 */}
{/* 警告数量 */}
{/* 错误数量 */}
); }; /** * 渲染搜索框 * 用于按文本搜索评查点 */ const renderSearchBar = () => { return (
setSearchText(e.target.value)} /> {searchText && ( )}
); }; /** * 渲染评查点状态标签 * @param status 状态文本 * @param result 评查结果 * @param title 标签提示内容 * @returns 状态标签组件 */ const renderStatusBadge = (status: string, result?: boolean, title?: string) => { // 优先根据result判断是否通过 if (result === true) { return ( {title &&
{title}
} } placement="top" theme="light" trigger="hover" showArrow={true} className="tooltip-custom-offset tooltip-top" fixedPlacement={true} > 通过
); } // 当result为false时,根据status决定显示警告还是错误 if (result === false) { if (status === 'warning' || status === 'info') { return ( {title &&
{title}
} } placement="top" theme="light" trigger="hover" showArrow={true} className="tooltip-custom-offset tooltip-top" fixedPlacement={true} > 警告
); } else if (status === 'error') { return ( {title &&
{title}
} } placement="top" theme="light" trigger="hover" showArrow={true} className="tooltip-custom-offset tooltip-top" fixedPlacement={true} > 不通过
); } } }; /** * 渲染人工审核标记 * @param reviewPoint 评查点 * @returns 人工审核标记组件 */ const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => { if (reviewPoint.postAction === 'manual') { return ( 需人工 ); } return null; }; /** * 渲染人工审核注释 * @param reviewPoint 评查点 * @returns 人工审核注释组件 */ // const renderHumanReviewNote = (reviewPoint: ReviewPoint) => { // // 目前needsHumanReview和humanReviewNote都为空,所以不显示 // if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) { // return ( //
// {reviewPoint.humanReviewNote} // {reviewPoint.humanReviewBy && reviewPoint.humanReviewTime && ( //
// 审核人:{reviewPoint.humanReviewBy} | 时间:{reviewPoint.humanReviewTime} //
// )} //
// ); // } // return null; // }; /** * 渲染评查点主要内容 * @param reviewPoint 评查点 * @returns 评查点主要内容组件 */ const renderContent = (reviewPoint: ReviewPoint, otherRules: Array>) => { return ( <> {/* 渲染其他规则分组 */} {otherRules.map((rule, index) => { return
{renderOtherRule(rule, reviewPoint)}
; })} {/*
*/} {/* 渲染各个一致性的规则分组 */} {reviewPoint.evaluatedPointResultsLog?.rules?.map((rule, index) => { // console.log('rule-------', rule); if (rule.type === 'consistency') { // if (rule.res === true && reviewPoint.result === true) { return
{otherRules.length > 0 &&
} {renderConsistencyRule(rule, reviewPoint)}
; // }else { // return null; // } } if (rule.type === 'ai') { return
{otherRules.length > 0 &&
} {renderModelRule(rule, reviewPoint)}
; } })} ); }; /** * 渲染评查点一致性的规则的样式 * @param singleReviewPoint 一个评查点的一致性规则对象 * @param reviewPoint 评查点 * @returns 评查点一致性的规则的样式 */ const renderConsistencyRule = (singleReviewPoint: Record,reviewPoint: ReviewPoint) => { // 如果评查点结果为false,则判断单个规则是否通过,如果一致,则渲染 if (reviewPoint.result !== singleReviewPoint.res) { return null; } if (!singleReviewPoint || Object.keys(singleReviewPoint).length === 0) { return null; } // console.log('singleReviewPoint-------', singleReviewPoint); // 检查是否存在配置和pairs数组 const config = singleReviewPoint.config as { logic?: string; pairs?: Array<{ sourceField: Record; targetField: Record; res: boolean; compareMethod?: string; }>; selectedFields?: string[] } | undefined; if (!config || !config.pairs || !Array.isArray(config.pairs) || config.pairs.length === 0) { return null; } // 处理配对数据 const pairs = config.pairs; // 获取第一个有效页码 if (reviewPoint.id && !effectivePages[reviewPoint.id]) { for (const pair of pairs) { // 检查sourceField中是否有有效页码 const sourceFieldKey = Object.keys(pair.sourceField)[0]; if (sourceFieldKey && pair.sourceField[sourceFieldKey].page && Number(pair.sourceField[sourceFieldKey].page) > 0) { // 保存页码 setEffectivePages(prev => ({ ...prev, [reviewPoint.id || '']: Number(pair.sourceField[sourceFieldKey].page) })); break; } // 如果sourceField没有有效页码,检查targetField const targetFieldKey = Object.keys(pair.targetField)[0]; if (targetFieldKey && pair.targetField[targetFieldKey].page && Number(pair.targetField[targetFieldKey].page) > 0) { // 保存页码 setEffectivePages(prev => ({ ...prev, [reviewPoint.id || '']: Number(pair.targetField[targetFieldKey].page) })); break; } } } // 查找链条关系 const findChains = () => { type ChainItem = { field: string; data: { key: string; page: number; value: string }; res: boolean; compareMethod?: string; }; const chains: Array> = []; const visited = new Set(); // 构建字段映射关系 const fieldMap = new Map>(); pairs.forEach(pair => { // 提取源字段和目标字段的名称 const sourceFieldKey = Object.keys(pair.sourceField)[0]; const targetFieldKey = Object.keys(pair.targetField)[0]; if (!fieldMap.has(sourceFieldKey)) { fieldMap.set(sourceFieldKey, []); } fieldMap.get(sourceFieldKey)?.push({ targetField: targetFieldKey, data: { source: { key: sourceFieldKey, ...pair.sourceField[sourceFieldKey] }, target: { key: targetFieldKey, ...pair.targetField[targetFieldKey] } }, res: pair.res, compareMethod: pair.compareMethod }); }); // console.log('fieldMap-------', fieldMap); // 查找链条的起始点(只作为源不作为目标的字段) const startPoints = new Set(); for (const [key] of fieldMap.entries()) { let isTarget = false; for (const pair of pairs) { const targetFieldKey = Object.keys(pair.targetField)[0]; if (targetFieldKey === key) { isTarget = true; break; } } if (!isTarget) { startPoints.add(key); } } // console.log('startPoints-------', startPoints); // 从每个起始点开始构建链条 for (const startPoint of startPoints) { if (visited.has(startPoint)) continue; const tempChain: Array = []; let currentField = startPoint; // 向后构建链条 while (fieldMap.has(currentField)) { const targets = fieldMap.get(currentField); if (!targets || targets.length === 0) break; // 找到第一个未访问的目标 let nextTarget = null; for (const target of targets) { if (!visited.has(target.targetField)) { nextTarget = target; break; } } if (!nextTarget) break; // 添加源字段到链条 if (tempChain.length === 0) { tempChain.push({ field: currentField, data: nextTarget.data.source, res: nextTarget.res, compareMethod: nextTarget.compareMethod }); } // 添加目标字段到链条 tempChain.push({ field: nextTarget.targetField, data: nextTarget.data.target, res: nextTarget.res, compareMethod: nextTarget.compareMethod }); // 标记为已访问 visited.add(currentField); visited.add(nextTarget.targetField); // 移动到下一个字段 currentField = nextTarget.targetField; } // console.log('tempChain-------', tempChain); // 检查是否有链条,并处理链条断点 if (tempChain.length > 0) { // 如果链条长度大于1 if (tempChain.length > 1) { // 存储所有拆分后的链条 const splittedChains: Array> = []; // 从后往前遍历,检查每个相邻元素之间的连接 let endIndex = tempChain.length - 1; // 从倒数第一个元素开始往前遍历 for (let i = tempChain.length - 1; i > 0; i--) { // 检查当前元素与前一个元素的连接是否为false // 当前元素为tempChain[i],前一个元素为tempChain[i-1] // 连接结果存储在当前元素(tempChain[i])的result中 const connectionResult = tempChain[i].res; // 如果连接为false,或已到达起始位置,拆分链条 if (!connectionResult) { // 从当前断点到结束索引构建一个新链条 const newChain = tempChain.slice(i, endIndex + 1); if (newChain.length > 1) { splittedChains.push(newChain); // 将当前断点的前一个元素和后一个元素组成一个新链条 const newChain_before = tempChain.slice(i-1, i+1); // console.log('newChain_before-------', newChain_before); splittedChains.push(newChain_before); } // 更新结束索引为当前位置的前一个 endIndex = i - 1; } // 当到达第一个元素前一个位置时,需要处理剩余的链条 if (i === 1) { // 处理剩余部分 (0 到 endIndex) const remainingChain = tempChain.slice(0, endIndex + 1); if (remainingChain.length > 1) { splittedChains.push(remainingChain); } } } // 如果没有任何断点,添加整个链条 if (splittedChains.length === 0) { splittedChains.push([...tempChain]); } // 将拆分的链条添加到结果中 splittedChains.reverse().forEach(chain => { chains.push(chain); }); } else { // 如果链条长度为1,直接添加 chains.push([...tempChain]); } } } // console.log('chains-------', chains); // 处理没有找到的孤立对(这种情况只要规则配置是没问题的,就一定不会存在孤立的情况) for (const pair of pairs) { const sourceFieldKey = Object.keys(pair.sourceField)[0]; const targetFieldKey = Object.keys(pair.targetField)[0]; if (!visited.has(sourceFieldKey) || !visited.has(targetFieldKey)) { const isolatedPair: Array = [ { field: sourceFieldKey, data: { key: sourceFieldKey, ...pair.sourceField[sourceFieldKey] }, res: pair.res }, { field: targetFieldKey, data: { key: targetFieldKey, ...pair.targetField[targetFieldKey] }, res: pair.res } ]; chains.push(isolatedPair); visited.add(sourceFieldKey); visited.add(targetFieldKey); } } return chains; }; const chains = findChains(); return (
{chains.map((chain, chainIndex) => { const isLongChain = chain.length > 2; const res = chain[1].res; // 获取compareMethod // const compareMethod = chain[1].compareMethod || ''; // 转换为友好的显示文本 // const compareMethodText = getCompareMethodText(compareMethod); // 确定样式类名 const itemClassName = res ? "comparison-item match" : "comparison-item mismatch"; // console.log('currentchain-------', chain); // 如果是长链(3个或以上元素) if (isLongChain) { // console.log('currentlongchain-------', chain); return (
{ e.stopPropagation(); // 遍历chain找到第一个有效的page let hasPage = false; for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { hasPage = true; onReviewPointSelect(reviewPoint.id, Number(item.data.page)); break; } } if (!hasPage) { // toastService.error('没有找到有效的页码'); } }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); // 遍历chain找到第一个有效的page for (const item of chain) { if (item.data.page && typeof onReviewPointSelect === 'function') { onReviewPointSelect(reviewPoint.id, Number(item.data.page)); break; } } } }} role="button" tabIndex={0} >
{/* 展示链条 */}
{chain.map((item, idx) => ( {item.field} {idx < chain.length - 1 && ( {typeof chain[idx+1].compareMethod === 'object' ? '' : getCompareMethodText(chain[idx+1].compareMethod)} )} ))}
{/* 展示链条的每个元素的内容 */}
{chain.map((item, idx) => ( ))}
{res ? ( ) : ( )} {/* 使用鼠标事件处理悬停提示 */}
{ // 获取元素位置信息 const rect = e.currentTarget.getBoundingClientRect(); // 创建提示框内容 const content = (
{chain.map((item, idx) => idx >= 1 ? (
{typeof item.compareMethod === 'object' ? '' : `${getCompareMethodText(item.compareMethod)}:`}
{res ? '通过' : '不通过'}
) : null )}
); // 显示提示框 showTooltip(content, { top: rect.top + rect.height/2, left: rect.left }); }} onMouseLeave={hideTooltip} />
); } // 如果是标准的成对比较(2个元素) return (
{chain[0].field.split('-').pop()}
{res ? ( ) : ( )} {/* 使用鼠标事件处理悬停提示 */}
{ // 获取元素位置信息 const rect = e.currentTarget.getBoundingClientRect(); // 创建提示框内容 const content = (
{typeof chain[1].compareMethod === 'object' ? '' : `${getCompareMethodText(chain[1].compareMethod)}:`}
{res ? '通过' : '不通过'}
); // 显示提示框,稍微向下偏移,便于鼠标移动到tooltip上 showTooltip(content, { top: rect.top + rect.height/2, left: rect.left }); }} onMouseLeave={hideTooltip} />
); })}
); }; /** * 渲染评查点有无判断,格式判断,逻辑判断,正则表达式的规则的样式 * @param otherRule 评查点规则数据 * @param reviewPoint 关联的评查点 * @returns 评查点有无判断,格式判断,逻辑判断,正则表达式的规则的样式 */ const renderOtherRule = (otherRule: Record, reviewPoint: ReviewPoint) => { const fieldKey = otherRule.fieldKey as string; const fieldValue = otherRule.fieldValue as { type: Record; }; // 获取res的综合结果 // 如果存在res=false,则整体结果为false,否则为true const hasFailure = Object.values(fieldValue?.type || {}).some(item => item.res === false); const overallResult = !hasFailure; // 找到res为false的条目,用于主要显示 const failedTypeEntry = Object.entries(fieldValue?.type || {}).find(([, item]) => item.res === false); // 如果没有失败的条目,则使用第一个条目 const mainTypeEntry = failedTypeEntry || Object.entries(fieldValue?.type || {})[0]; // 如果没有任何条目,则返回空 if (!mainTypeEntry) return null; const [, mainTypeValue] = mainTypeEntry; /** * 创建提示框内容 * 这个函数返回一个React节点,用于在提示框中显示 * 它将为每种规则类型(exists/format/logic/regex)创建一个带有状态标识的项目 */ const createTooltipContent = () => { return (
{Object.entries(fieldValue?.type || {}).map(([typeKey, typeValue]) => (
{getRuleTypeText(typeKey)}:
{typeValue.res ? '通过' : '不通过'}
))}
); }; /** * 处理鼠标悬停事件 * 当鼠标悬停在状态指示器上时,计算提示框应该显示的位置并显示提示框 * @param e 鼠标事件对象 */ const handleMouseEnter = (e: React.MouseEvent): void => { // 获取触发元素的位置信息 const rect = e.currentTarget.getBoundingClientRect(); // 调用全局函数显示提示框,传递内容和位置信息 showTooltip( createTooltipContent(), { top: rect.top + rect.height/2, left: rect.left } ); }; return ( ); }; /** * 渲染评查点大模型判断的规则的样式 * * 该函数处理AI模型评估的结果展示,包括: * 1. 从规则配置中提取字段和评估结果 * 2. 为每个字段创建可点击的UI元素,显示内容和评估状态 * 3. 展示模型的评估消息 * 4. 处理字段点击导航到相应页面的逻辑 * * @param aiRule 评查点大模型判断的规则对象 * @param reviewPoint 关联的评查点对象 * @returns React组件,用于显示AI模型评估结果 */ const renderModelRule = (aiRule: Record, reviewPoint: ReviewPoint) => { // 从aiRule中提取配置信息 const config = aiRule.config as { model?: string; fields?: Record; message?: string; res?: boolean; } | undefined; // 如果评查点评查结果和规则的结果不一致,则不渲染,跳过 if(config?.res !== reviewPoint.result){ return null; } // 如果配置不存在,不渲染任何内容 if (!config) return null; // 获取第一个有效页码 if (reviewPoint.id && !effectivePages[reviewPoint.id] && config.fields) { for (const field of Object.values(config.fields || {})) { if (field.page && Number(field.page) > 0) { setEffectivePages(prev => ({ ...prev, [reviewPoint.id || '']: Number(field.page) })); break; } } } // 创建一个数组来存储需要渲染的JSX元素 const fieldElements: JSX.Element[] = []; // 遍历fields,获取每个字段的值并生成对应的JSX元素 if (config.fields) { Object.entries(config.fields).forEach(([key, value], index) => { const res = value.value.trim() !== ''; fieldElements.push( ); }); } // 渲染AI模型返回的评估消息 if (config.message) { // 检查message是否为对象,如果是则转换为字符串 const messageContent = typeof config.message === 'object' ? JSON.stringify(config.message) : String(config.message); // 添加模型评估消息区域,使用蓝色背景突出显示 fieldElements.push(

{messageContent}

); } // 返回包含所有元素的React片段 return <>{fieldElements}; }; /** * 过滤评查点中的规则,把type是exists、format、logic、regex的规则中重复的进行去重和合并 * * 该函数的主要作用: * 1. 从评查点的evaluatedPointResultsLog中提取特定类型的规则 * 2. 将相同字段(fieldKey)的不同规则类型结果合并到一起 * 3. 为UI渲染准备统一结构的数据 * * 支持的规则类型: * - exists: 有无判断规则 * - format: 格式判断规则 * - logic: 逻辑判断规则 * - regex: 正则表达式规则 * * @param reviewPoint 评查点对象 * @returns 合并后的规则数组,每个元素包含字段名和各类规则的评估结果 */ const filterOtherRule = (reviewPoint: ReviewPoint) => { // 定义接口描述规则字段值的结构 interface RuleFieldValue { page?: number | string; value?: string; type: Record; } const allRule: Array<{ fieldKey: string; fieldValue: RuleFieldValue; }> = []; for (const rule of reviewPoint.evaluatedPointResultsLog?.rules || []) { // 如果评查点评查结果和规则的结果不一致,则不渲染,跳过 if(rule.config.res !== reviewPoint.result){ continue; } // 处理"有无判断"类型的规则 if (rule.type === 'exists') { // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; fields: Record; logic?: string; }; // 如果res为true,则遍历fields,提取不为空的字段 if (config.res) { // 遍历fields对象的每个属性 Object.entries(config.fields).forEach(([key, fieldValue]) => { // 只处理值不为空的字段 if (fieldValue.value && fieldValue.value.trim() !== '') { // 创建新对象并添加type标记 const newItem = { fieldKey: key, fieldValue: { ...fieldValue, type: { exists: true } } }; allRule.push(newItem); } }); } else { // 如果res为false,则遍历fields,提取所有字段 Object.entries(config.fields).forEach(([key, fieldValue]) => { // 根据值是否为空添加不同的type标记 const isValueEmpty = !fieldValue.value || fieldValue.value.trim() === ''; // 创建新对象并添加type标记 const newItem = { fieldKey: key, fieldValue: { ...fieldValue, type: { exists: isValueEmpty ? false : true } } }; allRule.push(newItem); }); } } // 处理"格式判断"类型的规则 if (rule.type === 'format') { // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; field: Record; formatType?: string; parameters?: string; }; // 从config中获取field对象 // 注意:根据示例,format类型中是field而不是fields if (config.field) { // 获取field中唯一的键值对 const entries = Object.entries(config.field); if (entries.length > 0) { const [key, fieldValue] = entries[0]; // 创建新对象并添加type标记 const newItem = { fieldKey: key, fieldValue: { ...fieldValue, type: { format: config.res } // 标记为format类型,结果为config.res } }; allRule.push(newItem); } } } // 处理"逻辑判断"类型的规则 if (rule.type === 'logic') { // 使用类型断言获取config对象的具体结构 const config = rule.config as { logic: string; res: boolean; conditions: Array<{ field: Record; value: string; operator: string; res: boolean; }>; }; // 遍历conditions数组 if (config.conditions && Array.isArray(config.conditions)) { config.conditions.forEach(condition => { // 从condition中获取field对象 const entries = Object.entries(condition.field); if (entries.length > 0) { const [key, fieldValue] = entries[0]; // 创建新对象并添加type标记 const newItem = { fieldKey: key, fieldValue: { ...fieldValue, type: { logic: condition.res } } }; allRule.push(newItem); } }); } } // 处理"正则表达式"类型的规则 if (rule.type === 'regex') { // 使用类型断言获取config对象的具体结构 const config = rule.config as { res: boolean; field: Record; pattern?: string; matchType?: string; selectedFields?: string[]; }; if (config.field) { const entries = Object.entries(config.field); if (entries.length > 0) { const [key, fieldValue] = entries[0]; // 创建新对象并添加type标记 const newItem = { fieldKey: key, fieldValue: { ...fieldValue, type: { regex: config.res } } }; allRule.push(newItem); } } } } // console.log('allRule-------', allRule); // 对allRule进行去重和合并 const mergedRules: Array<{ fieldKey: string; fieldValue: { type: Record; }; }> = []; // 使用对象存储相同fieldKey的项,便于快速查找和合并 const fieldKeyMap: Record; }; }> = {}; // 第一步:按fieldKey分组并合并不同类型的规则结果 allRule.forEach(item => { const fieldKey = item.fieldKey; const fieldValue = item.fieldValue; const typeKey = Object.keys(fieldValue.type)[0]; // 获取类型名称(exists/logic/regex/format) const typeValue = fieldValue.type[typeKey]; // 获取类型值(true/false) // 提取页码和值 const page = fieldValue.page; const value = fieldValue.value; // 如果是第一次遇到这个fieldKey,创建新条目 if (!fieldKeyMap[fieldKey]) { // 创建新的结构 fieldKeyMap[fieldKey] = { fieldKey, fieldValue: { type: {} } }; } // 将类型信息添加到type对象中,允许一个字段有多种规则类型的结果 fieldKeyMap[fieldKey].fieldValue.type[typeKey] = { res: typeValue, page, value }; }); // 将合并后的对象转换为数组 for (const key in fieldKeyMap) { mergedRules.push(fieldKeyMap[key]); } // 获取第一个有效页码 if (reviewPoint.id && !effectivePages[reviewPoint.id]) { // 遍历合并后的规则数组,查找第一个有效页码 for (const rule of mergedRules) { // 遍历字段类型对象 const typeEntries = Object.entries(rule.fieldValue.type); // 遍历每种类型规则 for (const [, typeValue] of typeEntries) { // 检查是否有有效页码 if (typeValue.page && Number(typeValue.page) > 0) { // 找到有效页码,设置状态并跳出循环 setEffectivePages(prev => ({ ...prev, [reviewPoint.id || '']: Number(typeValue.page) })); // 使用break跳出当前循环 break; } } // 如果已经找到有效页码,跳出外层循环 if (reviewPoint.id && effectivePages[reviewPoint.id]) { break; } } } // 返回合并后的规则数组 return mergedRules; }; /** * 渲染评查点内容与建议 * @param reviewPoint 评查点 * @returns 评查点内容与建议组件 */ const renderReviewPointContent = (reviewPoint: ReviewPoint) => { const mergedRules = filterOtherRule(reviewPoint); // console.log('mergedRules1-------', mergedRules); const handleManualReviewNotesChange = (reviewPointId: string, text: string) => { setManualReviewNotes(prev => ({ ...prev, [reviewPointId]: text })); }; // 如果当前评查点不处于编辑状态 TODO delete if (editingReviewPoint !== reviewPoint.id) { // 根据result和status决定渲染哪种样式 if (reviewPoint.result === true) { // 已通过的评查点只显示基本信息和人工审核注释 // 处理 result=true 且 postAction=manual 的情况 if (reviewPoint.postAction === 'manual') { const note = manualReviewNotes[reviewPoint.id] || ''; // 处理重新审核意见的输入 const handleNoteChange = (reviewPointId: string, text: string) => { setManualReviewNotes(prev => ({ ...prev, [reviewPointId]: text })); }; return ( <> {checkContentPage(reviewPoint).pageIndex === 0 && (

该评查点无法找到索引内容,无法自动定位到对应页面。

)}
{/* {reviewPoint.suggestion && (

{reviewPoint.suggestion}

)} */} {/* 评查点内容显示区域 */} {reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
{/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint, mergedRules)}
)} {/* 额外的文本输入框区域 */}
{reviewPoint.editAuditStatus === 0 ? (
) : ( )}
); } // 处理 result=true 且 postAction!=manual 的情况 return ( <> {checkContentPage(reviewPoint).pageIndex === 0 && (

该评查点无法找到索引内容,无法自动定位到对应页面。

)} {/* 评查点内容显示区域 */} {reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
{/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint, mergedRules)}
)} ); } return (
{/* 没有索引内容提示 */} {checkContentPage(reviewPoint).pageIndex === 0 && (

该评查点无法找到索引内容,无法自动定位到对应页面。

)} {/* 建议内容显示区域 */} {reviewPoint.suggestion && (

{reviewPoint.suggestion}

)} {/* 法律依据内容 */} {reviewPoint.legalBasis && (typeof reviewPoint.legalBasis === 'object') && ( (reviewPoint.legalBasis.name || reviewPoint.legalBasis.content || (reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0)) && (
法律依据
{reviewPoint.legalBasis.name && (

{reviewPoint.legalBasis.name}

)} {reviewPoint.legalBasis.content && (

条款内容:{reviewPoint.legalBasis.content}

)} {reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (

相关条款:

    {reviewPoint.legalBasis.articles.map((item, index) => (
  • {typeof item === 'string' ? item : typeof item === 'object' && item !== null ? (item.name ? `${item.name}: ${item.content || ''}` : item.content || JSON.stringify(item)) : String(item)}
  • ))}
)}
) )} {reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0 && ( <> {/* 内容显示区域 */}
{/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint, mergedRules)}
)} {/* 建议修改区域 */} {/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */} {(reviewPoint.postAction === 'manual') && (
{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"} {/* 符合规范 */}