306 lines
9.8 KiB
TypeScript
306 lines
9.8 KiB
TypeScript
import React from 'react';
|
||
import { json, type MetaFunction } from '@remix-run/node';
|
||
import { useLoaderData, useSearchParams, Form } from '@remix-run/react';
|
||
import { Button } from '~/components/ui/Button';
|
||
import { Card } from '~/components/ui/Card';
|
||
import { Table } from '~/components/ui/Table';
|
||
import { Breadcrumb } from '~/components/layout/Breadcrumb';
|
||
import type { File } from '~/models/file';
|
||
import { REVIEW_STATUS_LABELS, REVIEW_STATUS_COLORS } from '~/models/file';
|
||
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "中国烟草AI合同及卷宗审核系统 - 文件列表" },
|
||
{ name: "description", content: "评查文件列表" }
|
||
];
|
||
};
|
||
|
||
export const handle = {
|
||
breadcrumb: '文件列表'
|
||
};
|
||
|
||
interface LoaderData {
|
||
files: File[];
|
||
documentTypes: {
|
||
id: string;
|
||
name: string;
|
||
}[];
|
||
totalCount: number;
|
||
}
|
||
|
||
export async function loader({ request }) {
|
||
const url = new URL(request.url);
|
||
const documentTypeId = url.searchParams.get("documentTypeId") || "";
|
||
const reviewStatus = url.searchParams.get("reviewStatus") || "";
|
||
const keyword = url.searchParams.get("keyword") || "";
|
||
|
||
// 模拟数据,实际项目中应从API获取
|
||
const files: 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"
|
||
},
|
||
{
|
||
id: "2",
|
||
fileName: "烟草制品购销合同(2023-12).docx",
|
||
fileType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
documentTypeId: "1",
|
||
documentTypeName: "合同文档",
|
||
fileSize: 1024 * 1024 * 1.2, // 1.2MB
|
||
uploaderId: "1",
|
||
uploaderName: "张三",
|
||
status: "completed",
|
||
reviewStatus: "warning",
|
||
createdAt: "2023-12-23 09:15",
|
||
updatedAt: "2023-12-23 10:30"
|
||
},
|
||
{
|
||
id: "3",
|
||
fileName: "专卖管理处罚决定书(2023-145).pdf",
|
||
fileType: "application/pdf",
|
||
documentTypeId: "3",
|
||
documentTypeName: "行政处罚决定书",
|
||
fileSize: 1024 * 1024 * 3.1, // 3.1MB
|
||
uploaderId: "2",
|
||
uploaderName: "李四",
|
||
status: "completed",
|
||
reviewStatus: "fail",
|
||
createdAt: "2023-12-22 16:45",
|
||
updatedAt: "2023-12-22 18:20"
|
||
},
|
||
{
|
||
id: "4",
|
||
fileName: "2023年第四季度采购合同.docx",
|
||
fileType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
||
documentTypeId: "1",
|
||
documentTypeName: "合同文档",
|
||
fileSize: 1024 * 1024 * 1.8, // 1.8MB
|
||
uploaderId: "3",
|
||
uploaderName: "王五",
|
||
status: "completed",
|
||
reviewStatus: "pass",
|
||
createdAt: "2023-12-20 11:20",
|
||
updatedAt: "2023-12-20 14:35"
|
||
},
|
||
{
|
||
id: "5",
|
||
fileName: "广告宣传协议书.pdf",
|
||
fileType: "application/pdf",
|
||
documentTypeId: "1",
|
||
documentTypeName: "合同文档",
|
||
fileSize: 1024 * 1024 * 0.9, // 0.9MB
|
||
uploaderId: "2",
|
||
uploaderName: "李四",
|
||
status: "pending",
|
||
reviewStatus: "pending",
|
||
createdAt: "2023-12-18 15:30",
|
||
updatedAt: "2023-12-18 15:30"
|
||
}
|
||
];
|
||
|
||
const documentTypes = [
|
||
{ id: "1", name: "合同文档" },
|
||
{ id: "2", name: "专卖许可证" },
|
||
{ id: "3", name: "行政处罚决定书" },
|
||
{ id: "4", name: "其他文档" }
|
||
];
|
||
|
||
// 过滤数据
|
||
let filteredFiles = [...files];
|
||
|
||
if (documentTypeId) {
|
||
filteredFiles = filteredFiles.filter(file => file.documentTypeId === documentTypeId);
|
||
}
|
||
|
||
if (reviewStatus) {
|
||
filteredFiles = filteredFiles.filter(file => file.reviewStatus === reviewStatus);
|
||
}
|
||
|
||
if (keyword) {
|
||
const lowerKeyword = keyword.toLowerCase();
|
||
filteredFiles = filteredFiles.filter(file =>
|
||
file.fileName.toLowerCase().includes(lowerKeyword)
|
||
);
|
||
}
|
||
|
||
return json<LoaderData>({
|
||
files: filteredFiles,
|
||
documentTypes,
|
||
totalCount: files.length
|
||
});
|
||
}
|
||
|
||
export default function FilesList() {
|
||
const { files, documentTypes } = useLoaderData<typeof loader>();
|
||
const [searchParams] = useSearchParams();
|
||
|
||
// 文件大小格式化
|
||
const formatFileSize = (bytes: number): string => {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
|
||
};
|
||
|
||
// 获取文件图标
|
||
const getFileIcon = (fileType: string): string => {
|
||
if (fileType.includes('pdf')) return 'ri-file-pdf-line';
|
||
if (fileType.includes('word')) return 'ri-file-word-line';
|
||
if (fileType.includes('excel') || fileType.includes('spreadsheet')) return 'ri-file-excel-line';
|
||
if (fileType.includes('image')) return 'ri-file-image-line';
|
||
return 'ri-file-text-line';
|
||
};
|
||
|
||
return (
|
||
<div>
|
||
<Breadcrumb
|
||
items={[
|
||
{ title: '文件管理', to: '/files' },
|
||
{ title: '文件列表', to: '/files' }
|
||
]}
|
||
/>
|
||
|
||
<div className="flex justify-between items-center mb-4">
|
||
<div className="flex items-center">
|
||
<h2 className="text-xl font-medium">评查文件列表</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-bold text-primary ml-1">{files.length}</span>
|
||
</div>
|
||
</div>
|
||
<Button type="primary" icon="ri-file-upload-line" to="/files/new">
|
||
上传新文件
|
||
</Button>
|
||
</div>
|
||
|
||
<Card className="mb-5">
|
||
<Form method="get" className="flex flex-wrap items-end gap-3">
|
||
<div className="w-48">
|
||
<label className="form-label">文件类型</label>
|
||
<select
|
||
name="documentTypeId"
|
||
className="form-select w-full"
|
||
defaultValue={searchParams.get('documentTypeId') || ''}
|
||
>
|
||
<option value="">全部</option>
|
||
{documentTypes.map(type => (
|
||
<option key={type.id} value={type.id}>{type.name}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="w-48">
|
||
<label className="form-label">评查状态</label>
|
||
<select
|
||
name="reviewStatus"
|
||
className="form-select w-full"
|
||
defaultValue={searchParams.get('reviewStatus') || ''}
|
||
>
|
||
<option value="">全部</option>
|
||
{Object.entries(REVIEW_STATUS_LABELS).map(([value, label]) => (
|
||
<option key={value} value={value}>{label}</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
|
||
<div className="w-64">
|
||
<label className="form-label">文件名称</label>
|
||
<div className="search-box">
|
||
<input
|
||
type="text"
|
||
name="keyword"
|
||
className="form-input"
|
||
placeholder="搜索文件名称"
|
||
defaultValue={searchParams.get('keyword') || ''}
|
||
/>
|
||
<button type="submit" className="ant-btn ant-btn-primary">
|
||
<i className="ri-search-line"></i>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<Button type="default" className="ml-2">重置</Button>
|
||
</Form>
|
||
</Card>
|
||
|
||
<Table
|
||
columns={[
|
||
{
|
||
title: "文件名称",
|
||
render: (_, record: File) => (
|
||
<div className="flex items-center">
|
||
<i className={`${getFileIcon(record.fileType)} text-lg text-gray-500 mr-2`}></i>
|
||
<div>
|
||
<div className="font-medium">{record.fileName}</div>
|
||
<div className="text-xs text-gray-500">{formatFileSize(record.fileSize)}</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
{ title: "文档类型", dataIndex: "documentTypeName" },
|
||
{
|
||
title: "评查状态",
|
||
dataIndex: "reviewStatus",
|
||
render: (value) => (
|
||
<span className={`status-badge status-${REVIEW_STATUS_COLORS[value]}`}>
|
||
<i className={`ri-${value === 'pass' ? 'checkbox-circle' : value === 'warning' ? 'error-warning' : value === 'fail' ? 'close-circle' : 'time'}-line mr-1`}></i>
|
||
{REVIEW_STATUS_LABELS[value]}
|
||
</span>
|
||
)
|
||
},
|
||
{
|
||
title: "上传人",
|
||
dataIndex: "uploaderName"
|
||
},
|
||
{
|
||
title: "上传时间",
|
||
dataIndex: "createdAt"
|
||
},
|
||
{
|
||
title: "操作",
|
||
render: (_, record: File) => (
|
||
<div className="space-x-2">
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
icon="ri-file-search-line"
|
||
to={`/reviews/${record.id}`}
|
||
>
|
||
查看
|
||
</Button>
|
||
{record.status === 'pending' && (
|
||
<Button
|
||
type="primary"
|
||
size="small"
|
||
icon="ri-play-circle-line"
|
||
>
|
||
开始评查
|
||
</Button>
|
||
)}
|
||
<Button
|
||
type="danger"
|
||
size="small"
|
||
icon="ri-delete-bin-line"
|
||
>
|
||
删除
|
||
</Button>
|
||
</div>
|
||
)
|
||
}
|
||
]}
|
||
dataSource={files}
|
||
rowKey="id"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|