完善评查详情
This commit is contained in:
@@ -37,24 +37,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
||||
path: '/files/upload',
|
||||
icon: 'ri-upload-cloud-line'
|
||||
},
|
||||
// {
|
||||
// id: 'file-list',
|
||||
// title: '文件列表',
|
||||
// path: '/files',
|
||||
// icon: 'ri-file-list-3-line'
|
||||
// },
|
||||
{
|
||||
id:'documents',
|
||||
title:'文档列表',
|
||||
path:'/documents',
|
||||
icon:'ri-file-list-3-line'
|
||||
}
|
||||
// {
|
||||
// id:'documents-edit',
|
||||
// title:'文档编辑',
|
||||
// path:'/documents/edit',
|
||||
// icon:'ri-file-edit-line'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -87,12 +75,12 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) {
|
||||
path: '/rules-new',
|
||||
icon: 'ri-add-circle-line'
|
||||
},
|
||||
{
|
||||
id: 'review-detail',
|
||||
title: '评查详情',
|
||||
path: '/reviews',
|
||||
icon: 'ri-file-chart-line'
|
||||
}
|
||||
// {
|
||||
// id: 'review-detail',
|
||||
// title: '评查详情',
|
||||
// path: '/reviews',
|
||||
// icon: 'ri-file-chart-line'
|
||||
// }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -34,7 +34,7 @@ export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
</span>
|
||||
{fileInfo.fileSize && (
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
{fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
||||
| {fileInfo.fileSize} | {fileInfo.fileFormat} | {fileInfo.pageCount}页
|
||||
</span>
|
||||
)}
|
||||
{fileInfo.uploadTime && (
|
||||
|
||||
@@ -3,6 +3,11 @@
|
||||
* 显示文档内容和评查点高亮
|
||||
*/
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
|
||||
// 设置worker路径为public目录下的worker文件
|
||||
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
// 导入统一的ReviewPoint类型
|
||||
import { type ReviewPoint } from './';
|
||||
@@ -11,6 +16,12 @@ import { type ReviewPoint } from './';
|
||||
interface FileContent {
|
||||
title: string;
|
||||
contractNumber: string;
|
||||
path: string;
|
||||
ocrResult?: {
|
||||
__meta?: {
|
||||
page_offset?: number;
|
||||
};
|
||||
}; // 添加ocrResult属性
|
||||
parties: {
|
||||
partyA: {
|
||||
name: string;
|
||||
@@ -35,12 +46,15 @@ interface FilePreviewProps {
|
||||
fileContent: FileContent;
|
||||
reviewPoints: ReviewPoint[];
|
||||
activeReviewPointId: string | null;
|
||||
targetPage?: number; // 新增目标页码参数
|
||||
}
|
||||
|
||||
export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, reviewPoints, activeReviewPointId, targetPage }: FilePreviewProps) {
|
||||
const [zoomLevel, setZoomLevel] = useState(100);
|
||||
const [highlightsVisible, setHighlightsVisible] = useState(true);
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const [numPages, setNumPages] = useState<number | null>(null);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
|
||||
// 放大文档
|
||||
const handleZoomIn = () => {
|
||||
@@ -77,6 +91,28 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
|
||||
}
|
||||
}, [activeReviewPointId]);
|
||||
|
||||
// 处理页面跳转
|
||||
useEffect(() => {
|
||||
if (targetPage && numPages && targetPage <= numPages) {
|
||||
let newTargetPage = targetPage;
|
||||
try {
|
||||
// 安全地访问ocrResult
|
||||
if (fileContent.ocrResult && fileContent.ocrResult.__meta && fileContent.ocrResult.__meta.page_offset) {
|
||||
// 可以根据需要使用page_offset调整目标页面
|
||||
newTargetPage = targetPage + fileContent.ocrResult.__meta.page_offset;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("访问ocrResult时出错:", error);
|
||||
}
|
||||
|
||||
const pageElement = document.getElementById(`page-${newTargetPage}`);
|
||||
if (pageElement) {
|
||||
console.log(`跳转到第${newTargetPage}页`);
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
}
|
||||
}
|
||||
}, [targetPage, numPages, fileContent]);
|
||||
|
||||
// 获取评查点对应的样式类
|
||||
const getHighlightClass = (status: string) => {
|
||||
switch (status) {
|
||||
@@ -91,69 +127,114 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
|
||||
}
|
||||
};
|
||||
|
||||
// PDF文档加载成功回调函数
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
|
||||
setNumPages(numPages);
|
||||
console.log("PDF加载成功,页数:", numPages);
|
||||
}
|
||||
|
||||
/**
|
||||
* 渲染PDF文档的所有页面
|
||||
*
|
||||
* 功能描述:
|
||||
* 1. 生成PDF所有页面的渲染数组,每个页面包含页码标识和实际页面内容
|
||||
* 2. 处理页面缩放,通过CSS transform实现页面大小调整
|
||||
* 3. 在每个页面上标记对应的评查点高亮区域
|
||||
* 4. 处理评查点的激活状态,显示特殊的高亮效果
|
||||
*
|
||||
* @returns {JSX.Element[] | null} 返回所有页面组件的数组,如果没有页数信息则返回null
|
||||
*/
|
||||
const renderAllPages = () => {
|
||||
// 如果还没有获取到PDF总页数,返回null
|
||||
if (!numPages) return null;
|
||||
|
||||
// 用于存储所有页面组件的数组
|
||||
const pages = [];
|
||||
|
||||
// 遍历每一页,生成对应的页面组件
|
||||
for (let i = 1; i <= numPages; i++) {
|
||||
// 查找该页面上的评查点,基于position.section匹配页面ID
|
||||
const pageReviewPoints = reviewPoints.filter(point =>
|
||||
point.position && point.position.section === `page-${i}`
|
||||
);
|
||||
// console.log("pageReviewPoints-------",pageReviewPoints);
|
||||
|
||||
// 为每一页创建组件
|
||||
pages.push(
|
||||
<div key={i} id={`page-${i}`} className="mb-6">
|
||||
{/* 页码标识,显示在页面上方 */}
|
||||
<div className="text-center text-gray-500 text-sm mb-2">第 {i} 页</div>
|
||||
|
||||
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
||||
<div style={{
|
||||
transform: `scale(${zoomLevel / 100})`, // 根据zoomLevel应用缩放
|
||||
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||
position: 'relative' // 相对定位,作为评查点高亮的定位参考
|
||||
}}>
|
||||
{/* 渲染PDF页面组件 */}
|
||||
<Page
|
||||
pageNumber={i} // 当前页码
|
||||
renderTextLayer={true} // 启用文本层,使文本可选择
|
||||
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
|
||||
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
|
||||
/>
|
||||
|
||||
{/* 渲染评查点高亮区域 */}
|
||||
{highlightsVisible && pageReviewPoints.map(point => {
|
||||
// 判断当前评查点是否为激活状态(被选中)
|
||||
const isActive = point.id === activeReviewPointId;
|
||||
|
||||
return (
|
||||
// 评查点高亮区域
|
||||
<div
|
||||
key={point.id}
|
||||
// 添加多个类名:基础高亮区域、PDF专用高亮、状态样式类、激活状态类
|
||||
className={`highlight-area pdf-highlight ${getHighlightClass(point.status)} ${isActive ? 'highlight-focus' : ''}`}
|
||||
// 设置唯一标识,用于滚动定位和DOM查询
|
||||
data-review-id={point.id}
|
||||
// 设置高亮区域的样式
|
||||
style={{
|
||||
position: 'absolute', // 绝对定位,相对于页面容器
|
||||
zIndex: isActive ? 20 : 10, // 激活状态时提高层级,确保在最上层显示
|
||||
boxShadow: isActive ? '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)' : '', // 激活时添加特殊阴影效果
|
||||
// 根据评查点的位置信息计算高亮区域位置,实际项目中需根据真实位置坐标计算
|
||||
top: `${point.position?.index ? point.position.index * 20 : 20}px`,
|
||||
left: '50px',
|
||||
width: '300px',
|
||||
height: '30px',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 返回所有页面组件数组
|
||||
return pages;
|
||||
};
|
||||
|
||||
// 渲染文档内容
|
||||
const renderDocumentContent = () => {
|
||||
return (
|
||||
<div className="word-document" ref={contentRef} style={{transform: `scale(${zoomLevel/100})`, transformOrigin: 'center top'}}>
|
||||
<h1>{fileContent.title}</h1>
|
||||
<p style={{textAlign: 'right'}}>合同编号:{fileContent.contractNumber}</p>
|
||||
|
||||
<div style={{margin: '20px 0'}}>
|
||||
<p><strong>甲方(供方):</strong>{fileContent.parties.partyA.name}</p>
|
||||
<p>地址:{fileContent.parties.partyA.address}</p>
|
||||
<p>法定代表人:{fileContent.parties.partyA.representative}</p>
|
||||
<p>联系电话:{fileContent.parties.partyA.phone}</p>
|
||||
<p> </p>
|
||||
<p><strong>乙方(需方):</strong>{fileContent.parties.partyB.name}</p>
|
||||
<p>地址:{fileContent.parties.partyB.address}</p>
|
||||
<p>法定代表人:{fileContent.parties.partyB.representative}</p>
|
||||
<p>联系电话:{fileContent.parties.partyB.phone}</p>
|
||||
</div>
|
||||
|
||||
<p>根据《中华人民共和国合同法》及有关法律法规的规定,经双方协商一致,签订本合同,共同遵守。</p>
|
||||
|
||||
{fileContent.sections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<h2>{section.title}</h2>
|
||||
{renderSectionContent(section.content, section.title, sectionIndex)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<Document
|
||||
file={'http://172.18.0.100:9000/docauditai/'+fileContent.path}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error("PDF加载错误:", error);
|
||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||
}}
|
||||
className="flex flex-col items-center"
|
||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||
noData={<div>无数据</div>}
|
||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||
>
|
||||
{renderAllPages()}
|
||||
</Document>
|
||||
);
|
||||
};
|
||||
|
||||
// 渲染章节内容,处理高亮
|
||||
const renderSectionContent = (content: string, sectionTitle: string, sectionIndex: number) => {
|
||||
const lines = content.split('\n');
|
||||
|
||||
return lines.map((line, lineIndex) => {
|
||||
// 查找该行对应的评查点
|
||||
const reviewPoint = reviewPoints.find(point =>
|
||||
point.position &&
|
||||
point.position.section === sectionTitle &&
|
||||
point.position.index === lineIndex
|
||||
);
|
||||
|
||||
if (reviewPoint && highlightsVisible) {
|
||||
// 如果有对应的评查点,添加高亮
|
||||
const isActive = reviewPoint.id === activeReviewPointId;
|
||||
return (
|
||||
<div
|
||||
key={`${sectionIndex}-${lineIndex}`}
|
||||
className={`highlight-area ${getHighlightClass(reviewPoint.status)} ${isActive ? 'highlight-focus' : ''}`}
|
||||
data-review-id={reviewPoint.id}
|
||||
style={isActive ? {zIndex: 20, boxShadow: '0 0 0 2px yellow, 0 0 10px rgba(0,0,0,0.3)'} : {}}
|
||||
>
|
||||
<p>{line}</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
// 没有评查点,正常显示
|
||||
return <p key={`${sectionIndex}-${lineIndex}`}>{line}</p>;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-preview">
|
||||
<div className="file-preview-header py-2 px-4">
|
||||
@@ -182,8 +263,14 @@ export function FilePreview({ fileContent, reviewPoints, activeReviewPointId }:
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="file-preview-content">
|
||||
{renderDocumentContent()}
|
||||
<div className="file-preview-content" ref={contentRef}>
|
||||
{loadError ? (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>{loadError}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderDocumentContent()
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -34,6 +34,7 @@ export interface ReviewPoint {
|
||||
humanReviewNote?: string;
|
||||
humanReviewBy?: string;
|
||||
humanReviewTime?: string;
|
||||
contentPage?: number[];
|
||||
position?: {
|
||||
section: string;
|
||||
index: number;
|
||||
@@ -45,6 +46,8 @@ export interface ReviewPoint {
|
||||
articles?: Array<string | { name?: string; content?: string; [key: string]: unknown }>;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
postAction?: string;
|
||||
actionContent?: string;
|
||||
}
|
||||
|
||||
// 统计数据类型
|
||||
@@ -60,8 +63,8 @@ interface ReviewPointsListProps {
|
||||
reviewPoints: ReviewPoint[];
|
||||
statistics: Statistics;
|
||||
activeReviewPointId: string | null;
|
||||
onReviewPointSelect: (id: string) => void;
|
||||
onStatusChange: (id: string, status: string) => void;
|
||||
onReviewPointSelect: (id: string, page?: number) => void;
|
||||
onStatusChange: (id: string, status: string, message: string) => void;
|
||||
}
|
||||
|
||||
export function ReviewPointsList({
|
||||
@@ -73,24 +76,35 @@ export function ReviewPointsList({
|
||||
}: ReviewPointsListProps) {
|
||||
// 状态管理
|
||||
const [editingReviewPoint, setEditingReviewPoint] = useState<string | null>(null); // 当前正在编辑的评查点ID
|
||||
const [userInputText, setUserInputText] = useState(''); // 用户输入的审核意见文本
|
||||
const [searchText, setSearchText] = useState(''); // 搜索文本
|
||||
const [statusFilter, setStatusFilter] = useState<string | null>(null); // 状态过滤
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
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]);
|
||||
|
||||
// 处理建议文本变更
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const handleSuggestionChange = (reviewPointId: string, text: string) => {
|
||||
setSuggestionTexts(prev => ({
|
||||
...prev,
|
||||
@@ -98,11 +112,35 @@ export function ReviewPointsList({
|
||||
}));
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理评查点审核操作
|
||||
* @param reviewPointResultId 评查点结果ID
|
||||
* @param action 操作类型: 'approve' 通过 / 'reject' 不通过
|
||||
* @param message 用户输入的审核内容
|
||||
*/
|
||||
const handleReviewAction = (reviewPointResultId: string, action: 'approve' | 'reject', message: string) => {
|
||||
// 更新评查点状态
|
||||
onStatusChange(reviewPointResultId, action === 'approve' ? 'true' : 'false', message);
|
||||
|
||||
// 将参数输出到控制台
|
||||
console.log('评查点审核通过不通过操作', {
|
||||
id: reviewPointResultId,
|
||||
action: action,
|
||||
content: message,
|
||||
status: action === 'approve' ? 'true' : 'false'
|
||||
});
|
||||
|
||||
// 清除编辑状态
|
||||
setEditingReviewPoint(null);
|
||||
|
||||
alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointResultId},审核内容: ${message}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 过滤评查点
|
||||
* 根据搜索文本和状态过滤条件筛选评查点
|
||||
*/
|
||||
const filteredReviewPoints = reviewPoints.filter(point => {
|
||||
const filteredReviewPoints = reviewPoints.filter(point => {
|
||||
// 匹配搜索文本
|
||||
const matchesSearch = searchText === '' ||
|
||||
point.title.toLowerCase().includes(searchText.toLowerCase()) ||
|
||||
@@ -146,36 +184,6 @@ export function ReviewPointsList({
|
||||
// onStatusChange(reviewPointId, 'success');
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理评查点审核操作
|
||||
* @param reviewPointId 评查点ID
|
||||
* @param action 操作类型: 'approve' 通过 / 'reject' 不通过
|
||||
*/
|
||||
const handleReviewAction = (reviewPointId: string, action: 'approve' | 'reject') => {
|
||||
// 更新评查点状态
|
||||
onStatusChange(reviewPointId, action === 'approve' ? 'success' : 'error');
|
||||
|
||||
// 清除编辑状态
|
||||
setEditingReviewPoint(null);
|
||||
setUserInputText('');
|
||||
|
||||
alert(`${action === 'approve' ? '通过' : '不通过'}了评查点 ${reviewPointId}`);
|
||||
};
|
||||
|
||||
/**
|
||||
* 显示评查点详情编辑界面
|
||||
* @param reviewPointId 评查点ID
|
||||
*/
|
||||
const handleEditReviewPoint = (reviewPointId: string) => {
|
||||
setEditingReviewPoint(reviewPointId);
|
||||
|
||||
// 获取评查点的建议内容作为初始值
|
||||
const reviewPoint = reviewPoints.find(point => point.id === reviewPointId);
|
||||
if (reviewPoint) {
|
||||
setUserInputText(reviewPoint.suggestion || '');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染评查统计信息
|
||||
* 显示总计、通过、警告、错误数量
|
||||
@@ -215,7 +223,7 @@ export function ReviewPointsList({
|
||||
{/* 总计数量 */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={`w-7 h-7 bg-gray-100 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === null && searchText === '' ? 'ring-2 ring-gray-400' : ''}`}
|
||||
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('');
|
||||
@@ -224,47 +232,47 @@ export function ReviewPointsList({
|
||||
type="button"
|
||||
>
|
||||
<span className="text-sm font-semibold text-gray-600">{totalToShow}</span>
|
||||
<span className="text-xs text-gray-500 ml-1">总计</span>
|
||||
</button>
|
||||
<span className="text-xs text-gray-500 ml-1">总计</span>
|
||||
</div>
|
||||
<div className="h-8 border-r border-gray-200"></div>
|
||||
{/* 通过数量 */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={`w-7 h-7 bg-green-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'success' ? 'ring-2 ring-success' : ''}`}
|
||||
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>
|
||||
<span className="text-xs text-gray-500 ml-1">通过</span>
|
||||
</div>
|
||||
<div className="h-8 border-r border-gray-200"></div>
|
||||
{/* 警告数量 */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={`w-7 h-7 bg-yellow-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'warning' ? 'ring-2 ring-warning' : ''}`}
|
||||
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>
|
||||
<span className="text-xs text-gray-500 ml-1">警告</span>
|
||||
</div>
|
||||
<div className="h-8 border-r border-gray-200"></div>
|
||||
{/* 错误数量 */}
|
||||
<div className="flex items-center">
|
||||
<button
|
||||
className={`w-7 h-7 bg-red-50 rounded-md flex items-center justify-center cursor-pointer ${statusFilter === 'error' ? 'ring-2 ring-error' : ''}`}
|
||||
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>
|
||||
<span className="text-xs text-gray-500 ml-1">错误</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -374,9 +382,9 @@ export function ReviewPointsList({
|
||||
* @returns 人工审核标记组件
|
||||
*/
|
||||
const renderHumanReviewBadge = (reviewPoint: ReviewPoint) => {
|
||||
if (reviewPoint.needsHumanReview) {
|
||||
if (reviewPoint.postAction === 'manual') {
|
||||
return (
|
||||
<span className="status-badge status-waiting ml-2">
|
||||
<span className="status-badge status-waiting ml-2 mt-1 text-xs">
|
||||
<i className="ri-user-line mr-1"></i>需人工
|
||||
</span>
|
||||
);
|
||||
@@ -411,6 +419,14 @@ export function ReviewPointsList({
|
||||
* @returns 评查点内容组件
|
||||
*/
|
||||
const renderReviewPointContent = (reviewPoint: ReviewPoint) => {
|
||||
|
||||
const handleManualReviewNotesChange = (reviewPointId: string, text: string) => {
|
||||
setManualReviewNotes(prev => ({
|
||||
...prev,
|
||||
[reviewPointId]: text
|
||||
}));
|
||||
};
|
||||
|
||||
// 如果当前评查点不处于编辑状态,只显示简单信息
|
||||
if (editingReviewPoint !== reviewPoint.id) {
|
||||
// 根据result和status决定渲染哪种样式
|
||||
@@ -423,19 +439,77 @@ export function ReviewPointsList({
|
||||
<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">{reviewPoint.suggestion}</p>
|
||||
<p className="text-xs text-gray-600 select-text">{reviewPoint.suggestion}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 处理 result=true 且 postAction=manual 的情况
|
||||
if (reviewPoint.postAction === 'manual') {
|
||||
const handleReReview = (reviewPointId: string, status: string) => {
|
||||
const note = manualReviewNotes[reviewPointId] || '';
|
||||
if (!note.trim()) {
|
||||
alert('请输入审核意见');
|
||||
return;
|
||||
}
|
||||
// 在实际应用中,这里应该调用API提交审核意见
|
||||
onStatusChange(reviewPointId, status, note);
|
||||
alert(`提交重新审核意见: ${note}`);
|
||||
// 可以添加提交成功后的状态更新等操作
|
||||
};
|
||||
|
||||
const handleNoteChange = (reviewPointId: string, text: string) => {
|
||||
setManualReviewNotes(prev => ({
|
||||
...prev,
|
||||
[reviewPointId]: text
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<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>
|
||||
)}
|
||||
|
||||
|
||||
{/* 额外的文本输入框区域 */}
|
||||
<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">
|
||||
<button
|
||||
className="bg-purple-600 hover:bg-purple-700 text-xs text-white py-1 px-2 rounded-md flex items-center justify-center"
|
||||
onClick={() => handleReReview(reviewPoint.id, 'false')}
|
||||
>
|
||||
<i className="ri-refresh-line mr-1"></i> 重新审核
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// 非通过状态,显示内容和修改建议
|
||||
const isErrorStatus = reviewPoint.result === false && reviewPoint.status === 'error';
|
||||
|
||||
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{reviewPoint.content !== null && (
|
||||
@@ -443,8 +517,18 @@ export function ReviewPointsList({
|
||||
(typeof reviewPoint.content === 'object' && Object.keys(reviewPoint.content).length > 0)
|
||||
) && (
|
||||
<>
|
||||
{/* 建议内容显示区域 */}
|
||||
{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>
|
||||
)}
|
||||
|
||||
{/* 内容显示区域 */}
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
{/* 移除顶部的"当前值"标题,在每个内容项中显示 */}
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
@@ -461,7 +545,7 @@ export function ReviewPointsList({
|
||||
{value ? '' : '缺失'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left">{value || ' '}</p>
|
||||
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -475,7 +559,7 @@ export function ReviewPointsList({
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left">{reviewPoint.content || '(内容为空)'}</p>
|
||||
<p className="text-xs text-left select-text">{reviewPoint.content || '(内容为空)'}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -484,22 +568,22 @@ export function ReviewPointsList({
|
||||
{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">
|
||||
<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">{reviewPoint.legalBasis.name}</p>
|
||||
<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"><span className="font-medium">条款内容:</span>{reviewPoint.legalBasis.content}</p>
|
||||
<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">
|
||||
<ul className="list-disc pl-4 select-text">
|
||||
{reviewPoint.legalBasis.articles.map((item, index) => (
|
||||
<li key={index} className="text-xs text-left">
|
||||
<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 || ''}` :
|
||||
@@ -515,22 +599,25 @@ export function ReviewPointsList({
|
||||
)}
|
||||
|
||||
{/* 建议修改区域 */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700">建议修改为:</span>
|
||||
{/* <span className="text-green-500">符合规范</span> */}
|
||||
{/* {(reviewPoint.postAction !== 'none') && ( */}
|
||||
<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>
|
||||
<textarea
|
||||
value={suggestionTexts[reviewPoint.id] || ''}
|
||||
onChange={(e) => handleSuggestionChange(reviewPoint.id, e.target.value)}
|
||||
className="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>
|
||||
|
||||
{/* )} */}
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex space-x-2 mt-2">
|
||||
{/* 一键替换按钮 - 只有非人工审核的点或未通过的人工审核点才显示 */}
|
||||
{(!reviewPoint.needsHumanReview || (!reviewPoint.result && reviewPoint.status !== 'success')) && (
|
||||
{(reviewPoint.postAction !== 'manual') && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center"
|
||||
onClick={() => handleReplace(reviewPoint.id)}
|
||||
@@ -540,13 +627,27 @@ export function ReviewPointsList({
|
||||
)}
|
||||
|
||||
{/* 人工审核按钮 */}
|
||||
{reviewPoint.needsHumanReview && !reviewPoint.result && reviewPoint.status !== 'success' && (
|
||||
<button
|
||||
className="replace-action flex-1 justify-center human-review-request"
|
||||
onClick={() => handleEditReviewPoint(reviewPoint.id)}
|
||||
>
|
||||
<i className="ri-edit-line"></i> 人工审核
|
||||
</button>
|
||||
{reviewPoint.postAction === 'manual' && (
|
||||
<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, '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, 'reject', note);
|
||||
}}
|
||||
>
|
||||
<i className="ri-close-line mr-1"></i> 不通过
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
@@ -563,7 +664,7 @@ export function ReviewPointsList({
|
||||
return (
|
||||
<div className="mt-2">
|
||||
{/* 内容显示区域 */}
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3">
|
||||
<div className="p-2 bg-white rounded border border-gray-200 text-xs mb-3 select-text">
|
||||
{/* 隐藏顶部的"当前值"标题,在每个内容项中显示 */}
|
||||
{typeof reviewPoint.content === 'object' && reviewPoint.content !== null ? (
|
||||
// 当 content 是对象时的渲染方式
|
||||
@@ -577,7 +678,7 @@ export function ReviewPointsList({
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs text-left">{value || '(内容为空)'}</p>
|
||||
<p className="text-xs text-left select-text">{value || (value === '' ? <span className="invisible">占位符</span> : '')}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -591,7 +692,7 @@ export function ReviewPointsList({
|
||||
{isErrorStatus ? '不符合规范' : '需优化'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-xs">{reviewPoint.content || '(内容为空)'}</p>
|
||||
<p className="text-xs select-text">{reviewPoint.content || '(内容为空)'}</p>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@@ -599,8 +700,8 @@ export function ReviewPointsList({
|
||||
{/* 建议修改区域 */}
|
||||
<div className="mb-2">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<span className="text-gray-700">建议修改为:</span>
|
||||
<span className="text-green-500">符合规范</span>
|
||||
<span className="text-gray-700 text-xs">建议修改为:</span>
|
||||
{/* <span className="text-green-500 text-xs">符合规范</span> */}
|
||||
</div>
|
||||
<textarea
|
||||
value={suggestionTexts[reviewPoint.id] || ''}
|
||||
@@ -610,26 +711,26 @@ export function ReviewPointsList({
|
||||
</div>
|
||||
|
||||
{/* 审核意见区域 */}
|
||||
<div className="bg-gray-50 rounded border border-gray-200 p-2">
|
||||
<div className="bg-blue-50 rounded border border-blue-200 p-2">
|
||||
<label htmlFor="reviewNote" className="block text-xs text-gray-700 mb-1">审核意见</label>
|
||||
<textarea
|
||||
id="reviewNote"
|
||||
className="w-full border border-gray-200 rounded-md text-xs p-2 mb-2"
|
||||
placeholder="请输入审核意见(可选)..."
|
||||
rows={2}
|
||||
value={userInputText}
|
||||
onChange={(e) => setUserInputText(e.target.value)}
|
||||
value={manualReviewNotes[reviewPoint.id] || ''}
|
||||
onChange={(e) => handleManualReviewNotesChange(reviewPoint.id, e.target.value)}
|
||||
></textarea>
|
||||
<div className="flex justify-end mt-2 space-x-2">
|
||||
<button
|
||||
className="replace-action"
|
||||
onClick={() => handleReviewAction(reviewPoint.id, 'approve')}
|
||||
onClick={() => handleReviewAction(reviewPoint.id, 'approve', manualReviewNotes[reviewPoint.id] || '')}
|
||||
>
|
||||
<i className="ri-check-line"></i> 通过
|
||||
</button>
|
||||
<button
|
||||
className="replace-action status-error"
|
||||
onClick={() => handleReviewAction(reviewPoint.id, 'reject')}
|
||||
onClick={() => handleReviewAction(reviewPoint.id, 'reject', manualReviewNotes[reviewPoint.id] || '')}
|
||||
>
|
||||
<i className="ri-close-line"></i> 不通过
|
||||
</button>
|
||||
@@ -664,9 +765,22 @@ export function ReviewPointsList({
|
||||
);
|
||||
};
|
||||
|
||||
// 处理评查点点击事件
|
||||
const handleReviewPointClick = (id: string) => {
|
||||
// 找到被点击的评查点
|
||||
const reviewPoint = reviewPoints.find(point => point.id === id);
|
||||
|
||||
// 如果评查点存在并且有contentPage数组,传递第一个页码
|
||||
if (reviewPoint && reviewPoint.contentPage && reviewPoint.contentPage.length > 0) {
|
||||
onReviewPointSelect(id, reviewPoint.contentPage[0]);
|
||||
} else {
|
||||
onReviewPointSelect(id);
|
||||
}
|
||||
};
|
||||
|
||||
// 组件主渲染函数
|
||||
return (
|
||||
<div className="review-points-panel">
|
||||
<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>
|
||||
@@ -686,24 +800,24 @@ export function ReviewPointsList({
|
||||
<button
|
||||
key={reviewPoint.id}
|
||||
className={`review-point-item ${activeReviewPointId === reviewPoint.id ? 'active' : ''}`}
|
||||
onClick={() => onReviewPointSelect(reviewPoint.id)}
|
||||
onClick={() => handleReviewPointClick(reviewPoint.id)}
|
||||
type="button"
|
||||
style={{ userSelect: 'text' }}
|
||||
>
|
||||
{/* 评查点标题和状态 */}
|
||||
<div className="review-point-header flex justify-between items-start">
|
||||
<div className="review-point-title flex-1 text-left">{reviewPoint.title}</div>
|
||||
<div className="flex items-center ml-2 flex-shrink-0">
|
||||
{/* 评查点所属分组 */}
|
||||
<div className="review-point-location">
|
||||
<i className="ri-file-list-line mr-1"></i>
|
||||
<span>{reviewPoint.groupName}</span>
|
||||
</div>
|
||||
<div className="flex flex-col items-center ml-2 flex-shrink-0">
|
||||
{renderStatusBadge(reviewPoint.status, reviewPoint.result)}
|
||||
{renderHumanReviewBadge(reviewPoint)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 评查点所属分组 */}
|
||||
<div className="review-point-location">
|
||||
<i className="ri-file-list-line mr-1"></i>
|
||||
<span>{reviewPoint.groupName}</span>
|
||||
</div>
|
||||
|
||||
{/* 人工审核注释 */}
|
||||
{renderHumanReviewNote(reviewPoint)}
|
||||
|
||||
|
||||
@@ -12,8 +12,8 @@ interface ReviewTabsProps {
|
||||
|
||||
export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps) {
|
||||
return (
|
||||
<div className="tab-container">
|
||||
<div className="tab-nav">
|
||||
<div className="tab-container w-full flex-1">
|
||||
<div className="tab-nav w-full flex">
|
||||
<button
|
||||
className={`tab-nav-item ${activeTab === 'preview' ? 'active' : ''}`}
|
||||
onClick={() => onTabChange('preview')}
|
||||
@@ -40,7 +40,7 @@ export function ReviewTabs({ activeTab, onTabChange, children }: ReviewTabsProps
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-content">
|
||||
<div className="tab-content w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user