33f10896a0
2. 接入合同起草功能。
1955 lines
72 KiB
TypeScript
1955 lines
72 KiB
TypeScript
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 documentVersionStyles from "~/styles/components/document-version.css?url";
|
||
import { deleteDocument, getDocumentsListFromAPI, type DocumentUI, type DocumentVersionUI } from "~/api/files/documents";
|
||
// import { IssuesDiff } from "~/components/ui/IssuesDiff";
|
||
import { ResultStats } from "~/components/ui/ResultStats";
|
||
import { getDocumentTypes } from "~/api/document-types/document-types";
|
||
import { updateDocumentAuditStatus } from "~/api/evaluation_points/rules-files";
|
||
import { appendContractAttachments, uploadContractTemplate } from "~/api/files/files-upload";
|
||
import { toastService } from "~/components/ui/Toast";
|
||
import { messageService } from "~/components/ui/MessageModal";
|
||
import { loadingBarService } from "~/components/ui/LoadingBar";
|
||
// import { DOCUMENT_URL } from "~/api/axios-client";
|
||
|
||
// 导入样式
|
||
export function links() {
|
||
return [
|
||
{ rel: "stylesheet", href: documentsIndexStyles },
|
||
{ rel: "stylesheet", href: documentVersionStyles }
|
||
];
|
||
}
|
||
|
||
// 元数据
|
||
export const meta: MetaFunction = () => {
|
||
return [
|
||
{ title: "文档列表 - 中国烟草AI合同及卷宗审核系统" },
|
||
{ name: "description", content: "查看和管理系统中的所有文档,包括合同、许可证和行政处罚决定书等" },
|
||
];
|
||
};
|
||
|
||
// 数据加载器
|
||
export const loader = async ({ request }: LoaderFunctionArgs) => {
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
// 获取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);
|
||
|
||
// 获取文档类型列表(服务端需要显式传递 token,客户端依赖 axios 拦截器)
|
||
const typesResponse = await getDocumentTypes({ pageSize: 500 }, frontendJWT);
|
||
const documentTypes = typesResponse.data?.types || [];
|
||
const documentTypeOptions = documentTypes.map(type => ({
|
||
value: type.id,
|
||
label: type.name
|
||
}));
|
||
|
||
// 初始返回空数据,将在客户端根据 sessionStorage 中的 documentTypeIds 加载实际数据
|
||
return Response.json({
|
||
documents: [],
|
||
total: 0,
|
||
page,
|
||
pageSize,
|
||
documentTypeOptions,
|
||
userInfo, // 传递用户信息到客户端
|
||
frontendJWT, // 传递 JWT 到客户端
|
||
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<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 { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = await getUserSession(request);
|
||
|
||
if (!userInfo?.user_id) {
|
||
return Response.json({ result: false, message: "用户身份验证失败" }, { status: 401 });
|
||
}
|
||
|
||
const userId = userInfo.user_id.toString();
|
||
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, userId, frontendJWT);
|
||
|
||
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, userId, frontendJWT)));
|
||
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();
|
||
|
||
// 存储从 sessionStorage 获取的 documentTypeIds
|
||
const [documentTypeIds, setDocumentTypeIds] = useState<number[] | null>(null);
|
||
|
||
// 添加页面加载状态管理
|
||
const [isLoadingData, setIsLoadingData] = useState(true);
|
||
const [documents, setDocuments] = useState<DocumentUI[]>([]);
|
||
const [total, setTotal] = useState(0);
|
||
const [filteredDocumentTypeOptions, setFilteredDocumentTypeOptions] = useState(loaderData.documentTypeOptions);
|
||
const dataCache = useRef<typeof loaderData | null>(null);
|
||
|
||
// 添加一个状态来跟踪是否执行了删除操作
|
||
const [isDeleting, setIsDeleting] = useState(false);
|
||
|
||
// 辅助函数:从 localStorage 获取用户ID(与 token 管理保持一致)
|
||
const getUserId = useCallback((): string | undefined => {
|
||
if (typeof window === 'undefined') return undefined;
|
||
|
||
const userInfoStr = localStorage.getItem('user_info');
|
||
if (!userInfoStr) return undefined;
|
||
|
||
try {
|
||
const userInfoData = JSON.parse(userInfoStr);
|
||
return userInfoData.user_id?.toString();
|
||
} catch (error) {
|
||
console.error('解析 localStorage 用户信息失败:', error);
|
||
return undefined;
|
||
}
|
||
}, []);
|
||
|
||
// 版本管理:展开的文档行
|
||
const [expandedRows, setExpandedRows] = useState<Set<number>>(new Set());
|
||
// 版本管理:正在加载历史版本的文档
|
||
const [loadingHistory, setLoadingHistory] = useState<Set<number>>(new Set());
|
||
|
||
// 附件追加和模板上传状态
|
||
const [showAttachmentUpload, setShowAttachmentUpload] = useState<boolean>(false);
|
||
const [showTemplateUpload, setShowTemplateUpload] = useState<boolean>(false);
|
||
const [selectedDocumentId, setSelectedDocumentId] = useState<number | null>(null);
|
||
const [selectedDocumentName, setSelectedDocumentName] = useState<string | null>(null);
|
||
const [selectedDocumentVersion, setSelectedDocumentVersion] = useState<number | null>(null);
|
||
const [attachmentFiles, setAttachmentFiles] = useState<File[]>([]);
|
||
const [templateFile, setTemplateFile] = useState<File | null>(null);
|
||
const [attachmentMergeMode, setAttachmentMergeMode] = useState<'overwrite' | 'new'>('new');
|
||
const [attachmentRemark, setAttachmentRemark] = useState<string>("");
|
||
const [attachmentUploading, setAttachmentUploading] = useState<boolean>(false);
|
||
const [templateUploading, setTemplateUploading] = useState<boolean>(false);
|
||
|
||
// 查询参数记忆 key 与保存/恢复方法
|
||
const SEARCH_PARAMS_STORAGE_KEY = 'documents.searchParams';
|
||
const persistSearchParams = (params: URLSearchParams) => {
|
||
if (typeof window !== 'undefined') {
|
||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, params.toString());
|
||
}
|
||
};
|
||
|
||
// 首次进入且 URL 无任何查询参数时,尝试从 sessionStorage 恢复
|
||
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
|
||
}, []);
|
||
|
||
// 从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 (typeIds: number[]) => {
|
||
setIsLoadingData(true);
|
||
loadingBarService.show();
|
||
|
||
try {
|
||
// 从 loaderData 获取 JWT Token
|
||
const jwtToken = loaderData.frontendJWT;
|
||
if (!jwtToken) {
|
||
toastService.error('用户身份验证失败,无法获取文档列表');
|
||
setIsLoadingData(false);
|
||
loadingBarService.hide();
|
||
return;
|
||
}
|
||
|
||
console.log('🔑 [fetchData] 文档类型IDs:', typeIds);
|
||
|
||
// 调用新的 API 函数
|
||
const result = await getDocumentsListFromAPI({
|
||
page: currentPage,
|
||
pageSize: pageSize,
|
||
name: search || undefined,
|
||
documentNumber: documentNumber || undefined,
|
||
documentTypeIds: documentType ? [parseInt(documentType, 10)] : typeIds, // 如果有单独选择的类型,优先使用
|
||
auditStatus: auditStatus || undefined,
|
||
fileStatus: fileStatus || undefined,
|
||
dateFrom: dateFrom || undefined,
|
||
dateTo: dateTo || undefined,
|
||
token: jwtToken
|
||
});
|
||
|
||
if (result.error) {
|
||
toastService.error('获取文档列表失败: ' + result.error);
|
||
return;
|
||
}
|
||
|
||
if (!result.data) {
|
||
toastService.error('获取文档列表失败: 返回数据为空');
|
||
return;
|
||
}
|
||
|
||
// 更新状态
|
||
setDocuments(result.data.documents);
|
||
setTotal(result.data.total);
|
||
|
||
// 获取经过过滤的文档类型列表
|
||
const filteredTypesResponse = await getDocumentTypes({
|
||
pageSize: 500,
|
||
documentTypeIds: typeIds
|
||
}, jwtToken);
|
||
const filteredDocumentTypes = filteredTypesResponse.data?.types || [];
|
||
const filteredOptions = filteredDocumentTypes.map(type => ({
|
||
value: type.id,
|
||
label: type.name
|
||
}));
|
||
setFilteredDocumentTypeOptions(filteredOptions);
|
||
|
||
} catch (error) {
|
||
console.error('❌ [fetchData] 获取文档列表失败:', error);
|
||
toastService.error('获取文档列表失败: ' + (error instanceof Error ? error.message : '未知错误'));
|
||
} finally {
|
||
setIsLoadingData(false);
|
||
loadingBarService.hide();
|
||
}
|
||
}, [search, documentNumber, documentType, auditStatus, fileStatus, dateFrom, dateTo, currentPage, pageSize, loaderData.frontendJWT]);
|
||
|
||
// 在组件挂载时从 sessionStorage 获取 documentTypeIds 并加载数据
|
||
useEffect(() => {
|
||
try {
|
||
if (typeof window !== 'undefined') {
|
||
const typeIdsStr = sessionStorage.getItem('documentTypeIds');
|
||
if (typeIdsStr) {
|
||
const typeIds = JSON.parse(typeIdsStr) as number[];
|
||
console.log('📋 [useEffect] 从 sessionStorage 获取文档类型IDs:', typeIds);
|
||
setDocumentTypeIds(typeIds);
|
||
|
||
// 加载数据
|
||
fetchData(typeIds);
|
||
} else {
|
||
console.warn('⚠️ [useEffect] sessionStorage 中没有 documentTypeIds');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('❌ [useEffect] 获取 sessionStorage 中的 documentTypeIds 失败:', error);
|
||
}
|
||
}, [fetchData]);
|
||
|
||
// 监听 URL 参数变化,重新获取数据
|
||
useEffect(() => {
|
||
if (documentTypeIds) {
|
||
fetchData(documentTypeIds);
|
||
}
|
||
}, [searchParams, fetchData, documentTypeIds]);
|
||
|
||
// 监听 documents 数据变化,自动修正不一致的展开状态
|
||
useEffect(() => {
|
||
if (documents.length === 0) return;
|
||
|
||
const newExpandedRows = new Set(expandedRows);
|
||
let hasChanges = false;
|
||
|
||
// 检查每个展开的行
|
||
expandedRows.forEach(docId => {
|
||
const doc = documents.find(d => d.id === docId);
|
||
|
||
// 如果文档不存在或没有历史版本数据,自动折叠
|
||
if (!doc || !doc.historyVersions || doc.historyVersions.length === 0) {
|
||
console.warn(`自动折叠文档 ${docId}:数据不完整`);
|
||
newExpandedRows.delete(docId);
|
||
hasChanges = true;
|
||
}
|
||
});
|
||
|
||
// 如果有变化,更新状态
|
||
if (hasChanges) {
|
||
setExpandedRows(newExpandedRows);
|
||
|
||
// 同时更新 documents 中的 isExpanded 状态
|
||
setDocuments(prevDocs =>
|
||
prevDocs.map(d =>
|
||
newExpandedRows.has(d.id) ? { ...d, isExpanded: true } : { ...d, isExpanded: false }
|
||
)
|
||
);
|
||
}
|
||
}, [documents, expandedRows]);
|
||
|
||
// 使用并更新缓存数据
|
||
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' && isDeleting) {
|
||
// 重置删除状态
|
||
setIsDeleting(false);
|
||
|
||
if (fetcher.data.result) {
|
||
toastService.success(fetcher.data.message);
|
||
// 删除成功后重新加载数据
|
||
if (documentTypeIds) {
|
||
fetchData(documentTypeIds);
|
||
}
|
||
} else if (fetcher.data.message) {
|
||
toastService.error(fetcher.data.message);
|
||
// 删除失败只显示错误信息,不刷新数据
|
||
}
|
||
}
|
||
}, [fetcher.data, fetcher.state, fetchData, documentTypeIds, isDeleting]);
|
||
|
||
// 分页处理函数
|
||
const handlePageChange = (page: number) => {
|
||
const params = new URLSearchParams(searchParams);
|
||
params.set("page", page.toString());
|
||
persistSearchParams(params);
|
||
setSearchParams(params);
|
||
};
|
||
|
||
// 每页条数变更处理函数
|
||
const handlePageSizeChange = (size: number) => {
|
||
const params = new URLSearchParams(searchParams);
|
||
params.set("pageSize", size.toString());
|
||
params.set("page", "1"); // 重置到第一页
|
||
persistSearchParams(params);
|
||
setSearchParams(params);
|
||
};
|
||
|
||
// 处理文档名称搜索
|
||
const handleNameSearch = (value: string) => {
|
||
const params = new URLSearchParams(searchParams);
|
||
if (value) {
|
||
params.set("search", value);
|
||
} else {
|
||
params.delete("search");
|
||
}
|
||
params.set("page", "1"); // 重置页码
|
||
persistSearchParams(params);
|
||
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"); // 重置页码
|
||
persistSearchParams(params);
|
||
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"); // 重置页码
|
||
persistSearchParams(params);
|
||
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"); // 重置页码
|
||
persistSearchParams(params);
|
||
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"); // 重置页码
|
||
persistSearchParams(params);
|
||
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参数并清除保存
|
||
if (typeof window !== 'undefined') {
|
||
sessionStorage.removeItem(SEARCH_PARAMS_STORAGE_KEY);
|
||
}
|
||
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 {
|
||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(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: "取消",
|
||
confirmDelay: 4,
|
||
onConfirm: () => {
|
||
// 设置删除状态为true
|
||
setIsDeleting(true);
|
||
|
||
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: "取消",
|
||
confirmDelay: 4,
|
||
onConfirm: () => {
|
||
// 设置删除状态为true
|
||
setIsDeleting(true);
|
||
|
||
// 使用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 (selectedRowKeys.length === 0) {
|
||
toastService.error('请至少选择一个文档');
|
||
return;
|
||
}
|
||
|
||
// 过滤出选中的文档(仅主文档,不包括历史版本)
|
||
const selectedDocuments = documents.filter((doc: DocumentUI) =>
|
||
selectedRowKeys.includes(doc.id.toString())
|
||
);
|
||
|
||
if (selectedDocuments.length === 0) {
|
||
toastService.error('没有可导出的文档');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
// 创建一个ZIP文件
|
||
const JSZip = await import('jszip').then(module => module.default);
|
||
const zip = new JSZip();
|
||
|
||
// 准备所有下载任务(仅选中的文档)
|
||
const downloadTasks = selectedDocuments.map(async (doc: DocumentUI) => {
|
||
try {
|
||
if (!doc.path) {
|
||
console.warn(`文档 ${doc.name} 没有有效的路径`);
|
||
return;
|
||
}
|
||
|
||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(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 {
|
||
// 从 localStorage 获取用户ID(与 token 管理保持一致)
|
||
const userId = getUserId();
|
||
if (!userId) {
|
||
toastService.error('用户身份验证失败');
|
||
return;
|
||
}
|
||
|
||
// console.log('开始审核',fileId,auditStatus)
|
||
const response = await updateDocumentAuditStatus(fileId.toString(), 2, userId);
|
||
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;
|
||
}
|
||
}
|
||
// 导航到评查详情页前保存查询参数
|
||
if (typeof window !== 'undefined') {
|
||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, searchParams.toString());
|
||
}
|
||
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
||
};
|
||
|
||
// 处理附件追加文件选择
|
||
const handleAttachmentFilesSelected = (files: FileList) => {
|
||
try {
|
||
console.log('【附件追加】开始处理附件文件选择, 文件数量:', files.length);
|
||
|
||
if (files.length > 0) {
|
||
// 检查主文件类型
|
||
const selectedDocument = documents.find(doc => doc.id === selectedDocumentId);
|
||
const isMainFileDocx = selectedDocument?.path.toLowerCase().endsWith('.docx');
|
||
|
||
// 验证文件类型,支持PDF、Word、ZIP、RAR
|
||
const validFiles: File[] = [];
|
||
let hasInvalidFiles = false;
|
||
let hasPdfForDocx = false;
|
||
|
||
Array.from(files).forEach(file => {
|
||
const fileName = file.name.toLowerCase();
|
||
const isPdf = file.type === 'application/pdf' || fileName.endsWith('.pdf');
|
||
const isValidType =
|
||
isPdf ||
|
||
// file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx') ||
|
||
file.type === 'application/zip' || fileName.endsWith('.zip') ||
|
||
file.type === 'application/x-rar-compressed' || fileName.endsWith('.rar');
|
||
|
||
// 如果主文件是docx,不允许上传pdf附件
|
||
if (isMainFileDocx && isPdf) {
|
||
hasPdfForDocx = true;
|
||
console.error(`【附件追加】主文件为DOCX格式时不允许上传PDF附件: ${file.name}`);
|
||
return;
|
||
}
|
||
|
||
if (isValidType) {
|
||
validFiles.push(file);
|
||
} else {
|
||
hasInvalidFiles = true;
|
||
console.error(`【附件追加】无效的文件类型: ${file.name}, 类型: ${file.type}`);
|
||
}
|
||
});
|
||
|
||
if (hasPdfForDocx) {
|
||
messageService.error('主文件为DOCX格式时,附件不可以是PDF格式', {
|
||
title: '文件类型限制',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
} else if (hasInvalidFiles) {
|
||
messageService.error('只支持PDF、Word、ZIP、RAR格式的文件', {
|
||
title: '文件类型错误',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
}
|
||
|
||
if (validFiles.length > 0) {
|
||
setAttachmentFiles(validFiles);
|
||
console.log('【附件追加】有效文件数量:', validFiles.length);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('【附件追加】处理文件选择时发生错误:', error);
|
||
}
|
||
};
|
||
|
||
// 处理附件追加上传
|
||
const handleAttachmentUpload = async () => {
|
||
if (!selectedDocumentId || attachmentFiles.length === 0) {
|
||
toastService.error('请选择文档和附件文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setAttachmentUploading(true);
|
||
|
||
const result = await appendContractAttachments(
|
||
selectedDocumentId,
|
||
attachmentFiles,
|
||
attachmentMergeMode,
|
||
true, // isReprocess
|
||
attachmentRemark || undefined,
|
||
loaderData.frontendJWT
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
toastService.success('附件追加成功!');
|
||
|
||
// 重置状态
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
|
||
// 刷新文档列表
|
||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||
fetchData(documentTypeIds);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('【附件追加】上传失败:', error);
|
||
toastService.error(error instanceof Error ? error.message : '附件追加失败');
|
||
} finally {
|
||
setAttachmentUploading(false);
|
||
}
|
||
};
|
||
|
||
// 处理合同模板文件选择
|
||
const handleTemplateFileSelected = (files: FileList) => {
|
||
try {
|
||
console.log('【合同模板上传】开始处理模板文件选择, 文件数量:', files.length);
|
||
|
||
if (files.length > 0) {
|
||
const file = files[0];
|
||
// 验证文件类型,支持PDF和Word
|
||
const fileName = file.name.toLowerCase();
|
||
const isValidType =
|
||
file.type === 'application/pdf' || fileName.endsWith('.pdf') ||
|
||
// file.type === 'application/msword' || fileName.endsWith('.doc') ||
|
||
file.type === 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' || fileName.endsWith('.docx');
|
||
|
||
if (isValidType) {
|
||
setTemplateFile(file);
|
||
console.log('【合同模板上传】有效文件:', file.name);
|
||
} else {
|
||
messageService.error('只支持.pdf、.docx格式的文件', {
|
||
title: '文件类型错误',
|
||
confirmText: '确定',
|
||
cancelText: '',
|
||
});
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('【合同模板上传】处理文件选择时发生错误:', error);
|
||
}
|
||
};
|
||
|
||
// 处理合同模板上传
|
||
const handleTemplateUpload = async () => {
|
||
if (!selectedDocumentId || !templateFile) {
|
||
toastService.error('请选择文档和模板文件');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setTemplateUploading(true);
|
||
|
||
const result = await uploadContractTemplate(
|
||
templateFile,
|
||
selectedDocumentId,
|
||
undefined // comparisonId
|
||
);
|
||
|
||
if (result.error) {
|
||
throw new Error(result.error);
|
||
}
|
||
|
||
toastService.success('合同模板上传成功!');
|
||
|
||
// 重置状态
|
||
setTemplateFile(null);
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
|
||
// 刷新文档列表
|
||
if (documentTypeIds && documentTypeIds.length > 0) {
|
||
fetchData(documentTypeIds);
|
||
}
|
||
|
||
} catch (error) {
|
||
console.error('【合同模板上传】上传失败:', error);
|
||
toastService.error(error instanceof Error ? error.message : '合同模板上传失败');
|
||
} finally {
|
||
setTemplateUploading(false);
|
||
}
|
||
};
|
||
|
||
// 展开/折叠历史版本
|
||
const handleToggleExpand = async (doc: DocumentUI) => {
|
||
const newExpanded = new Set(expandedRows);
|
||
|
||
if (expandedRows.has(doc.id)) {
|
||
// 折叠:移除展开状态
|
||
newExpanded.delete(doc.id);
|
||
setExpandedRows(newExpanded);
|
||
|
||
// 更新展开状态
|
||
setDocuments(prevDocs =>
|
||
prevDocs.map(d =>
|
||
d.id === doc.id ? { ...d, isExpanded: false } : d
|
||
)
|
||
);
|
||
} else {
|
||
// 展开前检查是否有历史版本数据
|
||
const hasHistoryData = doc.historyVersions && doc.historyVersions.length > 0;
|
||
const hasHistoryCount = doc.historyCount && doc.historyCount > 0;
|
||
|
||
// 如果有历史版本计数但没有数据,可能是数据加载失败
|
||
if (hasHistoryCount && !hasHistoryData) {
|
||
console.warn(`文档 ${doc.id} 有 ${doc.historyCount} 个历史版本,但数据为空`);
|
||
toastService.warning('历史版本数据加载失败,请刷新页面重试');
|
||
return;
|
||
}
|
||
|
||
// 如果没有历史版本,不允许展开
|
||
if (!hasHistoryCount) {
|
||
console.log(`文档 ${doc.id} 没有历史版本`);
|
||
return;
|
||
}
|
||
|
||
// 展开:显示历史版本
|
||
newExpanded.add(doc.id);
|
||
setExpandedRows(newExpanded);
|
||
|
||
// 更新展开状态
|
||
setDocuments(prevDocs =>
|
||
prevDocs.map(d =>
|
||
d.id === doc.id ? { ...d, isExpanded: true } : d
|
||
)
|
||
);
|
||
}
|
||
};
|
||
|
||
// 渲染历史版本行的辅助函数
|
||
const renderHistoryRow = (historyDoc: DocumentVersionUI, parentDoc: DocumentUI) => {
|
||
return (
|
||
<tr key={`history-${historyDoc.id}`} className="history-row">
|
||
<td className="align-middle px-4 py-3" style={{ width: '50px' }}>
|
||
<input type="checkbox" disabled style={{ visibility: 'hidden' }} />
|
||
</td>
|
||
<td className="align-middle px-4 py-3" style={{ width: '25%' }}>
|
||
<div className="flex items-center gap-3">
|
||
<i className="ri-history-line text-gray-400 text-lg"></i>
|
||
<span className="history-version-label text-sm">
|
||
v{historyDoc.versionNumber} 版本
|
||
</span>
|
||
<span className="history-version-label text-sm">
|
||
{historyDoc.documentNumber}
|
||
</span>
|
||
</div>
|
||
</td>
|
||
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '8%' }}>{formatFileSize(historyDoc.size)}</td>
|
||
<td className="px-4 py-3" style={{ width: '8%' }}>
|
||
{(() => {
|
||
const fileStatus = historyDoc.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>
|
||
);
|
||
})()}
|
||
</td>
|
||
<td className="px-4 py-3" style={{ width: '8%' }}>
|
||
{(() => {
|
||
// 处理auditStatus为null或undefined的情况,默认为0(待审核)
|
||
const auditStatus = historyDoc.auditStatus != null ? historyDoc.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>
|
||
);
|
||
})()}
|
||
</td>
|
||
<td className="px-4 py-3" style={{ width: '15%' }}>
|
||
<ResultStats
|
||
passCount={historyDoc.pass_count}
|
||
warningCount={historyDoc.warning_count}
|
||
errorCount={historyDoc.error_count}
|
||
manualCount={historyDoc.manual_count}
|
||
previousPassCount={historyDoc.previous_pass_count}
|
||
previousWarningCount={historyDoc.previous_warning_count}
|
||
previousErrorCount={historyDoc.previous_error_count}
|
||
previousManualCount={historyDoc.previous_manual_count}
|
||
/>
|
||
</td>
|
||
<td className="text-xs text-gray-600 px-4 py-3" style={{ width: '10%' }}>{historyDoc.uploadTime}</td>
|
||
<td className="px-4 py-3" style={{ width: '25%' }}>
|
||
<div className="operations-cell flex flex-wrap gap-1">
|
||
<Link
|
||
to={`/reviews?id=${historyDoc.id}&previousRoute=documents`}
|
||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||
>
|
||
<i className="ri-eye-line"></i>
|
||
查看
|
||
</Link>
|
||
<Link
|
||
to={`/documents/edit?id=${historyDoc.id}`}
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
>
|
||
<i className="ri-edit-line"></i>
|
||
修改
|
||
</Link>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
onClick={() => handleDownload(historyDoc.path)}
|
||
>
|
||
<i className="ri-download-line"></i>
|
||
下载
|
||
</button>
|
||
{parentDoc.type === '1' && historyDoc.fileStatus === 'Processed' && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
||
onClick={() => {
|
||
setSelectedDocumentId(historyDoc.id);
|
||
setSelectedDocumentName(historyDoc.name);
|
||
setSelectedDocumentVersion(historyDoc.versionNumber || null);
|
||
setShowAttachmentUpload(true);
|
||
}}
|
||
>
|
||
<i className="ri-attachment-line"></i>
|
||
追加附件
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
onClick={() => {
|
||
setSelectedDocumentId(historyDoc.id);
|
||
setSelectedDocumentName(historyDoc.name);
|
||
setSelectedDocumentVersion(historyDoc.versionNumber || null);
|
||
setShowTemplateUpload(true);
|
||
}}
|
||
>
|
||
<i className="ri-file-copy-line"></i>
|
||
上传模板
|
||
</button>
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 text-error hover:underline hover:text-red-700"
|
||
onClick={() => handleDelete(historyDoc.id.toString(), historyDoc.name, historyDoc.fileStatus)}
|
||
>
|
||
<i className="ri-delete-bin-line"></i>
|
||
删除
|
||
</button>
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
);
|
||
};
|
||
|
||
|
||
// 表格列定义
|
||
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 items-center gap-3">
|
||
{/* 展开/折叠图标(仅在有历史版本时显示) */}
|
||
{record.historyCount && record.historyCount > 0 ? (
|
||
loadingHistory.has(record.id) ? (
|
||
<i className="ri-loader-4-line expand-icon animate-spin" title="加载中..."></i>
|
||
) : (
|
||
<i
|
||
className={`ri-arrow-right-s-line expand-icon ${expandedRows.has(record.id) ? 'expanded' : ''}`}
|
||
onClick={() => handleToggleExpand(record)}
|
||
title={expandedRows.has(record.id) ? '折叠历史版本' : '展开历史版本'}
|
||
></i>
|
||
)
|
||
) : (
|
||
<span style={{ width: '20px', display: 'inline-block' }}></span>
|
||
)}
|
||
|
||
<FileTag
|
||
extension={record.fileType}
|
||
showIcon={true}
|
||
showText={false}
|
||
showBackground={false}
|
||
size="lg"
|
||
className="flex-shrink-0"
|
||
/>
|
||
|
||
<div className="flex flex-col gap-2 flex-1 min-w-0">
|
||
<span className="doc-name-text font-medium text-gray-900" title={record.name}>
|
||
{record.name}
|
||
</span>
|
||
<span className="document-number">{record.documentNumber}</span>
|
||
<div className="flex items-center gap-2 flex-wrap">
|
||
<FileTypeTag
|
||
type={record.type}
|
||
typeName={record.typeName}
|
||
text={record.typeName}
|
||
size="sm"
|
||
showIcon={false}
|
||
fileType={record.fileType}
|
||
colorMode="light"
|
||
/>
|
||
{record.isTest && (
|
||
<span className="text-xs bg-gray-100 text-gray-500 px-1 rounded">测试</span>
|
||
)}
|
||
{/* 版本徽章*/}
|
||
{record.historyCount !== undefined && record.historyCount > 0 ?
|
||
<span className="version-badge">
|
||
<i className="ri-history-line"></i>
|
||
v{record.historyCount + 1} {record.historyCount !== undefined && `(有${record.historyCount}个历史版本)`}
|
||
</span> : ""
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
},
|
||
{
|
||
title: "文件大小",
|
||
key: "size",
|
||
width: "8%",
|
||
render: (_: unknown, record: DocumentUI) => formatFileSize(record.size)
|
||
},
|
||
{
|
||
title: "文件状态",
|
||
key: "fileStatus",
|
||
width:'8%',
|
||
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:"8%",
|
||
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:"18%",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<ResultStats
|
||
passCount={record.pass_count}
|
||
warningCount={record.warning_count}
|
||
errorCount={record.error_count}
|
||
manualCount={record.manual_count}
|
||
previousPassCount={record.previous_pass_count}
|
||
previousWarningCount={record.previous_warning_count}
|
||
previousErrorCount={record.previous_error_count}
|
||
previousManualCount={record.previous_manual_count}
|
||
warningMessages={record.warning_messages}
|
||
errorMessages={record.error_messages}
|
||
manualMessages={record.manual_messages}
|
||
/>
|
||
)
|
||
},
|
||
{
|
||
title: "上传时间",
|
||
key: "uploadTime",
|
||
width:"8%",
|
||
render: (_: unknown, record: DocumentUI) => record.uploadTime
|
||
},
|
||
{
|
||
title: "操作",
|
||
key: "actions",
|
||
width: "25%",
|
||
render: (_: unknown, record: DocumentUI) => (
|
||
<div className="operations-cell flex flex-wrap gap-1">
|
||
{(record.auditStatus === 0 || record.auditStatus == null) ? (
|
||
<button
|
||
onClick={() => handleReviewFileClick(record.id, record.auditStatus)}
|
||
disabled={record.fileStatus !== 'Processed'}
|
||
className={`text-xs px-2 py-1 h-7 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="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||
>
|
||
<i className="ri-eye-line"></i>
|
||
查看进度
|
||
</Link>
|
||
) : (
|
||
<Link
|
||
to={`/reviews?id=${record.id}&previousRoute=documents`}
|
||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||
>
|
||
<i className="ri-eye-line"></i>
|
||
查看
|
||
</Link>
|
||
)}
|
||
<Link
|
||
to={`/documents/edit?id=${record.id}`}
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
>
|
||
<i className="ri-edit-line"></i>
|
||
修改
|
||
</Link>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
onClick={() => handleDownload(record.path)}
|
||
>
|
||
<i className="ri-download-line"></i>
|
||
下载
|
||
</button>
|
||
{record.type === '1' && record.fileStatus === 'Processed' && (
|
||
<>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline hover:text-primary"
|
||
onClick={() => {
|
||
setSelectedDocumentId(record.id);
|
||
setSelectedDocumentName(record.name);
|
||
setSelectedDocumentVersion(record.historyCount !== undefined && record.historyCount > 0 ? record.historyCount + 1 : null);
|
||
setShowAttachmentUpload(true);
|
||
}}
|
||
>
|
||
<i className="ri-attachment-line"></i>
|
||
追加附件
|
||
</button>
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 mr-1 text-gray-500 hover:underline hover:text-gray-700"
|
||
onClick={() => {
|
||
setSelectedDocumentId(record.id);
|
||
setSelectedDocumentName(record.name);
|
||
setSelectedDocumentVersion(record.historyCount !== undefined && record.historyCount > 0 ? record.historyCount + 1 : null);
|
||
setShowTemplateUpload(true);
|
||
}}
|
||
>
|
||
<i className="ri-file-copy-line"></i>
|
||
上传模板
|
||
</button>
|
||
</>
|
||
)}
|
||
<button
|
||
type="button"
|
||
className="text-xs px-2 py-1 h-7 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">
|
||
<div className="flex items-center">
|
||
<h2 className="text-xl font-medium">文档列表</h2>
|
||
{isLoadingData ? (
|
||
<div className="ml-4">
|
||
<NumberSkeleton />
|
||
</div>
|
||
) : (
|
||
<div className="ml-4 text-sm text-secondary">
|
||
共 <span className="font-medium text-primary">{total}</span> 条记录
|
||
</div>
|
||
)}
|
||
</div>
|
||
<div>
|
||
<Button
|
||
type="primary"
|
||
icon="ri-upload-line"
|
||
to="/files/upload"
|
||
className="hover:text-white"
|
||
>
|
||
上传文档
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 搜索筛选区 */}
|
||
<FilterPanel
|
||
actions={
|
||
<>
|
||
<Button
|
||
type="primary"
|
||
icon="ri-search-line"
|
||
onClick={() => {
|
||
if (documentTypeIds) {
|
||
fetchData(documentTypeIds);
|
||
}
|
||
}}
|
||
className="mr-2"
|
||
>
|
||
搜索
|
||
</Button>
|
||
<Button
|
||
type="default"
|
||
icon="ri-refresh-line"
|
||
onClick={handleReset}
|
||
className="mr-2"
|
||
>
|
||
重置
|
||
</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={filteredDocumentTypeOptions}
|
||
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>
|
||
{isLoadingData && <LoadingIndicator />}
|
||
|
||
<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">
|
||
{isLoadingData && documents.length === 0 ? (
|
||
<TableRowSkeleton count={5} />
|
||
) : documents.length === 0 ? (
|
||
<div className="text-center py-8 text-gray-500">
|
||
{isLoadingData ? "加载中..." : "暂无数据"}
|
||
</div>
|
||
) : (
|
||
<table className="w-full border-collapse">
|
||
<thead className="bg-gray-50">
|
||
<tr>
|
||
{columns.map((col, index) => (
|
||
<th
|
||
key={col.key || index}
|
||
style={{ width: col.width }}
|
||
className="px-4 py-3 text-left text-sm font-semibold text-gray-700 border-b"
|
||
>
|
||
{col.title}
|
||
</th>
|
||
))}
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
{documents.map((doc) => (
|
||
<>
|
||
{/* 主文档行 */}
|
||
<tr
|
||
key={doc.id}
|
||
className={`border-b hover:bg-gray-50 transition-colors ${
|
||
doc.historyCount && doc.historyCount > 0 ? 'cursor-pointer' : ''
|
||
}`}
|
||
onClick={(e) => {
|
||
// 只有有历史版本的行才可以点击
|
||
if (!doc.historyCount || doc.historyCount === 0) return;
|
||
|
||
// 检查点击的是否是可交互元素(链接、按钮、输入框等)
|
||
const target = e.target as HTMLElement;
|
||
const isInteractiveElement =
|
||
target.tagName === 'A' ||
|
||
target.tagName === 'BUTTON' ||
|
||
target.tagName === 'INPUT' ||
|
||
target.closest('a') ||
|
||
target.closest('button') ||
|
||
target.closest('input') ||
|
||
target.closest('.result-stats-wrapper') ||
|
||
target.closest('.result-stat-item');
|
||
|
||
// 如果点击的是可交互元素,不触发展开/收起
|
||
if (isInteractiveElement) return;
|
||
|
||
// 触发展开/收起
|
||
handleToggleExpand(doc);
|
||
}}
|
||
>
|
||
{columns.map((col, index) => (
|
||
<td key={col.key || index} className="px-4 py-3 text-sm">
|
||
{col.render ? col.render(null, doc, index) : (doc as any)[col.key]}
|
||
</td>
|
||
))}
|
||
</tr>
|
||
{/* 历史版本行 */}
|
||
{doc.isExpanded && (
|
||
<>
|
||
{/* 正在加载历史版本 */}
|
||
{loadingHistory.has(doc.id) ? (
|
||
<tr key={`loading-${doc.id}`} className="history-row">
|
||
<td colSpan={columns.length} className="px-4 py-3">
|
||
<div className="version-loading">
|
||
<i className="ri-loader-4-line"></i>
|
||
加载历史版本中...
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
) : doc.historyVersions && doc.historyVersions.length > 0 ? (
|
||
/* 显示历史版本数据 */
|
||
doc.historyVersions.map((historyDoc) => renderHistoryRow(historyDoc, doc))
|
||
) : (
|
||
/* 数据为空时的提示 */
|
||
<tr key={`empty-${doc.id}`} className="history-row">
|
||
<td colSpan={columns.length} className="px-4 py-3">
|
||
<div className="text-center text-gray-500 text-sm py-2">
|
||
<i className="ri-information-line mr-1"></i>
|
||
暂无历史版本数据
|
||
</div>
|
||
</td>
|
||
</tr>
|
||
)}
|
||
</>
|
||
)}
|
||
</>
|
||
))}
|
||
</tbody>
|
||
</table>
|
||
)}
|
||
</div>
|
||
|
||
{/* 分页 */}
|
||
<Pagination
|
||
currentPage={currentPage}
|
||
total={total}
|
||
pageSize={pageSize}
|
||
onChange={handlePageChange}
|
||
onPageSizeChange={handlePageSizeChange}
|
||
pageSizeOptions={[10, 20, 50, 100]}
|
||
/>
|
||
</Card>
|
||
</div>
|
||
|
||
{/* 附件追加模态框 */}
|
||
{showAttachmentUpload && (
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||
onClick={() => {
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
}}
|
||
>
|
||
<div
|
||
className="bg-white rounded-lg p-6 w-full max-w-2xl mx-4 max-h-[90vh] overflow-y-auto"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">追加合同附件</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<i className="ri-close-line text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* 文档信息 */}
|
||
<div className="bg-gray-50 p-3 rounded">
|
||
<p className="text-sm text-gray-600">
|
||
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||
{selectedDocumentVersion && (
|
||
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||
v{selectedDocumentVersion}
|
||
</span>
|
||
)}
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
支持.pdf、.docx、ZIP、RAR格式。
|
||
<i className="ri-information-line mr-1"></i>
|
||
ZIP/RAR内需要保证文件格式一致,否则报错
|
||
</p>
|
||
</div>
|
||
|
||
{/* 文件上传区域 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
选择附件文件 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
||
<input
|
||
type="file"
|
||
multiple
|
||
accept=".pdf,.doc,.docx,.zip,.rar"
|
||
onChange={(e) => e.target.files && handleAttachmentFilesSelected(e.target.files)}
|
||
className="hidden"
|
||
id="attachment-file-input"
|
||
/>
|
||
<label htmlFor="attachment-file-input" className="cursor-pointer">
|
||
<i className="ri-attachment-line text-3xl text-gray-400 mb-2 block"></i>
|
||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
||
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx、.zip、.rar格式,可多选</p>
|
||
</label>
|
||
</div>
|
||
{attachmentFiles.length > 0 && (
|
||
<div className="mt-2">
|
||
<p className="text-sm text-green-600 mb-2">
|
||
<i className="ri-checkbox-circle-line"></i> 已选择 {attachmentFiles.length} 个文件
|
||
</p>
|
||
<div className="space-y-1 max-h-32 overflow-y-auto">
|
||
{attachmentFiles.map((file, index) => (
|
||
<div key={index} className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||
<i className="ri-file-line mr-1"></i>
|
||
{file.name} ({formatFileSize(file.size)})
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 合并模式选择 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
合并模式
|
||
</label>
|
||
<div className="space-y-2">
|
||
{/* <label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
name="mergeMode"
|
||
value="overwrite"
|
||
checked={attachmentMergeMode === 'overwrite'}
|
||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-sm">覆盖原文档(推荐)</span>
|
||
</label> */}
|
||
<label className="flex items-center">
|
||
<input
|
||
type="radio"
|
||
name="mergeMode"
|
||
value="new"
|
||
checked={attachmentMergeMode === 'new'}
|
||
onChange={(e) => setAttachmentMergeMode(e.target.value as 'overwrite' | 'new')}
|
||
className="mr-2"
|
||
/>
|
||
<span className="text-sm">新建文档记录</span>
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
{/* 备注 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
备注(可选)
|
||
</label>
|
||
<textarea
|
||
value={attachmentRemark}
|
||
onChange={(e) => setAttachmentRemark(e.target.value)}
|
||
className="w-full p-2 border border-gray-300 rounded-md text-sm"
|
||
rows={3}
|
||
placeholder="请输入备注信息..."
|
||
/>
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||
<button
|
||
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
|
||
onClick={() => {
|
||
setShowAttachmentUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setAttachmentFiles([]);
|
||
setAttachmentRemark("");
|
||
}}
|
||
disabled={attachmentUploading}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
|
||
onClick={handleAttachmentUpload}
|
||
disabled={attachmentFiles.length === 0 || attachmentUploading}
|
||
>
|
||
{attachmentUploading ? '上传中...' : '开始追加'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* 合同模板上传模态框 */}
|
||
{showTemplateUpload && (
|
||
<div
|
||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-[9999]"
|
||
onClick={() => {
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setTemplateFile(null);
|
||
}}
|
||
>
|
||
<div
|
||
className="bg-white rounded-lg p-6 w-full max-w-xl mx-4"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<div className="flex justify-between items-center mb-4">
|
||
<h3 className="text-lg font-semibold">上传合同模板</h3>
|
||
<button
|
||
onClick={() => {
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setTemplateFile(null);
|
||
}}
|
||
className="text-gray-400 hover:text-gray-600"
|
||
>
|
||
<i className="ri-close-line text-xl"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div className="space-y-4">
|
||
{/* 文档信息 */}
|
||
<div className="bg-gray-50 p-3 rounded">
|
||
<p className="text-sm text-gray-600">
|
||
{/* 目标文档ID: <span className="font-medium">{selectedDocumentId}</span> */}
|
||
目标文档名称: <span className="font-medium">{selectedDocumentName}</span>
|
||
{selectedDocumentVersion && (
|
||
<span className="ml-2 text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||
v{selectedDocumentVersion}
|
||
</span>
|
||
)}
|
||
</p>
|
||
<p className="text-xs text-gray-500 mt-1">
|
||
支持.pdf、.docx格式,用于与合同文档进行结构对比
|
||
</p>
|
||
</div>
|
||
|
||
{/* 文件上传区域 */}
|
||
<div>
|
||
<label className="block text-sm font-medium text-gray-700 mb-2">
|
||
选择模板文件 <span className="text-red-500">*</span>
|
||
</label>
|
||
<div className="border-2 border-dashed border-gray-300 rounded-lg p-6 text-center hover:border-gray-400 transition-colors">
|
||
<input
|
||
type="file"
|
||
accept=".pdf,.docx"
|
||
onChange={(e) => e.target.files && handleTemplateFileSelected(e.target.files)}
|
||
className="hidden"
|
||
id="template-file-input"
|
||
/>
|
||
<label htmlFor="template-file-input" className="cursor-pointer">
|
||
<i className="ri-file-copy-line text-3xl text-gray-400 mb-2 block"></i>
|
||
<p className="text-sm text-gray-600">点击选择文件或拖拽文件到此处</p>
|
||
<p className="text-xs text-gray-500 mt-1">支持.pdf、.docx格式</p>
|
||
</label>
|
||
</div>
|
||
{templateFile && (
|
||
<div className="mt-2">
|
||
<p className="text-sm text-green-600 mb-2">
|
||
<i className="ri-checkbox-circle-line"></i> 已选择模板文件
|
||
</p>
|
||
<div className="text-xs text-gray-600 bg-gray-50 p-2 rounded">
|
||
<i className="ri-file-line mr-1"></i>
|
||
{templateFile.name} ({formatFileSize(templateFile.size)})
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
|
||
{/* 操作按钮 */}
|
||
<div className="flex justify-end gap-3 pt-4 border-t">
|
||
<button
|
||
className="px-4 py-2 text-sm border border-gray-300 rounded-md hover:bg-gray-50"
|
||
onClick={() => {
|
||
setShowTemplateUpload(false);
|
||
setSelectedDocumentId(null);
|
||
setSelectedDocumentName(null);
|
||
setSelectedDocumentVersion(null);
|
||
setTemplateFile(null);
|
||
}}
|
||
disabled={templateUploading}
|
||
>
|
||
取消
|
||
</button>
|
||
<button
|
||
className="px-4 py-2 text-sm bg-primary text-white rounded-md hover:bg-primary-dark disabled:opacity-50"
|
||
onClick={handleTemplateUpload}
|
||
disabled={!templateFile || templateUploading}
|
||
>
|
||
{templateUploading ? '上传中...' : '开始上传'}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 错误边界处理
|
||
export function ErrorBoundary() {
|
||
return (
|
||
<div className="error-container">
|
||
<h1 className="text-xl font-bold text-red-500">出错了</h1>
|
||
<p>加载文档列表时出现错误。请刷新页面或联系系统管理员。</p>
|
||
</div>
|
||
);
|
||
}
|