411 lines
12 KiB
TypeScript
411 lines
12 KiB
TypeScript
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>
|
||
);
|
||
} |