573 lines
18 KiB
TypeScript
573 lines
18 KiB
TypeScript
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>
|
||
);
|
||
} |