Files
leaudit-platform-frontend/app/routes/reviews/$reviewId.tsx
T

414 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState } from 'react';
import { json, type MetaFunction } from '@remix-run/node';
import { useLoaderData, useParams } from '@remix-run/react';
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Breadcrumb } from '~/components/layout/Breadcrumb';
import type { ReviewResult, RuleCheckResult } from '~/models/review';
import type { File } from '~/models/file';
import { RULE_CHECK_STATUS_LABELS, RULE_CHECK_STATUS_COLORS } from '~/models/review';
export const meta: MetaFunction = () => {
return [
{ title: "中国烟草AI合同及卷宗审核系统 - 评查详情" },
{ name: "description", content: "文件评查详情页面" }
];
};
export const handle = {
breadcrumb: '评查详情'
};
interface LoaderData {
file: File;
reviewResult: ReviewResult;
reviewPoints: RuleCheckResult[];
fileContent?: string; // 模拟文件内容
}
export async function loader({ params }) {
const { reviewId } = params;
// 模拟数据,实际项目中应从API获取
const file: File = {
id: "1",
fileName: "2023年度烟草专卖零售许可证.pdf",
fileType: "application/pdf",
documentTypeId: "2",
documentTypeName: "专卖许可证",
fileSize: 1024 * 1024 * 2.5, // 2.5MB
uploaderId: "1",
uploaderName: "张三",
status: "completed",
reviewStatus: "pass",
createdAt: "2023-12-24 14:30",
updatedAt: "2023-12-24 16:45"
};
const reviewResult: ReviewResult = {
id: reviewId,
fileId: "1",
fileName: "2023年度烟草专卖零售许可证.pdf",
totalPoints: 15,
passPoints: 6,
warningPoints: 7,
errorPoints: 2,
score: 80,
reviewStatus: "warning",
reviewedAt: "2023-12-24 16:45",
reviewerId: "system",
reviewerName: "AI系统",
createdAt: "2023-12-24 14:35",
updatedAt: "2023-12-24 16:45"
};
const reviewPoints: RuleCheckResult[] = [
{
id: "1",
reviewResultId: reviewId,
ruleId: "1",
ruleName: "合同主体信息完整性检查",
status: "pass",
location: "第1页 第3段",
content: "甲方:XX烟草公司,地址:XX市XX区XX路XX号,法定代表人:张XX",
suggestion: "主体信息完整,符合规范",
manualReviewed: false,
createdAt: "2023-12-24 14:40",
updatedAt: "2023-12-24 14:40"
},
{
id: "2",
reviewResultId: reviewId,
ruleId: "2",
ruleName: "许可证编号格式检查",
status: "warning",
location: "第1页 第5段",
content: "许可证编号:(2023)12345",
suggestion: "许可证编号格式不完全符合规范,建议修改为'烟零许(2023)12345号'",
manualReviewed: true,
createdAt: "2023-12-24 14:40",
updatedAt: "2023-12-24 15:20"
},
{
id: "3",
reviewResultId: reviewId,
ruleId: "3",
ruleName: "许可证有效期检查",
status: "fail",
location: "第1页 第8段",
content: "有效期:自2023年1月1日",
suggestion: "许可证缺少有效期截止日期,必须明确注明有效期限",
manualReviewed: false,
createdAt: "2023-12-24 14:40",
updatedAt: "2023-12-24 14:40"
},
{
id: "4",
reviewResultId: reviewId,
ruleId: "4",
ruleName: "经营场所信息检查",
status: "pass",
location: "第1页 第12段",
content: "经营场所:XX市XX区XX街XX号,面积:120平方米",
suggestion: "经营场所信息完整",
manualReviewed: false,
createdAt: "2023-12-24 14:40",
updatedAt: "2023-12-24 14:40"
}
];
// 模拟文件内容,实际项目中应从API获取或使用专用组件展示
const fileContent = `烟草专卖零售许可证
发证机关:XX市烟草专卖局
发证日期:2023年1月1日
甲方:XX烟草公司,地址:XX市XX区XX路XX号,法定代表人:张XX
零售单位名称:XX便利店
许可证编号:(2023)12345
法定代表人/负责人:李XX
经营者类型:个体工商户
有效期:自2023年1月1日
联系电话:123-4567890
经营场所:XX市XX区XX街XX号,面积:120平方米
零售烟草制品品种:卷烟、雪茄烟
特别说明:本许可证不得伪造、变造、转让、涂改。
`;
return json<LoaderData>({
file,
reviewResult,
reviewPoints,
fileContent
});
}
export default function ReviewDetail() {
const { file, reviewResult, reviewPoints, fileContent } = useLoaderData<typeof loader>();
const [activeTab, setActiveTab] = useState('tab-preview');
const [selectedPoint, setSelectedPoint] = useState<string | null>(null);
const handleTabChange = (tabId: string) => {
setActiveTab(tabId);
};
const handlePointSelect = (pointId: string) => {
setSelectedPoint(pointId === selectedPoint ? null : pointId);
};
return (
<div>
<Breadcrumb
items={[
{ title: '评查结果', to: '/reviews' },
{ title: '评查详情', to: `/reviews/${reviewResult.id}` }
]}
/>
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<h2 className="text-xl font-medium">{file.fileName}</h2>
<span className={`ml-3 status-badge status-${reviewResult.reviewStatus === 'pass' ? 'success' : reviewResult.reviewStatus === 'warning' ? 'warning' : 'error'}`}>
<i className={`ri-${reviewResult.reviewStatus === 'pass' ? 'checkbox-circle' : reviewResult.reviewStatus === 'warning' ? 'error-warning' : 'close-circle'}-line mr-1`}></i>
{reviewResult.reviewStatus === 'pass' ? '通过' : reviewResult.reviewStatus === 'warning' ? '警告' : '不通过'}
</span>
</div>
<div className="space-x-2">
<Button type="default" icon="ri-download-line">
</Button>
<Button type="primary" icon="ri-check-double-line">
</Button>
</div>
</div>
<div className="tab-container">
<div className="tab-nav">
<div
className={`tab-nav-item ${activeTab === 'tab-preview' ? 'active' : ''}`}
onClick={() => handleTabChange('tab-preview')}
>
<i className="ri-file-text-line"></i>
</div>
<div
className={`tab-nav-item ${activeTab === 'tab-suggestion' ? 'active' : ''}`}
onClick={() => handleTabChange('tab-suggestion')}
>
<i className="ri-lightbulb-line"></i> AI智能分析
</div>
<div
className={`tab-nav-item ${activeTab === 'tab-fileinfo' ? 'active' : ''}`}
onClick={() => handleTabChange('tab-fileinfo')}
>
<i className="ri-information-line"></i>
</div>
</div>
<div className="tab-content">
<div className={`tab-pane ${activeTab === 'tab-preview' ? 'active' : ''}`}>
<div className="flex flex-col lg:flex-row lg:h-[calc(100vh-250px)]">
{/* 文件内容预览 */}
<div className="w-full lg:w-2/3 h-full mb-4 lg:mb-0 lg:pr-4">
<div className="bg-white p-4 rounded-md shadow-sm h-full overflow-y-auto">
<pre className="whitespace-pre-wrap font-sans text-gray-800">
{fileContent}
</pre>
</div>
</div>
{/* 评查点列表 */}
<div className="w-full lg:w-1/3 h-full lg:pl-4">
<div className="review-points-panel h-full flex flex-col">
<div className="review-panel-header py-2 px-4 flex items-center bg-primary-light">
<i className="ri-file-list-check-line text-primary mr-2"></i>
<span className="font-medium text-primary"></span>
</div>
{/* 评查统计 */}
<div className="review-statistics bg-white border-b border-gray-100 py-3 px-4">
<div className="flex justify-between items-center">
<div className="flex items-center">
<div className="w-7 h-7 bg-gray-100 rounded-md flex items-center justify-center">
<span className="text-sm font-semibold text-gray-600">{reviewResult.totalPoints}</span>
</div>
<span className="text-xs text-gray-500 ml-1"></span>
</div>
<div className="h-8 border-r border-gray-200"></div>
<div className="flex items-center">
<div className="w-7 h-7 bg-green-50 rounded-md flex items-center justify-center">
<span className="text-sm font-semibold text-success">{reviewResult.passPoints}</span>
</div>
<span className="text-xs text-gray-500 ml-1"></span>
</div>
<div className="h-8 border-r border-gray-200"></div>
<div className="flex items-center">
<div className="w-7 h-7 bg-yellow-50 rounded-md flex items-center justify-center">
<span className="text-sm font-semibold text-warning">{reviewResult.warningPoints}</span>
</div>
<span className="text-xs text-gray-500 ml-1"></span>
</div>
<div className="h-8 border-r border-gray-200"></div>
<div className="flex items-center">
<div className="w-7 h-7 bg-red-50 rounded-md flex items-center justify-center">
<span className="text-sm font-semibold text-error">{reviewResult.errorPoints}</span>
</div>
<span className="text-xs text-gray-500 ml-1"></span>
</div>
</div>
</div>
{/* 评查点列表 */}
<div className="flex-1 overflow-y-auto">
{reviewPoints.map(point => (
<div
key={point.id}
className={`review-point-item ${selectedPoint === point.id ? 'bg-gray-50' : ''}`}
onClick={() => handlePointSelect(point.id)}
>
<div className="review-point-header">
<div className="review-point-title">{point.ruleName}</div>
<span className={`status-badge status-${RULE_CHECK_STATUS_COLORS[point.status]}`}>
<i className={`ri-${point.status === 'pass' ? 'checkbox-circle' : point.status === 'warning' ? 'error-warning' : 'close-circle'}-line mr-1`}></i>
{RULE_CHECK_STATUS_LABELS[point.status]}
</span>
</div>
<div className="review-point-location">
<i className="ri-file-list-line mr-1"></i>
<span>{point.location}</span>
</div>
{selectedPoint === point.id && (
<div className="mt-2 pt-2 border-t border-gray-100">
<div className="text-xs text-gray-600 mb-1">
<span className="font-medium"></span>
<span>{point.content}</span>
</div>
<div className="text-xs text-gray-600">
<span className="font-medium"></span>
<span>{point.suggestion}</span>
</div>
<div className="mt-2 flex justify-between">
<div className="text-xs text-gray-500">
{point.manualReviewed &&
<span><i className="ri-user-line mr-1"></i></span>
}
</div>
<div>
<Button type="default" size="small">
<i className="ri-edit-line mr-1"></i>
</Button>
</div>
</div>
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
</div>
<div className={`tab-pane ${activeTab === 'tab-suggestion' ? 'active' : ''}`}>
<Card>
<div className="text-lg font-medium mb-4 text-gray-800">AI智能分析意见</div>
<div className="mb-6">
<div className="font-medium text-gray-700 mb-2"></div>
<div className="p-3 bg-gray-50 rounded-md text-gray-600">
</div>
</div>
<div className="mb-6">
<div className="font-medium text-gray-700 mb-2"></div>
<ul className="list-disc pl-5 space-y-2 text-gray-600">
<li><span className="text-warning font-medium"></span> - "(2023)12345""烟零许(2023)12345号"</li>
<li><span className="text-error font-medium"></span> - "自2023年1月1日"</li>
</ul>
</div>
<div>
<div className="font-medium text-gray-700 mb-2"></div>
<ul className="list-decimal pl-5 space-y-2 text-gray-600">
<li>"烟零许""号"</li>
<li>"自2023年1月1日至2023年12月31日"</li>
<li></li>
</ul>
</div>
</Card>
</div>
<div className={`tab-pane ${activeTab === 'tab-fileinfo' ? 'active' : ''}`}>
<Card>
<div className="text-lg font-medium mb-4 text-gray-800"></div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<table className="w-full">
<tbody>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500 w-1/3"></td>
<td className="py-2 text-gray-800">{file.fileName}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{file.documentTypeName}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{(file.fileSize / (1024 * 1024)).toFixed(2)} MB</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{file.uploaderName}</td>
</tr>
<tr>
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{file.createdAt}</td>
</tr>
</tbody>
</table>
</div>
<div>
<table className="w-full">
<tbody>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500 w-1/3"></td>
<td className="py-2 text-gray-800">
<span className={`status-badge status-${reviewResult.reviewStatus === 'pass' ? 'success' : reviewResult.reviewStatus === 'warning' ? 'warning' : 'error'}`}>
<i className={`ri-${reviewResult.reviewStatus === 'pass' ? 'checkbox-circle' : reviewResult.reviewStatus === 'warning' ? 'error-warning' : 'close-circle'}-line mr-1`}></i>
{reviewResult.reviewStatus === 'pass' ? '通过' : reviewResult.reviewStatus === 'warning' ? '警告' : '不通过'}
</span>
</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{reviewResult.score} </td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{reviewResult.reviewedAt}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{reviewResult.reviewerName}</td>
</tr>
<tr>
<td className="py-2 text-gray-500"></td>
<td className="py-2 text-gray-800">{reviewResult.totalPoints} </td>
</tr>
</tbody>
</table>
</div>
</div>
</Card>
</div>
</div>
</div>
</div>
);
}