Files
leaudit-platform-frontend/app/routes/rules-files.tsx
T

573 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 { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node";
import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react";
import { Button } from "~/components/ui/Button";
import { Card } from "~/components/ui/Card";
import { FileIcon } from "~/components/ui/FileIcon";
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
import { Pagination } from "~/components/ui/Pagination";
import { Table } from "~/components/ui/Table";
import { Tag } from "~/components/ui/Tag";
import { StatusBadge } from "~/components/ui/StatusBadge";
import rulesFilesStyles from "~/styles/pages/rules-files.css?url";
import {
getReviewFiles,
type ReviewFileUI,
updateDocumentAuditStatus
} from "~/api/evaluation_points/rules-files";
import { getDocumentTypes } from "~/api/document-types/document-types";
import { toastService } from "~/components/ui/Toast";
export const links = () => [
{ rel: "stylesheet", href: rulesFilesStyles }
];
export const handle = {
breadcrumb: "评查文件列表"
};
export const meta: MetaFunction = () => {
return [
{ title: "评查文件列表 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "管理系统中所有上传的评查文件,支持按文件类型、评查状态进行筛选" },
{ name: "keywords", content: "评查文件,合同审核,中国烟草,文件管理" }
];
};
// 日期范围枚举
export enum DateRange {
ALL = 'all',
TODAY = 'today',
WEEK = 'week',
MONTH = 'month',
CUSTOM = 'custom'
}
// 评查状态标签映射
export const REVIEW_STATUS_LABELS: Record<string, string> = {
'pass': '通过',
'warning': '警告',
'fail': '不通过',
'pending': '待人工确认'
};
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const fileType = url.searchParams.get("fileType") || "";
const reviewStatus = url.searchParams.get("reviewStatus") || "";
const dateRange = url.searchParams.get("dateRange") || "";
const dateFrom = url.searchParams.get("dateFrom") || "";
const dateTo = url.searchParams.get("dateTo") || "";
const keyword = url.searchParams.get("keyword") || "";
const sortOrder = url.searchParams.get("sortOrder") || "upload_time_desc";
const currentPage = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10);
try {
// 获取文档类型列表
const typesResponse = await getDocumentTypes({pageSize:500});
const documentTypes = typesResponse.data?.types || [];
// 获取文件列表
const searchParams = {
fileType,
reviewStatus,
dateRange,
dateFrom,
dateTo,
keyword,
sortOrder,
page: currentPage,
pageSize,
};
// console.log('rules-filessearchParams-----',searchParams);
const filesResponse = await getReviewFiles(searchParams);
if (filesResponse.error) {
console.error('获取评查文件列表失败:', filesResponse.error);
throw new Response('获取评查文件列表失败', { status: filesResponse.status || 500 });
}
const files = filesResponse.data?.files || [];
const totalCount = filesResponse.data?.total || 0;
return Response.json({
files,
documentTypes,
totalCount,
currentPage,
pageSize,
});
} catch (error) {
console.error('加载评查文件列表失败:', error);
throw new Response('加载评查文件列表失败', { status: 500 });
}
}
// 提取renderErrorBoundary函数作为命名导出
export function ErrorBoundary() {
return (
<div className="error-container p-6">
<h1 className="text-xl font-normal text-red-500 mb-4"></h1>
<p className="mb-4"></p>
<Button type="primary" to="/"></Button>
</div>
);
}
// 在文件中定义一个与路由文件名匹配的命名函数组件
export default function RulesFiles() {
const navigate = useNavigate();
const { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
const dateFrom = searchParams.get('dateFrom') || '';
const dateTo = searchParams.get('dateTo') || '';
// 处理筛选条件变更
const handleFilterChange = (e: React.ChangeEvent<HTMLSelectElement | HTMLInputElement>) => {
const { name, value } = e.target;
const newParams = new URLSearchParams(searchParams);
if (value) {
newParams.set(name, value);
} else {
newParams.delete(name);
}
// 切换筛选条件时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理搜索操作
const handleSearch = (keyword: string) => {
const newParams = new URLSearchParams(searchParams);
if (keyword) {
newParams.set('keyword', keyword);
} else {
newParams.delete('keyword');
}
// 搜索时,重置到第一页
newParams.set('page', '1');
setSearchParams(newParams);
};
// 处理页码变更
const handlePageChange = (page: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('page', page.toString());
setSearchParams(newParams);
};
// 处理每页条数变更
const handlePageSizeChange = (size: number) => {
const newParams = new URLSearchParams(searchParams);
newParams.set('pageSize', size.toString());
newParams.set('page', '1'); // 改变每页条数时重置为第一页
setSearchParams(newParams);
};
// 查看评查文件
const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => {
// 检查audit_status是否为0,如果是则更新为2
if (auditStatus === 0 || auditStatus === null) {
try {
const response = await updateDocumentAuditStatus(fileId, 2);
if (response.error) {
console.error('更新文件审核状态失败:', response.error);
// 尽管更新失败,仍然导航到文件详情页
}
} catch (error) {
console.error('更新文件审核状态时出错:', error);
// 尽管发生错误,仍然导航到文件详情页
}
}
// 导航到评查详情页
navigate(`/reviews?id=${fileId}&previousRoute=rulesFiles`);
};
// 渲染问题摘要
const renderIssues = (file: ReviewFileUI) => {
// 如果评查状态为通过(说明所有评查结果为true),显示"所有评查点均通过"
if (file.status === 'Processed') {
if (file.reviewStatus === 'pass') {
return (
<div className="text-sm text-success">
<i className="ri-check-double-line mr-1"></i>
</div>
);
}
// 如果评查状态为不通过,显示"统计分数为:{file.score || 0}。分数低于80分。"
// if (file.reviewStatus === 'fail') {
// return (
// <div className="text-sm text-error">
// <i className="ri-error-warning-line mr-1"></i>统计分数为:{file.score || 0}。分数低于80分。
// </div>
// );
// }
// 显示问题列表
if (file.reviewStatus !== 'pass' && file.reviewStatus !== 'fail' && file.issues && file.issues.length > 0) {
// 最多显示2个问题
const displayIssues = file.issues.slice(0, 2);
return (
<div className="text-sm">
{displayIssues.map((issue, index) => (
<div key={index} className="mb-1">
<i className="ri-circle-fill mr-1 text-warning"></i>
{issue.message}
</div>
))}
{file.issues.length > 2 && (
<div className="text-secondary mt-1">
{file.issues.length - 2} ...
</div>
)}
</div>
);
}
}
// 其他状态显示占位符
return <div className="text-sm text-secondary">-</div>;
};
// 下载文件
const handleDownload = async (path: string) => {
try {
const urlBefore = 'http://172.18.0.100:9000/docauditai/';
const downloadUrl = `${urlBefore}${path}`;
// 使用fetch获取文件内容
const response = await fetch(downloadUrl);
if (!response.ok) {
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
}
// 将响应转换为Blob
const blob = await response.blob();
// 创建Blob URL
const blobUrl = URL.createObjectURL(blob);
// 创建一个隐藏的a标签并点击它
const a = document.createElement('a');
a.style.display = 'none';
a.href = blobUrl;
// 从路径中获取文件名
const fileName = path.split('/').pop() || 'document';
a.download = decodeURIComponent(fileName);
document.body.appendChild(a);
a.click();
// 清理
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
}, 100);
} catch (error) {
console.error('下载文件失败:', error);
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
};
// 处理时间范围变更
const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => {
const newParams = new URLSearchParams(searchParams);
if(value) {
newParams.set(field, value);
} else {
newParams.delete(field);
}
newParams.set('page', '1');
setSearchParams(newParams);
};
const handleReset = () => {
const newParams = new URLSearchParams(searchParams);
const searchInput = document.querySelector('input[name="keyword"]');
if(searchInput) {
(searchInput as HTMLInputElement).value = '';
}
// newParams.delete('keyword');
newParams.delete('dateFrom');
newParams.delete('dateTo');
newParams.delete('fileType');
// newParams.delete('reviewStatus');
newParams.delete('sortOrder');
newParams.set('page', '1');
setSearchParams(newParams);
};
// 文件类型选项
const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({
value: type.id.toString(),
label: type.name
}));
// 评查状态选项
// const reviewStatusOptions = [
// { value: 'pass', label: '通过' },
// { value: 'warning', label: '警告' },
// { value: 'fail', label: '不通过' },
// { value: 'pending', label: '待人工确认' }
// ];
// 时间范围选项
// const dateRangeOptions = [
// { value: DateRange.TODAY, label: '今天' },
// { value: DateRange.WEEK, label: '本周' },
// { value: DateRange.MONTH, label: '本月' },
// // { value: DateRange.CUSTOM, label: '自定义时间段' }
// ];
// 定义表格列配置
const columns = [
{
title: "文件名称",
key: "fileName",
width: "30%",
render: (_: unknown, file: ReviewFileUI) => (
<div className="flex">
<div className="flex-shrink-0 flex items-center self-center">
<FileIcon fileName={file.fileName} className="text-lg w-10 h-10" />
</div>
<div className="min-w-0 flex-1 flex flex-col py-2 ml-2">
<div className="font-normal text-base break-words whitespace-normal leading-normal" title={file.fileName}>{file.fileName}</div>
<div className="text-xs text-secondary mt-2">
{file.fileCode}
</div>
</div>
</div>
)
},
{
title: "文件类型",
key: "fileType",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => (
<Tag
color="blue"
size="sm"
>
{file.fileType}
</Tag>
)
},
{
title: "上传时间",
key: "uploadTime",
width: "12%",
render: (_: unknown, file: ReviewFileUI) => {
const [date, time] = file.uploadTime.split(' ');
return (
<div>
<span className="text-base">{date}</span>
<br />
<span className="text-xs text-secondary">{time}</span>
</div>
);
}
},
{
title: "评查统计",
key: "reviewStatus",
width: "12%",
render: (_: unknown, file: ReviewFileUI) =>
// 要文件切分处理完之后,再显示评查统计
file.status === 'Processed' ? (
<div>
{file.passCount > 0 && (
<StatusBadge
status="pass"
text={`通过(${file.passCount})`}
showIcon={true}
className="my-2"
/>
)}
{file.warningCount > 0 && (
<StatusBadge
status="warning"
text={`警告(${file.warningCount})`}
showIcon={true}
className="my-2"
/>
)}
{file.failCount > 0 && (
<StatusBadge
status="fail"
text={`不通过(${file.failCount})`}
showIcon={true}
className="my-2"
/>
)}
{file.manualCount > 0 && (
<StatusBadge
status="pending"
text={`需人工(${file.manualCount})`}
showIcon={true}
className="my-2"
/>
)}
</div>
) : (
<div className="text-sm">
-
</div>
)
},
{
title: "问题摘要",
key: "issues",
width: "20%",
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
},
{
title: "操作",
key: "operation",
width: "14%",
render: (_: unknown, file: ReviewFileUI) => (
<>
<Button
type="default"
size="small"
icon="ri-eye-line"
// to={`/reviews?id=${file.id}`}
onClick={() => handleReviewFileClick(file.id, file.auditStatus)}
disabled={file.status !== 'Processed'}
className="mr-2"
>
</Button>
<Button type="default" size="small" icon="ri-download-2-line" className="mt-1" onClick={() => handleDownload(file.path)}>
</Button>
</>
)
}
];
return (
<div className="review-files-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<div className="flex items-center">
<h2 className="text-xl font-normal"></h2>
<div className="flex items-center ml-4 bg-white px-3 py-1 rounded-md">
<i className="ri-file-list-3-line text-primary text-lg mr-1"></i>
<span className="text-sm text-secondary"></span>
<span className="text-base font-normal text-primary ml-1">{totalCount}</span>
</div>
</div>
<Button type="primary" icon="ri-file-upload-line" to="/files/upload">
</Button>
</div>
{/* 筛选区域 */}
<FilterPanel className="px-3 py-3" noActionDivider={true}
actions={
<>
<Button type="default" icon="ri-refresh-line" onClick={handleReset} className="mr-2 hover:!border-gray-300">
</Button>
</>
}
>
<FilterSelect
label="文件类型"
name="fileType"
value={searchParams.get('fileType') || ''}
options={fileTypeOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/>
{/* <FilterSelect
label="评查状态"
name="reviewStatus"
value={searchParams.get('reviewStatus') || ''}
options={reviewStatusOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/> */}
{/* <FilterSelect
label="时间范围"
name="dateRange"
value={searchParams.get('dateRange') || ''}
options={dateRangeOptions}
onChange={handleFilterChange}
className="mr-2 w-40"
/> */}
<DateRangeFilter
label="时间范围"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
simple={true}
/>
<FilterSelect
label="排序方式"
name="sortOrder"
value={searchParams.get('sortOrder') || 'upload_time_desc'}
onChange={handleFilterChange}
className="w-32"
options={[
{ value: "upload_time_desc", label: "上传时间 ↓" },
{ value: "upload_time_asc", label: "上传时间 ↑" },
// { value: "issue_count_desc", label: "问题数量 ↓" },
// { value: "issue_count_asc", label: "问题数量 ↑" }
]}
/>
<SearchFilter
label="搜索"
placeholder="搜索文件名、合同编号"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
buttonText=""
className="mr-2 flex-1"
/>
</FilterPanel>
{/* 文件列表 */}
<Card >
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table table-auto-height"
/>
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
</div>
);
}