import { type MetaFunction, type LoaderFunctionArgs } from "@remix-run/node"; import { useLoaderData, useSearchParams, useNavigate } from "@remix-run/react"; import { useEffect, useState, useCallback } from "react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import { FileIcon } from "~/components/ui/FileIcon"; import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel"; import { Pagination } from "~/components/ui/Pagination"; import { Table } from "~/components/ui/Table"; import { StatusBadge } from "~/components/ui/StatusBadge"; import { FileTypeTag, links as fileTypeTagLinks } from "~/components/ui/FileTypeTag"; import { NumberSkeleton, TableRowSkeleton, LoadingIndicator } from "~/components/ui/SkeletonScreen"; import rulesFilesStyles from "~/styles/pages/rules-files.css?url"; import { getReviewFiles, type ReviewFileUI, updateDocumentAuditStatus, type DocumentSearchParams } from "~/api/evaluation_points/rules-files"; import { getDocumentTypes } from "~/api/document-types/document-types"; import { toastService } from "~/components/ui/Toast"; // 导入axios下载文件方法 import { downloadFile } from "~/api/axios-client"; export const links = () => [ { rel: "stylesheet", href: rulesFilesStyles }, ...fileTypeTagLinks() ]; export const handle = { breadcrumb: "评查文件列表" }; export const meta: MetaFunction = () => { return [ { title: "评查文件列表 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "管理系统中所有上传的评查文件,支持按文件类型、评查状态进行筛选" }, { name: "keywords", content: "评查文件,合同审核,中国烟草,文件管理" } ]; }; // 日期范围枚举 export enum DateRange { ALL = 'all', TODAY = 'today', WEEK = 'week', MONTH = 'month', CUSTOM = 'custom' } // 评查状态标签映射 export const REVIEW_STATUS_LABELS: Record = { 'pass': '通过', 'warning': '警告', 'fail': '不通过', 'pending': '待人工确认' }; // 加载评查文件列表 export async function loader({ request }: LoaderFunctionArgs) { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo } = await getUserSession(request); // 获取分页参数 const url = new URL(request.url); const currentPage = parseInt(url.searchParams.get("page") || "1", 10); const pageSize = parseInt(url.searchParams.get("pageSize") || "10", 10); try { // 获取文档类型列表 const typesResponse = await getDocumentTypes({pageSize:500}); const documentTypes = typesResponse.data?.types || []; // 返回初始空数据,客户端将根据 sessionStorage 中的 reviewType 加载实际数据 return Response.json({ files: [], documentTypes, totalCount: 0, currentPage, pageSize, userInfo, // 传递用户信息到客户端 initialLoad: true }); } catch (error) { console.error('加载评查文件列表失败:', error); return Response.json({ result: false, message: error instanceof Error ? error.message : '加载评查文件列表失败' }, { status: 500 }); } } export default function RulesFiles() { const navigate = useNavigate(); const { files: initialFiles, documentTypes: allDocumentTypes, totalCount: initialTotal, currentPage, pageSize, userInfo, result, message } = useLoaderData(); const [searchParams, setSearchParams] = useSearchParams(); const dateFrom = searchParams.get('dateFrom') || ''; const dateTo = searchParams.get('dateTo') || ''; // 添加状态管理 const [files, setFiles] = useState(initialFiles); const [documentTypes, setDocumentTypes] = useState(allDocumentTypes); const [totalCount, setTotalCount] = useState(initialTotal); const [isLoading, setIsLoading] = useState(true); const [reviewType, setReviewType] = useState(null); // 保存/恢复 查询参数 的 sessionStorage key const SEARCH_PARAMS_STORAGE_KEY = 'rulesFiles.searchParams'; const persistSearchParams = useCallback((params: URLSearchParams) => { if (typeof window !== 'undefined') { sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString()); } }, []); // 首次进入列表页且 URL 无查询参数时,尝试恢复上次保存的参数 useEffect(() => { if (typeof window === 'undefined') return; const hasAnyParam = Array.from(searchParams.keys()).length > 0; const stored = sessionStorage.getItem(SEARCH_PARAMS_STORAGE_KEY); if (!hasAnyParam && stored) { setSearchParams(new URLSearchParams(stored)); } // 仅在初始渲染时检查 // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // 处理初始加载数据loader的错误 useEffect(() => { if(result === false && message) { toastService.error(message); } }, [result, message]); // 客户端数据请求 const fetchData = useCallback(async (params: Record) => { setIsLoading(true); try { // 构建搜索参数 const searchParams: DocumentSearchParams = { fileType: params.fileType || undefined, reviewStatus: params.reviewStatus || undefined, dateFrom: params.dateFrom || undefined, dateTo: params.dateTo || undefined, keyword: params.keyword || undefined, sortOrder: params.sortOrder || 'upload_time_desc', page: parseInt(params.page || "1", 10), pageSize: parseInt(params.pageSize || "10", 10) }; // 根据 reviewType 添加类型过滤 if (reviewType === 'contract') { searchParams.fileType = 'contract'; } else if (reviewType === 'record') { // 在 API 层处理 type_id 为 2 或 3 的过滤 searchParams.fileType = 'record'; } // 如果用户手动选择了文件类型,优先使用用户选择的 if (params.fileType) { searchParams.fileType = params.fileType; } // 从loader data中获取用户ID const userId = userInfo?.user_id?.toString(); // 获取文件列表 const filesResponse = await getReviewFiles(searchParams, null, userId); if (filesResponse.error) { throw new Error(filesResponse.error); } setFiles(filesResponse.data?.files || []); setTotalCount(filesResponse.data?.total || 0); } catch (error) { console.error('获取评查文件列表失败:', error); toastService.error('获取评查文件列表失败: ' + (error instanceof Error ? error.message : '未知错误')); } finally { setIsLoading(false); } }, [reviewType]); // 在组件挂载时从 sessionStorage 获取 reviewType 并加载数据 useEffect(() => { try { if (typeof window !== 'undefined') { const storedReviewType = sessionStorage.getItem('reviewType'); // 根据 reviewType 过滤文档类型选项 if (storedReviewType) { setReviewType(storedReviewType); if (storedReviewType === 'contract') { // 只保留 id=1 的选项 const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 1); setDocumentTypes(filteredTypes); } else if (storedReviewType === 'record') { // 只保留 id=2 和 id=3 的选项 const filteredTypes = allDocumentTypes.filter((type: {id: number}) => type.id === 2 || type.id === 3); setDocumentTypes(filteredTypes); } // 直接使用 storedReviewType 构建搜索参数 const currentParams = Object.fromEntries(searchParams.entries()); const apiSearchParams: DocumentSearchParams = { fileType: currentParams.fileType || undefined, reviewStatus: currentParams.reviewStatus || undefined, dateFrom: currentParams.dateFrom || undefined, dateTo: currentParams.dateTo || undefined, keyword: currentParams.keyword || undefined, sortOrder: currentParams.sortOrder || 'upload_time_desc', page: parseInt(currentParams.page || "1", 10), pageSize: parseInt(currentParams.pageSize || "10", 10) }; // 根据 storedReviewType 添加类型过滤 if (storedReviewType === 'contract') { apiSearchParams.fileType = '1'; } else if (storedReviewType === 'record') { apiSearchParams.fileType = 'record'; } // 如果用户手动选择了文件类型,优先使用用户选择的 if (currentParams.fileType) { apiSearchParams.fileType = currentParams.fileType; } // 设置加载状态 setIsLoading(true); // 从loader data中获取用户ID const userId = userInfo?.user_id?.toString(); // 获取文件列表 getReviewFiles(apiSearchParams, null, userId) .then(filesResponse => { if (filesResponse.error) { throw new Error(filesResponse.error); } setFiles(filesResponse.data?.files || []); setTotalCount(filesResponse.data?.total || 0); }) .catch(error => { console.error('获取评查文件列表失败:', error); toastService.error('获取评查文件列表失败: ' + (error instanceof Error ? error.message : '未知错误')); }) .finally(() => { setIsLoading(false); }); } } } catch (error) { console.error('获取 sessionStorage 中的 reviewType 失败:', error); } }, [allDocumentTypes, searchParams]); // 监听 URL 参数变化,重新获取数据 useEffect(() => { if (reviewType) { fetchData(Object.fromEntries(searchParams.entries())); } }, [searchParams, fetchData, reviewType]); // 处理筛选条件变更 const handleFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; const newParams = new URLSearchParams(searchParams); if (value) { newParams.set(name, value); } else { newParams.delete(name); } // 切换筛选条件时,重置到第一页 newParams.set('page', '1'); persistSearchParams(newParams); setSearchParams(newParams); }; // 处理搜索操作 const handleSearch = (keyword: string) => { const newParams = new URLSearchParams(searchParams); if (keyword) { newParams.set('keyword', keyword); } else { newParams.delete('keyword'); } // 搜索时,重置到第一页 newParams.set('page', '1'); persistSearchParams(newParams); setSearchParams(newParams); }; // 处理页码变更 const handlePageChange = (page: number) => { const newParams = new URLSearchParams(searchParams); newParams.set('page', page.toString()); persistSearchParams(newParams); setSearchParams(newParams); }; // 处理每页条数变更 const handlePageSizeChange = (size: number) => { const newParams = new URLSearchParams(searchParams); newParams.set('pageSize', size.toString()); newParams.set('page', '1'); // 改变每页条数时重置为第一页 persistSearchParams(newParams); setSearchParams(newParams); }; // 查看评查文件 const handleReviewFileClick = async (fileId: string, auditStatus: number | null) => { // 检查audit_status是否为0,如果是则更新为2 if (auditStatus === 0 || auditStatus === null) { try { // 从loader data中获取用户ID const userId = userInfo?.user_id?.toString(); if (!userId) { toastService.error('用户身份验证失败'); return; } const response = await updateDocumentAuditStatus(fileId, 2, userId); if (response.error) { throw new Error(response.error); } } catch (error) { console.error('更新文件审核状态时出错:', error); toastService.error(`更新文件审核状态时出错:${error instanceof Error ? error.message : '未知错误'}`); return; } } // 导航到评查详情页 // 在离开当前页前保存当前查询参数,返回时可恢复 if (typeof window !== 'undefined') { sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, searchParams.toString()); } navigate(`/reviews?id=${fileId}&previousRoute=rulesFiles`); }; // 渲染问题摘要 const renderIssues = (file: ReviewFileUI) => { // 如果文件状态为完成 if (file.status === 'Processed') { // 如果没有问题,显示"所有评查点均通过" if (file.warningCount <= 0 && file.failCount <= 0) { return (
所有评查点均通过
); } // 如果评查状态为不通过,显示"统计分数为:{file.score || 0}。分数低于80分。" // if (file.reviewStatus === 'fail') { // return ( //
// 统计分数为:{file.score || 0}。分数低于80分。 //
// ); // } // 显示问题列表 if (file.issues && file.issues.length > 0) { // 最多显示2个问题 const displayIssues = file.issues.slice(0, 2); return (
{displayIssues.map((issue, index) => (
{issue.message}
))} {file.issues.length > 2 && (
还有 {file.issues.length - 2} 个问题...
)}
); } } // 其他状态显示占位符 return
-
; }; // 下载文件 const handleDownload = async (path: string) => { try { // 使用axios封装的下载方法 const blob = await downloadFile(path); // 创建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 handleDateChange = (field: 'dateFrom' | 'dateTo', value: string) => { const newParams = new URLSearchParams(searchParams); if(value) { newParams.set(field, value); } else { newParams.delete(field); } newParams.set('page', '1'); setSearchParams(newParams); }; const handleReset = () => { const newParams = new URLSearchParams(); const searchInput = document.querySelector('input[name="keyword"]'); if(searchInput) { (searchInput as HTMLInputElement).value = ''; } if (typeof window !== 'undefined') { sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY); } setSearchParams(newParams); }; // 文件类型选项 const fileTypeOptions = documentTypes.map((type: {id: number, name: string}) => ({ value: type.id.toString(), label: type.name })); // 定义表格列配置 const columns = [ { title: "文件名称", key: "fileName", width: "30%", render: (_: unknown, file: ReviewFileUI) => (
{file.fileName}
文件编号:{file.fileCode}
) }, { title: "文件类型", key: "fileType", width: "12%", render: (_: unknown, file: ReviewFileUI) => ( ) }, { title: "上传时间", key: "uploadTime", width: "12%", render: (_: unknown, file: ReviewFileUI) => { const [date, time] = file.uploadTime.split(' '); return (
{date}
{time}
); } }, { title: "评查统计", key: "reviewStatus", width: "12%", render: (_: unknown, file: ReviewFileUI) => // 要文件切分处理完之后,再显示评查统计 file.status === 'Processed' ? (
{file.passCount > 0 && ( )} {file.warningCount > 0 && ( )} {file.failCount > 0 && ( )} {file.manualCount > 0 && ( )}
) : (
-
) }, { title: "问题摘要", key: "issues", width: "20%", render: (_: unknown, file: ReviewFileUI) => renderIssues(file) }, { title: "操作", key: "operation", width: "14%", render: (_: unknown, file: ReviewFileUI) => ( <> ) } ]; return (
{/* 页面头部 */}

评查文件列表

总文件数: {isLoading ? ( ) : ( {totalCount} )}
{/* 筛选区域 */} } > {/* */} {/* */} handleDateChange('dateFrom', value)} onEndDateChange={(value) => handleDateChange('dateTo', value)} simple={true} colorMode="light" /> {/* 文件列表 */}
{isLoading && } {isLoading && files.length === 0 ? ( ) : ( )} {/* 分页组件 */} {totalCount > 0 && ( )} ); } // 错误边界 export function ErrorBoundary() { return (

出错了

加载评查文件列表时发生错误。请稍后再试,或联系管理员。

); }