909 lines
32 KiB
TypeScript
909 lines
32 KiB
TypeScript
/**
|
||
* 交叉评查详情页面
|
||
*
|
||
* 功能概述:
|
||
* - 显示文档交叉评查结果和详细信息
|
||
* - 支持查看文档内容及评查点高亮标记
|
||
* - 提供评查点列表,分为通过、警告和错误三种类型
|
||
* - 支持评查点处理,如一键替换、人工审核等功能
|
||
* - 支持导出评查报告和下载原文件
|
||
*
|
||
* 组件结构:
|
||
* - FileInfo: 显示文件基本信息和操作按钮
|
||
* - FilePreview: 文档预览组件,显示文档内容及高亮问题
|
||
* - ReviewPointsList: 评查点列表组件,显示所有评查结果
|
||
*
|
||
* 数据流转:
|
||
* 1. 页面加载时从API获取评查详情数据
|
||
* 2. 根据评查点ID关联文档中的高亮区域
|
||
* 3. 点击评查点时在文档中定位对应位置
|
||
* 4. 处理评查点时更新状态并反馈到UI
|
||
*
|
||
* @author 中国烟草AI合同及卷宗审核系统开发团队
|
||
*/
|
||
|
||
import { type MetaFunction, type LoaderFunctionArgs, type ActionFunctionArgs } from "@remix-run/node";
|
||
import React, { useState, useEffect } from "react";
|
||
import { useNavigate, useLoaderData } from "@remix-run/react";
|
||
import crossCheckingStyles from "~/styles/cross-checking-result.css?url";
|
||
import { getReviewPoints, updateReviewResult} from "~/api/evaluation_points/reviews";
|
||
import { toastService } from "~/components/ui/Toast";
|
||
import { confirmReviewResults, checkProposalVotes, findIsProposer } from "~/api/cross-checking/cross-file-result";
|
||
|
||
// 导入交叉评查详情页面组件
|
||
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 taskId = url.searchParams.get('tId') || undefined;
|
||
const previousRoute = url.searchParams.get('previousRoute') || '';
|
||
// console.log("id-------",id);
|
||
if (!id) {
|
||
return Response.json({ result: false, message: '文件ID不能为空' });
|
||
}
|
||
if (!taskId) {
|
||
return Response.json({ result: false, message: '任务ID不能为空' });
|
||
}
|
||
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
// 获取评查点数据,传递request对象
|
||
const reviewData = await getReviewPoints(id, request);
|
||
|
||
// 获取当前登录用户是否是发起人
|
||
const isProposer = await findIsProposer(taskId, userInfo?.user_id);
|
||
|
||
// console.log("documentData-------",JSON.stringify(documentData.data,null,2));
|
||
// console.log("reviewData-------",JSON.stringify('data' in reviewData ? reviewData.data : '',null,2));
|
||
// console.log("reviewData-------",JSON.stringify(reviewData,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 || [],
|
||
userInfo: userInfo,
|
||
jwtToken: frontendJWT, // 传递JWT token
|
||
isProposer: isProposer
|
||
});
|
||
} 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: '获取评查数据失败' });
|
||
}
|
||
}
|
||
|
||
// 添加 action 函数处理需要用户认证的操作
|
||
export async function action({ request }: ActionFunctionArgs) {
|
||
const formData = await request.formData();
|
||
const intent = formData.get("intent") as string;
|
||
|
||
try {
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
if (intent === "updateReviewResult") {
|
||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||
const editAuditStatusId = formData.get("editAuditStatusId") as string;
|
||
const result = formData.get("result") as string;
|
||
const message = formData.get("message") as string;
|
||
|
||
const response = await updateReviewResult(reviewPointResultId, editAuditStatusId, result, message, request);
|
||
|
||
if (response.error) {
|
||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||
}
|
||
|
||
return Response.json({ success: true, data: response.data });
|
||
}
|
||
|
||
if (intent === "submitCrossCheckingOpinion") {
|
||
const { submitCrossCheckingOpinion } = await import("~/api/cross-checking/cross-file-result");
|
||
|
||
const reviewPointResultId = formData.get("reviewPointResultId") as string;
|
||
const documentId = formData.get("documentId") as string;
|
||
const auditPoint = formData.get("auditPoint") as string;
|
||
const foundIssue = formData.get("foundIssue") as string;
|
||
const auditOpinion = formData.get("auditOpinion") as string;
|
||
const deductionScore = parseFloat(formData.get("deductionScore") as string);
|
||
|
||
const opinionData = {
|
||
reviewPointResultId,
|
||
documentId,
|
||
auditPoint,
|
||
foundIssue,
|
||
auditOpinion,
|
||
deductionScore
|
||
};
|
||
|
||
const response = await submitCrossCheckingOpinion(opinionData, frontendJWT);
|
||
|
||
if (response.error) {
|
||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||
}
|
||
|
||
return Response.json({ success: true, data: response.data });
|
||
}
|
||
|
||
if (intent === "getCrossCheckingOpinions") {
|
||
const { getCrossCheckingOpinions } = await import("~/api/cross-checking/cross-file-result");
|
||
|
||
const documentId = formData.get("documentId") as string;
|
||
const page = parseInt(formData.get("page") as string || "1", 10);
|
||
const pageSize = parseInt(formData.get("pageSize") as string || "10", 10);
|
||
const userId = userInfo?.user_id;
|
||
|
||
const response = await getCrossCheckingOpinions(documentId, page, pageSize, userId, frontendJWT);
|
||
|
||
if (response.error) {
|
||
return Response.json({ success: false, error: response.error }, { status: response.status || 500 });
|
||
}
|
||
|
||
return Response.json({ success: true, data: response.data });
|
||
}
|
||
|
||
if (intent === "confirmReviewResults") {
|
||
// 检查文档下提案是否存在未投票用户,首先先打开一个模态框,提示用户是否确认完成评查,如果用户点击确认,则调用confirmReviewResults接口,如果用户点击取消,则关闭模态框
|
||
// 模态框内的数据需要根据checkProposalVotes返回回来的数据进行显示,如果存在未投票用户,则提示用户存在未投票用户,如果不存在未投票用户,则提示用户完成评查
|
||
}
|
||
} catch (error) {
|
||
console.error('Action处理失败:', error);
|
||
return Response.json({
|
||
success: false,
|
||
error: error instanceof Error ? error.message : '操作失败'
|
||
}, { status: 500 });
|
||
}
|
||
}
|
||
|
||
export default function CrossCheckingResult() {
|
||
const navigate = useNavigate();
|
||
const loaderData = useLoaderData<typeof loader>();
|
||
const { document, reviewPoints, statistics, reviewInfo, scoring_proposals, jwtToken, userInfo, isProposer } = 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 {
|
||
// 使用 fetch 调用 action
|
||
const formData = new FormData();
|
||
formData.append("intent", "updateReviewResult");
|
||
formData.append("reviewPointResultId", reviewPointResultId);
|
||
formData.append("editAuditStatusId", editAuditStatusId.toString());
|
||
formData.append("result", boolResult);
|
||
formData.append("message", message);
|
||
|
||
const response = await fetch(window.location.pathname, {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
|
||
const result = await response.json();
|
||
|
||
if (!result.success) {
|
||
console.error('更新评查结果失败:', result.error);
|
||
toastService.error(`更新评查结果失败: ${result.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('更新评查结果失败,请稍后重试');
|
||
}
|
||
};
|
||
|
||
/**
|
||
* 处理确认评查结果
|
||
* 1. 检查未投票提案
|
||
* 2. 根据结果弹出确认模态框
|
||
* 3. 用户确认后更新文档状态并跳转
|
||
*/
|
||
const handleConfirmResults = async (event?: React.MouseEvent) => {
|
||
// 阻止默认行为,防止页面刷新
|
||
if (event) {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
}
|
||
|
||
if (!document || !document.id) {
|
||
toastService.error('文档数据不完整,无法确认评查结果');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsLoading(true);
|
||
|
||
// 1. 先检查未投票
|
||
const checkRes = await checkProposalVotes(document.id, jwtToken);
|
||
console.log("checkRes", checkRes);
|
||
|
||
if (checkRes.error) {
|
||
toastService.error(checkRes.error);
|
||
setIsLoading(false);
|
||
return;
|
||
}
|
||
|
||
// 2. 解析返回数据,定义明确的类型
|
||
interface CheckProposalResponse {
|
||
success: boolean;
|
||
message: string;
|
||
data: {
|
||
pending_proposals: Array<{
|
||
evaluation_point_name: string;
|
||
pending_voters_num: number;
|
||
}>;
|
||
};
|
||
}
|
||
|
||
const responseData = checkRes.data as CheckProposalResponse;
|
||
const pendingProposals = responseData?.data?.pending_proposals || [];
|
||
console.log("pendingProposals", pendingProposals);
|
||
|
||
// 3. 构建模态框消息
|
||
let modalMessage: string = '';
|
||
if (Array.isArray(pendingProposals) && pendingProposals.length > 0) {
|
||
modalMessage = pendingProposals.map((item) =>
|
||
`评查名称为:${item.evaluation_point_name} 还剩余 ${item.pending_voters_num}人未投票。`
|
||
).join('\n');
|
||
} else {
|
||
modalMessage = '是否完成评查?';
|
||
}
|
||
|
||
// 4. 弹出模态框
|
||
messageService.show({
|
||
title: '提示',
|
||
message: modalMessage,
|
||
type: 'warning',
|
||
confirmText: '确认',
|
||
cancelText: '取消',
|
||
onConfirm: async () => {
|
||
setIsLoading(true);
|
||
const res = await confirmReviewResults(document.id);
|
||
setIsLoading(false);
|
||
|
||
if (res.error) {
|
||
toastService.error(res.error);
|
||
return;
|
||
}
|
||
|
||
toastService.success('评查结果已确认,文档审核状态已更新');
|
||
// 注释掉自动跳转,让用户停留在当前页面
|
||
navigate('/cross-checking');
|
||
}
|
||
});
|
||
|
||
setIsLoading(false);
|
||
} catch (error) {
|
||
setIsLoading(false);
|
||
toastService.error(`确认评查结果失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||
}
|
||
};
|
||
|
||
// 构建自定义面包屑项
|
||
const getBreadcrumbItems = () => {
|
||
const items = [
|
||
{ title: "交叉评查详情", to: `/cross-checking/result?id=${document?.id}` }
|
||
];
|
||
|
||
// 添加前置路由
|
||
if (loaderData.previousRoute) {
|
||
if (loaderData.previousRoute === 'crossChecking') {
|
||
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>
|
||
|
||
{/* 完成评查按钮 */}
|
||
{isProposer && (
|
||
<button
|
||
type="button"
|
||
onClick={(event) => {
|
||
event.preventDefault();
|
||
event.stopPropagation();
|
||
handleConfirmResults(event);
|
||
}}
|
||
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[]}
|
||
jwtToken={jwtToken}
|
||
userInfo={userInfo}
|
||
/>
|
||
</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: "本合同基本结构完整,主体内容清晰,但存在多处条款描述不完善的问题,主要体现在支付条件、违约责任、不可抗力、保密条款、合同终止条件等方面。这些问题虽不影响合同的基本合规性,但可能在合同履行过程中引发争议和纠纷。同时,合同签章不完整,也影响了合同的法律效力。建议对上述问题进行修改完善后再行签署。"
|
||
}
|
||
};
|
||
}
|