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

448 lines
15 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 } 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";
export function links() {
return [{ rel: "stylesheet", href: documentEditStyles }];
}
export const meta: MetaFunction = () => {
return [
{ title: "修改文档 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "修改文档信息,包括文档类型、编号、状态和备注信息等" }
];
};
// 文档状态定义
enum DocumentStatus {
WAITING = "waiting",
PROCESSING = "processing",
PASS = "pass",
WARNING = "warning",
FAIL = "fail"
}
// 文档状态对应的中文标签
const STATUS_LABELS: Record<DocumentStatus, string> = {
[DocumentStatus.WAITING]: "待审核",
[DocumentStatus.PROCESSING]: "审核中",
[DocumentStatus.PASS]: "通过",
[DocumentStatus.WARNING]: "警告",
[DocumentStatus.FAIL]: "不通过"
};
// 格式化文件大小
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()
]);
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 status = formData.get("status") as DocumentStatus;
const isTest = formData.get("is_test_document") === "on";
const remark = formData.get("remark") as string;
// 验证必填字段
if (!type || !status) {
return Response.json(
{
error: "缺少必填字段",
fieldErrors: {
type_id: !type ? "文档类型不能为空" : null,
status: !status ? "状态不能为空" : null
}
},
{ status: 400 }
);
}
console.log('提交更新:', { type, documentNumber, status, isTest, remark });
// 更新文档
const updateResponse = await updateDocument(id, {
type,
documentNumber,
status,
isTest,
remark
});
if (updateResponse.error) {
console.error('更新文档失败:', updateResponse.error);
return Response.json({
error: updateResponse.error,
message: "更新文档失败,请检查提交的数据是否正确"
}, { status: updateResponse.status || 500 });
}
// 重定向回文档列表
return redirect("/documents");
} catch (error) {
console.error("更新文档失败:", 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 navigate = useNavigate();
// 定义类型
interface DocType {
id: string | number;
name: string;
}
// 状态
const [localStatus, setLocalStatus] = useState<DocumentStatus>(document.status as DocumentStatus);
// 处理状态变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setLocalStatus(e.target.value as DocumentStatus);
};
// 获取文档类型名称
const getDocumentTypeName = (typeId: string): string => {
const docType = documentTypes.find((type) => (type as any).id.toString() === typeId);
return docType ? (docType as any).name : "未知类型";
};
// 渲染状态徽章
const renderStatusBadge = (status: string) => {
const statusClasses: Record<string, string> = {
"waiting": "status-badge status-pending",
"processing": "status-badge status-processing",
"pass": "status-badge status-pass",
"warning": "status-badge status-warning",
"fail": "status-badge status-fail"
};
const statusLabel: Record<string, string> = {
"waiting": "待审核",
"processing": "审核中",
"pass": "通过",
"warning": "警告",
"fail": "不通过"
};
return (
<span className={statusClasses[status] || "status-badge"}>
{statusLabel[status] || status}
</span>
);
};
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.status)}
</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}
required
>
{documentTypes.map(type => (
<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="status" className="form-label"> <span className="text-red-500">*</span></label>
<select
id="status"
name="status"
className="form-select"
value={localStatus}
onChange={handleStatusChange}
required
>
<option value={DocumentStatus.WAITING}>{STATUS_LABELS[DocumentStatus.WAITING]}</option>
<option value={DocumentStatus.PROCESSING}>{STATUS_LABELS[DocumentStatus.PROCESSING]}</option>
<option value={DocumentStatus.PASS}>{STATUS_LABELS[DocumentStatus.PASS]}</option>
<option value={DocumentStatus.WARNING}>{STATUS_LABELS[DocumentStatus.WARNING]}</option>
<option value={DocumentStatus.FAIL}>{STATUS_LABELS[DocumentStatus.FAIL]}</option>
</select>
<div className="text-sm text-secondary mt-1"></div>
{actionData?.fieldErrors?.status && (
<div className="text-red-500 text-sm mt-1">{actionData.fieldErrors.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-pdf-line text-red-500 mr-1"></i>
<span>{document.name}</span>
</div>
<div>
<Button
type="default"
size="small"
icon="ri-download-line"
>
</Button>
</div>
</div>
<div className="preview-content">
<div className="preview-placeholder">
<i className="ri-file-pdf-line"></i>
<p></p>
<p className="text-xs mt-2">PDF文件需要外部查看器支持</p>
<Button
type="primary"
size="small"
icon="ri-external-link-line"
className="mt-4"
>
</Button>
</div>
</div>
</div>
</Card>
{/* 修改历史 */}
<Card title="修改历史">
<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>
);
}