647 lines
20 KiB
TypeScript
647 lines
20 KiB
TypeScript
import { useState } from "react";
|
||
import { useSearchParams, Link, useLoaderData, useFetcher } 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";
|
||
import { getDocuments, deleteDocument, type DocumentUI, getFileDownloadUrl } from "~/api/files/documents";
|
||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||
|
||
// 导入样式
|
||
export function links() {
|
||
return [
|
||
{ rel: "stylesheet", href: documentsIndexStyles }
|
||
];
|
||
}
|
||
|
||
// 元数据
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "文档列表 - 中国烟草AI合同及卷宗审核系统" },
|
||
{ name: "description", content: "查看和管理系统中的所有文档,包括合同、许可证和行政处罚决定书等" },
|
||
];
|
||
};
|
||
|
||
// 数据加载器
|
||
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") || "10", 10);
|
||
|
||
// 构建搜索参数
|
||
const searchParams = {
|
||
name: search || undefined,
|
||
documentNumber: documentNumber || undefined,
|
||
documentType: documentType || undefined,
|
||
status: status || undefined,
|
||
dateFrom: dateFrom || undefined,
|
||
dateTo: dateTo || undefined,
|
||
page,
|
||
pageSize
|
||
};
|
||
|
||
// 获取文档列表
|
||
const documentsResponse = await getDocuments(searchParams);
|
||
if (documentsResponse.error) {
|
||
throw new Error(documentsResponse.error);
|
||
}
|
||
|
||
// 获取文档类型列表,用于筛选条件
|
||
const typesResponse = await getDocumentTypes();
|
||
const documentTypes = typesResponse.data?.types || [];
|
||
const documentTypeOptions = documentTypes.map(type => ({
|
||
value: type.id,
|
||
label: type.name
|
||
}));
|
||
|
||
return Response.json({
|
||
documents: documentsResponse.data?.documents || [],
|
||
total: documentsResponse.data?.total || 0,
|
||
page,
|
||
pageSize,
|
||
documentTypeOptions
|
||
});
|
||
};
|
||
|
||
// 处理表单提交和删除等操作
|
||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||
const formData = await request.formData();
|
||
const action = formData.get("_action");
|
||
|
||
if (action === "delete") {
|
||
const id = formData.get("id") as string;
|
||
const response = await deleteDocument(id);
|
||
|
||
if (response.error) {
|
||
return Response.json({ success: false, message: response.error }, { status: response.status || 500 });
|
||
}
|
||
|
||
return Response.json({ success: true, message: "文档已成功删除" });
|
||
}
|
||
|
||
if (action === "batchDelete") {
|
||
const ids = formData.getAll("ids") as string[];
|
||
|
||
// 批量删除处理
|
||
const results = await Promise.all(ids.map(id => deleteDocument(id)));
|
||
const failures = results.filter(r => r.error);
|
||
|
||
if (failures.length > 0) {
|
||
return Response.json({
|
||
success: false,
|
||
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
||
}, { status: 400 });
|
||
}
|
||
|
||
return Response.json({ success: true, message: `已成功删除${ids.length}个文档` });
|
||
}
|
||
|
||
// 未知操作
|
||
return Response.json({ success: false, message: "未知操作" }, { status: 400 });
|
||
};
|
||
|
||
// 文档状态选项
|
||
const documentStatusOptions = [
|
||
{ value: "waiting", 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[]>([]);
|
||
const loaderData = useLoaderData<typeof loader>();
|
||
const fetcher = useFetcher();
|
||
|
||
// 从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") || "10", 10);
|
||
|
||
// 获取API返回的数据
|
||
const { documents, total, documentTypeOptions } = loaderData;
|
||
|
||
// 分页处理函数
|
||
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 = () => {
|
||
// 直接重置所有筛选条件的DOM值
|
||
const resetInput = (selector: string, value: string = "") => {
|
||
const element = document.querySelector<HTMLInputElement | HTMLSelectElement>(selector);
|
||
if (element) {
|
||
element.value = value;
|
||
|
||
// 对于搜索框,触发其input事件以激活搜索
|
||
if (element instanceof HTMLInputElement && element.type === "text") {
|
||
// 创建一个input事件
|
||
const event = new Event('input', { bubbles: true });
|
||
element.dispatchEvent(event);
|
||
}
|
||
}
|
||
};
|
||
|
||
// 重置所有搜索字段
|
||
resetInput('input[placeholder="请输入文档名称"]');
|
||
resetInput('input[placeholder="请输入文档编号"]');
|
||
resetInput('select[name="documentType"]');
|
||
resetInput('select[name="status"]');
|
||
resetInput('input[name="dateFrom"]');
|
||
resetInput('input[name="dateTo"]');
|
||
|
||
// 重置URL参数
|
||
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(documents.map((doc: DocumentUI) => doc.id.toString()));
|
||
} else {
|
||
setSelectedRowKeys([]);
|
||
}
|
||
};
|
||
|
||
// 下载文档
|
||
const handleDownload = async (path: string, fileName: string) => {
|
||
console.log('handleDownload',path,fileName)
|
||
try {
|
||
// 使用API获取授权的下载链接
|
||
// const { data, error } = await getFileDownloadUrl(path);
|
||
|
||
// if (error || !data?.downloadUrl) {
|
||
// console.error('获取下载链接失败:', error);
|
||
// alert('获取下载链接失败: ' + (error || '未知错误'));
|
||
// return;
|
||
// }
|
||
|
||
// 创建一个隐藏的a标签并点击它
|
||
const a = document.createElement('a');
|
||
// a.href = data.downloadUrl;
|
||
a.href = path;
|
||
a.download = fileName; // 设置下载的文件名
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
} catch (err) {
|
||
console.error('下载文件失败:', err);
|
||
alert('下载文件失败: ' + (err instanceof Error ? err.message : '未知错误'));
|
||
}
|
||
};
|
||
|
||
// 删除文档
|
||
const handleDelete = (id: string, name: string) => {
|
||
if (window.confirm(`确认删除文档 "${name}"?`)) {
|
||
// 使用fetcher提交表单
|
||
const formData = new FormData();
|
||
formData.append('_action', 'delete');
|
||
formData.append('id', id);
|
||
|
||
fetcher.submit(formData, { method: 'post' });
|
||
|
||
// 更新选中行
|
||
setSelectedRowKeys(selectedRowKeys.filter(key => key !== id));
|
||
}
|
||
};
|
||
|
||
// 批量删除
|
||
const handleBatchDelete = () => {
|
||
if (selectedRowKeys.length === 0) {
|
||
alert('请至少选择一个文档');
|
||
return;
|
||
}
|
||
|
||
if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) {
|
||
// 使用fetcher提交表单
|
||
const formData = new FormData();
|
||
formData.append('_action', 'batchDelete');
|
||
|
||
// 添加所有选中的ID
|
||
selectedRowKeys.forEach(id => {
|
||
formData.append('ids', id);
|
||
});
|
||
|
||
fetcher.submit(formData, { method: 'post' });
|
||
|
||
// 清空选中行
|
||
setSelectedRowKeys([]);
|
||
}
|
||
};
|
||
|
||
// 表格列定义
|
||
const columns = [
|
||
{
|
||
title: (
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedRowKeys.length === documents.length}
|
||
onChange={(e) => handleSelectAll(e.target.checked)}
|
||
/>
|
||
),
|
||
key: "selection",
|
||
width: "50px",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<input
|
||
type="checkbox"
|
||
checked={selectedRowKeys.includes(record.id.toString())}
|
||
onChange={() => handleRowSelectionChange(record.id.toString())}
|
||
/>
|
||
)
|
||
},
|
||
{
|
||
title: "文档名称",
|
||
key: "name",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<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}
|
||
fileType={record.fileType}
|
||
/>
|
||
{record.isTest && (
|
||
<span className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">测试</span>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
{
|
||
title: "文档编号",
|
||
key: "documentNumber",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<span className="document-number">{record.documentNumber}</span>
|
||
)
|
||
},
|
||
{
|
||
title: "文件大小",
|
||
key: "size",
|
||
width: "100px",
|
||
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
|
||
},
|
||
{
|
||
title: "审核状态",
|
||
key: "status",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<StatusBadge status={record.status} showIcon={false} />
|
||
)
|
||
},
|
||
{
|
||
title: "问题数量",
|
||
key: "issues",
|
||
width:"60px",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
record.issues === null ? "-" : record.issues
|
||
)
|
||
},
|
||
{
|
||
title: "上传时间",
|
||
key: "uploadTime",
|
||
render: (_: unknown, record: DocumentUI) => record.uploadTime
|
||
},
|
||
{
|
||
title: "操作",
|
||
key: "actions",
|
||
width: "280px",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<div className="operations-cell">
|
||
{record.status === "waiting" ? (
|
||
<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/edit?id=${record.id}`}
|
||
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={() => handleDownload(record.path, record.name)}
|
||
>
|
||
<i className="ri-download-line"></i>
|
||
下载
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="text-error hover:underline hover:text-red-700"
|
||
onClick={() => handleDelete(record.id.toString(), 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={handleBatchDelete}
|
||
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">{total}</span> 条记录
|
||
</div>
|
||
</div>
|
||
|
||
<div className="overflow-x-auto">
|
||
<Table
|
||
columns={columns}
|
||
dataSource={documents}
|
||
rowKey="id"
|
||
emptyText="暂无数据"
|
||
/>
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
total={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>
|
||
);
|
||
}
|