Files
leaudit-platform-frontend/app/routes/documents._index.tsx
T

701 lines
21 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 { useState } from "react";
import { useSearchParams, Link } from "@remix-run/react";
import { type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node";
import { Card } from "~/components/ui/Card";
import { Button } from "~/components/ui/Button";
import { Table } from "~/components/ui/Table";
import { Pagination } from "~/components/ui/Pagination";
import { StatusBadge } from "~/components/ui/StatusBadge";
import { FileTypeTag } from "~/components/ui/FileTypeTag";
import { FileTag } from "~/components/ui/FileTag";
import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel";
import documentsIndexStyles from "~/styles/pages/documents_index.css?url";
// 导入样式
export function links() {
return [
{ rel: "stylesheet", href: documentsIndexStyles }
];
}
// 元数据
export const meta: MetaFunction = () => {
return [
{ title: "文档列表 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "查看和管理系统中的所有文档,包括合同、许可证和行政处罚决定书等" },
];
};
interface DocumentItem {
id: string;
name: string;
documentNumber: string;
type: string;
typeName: string;
size: number;
status: string;
issues: number | null;
uploadTime: string;
fileType: string;
tags?: string[];
}
// 数据加载器
export const loader = async ({ request }: LoaderFunctionArgs) => {
// 获取URL查询参数
const url = new URL(request.url);
const search = url.searchParams.get("search") || "";
const documentType = url.searchParams.get("documentType") || "";
const status = url.searchParams.get("status") || "";
const documentNumber = url.searchParams.get("documentNumber") || "";
const dateFrom = url.searchParams.get("dateFrom") || "";
const dateTo = url.searchParams.get("dateTo") || "";
const page = parseInt(url.searchParams.get("page") || "1", 10);
const pageSize = parseInt(url.searchParams.get("pageSize") || "20", 10);
// 在实际应用中,这里会调用API获取数据
// const response = await fetch(`/api/documents?search=${search}&...`);
// const data = await response.json();
// 使用模拟数据
const mockData = {
documents: [
{
id: "1",
name: "2023年度烟草销售框架合同.pdf",
documentNumber: "XS20230001",
type: "sales-contract",
typeName: "销售合同",
size: 2.5 * 1024 * 1024, // 2.5MB
status: "pass",
issues: 0,
uploadTime: "2023-10-15 15:30",
fileType: "pdf"
},
{
id: "2",
name: "设备采购合同-打印机.docx",
documentNumber: "CG20230052",
type: "purchase-contract",
typeName: "采购合同",
size: 1.2 * 1024 * 1024, // 1.2MB
status: "warning",
issues: 3,
uploadTime: "2023-10-14 09:15",
fileType: "docx"
},
{
id: "3",
name: "烟草零售许可证.pdf",
documentNumber: "ZM2023100345",
type: "license",
typeName: "专卖许可证",
size: 0.8 * 1024 * 1024, // 0.8MB
status: "pending",
issues: null,
uploadTime: "2023-10-13 14:20",
fileType: "pdf"
},
{
id: "4",
name: "非法售烟行政处罚决定书.docx",
documentNumber: "CF20230087",
type: "punishment",
typeName: "行政处罚决定书",
size: 1.5 * 1024 * 1024, // 1.5MB
status: "processing",
issues: null,
uploadTime: "2023-10-10 16:45",
fileType: "docx"
},
{
id: "5",
name: "烟草种植承包协议-2023.pdf",
documentNumber: "CB20230024",
type: "agreement",
typeName: "承包协议",
size: 3.2 * 1024 * 1024, // 3.2MB
status: "fail",
issues: 8,
uploadTime: "2023-10-09 10:30",
fileType: "pdf",
tags: ["测试"]
},
],
total: 156,
page,
pageSize
};
// 返回数据
return Response.json(mockData);
};
// 处理表单提交和删除等操作
export const action = async ({ request }: ActionFunctionArgs) => {
const formData = await request.formData();
const action = formData.get("_action");
// 在实际应用中,这里会根据action类型调用相应的API
// 例如删除文档,批量删除,等等
if (action === "delete") {
const id = formData.get("id");
// await fetch(`/api/documents/${id}`, { method: "DELETE" });
return Response.json({ success: true, message: "文档已成功删除" });
}
if (action === "batchDelete") {
const ids = formData.getAll("ids");
// await fetch(`/api/documents/batch-delete`, {
// method: "POST",
// body: JSON.stringify({ ids }),
// headers: { "Content-Type": "application/json" }
// });
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
}
// 未知操作
return Response.json({ success: false, message: "未知操作" }, { status: 400 });
};
// 文档类型选项
const documentTypeOptions = [
{ value: "sales-contract", label: "销售合同" },
{ value: "purchase-contract", label: "采购合同" },
{ value: "license", label: "专卖许可证" },
{ value: "punishment", label: "行政处罚决定书" },
{ value: "agreement", label: "承包协议" },
];
// 文档状态选项
const documentStatusOptions = [
{ value: "pending", label: "待审核" },
{ value: "processing", label: "审核中" },
{ value: "pass", label: "通过" },
{ value: "warning", label: "警告" },
{ value: "fail", label: "不通过" },
];
// 格式化文件大小
const formatFileSize = (bytes: number) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const sizes = ["Bytes", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
};
// 获取文档类型标签背景颜色
// 此函数已不再需要,改用 FileTypeTag 组件
// const getDocumentTypeTagColor = (type: string): string => {
// const colorMap: Record<string, string> = {
// "sales-contract": "blue",
// "purchase-contract": "green",
// "license": "purple",
// "punishment": "yellow",
// "agreement": "orange",
// "default": "gray"
// };
// return colorMap[type] || colorMap.default;
// };
export default function DocumentsIndex() {
const [searchParams, setSearchParams] = useSearchParams();
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
// 从URL获取当前筛选条件
const search = searchParams.get("search") || "";
const documentType = searchParams.get("documentType") || "";
const status = searchParams.get("status") || "";
const documentNumber = searchParams.get("documentNumber") || "";
const dateFrom = searchParams.get("dateFrom") || "";
const dateTo = searchParams.get("dateTo") || "";
const currentPage = parseInt(searchParams.get("page") || "1", 10);
const pageSize = parseInt(searchParams.get("pageSize") || "20", 10);
// API 返回的模拟数据
const mockData = {
documents: [
{
id: "1",
name: "2023年度烟草销售框架合同.pdf",
documentNumber: "XS20230001",
type: "sales-contract",
typeName: "销售合同",
size: 2.5 * 1024 * 1024, // 2.5MB
status: "pass",
issues: 0,
uploadTime: "2023-10-15 15:30",
fileType: "pdf"
},
{
id: "2",
name: "设备采购合同-打印机.docx",
documentNumber: "CG20230052",
type: "purchase-contract",
typeName: "采购合同",
size: 1.2 * 1024 * 1024, // 1.2MB
status: "warning",
issues: 3,
uploadTime: "2023-10-14 09:15",
fileType: "docx"
},
{
id: "3",
name: "烟草零售许可证.pdf",
documentNumber: "ZM2023100345",
type: "license",
typeName: "专卖许可证",
size: 0.8 * 1024 * 1024, // 0.8MB
status: "pending",
issues: null,
uploadTime: "2023-10-13 14:20",
fileType: "pdf"
},
{
id: "4",
name: "非法售烟行政处罚决定书.docx",
documentNumber: "CF20230087",
type: "punishment",
typeName: "行政处罚决定书",
size: 1.5 * 1024 * 1024, // 1.5MB
status: "processing",
issues: null,
uploadTime: "2023-10-10 16:45",
fileType: "docx"
},
{
id: "5",
name: "烟草种植承包协议-2023.pdf",
documentNumber: "CB20230024",
type: "agreement",
typeName: "承包协议",
size: 3.2 * 1024 * 1024, // 3.2MB
status: "fail",
issues: 8,
uploadTime: "2023-10-09 10:30",
fileType: "pdf",
tags: ["测试"]
},
],
total: 156,
page: currentPage,
pageSize
};
// 分页处理函数
const handlePageChange = (page: number) => {
searchParams.set("page", page.toString());
setSearchParams(searchParams);
};
// 每页条数变更处理函数
const handlePageSizeChange = (size: number) => {
searchParams.set("pageSize", size.toString());
searchParams.set("page", "1"); // 重置到第一页
setSearchParams(searchParams);
};
// 处理文档名称搜索
const handleNameSearch = (value: string) => {
const params = new URLSearchParams(searchParams);
if (value) {
params.set("search", value);
} else {
params.delete("search");
}
params.set("page", "1"); // 重置页码
setSearchParams(params);
};
// 处理文档编号变更
const handleDocumentNumberChange = (value: string) => {
const params = new URLSearchParams(searchParams);
if (value) {
params.set("documentNumber", value);
} else {
params.delete("documentNumber");
}
params.set("page", "1"); // 重置页码
setSearchParams(params);
};
// 处理文档类型变更
const handleDocumentTypeChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const params = new URLSearchParams(searchParams);
if (e.target.value) {
params.set("documentType", e.target.value);
} else {
params.delete("documentType");
}
params.set("page", "1"); // 重置页码
setSearchParams(params);
};
// 处理状态变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const params = new URLSearchParams(searchParams);
if (e.target.value) {
params.set("status", e.target.value);
} else {
params.delete("status");
}
params.set("page", "1"); // 重置页码
setSearchParams(params);
};
// 处理日期范围变更
const handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => {
const params = new URLSearchParams(searchParams);
if (value) {
params.set(field, value);
} else {
params.delete(field);
}
params.set("page", "1"); // 重置页码
setSearchParams(params);
};
// 重置搜索条件
const handleReset = () => {
setSearchParams(new URLSearchParams({
page: "1",
pageSize: pageSize.toString()
}));
};
// 行选择变更处理
const handleRowSelectionChange = (id: string) => {
if (selectedRowKeys.includes(id)) {
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
} else {
setSelectedRowKeys([...selectedRowKeys, id]);
}
};
// 全选处理
const handleSelectAll = (checked: boolean) => {
if (checked) {
setSelectedRowKeys(mockData.documents.map(doc => doc.id));
} else {
setSelectedRowKeys([]);
}
};
// 删除确认
const confirmDelete = (id: string, name: string) => {
if (window.confirm(`确认删除文档 "${name}"`)) {
// 在实际应用中这里会提交表单到action处理
console.log('删除文档:', id, name);
// 更新选中行
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
}
};
// 批量删除确认
const confirmBatchDelete = () => {
if (selectedRowKeys.length === 0) {
alert('请至少选择一个文档');
return;
}
if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) {
// 在实际应用中这里会提交表单到action处理
console.log('批量删除文档IDs:', selectedRowKeys);
// 清空选中行
setSelectedRowKeys([]);
}
};
// 表格列定义
const columns = [
{
title: (
<input
type="checkbox"
checked={selectedRowKeys.length === mockData.documents.length}
onChange={(e) => handleSelectAll(e.target.checked)}
/>
),
key: "selection",
width: "50px",
render: (_: unknown, record: DocumentItem) => (
<input
type="checkbox"
checked={selectedRowKeys.includes(record.id)}
onChange={() => handleRowSelectionChange(record.id)}
/>
)
},
{
title: "文档名称",
key: "name",
render: (_: unknown, record: DocumentItem) => (
<div className="flex items-center m-1">
<FileTag
extension={record.fileType}
showIcon={true}
showText={false}
showBackground={false}
size="lg"
className="mr-2"
/>
<div>
<span className="file-name" title={record.name}>{record.name}</span>
<div className="mt-2 flex inline-block">
<FileTypeTag
type={record.type}
text={record.typeName}
size="sm"
showIcon={false}
/>
{record.tags && record.tags.map((tag: string) => (
<span key={tag} className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">{tag}</span>
))}
</div>
</div>
</div>
)
},
{
title: "文档编号",
key: "documentNumber",
render: (_: unknown, record: DocumentItem) => (
<span className="document-number">{record.documentNumber}</span>
)
},
{
title: "文件大小",
key: "size",
render: (_: unknown, record: DocumentItem) => formatFileSize(record.size)
},
{
title: "审核状态",
key: "status",
render: (_: unknown, record: DocumentItem) => (
<StatusBadge status={record.status} showIcon={false} />
)
},
{
title: "问题数量",
key: "issues",
render: (_: unknown, record: DocumentItem) => (
record.issues === null ? "-" : record.issues
)
},
{
title: "上传时间",
key: "uploadTime",
render: (_: unknown, record: DocumentItem) => record.uploadTime
},
{
title: "操作",
key: "actions",
width: "280px",
render: (_: unknown, record: DocumentItem) => (
<div className="operations-cell">
{record.status === "pending" ? (
<Link
to={`/documents/${record.id}/review`}
className="mr-1 hover:underline"
>
<i className="ri-play-circle-line"></i>
</Link>
) : record.status === "processing" ? (
<Link
to={`/documents/${record.id}/progress`}
className="mr-1 hover:underline"
>
<i className="ri-eye-line"></i>
</Link>
) : (
<Link
to={`/documents/${record.id}`}
className="mr-1 hover:underline"
>
<i className="ri-eye-line"></i>
</Link>
)}
<Link
to={`/documents/${record.id}/edit`}
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
>
<i className="ri-edit-line"></i>
</Link>
<button
type="button"
className="mr-1 text-gray-500 hover:underline hover:text-gray-700"
onClick={() => alert(`下载文档: ${record.name}`)}
>
<i className="ri-download-line"></i>
</button>
<button
type="button"
className="text-error hover:underline hover:text-red-700"
onClick={() => confirmDelete(record.id, record.name)}
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
)
}
];
return (
<div className="documents-page">
{/* 页面头部 */}
<div className="flex justify-between items-center mb-4">
<h2 className="text-xl font-medium"></h2>
<div>
<Button
type="primary"
icon="ri-upload-line"
to="/documents/upload"
className="hover:text-white"
>
</Button>
</div>
</div>
{/* 搜索筛选区 */}
<FilterPanel
actions={
<>
<Button
type="default"
icon="ri-refresh-line"
onClick={handleReset}
className="mr-2"
>
</Button>
<Button
type="primary"
icon="ri-search-line"
onClick={() => {
// 保持当前筛选条件,刷新数据
// 在实际应用中,这里可能需要触发某些操作
}}
>
</Button>
</>
}
noActionDivider={true}
>
<SearchFilter
label="文档名称"
placeholder="请输入文档名称"
value={search}
onSearch={handleNameSearch}
instantSearch={true}
className="mr-2 w-50"
/>
<SearchFilter
label="文档编号"
placeholder="请输入文档编号"
value={documentNumber}
onSearch={handleDocumentNumberChange}
instantSearch={true}
className="mr-2 w-50"
/>
<FilterSelect
label="文档类型"
name="documentType"
value={documentType}
options={documentTypeOptions}
onChange={handleDocumentTypeChange}
className="mr-2 w-30"
/>
<FilterSelect
label="审核状态"
name="status"
value={status}
options={documentStatusOptions}
onChange={handleStatusChange}
className="mr-2 w-50"
/>
<DateRangeFilter
label="上传时间"
startDate={dateFrom}
endDate={dateTo}
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
onEndDateChange={(value) => handleDateChange('dateTo', value)}
className="flex-1"
simple={true}
/>
</FilterPanel>
{/* 数据表格 */}
<Card>
<div className="mb-3 flex items-center justify-between">
<div>
<Button
type="default"
icon="ri-delete-bin-line"
onClick={confirmBatchDelete}
className="mr-2"
disabled={selectedRowKeys.length === 0}
>
</Button>
<Button
type="default"
icon="ri-download-line"
>
</Button>
</div>
<div className="text-sm text-secondary">
<span className="font-medium text-primary">{mockData.total}</span>
</div>
</div>
<div className="overflow-x-auto">
<Table
columns={columns}
dataSource={mockData.documents}
rowKey="id"
emptyText="暂无数据"
/>
</div>
{/* 分页 */}
<Pagination
currentPage={currentPage}
total={mockData.total}
pageSize={pageSize}
onChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
pageSizeOptions={[10, 20, 50, 100]}
/>
</Card>
</div>
);
}
// 错误边界处理
export function ErrorBoundary() {
return (
<div className="error-container">
<h1 className="text-xl font-bold text-red-500"></h1>
<p></p>
</div>
);
}