优化交叉评查详情页面
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import { postgrestPost } from "../postgrest-client";
|
||||
import { API_BASE_URL } from "../../config/api-config";
|
||||
|
||||
/**
|
||||
* 提出意见的请求参数接口
|
||||
*/
|
||||
export interface SubmitOpinionRequest {
|
||||
reviewPointResultId: string | number;
|
||||
documentId: string | number;
|
||||
auditPoint: string;
|
||||
foundIssue: string;
|
||||
auditOpinion: string;
|
||||
deductionScore: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提出意见的响应接口
|
||||
*/
|
||||
export interface SubmitOpinionResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
data?: {
|
||||
id: string | number;
|
||||
created_at: string;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 交叉评查意见数据接口
|
||||
*/
|
||||
export interface CrossCheckingOpinion {
|
||||
id: string | number;
|
||||
evaluation_point_id: string | number;
|
||||
document_id: string | number;
|
||||
audit_point: string;
|
||||
found_issue: string;
|
||||
audit_opinion: string;
|
||||
deduction_score: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* API响应格式
|
||||
*/
|
||||
export interface ApiResponse<T> {
|
||||
data?: T;
|
||||
error?: string;
|
||||
status?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交交叉评查意见
|
||||
* @param opinionData 意见数据
|
||||
* @returns 提交结果
|
||||
*/
|
||||
export async function submitCrossCheckingOpinion(
|
||||
opinionData: SubmitOpinionRequest
|
||||
): Promise<ApiResponse<SubmitOpinionResponse>> {
|
||||
try {
|
||||
const requestData = {
|
||||
proposer_user_id: 1,
|
||||
evaluation_result_id: opinionData.reviewPointResultId,
|
||||
// document_id: opinionData.documentId,
|
||||
// audit_point: opinionData.auditPoint,
|
||||
// found_issue: opinionData.foundIssue,
|
||||
proposed_score: opinionData.deductionScore,
|
||||
reason: opinionData.auditOpinion
|
||||
};
|
||||
|
||||
const response = await fetch(`${API_BASE_URL}/admin/cross_review/proposals`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(requestData)
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(data.message || '提交失败');
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
success: true,
|
||||
message: '意见提交成功',
|
||||
data: data
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('提交交叉评查意见失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '提交意见失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取交叉评查意见列表
|
||||
* @param documentId 文档ID
|
||||
* @returns 意见列表
|
||||
*/
|
||||
export async function getCrossCheckingOpinions(documentId: string | number): Promise<ApiResponse<CrossCheckingOpinion[]>> {
|
||||
try {
|
||||
const response = await postgrestPost('rpc/get_cross_checking_opinions', {
|
||||
p_document_id: documentId
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
return {
|
||||
error: response.error,
|
||||
status: response.status || 500
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
data: (response.data as CrossCheckingOpinion[]) || []
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('获取交叉评查意见失败:', error);
|
||||
return {
|
||||
error: error instanceof Error ? error.message : '获取意见列表失败',
|
||||
status: 500
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -109,6 +109,19 @@ interface ContractStructureComparison {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
// 定义评分提案数据接口
|
||||
interface ScoringProposal {
|
||||
id: string | number;
|
||||
evaluation_result_id: string | number;
|
||||
proposer_id: string | number;
|
||||
proposed_score: number;
|
||||
reason: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
document_id: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前评查文件的所有评查点结果
|
||||
* @param fileId 评查文件ID
|
||||
@@ -294,6 +307,26 @@ export async function getReviewPoints(fileId: string) {
|
||||
|
||||
// console.log('groupsMap-------', groupsMap);
|
||||
|
||||
|
||||
//从scoring_proposals表中获取评分提案数据,用于交叉评查
|
||||
const scoringProposalsParams: PostgrestParams = {
|
||||
select: '*',
|
||||
filter: {
|
||||
'document_id': `eq.${fileId}`
|
||||
}
|
||||
};
|
||||
const scoringProposalsResponse = await postgrestGet('scoring_proposals', scoringProposalsParams);
|
||||
|
||||
if (scoringProposalsResponse.error) {
|
||||
return { error: scoringProposalsResponse.error, status: scoringProposalsResponse.status };
|
||||
}
|
||||
const scoringProposalsData = extractApiData<ScoringProposal[]>(scoringProposalsResponse.data) || [];
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// 构建前端所需的数据格式
|
||||
const resultData: ReviewPointResult[] = evaluationResultsData.map(result => {
|
||||
const point = pointsMap.get(result.evaluation_point_id) || {} as EvaluationPoint;
|
||||
@@ -394,6 +427,10 @@ export async function getReviewPoints(fileId: string) {
|
||||
// 评查配置: point.evaluation_config
|
||||
evaluationConfig: point.evaluation_config || {},
|
||||
|
||||
// 评查点evaluation_point中的fail_message和pass_message 用于交叉评查的提出意见
|
||||
failMessage: point.fail_message || '',
|
||||
passMessage: point.pass_message || '',
|
||||
|
||||
evaluatedPointResultsLog: evaluatedPointResultsLog || {}
|
||||
// evaluatedPointResultsLog: {
|
||||
// rules:[
|
||||
@@ -671,8 +708,8 @@ export async function getReviewPoints(fileId: string) {
|
||||
issueCount: issueCount
|
||||
};
|
||||
// console.log("reviewInfo-------",JSON.stringify(reviewInfo,null,2));
|
||||
// data->reviewPoints stats->statistics reviewInfo->reviewInfo document->document
|
||||
return { data: resultData, stats, reviewInfo, document: documentData.data, comparison_document: comparisonDocument };
|
||||
// data->reviewPoints stats->statistics reviewInfo->reviewInfo document->document scoring_proposals->scoringProposalsData
|
||||
return { data: resultData, stats, reviewInfo, document: documentData.data, comparison_document: comparisonDocument, scoring_proposals: scoringProposalsData };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* 交叉评查文件信息组件
|
||||
*/
|
||||
|
||||
interface FileInfoProps {
|
||||
fileInfo: {
|
||||
fileName: string;
|
||||
contractNumber: string;
|
||||
fileSize?: string;
|
||||
fileFormat?: string;
|
||||
pageCount?: number;
|
||||
uploadTime?: string;
|
||||
uploadUser?: string;
|
||||
auditStatus?: number;
|
||||
path?: string;
|
||||
previousRoute?: string;
|
||||
fileType?: string;
|
||||
};
|
||||
onConfirmResults: () => void;
|
||||
}
|
||||
|
||||
export function FileInfo({ fileInfo, onConfirmResults }: FileInfoProps) {
|
||||
|
||||
const handleDownloadFile = () => {
|
||||
if (fileInfo.path) {
|
||||
// 创建一个隐藏的下载链接
|
||||
const link = document.createElement('a');
|
||||
link.href = fileInfo.path;
|
||||
link.download = fileInfo.fileName;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
} else {
|
||||
alert('文件路径不存在,无法下载');
|
||||
}
|
||||
};
|
||||
|
||||
const handleExportReport = () => {
|
||||
alert('导出交叉评查报告功能');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-4 file-info-header">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="flex-1">
|
||||
{/* 文件基本信息已在面包屑区域显示,这里可以不重复显示 */}
|
||||
</div>
|
||||
|
||||
{/* 操作按钮区域 */}
|
||||
<div className="flex items-center gap-3">
|
||||
{/* 下载原文件按钮 */}
|
||||
<button
|
||||
onClick={handleDownloadFile}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
|
||||
>
|
||||
<i className="fas fa-download mr-1.5"></i>
|
||||
下载原文件
|
||||
</button>
|
||||
|
||||
{/* 导出评查报告按钮 */}
|
||||
<button
|
||||
onClick={handleExportReport}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500"
|
||||
>
|
||||
<i className="fas fa-file-export mr-1.5"></i>
|
||||
导出评查报告
|
||||
</button>
|
||||
|
||||
{/* 确认评查结果按钮 - 只在未审核通过时显示 */}
|
||||
{fileInfo.auditStatus !== 1 && (
|
||||
<button
|
||||
onClick={onConfirmResults}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-green-600 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
|
||||
>
|
||||
<i className="fas fa-check mr-1.5"></i>
|
||||
确认评查结果
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* 已确认状态显示 */}
|
||||
{fileInfo.auditStatus === 1 && (
|
||||
<span className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-green-700 bg-green-100 border border-green-200 rounded-md">
|
||||
<i className="fas fa-check-circle mr-1.5"></i>
|
||||
评查结果已确认
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,599 @@
|
||||
/**
|
||||
* 文件预览组件
|
||||
* 显示文档内容和评查点高亮
|
||||
*/
|
||||
import { useState, useEffect, useRef, ChangeEvent } from 'react';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
import { DOCUMENT_URL } from '~/api/axios-client';
|
||||
|
||||
// 设置worker路径为public目录下的worker文件
|
||||
// 使用已经下载的兼容版本 (pdfjs-dist v2.12.313)
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js';
|
||||
|
||||
// 导入统一的ReviewPoint类型
|
||||
import { type ReviewPoint } from './';
|
||||
import { toastService } from '../ui/Toast';
|
||||
|
||||
/**
|
||||
* 自定义样式
|
||||
* 这些样式解决了PDF页面在放大时互相重叠的问题
|
||||
*/
|
||||
const styles = {
|
||||
pdfContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
position: 'relative' as const,
|
||||
},
|
||||
pageContainer: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column' as const,
|
||||
alignItems: 'center',
|
||||
width: '100%',
|
||||
position: 'relative' as const,
|
||||
}
|
||||
};
|
||||
|
||||
// 定义文档内容类型
|
||||
interface FileContent {
|
||||
title: string;
|
||||
contractNumber: string;
|
||||
path: string;
|
||||
ocrResult?: {
|
||||
__meta?: {
|
||||
page_offset?: number;
|
||||
};
|
||||
}; // 添加ocrResult属性
|
||||
parties: {
|
||||
partyA: {
|
||||
name: string;
|
||||
address: string;
|
||||
representative: string;
|
||||
phone: string;
|
||||
};
|
||||
partyB: {
|
||||
name: string;
|
||||
address: string;
|
||||
representative: string;
|
||||
phone: string;
|
||||
};
|
||||
};
|
||||
sections: {
|
||||
title: string;
|
||||
content: string;
|
||||
}[];
|
||||
template_contract_path?: string;
|
||||
}
|
||||
|
||||
interface FilePreviewProps {
|
||||
fileContent: FileContent;
|
||||
reviewPoints?: ReviewPoint[]; // 设为可选
|
||||
activeReviewPointResultId: string | null;
|
||||
targetPage?: number; // 新增目标页码参数
|
||||
isStructuredView?: boolean; // 是否显示结构化视图
|
||||
}
|
||||
|
||||
// export function FilePreview({ fileContent, reviewPoints, activeReviewPointResultId, targetPage }: FilePreviewProps) {
|
||||
export function FilePreview({ fileContent, activeReviewPointResultId, targetPage, isStructuredView = false }: 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 [pageInputValue, setPageInputValue] = useState<string>('');
|
||||
|
||||
// 拖拽状态管理
|
||||
const [dragMode, setDragMode] = useState(false); // 是否处于拖拽模式
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [dragCursor, setDragCursor] = useState('default');
|
||||
const lastMousePosRef = useRef({ x: 0, y: 0 });
|
||||
|
||||
// 放大文档
|
||||
const handleZoomIn = () => {
|
||||
if (zoomLevel < 200) {
|
||||
setZoomLevel(prevZoom => prevZoom + 10);
|
||||
}
|
||||
};
|
||||
|
||||
// 缩小文档
|
||||
const handleZoomOut = () => {
|
||||
if (zoomLevel > 50) {
|
||||
setZoomLevel(prevZoom => prevZoom - 10);
|
||||
}
|
||||
};
|
||||
|
||||
// 切换拖拽模式
|
||||
const toggleDragMode = () => {
|
||||
setDragMode(prev => !prev);
|
||||
setDragCursor(prev => prev === 'default' ? 'grab' : 'default');
|
||||
setIsDragging(false);
|
||||
};
|
||||
|
||||
// 处理拖拽开始
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!dragMode || e.button !== 0) return; // 只在拖拽模式下响应左键点击
|
||||
|
||||
// 防止选中文本
|
||||
e.preventDefault();
|
||||
|
||||
// 设置拖拽状态
|
||||
setIsDragging(true);
|
||||
setDragCursor('grabbing');
|
||||
|
||||
// 记录鼠标初始位置
|
||||
lastMousePosRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
};
|
||||
|
||||
// 处理拖拽过程
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (!dragMode || !isDragging || !contentRef.current) return;
|
||||
|
||||
// 计算鼠标移动距离
|
||||
const dx = e.clientX - lastMousePosRef.current.x;
|
||||
const dy = e.clientY - lastMousePosRef.current.y;
|
||||
|
||||
// 更新容器滚动位置
|
||||
contentRef.current.scrollLeft -= dx;
|
||||
contentRef.current.scrollTop -= dy;
|
||||
|
||||
// 更新鼠标位置记录
|
||||
lastMousePosRef.current = {
|
||||
x: e.clientX,
|
||||
y: e.clientY
|
||||
};
|
||||
};
|
||||
|
||||
// 处理拖拽结束
|
||||
const handleMouseUp = () => {
|
||||
if (!dragMode) return;
|
||||
|
||||
setIsDragging(false);
|
||||
setDragCursor('grab');
|
||||
};
|
||||
|
||||
// 监听鼠标离开窗口事件
|
||||
useEffect(() => {
|
||||
const handleMouseLeave = () => {
|
||||
if (dragMode && isDragging) {
|
||||
setIsDragging(false);
|
||||
setDragCursor('grab');
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('mouseleave', handleMouseLeave);
|
||||
document.addEventListener('mouseup', handleMouseUp as EventListener);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mouseleave', handleMouseLeave);
|
||||
document.removeEventListener('mouseup', handleMouseUp as EventListener);
|
||||
};
|
||||
}, [isDragging, dragMode]);
|
||||
|
||||
// 处理页面跳转
|
||||
const prevTargetPageRef = useRef<number | undefined>(undefined);
|
||||
useEffect(() => {
|
||||
// 调试信息:记录组件状态
|
||||
// console.log(`FilePreview更新 - isStructuredView:${isStructuredView}, targetPage:${targetPage}, activeReviewPointResultId:${activeReviewPointResultId}, numPages:${numPages}`);
|
||||
|
||||
// 如果有目标页码,并且与上次相同,提示用户
|
||||
if(targetPage && numPages && targetPage <= numPages && targetPage === prevTargetPageRef.current){
|
||||
// toastService.success(`已跳转至目标页码`);
|
||||
}
|
||||
// 如果有目标页码,并且与上次不同或activeReviewPointId变化了,则执行跳转
|
||||
if (targetPage && numPages && targetPage <= numPages) {
|
||||
// if (targetPage && numPages && targetPage <= numPages && (targetPage !== prevTargetPageRef.current || activeReviewPointResultId)) {
|
||||
prevTargetPageRef.current = targetPage;
|
||||
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);
|
||||
toastService.error("访问ocrResult时出错:" + (error instanceof Error ? error.message : '未知错误'));
|
||||
}
|
||||
|
||||
const pageElementId = `page-${newTargetPage}${isStructuredView ? '-structured' : ''}`;
|
||||
// console.log(`尝试跳转到元素ID: ${pageElementId}`);
|
||||
|
||||
const pageElement = document.getElementById(pageElementId);
|
||||
if (pageElement) {
|
||||
// console.log(`跳转到第${newTargetPage}页,对应评查点结果ID: ${activeReviewPointResultId}`);
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
} else {
|
||||
console.warn(`未找到页面元素: ${pageElementId}`);
|
||||
}
|
||||
}
|
||||
}, [targetPage, numPages, fileContent, activeReviewPointResultId, isStructuredView]);
|
||||
|
||||
// 获取评查点对应的样式类
|
||||
// const getHighlightClass = (status: string) => {
|
||||
// switch (status) {
|
||||
// case 'warning':
|
||||
// return 'warning';
|
||||
// case 'error':
|
||||
// return 'error';
|
||||
// case 'success':
|
||||
// return 'success';
|
||||
// default:
|
||||
// return 'warning';
|
||||
// }
|
||||
// };
|
||||
|
||||
// 处理页码输入变化
|
||||
const handlePageInputChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
// 只允许输入数字
|
||||
const value = e.target.value.replace(/\D/g, '');
|
||||
setPageInputValue(value);
|
||||
};
|
||||
|
||||
// 处理页码跳转
|
||||
const handlePageJump = () => {
|
||||
if (!pageInputValue || !numPages) return;
|
||||
|
||||
const targetPageNum = parseInt(pageInputValue, 10);
|
||||
|
||||
// 验证页码是否在有效范围内
|
||||
if (targetPageNum > 0 && targetPageNum <= numPages) {
|
||||
// 找到目标页面元素并滚动到该位置
|
||||
const pageElement = document.getElementById(`page-${targetPageNum}`);
|
||||
if (pageElement) {
|
||||
pageElement.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
} else {
|
||||
// 页码超出范围,显示错误信息或重置输入
|
||||
toastService.warning(`请输入有效页码 (1-${numPages})`);
|
||||
setPageInputValue('');
|
||||
}
|
||||
};
|
||||
|
||||
// 处理回车键跳转
|
||||
const handlePageInputKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
handlePageJump();
|
||||
}
|
||||
};
|
||||
|
||||
// PDF文档加载成功回调函数
|
||||
function onDocumentLoadSuccess({ numPages }: { numPages: number }) {
|
||||
setNumPages(numPages);
|
||||
// console.log("PDF加载成功,页数:", numPages);
|
||||
}
|
||||
|
||||
// 计算页面在缩放后的实际间距
|
||||
const calculatePageMargin = (zoomFactor: number) => {
|
||||
// 基础间距为30px,随着缩放倍数线性增加
|
||||
const baseMargin = 30;
|
||||
// 页面缩放后,需要额外添加的间距 = (缩放倍数 - 1) * 页面高度
|
||||
const additionalMargin = Math.max(0, (zoomFactor - 1) * 800); // 800是估计的页面高度
|
||||
return baseMargin + additionalMargin;
|
||||
};
|
||||
|
||||
// 滚动到顶部
|
||||
const handleScrollToTop = () => {
|
||||
if (contentRef.current) {
|
||||
contentRef.current.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 渲染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++) {
|
||||
// 计算当前缩放级别下的页面容器样式
|
||||
const zoomFactor = zoomLevel / 100;
|
||||
const pageContainerStyle = {
|
||||
...styles.pageContainer,
|
||||
marginBottom: `${calculatePageMargin(zoomFactor)}px`, // 动态计算页面间距
|
||||
};
|
||||
|
||||
// 为结构化视图和普通视图创建不同的ID
|
||||
const pageId = isStructuredView ? `page-${i}-structured` : `page-${i}`;
|
||||
|
||||
// 为每一页创建组件
|
||||
pages.push(
|
||||
<div key={i} id={pageId} style={pageContainerStyle}>
|
||||
{/* 页码标识,显示在页面上方 */}
|
||||
<div className="text-center text-gray-500 text-sm mb-2">第 {i} 页</div>
|
||||
|
||||
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={{
|
||||
transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
|
||||
transformOrigin: 'top center', // 缩放原点设置为顶部中心
|
||||
position: 'relative', // 相对定位,作为评查点高亮的定位参考
|
||||
display: 'inline-block', // 内联块级元素,宽度由内容决定
|
||||
margin: '0 auto', // 水平居中
|
||||
}}
|
||||
>
|
||||
{/* 渲染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 = () => {
|
||||
const real_path = fileContent.path || fileContent.template_contract_path || '';
|
||||
|
||||
// 如果路径无效,显示错误信息
|
||||
if (!real_path) {
|
||||
if(!fileContent.template_contract_path){
|
||||
return (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>无法加载文件:合同模板未上传</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>无法加载文件:路径无效</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// console.log('real_path',real_path);
|
||||
// 获取文件扩展名
|
||||
const fileExtension = real_path.split('.').pop()?.toLowerCase();
|
||||
|
||||
// PDF内容渲染
|
||||
const renderPdfContent = () => (
|
||||
<div
|
||||
style={{
|
||||
...styles.pdfContainer,
|
||||
// 当缩放大于100%时设置最小宽度,确保出现横向滚动条
|
||||
minWidth: zoomLevel > 100 ? `${zoomLevel}%` : '100%',
|
||||
overflow: 'visible'
|
||||
}}
|
||||
>
|
||||
<Document
|
||||
file={DOCUMENT_URL+real_path}
|
||||
onLoadSuccess={onDocumentLoadSuccess}
|
||||
onLoadError={(error) => {
|
||||
console.error("PDF加载错误:", error);
|
||||
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
|
||||
}}
|
||||
className="w-full"
|
||||
error={<div className="text-red-500">PDF文档加载失败,请检查链接或网络连接。</div>}
|
||||
noData={<div>无数据</div>}
|
||||
loading={<div className="text-center py-10">PDF加载中...</div>}
|
||||
>
|
||||
{renderAllPages()}
|
||||
</Document>
|
||||
</div>
|
||||
);
|
||||
|
||||
// 结构化数据渲染
|
||||
const renderStructuredData = () => (
|
||||
<div className="structured-view p-4 text-left mt-4 border-t border-gray-200">
|
||||
<div className="text-xs font-medium mb-2">结构化数据:</div>
|
||||
{fileContent.ocrResult ? (
|
||||
<div className="overflow-auto max-h-[300px]">
|
||||
<pre className="text-xs whitespace-pre-wrap bg-gray-50 p-3 rounded">
|
||||
{JSON.stringify(fileContent.ocrResult, null, 2)}
|
||||
</pre>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-gray-500 p-4 text-center">
|
||||
<p>无结构化数据可显示</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
// 根据文件类型选择不同的渲染方式
|
||||
if (fileExtension === 'pdf') {
|
||||
// 结构化视图模式:显示PDF和结构化数据
|
||||
if (isStructuredView) {
|
||||
return (
|
||||
<div>
|
||||
{renderPdfContent()}
|
||||
{renderStructuredData()}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
// 普通模式:仅显示PDF
|
||||
return renderPdfContent();
|
||||
} else {
|
||||
// 非PDF文件显示不支持消息
|
||||
return (
|
||||
<div className="text-gray-500 p-4">
|
||||
<p>暂不支持预览此类型的文件:{fileExtension}</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="file-preview">
|
||||
<div className="file-preview-header px-2 text-xs sm:text-xs md:text-sm max-w-full flex items-center justify-between min-w-0">
|
||||
<div className="flex items-center min-w-0 flex-shrink-0">
|
||||
<i className={`${isStructuredView ? 'ri-file-list-line' : 'ri-file-text-line'} text-primary mr-2 flex-shrink-0`}></i>
|
||||
<span className="font-medium text-primary truncate max-w-[120px]" title={isStructuredView ? '模板预览' : '文件预览'}>
|
||||
{isStructuredView ? '模板预览' : '文件预览'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="file-preview-actions flex items-center ml-2 min-w-0 flex-1 justify-end overflow-hidden">
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
onClick={handleScrollToTop}
|
||||
title="返回顶部"
|
||||
>
|
||||
<i className="ri-arrow-up-double-line"></i>
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[60px]">返回顶部</span>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
onClick={handleZoomIn}
|
||||
title="放大"
|
||||
>
|
||||
<i className="ri-zoom-in-line"></i>
|
||||
</button>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 flex-shrink-0"
|
||||
onClick={handleZoomOut}
|
||||
title="缩小"
|
||||
>
|
||||
<i className="ri-zoom-out-line"></i>
|
||||
</button>
|
||||
{/* 页码跳转控件 */}
|
||||
<div className="inline-flex items-center ml-2 flex-shrink-0">
|
||||
<input
|
||||
type="text"
|
||||
className="ant-input ant-input-sm py-0 px-1 text-xs max-h-6 leading-5 w-[2.5rem] text-center
|
||||
focus:outline-none focus:ring-1 focus:ring-green-900"
|
||||
placeholder="页码"
|
||||
value={pageInputValue}
|
||||
onChange={handlePageInputChange}
|
||||
onKeyDown={handlePageInputKeyDown}
|
||||
/>
|
||||
<button
|
||||
className="ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-1"
|
||||
onClick={handlePageJump}
|
||||
disabled={!numPages}
|
||||
title="跳转到页面"
|
||||
>
|
||||
<i className="ri-arrow-right-line"></i>
|
||||
</button>
|
||||
{numPages && (
|
||||
<span className="ml-1 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap">
|
||||
/ {numPages}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="ml-2 text-xs text-gray-500 hidden sm:hidden md:hidden lg:hidden xl:inline whitespace-nowrap flex-shrink-0">
|
||||
比例:{zoomLevel}%
|
||||
</span>
|
||||
<button
|
||||
className={`ant-btn ant-btn-sm ant-btn-default py-0 px-1 text-xs max-h-6 leading-5 ml-2 flex-shrink-0 ${dragMode ? 'active bg-green-300' : ''}`}
|
||||
title="切换拖拽模式"
|
||||
aria-pressed={dragMode}
|
||||
onClick={toggleDragMode}
|
||||
>
|
||||
<i className="ri-drag-move-line"></i>
|
||||
<span className="ml-1 hidden sm:hidden md:hidden lg:hidden xl:inline truncate max-w-[80px]">
|
||||
拖拽模式{dragMode ? '(已激活)' : ''}
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="file-preview-content"
|
||||
ref={contentRef}
|
||||
style={{
|
||||
maxHeight: 'calc(100vh - 80px)',
|
||||
overflowY: 'auto',
|
||||
overflowX: 'auto',
|
||||
cursor: dragCursor,
|
||||
userSelect: dragMode ? 'none' : 'auto', // 拖拽模式下防止文本被选中
|
||||
}}
|
||||
>
|
||||
<button
|
||||
className="pdf-interactive-container"
|
||||
aria-label="PDF文档查看区域"
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onKeyDown={(e) => {
|
||||
// 添加键盘导航支持
|
||||
const scrollAmount = 50;
|
||||
if (e.key === 'ArrowUp') {
|
||||
if (contentRef.current) contentRef.current.scrollTop -= scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowDown') {
|
||||
if (contentRef.current) contentRef.current.scrollTop += scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
if (contentRef.current) contentRef.current.scrollLeft -= scrollAmount;
|
||||
e.preventDefault();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
if (contentRef.current) contentRef.current.scrollLeft += scrollAmount;
|
||||
e.preventDefault();
|
||||
}
|
||||
}}
|
||||
style={{
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
display: 'block',
|
||||
border: 'none',
|
||||
background: 'transparent',
|
||||
textAlign: 'center',
|
||||
padding: 0
|
||||
}}
|
||||
>
|
||||
{loadError ? (
|
||||
<div className="text-red-500 p-4">
|
||||
<p>{loadError}</p>
|
||||
</div>
|
||||
) : (
|
||||
renderDocumentContent()
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* 交叉评查详情组件导出文件
|
||||
*/
|
||||
|
||||
export { FileInfo } from './FileInfo';
|
||||
export { FilePreview } from './FilePreview';
|
||||
export { ReviewPointsList } from './ReviewPointsList';
|
||||
export type { ReviewPoint } from './ReviewPointsList';
|
||||
@@ -222,7 +222,7 @@ export default function CrossCheckingIndex() {
|
||||
type="default"
|
||||
size="small"
|
||||
className="operation-btn secondary"
|
||||
onClick={() => navigate(`/cross-checking/${task.id}/results`)}
|
||||
onClick={() => navigate(`/cross-checking/result?id=1355&previousRoute=cross-checking`)}
|
||||
>
|
||||
<i className="ri-file-text-line"></i>
|
||||
查看结果
|
||||
@@ -478,7 +478,6 @@ export default function CrossCheckingIndex() {
|
||||
name="docType"
|
||||
value={searchParams.get('docType') || ''}
|
||||
options={[
|
||||
{ value: "", label: "全部类型" },
|
||||
{ value: CrossCheckingDocType.PENALTY, label: "行政处罚" },
|
||||
{ value: CrossCheckingDocType.PERMIT, label: "行政许可" }
|
||||
]}
|
||||
|
||||
@@ -0,0 +1,733 @@
|
||||
/**
|
||||
* 交叉评查详情页面
|
||||
*
|
||||
* 功能概述:
|
||||
* - 显示文档交叉评查结果和详细信息
|
||||
* - 支持查看文档内容及评查点高亮标记
|
||||
* - 提供评查点列表,分为通过、警告和错误三种类型
|
||||
* - 支持评查点处理,如一键替换、人工审核等功能
|
||||
* - 支持导出评查报告和下载原文件
|
||||
*
|
||||
* 组件结构:
|
||||
* - FileInfo: 显示文件基本信息和操作按钮
|
||||
* - FilePreview: 文档预览组件,显示文档内容及高亮问题
|
||||
* - ReviewPointsList: 评查点列表组件,显示所有评查结果
|
||||
*
|
||||
* 数据流转:
|
||||
* 1. 页面加载时从API获取评查详情数据
|
||||
* 2. 根据评查点ID关联文档中的高亮区域
|
||||
* 3. 点击评查点时在文档中定位对应位置
|
||||
* 4. 处理评查点时更新状态并反馈到UI
|
||||
*
|
||||
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
||||
*/
|
||||
|
||||
import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
|
||||
import { useState, useEffect } from "react";
|
||||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||||
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||||
import { getReviewPoints, updateReviewResult, confirmReviewResults } from "~/api/evaluation_points/reviews";
|
||||
import { toastService } from "~/components/ui/Toast";
|
||||
|
||||
// 导入交叉评查详情页面组件
|
||||
import {
|
||||
FileInfo,
|
||||
FilePreview,
|
||||
ReviewPointsList
|
||||
} from "~/components/cross-checking";
|
||||
|
||||
// 从ReviewPointsList组件中导入ReviewPoint类型
|
||||
import { type ReviewPoint } from '~/components/cross-checking';
|
||||
import { messageService } from "~/components/ui/MessageModal";
|
||||
import { loadingBarService } from "~/components/ui/LoadingBar";
|
||||
import { Breadcrumb } from "~/components/layout/Breadcrumb";
|
||||
|
||||
// 定义评分提案数据接口(与reviews.ts中保持一致)
|
||||
interface ScoringProposal {
|
||||
id: string | number;
|
||||
evaluation_result_id: string | number;
|
||||
proposer_id: string | number;
|
||||
proposed_score: number;
|
||||
reason: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
document_id: string | number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件信息组件
|
||||
* 显示文件名称、状态信息以及操作按钮(下载原文件、导出评查报告、确认评查结果)
|
||||
*/
|
||||
// 格式化文件大小
|
||||
const formatFileSize = (bytes: number) => {
|
||||
if (bytes === 0) return "0 Bytes";
|
||||
const k = 1024;
|
||||
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||||
};
|
||||
|
||||
// 定义统计数据类型
|
||||
interface Statistics {
|
||||
total: number;
|
||||
success: number;
|
||||
warning: number;
|
||||
error: number;
|
||||
score: number;
|
||||
}
|
||||
|
||||
// 定义文件信息类型
|
||||
interface FileInfo {
|
||||
fileName: string;
|
||||
contractNumber: string;
|
||||
fileSize: string;
|
||||
fileFormat: string;
|
||||
pageCount: number;
|
||||
uploadTime: string;
|
||||
uploadUser: string;
|
||||
auditStatus: number;
|
||||
fileType: string; // 文件类型(1:合同,2:卷宗等)
|
||||
}
|
||||
|
||||
// 定义合同信息类型
|
||||
interface ContractInfo {
|
||||
contractType: string;
|
||||
signDate: string;
|
||||
parties: {
|
||||
partyA: string;
|
||||
partyB: string;
|
||||
};
|
||||
amount: string;
|
||||
period: string;
|
||||
}
|
||||
|
||||
// 定义评查信息类型
|
||||
interface ReviewInfo {
|
||||
reviewTime: string;
|
||||
reviewModel: string;
|
||||
ruleGroup: string;
|
||||
result: string;
|
||||
issueCount: number;
|
||||
}
|
||||
|
||||
// 定义文档内容类型
|
||||
interface FileContent {
|
||||
title: string;
|
||||
contractNumber: string;
|
||||
parties: {
|
||||
partyA: {
|
||||
name: string;
|
||||
address: string;
|
||||
representative: string;
|
||||
phone: string;
|
||||
};
|
||||
partyB: {
|
||||
name: string;
|
||||
address: string;
|
||||
representative: string;
|
||||
phone: string;
|
||||
};
|
||||
};
|
||||
sections: {
|
||||
title: string;
|
||||
content: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// 定义分析项类型
|
||||
interface AnalysisItem {
|
||||
title: string;
|
||||
content: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
// 定义分析数据类型
|
||||
interface AnalysisData {
|
||||
riskAlerts: AnalysisItem[];
|
||||
suggestions: AnalysisItem[];
|
||||
summary: string;
|
||||
}
|
||||
|
||||
// 定义评查数据类型
|
||||
interface ReviewData {
|
||||
fileInfo: FileInfo;
|
||||
contractInfo: ContractInfo;
|
||||
reviewInfo: ReviewInfo;
|
||||
statistics: Statistics;
|
||||
fileContent: FileContent;
|
||||
reviewPoints: ReviewPoint[];
|
||||
aiAnalysis: AnalysisData;
|
||||
}
|
||||
|
||||
|
||||
export const meta: MetaFunction = () => {
|
||||
return [
|
||||
{ title: "交叉评查详情 - 中国烟草AI合同及卷宗审核系统" },
|
||||
{
|
||||
name: "description",
|
||||
content: "查看文档交叉评查结果,处理问题点,确认评查结果"
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
export function links() {
|
||||
return [{ rel: "stylesheet", href: crossCheckingStyles }];
|
||||
}
|
||||
|
||||
export const handle = {
|
||||
hideBreadcrumb: true
|
||||
};
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const url = new URL(request.url);
|
||||
const id = url.searchParams.get('id') || undefined;
|
||||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||||
// console.log("id-------",id);
|
||||
if (!id) {
|
||||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||||
}
|
||||
|
||||
// 获取评查点数据
|
||||
const reviewData = await getReviewPoints(id);
|
||||
|
||||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||||
if ('error' in reviewData && reviewData.error) {
|
||||
console.error("获取评查点数据错误:", reviewData.error);
|
||||
return Response.json({ result: false, message: reviewData.error });
|
||||
}
|
||||
|
||||
// 确保reviewData有效且具有预期的属性
|
||||
if ('document' in reviewData && 'data' in reviewData && 'reviewInfo' in reviewData && 'stats' in reviewData) {
|
||||
// console.log("reviewData-------",JSON.stringify(reviewData.data));
|
||||
return Response.json({
|
||||
previousRoute: previousRoute,
|
||||
document: reviewData.document,
|
||||
reviewPoints: reviewData.data,
|
||||
reviewInfo: reviewData.reviewInfo,
|
||||
statistics: reviewData.stats,
|
||||
comparison_document: reviewData.comparison_document,
|
||||
scoring_proposals: reviewData.scoring_proposals || []
|
||||
});
|
||||
} else {
|
||||
console.error("返回的评查数据格式不正确",JSON.stringify(reviewData,null,2));
|
||||
return Response.json({ result: false, message: '返回的评查数据格式不正确' });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取评查数据失败:', error);
|
||||
return Response.json({ result: false, message: '获取评查数据失败' });
|
||||
}
|
||||
}
|
||||
|
||||
export default function CrossCheckingResult() {
|
||||
const navigate = useNavigate();
|
||||
const loaderData = useLoaderData<typeof loader>();
|
||||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals } = loaderData;
|
||||
const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态
|
||||
const [reviewData, setReviewData] = useState<ReviewData | null>(null);
|
||||
const [activeReviewPointResultId, setActiveReviewPointResultId] = useState<string | null>(null);
|
||||
const [targetPage, setTargetPage] = useState<number | undefined>(undefined);
|
||||
|
||||
// loader 数据加载出错
|
||||
useEffect(()=>{
|
||||
loadingBarService.hide();
|
||||
if(Object.keys(loaderData).find(key => key === 'result') && !loaderData.result){
|
||||
messageService.show({
|
||||
title: '错误',
|
||||
message: loaderData.message,
|
||||
type: 'error',
|
||||
confirmText: '确定',
|
||||
cancelText: '',
|
||||
onConfirm: () => {
|
||||
navigate(-1);
|
||||
}
|
||||
})
|
||||
}
|
||||
},[loaderData, navigate]);
|
||||
|
||||
|
||||
// 模拟获取评查数据
|
||||
useEffect(() => {
|
||||
if (!document) return;
|
||||
|
||||
// 构建文件信息对象
|
||||
const fileInfo = {
|
||||
fileName: document.name || "未知文件名",
|
||||
path: document.path || "未知路径",
|
||||
contractNumber: document.documentNumber || "未知编号",
|
||||
fileSize: document.size ? formatFileSize(document.size) : "未知大小",
|
||||
// 文件格式类型
|
||||
fileFormat: document.fileType ? document.fileType.toUpperCase() : "未知格式",
|
||||
pageCount: document.pageCount || 0,
|
||||
uploadTime: document.uploadTime || "未知时间",
|
||||
uploadUser: document.uploadUser || "未知用户",
|
||||
auditStatus: document.auditStatus || 0,
|
||||
legalBasis: document.legalBasis || {},
|
||||
// 文件类型(1:合同,2:卷宗。。。)
|
||||
fileType: document.type || ""
|
||||
};
|
||||
|
||||
// 创建包含真实文档数据的评查数据对象
|
||||
const reviewDataObj: ReviewData = {
|
||||
// 使用真实文件信息
|
||||
fileInfo: fileInfo,
|
||||
// 其他字段暂时使用默认值
|
||||
contractInfo: getMockReviewData().contractInfo,
|
||||
reviewInfo: reviewInfo,
|
||||
statistics: statistics,
|
||||
fileContent: getMockReviewData().fileContent,
|
||||
reviewPoints: reviewPoints,
|
||||
aiAnalysis: getMockReviewData().aiAnalysis,
|
||||
};
|
||||
|
||||
|
||||
setReviewData(reviewDataObj);
|
||||
setIsLoading(false);
|
||||
}, [document, reviewPoints, statistics, reviewInfo]);
|
||||
|
||||
|
||||
const handleReviewPointSelect = (reviewPointId: string, page?: number) => {
|
||||
// 如果点击的是相同的评查点,但有page参数,先重置targetPage以确保useEffect能够触发
|
||||
if (reviewPointId === activeReviewPointResultId && page) {
|
||||
setTargetPage(undefined);
|
||||
// 使用setTimeout确保状态更新后再设置新的targetPage
|
||||
setTimeout(() => {
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
}, 0);
|
||||
} else {
|
||||
// 正常设置activeReviewPointId和targetPage
|
||||
setActiveReviewPointResultId(reviewPointId);
|
||||
setTargetPage(page);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 处理评审点状态变更
|
||||
const handleReviewPointStatusChange = async (reviewPointResultId: string, editAuditStatusId: string | number, newStatus: string, message: string) => {
|
||||
// 将字符串的布尔值转换为布尔类型
|
||||
let boolResult = 'review';
|
||||
if(newStatus !== 'review'){
|
||||
boolResult = newStatus === 'true' ? 'true' : 'false';
|
||||
}
|
||||
|
||||
try {
|
||||
// 调用 API 更新评查结果
|
||||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, boolResult, message);
|
||||
|
||||
if (response.error) {
|
||||
console.error('更新评查结果失败:', response.error);
|
||||
toastService.error(`更新评查结果失败: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// console.log('评查点状态更新成功:', {
|
||||
// id: reviewPointResultId,
|
||||
// result: boolResult,
|
||||
// message: message
|
||||
// });
|
||||
|
||||
// 更新本地状态
|
||||
if (reviewData) {
|
||||
// 找到要更新的评查点和它的原始状态
|
||||
const reviewPointToUpdate = reviewData.reviewPoints.find(point => point.id === reviewPointResultId);
|
||||
const oldStatus = reviewPointToUpdate?.status || '';
|
||||
const wasSuccess = reviewPointToUpdate?.result === true;
|
||||
const newIsSuccess = newStatus === 'true';
|
||||
|
||||
// 更新评查点
|
||||
const updatedReviewPoints = reviewData.reviewPoints.map(point =>
|
||||
point.id === reviewPointResultId ? {
|
||||
...point,
|
||||
result: newStatus === 'true' ? true : (newStatus === 'false' ? false : point.result),
|
||||
editAuditStatus: boolResult === 'review' ? 0 : 1,
|
||||
title: boolResult === 'review' ? point.title : message,
|
||||
editAuditStatusMessage: boolResult === 'review' ? point.editAuditStatusMessage : message
|
||||
} : point
|
||||
);
|
||||
|
||||
// 更新统计数据
|
||||
const updatedStatistics = { ...reviewData.statistics };
|
||||
|
||||
// 只处理结果实际变化的情况,即从通过变为不通过,或从不通过变为通过
|
||||
if (newStatus !== 'review' && wasSuccess !== newIsSuccess) {
|
||||
if (newIsSuccess) {
|
||||
// 从不通过变为通过
|
||||
updatedStatistics.success += 1;
|
||||
// 减少对应的错误或警告数量
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning = Math.max(0, updatedStatistics.warning - 1);
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error = Math.max(0, updatedStatistics.error - 1);
|
||||
}
|
||||
} else {
|
||||
// 从通过变为不通过
|
||||
updatedStatistics.success = Math.max(0, updatedStatistics.success - 1);
|
||||
// 增加对应的错误或警告数量
|
||||
if (oldStatus === 'warning') {
|
||||
updatedStatistics.warning += 1;
|
||||
} else if (oldStatus === 'error') {
|
||||
updatedStatistics.error += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新 UI 状态
|
||||
setReviewData({
|
||||
...reviewData,
|
||||
reviewPoints: updatedReviewPoints,
|
||||
statistics: updatedStatistics
|
||||
});
|
||||
|
||||
// 显示成功消息
|
||||
if(document && document.id && newStatus !== 'review'){
|
||||
toastService.success('评查点状态已更新');
|
||||
}
|
||||
|
||||
// console.log("newReviewPoints",updatedReviewPoints);
|
||||
|
||||
// 如果是review操作才调用API刷新
|
||||
// if (document && document.id && newStatus === 'review') {
|
||||
// await refreshReviewData(document.id.toString());
|
||||
// }
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新评查结果出错:', error);
|
||||
toastService.error('更新评查结果失败,请稍后重试');
|
||||
}
|
||||
};
|
||||
|
||||
const handleConfirmResults = async () => {
|
||||
if (!document || !document.id) {
|
||||
toastService.error('文档数据不完整,无法确认评查结果');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 显示加载状态
|
||||
setIsLoading(true);
|
||||
|
||||
// 调用API确认评查结果
|
||||
const response = await confirmReviewResults(document.id.toString());
|
||||
|
||||
if (response.error) {
|
||||
console.error('确认评查结果失败:', response.error);
|
||||
toastService.error(`确认评查结果失败: ${response.error}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示成功消息
|
||||
toastService.success('评查结果已确认,文档审核状态已更新');
|
||||
|
||||
// 导航到交叉评查列表页
|
||||
navigate('/cross-checking');
|
||||
} catch (error) {
|
||||
console.error('确认评查结果出错:', error);
|
||||
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建自定义面包屑项
|
||||
const getBreadcrumbItems = () => {
|
||||
const items = [
|
||||
{ title: "交叉评查详情", to: `/cross-checking/result?id=${document?.id}` }
|
||||
];
|
||||
|
||||
// 添加前置路由
|
||||
if (loaderData.previousRoute) {
|
||||
if (loaderData.previousRoute === 'cross-checking') {
|
||||
items.unshift({ title: "交叉评查", to: "/cross-checking" });
|
||||
}
|
||||
}
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="cross-checking-result-container">
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center items-center p-12">
|
||||
<div className="loading-spinner"></div>
|
||||
<span className="ml-3">加载中...</span>
|
||||
</div>
|
||||
) : reviewData && (
|
||||
<>
|
||||
{/* 自定义面包屑 */}
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Breadcrumb
|
||||
items={getBreadcrumbItems()}
|
||||
className="items-center flex !mb-0"
|
||||
/>
|
||||
|
||||
{/* 在面包屑右侧显示精简版的FileInfo */}
|
||||
<div className=" ml-10 text-left flex-1 flex flex-row flex-wrap">
|
||||
<span className="mr-2 text-xl font-medium">
|
||||
{reviewData.fileInfo.fileName}
|
||||
</span>
|
||||
<div className="text-xs text-gray-500 flex items-center">
|
||||
{/* 合同编号:{reviewData.fileInfo.contractNumber} */}
|
||||
{ reviewData.fileInfo.fileType != "1" ? "卷宗" : "合同" }
|
||||
编号:{reviewData.fileInfo.contractNumber}
|
||||
{reviewData.fileInfo.fileSize && (
|
||||
<span className="text-xs text-gray-500 ml-2">
|
||||
| {reviewData.fileInfo.fileSize} | {reviewData.fileInfo.fileFormat} | {reviewData.fileInfo.pageCount}页
|
||||
</span>
|
||||
)}
|
||||
{reviewData.fileInfo.uploadTime && (
|
||||
<div className="text-xs text-gray-500">
|
||||
| 上传时间:{reviewData.fileInfo.uploadTime}
|
||||
{/* | 上传用户:{reviewData.fileInfo.uploadUser} */}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 完成评查按钮 */}
|
||||
<button
|
||||
onClick={handleConfirmResults}
|
||||
className="inline-flex items-center px-3 py-1.5 text-sm font-medium text-white bg-green-800 border border-transparent rounded-md hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-800"
|
||||
>
|
||||
<i className="ri-check-double-line mr-1.5"></i>
|
||||
完成评查
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* 文件信息和操作按钮 */}
|
||||
{/* <FileInfo
|
||||
fileInfo={{
|
||||
...reviewData.fileInfo,
|
||||
previousRoute: loaderData.previousRoute
|
||||
}}
|
||||
onConfirmResults={handleConfirmResults}
|
||||
/> */}
|
||||
|
||||
{/* 交叉评查结果内容 */}
|
||||
<div className="flex flex-col lg:flex-row space-y-4 lg:space-y-0 lg:space-x-4 lg:justify-between">
|
||||
{/* 左侧:文件预览 */}
|
||||
<div className="w-full lg:w-[62%]">
|
||||
<FilePreview
|
||||
fileContent={document}
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
targetPage={targetPage}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 右侧:评查结果 */}
|
||||
<div className="w-full lg:w-[35%]">
|
||||
<ReviewPointsList
|
||||
reviewPoints={reviewData.reviewPoints}
|
||||
statistics={reviewData.statistics}
|
||||
activeReviewPointResultId={activeReviewPointResultId}
|
||||
onReviewPointSelect={handleReviewPointSelect}
|
||||
onStatusChange={handleReviewPointStatusChange}
|
||||
scoringProposals={scoring_proposals as ScoringProposal[]}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 模拟评查数据
|
||||
function getMockReviewData(): ReviewData {
|
||||
return {
|
||||
fileInfo: {
|
||||
fileName: "烟草产品销售合同(2023版).docx",
|
||||
contractNumber: "XS-2023-1025-001",
|
||||
fileSize: "5.2MB",
|
||||
fileFormat: "DOCX",
|
||||
pageCount: 5,
|
||||
uploadTime: "2023-10-25 14:30:45",
|
||||
uploadUser: "张三",
|
||||
auditStatus: 0,
|
||||
fileType: "1"
|
||||
},
|
||||
contractInfo: {
|
||||
contractType: "销售合同",
|
||||
signDate: "2023年10月20日",
|
||||
parties: {
|
||||
partyA: "XX烟草公司",
|
||||
partyB: "YY贸易有限公司"
|
||||
},
|
||||
amount: "¥ 1,580,000.00",
|
||||
period: "2023年11月1日至2024年10月31日"
|
||||
},
|
||||
reviewInfo: {
|
||||
reviewTime: "2023-10-25 14:35:12",
|
||||
reviewModel: "DeepSeek",
|
||||
ruleGroup: "合同标准规则组",
|
||||
result: "warning",
|
||||
issueCount: 9
|
||||
},
|
||||
statistics: {
|
||||
total: 15,
|
||||
success: 6,
|
||||
warning: 7,
|
||||
error: 2,
|
||||
score: 75
|
||||
},
|
||||
fileContent: {
|
||||
title: "烟草产品销售合同",
|
||||
contractNumber: "XS-2023-1025-001",
|
||||
parties: {
|
||||
partyA: {
|
||||
name: "XX烟草公司",
|
||||
address: "XX省XX市XX区XX路XX号",
|
||||
representative: "张XX",
|
||||
phone: "123-4567-8901"
|
||||
},
|
||||
partyB: {
|
||||
name: "YY贸易有限公司",
|
||||
address: "XX省XX市XX区YY路YY号",
|
||||
representative: "李YY",
|
||||
phone: "123-4567-8902"
|
||||
}
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
title: "总则",
|
||||
content: "1.1 本合同适用于甲乙双方之间的烟草制品买卖事宜。\n1.2 双方应本着平等互利、诚实信用的原则履行本合同。"
|
||||
},
|
||||
{
|
||||
title: "合同标的物",
|
||||
content: "2.1 产品名称:烟草制品\n2.2 规格型号:如附件所列\n2.3 数量:5000箱\n2.4 质量要求:符合国家标准GB/T XXXXX-XXXX"
|
||||
},
|
||||
{
|
||||
title: "交货与付款",
|
||||
content: "3.1 交货时间:自合同签订之日起30日内。\n3.2 乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。\n3.3 交货地点:乙方指定的仓库。\n3.4 运输方式:陆运,运费由甲方承担。"
|
||||
},
|
||||
{
|
||||
title: "合同文本",
|
||||
content: "本合同一式两份,甲乙双方各执一份,具有同等法律效力。"
|
||||
}
|
||||
]
|
||||
},
|
||||
reviewPoints: [
|
||||
{
|
||||
id: "1",
|
||||
pointName: "付款条款",
|
||||
title: "付款条件描述不明确",
|
||||
groupName: "付款条款清晰性",
|
||||
// location: "交货与付款条款",
|
||||
status: "error",
|
||||
editAuditStatus: 0,
|
||||
content: {
|
||||
'anjia':{
|
||||
page: 1,
|
||||
value: { text: "乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。" }
|
||||
},
|
||||
'yijia':{
|
||||
page: 1,
|
||||
value: { text: "乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。" }
|
||||
}
|
||||
},
|
||||
suggestion: "乙方应在收到货物验收合格之日起5个工作日内支付合同总额的70%,甲方收到该部分款项后3个工作日内向乙方开具等额增值税专用发票;乙方应在收到发票之日起5个工作日内支付剩余30%款项。",
|
||||
position: { section: "交货与付款", index: 2 },
|
||||
result: false
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
pointName: "违约责任",
|
||||
title: "违约责任条款缺失",
|
||||
groupName: "合同权利义务对等性",
|
||||
status: "warning",
|
||||
editAuditStatus: 0,
|
||||
content: {
|
||||
'clause': {
|
||||
page: 1,
|
||||
value: { text: "如合同发生纠纷,双方应协商解决。" }
|
||||
}
|
||||
},
|
||||
suggestion: "如合同发生纠纷,双方应友好协商解决;协商不成的,任何一方均有权向甲方所在地人民法院提起诉讼。任何一方未能履行本合同约定义务,应向守约方支付合同总金额的10%作为违约金;给对方造成损失的,还应赔偿由此产生的全部损失。",
|
||||
position: { section: "争议解决", index: 0 },
|
||||
result: false
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
pointName: "签章审核",
|
||||
title: "签章不完整",
|
||||
groupName: "合同签署规范性",
|
||||
status: "warning",
|
||||
editAuditStatus: 0,
|
||||
content: {
|
||||
'signature': {
|
||||
page: 5,
|
||||
value: { text: "乙方(盖章):YY贸易有限公司\n代表人签字:李YY\n日期:2023年10月20日" }
|
||||
}
|
||||
},
|
||||
suggestion: "需要联系甲方补充公章",
|
||||
needsHumanReview: true,
|
||||
humanReviewNote: "需要联系甲方补充公章",
|
||||
position: { section: "签章", index: 0 },
|
||||
result: false
|
||||
},
|
||||
{
|
||||
id: "9",
|
||||
pointName: "交货方式",
|
||||
title: "交货方式描述模糊",
|
||||
groupName: "履行条款明确性",
|
||||
status: "success",
|
||||
editAuditStatus: 0,
|
||||
content: {
|
||||
'delivery': {
|
||||
page: 3,
|
||||
value: { text: "3.4 运输方式:陆运,运费由甲方承担。" }
|
||||
}
|
||||
},
|
||||
suggestion: "建议补充具体的运输方式和时间",
|
||||
needsHumanReview: true,
|
||||
humanReviewNote: "经核实,该交货方式虽然描述不够详细,但符合行业惯例且双方已经多次合作,不会造成实际履行障碍。",
|
||||
humanReviewBy: "王法务",
|
||||
humanReviewTime: "2023-11-05 14:30:22",
|
||||
position: { section: "交货与付款", index: 4 },
|
||||
result: true
|
||||
},
|
||||
{
|
||||
id: "10",
|
||||
pointName: "法律适用",
|
||||
title: "法律适用条款缺失",
|
||||
groupName: "争议解决条款完整性",
|
||||
status: "error",
|
||||
editAuditStatus: 0,
|
||||
content: {
|
||||
'missing': {
|
||||
page: 0,
|
||||
value: { text: "" }
|
||||
}
|
||||
},
|
||||
suggestion: "第十三条 法律适用\n本合同的订立、效力、解释、履行及争议的解决均适用中华人民共和国法律。因本合同引起的或与本合同有关的任何争议,双方应友好协商解决。协商不成的,提交甲方所在地人民法院诉讼解决。",
|
||||
position: { section: "缺失", index: 0 },
|
||||
result: false
|
||||
}
|
||||
],
|
||||
aiAnalysis: {
|
||||
riskAlerts: [
|
||||
{
|
||||
title: "风险提示",
|
||||
content: "本合同缺少违约责任条款,可能导致权责不明。",
|
||||
description: "根据《中华人民共和国民法典》第五百七十七条规定,建议增加违约责任条款,明确双方违约责任及赔偿方式。"
|
||||
},
|
||||
{
|
||||
title: "完整性检查",
|
||||
content: "本合同缺少法律适用条款。",
|
||||
description: "根据行业惯例,销售合同应明确约定适用法律和纠纷解决方式,以避免后续争议解决时的不确定性。"
|
||||
}
|
||||
],
|
||||
suggestions: [
|
||||
{
|
||||
title: "优化建议",
|
||||
content: "建议完善付款条件描述。",
|
||||
description: "目前合同中关于付款条件的描述存在歧义,可能导致付款时间和条件不明确。建议按系统修改建议优化。"
|
||||
}
|
||||
],
|
||||
summary: "本合同基本结构完整,主体内容清晰,但存在多处条款描述不完善的问题,主要体现在支付条件、违约责任、不可抗力、保密条款、合同终止条件等方面。这些问题虽不影响合同的基本合规性,但可能在合同履行过程中引发争议和纠纷。同时,合同签章不完整,也影响了合同的法律效力。建议对上述问题进行修改完善后再行签署。"
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
:root {
|
||||
--primary-color: #00684a;
|
||||
--primary-hover: #005a40;
|
||||
--primary-light: rgba(0, 104, 74, 0.1);
|
||||
--success-color: #52c41a;
|
||||
--warning-color: #faad14;
|
||||
--error-color: #ff4d4f;
|
||||
--text-color: rgba(0, 0, 0, 0.85);
|
||||
--text-secondary: rgba(0, 0, 0, 0.45);
|
||||
--border-color: #f0f0f0;
|
||||
--bg-gray: #f5f5f5;
|
||||
}
|
||||
|
||||
.cross-checking-result-container {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.cross-checking-result-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 文件信息和操作按钮区域 */
|
||||
.file-info-header {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
/* 文件预览区域 */
|
||||
.file-preview {
|
||||
background-color: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
height: calc(100vh - 80px);
|
||||
min-height: 500px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.file-preview-header {
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--primary-light);
|
||||
height: 36px;
|
||||
}
|
||||
|
||||
.file-preview-content {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
max-width: 100%;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* PDF文档样式约束 */
|
||||
.react-pdf__Document {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.react-pdf__Page {
|
||||
max-width: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.react-pdf__Page__canvas {
|
||||
max-width: 100%;
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.file-preview-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 文档内容样式 */
|
||||
.word-document {
|
||||
padding: 40px;
|
||||
background-color: white;
|
||||
font-family: 'SimSun', serif;
|
||||
line-height: 1.8;
|
||||
}
|
||||
|
||||
.word-document h1 {
|
||||
font-size: 18px;
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.word-document h2 {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.word-document p {
|
||||
text-indent: 2em;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
/* 高亮区域 */
|
||||
.highlight-area {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: 2px;
|
||||
margin: -2px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
|
||||
.highlight-area:hover {
|
||||
box-shadow: 0 0 0 3px rgba(250, 173, 20, 0.6);
|
||||
}
|
||||
|
||||
.highlight-area.warning {
|
||||
background-color: rgba(250, 173, 20, 0.2);
|
||||
border: 2px solid var(--warning-color);
|
||||
}
|
||||
|
||||
.highlight-area.error {
|
||||
background-color: rgba(255, 77, 79, 0.2);
|
||||
border: 2px solid var(--error-color);
|
||||
}
|
||||
|
||||
.highlight-area.success {
|
||||
background-color: rgba(82, 196, 26, 0.2);
|
||||
border: 2px solid var(--success-color);
|
||||
}
|
||||
|
||||
/* 评查点列表区域 */
|
||||
.review-points-panel {
|
||||
background-color: white;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 6px;
|
||||
height: calc(100vh - 80px);
|
||||
min-height: 500px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.review-panel-header {
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: var(--primary-light);
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.review-statistics {
|
||||
padding: 16px;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.review-points-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.review-point-item {
|
||||
padding: 5px 10px 10px 10px;
|
||||
cursor: pointer;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.review-point-item:hover {
|
||||
box-shadow: 1px 4px 10px rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.review-point-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.text-error {
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.text-warning {
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
.review-point-title {
|
||||
text-align: left;
|
||||
font-weight: 500;
|
||||
font-size: 13px;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.review-point-location {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
padding: 2px 8px;
|
||||
width: fit-content;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
/* 状态标签 */
|
||||
.status-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.status-waiting {
|
||||
background-color: #f9f0ff;
|
||||
color: #722ed1;
|
||||
}
|
||||
|
||||
.status-processing {
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
.status-success {
|
||||
background-color: #e7ffcd;
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
.status-error {
|
||||
background-color: #fff1f0;
|
||||
color: #f5222d;
|
||||
}
|
||||
|
||||
.status-warning {
|
||||
background-color: #fff2aec7;
|
||||
color: #faad14;
|
||||
}
|
||||
|
||||
/* 替换动作按钮 */
|
||||
.replace-action {
|
||||
background-color: #1890ff;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
cursor: pointer;
|
||||
margin-top: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.replace-action i {
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.replace-action:hover {
|
||||
background-color: #40a9ff;
|
||||
}
|
||||
|
||||
/* 人工审核标记 */
|
||||
.human-review-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background-color: #e6f7ff;
|
||||
color: #1890ff;
|
||||
padding: 1px 4px;
|
||||
border-radius: 4px;
|
||||
font-size: 11px;
|
||||
margin-left: 4px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.human-review-note {
|
||||
background-color: #e6f7ff;
|
||||
border-left: 3px solid #1890ff;
|
||||
padding: 6px 10px;
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
color: #333;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
/* 文件详情页面 */
|
||||
.info-section {
|
||||
margin-bottom: 24px;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.info-header {
|
||||
padding: 12px 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-content {
|
||||
padding: 16px;
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.info-row {
|
||||
display: flex;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
width: 120px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.info-value {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user