/** * 评查详情页面 * * 功能概述: * - 显示文档评查结果和详细信息 * - 支持查看文档内容及评查点高亮标记 * - 提供评查点列表,分为通过、警告和错误三种类型 * - 支持评查点处理,如一键替换、人工审核等功能 * - 支持导出评查报告和下载原文件 * * 组件结构: * - FileInfo: 显示文件基本信息和操作按钮 * - ReviewTabs: 页面选项卡,包括评查结果、AI智能分析和文件信息 * - FilePreview: 文档预览组件,显示文档内容及高亮问题 * - ReviewPointsList: 评查点列表组件,显示所有评查结果 * - AIAnalysis: AI智能分析结果,提供综合评价 * - FileDetails: 文件详情信息 * * 数据流转: * 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 {getDocument} from "~/api/files/documents"; import reviewsStyles from "~/styles/reviews.css?url"; import { getReviewPoints } from "~/api/evaluation_points/reviews"; // 导入评查详情页面组件 import { FileInfo, ReviewTabs, FilePreview, ReviewPointsList, AIAnalysis, FileDetails } from "~/components/reviews"; // 从ReviewPointsList组件中导入ReviewPoint类型 import { type ReviewPoint } from '~/components/reviews'; /** * 文件信息组件 * 显示文件名称、状态信息以及操作按钮(下载原文件、导出评查报告、确认评查结果) */ // 格式化文件大小 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; } // 定义合同信息类型 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: reviewsStyles }]; } export const handle = { breadcrumb: "评查详情" }; export async function loader({ request }: LoaderFunctionArgs) { try { const url = new URL(request.url); const id = url.searchParams.get('id') || undefined; // console.log("id-------",id); if (!id) { return Response.json({ error: '评查ID不能为空' }, { status: 400 }); } const documentData = await getDocument(id); if (documentData.error) { console.error("获取文档数据错误:", documentData.error); return Response.json({ error: documentData.error }, { status: documentData.status || 500 }); } const reviewData = await getReviewPoints(id); console.log("reviewData-------",reviewData); if (reviewData.error) { console.error("获取评查点数据错误:", reviewData.error); return Response.json({ error: reviewData.error }, { status: reviewData.status || 500 }); } return Response.json({ document: documentData.data, reviewPoints: reviewData.data, statistics: reviewData.stats }); } catch (error) { console.error('获取评查数据失败:', error); return Response.json({ error: '获取评查数据失败' }, { status: 500 }); } } export default function ReviewDetails() { const navigate = useNavigate(); const { document, reviewPoints, statistics } = useLoaderData(); const [isLoading, setIsLoading] = useState(false); // 已经通过loader加载了数据,不需要再显示加载状态 const [activeTab, setActiveTab] = useState('preview'); // 'preview', 'analysis', 'fileinfo' const [reviewData, setReviewData] = useState(null); const [activeReviewPointId, setActiveReviewPointId] = useState(null); // 模拟获取评查数据 useEffect(() => { if (!document) return; // 构建文件信息对象 const fileInfo = { fileName: document.name || "未知文件名", 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, }; // 创建包含真实文档数据的评查数据对象 const reviewDataObj: ReviewData = { // 使用真实文件信息 fileInfo: fileInfo, // 其他字段暂时使用默认值 contractInfo: getMockReviewData().contractInfo, reviewInfo: getMockReviewData().reviewInfo, statistics: statistics, fileContent: getMockReviewData().fileContent, reviewPoints: reviewPoints, aiAnalysis: getMockReviewData().aiAnalysis, }; setReviewData(reviewDataObj); setIsLoading(false); }, [document, reviewPoints, statistics]); const handleTabChange = (tabKey: string) => { setActiveTab(tabKey); }; const handleReviewPointSelect = (reviewPointId: string) => { setActiveReviewPointId(reviewPointId); }; const handleReviewPointStatusChange = (reviewPointId: string, newStatus: string) => { // 更新评查点状态 if (reviewData) { const updatedReviewPoints = reviewData.reviewPoints.map(point => point.id === reviewPointId ? { ...point, status: newStatus } : point ); setReviewData({ ...reviewData, reviewPoints: updatedReviewPoints, statistics: calculateStatistics(updatedReviewPoints) }); } }; const handleConfirmResults = () => { alert('评查结果已确认'); navigate('/reviews'); // 假设评查列表页面路径为 /reviews }; return (
{isLoading ? (
加载中...
) : reviewData && ( <> {/* 文件信息和操作按钮 */} {/* 选项卡 */} {/* 评查结果选项卡内容 */} {activeTab === 'preview' && (
{/* 左侧:文件预览 */}
{/* 右侧:评查结果 */}
)} {/* AI智能分析选项卡内容 */} {activeTab === 'analysis' && ( )} {/* 文件信息选项卡内容 */} {activeTab === 'fileinfo' && ( )}
)}
); } // 计算评查统计数据 function calculateStatistics(reviewPoints: ReviewPoint[]): Statistics { const total = reviewPoints.length; const success = reviewPoints.filter(point => point.status === 'success').length; const warning = reviewPoints.filter(point => point.status === 'warning').length; const error = reviewPoints.filter(point => point.status === 'error').length; // 计算评分:通过占总数的百分比,错误项有额外惩罚 const score = Math.round((success / total) * 100 - (error * 5)); return { total, success, warning, error, score: Math.max(0, Math.min(100, score)) // 确保分数在0-100之间 }; } // 模拟评查数据 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 }, 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", title: "付款条件描述不明确", groupName: "付款条款清晰性", // location: "交货与付款条款", status: "error", content: "乙方应在收到货物之日起5个工作日内支付合同款项,甲方应在收到乙方全部付款后开具增值税专用发票,乙方应在收到发票后支付剩余款项。", suggestion: "乙方应在收到货物验收合格之日起5个工作日内支付合同总额的70%,甲方收到该部分款项后3个工作日内向乙方开具等额增值税专用发票;乙方应在收到发票之日起5个工作日内支付剩余30%款项。", position: { section: "交货与付款", index: 2 }, result: false }, { id: "2", title: "违约责任条款缺失", groupName: "合同权利义务对等性", status: "warning", content: "如合同发生纠纷,双方应协商解决。", suggestion: "如合同发生纠纷,双方应友好协商解决;协商不成的,任何一方均有权向甲方所在地人民法院提起诉讼。任何一方未能履行本合同约定义务,应向守约方支付合同总金额的10%作为违约金;给对方造成损失的,还应赔偿由此产生的全部损失。", position: { section: "争议解决", index: 0 }, result: false }, { id: "3", title: "签章不完整", groupName: "合同签署规范性", status: "warning", content: "乙方(盖章):YY贸易有限公司\n代表人签字:李YY\n日期:2023年10月20日", suggestion: "需要联系甲方补充公章", needsHumanReview: true, humanReviewNote: "需要联系甲方补充公章", position: { section: "签章", index: 0 }, result: false }, { id: "9", title: "交货方式描述模糊", groupName: "履行条款明确性", status: "success", content: "3.4 运输方式:陆运,运费由甲方承担。", suggestion: "建议补充具体的运输方式和时间", needsHumanReview: true, humanReviewNote: "经核实,该交货方式虽然描述不够详细,但符合行业惯例且双方已经多次合作,不会造成实际履行障碍。", humanReviewBy: "王法务", humanReviewTime: "2023-11-05 14:30:22", position: { section: "交货与付款", index: 4 }, result: true }, { id: "10", title: "法律适用条款缺失", groupName: "争议解决条款完整性", status: "error", content: "", suggestion: "第十三条 法律适用\n本合同的订立、效力、解释、履行及争议的解决均适用中华人民共和国法律。因本合同引起的或与本合同有关的任何争议,双方应友好协商解决。协商不成的,提交甲方所在地人民法院诉讼解决。", position: { section: "缺失", index: 0 }, result: false } ], aiAnalysis: { riskAlerts: [ { title: "风险提示", content: "本合同缺少违约责任条款,可能导致权责不明。", description: "根据《中华人民共和国民法典》第五百七十七条规定,建议增加违约责任条款,明确双方违约责任及赔偿方式。" }, { title: "完整性检查", content: "本合同缺少法律适用条款。", description: "根据行业惯例,销售合同应明确约定适用法律和纠纷解决方式,以避免后续争议解决时的不确定性。" } ], suggestions: [ { title: "优化建议", content: "建议完善付款条件描述。", description: "目前合同中关于付款条件的描述存在歧义,可能导致付款时间和条件不明确。建议按系统修改建议优化。" } ], summary: "本合同基本结构完整,主体内容清晰,但存在多处条款描述不完善的问题,主要体现在支付条件、违约责任、不可抗力、保密条款、合同终止条件等方面。这些问题虽不影响合同的基本合规性,但可能在合同履行过程中引发争议和纠纷。同时,合同签章不完整,也影响了合同的法律效力。建议对上述问题进行修改完善后再行签署。" } }; }