import { useState, useEffect, useRef } from "react"; import { useLoaderData, useActionData, useNavigate, Form } from "@remix-run/react"; import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import documentEditStyles from "~/styles/pages/documents_edit.css?url"; import { getDocument, updateDocument } from "~/api/files/documents"; import { getDocumentTypes } from "~/api/document-types/document-types"; import { FileTag } from "~/components/ui/FileTag"; import { toastService } from "~/components/ui/Toast"; import { Document, Page , pdfjs } from "react-pdf"; import { DOCUMENT_URL } from "~/api/axios-client"; pdfjs.GlobalWorkerOptions.workerSrc = '/pdf.worker.js'; export function links() { return [{ rel: "stylesheet", href: documentEditStyles }]; } export const meta: MetaFunction = () => { return [ { title: "修改文档 - 中国烟草AI合同及卷宗审核系统" }, { name: "description", content: "修改文档信息,包括文档类型、编号、状态和备注信息等" } ]; }; export const handle = { breadcrumb: "文档编辑" }; // 定义action返回数据类型 export interface ActionData { success?: boolean; error?: string; fieldErrors?: { document_number?: string; audit_status?: string; remark?: string; general?: string; }; values?: Record; } // 文档审核状态定义 enum DocumentAuditStatus { FAIL = -1, PASS = 1, WAITING = 0, PROCESSING = 2 } // 文档状态对应的中文标签 const STATUS_LABELS: Record = { [DocumentAuditStatus.FAIL]: "不通过", [DocumentAuditStatus.WAITING]: "待审核", [DocumentAuditStatus.PASS]: "通过", [DocumentAuditStatus.PROCESSING]: "审核中" }; // 文档状态样式配置 const STATUS_STYLES: Record = { [-1]: { color: "red", icon: "ri-close-line" }, [0]: { color: "blue", icon: "ri-time-line" }, [1]: { color: "green", icon: "ri-check-line" }, [2]: { color: "yellow", icon: "ri-alert-line" }, [3]: { color: "purple", icon: "ri-search-line" } }; // 格式化文件大小 function formatFileSize(bytes: number): string { 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]; } // Loader函数 export async function loader({ request }: LoaderFunctionArgs) { try { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo } = await getUserSession(request); if (!userInfo?.user_id) { throw new Response("用户身份验证失败", { status: 401 }); } const userId = userInfo.user_id.toString(); // 从URL查询参数获取文档ID const url = new URL(request.url); const id = url.searchParams.get("id"); if (!id) { throw new Response("缺少文档ID", { status: 400 }); } // 并行获取文档详情和文档类型列表 const [documentResponse, documentTypesResponse] = await Promise.all([ getDocument(id, userId), getDocumentTypes({ pageSize: 500 }) ]); if (documentResponse.error) { throw new Response(documentResponse.error, { status: documentResponse.status || 404 }); } if (documentTypesResponse.error) { console.error("获取文档类型列表失败:", documentTypesResponse.error); } return Response.json({ document: documentResponse.data, documentTypes: documentTypesResponse.data?.types || [] }); } catch (error) { console.error("加载文档数据失败:", error); throw new Response("加载文档数据失败", { status: 500 }); } } // Action函数处理表单提交 export async function action({ request }: ActionFunctionArgs) { // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { userInfo } = await getUserSession(request); if (!userInfo?.user_id) { return Response.json({ error: "用户身份验证失败" }, { status: 401 }); } const userId = userInfo.user_id.toString(); // 从URL查询参数获取文档ID const url = new URL(request.url); const id = url.searchParams.get("id"); if (!id) { return Response.json({ error: "缺少文档ID" }, { status: 400 }); } try { const formData = await request.formData(); // 从表单数据中提取字段 // const type = formData.get("type_id") as string; const documentNumber = formData.get("document_number") as string; const auditStatus = parseInt(formData.get("audit_status") as string); const isTest = formData.get("is_test_document") === "on"; const remark = formData.get("remark") as string; // 表单验证 const errors: ActionData["fieldErrors"] = {}; if (isNaN(auditStatus)) { errors.audit_status = "审核状态必须选择"; } if (Object.keys(errors).length > 0) { return Response.json({ fieldErrors: errors, values: Object.fromEntries(formData) as Record }, { status: 400 }); } // 更新文档 const updateResponse = await updateDocument(id, { // type, documentNumber, auditStatus, isTest, remark }, userId); if (updateResponse.error) { console.error('更新文档失败:', updateResponse.error); return Response.json({ error: updateResponse.error, fieldErrors: { general: "更新文档失败,请检查提交的数据是否正确" }, values: Object.fromEntries(formData) as Record }, { status: updateResponse.status || 500 }); } toastService.success('更新文档成功'); return redirect("/documents"); } catch (error) { console.error("更新文档失败2:", error); return Response.json({ error: "更新文档失败", fieldErrors: { general: error instanceof Error ? error.message : "发生未知错误" } }, { status: 500 }); } } // 文档编辑页面组件 export default function DocumentEdit() { const { document: documentData, documentTypes } = useLoaderData(); const actionData = useActionData(); const navigate = useNavigate(); const [numPages, setNumPages] = useState(0); const [loadError, setLoadError] = useState(null); const formRef = useRef(null); // 表单状态管理 - 使用受控组件 const [formValues, setFormValues] = useState({ documentNumber: documentData.documentNumber || "", auditStatus: documentData.auditStatus, isTest: documentData.isTest || false, remark: documentData.remark || "" }); // 表单验证错误状态 const [formErrors, setFormErrors] = useState<{ documentNumber?: string; auditStatus?: string; remark?: string; general?: string; }>({}); // 字段是否被触摸过(用于确定何时显示错误) const [touchedFields, setTouchedFields] = useState({ documentNumber: false, auditStatus: false, remark: false }); // 从 actionData 初始化表单错误 useEffect(() => { if (actionData?.fieldErrors) { // general 是loader的时候返回的错误信息 setFormErrors(actionData.fieldErrors); } }, [actionData]); // 验证表单字段 const validateField = (field: string, value: string | number | undefined): string => { switch (field) { case 'auditStatus': { const statusValue = typeof value === 'string' ? parseInt(value) : value; return statusValue === undefined || isNaN(statusValue as number) ? "审核状态必须选择" : ""; } default: return ""; } }; // 处理字段改变 const handleChange = (e: React.ChangeEvent) => { const { name, value, type } = e.target; if (type === 'checkbox') { const checked = (e.target as HTMLInputElement).checked; setFormValues(prev => ({ ...prev, [name === 'is_test_document' ? 'isTest' : name]: checked })); } else { setFormValues(prev => ({ ...prev, [name === 'document_number' ? 'documentNumber' : name === 'audit_status' ? 'auditStatus' : name]: name === 'audit_status' ? (parseInt(value)) : value })); } // 标记字段为已触摸 if (name === 'document_number' || name === 'audit_status' || name === 'remark') { const fieldName = name === 'document_number' ? 'documentNumber' : name === 'audit_status' ? 'auditStatus' : name; setTouchedFields(prev => ({ ...prev, [fieldName]: true })); } // 实时验证 if (name === 'audit_status') { const statusValue = parseInt(value); const error = validateField('auditStatus', statusValue); setFormErrors(prev => ({ ...prev, auditStatus: error })); } }; // 处理表单提交前验证 const handleBeforeSubmit = (e: React.FormEvent) => { // 标记所有字段为已触摸 setTouchedFields({ documentNumber: true, auditStatus: true, remark: true }); // 验证所有字段 const errors = { auditStatus: validateField('auditStatus', formValues.auditStatus) }; setFormErrors(errors); // 如果有错误,阻止提交 if (errors.auditStatus) { toastService.error('审核状态不能为空'); e.preventDefault(); } }; const onDocumentLoadSuccess = ({numPages}: {numPages: number}) => { setNumPages(numPages); // console.log('文档加载成功', numPages); } const renderDocumentContent = () => { return (
{ console.error("PDF加载错误:", error); setLoadError("PDF文档加载失败:" + (error.message || "未知错误")); }} className="flex flex-col items-center w-full" error={
PDF文档加载失败,请检查链接或网络连接。
} noData={
无数据
} loading={
PDF加载中...
} > {renderAllPages()}
) } const renderAllPages = () => { // 如果还没有获取到PDF总页数,返回null if (!numPages) return null; // 用于存储所有页面组件的数组 const pages = []; const styles = { pageContainer: { display: 'flex', flexDirection: 'column' as const, alignItems: 'center', width: '100%', position: 'relative' as const, } }; // 遍历每一页,生成对应的页面组件 for (let i = 1; i <= numPages; i++) { // 为每一页创建组件 pages.push(
{/* 页码标识,显示在页面上方 */}
第 {i} 页
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
{/* 渲染PDF页面组件 */}
); } // 返回所有页面组件数组 return pages; }; // 定义类型 interface DocType { id: string | number; name: string; } // 获取文档类型名称 const getDocumentTypeName = (typeId: string): string => { const docType = documentTypes.find((type: DocType) => type.id.toString() === typeId); return docType ? docType.name : "未知类型"; }; // 渲染状态徽章 const renderStatusBadge = (status: number) => { const style = STATUS_STYLES[status] || STATUS_STYLES[0]; const label = STATUS_LABELS[status as DocumentAuditStatus] || STATUS_LABELS[DocumentAuditStatus.WAITING]; return ( {label} ); }; // 下载文档 const downloadDocument = async () => { try { const downloadUrl = `${DOCUMENT_URL}${documentData.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 = documentData.path.split('/').pop() || documentData.name; a.download = decodeURIComponent(fileName); document.body.appendChild(a); a.click(); // 清理 setTimeout(() => { document.body.removeChild(a); URL.revokeObjectURL(blobUrl); }, 100); toastService.success('文件下载成功'); } catch (error) { console.error('下载文件失败:', error); toastService.error(`下载文件失败: ${error instanceof Error ? error.message : '未知错误'}`); } }; // 在新窗口打开文档预览 const openPreview = () => { const previewUrl = `${DOCUMENT_URL}${documentData.path}`; window.open(previewUrl, '_blank'); }; return (
{/* 页面头部 */}

修改文档信息

{/* 文档信息 */}
{documentData.name}
{getDocumentTypeName(documentData.type)}
{documentData.uploadTime}
{formatFileSize(documentData.size)}
{renderStatusBadge(documentData.auditStatus)}
您可以修改此文档的基本信息,但不能更改文档内容。如需修改内容,请删除后重新上传新文档。
{/* 错误提示 */} {formErrors.general && (
{formErrors.general}
)}
更改文档类型将重新应用对应的评查规则
如无编号可留空
更改状态可能会影响此文档在列表中的显示和排序
{touchedFields.auditStatus && formErrors.auditStatus && (
{formErrors.auditStatus}
)}
文档属性
标记为测试文档(不计入正式统计)
{/* 文档预览 */}
{documentData.name}
{/* 预览窗口 */} {loadError ? (
{loadError}
) : ( renderDocumentContent() )}
{/* 修改历史 */}
{[ { time: "2023-10-15 15:30", user: "系统", action: "创建了此文档", details: `首次上传文档,文档类型:${getDocumentTypeName(documentData.type)},状态:待审核` }, { time: "2023-10-15 16:45", user: "张三", action: "启动了文档审核", details: "状态由'待审核'变更为'审核中'" }, { time: "2023-10-15 17:20", user: "系统", action: "完成了文档审核", details: "状态由'审核中'变更为'通过',未发现问题" }, { time: "2023-10-16 09:10", user: "李四", action: "修改了文档属性", details: "添加了备注信息,完善了文档编号" } ].map((item, index) => (
{item.time}
{item.user} {item.action}
{item.details}
))}
); } // 错误边界 export function ErrorBoundary() { return (

出错了

文档编辑页面加载失败。请检查文档ID是否正确,或稍后重试。

); }