2257 lines
88 KiB
TypeScript
2257 lines
88 KiB
TypeScript
/**
|
||
* 评查点列表组件
|
||
*
|
||
* 功能概述:
|
||
* - 展示评查结果统计信息(总计、通过、警告、错误数量)
|
||
* - 提供评查点过滤功能(按状态和搜索文本)
|
||
* - 显示评查点详细信息(标题、状态、内容、建议修改等)
|
||
* - 支持评查点操作(一键替换、人工审核等)
|
||
*
|
||
* 组件结构:
|
||
* - 统计区域: 显示评查点数量统计
|
||
* - 搜索区域: 提供文本搜索功能
|
||
* - 评查点列表: 展示所有评查点
|
||
* - 评查点卡片: 展示单个评查点详情
|
||
* - 评查点头部: 显示标题和状态
|
||
* - 评查点内容: 显示当前内容和问题
|
||
* - 建议修改区域: 显示建议的修改内容
|
||
* - 操作按钮: 提供一键替换和人工审核功能
|
||
*/
|
||
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<string, string> = {
|
||
'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<string, string> = {
|
||
'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;
|
||
pointName: string;
|
||
title: string;
|
||
groupName: string;
|
||
status: string;
|
||
content: Record<string, { page?: number | string, value?: object }>;
|
||
suggestion: string;
|
||
needsHumanReview?: boolean;
|
||
humanReviewNote?: string;
|
||
humanReviewBy?: string;
|
||
humanReviewTime?: string;
|
||
contentPage?: Record<string, number | string>;
|
||
position?: {
|
||
section: string;
|
||
index: number;
|
||
};
|
||
result?: boolean;
|
||
legalBasis?: {
|
||
name?: string;
|
||
content?: string;
|
||
articles?: Array<string | { name?: string; content?: string;[key: string]: unknown }>;
|
||
[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<string, unknown>;
|
||
}>;
|
||
};
|
||
}
|
||
|
||
// 统计数据类型
|
||
interface Statistics {
|
||
total: number;
|
||
success: number;
|
||
warning: number;
|
||
error: number;
|
||
score: number;
|
||
}
|
||
|
||
// 统一规则的类型
|
||
// interface pointRule {
|
||
// id: string;
|
||
// type: string;
|
||
// config: Record<string, unknown>;
|
||
// }
|
||
|
||
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 } // 提示框在屏幕上的位置
|
||
};
|
||
|
||
/**
|
||
* 提示框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(
|
||
<div
|
||
className="fixed bg-white shadow-lg rounded-md p-1 border border-gray-200 z-[9999]"
|
||
style={{
|
||
top: `${tooltip.position.top}px`,
|
||
left: `${tooltip.position.left}px`,
|
||
transform: 'translate(-100%, -50%)' // 调整位置,使提示框在指针左侧居中显示
|
||
}}
|
||
>
|
||
{tooltip.content}
|
||
{/* 添加小三角形指向提示框指向的元素 */}
|
||
<div className="absolute top-1/2 right-0 transform translate-x-1/2 -translate-y-1/2 rotate-45 w-2 h-2 bg-white border-t border-r border-gray-200"></div>
|
||
</div>,
|
||
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
|
||
};
|
||
// 触发自定义事件,通知TooltipPortal组件更新状态
|
||
window.dispatchEvent(new Event('tooltip-update'));
|
||
}
|
||
|
||
/**
|
||
* 隐藏提示框的辅助函数
|
||
*/
|
||
function hideTooltip(): void {
|
||
// 设置为不显示状态
|
||
activeTooltip.show = 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 textRef = useRef<HTMLDivElement>(null);
|
||
|
||
useEffect(() => {
|
||
const checkTextOverflow = () => {
|
||
const element = textRef.current;
|
||
if (element) {
|
||
setShowTooltip(element.scrollHeight > element.clientHeight);
|
||
}
|
||
};
|
||
|
||
setTimeout(checkTextOverflow, 0);
|
||
window.addEventListener('resize', checkTextOverflow);
|
||
return () => {
|
||
window.removeEventListener('resize', checkTextOverflow);
|
||
};
|
||
}, [content]);
|
||
|
||
// 解析表格数据
|
||
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 (
|
||
<div className="overflow-auto max-h-[400px]">
|
||
<table className="min-w-full border-collapse border border-gray-300">
|
||
{hasHeader && (
|
||
<thead>
|
||
<tr>
|
||
{tableData[0].map((cell, cellIndex) => (
|
||
<th
|
||
key={`header-${cellIndex}`}
|
||
className="px-2 py-1 border border-gray-300 bg-gray-100 font-medium text-xs text-left"
|
||
>
|
||
{cell || ' '}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
)}
|
||
<tbody>
|
||
{tableData.slice(1).map((row, rowIndex) => (
|
||
<tr key={`row-${rowIndex}`}>
|
||
{row.map((cell, cellIndex) => (
|
||
<td
|
||
key={`cell-${rowIndex}-${cellIndex}`}
|
||
className="px-2 py-1 border border-gray-300 text-xs text-left"
|
||
>
|
||
{cell || ' '}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
);
|
||
} catch (error) {
|
||
console.error('表格渲染错误:', error);
|
||
return <div className="text-red-500">表格渲染错误</div>;
|
||
}
|
||
};
|
||
|
||
// 检测内容是否像表格
|
||
const isTableLike = content.includes('\t') && content.includes('\n');
|
||
|
||
return (
|
||
<div className="text-xs p-1 rounded cursor-text w-full text-left">
|
||
{showTooltip ? (
|
||
<Tooltip
|
||
content={isTableLike ? renderReactTable(content) : content}
|
||
placement="top"
|
||
theme="light"
|
||
trigger="hover"
|
||
showArrow={true}
|
||
className="tooltip-custom-offset"
|
||
>
|
||
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
|
||
{content}
|
||
</div>
|
||
</Tooltip>
|
||
) : (
|
||
<div className="text-gray-800 break-all overflow-hidden line-clamp-2" ref={textRef}>
|
||
{content}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export function ReviewPointsList({
|
||
reviewPoints,
|
||
statistics,
|
||
activeReviewPointResultId,
|
||
onReviewPointSelect,
|
||
onStatusChange
|
||
}: ReviewPointsListProps) {
|
||
// 状态管理
|
||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||
|
||
// const [suggestionTexts, setSuggestionTexts] = useState<Record<string, string>>({}); // 存储每个评查点的建议文本
|
||
|
||
// 添加重新审核意见的状态/ 用户输入的修改内容 / 用户提前写好的修改内容
|
||
const [manualReviewNotes, setManualReviewNotes] = useState<Record<string, string>>({});
|
||
|
||
// 存放属于有无判断,格式判断,逻辑判断,正则表达式这一类的评查点规则设置
|
||
// const [otherRule, setOtherRule] = useState<Record<string, unknown>[]>([]);
|
||
|
||
// 初始化建议文本
|
||
useEffect(() => {
|
||
// 将所有评查点的建议文本存储到状态中
|
||
const suggestions: Record<string, string> = {};
|
||
|
||
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 (
|
||
<div className="review-statistics bg-white border-b border-gray-100 py-3 px-4">
|
||
<div className="flex justify-between items-center">
|
||
{/* 总计数量 */}
|
||
<div className="flex items-center">
|
||
<button
|
||
className={`px-3 h-7 bg-gray-100 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === null && searchText === '' ? 'ring-2 ring-gray-400' : ''}`}
|
||
onClick={() => {
|
||
setStatusFilter(null);
|
||
setSearchText('');
|
||
}}
|
||
aria-label="显示所有评查点"
|
||
type="button"
|
||
>
|
||
<span className="text-sm font-semibold text-gray-600">{totalToShow}</span>
|
||
<span className="text-xs text-gray-500 ml-1">总计</span>
|
||
</button>
|
||
</div>
|
||
<div className="h-8 border-r border-gray-200"></div>
|
||
{/* 通过数量 */}
|
||
<div className="flex items-center">
|
||
<button
|
||
className={`px-3 h-7 bg-green-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'success' ? 'ring-2 ring-green-500' : ''}`}
|
||
onClick={() => setStatusFilter(statusFilter === 'success' ? null : 'success')}
|
||
aria-label={`过滤通过项 ${statusFilter === 'success' ? '(已选中)' : ''}`}
|
||
type="button"
|
||
>
|
||
<span className="text-sm font-semibold text-success">{successToShow}</span>
|
||
<span className="text-xs text-gray-500 ml-2">通过</span>
|
||
</button>
|
||
</div>
|
||
<div className="h-8 border-r border-gray-200"></div>
|
||
{/* 警告数量 */}
|
||
<div className="flex items-center">
|
||
<button
|
||
className={`px-3 h-7 bg-yellow-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'warning' ? 'ring-2 ring-yellow-500' : ''}`}
|
||
onClick={() => setStatusFilter(statusFilter === 'warning' ? null : 'warning')}
|
||
aria-label={`过滤警告项 ${statusFilter === 'warning' ? '(已选中)' : ''}`}
|
||
type="button"
|
||
>
|
||
<span className="text-sm font-semibold text-warning">{warningToShow}</span>
|
||
<span className="text-xs text-gray-500 ml-2">警告</span>
|
||
</button>
|
||
</div>
|
||
<div className="h-8 border-r border-gray-200"></div>
|
||
{/* 错误数量 */}
|
||
<div className="flex items-center">
|
||
<button
|
||
className={`px-3 h-7 bg-red-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'error' ? 'ring-2 ring-red-500' : ''}`}
|
||
onClick={() => setStatusFilter(statusFilter === 'error' ? null : 'error')}
|
||
aria-label={`过滤错误项 ${statusFilter === 'error' ? '(已选中)' : ''}`}
|
||
type="button"
|
||
>
|
||
<span className="text-sm font-semibold text-error">{errorToShow}</span>
|
||
<span className="text-xs text-gray-500 ml-2">错误</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 渲染搜索框
|
||
* 用于按文本搜索评查点
|
||
*/
|
||
const renderSearchBar = () => {
|
||
return (
|
||
<div className="py-2 px-3 border-b border-gray-100">
|
||
<div className="relative">
|
||
<input
|
||
type="text"
|
||
className="w-full border border-gray-200 rounded-md pl-8 pr-2 py-1 text-xs
|
||
focus:outline-none focus:ring-1 focus:ring-green-800"
|
||
placeholder="搜索评查点..."
|
||
value={searchText}
|
||
onChange={(e) => setSearchText(e.target.value)}
|
||
/>
|
||
<i className="ri-search-line absolute left-2 top-0.5 text-gray-400"></i>
|
||
{searchText && (
|
||
<button
|
||
className="absolute right-2 top-0.5 text-gray-400 hover:text-gray-600"
|
||
onClick={() => setSearchText('')}
|
||
>
|
||
<i className="ri-close-line"></i>
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 渲染评查点状态标签
|
||
* @param status 状态文本
|
||
* @param result 评查结果
|
||
* @param title 标签提示内容
|
||
* @returns 状态标签组件
|
||
*/
|
||
const renderStatusBadge = (status: string, result?: boolean, title?: string) => {
|
||
// 优先根据result判断是否通过
|
||
if (result === true) {
|
||
return (
|
||
<Tooltip
|
||
content={
|
||
<div className="p-1 gap-1 flex flex-col max-w-xs">
|
||
{title && <div className="text-xs text-gray-600">{title}</div>}
|
||
</div>
|
||
}
|
||
placement="top"
|
||
theme="light"
|
||
trigger="hover"
|
||
showArrow={true}
|
||
className="tooltip-custom-offset tooltip-top"
|
||
>
|
||
<span className="status-badge status-success text-xs m-1">
|
||
<i className="ri-checkbox-circle-line mr-1"></i>通过
|
||
</span>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
|
||
// 当result为false时,根据status决定显示警告还是错误
|
||
if (result === false) {
|
||
if (status === 'warning' || status === 'info') {
|
||
return (
|
||
<Tooltip
|
||
content={
|
||
<div className="p-1 flex flex-col gap-1 max-w-xs">
|
||
{title && <div className="text-xs text-gray-600">{title}</div>}
|
||
</div>
|
||
}
|
||
placement="top"
|
||
theme="light"
|
||
trigger="hover"
|
||
showArrow={true}
|
||
className="tooltip-custom-offset tooltip-top"
|
||
>
|
||
<span className="status-badge status-warning text-xs m-1">
|
||
<i className="ri-alert-line mr-1"></i>警告
|
||
</span>
|
||
</Tooltip>
|
||
);
|
||
} else if (status === 'error') {
|
||
return (
|
||
<Tooltip
|
||
content={
|
||
<div className="p-1 flex flex-col gap-1 max-w-xs">
|
||
{title && <div className="text-xs text-gray-600">{title}</div>}
|
||
</div>
|
||
}
|
||
placement="top"
|
||
theme="light"
|
||
trigger="hover"
|
||
showArrow={true}
|
||
className="tooltip-custom-offset tooltip-top"
|
||
>
|
||
<span className="status-badge status-error text-xs m-1">
|
||
<i className="ri-close-circle-line mr-1"></i>不通过
|
||
</span>
|
||
</Tooltip>
|
||
);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 渲染人工审核标记
|
||
* @param reviewPoint 评查点
|
||
* @returns 人工审核标记组件
|
||
*/
|
||
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
|
||
if (reviewPoint.postAction === 'manual') {
|
||
return (
|
||
<span className=" bg-[#f9f0ff] text-[#722ed1] text-xs rounded-sm p-0.5 my-auto">
|
||
<i className="ri-user-line mr-1"></i>需人工
|
||
</span>
|
||
);
|
||
}
|
||
return null;
|
||
};
|
||
|
||
/**
|
||
* 渲染人工审核注释
|
||
* @param reviewPoint 评查点
|
||
* @returns 人工审核注释组件
|
||
*/
|
||
// const renderHumanReviewNote = (reviewPoint: ReviewPoint) => {
|
||
// // 目前needsHumanReview和humanReviewNote都为空,所以不显示
|
||
// if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) {
|
||
// return (
|
||
// <div className="human-review-note">
|
||
// <i className="ri-information-line mr-1"></i> {reviewPoint.humanReviewNote}
|
||
// {reviewPoint.humanReviewBy && reviewPoint.humanReviewTime && (
|
||
// <div className="text-right text-xs text-gray-500 mt-1">
|
||
// 审核人:{reviewPoint.humanReviewBy} | 时间:{reviewPoint.humanReviewTime}
|
||
// </div>
|
||
// )}
|
||
// </div>
|
||
// );
|
||
// }
|
||
// return null;
|
||
// };
|
||
|
||
/**
|
||
* 渲染评查点主要内容
|
||
* @param reviewPoint 评查点
|
||
* @returns 评查点主要内容组件
|
||
*/
|
||
const renderContent = (reviewPoint: ReviewPoint, otherRules: Array<Record<string, unknown>>) => {
|
||
return (
|
||
<>
|
||
{/* 渲染其他规则分组 */}
|
||
{otherRules.map((rule, index) => {
|
||
return <div key={`other-rule-${index}`}>{renderOtherRule(rule, reviewPoint)}</div>;
|
||
})}
|
||
|
||
{/* <div key="line" className=" bg-gray-50 rounded border border-gray-200 text-xs mb-3"></div> */}
|
||
{/* 渲染各个一致性的规则分组 */}
|
||
{reviewPoint.evaluatedPointResultsLog?.rules?.map((rule, index) => {
|
||
// console.log('rule-------', rule);
|
||
// if (rule.type === 'consistency' && rule.res === true) {
|
||
if (rule.type === 'consistency') {
|
||
// console.log('rule-------', rule);
|
||
return <div key={`rule-${index}`}>
|
||
<div key="line" className=" bg-gray-50 rounded border border-gray-200 text-xs mb-3"></div>
|
||
{renderConsistencyRule(rule, reviewPoint)}
|
||
</div>;
|
||
}
|
||
|
||
if (rule.type === 'ai') {
|
||
return <div key={`rule-${index}`}>
|
||
<div key="line" className=" bg-gray-50 rounded border border-gray-200 text-xs mb-3"></div>
|
||
{renderModelRule(rule, reviewPoint)}
|
||
</div>;
|
||
}
|
||
|
||
|
||
// return (
|
||
// <>
|
||
// {/* 渲染组内内容 */}
|
||
// {entries.map(([key, value], index) =>
|
||
// !(result && value.value?.toString().trim() == '') && (
|
||
// <div
|
||
// key={`${groupKey}_${index}`}
|
||
// className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 cursor-pointer hover:bg-gray-100 transition-colors duration-200 rounded p-1 group"
|
||
// onClick={(e) => {
|
||
// 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;
|
||
// }
|
||
// }}
|
||
// >
|
||
// {/* <div className="flex justify-between items-center mb-1"> */}
|
||
// <div className="flex items-center mb-1">
|
||
// <span className="text-xs pr-5">
|
||
// {key}
|
||
// </span>
|
||
// <span className={`flex-shrink-0 text-xs w-15 ${value.value?.toString().trim() ? 'text-error' : 'text-warning'}`}>
|
||
// {parseInt(value.page as string)>0 || parseInt(reviewPoint.contentPage?.[key] as string)>0 ? '' : <i title="无法找到索引内容" className="ri-alert-line text-red-500 mr-2"></i>}
|
||
// {value.value?.toString().trim() ? '' : '缺失'}
|
||
// </span>
|
||
// </div>
|
||
|
||
// <div className="relative text-container max-h-96 group-hover:overflow-auto overflow-hidden">
|
||
// <p
|
||
// className="text-xs text-left select-text block overflow-hidden !line-clamp-2
|
||
// group-hover:!line-clamp-none group-hover:bg-white group-hover:shadow-md
|
||
// group-hover:z-10 group-hover:relative px-1 rounded transition-all duration-300 ease-in-out cursor-text"
|
||
// // title={value.value?.toString() || ''}
|
||
// // style={{ userSelect: 'all' }}
|
||
// >
|
||
// {(value.value?.toString().trim() === '')
|
||
// ? ""
|
||
// : value.value?.toString() || ''}
|
||
// </p>
|
||
// </div>
|
||
// </div>
|
||
// ))}
|
||
// </>
|
||
// );
|
||
})}
|
||
|
||
</>
|
||
);
|
||
};
|
||
|
||
|
||
/**
|
||
* 渲染评查点一致性的规则的样式
|
||
* @param singleReviewPoint 一个评查点的一致性规则对象
|
||
* @param reviewPoint 评查点
|
||
* @returns 评查点一致性的规则的样式
|
||
*/
|
||
const renderConsistencyRule = (singleReviewPoint: Record<string, unknown>,reviewPoint: ReviewPoint) => {
|
||
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<string, { page: number; value: string }>;
|
||
targetField: Record<string, { page: number; value: string }>;
|
||
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;
|
||
|
||
// 查找链条关系
|
||
const findChains = () => {
|
||
type ChainItem = {
|
||
field: string;
|
||
data: {
|
||
key: string;
|
||
page: number;
|
||
value: string
|
||
};
|
||
res: boolean;
|
||
compareMethod?: string;
|
||
};
|
||
|
||
const chains: Array<Array<ChainItem>> = [];
|
||
const visited = new Set<string>();
|
||
|
||
// 构建字段映射关系
|
||
const fieldMap = new Map<string, Array<{
|
||
targetField: string;
|
||
data: {
|
||
source: { key: string; page: number; value: string };
|
||
target: { key: string; page: number; value: string };
|
||
};
|
||
res: boolean;
|
||
compareMethod?: string;
|
||
}>>();
|
||
|
||
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<string>();
|
||
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<ChainItem> = [];
|
||
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<Array<ChainItem>> = [];
|
||
|
||
// 从后往前遍历,检查每个相邻元素之间的连接
|
||
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<ChainItem> = [
|
||
{
|
||
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 (
|
||
<div className="mt-3">
|
||
<div className="comparison-group">
|
||
{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 (
|
||
<div
|
||
key={`chain_${chainIndex}`}
|
||
className={`${itemClassName} border border-gray
|
||
rounded-md overflow-hidden mb-2 ${res ? 'bg-[rgba(246,255,237,1)]' : 'bg-[rgba(255,251,230,1)]'}
|
||
hover:shadow-[0_0_10px_rgba(0,0,0,0.1)] cursor-pointer ${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'}`}
|
||
onClick={(e) => {
|
||
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}
|
||
>
|
||
<div className="comparison-values flex w-full">
|
||
<div className="value-box p-2 pb-1 flex-1">
|
||
<div className="value-source text-xs text-gray-500 mb-1">
|
||
{chain.map((item, idx) => (
|
||
<span key={idx} className="inline-block">
|
||
{item.field}
|
||
{idx < chain.length - 1 && (
|
||
<i className="ri-arrow-left-s-line text-xs ml-1 text-primary">
|
||
{typeof chain[idx+1].compareMethod === 'object'
|
||
? ''
|
||
: getCompareMethodText(chain[idx+1].compareMethod)}
|
||
<i className="ri-arrow-right-s-line mr-1 text-xs text-primary"></i>
|
||
</i>
|
||
)}
|
||
</span>
|
||
))}
|
||
</div>
|
||
<div className="flex flex-col">
|
||
{chain.map((item, idx) => (
|
||
<button
|
||
key={`item_${idx}`}
|
||
className="value-content p-1 cursor-text text-xs border-b border-dashed border-gray-200 last:border-b-0 text-left w-full rounded transition-colors"
|
||
onClick={(e) => {
|
||
if (item.data.page) {
|
||
// console.log('currentitem-------', reviewPoint);
|
||
// 假设onReviewPointSelect在作用域内可用
|
||
const reviewPointId = reviewPoint.id as string;
|
||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||
e.stopPropagation();
|
||
onReviewPointSelect(reviewPointId, Number(item.data.page));
|
||
}
|
||
}else{
|
||
toastService.error(`没有找到${item.field}对应的索引内容`);
|
||
}
|
||
}}
|
||
aria-label={`查看${item.field}内容详情`}
|
||
>
|
||
<div className="flex justify-between w-full">
|
||
{/* <span className="font-medium">{item.field}:</span> */}
|
||
{/* <span className="w-full overflow-hidden line-clamp-2 hover:line-clamp-none
|
||
hover: z-10 hover:overflow-auto rounded max-h-96">{item.data.value?.toString() || ''}
|
||
{!item.data.page && !item.data.value && (
|
||
<i className="ri-information-line text-red-500 text-xs"></i>
|
||
)}
|
||
</span> */}
|
||
<ReactTableTooltip content={item.data.value?.toString() || ''} />
|
||
{!item.data.page && !item.data.value && (
|
||
<i className="ri-information-line text-red-500 text-xs" title="没有找到对应的文书内容"></i>
|
||
)}
|
||
</div>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<div className="status-indicator w-8 flex items-center justify-center group relative" >
|
||
{res ? (
|
||
<i className="ri-check-line text-success text-base" ></i>
|
||
) : (
|
||
<i className="ri-alert-line text-warning text-base" ></i>
|
||
)}
|
||
{/* 使用鼠标事件处理悬停提示 */}
|
||
<div
|
||
className="w-full h-full absolute top-0 left-0"
|
||
onMouseEnter={(e) => {
|
||
// 获取元素位置信息
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
// 创建提示框内容
|
||
const content = (
|
||
<div className="flex flex-row gap-2 overflow-x-auto">
|
||
{chain.map((item, idx) =>
|
||
idx >= 1 ? (
|
||
<div key={idx} className={`rounded-md flex flex-row items-center`}>
|
||
<div className="text-xs text-gray-600 whitespace-nowrap pl-2">
|
||
{typeof item.compareMethod === 'object'
|
||
? ''
|
||
: `${getCompareMethodText(item.compareMethod)}:`}
|
||
</div>
|
||
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
|
||
{res ? '通过' : '不通过'}
|
||
</div>
|
||
</div>
|
||
) : null
|
||
)}
|
||
</div>
|
||
);
|
||
// 显示提示框
|
||
showTooltip(content, { top: rect.top + rect.height/2, left: rect.left });
|
||
}}
|
||
onMouseLeave={hideTooltip}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 如果是标准的成对比较(2个元素)
|
||
return (
|
||
<div
|
||
key={`pair_${chainIndex}`}
|
||
className={`${itemClassName} border border-gray rounded-md overflow-hidden mb-2 ${res ? 'bg-[rgba(246,255,237,1)]' : 'bg-[rgba(255,251,230,1)]'} flex`}
|
||
>
|
||
<div className="field-label bg-gray-50 p-2 text-xs font-medium text-gray-500 border-r border-gray-200 min-w-[70px] items-center hidden">
|
||
{chain[0].field.split('-').pop()}
|
||
</div>
|
||
<div className="comparison-values flex flex-1">
|
||
<button
|
||
className={`value-box hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
|
||
flex-1 p-2 border-r-2 ${res ? 'border-green-200' : 'border-yellow-200'} text-left
|
||
${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors flex flex-col`}
|
||
onClick={(e) => {
|
||
if (chain[0].data.page) {
|
||
const reviewPointId = reviewPoint.id as string;
|
||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||
e.stopPropagation();
|
||
onReviewPointSelect(reviewPointId, chain[0].data.page);
|
||
}
|
||
}else{
|
||
toastService.error(`没有找到${chain[0].field}对应的索引内容`);
|
||
}
|
||
}}
|
||
aria-label={`查看${chain[0].field}内容详情`}
|
||
>
|
||
<div className="value-source text-xs text-gray-500 mb-1">{chain[0].field}
|
||
{!chain[0].data.page && !chain[0].data.value && (
|
||
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
||
)}
|
||
</div>
|
||
<ReactTableTooltip content={chain[0].data.value?.toString() || ''} />
|
||
</button>
|
||
<button
|
||
className={`value-box flex flex-col flex-1 p-2 text-left ${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'} transition-colors hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]`}
|
||
onClick={(e) => {
|
||
if (chain[1].data.page) {
|
||
const reviewPointId = reviewPoint.id as string;
|
||
if (reviewPointId && typeof onReviewPointSelect === 'function') {
|
||
e.stopPropagation();
|
||
onReviewPointSelect(reviewPointId, chain[1].data.page);
|
||
}
|
||
}else{
|
||
toastService.error(`没有找到${chain[1].field}对应的索引内容`);
|
||
}
|
||
}}
|
||
aria-label={`查看${chain[1].field}内容详情`}
|
||
>
|
||
<div className="value-source text-xs text-gray-500 mb-1">{chain[1].field}
|
||
{!chain[1].data.page && !chain[1].data.value && (
|
||
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
||
)}
|
||
</div>
|
||
<ReactTableTooltip content={chain[1].data.value?.toString() || ''} />
|
||
</button>
|
||
</div>
|
||
<div className="status-indicator tooltip w-8 flex items-center justify-center group relative">
|
||
{res ? (
|
||
<i className="ri-check-line text-success text-base"></i>
|
||
) : (
|
||
<i className="ri-alert-line text-warning text-base"></i>
|
||
)}
|
||
{/* 使用鼠标事件处理悬停提示 */}
|
||
<div
|
||
className="w-full h-full absolute top-0 left-0"
|
||
onMouseEnter={(e) => {
|
||
// 获取元素位置信息
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
// 创建提示框内容
|
||
const content = (
|
||
<div className="flex flex-row gap-2 overflow-x-auto">
|
||
<div key={chain[1].compareMethod} className={`rounded-md flex flex-row items-center`}>
|
||
<div className="text-xs text-gray-600 whitespace-nowrap pl-1">
|
||
{typeof chain[1].compareMethod === 'object'
|
||
? ''
|
||
: `${getCompareMethodText(chain[1].compareMethod)}:`}
|
||
</div>
|
||
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
|
||
{res ? '通过' : '不通过'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
// 显示提示框,稍微向下偏移,便于鼠标移动到tooltip上
|
||
showTooltip(content, {
|
||
top: rect.top + rect.height/2,
|
||
left: rect.left
|
||
});
|
||
}}
|
||
onMouseLeave={hideTooltip}
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
})}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
|
||
/**
|
||
* 渲染评查点有无判断,格式判断,逻辑判断,正则表达式的规则的样式
|
||
* @param otherRule 评查点规则数据
|
||
* @param reviewPoint 关联的评查点
|
||
* @returns 评查点有无判断,格式判断,逻辑判断,正则表达式的规则的样式
|
||
*/
|
||
const renderOtherRule = (otherRule: Record<string, unknown>, reviewPoint: ReviewPoint) => {
|
||
const fieldKey = otherRule.fieldKey as string;
|
||
const fieldValue = otherRule.fieldValue as {
|
||
type: Record<string, {
|
||
res: boolean;
|
||
page?: number | string;
|
||
value?: string;
|
||
}>;
|
||
};
|
||
|
||
// 获取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 (
|
||
<div className="flex flex-row gap-2 overflow-x-auto max-h-[300px]">
|
||
{Object.entries(fieldValue?.type || {}).map(([typeKey, typeValue]) => (
|
||
<div key={typeKey} className={`rounded-md flex flex-row items-center`}>
|
||
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">{getRuleTypeText(typeKey)}:</div>
|
||
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
|
||
{typeValue.res ? '通过' : '不通过'}
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
/**
|
||
* 处理鼠标悬停事件
|
||
* 当鼠标悬停在状态指示器上时,计算提示框应该显示的位置并显示提示框
|
||
* @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 (
|
||
<button
|
||
className={`border border-gray rounded-md overflow-hidden mb-2 ${overallResult ? 'bg-[rgba(246,255,237,1)]' : 'bg-[rgba(255,251,230,1)]'} flex w-full text-left
|
||
hover:shadow-[0_0_10px_rgba(0,0,0,0.1)] ${overallResult ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'}`}
|
||
onClick={(e) => {
|
||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||
e.stopPropagation();
|
||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
|
||
}else{
|
||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||
}
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
|
||
onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
|
||
}else{
|
||
toastService.error(`没有找到${fieldKey}对应的索引内容`);
|
||
}
|
||
}
|
||
}}
|
||
type="button"
|
||
aria-label={`查看${fieldKey}内容详情`}
|
||
>
|
||
<div className="p-1 flex-1">
|
||
{/* 字段名称 */}
|
||
<div className="text-xs text-gray-500 mb-1">
|
||
{fieldKey}
|
||
{!mainTypeValue.page && !mainTypeValue.value && (
|
||
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
||
)}
|
||
{/* 缺失显示 */}
|
||
{mainTypeValue.res === false && !mainTypeValue.value && (
|
||
<span className="ml-2 text-xs text-yellow-500">
|
||
缺失
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 主要值显示 */}
|
||
{mainTypeValue.value && (
|
||
<ReactTableTooltip content={mainTypeValue.value} />
|
||
)}
|
||
</div>
|
||
{/* 状态指示器 - 绑定鼠标事件用于显示/隐藏提示框 */}
|
||
<div
|
||
className="w-8 flex items-center justify-center rounded-r-md"
|
||
onMouseEnter={handleMouseEnter} // 鼠标进入时显示提示框
|
||
onMouseLeave={hideTooltip} // 鼠标离开时隐藏提示框
|
||
>
|
||
{overallResult ? (
|
||
<i className="ri-check-line text-success text-base hover:text-green-800" ></i>
|
||
) : (
|
||
<i className="ri-alert-line text-warning text-base hover:text-yellow-800" ></i>
|
||
)}
|
||
</div>
|
||
</button>
|
||
);
|
||
};
|
||
|
||
|
||
|
||
/**
|
||
* 渲染评查点大模型判断的规则的样式
|
||
*
|
||
* 该函数处理AI模型评估的结果展示,包括:
|
||
* 1. 从规则配置中提取字段和评估结果
|
||
* 2. 为每个字段创建可点击的UI元素,显示内容和评估状态
|
||
* 3. 展示模型的评估消息
|
||
* 4. 处理字段点击导航到相应页面的逻辑
|
||
*
|
||
* @param aiRule 评查点大模型判断的规则对象
|
||
* @param reviewPoint 关联的评查点对象
|
||
* @returns React组件,用于显示AI模型评估结果
|
||
*/
|
||
const renderModelRule = (aiRule: Record<string, unknown>, reviewPoint: ReviewPoint) => {
|
||
// 从aiRule中提取配置信息
|
||
const config = aiRule.config as {
|
||
model?: string;
|
||
fields?: Record<string, {
|
||
page: number | string;
|
||
value: string;
|
||
}>;
|
||
message?: string;
|
||
res?: boolean;
|
||
} | undefined;
|
||
|
||
// 如果配置不存在,不渲染任何内容
|
||
if (!config) return null;
|
||
|
||
// 创建一个数组来存储需要渲染的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(
|
||
<button
|
||
key={`field-${index}`}
|
||
className={`border border-gray w-full
|
||
rounded-md overflow-hidden mb-2 ${res ? 'bg-[rgba(246,255,237,1)]' : 'bg-[rgba(255,251,230,1)]'} flex
|
||
hover:shadow-[0_0_10px_rgba(0,0,0,0.1)] ${res ? 'hover:bg-[rgba(0,128,0,0.1)]' : 'hover:bg-[rgba(255,255,0,0.1)]'}`}
|
||
onClick={(e) => {
|
||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||
e.stopPropagation();
|
||
onReviewPointSelect(reviewPoint.id, Number(value.page));
|
||
}else{
|
||
toastService.error(`没有找到${key}对应的索引内容`);
|
||
}
|
||
}}
|
||
onKeyDown={(e) => {
|
||
// 键盘导航支持
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
e.preventDefault();
|
||
if (value.page && typeof onReviewPointSelect === 'function') {
|
||
e.preventDefault();
|
||
onReviewPointSelect(reviewPoint.id, Number(value.page));
|
||
}else{
|
||
toastService.error(`没有找到${key}对应的索引内容`);
|
||
}
|
||
}
|
||
}}
|
||
type="button"
|
||
aria-label={`查看${key}内容详情`}
|
||
>
|
||
<div className="p-1 flex-1 text-left">
|
||
{/* 字段名称 */}
|
||
<div className="text-xs text-left text-gray-500 mb-1">
|
||
{key}
|
||
{/* 没有抽取到目录内容,page和value都为空 */}
|
||
{!value.page && !value.value && (
|
||
<i className="ri-information-line text-red-500 text-xs ml-1" title="没有找到对应的文书内容"></i>
|
||
)}
|
||
{/* 缺失显示 */}
|
||
{!res && (
|
||
<span className="ml-2 text-xs text-yellow-500">
|
||
缺失
|
||
</span>
|
||
)}
|
||
</div>
|
||
|
||
{/* 主要值显示 */}
|
||
{res && (
|
||
<ReactTableTooltip content={value.value} />
|
||
)}
|
||
</div>
|
||
<div className={`w-8 flex items-center justify-center rounded-r-md group relative`}>
|
||
{res ? (
|
||
<i className="ri-check-line text-success text-base hover:text-green-800" ></i>
|
||
) : (
|
||
<i className="ri-alert-line text-warning text-base hover:text-yellow-800" ></i>
|
||
)}
|
||
{/* 使用鼠标事件处理悬停提示 */}
|
||
<div
|
||
className="w-full h-full absolute top-0 left-0"
|
||
onMouseEnter={(e) => {
|
||
// 获取元素位置信息
|
||
const rect = e.currentTarget.getBoundingClientRect();
|
||
// 创建提示框内容
|
||
const content = (
|
||
<div className="flex flex-row gap-2 overflow-x-auto">
|
||
<div className={`rounded-md flex flex-row items-center`}>
|
||
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">大模型判断:</div>
|
||
<div className={`p-1 text-xs rounded-full min-w-[50px] text-center`}>
|
||
{res ? '通过' : '不通过'}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
// 显示提示框,稍微向下偏移,便于鼠标移动到tooltip上
|
||
showTooltip(content, {
|
||
top: rect.top + rect.height/2,
|
||
left: rect.left
|
||
});
|
||
}}
|
||
onMouseLeave={hideTooltip}
|
||
/>
|
||
</div>
|
||
</button>
|
||
);
|
||
});
|
||
}
|
||
|
||
// 渲染AI模型返回的评估消息
|
||
if (config.message) {
|
||
// 检查message是否为对象,如果是则转换为字符串
|
||
const messageContent = typeof config.message === 'object'
|
||
? JSON.stringify(config.message)
|
||
: String(config.message);
|
||
|
||
// 添加模型评估消息区域,使用蓝色背景突出显示
|
||
fieldElements.push(
|
||
<div key="message" className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||
<div className="flex flex-row items-center">
|
||
<i className="ri-robot-2-line text-blue-500 mr-2 "></i>
|
||
<p className="text-xs text-gray-600 select-text">{messageContent}</p>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 返回包含所有元素的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<string, boolean>;
|
||
}
|
||
|
||
const allRule: Array<{
|
||
fieldKey: string;
|
||
fieldValue: RuleFieldValue;
|
||
}> = [];
|
||
|
||
for (const rule of reviewPoint.evaluatedPointResultsLog?.rules || []) {
|
||
// 处理"有无判断"类型的规则
|
||
if (rule.type === 'exists') {
|
||
// 使用类型断言获取config对象的具体结构
|
||
const config = rule.config as {
|
||
res: boolean;
|
||
fields: Record<string, { page: number; value: string }>;
|
||
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<string, { page: string | number; value: string }>;
|
||
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;
|
||
conditions: Array<{
|
||
field: Record<string, { page: number | string; value: string }>;
|
||
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<string, { page: number | string; value: string }>;
|
||
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<string, {
|
||
res: boolean;
|
||
page?: number | string;
|
||
value?: string;
|
||
}>;
|
||
};
|
||
}> = [];
|
||
|
||
// 使用对象存储相同fieldKey的项,便于快速查找和合并
|
||
const fieldKeyMap: Record<string, {
|
||
fieldKey: string;
|
||
fieldValue: {
|
||
type: Record<string, {
|
||
res: boolean;
|
||
page?: number | string;
|
||
value?: string;
|
||
}>;
|
||
};
|
||
}> = {};
|
||
|
||
// 第一步:按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]);
|
||
}
|
||
|
||
// 返回合并后的规则数组
|
||
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 && (
|
||
<p className="text-xs text-red-500 select-text text-left mb-1">该评查点无法找到索引内容,无法自动定位到对应页面。</p>
|
||
)}
|
||
|
||
<div className="mt-2">
|
||
{/* {reviewPoint.suggestion && (
|
||
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||
<div className="flex items-start">
|
||
<i className="ri-information-line text-blue-500 mr-2 mt-0.5"></i>
|
||
<p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
|
||
</div>
|
||
</div>
|
||
)} */}
|
||
|
||
{/* 评查点内容显示区域 */}
|
||
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
|
||
<div className="bg-white rounded border-gray-200 text-xs mb-3 select-text">
|
||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||
{renderContent(reviewPoint, mergedRules)}
|
||
</div>
|
||
)}
|
||
|
||
|
||
|
||
{/* 额外的文本输入框区域 */}
|
||
<div className="mb-3">
|
||
<textarea
|
||
id={`manual-review-${reviewPoint.id}`}
|
||
className="w-full p-2 border rounded bg-white text-xs min-h-[80px] focus:outline-none focus:border-primary"
|
||
placeholder="请输入重新审核意见..."
|
||
value={manualReviewNotes[reviewPoint.id] || ''}
|
||
onChange={(e) => handleNoteChange(reviewPoint.id, e.target.value)}
|
||
></textarea>
|
||
</div>
|
||
|
||
<div className="flex justify-end">
|
||
{reviewPoint.editAuditStatus === 0 ? (
|
||
<div className="w-full flex justify-end gap-2">
|
||
<button
|
||
className="bg-[#1890ff] hover:bg-blue-600 text-sm text-white py-1 px-2 rounded-md flex items-center justify-center"
|
||
onClick={() => handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'approve', note)}
|
||
>
|
||
<i className="ri-check-line mr-1"></i> 通过
|
||
</button>
|
||
<button
|
||
className="bg-[#f5222d] hover:bg-red-600 text-sm text-white py-1 px-2 rounded-md flex items-center justify-center"
|
||
onClick={() => handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'reject', note)}
|
||
>
|
||
<i className="ri-close-line mr-1"></i> 不通过
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<button
|
||
className="bg-purple-600 hover:bg-purple-700 text-sm text-white py-1 px-2 rounded-md flex items-center justify-center"
|
||
onClick={() => handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'review', note)}
|
||
>
|
||
<i className="ri-refresh-line mr-1"></i> 重新审核
|
||
</button>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
);
|
||
}
|
||
|
||
// 处理 result=true 且 postAction!=manual 的情况
|
||
return (
|
||
<>
|
||
{checkContentPage(reviewPoint).pageIndex === 0 && (
|
||
<p className="text-xs text-red-500 select-text text-left mb-1">该评查点无法找到索引内容,无法自动定位到对应页面。</p>
|
||
)}
|
||
{/* 评查点内容显示区域 */}
|
||
{reviewPoint.content && Object.entries(reviewPoint.content).length > 0 && (
|
||
<div className="bg-white rounded border-gray-200 text-xs mb-3 select-text">
|
||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||
{renderContent(reviewPoint, mergedRules)}
|
||
</div>
|
||
)}
|
||
</>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<div>
|
||
|
||
{/* 没有索引内容提示 */}
|
||
{checkContentPage(reviewPoint).pageIndex === 0 && (
|
||
<p className="text-xs text-red-500 select-text text-left mb-1">该评查点无法找到索引内容,无法自动定位到对应页面。</p>
|
||
)}
|
||
|
||
{/* 建议内容显示区域 */}
|
||
{reviewPoint.suggestion && (
|
||
<div className="p-2 bg-blue-50 rounded border border-blue-200 text-xs mb-3 select-text">
|
||
<div className="flex items-start">
|
||
<i className="ri-information-line text-blue-500 mr-2 mt-0.5"></i>
|
||
<p className="text-xs text-gray-600 select-text text-left">{reviewPoint.suggestion}</p>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
|
||
{/* 法律依据内容 */}
|
||
{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)) && (
|
||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||
<div className="flex justify-between items-center mb-1">
|
||
<span className="text-xs font-medium">法律依据</span>
|
||
</div>
|
||
{reviewPoint.legalBasis.name && (
|
||
<p className="text-xs text-left mb-1 select-text">{reviewPoint.legalBasis.name}</p>
|
||
)}
|
||
{reviewPoint.legalBasis.content && (
|
||
<p className="text-xs text-left mb-1 select-text"><span className="font-medium">条款内容:</span>{reviewPoint.legalBasis.content}</p>
|
||
)}
|
||
{reviewPoint.legalBasis.articles && Array.isArray(reviewPoint.legalBasis.articles) && reviewPoint.legalBasis.articles.length > 0 && (
|
||
<div>
|
||
<p className="text-xs text-left font-medium mb-1">相关条款:</p>
|
||
<ul className="list-disc pl-4 select-text">
|
||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||
<li key={index} className="text-xs text-left select-text">
|
||
{typeof item === 'string' ? item :
|
||
typeof item === 'object' && item !== null ?
|
||
(item.name ? `${item.name}: ${item.content || ''}` :
|
||
item.content || JSON.stringify(item)) :
|
||
String(item)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)
|
||
)}
|
||
|
||
|
||
{reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0 && (
|
||
<>
|
||
{/* 内容显示区域 */}
|
||
<div className="bg-white rounded border-gray-200 text-xs mb-3 select-text">
|
||
<div>
|
||
{/* 修改评查结果的结构之后,显示新的结构 */}
|
||
{renderContent(reviewPoint, mergedRules)}
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{/* 建议修改区域 */}
|
||
{/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */}
|
||
{(reviewPoint.postAction === 'manual') && (
|
||
<div className="mb-2">
|
||
<div className="flex justify-between items-center mb-2">
|
||
<span className="text-gray-700 text-[0.8rem]">{reviewPoint.postAction === 'manual' ? "审核意见:" : "建议修改为:"}</span>
|
||
{/* <span className="text-green-500">符合规范</span> */}
|
||
</div>
|
||
<textarea
|
||
value={manualReviewNotes[reviewPoint.id] || ''}
|
||
placeholder={reviewPoint.postAction === 'manual' ? "请输入审核意见(可选)..." : "请输入建议修改内容..."}
|
||
onChange={(e) => handleManualReviewNotesChange(reviewPoint.id, e.target.value)}
|
||
className="text-xs w-full p-2 border rounded bg-white min-h-[100px] focus:outline-none focus:border-[#00684a] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]"
|
||
/>
|
||
</div>
|
||
)}
|
||
|
||
{/* 操作按钮区域 */}
|
||
{/* {((reviewPoint.postAction === 'manual') || (reviewPoint.content !== null && Object.keys(reviewPoint.content).length > 0)) && ( */}
|
||
{(reviewPoint.postAction === 'manual') && (
|
||
<div className="flex space-x-2 mt-2">
|
||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||
{(reviewPoint.postAction !== 'manual') && (
|
||
<button
|
||
className="replace-action flex-1 justify-center"
|
||
onClick={() => handleReplace(reviewPoint.id)}
|
||
>
|
||
<i className="ri-replace-line"></i> 一键替换
|
||
</button>
|
||
)}
|
||
|
||
{/* 人工审核按钮 */}
|
||
{reviewPoint.editAuditStatus === 0 ? (
|
||
<div className="w-full flex justify-end gap-2">
|
||
<button
|
||
className="bg-[#1890ff] hover:bg-blue-600 text-white py-1 px-2 rounded-md text-sm"
|
||
onClick={() => {
|
||
const note = manualReviewNotes[reviewPoint.id] || '';
|
||
handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'approve', note);
|
||
}}
|
||
>
|
||
<i className="ri-check-line mr-1"></i> 通过
|
||
</button>
|
||
<button
|
||
className="bg-[#f5222d] hover:bg-red-600 text-white py-1 px-2 rounded-md text-sm"
|
||
onClick={() => {
|
||
const note = manualReviewNotes[reviewPoint.id] || '';
|
||
handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'reject', note);
|
||
}}
|
||
>
|
||
<i className="ri-close-line mr-1"></i> 不通过
|
||
</button>
|
||
</div>
|
||
) : (
|
||
<div className="w-full flex justify-end">
|
||
<button
|
||
className="bg-purple-600 hover:bg-purple-700 text-white py-1 px-2 rounded-md text-sm"
|
||
onClick={() => {
|
||
const note = manualReviewNotes[reviewPoint.id] || '';
|
||
handleReviewAction(reviewPoint.id, reviewPoint.editAuditStatusId, 'review', note);
|
||
}}
|
||
>
|
||
<i className="ri-refresh-line mr-1"></i> 重新审核
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
};
|
||
|
||
/**
|
||
* 渲染无匹配结果提示
|
||
* 当过滤后没有评查点时显示
|
||
*/
|
||
const renderEmptyState = () => {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center py-8 px-4 text-center text-gray-500">
|
||
<i className="ri-search-line text-3xl mb-2"></i>
|
||
<p className="text-sm mb-1">没有找到匹配的评查点</p>
|
||
<p className="text-xs">请尝试不同的搜索词或清除筛选条件</p>
|
||
{(searchText || statusFilter) && (
|
||
<button
|
||
className="mt-3 text-xs text-primary underline"
|
||
onClick={() => {
|
||
setSearchText('');
|
||
setStatusFilter(null);
|
||
}}
|
||
>
|
||
清除所有筛选
|
||
</button>
|
||
)}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
// 处理评查点点击事件
|
||
const handleReviewPointClick = (id: string) => {
|
||
// 找到被点击的评查点
|
||
const reviewPoint = reviewPoints.find(result => result.id === id);
|
||
|
||
// 如果评查点存在
|
||
if (reviewPoint) {
|
||
// // 使用checkContentPage方法获取页码和key
|
||
// const { pageIndex, key } = checkContentPage(reviewPoint);
|
||
|
||
// // 如果有有效页码,传递ID和页码
|
||
// if (pageIndex > 0) {
|
||
// console.log(`跳转到页面 ${pageIndex},对应内容 ${key || '未知'}`);
|
||
// onReviewPointSelect(id, pageIndex);
|
||
// return;
|
||
// }
|
||
|
||
// // 没有有效页码,只传递ID
|
||
onReviewPointSelect(id);
|
||
// console.log(`没有有效页码---评查点ID:${reviewPoint.pointId},评查点结果ID:${id}`);
|
||
} else {
|
||
// // 没有找到评查点,只传递ID
|
||
onReviewPointSelect(id);
|
||
// console.log(`没有找到评查点---评查点结果ID:${id}`);
|
||
}
|
||
};
|
||
|
||
// 检查评查点的contentPage,如果contentPage内也没有page,则返回默认值
|
||
const checkContentPage = (reviewPoint: ReviewPoint): { pageIndex: number, key?: string, id: string } => {
|
||
// 返回对象初始化
|
||
const result = { pageIndex: 0, id: reviewPoint.id };
|
||
|
||
// 如果contentPage不存在或是空对象,返回默认值
|
||
if (!reviewPoint.contentPage || Object.keys(reviewPoint.contentPage).length === 0) {
|
||
return result;
|
||
}
|
||
|
||
// 遍历contentPage中的每个key
|
||
for (const key of Object.keys(reviewPoint.contentPage)) {
|
||
if (reviewPoint.contentPage[key] && parseInt(reviewPoint.contentPage[key] as string) > 0) {
|
||
// 返回第一个找到的有效页码,以及对应的key
|
||
return {
|
||
pageIndex: parseInt(reviewPoint.contentPage[key] as string),
|
||
key,
|
||
id: reviewPoint.id
|
||
};
|
||
}
|
||
}
|
||
|
||
// 如果遍历完所有key都没找到有效页码,返回默认值
|
||
return result;
|
||
};
|
||
|
||
// 组件主渲染函数
|
||
return (
|
||
<div className="review-points-panel select-text">
|
||
<TooltipPortal />
|
||
{/* 面板头部 */}
|
||
<div className="review-panel-header py-2 px-4 flex items-center">
|
||
<i className="ri-file-list-check-line text-primary mr-2"></i>
|
||
<span className="font-medium text-primary">评查结果</span>
|
||
</div>
|
||
|
||
{/* 评查统计 */}
|
||
{renderStatistics()}
|
||
|
||
{/* 搜索框 */}
|
||
{renderSearchBar()}
|
||
|
||
{/* 评查点列表 */}
|
||
<div className="review-points-list">
|
||
{filteredReviewPoints.length > 0 ? (
|
||
filteredReviewPoints.map(reviewPoint => (
|
||
<div
|
||
key={reviewPoint.id}
|
||
className={`rounded-md review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active border-l-4 !border-l-[rgba(0,104,74,1)] shadow-md' : 'border-l-4 border-l-transparent'}
|
||
transition-all duration-300 ease-in-out shadow-sm
|
||
hover:shadow-lg hover:border-l-4 hover:border-l-[rgba(0,104,74,0.3)]
|
||
hover:bg-[rgba(0,0,0,0.02)] my-2`}
|
||
role="button"
|
||
tabIndex={0}
|
||
style={{ userSelect: 'text' }}
|
||
onClick={() => {
|
||
console.log('reviewPoint', reviewPoint);
|
||
handleReviewPointClick(reviewPoint.id);
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter') {
|
||
handleReviewPointClick(reviewPoint.id);
|
||
}
|
||
}}
|
||
>
|
||
{/* 评查点标题和状态 */}
|
||
{/* 评查点名称 pointName*/}
|
||
<div className="flex justify-between items-center mb-2">
|
||
{/* <div className='flex flex-col'> */}
|
||
<div className="review-point-title text-left text-blue-500 max-w-[75%] break-all">{reviewPoint.pointName}</div>
|
||
{/* <div className="review-point-header flex justify-between items-start">
|
||
<div className="flex-1 text-left min-w-[25%] font-medium text-[13px]">{reviewPoint.title}</div>
|
||
//评查点分组显示
|
||
<div className="review-point-location max-w-[40%] flex items-center">
|
||
<i className="ri-file-list-line mr-1 flex-shrink-0"></i>
|
||
<span
|
||
className="truncate block whitespace-nowrap overflow-hidden
|
||
hover:overflow-visible hover:text-clip hover:bg-white hover:shadow-md hover:z-10 hover:text-wrap px-1 rounded"
|
||
title={reviewPoint.groupName}
|
||
style={{ cursor: 'text', userSelect: 'all' }}
|
||
>
|
||
{reviewPoint.groupName}
|
||
</span>
|
||
</div>
|
||
</div> */}
|
||
{/* </div> */}
|
||
<div className="flex ml-2 flex-shrink-0 min-w-[15%]">
|
||
{renderStatusBadge(reviewPoint.status, reviewPoint.result,reviewPoint.title)}
|
||
{renderHumanReviewBadge(reviewPoint)}
|
||
</div>
|
||
</div>
|
||
|
||
{/* 评查点内容和操作 */}
|
||
{renderReviewPointContent(reviewPoint)}
|
||
</div>
|
||
))
|
||
) : (
|
||
renderEmptyState()
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|