Files
leaudit-platform-frontend/app/routes/documents.edit.tsx
T

557 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useState, useEffect } 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";
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: "文档编辑"
};
// 文档审核状态定义
enum DocumentAuditStatus {
FAIL = -1,
PASS = 1,
WAITING = 0,
PROCESSING = 2
}
// 文档状态对应的中文标签
const STATUS_LABELS: Record<DocumentAuditStatus, string> = {
[DocumentAuditStatus.FAIL]: "不通过",
[DocumentAuditStatus.WAITING]: "待审核",
[DocumentAuditStatus.PASS]: "通过",
[DocumentAuditStatus.PROCESSING]: "审核中"
};
// 文档状态样式配置
const STATUS_STYLES: Record<number, { color: string; icon: string }> = {
[-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 {
// 从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),
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) {
// 从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;
// 验证必填字段
// if (!type || auditStatus === undefined || isNaN(auditStatus)) {
// return Response.json(
// {
// error: "缺少必填字段",
// fieldErrors: {
// type_id: !type ? "文档类型不能为空" : null,
// audit_status: (auditStatus === undefined || isNaN(auditStatus)) ? "审核状态不能为空" : null
// }
// },
// { status: 400 }
// );
// }
// console.log('提交更新:', { type, documentNumber, auditStatus, isTest, remark });
// 更新文档
const updateResponse = await updateDocument(id, {
// type,
documentNumber,
auditStatus,
isTest,
remark
});
if (updateResponse.error) {
console.error('更新文档失败1:', updateResponse.error);
return Response.json({
error: updateResponse.error,
message: "更新文档失败,请检查提交的数据是否正确"
}, { status: updateResponse.status || 500 });
}
// toastService.success('更新文档成功');
// 重定向回文档列表
// return redirect("/documents");
return Response.json({
success: true,
message: "更新文档成功"
});
} catch (error) {
console.error("更新文档失败2:", error);
return Response.json({
error: "更新文档失败",
message: error instanceof Error ? error.message : "发生未知错误"
}, { status: 500 });
}
}
// 文档编辑页面组件
export default function DocumentEdit() {
const { document, documentTypes } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const [numPages, setNumPages] = useState(0);
const [loadError, setLoadError] = useState<string | null>(null);
const navigate = useNavigate();
useEffect(() => {
// console.log('actionData', actionData);
if (actionData?.error) {
toastService.error('更新文档失败:' + actionData.error);
}
if (actionData?.success) {
toastService.success('更新文档成功');
}
}, [actionData]);
const onDocumentLoadSuccess = ({numPages}: {numPages: number}) => {
setNumPages(numPages);
console.log('文档加载成功', numPages);
}
const renderDocumentContent = () => {
return (
<div className="preview-content relative overflow-y-auto max-h-[1000px]">
<Document
file={'http://172.18.0.100:9000/docauditai/'+document.path}
onLoadSuccess={onDocumentLoadSuccess}
onLoadError={(error) => {
console.error("PDF加载错误:", error);
setLoadError("PDF文档加载失败:" + (error.message || "未知错误"));
}}
className="flex flex-col items-center w-full"
error={<div className="text-red-500">PDF文档加载失败</div>}
noData={<div></div>}
loading={<div className="text-center py-10">PDF加载中...</div>}
>
{renderAllPages()}
</Document>
</div>
)
}
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(
<div key={i} id={`page-${i}`} style={styles.pageContainer}>
{/* 页码标识,显示在页面上方 */}
<div className="text-center text-gray-500 text-sm mb-2"> {i} </div>
{/* 页面容器,应用缩放变换,设置相对定位用于放置评查点高亮 */}
<div
className="page-wrapper flex justify-center"
style={{
// transform: `scale(${zoomFactor})`, // 根据zoomLevel应用缩放
transformOrigin: 'top center', // 缩放原点设置为顶部中心
position: 'relative', // 相对定位,作为评查点高亮的定位参考
maxWidth: '100%', // 限制最大宽度
}}
>
{/* 渲染PDF页面组件 */}
<Page
pageNumber={i} // 当前页码
renderTextLayer={true} // 启用文本层,使文本可选择
renderAnnotationLayer={true} // 启用注释层,显示PDF内置注释
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
/>
</div>
</div>
);
}
// 返回所有页面组件数组
return pages;
};
// 定义类型
interface DocType {
id: string | number;
name: string;
}
// 状态
const [localStatus, setLocalStatus] = useState<number>(document.auditStatus);
// 处理状态变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setLocalStatus(parseInt(e.target.value));
};
// 获取文档类型名称
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 (
<span className={`status-badge bg-${style.color}-100 text-${style.color}-800`}>
<i className={`${style.icon} mr-1`}></i>
{label}
</span>
);
};
// 在新窗口打开文档预览
const openPreview = () => {
// 假设有一个预览URL的格式,比如 /preview?path=xxx
// console.log('documentstest', document);
const urlBefore = 'http://172.18.0.100:9000/docauditai/'
const previewUrl = `${urlBefore}${document.path}`;
window.open(previewUrl, '_blank');
};
return (
<div className="document-edit-page">
{/* 页面头部 */}
<div className="page-header">
<h2 className="page-title"></h2>
<div>
<Button
type="default"
icon="ri-arrow-left-line"
onClick={() => navigate("/documents")}
className="mr-2"
>
</Button>
<Button
type="primary"
icon="ri-save-line"
form="edit-form"
>
</Button>
</div>
</div>
{/* 文档信息 */}
<Card className="mb-4">
<div className="document-info">
<div className="document-icon">
<FileTag
extension={document.fileType}
showIcon={true}
showText={false}
showBackground={false}
size="lg"
/>
</div>
<div className="document-details">
<div className="document-name">{document.name}</div>
<div className="document-meta">
<div className="meta-item">
<i className="ri-file-list-line"></i>
<span>{getDocumentTypeName(document.type)}</span>
</div>
<div className="meta-item">
<i className="ri-time-line"></i>
<span>{document.uploadTime}</span>
</div>
<div className="meta-item">
<i className="ri-hard-drive-line"></i>
<span>{formatFileSize(document.size)}</span>
</div>
<div className="meta-item">
{renderStatusBadge(document.auditStatus)}
</div>
</div>
</div>
</div>
<div className="alert alert-info mb-4">
<i className="ri-information-line mr-2"></i>
</div>
<Form id="edit-form" method="post">
<div className="grid grid-cols-2 gap-6">
<div className="form-group">
<label htmlFor="type-id" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="type-id"
name="type_id"
className="form-select"
defaultValue={document.type}
// disabled={document.fileStatus !== 'Processed'}
disabled={true}
required
>
{documentTypes.map((type: DocType) => (
<option key={type.id} value={type.id}>{type.name}</option>
))}
</select>
<div className="text-sm text-secondary mt-1"></div>
{actionData?.fieldErrors?.type_id && (
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.type_id}</div>
)}
</div>
<div className="form-group">
<label htmlFor="document-number" className="form-label"></label>
<input
type="text"
id="document-number"
name="document_number"
className="form-input"
placeholder="请输入合同编号、许可证号等"
defaultValue={document.documentNumber || ""}
/>
<div className="text-sm text-secondary mt-1"></div>
</div>
<div className="form-group">
<label htmlFor="audit-status" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="audit-status"
name="audit_status"
className="form-select"
value={localStatus}
onChange={handleStatusChange}
required
>
<option value={DocumentAuditStatus.WAITING}>{STATUS_LABELS[DocumentAuditStatus.WAITING]}</option>
<option value={DocumentAuditStatus.PROCESSING}>{STATUS_LABELS[DocumentAuditStatus.PROCESSING]}</option>
<option value={DocumentAuditStatus.PASS}>{STATUS_LABELS[DocumentAuditStatus.PASS]}</option>
<option value={DocumentAuditStatus.FAIL}>{STATUS_LABELS[DocumentAuditStatus.FAIL]}</option>
</select>
<div className="text-sm text-secondary mt-1"></div>
{actionData?.fieldErrors?.audit_status && (
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.audit_status}</div>
)}
</div>
<div className="form-group">
<div className="form-label"></div>
<div className="flex items-center mt-2">
<label className="switch mr-2" htmlFor="is-test-document">
<input
type="checkbox"
id="is-test-document"
name="is_test_document"
defaultChecked={document.isTest}
/>
<span className="slider"></span>
<span className="sr-only"></span>
</label>
<span></span>
</div>
</div>
<div className="form-group col-span-2">
<label htmlFor="remark" className="form-label"></label>
<textarea
id="remark"
name="remark"
className="form-textarea"
placeholder="可输入文档的相关描述或备注信息"
rows={3}
defaultValue={document.remark || ""}
></textarea>
</div>
</div>
</Form>
</Card>
{/* 文档预览 */}
<Card
title="文档预览"
className="mb-4"
>
<div className="document-preview">
<div className="preview-toolbar">
<div className="flex items-center">
<i className={`ri-file-${document.fileType}-line text-${document.fileType === 'pdf' ? 'red' : 'blue'}-500 mr-1`}></i>
<span>{document.name}</span>
</div>
<div>
<Button
type="default"
size="small"
icon="ri-download-line"
className="mr-2"
>
</Button>
<Button
type="primary"
size="small"
icon="ri-external-link-line"
onClick={openPreview}
>
</Button>
</div>
</div>
{/* 预览窗口 */}
{loadError ?(<div className="text-red-500">
{loadError}
</div>):(
renderDocumentContent()
)}
</div>
</Card>
{/* 修改历史 */}
<Card title="修改历史" className="hidden">
<div className="history-timeline">
{[
{
time: "2023-10-15 15:30",
user: "系统",
action: "创建了此文档",
details: `首次上传文档,文档类型:${getDocumentTypeName(document.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) => (
<div className="timeline-item" key={`${item.time}-${index}`}>
<div className="timeline-time">{item.time}</div>
<div className="timeline-content">
<div><strong>{item.user}</strong> {item.action}</div>
<div className="text-xs text-secondary mt-1">{item.details}</div>
</div>
</div>
))}
</div>
</Card>
</div>
);
}
// 错误边界
export function ErrorBoundary() {
return (
<div className="error-container">
<h1 className="text-xl font-bold text-red-500 mb-4"></h1>
<p className="mb-4">ID是否正确</p>
<Button
type="primary"
to="/documents"
>
</Button>
</div>
);
}