949 lines
32 KiB
TypeScript
949 lines
32 KiB
TypeScript
import { useState, useEffect, useRef } from "react";
|
|
import { useSearchParams, useLoaderData, useFetcher, useNavigate,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 { 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 } from "~/api/files/documents";
|
|
import { getDocumentTypes } from "~/api/document-types/document-types";
|
|
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
|
import { toastService } from "~/components/ui/Toast";
|
|
import { messageService } from "~/components/ui/MessageModal";
|
|
import { loadingBarService } from "~/components/ui/LoadingBar";
|
|
import { DOCUMENT_URL } from "~/api/client";
|
|
|
|
// 导入样式
|
|
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 auditStatus = url.searchParams.get("auditStatus") || "";
|
|
const documentNumber = url.searchParams.get("documentNumber") || "";
|
|
const fileStatus = url.searchParams.get("fileStatus") || "";
|
|
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,
|
|
auditStatus: auditStatus || undefined,
|
|
fileStatus: fileStatus || undefined,
|
|
dateFrom: dateFrom || undefined,
|
|
dateTo: dateTo || undefined,
|
|
page,
|
|
pageSize
|
|
};
|
|
|
|
try {
|
|
// 获取文档列表
|
|
const documentsResponse = await getDocuments(searchParams);
|
|
// console.log('documentsResponse---1--',JSON.stringify(documentsResponse,null,2));
|
|
if (documentsResponse.error) {
|
|
throw new Error(documentsResponse.error);
|
|
}
|
|
|
|
// 获取文档类型列表,用于筛选条件,设置较大的pageSize确保获取所有数据
|
|
const typesResponse = await getDocumentTypes({ pageSize: 500 });
|
|
// console.log('typesResponse-----',typesResponse);
|
|
const documentTypes = typesResponse.data?.types || [];
|
|
const documentTypeOptions = documentTypes.map(type => ({
|
|
value: type.id,
|
|
label: type.name
|
|
}));
|
|
|
|
// console.log('typesResponse-----',JSON.stringify(documentsResponse.data?.documents[1],null,2));
|
|
return Response.json({
|
|
documents: documentsResponse.data?.documents || [],
|
|
total: documentsResponse.data?.total || 0,
|
|
page,
|
|
pageSize,
|
|
documentTypeOptions
|
|
});
|
|
} catch (error) {
|
|
console.error('获取文档列表失败:', error);
|
|
return Response.json({
|
|
error: '获取文档列表失败',
|
|
status: 500
|
|
}, { status: 500 });
|
|
}
|
|
};
|
|
|
|
// 定义action返回的数据类型
|
|
interface ActionResponse {
|
|
result: boolean;
|
|
message: string;
|
|
}
|
|
|
|
// 审核状态筛选选项
|
|
const auditStatusOptions = [
|
|
// { value: "", label: "全部" },
|
|
// { value: "-2", label: "警告" },
|
|
{ value: "-1", label: "不通过" },
|
|
{ value: "0", label: "待审核" },
|
|
{ value: "1", label: "通过" },
|
|
{ value: "2", label: "审核中" },
|
|
];
|
|
|
|
// 文件处理状态选项
|
|
const fileProcessingStatusOptions = [
|
|
{ value: "Waiting", label: "上传中", icon: "ri-loader-line", color: "blue" },
|
|
{ value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" },
|
|
{ value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" },
|
|
{ value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" },
|
|
{ value: "Failed", label: "抽取异常", icon: "ri-close-circle-line", color: "red" },
|
|
{ value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" },
|
|
];
|
|
|
|
// 文件状态筛选选项
|
|
const fileStatusOptions = [
|
|
// { value: "", label: "全部" },
|
|
{ value: "Waiting", label: "上传中" },
|
|
{ value: "Cutting", label: "切分中" },
|
|
{ value: "Extractioning", label: "抽取中" },
|
|
{ value: "Evaluationing", label: "评查中" },
|
|
{ value: "Failed", label: "抽取异常" },
|
|
{ value: "Processed", label: "已完成" },
|
|
];
|
|
|
|
// 审核状态选项及样式
|
|
const auditStatusMapping: Record<string, { label: string; color: string; icon: string }> = {
|
|
"-1": { label: "不通过", color: "red", icon: "ri-close-line" },
|
|
"-2": { label: "警告", color: "yellow", icon: "ri-alert-line" },
|
|
"0": { label: "待审核", color: "blue", icon: "ri-time-line" },
|
|
"1": { label: "通过", color: "green", icon: "ri-check-line" },
|
|
"2": { label: "审核中", color: "purple", icon: "ri-search-line" },
|
|
};
|
|
|
|
// 格式化文件大小
|
|
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];
|
|
};
|
|
|
|
// 处理表单提交和删除等操作
|
|
export const action = async ({ request }: ActionFunctionArgs) => {
|
|
try {
|
|
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({ result: false, message: response.error }, { status: response.status || 500 });
|
|
}
|
|
return Response.json({ result: 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({
|
|
result: false,
|
|
message: `删除失败: ${failures.map(f => f.error).join(', ')}`
|
|
}, { status: 400 });
|
|
}
|
|
|
|
return Response.json({ result: true, message: `已成功删除${ids.length}个文档` });
|
|
}
|
|
|
|
// 未知操作
|
|
return Response.json({ result: false, message: "未知操作" }, { status: 400 });
|
|
} catch (error) {
|
|
console.error('处理表单提交和删除等操作失败:', error);
|
|
return Response.json({
|
|
result: false,
|
|
error: error instanceof Error ? error.message : "处理表单提交和删除等操作失败"
|
|
}, { status: 500 });
|
|
}
|
|
};
|
|
|
|
export default function DocumentsIndex() {
|
|
const [searchParams, setSearchParams] = useSearchParams();
|
|
const [selectedRowKeys, setSelectedRowKeys] = useState<string[]>([]);
|
|
const loaderData = useLoaderData<typeof loader>();
|
|
const fetcher = useFetcher<ActionResponse>();
|
|
const navigate = useNavigate();
|
|
|
|
// 添加页面加载状态管理
|
|
const [isLoadingData, setIsLoadingData] = useState(true);
|
|
const dataCache = useRef<typeof loaderData | null>(null);
|
|
|
|
// 从URL获取当前筛选条件
|
|
const search = searchParams.get("search") || "";
|
|
const documentType = searchParams.get("documentType") || "";
|
|
const auditStatus = searchParams.get("auditStatus") || "";
|
|
const documentNumber = searchParams.get("documentNumber") || "";
|
|
const fileStatus = searchParams.get("fileStatus") || "";
|
|
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;
|
|
|
|
// 使用并更新缓存数据
|
|
useEffect(() => {
|
|
// 如果有缓存数据,先显示缓存,再在后台加载新数据
|
|
if (dataCache.current) {
|
|
setIsLoadingData(false);
|
|
} else {
|
|
// 显示加载状态 - 确保显示加载条
|
|
loadingBarService.show();
|
|
setIsLoadingData(true);
|
|
}
|
|
|
|
// 设置缓存数据
|
|
dataCache.current = loaderData;
|
|
|
|
// 数据加载完成后,执行额外的延迟以确保UI效果
|
|
setIsLoadingData(false);
|
|
loadingBarService.hide();
|
|
|
|
// 处理loader错误
|
|
if (loaderData.error) {
|
|
toastService.error(loaderData.error);
|
|
}
|
|
}, [loaderData]);
|
|
|
|
// 使用useEffect监听fetcher状态变化并显示Toast
|
|
useEffect(() => {
|
|
if (fetcher.data && fetcher.state === 'idle') {
|
|
if (fetcher.data.result) {
|
|
toastService.success(fetcher.data.message);
|
|
} else if (fetcher.data.message) {
|
|
toastService.error(fetcher.data.message);
|
|
}
|
|
}
|
|
}, [fetcher.data, fetcher.state]);
|
|
|
|
// 分页处理函数
|
|
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("auditStatus", e.target.value);
|
|
} else {
|
|
params.delete("auditStatus");
|
|
}
|
|
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) => {
|
|
try {
|
|
const downloadUrl = `${DOCUMENT_URL}${path}`;
|
|
|
|
// 使用fetch获取文件内容
|
|
const response = await fetch(downloadUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
// 将响应转换为Blob
|
|
const blob = await response.blob();
|
|
|
|
// 创建Blob URL
|
|
const blobUrl = URL.createObjectURL(blob);
|
|
|
|
// 创建一个隐藏的a标签并点击它
|
|
const a = document.createElement('a');
|
|
a.style.display = 'none';
|
|
a.href = blobUrl;
|
|
// 从路径中获取文件名
|
|
const fileName = path.split('/').pop() || 'document';
|
|
a.download = decodeURIComponent(fileName);
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
|
|
// 清理
|
|
setTimeout(() => {
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(blobUrl);
|
|
}, 100);
|
|
} catch (error) {
|
|
console.error('下载文件失败:', error);
|
|
toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
}
|
|
};
|
|
|
|
// 删除文档
|
|
const handleDelete = (id: string, name: string, fileStatus: string) => {
|
|
// 禁止删除处理中的文件
|
|
if (fileStatus !== "Processed" && fileStatus !== "Failed") {
|
|
toastService.warning("文件正在处理中,无法删除");
|
|
return;
|
|
}
|
|
|
|
messageService.show({
|
|
title: "确认删除",
|
|
message: `确定要删除文档"${name}"吗?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
onConfirm: () => {
|
|
const form = new FormData();
|
|
form.append("_action", "delete");
|
|
form.append("id", id);
|
|
|
|
fetcher.submit(form, { method: "post" });
|
|
}
|
|
});
|
|
};
|
|
|
|
// 批量删除
|
|
const handleBatchDelete = () => {
|
|
if (selectedRowKeys.length === 0) {
|
|
toastService.error('请至少选择一个文档');
|
|
return;
|
|
}
|
|
|
|
// 检查是否有正在处理中的文件
|
|
const hasProcessingFiles = documents.some((doc: DocumentUI) =>
|
|
selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed'
|
|
);
|
|
|
|
if (hasProcessingFiles) {
|
|
toastService.error('存在服务器未处理完成的文件,请重新选择需要删除的文件');
|
|
return;
|
|
}
|
|
|
|
messageService.show({
|
|
title: "确认批量删除",
|
|
message: `确认删除选中的 ${selectedRowKeys.length} 个文档?`,
|
|
type: "warning",
|
|
confirmText: "删除",
|
|
cancelText: "取消",
|
|
onConfirm: () => {
|
|
// 使用fetcher提交表单
|
|
const formData = new FormData();
|
|
formData.append('_action', 'batchDelete');
|
|
|
|
// 添加所有选中的ID
|
|
selectedRowKeys.forEach(id => {
|
|
formData.append('ids', id);
|
|
});
|
|
|
|
fetcher.submit(formData, { method: 'post' });
|
|
|
|
// 清空选中行
|
|
setSelectedRowKeys([]);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 处理文件状态变更
|
|
const handleFileStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
|
const params = new URLSearchParams(searchParams);
|
|
if (e.target.value) {
|
|
params.set("fileStatus", e.target.value);
|
|
} else {
|
|
params.delete("fileStatus");
|
|
}
|
|
params.set("page", "1"); // 重置页码
|
|
setSearchParams(params);
|
|
};
|
|
|
|
// 导出列表
|
|
const handleExport = async () => {
|
|
// 如果没有文档,显示提示信息
|
|
if (documents.length === 0) {
|
|
// alert('当前页面没有文档可供导出');
|
|
toastService.error('当前页面没有文档可供导出');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
// 创建一个ZIP文件
|
|
const JSZip = await import('jszip').then(module => module.default);
|
|
const zip = new JSZip();
|
|
|
|
// 准备所有下载任务
|
|
const downloadTasks = documents.map(async (doc: DocumentUI) => {
|
|
try {
|
|
if (!doc.path) {
|
|
console.warn(`文档 ${doc.name} 没有有效的路径`);
|
|
return;
|
|
}
|
|
|
|
const downloadUrl = `${DOCUMENT_URL}${doc.path}`;
|
|
|
|
// 获取文件内容
|
|
const response = await fetch(downloadUrl);
|
|
if (!response.ok) {
|
|
throw new Error(`下载失败: ${response.status} ${response.statusText}`);
|
|
}
|
|
|
|
// 将响应转换为Blob
|
|
const blob = await response.blob();
|
|
|
|
// 从路径中获取文件名
|
|
const fileName = doc.path.split('/').pop() || doc.name;
|
|
|
|
// 添加到ZIP文件
|
|
zip.file(decodeURIComponent(fileName), blob);
|
|
|
|
return { success: true, name: fileName };
|
|
} catch (error) {
|
|
console.error(`下载文件 ${doc.name} 失败:`, error);
|
|
return { success: false, name: doc.name, error };
|
|
}
|
|
});
|
|
|
|
// 等待所有下载任务完成
|
|
const results = await Promise.all(downloadTasks);
|
|
|
|
// 计算成功和失败的数量
|
|
const succeeded = results.filter(r => r && r.success).length;
|
|
const failed = results.filter(r => r && !r.success).length;
|
|
|
|
if (succeeded === 0) {
|
|
// alert('所有文件下载失败');
|
|
toastService.error('所有文件下载失败');
|
|
return;
|
|
}
|
|
|
|
// 生成ZIP文件
|
|
const zipBlob = await zip.generateAsync({ type: 'blob' });
|
|
|
|
// 创建下载链接
|
|
const url = URL.createObjectURL(zipBlob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `文档导出_${new Date().toISOString().slice(0, 10)}.zip`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
|
|
// 清理
|
|
setTimeout(() => {
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}, 100);
|
|
|
|
// 显示结果消息
|
|
if (failed > 0) {
|
|
// alert(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`);
|
|
toastService.warning(`成功导出 ${succeeded} 个文件,${failed} 个文件失败`);
|
|
} else {
|
|
// alert(`成功导出 ${succeeded} 个文件`);
|
|
toastService.success(`成功导出 ${succeeded} 个文件`);
|
|
}
|
|
} catch (error) {
|
|
console.error('导出文件失败:', error);
|
|
// alert(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
toastService.error(`导出文件失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
|
}
|
|
};
|
|
|
|
// 开始审核
|
|
const handleReviewFileClick = async (fileId: number, auditStatus: number | null) => {
|
|
// 检查audit_status是否为0,如果是则更新为2
|
|
if (auditStatus === 0 || auditStatus === null) {
|
|
try {
|
|
|
|
const response = await updateDocumentAuditStatus(fileId.toString(), 2);
|
|
if (response.error) {
|
|
console.error('更新文件审核状态失败:', response.error);
|
|
toastService.error('更新文件审核状态失败:' + (response.error || '未知错误'));
|
|
return;
|
|
}
|
|
} catch (error) {
|
|
console.error('更新文件审核状态时出错:', error);
|
|
toastService.error('更新文件审核状态时出错:' + (error instanceof Error ? error.message : '未知错误'));
|
|
return;
|
|
}
|
|
}
|
|
|
|
// 导航到评查详情页
|
|
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
|
};
|
|
|
|
|
|
// 表格列定义
|
|
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",
|
|
width:'25%',
|
|
render: (_: unknown, record: DocumentUI) => (
|
|
<div className="flex m-1">
|
|
<FileTag
|
|
extension={record.fileType}
|
|
showIcon={true}
|
|
showText={false}
|
|
showBackground={false}
|
|
size="lg"
|
|
className="mr-2 flex-shrink-0 self-center"
|
|
/>
|
|
<div className="overflow-hidden">
|
|
<span className="file-name break-words block whitespace-normal leading-normal" title={record.name}>{record.name}</span>
|
|
<div className="mt-2 flex inline-block">
|
|
<FileTypeTag
|
|
type={record.type}
|
|
typeName={record.typeName}
|
|
text={record.typeName}
|
|
size="sm"
|
|
showIcon={false}
|
|
fileType={record.fileType}
|
|
colorMode="light"
|
|
/>
|
|
{record.isTest && (
|
|
<span className="ml-2 text-xs bg-gray-100 text-gray-500 px-1 rounded">测试</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
},
|
|
{
|
|
title: "文档编号",
|
|
key: "documentNumber",
|
|
width:'10%',
|
|
render: (_: unknown, record: DocumentUI) => (
|
|
<span className="document-number">{record.documentNumber}</span>
|
|
)
|
|
},
|
|
{
|
|
title: "文件大小",
|
|
key: "size",
|
|
width: "10%",
|
|
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
|
|
},
|
|
{
|
|
title: "文件状态",
|
|
key: "fileStatus",
|
|
width:'10%',
|
|
render: (_: unknown, record: DocumentUI) => {
|
|
// 处理fileStatus为null或undefined的情况
|
|
// console.log('fileStatus',record.fileStatus)
|
|
const fileStatus = record.fileStatus || "-";
|
|
const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) ||
|
|
fileProcessingStatusOptions[0];
|
|
const isSpinning = fileStatus !== "Processed" && fileStatus !== "Failed";
|
|
return (
|
|
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${status.color}-100 text-${status.color}-800`}>
|
|
<i className={`${status.icon} ${isSpinning ? "animate-spin" : ""} mr-1`}></i>
|
|
<span>{status.label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: "审核状态",
|
|
key: "auditStatus",
|
|
width:"10%",
|
|
render: (_: unknown, record: DocumentUI) => {
|
|
// 处理auditStatus为null或undefined的情况,默认为0(待审核)
|
|
const auditStatus = record.auditStatus != null ? record.auditStatus : 0;
|
|
const statusKey = auditStatus.toString();
|
|
const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"];
|
|
return (
|
|
<div className={`inline-flex items-center px-2 py-1 rounded-full text-xs bg-${statusInfo.color}-100 text-${statusInfo.color}-800`}>
|
|
<i className={`${statusInfo.icon} mr-1`}></i>
|
|
<span>{statusInfo.label}</span>
|
|
</div>
|
|
);
|
|
}
|
|
},
|
|
{
|
|
title: "问题数量",
|
|
key: "issues",
|
|
width:"7%",
|
|
render: (_: unknown, record: DocumentUI) => (
|
|
record.issues === null ? "-" : record.issues
|
|
)
|
|
},
|
|
{
|
|
title: "上传时间",
|
|
key: "uploadTime",
|
|
width:"10%",
|
|
render: (_: unknown, record: DocumentUI) => record.uploadTime
|
|
},
|
|
{
|
|
title: "操作",
|
|
key: "actions",
|
|
width: "20%",
|
|
render: (_: unknown, record: DocumentUI) => (
|
|
<div className="operations-cell">
|
|
{(record.auditStatus === 0 || record.auditStatus == null) ? (
|
|
<button
|
|
onClick={() => handleReviewFileClick(record.id, record.auditStatus)}
|
|
disabled={record.fileStatus !== 'Processed'}
|
|
className={`mr-1 ${
|
|
record.fileStatus === 'Processed'
|
|
? 'hover:underline hover:cursor-pointer text-primary'
|
|
: 'text-gray-400 cursor-not-allowed opacity-60'
|
|
}`}
|
|
>
|
|
<i className="ri-play-circle-line"></i>
|
|
开始审核
|
|
</button>
|
|
) : record.auditStatus === 3 ? (
|
|
//record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看
|
|
<Link
|
|
to={`/documents/${record.id}/progress`}
|
|
className="mr-1 hover:underline"
|
|
>
|
|
<i className="ri-eye-line"></i>
|
|
查看进度
|
|
</Link>
|
|
) : (
|
|
<Link
|
|
to={`/reviews?id=${record.id}&previousRoute=documents`}
|
|
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)}
|
|
>
|
|
<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, record.fileStatus)}
|
|
>
|
|
<i className="ri-delete-bin-line"></i>
|
|
删除
|
|
</button>
|
|
</div>
|
|
)
|
|
}
|
|
];
|
|
|
|
return (
|
|
<div className="documents-page relative">
|
|
{/* 页面内容,在加载时降低不透明度但不隐藏内容 */}
|
|
<div className={isLoadingData ? "opacity-70 pointer-events-none transition-opacity" : "transition-opacity"}>
|
|
{/* 页面头部 */}
|
|
<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="/files/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}
|
|
>
|
|
<div className="grid grid-cols-2 md:grid-cols-3 gap-4 w-full">
|
|
<SearchFilter
|
|
label="文档名称"
|
|
placeholder="请输入文档名称"
|
|
value={search}
|
|
onSearch={handleNameSearch}
|
|
instantSearch={true}
|
|
/>
|
|
|
|
<SearchFilter
|
|
label="文档编号"
|
|
placeholder="请输入文档编号"
|
|
value={documentNumber}
|
|
onSearch={handleDocumentNumberChange}
|
|
instantSearch={true}
|
|
/>
|
|
|
|
<FilterSelect
|
|
label="文档类型"
|
|
name="documentType"
|
|
value={documentType}
|
|
options={documentTypeOptions}
|
|
onChange={handleDocumentTypeChange}
|
|
/>
|
|
|
|
<FilterSelect
|
|
label="文件状态"
|
|
name="fileStatus"
|
|
value={fileStatus}
|
|
options={fileStatusOptions}
|
|
onChange={handleFileStatusChange}
|
|
/>
|
|
|
|
<FilterSelect
|
|
label="审核状态"
|
|
name="auditStatus"
|
|
value={auditStatus}
|
|
options={auditStatusOptions}
|
|
onChange={handleStatusChange}
|
|
/>
|
|
|
|
<DateRangeFilter
|
|
label="上传时间"
|
|
startDate={dateFrom}
|
|
endDate={dateTo}
|
|
onStartDateChange={(value) => handleDateChange('dateFrom', value)}
|
|
onEndDateChange={(value) => handleDateChange('dateTo', value)}
|
|
simple={true}
|
|
colorMode="light"
|
|
/>
|
|
</div>
|
|
</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"
|
|
onClick={handleExport}
|
|
>
|
|
导出列表
|
|
</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>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
// 错误边界处理
|
|
export function ErrorBoundary() {
|
|
return (
|
|
<div className="error-container">
|
|
<h1 className="text-xl font-bold text-red-500">出错了</h1>
|
|
<p>加载文档列表时出现错误。请刷新页面或联系系统管理员。</p>
|
|
</div>
|
|
);
|
|
}
|