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

943 lines
38 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';
/**
* 评查点类型定义
* 用于展示单个评查结果
*/
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;
}
// 统计数据类型
interface Statistics {
total: number;
success: number;
warning: number;
error: number;
score: number;
}
interface ReviewPointsListProps {
reviewPoints: ReviewPoint[];
statistics: Statistics;
activeReviewPointResultId: string | null;
onReviewPointSelect: (id: string, page?: number) => void;
onStatusChange: (id: string, editAuditStatusId: string | number, status: string, message: string) => void;
}
export function ReviewPointsList({
reviewPoints,
statistics,
activeReviewPointResultId,
onReviewPointSelect,
onStatusChange
}: ReviewPointsListProps) {
// 状态管理
const [editingReviewPoint, setEditingReviewPoint] = useState<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>>({});
// 初始化建议文本
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';
} else if (statusFilter === 'error') {
// 过滤"错误"状态
matchesStatus = point.result === false && point.status === 'error';
}
return matchesSearch && matchesStatus;
});
/**
* 处理一键替换操作
* @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 ml-2 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) => {
return (
<>
{/* 修改评查结果的结构之后,显示新的结构 */}
{Object.entries(reviewPoint.content).map(([key, value], index) => (
<div
key={index}
className="mb-2 pb-2 border-b border-gray-100 last:border-b-0 last:mb-0 last:pb-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 reviewPoint 评查点
* @returns 评查点内容与建议组件
*/
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
const handleManualReviewNotesChange = (reviewPointId: string, text: string) => {
setManualReviewNotes(prev => ({
...prev,
[reviewPointId]: text
}));
};
// 如果当前评查点不处于编辑状态,只显示简单信息
if (editingReviewPoint !== reviewPoint.id) {
// 根据result和status决定渲染哪种样式
if (reviewPoint.result === true) {
// 已通过的评查点只显示基本信息和人工审核注释 delete TODO
// if (reviewPoint.needsHumanReview && reviewPoint.humanReviewNote) {
// return (
// <div className="mt-2">
// <div className="p-2 bg-green-50 rounded border border-green-200 text-xs">
// <p className="text-xs text-success"><i className="ri-check-line mr-1"></i>已处理</p>
// {reviewPoint.suggestion && (
// <div className="border-t border-green-200 mt-1 pt-1">
// <p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
// </div>
// )}
// </div>
// </div>
// );
// }
// 处理 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"></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)}
</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"></p>
)}
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
<div>
{/* 修改评查结果的结构之后,显示新的结构 */}
{renderContent(reviewPoint)}
</div>
</div>
</>
);
}
return (
<div className="mt-2">
{/* 没有索引内容提示 */}
{checkContentPage(reviewPoint).pageIndex === 0 && (
<p className="text-xs text-red-500 select-text text-left"></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)}
</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={`review-point-item ${activeReviewPointResultId === reviewPoint.id ? 'active' : ''}`}
onClick={() => handleReviewPointClick(reviewPoint.id)}
onKeyDown={(e) => {
if (e.key === 'Enter') {
handleReviewPointClick(reviewPoint.id);
}
}}
role="button"
tabIndex={0}
style={{ userSelect: 'text' }}
>
{/* 评查点标题和状态 */}
{/* 评查点名称 pointName*/}
<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="review-point-title flex-1 text-left min-w-[25%]">{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 className="flex flex-col items-center ml-2 flex-shrink-0 max-w-[15%]">
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
{renderHumanReviewBadge(reviewPoint)}
</div>
</div>
{/* 人工审核注释 */}
{renderHumanReviewNote(reviewPoint)}
{/* 评查点内容和操作 */}
{renderReviewPointContent(reviewPoint)}
</div>
))
) : (
renderEmptyState()
)}
</div>
</div>
);
}