Files
leaudit-platform-frontend/app/routes/rules-files.tsx
T
2025-04-15 23:24:32 +08:00

411 lines
12 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 } 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 } 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,
updateReviewStatus,
type ReviewFileUI
} from "~/api/evaluation_points/rules-files";
import { getDocumentTypes } from "~/api/document-types/document-types";
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 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,
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 { files, documentTypes, totalCount, currentPage, pageSize } = useLoaderData<typeof loader>();
const [searchParams, setSearchParams] = useSearchParams();
// 处理筛选条件变更
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 handleConfirmStatus = async (id: string, status: string) => {
try {
await updateReviewStatus(id, status);
// 刷新页面获取最新数据
const newParams = new URLSearchParams(searchParams);
setSearchParams(newParams);
} catch (error) {
console.error('更新评查状态失败:', error);
// 可以在这里添加错误提示
}
};
// 渲染问题摘要
const renderIssues = (file: ReviewFileUI) => {
// 如果评查状态为通过,显示"所有评查点均通过"
if (file.reviewStatus === 'pass') {
return (
<div className="text-sm text-success">
<i className="ri-check-double-line mr-1"></i>
</div>
);
}
// 其他状态显示占位符
return <div className="text-sm text-secondary">-</div>;
};
// 文件类型选项
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 items-center">
<FileIcon fileName={file.fileName} className="mr-2 text-lg flex-shrink-0 w-10 h-10" />
<div>
<div className="font-normal text-base break-words" title={file.fileName}>{file.fileName}</div>
<div className="text-xs text-secondary mt-1">
{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) => (
<StatusBadge
status={file.reviewStatus}
text={REVIEW_STATUS_LABELS[file.reviewStatus]}
showIcon={true}
/>
)
},
{
title: "问题摘要",
key: "issues",
width: "20%",
render: (_: unknown, file: ReviewFileUI) => renderIssues(file)
},
{
title: "操作",
key: "operation",
width: "14%",
render: (_: unknown, file: ReviewFileUI) => (
<>
{file.reviewStatus === 'pending' ? (
<Button
type="primary"
size="small"
icon="ri-check-double-line"
className="mr-2"
onClick={() => handleConfirmStatus(file.id, 'pass')}
>
</Button>
) : (
<Button
type="default"
size="small"
icon="ri-eye-line"
to={`/files/${file.id}`}
className="mr-2"
>
</Button>
)}
<Button type="default" size="small" icon="ri-download-2-line">
</Button>
</>
)
}
];
return (
<div className="p-6 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}>
<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"
/>
<SearchFilter
label="搜索"
placeholder="搜索文件名、合同编号或关键词"
value={searchParams.get('keyword') || ''}
onSearch={handleSearch}
buttonText=""
className="mr-2 w-64"
/>
<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: "问题数量 ↑" }
]}
/>
</FilterPanel>
{/* 文件列表 */}
<Card >
<Table
columns={columns}
dataSource={files}
rowKey="id"
emptyText="暂无文件数据"
className="files-table"
/>
{/* 分页组件 */}
{totalCount > 0 && (
<Pagination
currentPage={currentPage}
total={totalCount}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
showTotal={true}
showPageSizeChanger={true}
pageSizeOptions={[10, 20, 30, 50]}
/>
)}
</Card>
</div>
);
}