Files
leaudit-platform-frontend/app/components/reviews/ReviewPointsList.tsx
T

2043 lines
83 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 评查点列表组件
*
* 功能概述:
* - 展示评查结果统计信息(总计、通过、警告、错误数量)
* - 提供评查点过滤功能(按状态和搜索文本)
* - 显示评查点详细信息(标题、状态、内容、建议修改等)
* - 支持评查点操作(一键替换、人工审核等)
*
* 组件结构:
* - 统计区域: 显示评查点数量统计
* - 搜索区域: 提供文本搜索功能
* - 评查点列表: 展示所有评查点
* - 评查点卡片: 展示单个评查点详情
* - 评查点头部: 显示标题和状态
* - 评查点内容: 显示当前内容和问题
* - 建议修改区域: 显示建议的修改内容
* - 操作按钮: 提供一键替换和人工审核功能
*/
import { useState, useEffect } from 'react';
import { toastService } from '../ui/Toast';
// import { toastService } from '../ui/Toast';
/**
* 比较方法映射
* 将后端返回的比较方法英文值映射为友好的中文显示
*/
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;
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;
}
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 评查结果
* @returns 状态标签组件
*/
const renderStatusBadge = (status: string, result?: boolean) => {
// 优先根据result判断是否通过
if (result === true) {
return (
<span className="status-badge status-success">
<i className="ri-checkbox-circle-line mr-1"></i>
</span>
);
}
// 当result为false时,根据status决定显示警告还是错误
if (result === false) {
if (status === 'warning') {
return (
<span className="status-badge status-warning">
<i className="ri-alert-line mr-1"></i>
</span>
);
} else if (status === 'error') {
return (
<span className="status-badge status-error">
<i className="ri-close-circle-line mr-1"></i>
</span>
);
}
}
// 兼容旧版逻辑,当没有result时,仍按status判断
switch (status) {
case 'success':
return (
<span className="status-badge status-success">
<i className="ri-checkbox-circle-line mr-1"></i>
</span>
);
case 'warning':
return (
<span className="status-badge status-warning">
<i className="ri-alert-line mr-1"></i>
</span>
);
case 'error':
return (
<span className="status-badge status-error">
<i className="ri-close-circle-line mr-1"></i>
</span>
);
case 'processing':
return (
<span className="status-badge status-processing">
<i className="ri-time-line mr-1"></i>
</span>
);
default:
return (
<span className="status-badge status-warning">
<i className="ri-alert-line mr-1"></i>
</span>
);
}
};
/**
* 渲染人工审核标记
* @param reviewPoint 评查点
* @returns 人工审核标记组件
*/
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
if (reviewPoint.postAction === 'manual') {
return (
<span className="status-badge status-waiting mt-1 text-xs">
<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') {
// 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 评查点
* @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:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out 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>
</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-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
<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
${
res ? 'bg-green-100 text-green-600': 'bg-yellow-100 text-yellow-600'
}`}>
<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 font-medium rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
) : null
)}
</div>
</div>
<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>
</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"></i>
)}
</div>
<div className="value-content text-xs overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover:z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">{chain[0].data.value?.toString() || ''}</div>
</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"></i>
)}
</div>
<div className="value-content text-xs overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover:z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">{chain[1].data.value?.toString() || ''}</div>
</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-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
<div className="flex flex-row gap-2 overflow-x-auto">
<div key={chain[1].compareMethod} className={`rounded-md flex flex-row items-center ${
res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<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 font-medium rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
</div>
</div>
<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>
</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;
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"></i>
)}
{/* 缺失显示 */}
{mainTypeValue.res === false && !mainTypeValue.value && (
<span className="ml-2 text-xs text-yellow-500">
</span>
)}
</div>
{/* 规则类型标签组 - 只在有多个类型时显示,且只显示非主要类型 */}
{/* {Object.keys(fieldValue?.type || {}).length > 1 && (
<div className="flex flex-wrap gap-1 mb-2">
{Object.entries(fieldValue?.type || {})
.filter(([typeKey]) => typeKey !== mainTypeKey) // 排除主要类型
.map(([typeKey, typeValue]) => (
<button
key={typeKey}
className={`text-xs px-2 py-0.5 rounded-full ${typeValue.res ? 'bg-green-100 text-green-800 hover:bg-green-300' : 'bg-yellow-100 text-yellow-800'}`}
onClick={() => {
if (typeValue.page && typeof onReviewPointSelect === 'function') {
onReviewPointSelect(reviewPoint.id, Number(typeValue.page));
}
}}
title={`${typeKey}: ${typeValue.res ? '通过' : '不通过'}`}
>
{typeKey}
</button>
))}
</div>
)} */}
{/* 主要值显示 */}
{mainTypeValue.value && (
<button
className="text-xs p-1 rounded cursor-text w-full text-left"
// onClick={(e) => {
// if (mainTypeValue.page && typeof onReviewPointSelect === 'function') {
// e.stopPropagation();
// onReviewPointSelect(reviewPoint.id, Number(mainTypeValue.page));
// }
// }}
>
<div className="text-gray-800 break-all overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">
{mainTypeValue.value}
</div>
</button>
)}
</div>
<div className={`w-8 flex items-center justify-center rounded-r-md group relative`}>
{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 className="w-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
<div className="flex flex-row gap-2 overflow-x-auto">
{Object.entries(fieldValue?.type || {}).map(([typeKey, typeValue]) => (
<div key={typeKey} className={`rounded-md flex flex-row items-center ${
typeValue.res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap">{getRuleTypeText(typeKey)}</div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
{typeValue.res ? '通过' : '不通过'}
</div>
</div>
))}
</div>
</div>
<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>
</div>
</button>
);
};
/**
* 渲染评查点大模型判断的规则的样式
* @param aiRule 评查点大模型判断的规则
* @param reviewPoint 关联的评查点
* @returns 评查点大模型判断的规则的样式
*/
const renderModelRule = (aiRule: Record<string, unknown>, reviewPoint: ReviewPoint) => {
// console.log('aiRule-------', 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[] = [];
// 先存放一条粗的横线
// fieldElements.push(
// <div key="line" className=" bg-gray-50 rounded border border-gray-200 text-xs mb-3"></div>
// );
// 遍历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">
{/* 字段名称 */}
<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"></i>
)}
{/* 缺失显示 */}
{!res && (
<span className="ml-2 text-xs text-yellow-500">
</span>
)}
</div>
{/* 主要值显示 */}
{res && (
<button
className="text-xs p-1 rounded cursor-text w-full text-left"
// onClick={() => {
// if (value.page && typeof onReviewPointSelect === 'function') {
// onReviewPointSelect(reviewPoint.id, Number(value.page));
// }
// }}
>
<div className="text-gray-800 break-all overflow-hidden line-clamp-2 hover:line-clamp-none hover:shadow-[0_0_10px_rgba(0,0,0,0.1)]
hover: z-10 hover:overflow-auto rounded transition-all duration-300 ease-in-out cursor-text max-h-96">
{value.value}
</div>
</button>
)}
</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-auto absolute hidden group-hover:block right-full top-1/2 transform -translate-y-1/2 mr-2 z-100">
<div className="bg-white shadow-md rounded-md p-1 border border-gray-200">
{/* <div className="text-xs font-medium text-gray-700 w-auto">规则检查结果</div> */}
<div className="flex flex-row gap-2 overflow-x-auto">
<div className={`rounded-md flex flex-row items-center ${
res
? 'bg-green-100 text-green-600'
: 'bg-yellow-100 text-yellow-600'
}`}>
<div className="text-xs text-gray-600 pl-1 whitespace-nowrap"></div>
<div className={`p-1 text-xs font-medium rounded-full min-w-[50px] text-center`}>
{res ? '通过' : '不通过'}
</div>
</div>
</div>
</div>
<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>
</div>
</button>
);
});
}
// 后处理message
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是exist、format、logic、regex的规则中重复的进行去重
* @param reviewPoint 评查点
* @returns 合并后的规则数组
*/
const filterOtherRule = (reviewPoint: ReviewPoint) => {
// 遍历这些评查点中的规则evaluatedPointResultsLog,把type是exist、format、logic、regex的规则添加到allRule中
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 }
}
};
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]);
}
// console.log('合并后的规则:', mergedRules);
// setOtherRule(mergedRules);
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="p-2 bg-white rounded border 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="p-2 bg-white rounded border 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">
{/* 面板头部 */}
<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-[#00684a] shadow-md' : 'border-l-4 border-l-transparent'}
transition-all duration-300 ease-in-out
hover:shadow-lg hover:-translate-x-0.5 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 flex-1 text-left text-blue-500">{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 flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
{renderHumanReviewBadge(reviewPoint)}
</div>
</div>
{/* 评查点内容和操作 */}
{renderReviewPointContent(reviewPoint)}
</div>
))
) : (
renderEmptyState()
)}
</div>
</div>
);
}