/** * 评查点列表组件 * * 功能概述: * - 展示评查结果统计信息(总计、通过、警告、错误数量) * - 提供评查点过滤功能(按状态和搜索文本) * - 显示评查点详细信息(标题、状态、内容、建议修改等) * - 支持评查点操作(一键替换、人工审核等) * * 组件结构: * - 统计区域: 显示评查点数量统计 * - 搜索区域: 提供文本搜索功能 * - 评查点列表: 展示所有评查点 * - 评查点卡片: 展示单个评查点详情 * - 评查点头部: 显示标题和状态 * - 评查点内容: 显示当前内容和问题 * - 建议修改区域: 显示建议的修改内容 * - 操作按钮: 提供一键替换和人工审核功能 */ import { useState, useEffect } from 'react'; import { toastService } from '../ui/Toast'; // import { toastService } from '../ui/Toast'; /** * 评查点类型定义 * 用于展示单个评查结果 */ export interface ReviewPoint { id: string; documentId?: string; pointId?: string; editAuditStatusId?: string | number; editAuditStatus: number; 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; }; }>; }; } // 统计数据类型 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; } 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>({}); // 初始化建议文本 useEffect(() => { // 将所有评查点的建议文本存储到状态中 const suggestions: Record = {}; reviewPoints.forEach(point => { suggestions[point.id] = point.suggestion || ''; }); // setSuggestionTexts(suggestions); // 使用函数式更新,不再需要外部 manualReviewNotes 变量 setManualReviewNotes(prev => { const notes = { ...prev }; reviewPoints.forEach(point => { notes[point.id] = point.actionContent || ''; }); 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) => { // 更新评查点状态 // console.log('handleReviewAction-------', reviewPointResultId, editAuditStatusId, action, message); if(message.trim() === ''){ toastService.error('请输入审核意见'); return; } if (action === 'review') { // 重新审核时,不更新结果状态,只更新审核意见和审核状态 // console.log('重新审核-------', reviewPointResultId, editAuditStatusId || '', 'review', message); onStatusChange(reviewPointResultId, editAuditStatusId || '', 'review', message); // 找到当前评查点并更新其editAuditStatus为0,使其立即显示通过/不通过按钮 const updatedReviewPoint = reviewPoints.find(point => point.id === reviewPointResultId); if (updatedReviewPoint) { updatedReviewPoint.editAuditStatus = 0; } } else { // 通过/不通过时,更新结果状态和审核意见 // console.log('通过/不通过-------', reviewPointResultId, editAuditStatusId || '', action === 'approve' ? 'true' : 'false', message); onStatusChange(reviewPointResultId, editAuditStatusId || '', action === 'approve' ? 'true' : 'false', message); } // 将参数输出到控制台 console.log('评查点审核操作', { id: reviewPointResultId, editAuditStatusId: editAuditStatusId, action: action, content: message, status: action === 'approve' ? 'true' : (action === 'reject' ? 'false' : 'review') }); // 清除编辑状态 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 评查结果 * @returns 状态标签组件 */ const renderStatusBadge = (status: string, result?: boolean) => { // 优先根据result判断是否通过 if (result === true) { return ( 通过 ); } // 当result为false时,根据status决定显示警告还是错误 if (result === false) { if (status === 'warning') { return ( 警告 ); } else if (status === 'error') { return ( 不通过 ); } } // 兼容旧版逻辑,当没有result时,仍按status判断 switch (status) { case 'success': return ( 通过 ); case 'warning': return ( 警告 ); case 'error': return ( 不通过 ); case 'processing': return ( 处理中 ); default: return ( 警告 ); } }; /** * 渲染人工审核标记 * @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, result?: boolean) => { // 获取evaluationConfig中type为consistency的规则 评查点一致性规则组的规则 const consistencyRules = reviewPoint.evaluationConfig?.rules?.filter(rule => rule.type === 'consistency') || []; // 获取所有consistency规则中的fields const allConsistencyFields: string[][] = []; // 存储 sourceField 和 targetField 的映射关系 const pairsMapping: Record = {}; consistencyRules.forEach(rule => { if (rule.config?.fields) { allConsistencyFields.push(rule.config.fields); } else if (rule.config?.pairs) { // 处理pairs情况,提取sourceField和targetField const fields: string[] = []; rule.config.pairs.forEach(pair => { if (pair.sourceField) fields.push(pair.sourceField); if (pair.targetField) fields.push(pair.targetField); // 记录 sourceField 和 targetField 的映射关系 if (pair.sourceField && pair.targetField) { pairsMapping[pair.sourceField] = pair.targetField; } }); if (fields.length > 0) { allConsistencyFields.push(fields); } } }); // 对content进行排序 const contentEntries = Object.entries(reviewPoint.content); // 按照consistency规则分组 const groupedContent: Record> = { 'default': [] // 默认组,存放不属于任何consistency规则的项 }; // 为每个consistency规则创建分组 allConsistencyFields.forEach((fields, index) => { groupedContent[`consistency_${index}`] = []; }); // 将content按照规则分组 contentEntries.forEach(entry => { const [key, value] = entry; // 检查是否属于某个consistency规则 let assigned = false; allConsistencyFields.forEach((fields, index) => { if (fields.includes(key)) { groupedContent[`consistency_${index}`].push(entry); assigned = true; } }); // 如果不属于任何规则,放入默认组 if (!assigned) { groupedContent['default'].push(entry); } }); // 对每个分组内的条目按照 sourceField 和 targetField 的关系进行排序 Object.keys(groupedContent).forEach(groupKey => { if (groupKey !== 'default' && groupedContent[groupKey].length > 1) { // 创建一个新数组用于存储排序后的结果 const sortedEntries: Array<[string, { page?: number | string, value?: object }]> = []; const entriesMap = new Map(groupedContent[groupKey]); // 找出所有的源字段和目标字段对 const processed = new Set(); // 构建一个字段之间的连接关系图,用于处理嵌套关系 const fieldChains: Array = []; // 遍历所有映射关系,构建字段链 const buildFieldChains = () => { // 创建一个图结构,记录每个字段的后继字段 const graph: Record = {}; // 根据映射关系建立图 Object.entries(pairsMapping).forEach(([source, target]) => { if (!graph[source]) graph[source] = []; graph[source].push(target); // 确保目标字段在图中有一个空数组 if (!graph[target]) graph[target] = []; }); // 查找所有在当前分组中的字段 const fieldsInGroup = new Set(Array.from(entriesMap.keys())); // 找出入度为0的节点(即只作为sourceField而不是任何targetField的字段) const startNodes: string[] = []; for (const field of fieldsInGroup) { // 检查该字段是否作为targetField存在 const isTarget = Object.values(pairsMapping).includes(field); // 如果该字段是sourceField但不是targetField,则为起始节点 if (!isTarget && field in pairsMapping) { startNodes.push(field); } } // 从每个起始节点开始,使用DFS构建字段链 for (const startNode of startNodes) { const chain: string[] = []; const dfs = (node: string) => { // 如果该节点不在当前分组中,则跳过 if (!fieldsInGroup.has(node)) return; chain.push(node); // 遍历所有后继节点 for (const nextNode of graph[node] || []) { dfs(nextNode); } }; dfs(startNode); // 如果链不为空,则添加到字段链列表中 if (chain.length > 0) { fieldChains.push(chain); } } // 处理环形依赖或没有入度为0的节点的情况 // 找出未被处理的字段 const processedInChains = new Set(fieldChains.flat()); const remainingFields = Array.from(fieldsInGroup).filter(f => !processedInChains.has(f)); // 将剩余字段按照pairsMapping的关系组织成链 while (remainingFields.length > 0) { // !的作用是确保remainingFields.shift()不会返回undefined const field = remainingFields.shift()!; // 如果该字段已经在某个链中,则跳过 if (processedInChains.has(field)) continue; const chain: string[] = [field]; processedInChains.add(field); // 向后查找链 let currentField = field; while (currentField in pairsMapping) { const nextField = pairsMapping[currentField]; // 如果下一个字段不在分组中或已处理,则中断 if (!fieldsInGroup.has(nextField) || processedInChains.has(nextField)) break; chain.push(nextField); processedInChains.add(nextField); currentField = nextField; // 从剩余字段中移除 const index = remainingFields.indexOf(nextField); if (index !== -1) { remainingFields.splice(index, 1); } } if (chain.length > 0) { fieldChains.push(chain); } } }; buildFieldChains(); // 根据字段链构建排序后的结果 fieldChains.forEach(chain => { chain.forEach(field => { if (entriesMap.has(field) && !processed.has(field)) { sortedEntries.push([field, entriesMap.get(field)!]); processed.add(field); } }); }); // 添加剩余未处理的字段 for (const [key] of groupedContent[groupKey]) { if (!processed.has(key)) { sortedEntries.push([key, entriesMap.get(key)!]); } } // 用排序后的结果替换原数组 groupedContent[groupKey] = sortedEntries; } }); return ( <> {/* 渲染各个分组 */} {Object.entries(groupedContent).map(([groupKey, entries]) => { if (entries.length === 0) return null; // 非默认组添加边框 const isDefaultGroup = groupKey === 'default'; return (
{/* 分组标题,只有非默认组显示 */} {/* {!isDefaultGroup && (
规则组 {groupIndex}
)} */} {/* 渲染组内内容 */} {entries.map(([key, value], index) => !(result && value.value?.toString().trim() == '') && (
{ e.stopPropagation(); console.log(`单独点击${key}----`, reviewPoint); const valuePage = parseInt(value.page as string); const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); // 检查value中的page属性是否存在,优先取value中的page if (valuePage > 0) { console.log(`存在page且不为空:单独点击${key}---------->evaluated_results内的页码:`, valuePage); onReviewPointSelect(reviewPoint.id, valuePage); } else if(contentPage && contentPage > 0) { console.log(`存在page且为空:单独点击${key}---------->ocr_result内的页码:`, contentPage); onReviewPointSelect(reviewPoint.id, contentPage); }else { toastService.error(`无法找到"${key}"对应的索引内容`); console.log(`单独点击${key}--------没有对应页码`); } }} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); const valuePage = parseInt(value.page as string); const contentPage = parseInt(reviewPoint.contentPage?.[key] as string); // 检查value中的page属性是否存在,优先取value中的page if (valuePage > 0) { onReviewPointSelect(reviewPoint.id, valuePage); } else if(contentPage && contentPage > 0) { onReviewPointSelect(reviewPoint.id, contentPage); } else { toastService.error(`无法找到"${key}"对应的索引内容`); console.log(`单独点击${key}--------没有对应页码`); } } }} role="button" tabIndex={0} aria-label={`查看${key}内容详情`} onMouseLeave={(e) => { // 获取容器内的滚动区域元素 const scrollContainer = e.currentTarget.querySelector('.text-container'); if (scrollContainer) { // 在文本缩回之前重置滚动位置 scrollContainer.scrollTop = 0; } }} > {/*
*/}
{key} {parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : } {value.value?.toString().trim() ? '' : '缺失'}

{(value.value?.toString().trim() === '') ? "" : value.value?.toString() || ''}

))}
); })} ); }; /** * 渲染评查点内容与建议 * @param reviewPoint 评查点 * @returns 评查点内容与建议组件 */ const renderReviewPointContent = (reviewPoint: ReviewPoint) => { 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, true)}
)} {/* 额外的文本输入框区域 */}
{reviewPoint.editAuditStatus === 0 ? (
) : ( )}
); } // 处理 result=true 且 postAction!=manual 的情况 return ( <> {checkContentPage(reviewPoint).pageIndex === 0 && (

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

)} {/* 评查点内容显示区域 */} {reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
{/* 修改评查结果的结构之后,显示新的结构 */} {renderContent(reviewPoint, true)}
)} ); } 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,false)}
)} {/* 建议修改区域 */} {/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */} {(reviewPoint.postAction === 'manual') && (
{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"} {/* 符合规范 */}