完成文档列表页面ui,封装部分上传文件的公共组件,封装请求接口

This commit is contained in:
2025-04-01 22:14:43 +08:00
parent 8fe88c1d15
commit 706cea8705
37 changed files with 4512 additions and 1459 deletions
+700
View File
@@ -0,0 +1,700 @@
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>
);
}