完成文档类型增删改查

This commit is contained in:
2025-04-11 18:45:03 +08:00
parent d54d0f25c6
commit 8177b4195f
18 changed files with 3298 additions and 371 deletions
+486
View File
@@ -0,0 +1,486 @@
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";
export function links() {
return [{ rel: "stylesheet", href: documentEditStyles }];
}
export const meta: MetaFunction = () => {
return [
{ title: "修改文档 - 中国烟草AI合同及卷宗审核系统" },
{ name: "description", content: "修改文档信息,包括文档类型、编号、状态和备注信息等" }
];
};
// 文档状态定义
enum DocumentStatus {
PENDING = "pending",
PROCESSING = "processing",
PASS = "pass",
WARNING = "warning",
FAIL = "fail"
}
// 文档状态对应的中文标签
const STATUS_LABELS: Record<DocumentStatus, string> = {
[DocumentStatus.PENDING]: "待审核",
[DocumentStatus.PROCESSING]: "审核中",
[DocumentStatus.PASS]: "通过",
[DocumentStatus.WARNING]: "警告",
[DocumentStatus.FAIL]: "不通过"
};
// 文档类型接口
interface DocumentType {
id: string;
name: string;
}
// 历史记录项接口
interface HistoryItem {
time: string;
user: string;
action: string;
details: string;
}
// 文档接口
interface Document {
id: string;
name: string;
type_id: string;
document_number: string | null;
file_size: number;
upload_time: string;
is_test_document: boolean;
status: DocumentStatus;
remark: string | null;
history: HistoryItem[];
file_url?: string;
}
// 模拟API获取文档类型列表
async function getDocumentTypes(): Promise<DocumentType[]> {
// 这里应该是实际API调用
return [
{ id: "1", name: "销售合同" },
{ id: "2", name: "采购合同" },
{ id: "3", name: "专卖许可证" },
{ id: "4", name: "行政处罚决定书" },
{ id: "5", name: "承包协议" }
];
}
// 模拟API获取文档详情
async function getDocument(id: string): Promise<Document> {
// 这里应该是实际API调用
return {
id,
name: "2023年度烟草销售框架合同.pdf",
type_id: "1", // 销售合同
document_number: "XS20230001",
file_size: 2.5 * 1024 * 1024, // 2.5MB
upload_time: "2023-10-15 15:30",
is_test_document: false,
status: DocumentStatus.PASS,
remark: "此合同为2023年度与XX公司的销售框架协议,适用于全年的烟草销售业务。",
history: [
{
time: "2023-10-15 15:30",
user: "系统",
action: "创建了此文档",
details: "首次上传文档,文档类型:销售合同,状态:待审核"
},
{
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: "添加了备注信息,完善了文档编号"
}
],
file_url: "/mock/documents/sample.pdf"
};
}
// 模拟API更新文档信息
async function updateDocument(id: string, data: Partial<Document>): Promise<Document> {
// 这里应该是实际API调用
console.log("更新文档:", id, data);
// 模拟获取原始数据
const document = await getDocument(id);
// 合并更新的数据
const updatedDocument = {
...document,
...data,
// 添加新的历史记录
history: [
{
time: new Date().toISOString().replace("T", " ").slice(0, 16),
user: "当前用户",
action: "修改了文档信息",
details: `更新了文档类型、状态和备注信息`
},
...document.history
]
};
return updatedDocument;
}
// 格式化文件大小
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 [document, documentTypes] = await Promise.all([
getDocument(id),
getDocumentTypes()
]);
return Response.json({ document, documentTypes });
} 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_id = formData.get("type_id") as string;
const document_number = formData.get("document_number") as string;
const status = formData.get("status") as DocumentStatus;
const is_test_document = formData.get("is_test_document") === "on";
const remark = formData.get("remark") as string;
// 验证必填字段
if (!type_id || !status) {
return Response.json(
{
error: "缺少必填字段",
fieldErrors: {
type_id: !type_id ? "文档类型不能为空" : null,
status: !status ? "状态不能为空" : null
}
},
{ status: 400 }
);
}
// 更新文档
await updateDocument(id, {
type_id,
document_number: document_number || null,
status,
is_test_document,
remark: remark || null
});
// 重定向回文档列表
return redirect("/documents");
} catch (error) {
console.error("更新文档失败:", error);
return Response.json({ error: "更新文档失败" }, { status: 500 });
}
}
// 文档编辑页面组件
export default function DocumentEdit() {
const { document, documentTypes } = useLoaderData<typeof loader>();
const actionData = useActionData<typeof action>();
const navigate = useNavigate();
// 状态
const [localStatus, setLocalStatus] = useState<DocumentStatus>(document.status);
// 处理状态变更
const handleStatusChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setLocalStatus(e.target.value as DocumentStatus);
};
// 获取文档类型名称
const getDocumentTypeName = (typeId: string): string => {
const docType = documentTypes.find((type: DocumentType) => type.id === typeId);
return docType ? docType.name : "未知类型";
};
// 渲染状态徽章
const renderStatusBadge = (status: DocumentStatus) => {
const statusClasses: Record<DocumentStatus, string> = {
[DocumentStatus.PENDING]: "status-badge status-pending",
[DocumentStatus.PROCESSING]: "status-badge status-processing",
[DocumentStatus.PASS]: "status-badge status-pass",
[DocumentStatus.WARNING]: "status-badge status-warning",
[DocumentStatus.FAIL]: "status-badge status-fail"
};
return (
<span className={statusClasses[status]}>
{STATUS_LABELS[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">
<i className="ri-file-pdf-line text-red-500"></i>
</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_id)}</span>
</div>
<div className="meta-item">
<i className="ri-time-line"></i>
<span>{document.upload_time}</span>
</div>
<div className="meta-item">
<i className="ri-hard-drive-line"></i>
<span>{formatFileSize(document.file_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_id}
required
>
{documentTypes.map((type: DocumentType) => (
<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.document_number || ""}
/>
<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.PENDING}>{STATUS_LABELS[DocumentStatus.PENDING]}</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.is_test_document}
/>
<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">
{document.history.map((item: HistoryItem, index: number) => (
<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>
);
}