import { useState, useEffect, useRef, useCallback } 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 { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen'; 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 page = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); // 获取文档类型列表,用于筛选条件 const typesResponse = await getDocumentTypes({ pageSize: 500 }); const documentTypes = typesResponse.data?.types || []; const documentTypeOptions = documentTypes.map(type => ({ value: type.id, label: type.name })); // 初始返回空数据,将在客户端根据 sessionStorage 中的 reviewType 加载实际数据 return Response.json({ documents: [], total: 0, page, pageSize, documentTypeOptions, initialLoad: true // 标记这是初始加载 }); }; // 定义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 = { "-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([]); const loaderData = useLoaderData(); const fetcher = useFetcher(); const navigate = useNavigate(); // 存储从 sessionStorage 获取的 reviewType const [reviewType, setReviewType] = useState(null); // 添加页面加载状态管理 const [isLoadingData, setIsLoadingData] = useState(true); const [documents, setDocuments] = useState([]); const [total, setTotal] = useState(0); const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions); const dataCache = useRef(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); // 客户端数据请求 const fetchData = useCallback(async (storedReviewType: string) => { setIsLoadingData(true); loadingBarService.show(); try { // 构建搜索参数 const searchParams = { name: search || undefined, documentNumber: documentNumber || undefined, documentType: documentType || undefined, auditStatus: auditStatus || undefined, fileStatus: fileStatus || undefined, dateFrom: dateFrom || undefined, dateTo: dateTo || undefined, reviewType: storedReviewType || undefined, page: currentPage, pageSize }; // 获取文档列表 const documentsResponse = await getDocuments(searchParams); if (documentsResponse.error) { throw new Error(documentsResponse.error); } // 获取经过过滤的文档类型列表 const filteredTypesResponse = await getDocumentTypes({ pageSize: 500, reviewType: storedReviewType || undefined }); const filteredDocumentTypes = filteredTypesResponse.data?.types || []; const filteredOptions = filteredDocumentTypes.map(type => ({ value: type.id, label: type.name })); // 更新状态 setDocuments(documentsResponse.data?.documents || []); setTotal(documentsResponse.data?.total || 0); setFilteredDocumentTypeOptions(filteredOptions); } catch (error) { console.error('获取文档列表失败:', error); toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误')); } finally { setIsLoadingData(false); loadingBarService.hide(); } }, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize]); // 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据 useEffect(() => { try { if (typeof window !== 'undefined') { const storedReviewType = sessionStorage.getItem('reviewType'); setReviewType(storedReviewType); // 如果有 reviewType,则加载数据 if (storedReviewType) { fetchData(storedReviewType); } } } catch (error) { console.error('获取 sessionStorage 中的 reviewType 失败:', error); } }, [fetchData]); // 监听 URL 参数变化,重新获取数据 useEffect(() => { if (reviewType) { fetchData(reviewType); } }, [searchParams, fetchData, reviewType]); // 使用并更新缓存数据 useEffect(() => { // 如果有缓存数据,先显示缓存,再在后台加载新数据 if (dataCache.current) { setIsLoadingData(false); } else { // 显示加载状态 - 确保显示加载条 loadingBarService.show(); setIsLoadingData(true); } // 设置缓存数据 dataCache.current = loaderData; // 处理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) => { 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) => { 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(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) => { 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: ( handleSelectAll(e.target.checked)} /> ), key: "selection", width: "50px", render: (_: unknown, record: DocumentUI) => ( handleRowSelectionChange(record.id.toString())} /> ) }, { title: "文档名称", key: "name", width:'25%', render: (_: unknown, record: DocumentUI) => (
{record.name}
{record.isTest && ( 测试 )}
) }, { title: "文档编号", key: "documentNumber", width:'10%', render: (_: unknown, record: DocumentUI) => ( {record.documentNumber} ) }, { 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 (
{status.label}
); } }, { 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 (
{statusInfo.label}
); } }, { 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) => (
{(record.auditStatus === 0 || record.auditStatus == null) ? ( ) : record.auditStatus === 3 ? ( //record.auditStatus === 3 目前这个状态不存在,所以除了待审核(0)-开始审核,其他都是审核中(2)-查看 查看进度 ) : ( 查看 )} 修改
) } ]; return (
{/* 页面内容,在加载时降低不透明度但不隐藏内容 */}
{/* 页面头部 */}

文档列表

{isLoadingData ? (
) : (
{total} 条记录
)}
{/* 搜索筛选区 */} } noActionDivider={true} >
handleDateChange('dateFrom', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)} simple={true} colorMode="light" />
{/* 数据表格 */} {isLoadingData && }
{total} 条记录
{isLoadingData && documents.length === 0 ? ( ) : ( )} {/* 分页 */} ); } // 错误边界处理 export function ErrorBoundary() { return (

出错了

加载文档列表时出现错误。请刷新页面或联系系统管理员。

); }