bfe39e45a9
5. 修改统一认证登录和管理员登录是通过接口形式进行,存储返回的accessToken。 6. 修改交叉评查的部分样式
724 lines
24 KiB
TypeScript
724 lines
24 KiB
TypeScript
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<string, string>;
|
||
}
|
||
|
||
// 文档审核状态定义
|
||
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 {
|
||
// 获取用户会话信息
|
||
const { getUserSession } = await import("~/api/login/auth.server");
|
||
const { userInfo, frontendJWT } = 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, frontendJWT),
|
||
getDocumentTypes({ pageSize: 500 }, frontendJWT)
|
||
]);
|
||
|
||
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 || [],
|
||
frontendJWT: frontendJWT
|
||
});
|
||
} 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, frontendJWT } = 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<string, string>
|
||
}, { status: 400 });
|
||
}
|
||
|
||
// 更新文档
|
||
const updateResponse = await updateDocument(id, {
|
||
// type,
|
||
documentNumber,
|
||
auditStatus,
|
||
isTest,
|
||
remark
|
||
}, userId, frontendJWT);
|
||
|
||
if (updateResponse.error) {
|
||
console.error('更新文档失败:', updateResponse.error);
|
||
return Response.json({
|
||
error: updateResponse.error,
|
||
fieldErrors: {
|
||
general: "更新文档失败,请检查提交的数据是否正确"
|
||
},
|
||
values: Object.fromEntries(formData) as Record<string, string>
|
||
}, { 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<typeof loader>();
|
||
const actionData = useActionData<ActionData>();
|
||
const navigate = useNavigate();
|
||
const [numPages, setNumPages] = useState(0);
|
||
const [loadError, setLoadError] = useState<string | null>(null);
|
||
const formRef = useRef<HTMLFormElement>(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<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>) => {
|
||
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 (
|
||
<div className="preview-content relative overflow-y-auto max-h-[1000px]">
|
||
<Document
|
||
file={`/api/pdf-proxy?path=${encodeURIComponent(documentData.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={{
|
||
position: 'relative',
|
||
maxWidth: '100%',
|
||
}}
|
||
>
|
||
{/* 渲染PDF页面组件 */}
|
||
<Page
|
||
pageNumber={i} // 当前页码
|
||
renderTextLayer={false} // 停用文本层,使文本可选择
|
||
renderAnnotationLayer={false} // 停用注释层,显示PDF内置注释
|
||
className="border border-gray-300 shadow-md" // 添加边框和阴影样式
|
||
/>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// 返回所有页面组件数组
|
||
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 (
|
||
<span className={`status-badge bg-${style.color}-100 text-${style.color}-800`}>
|
||
<i className={`${style.icon} mr-1`}></i>
|
||
{label}
|
||
</span>
|
||
);
|
||
};
|
||
|
||
// 下载文档
|
||
const downloadDocument = async () => {
|
||
try {
|
||
// 使用 PDF 代理路由获取文件,自动添加 JWT 认证
|
||
const downloadUrl = `/api/pdf-proxy?path=${encodeURIComponent(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 = () => {
|
||
// 使用 PDF 代理路由,自动添加 JWT 认证
|
||
const previewUrl = `/api/pdf-proxy?path=${encodeURIComponent(documentData.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/list")}
|
||
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={documentData.fileType}
|
||
showIcon={true}
|
||
showText={false}
|
||
showBackground={false}
|
||
size="lg"
|
||
/>
|
||
</div>
|
||
<div className="document-details">
|
||
<div className="document-name">{documentData.name}</div>
|
||
<div className="document-meta">
|
||
<div className="meta-item">
|
||
<i className="ri-file-list-line"></i>
|
||
<span>{getDocumentTypeName(documentData.type)}</span>
|
||
</div>
|
||
<div className="meta-item">
|
||
<i className="ri-time-line"></i>
|
||
<span>{documentData.uploadTime}</span>
|
||
</div>
|
||
<div className="meta-item">
|
||
<i className="ri-hard-drive-line"></i>
|
||
<span>{formatFileSize(documentData.size)}</span>
|
||
</div>
|
||
<div className="meta-item">
|
||
{renderStatusBadge(documentData.auditStatus)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="alert alert-info mb-4">
|
||
<i className="ri-information-line mr-2"></i> 您可以修改此文档的基本信息,但不能更改文档内容。如需修改内容,请删除后重新上传新文档。
|
||
</div>
|
||
|
||
{/* 错误提示 */}
|
||
{formErrors.general && (
|
||
<div className="general-error mb-4">
|
||
<i className="ri-error-warning-line mr-2"></i>
|
||
{formErrors.general}
|
||
</div>
|
||
)}
|
||
|
||
<Form id="edit-form" method="post" ref={formRef} onSubmit={handleBeforeSubmit}>
|
||
<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"
|
||
value={documentData.type}
|
||
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>
|
||
</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="请输入合同编号、许可证号等"
|
||
value={formValues.documentNumber}
|
||
onChange={handleChange}
|
||
/>
|
||
<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 ${touchedFields.auditStatus && formErrors.auditStatus ? 'error' : ''}`}
|
||
value={formValues.auditStatus}
|
||
onChange={handleChange}
|
||
required
|
||
>
|
||
{/* <option value='全部'>请选择审核状态</option> */}
|
||
<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>
|
||
{touchedFields.auditStatus && formErrors.auditStatus && (
|
||
<div className="text-red-500 text-sm mt-1">{formErrors.auditStatus}</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"
|
||
checked={formValues.isTest}
|
||
onChange={handleChange}
|
||
/>
|
||
<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}
|
||
value={formValues.remark}
|
||
onChange={handleChange}
|
||
></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-${documentData.fileType}-line text-${documentData.fileType === 'pdf' ? 'red' : 'blue'}-500 mr-1`}></i>
|
||
<span>{documentData.name}</span>
|
||
</div>
|
||
<div>
|
||
<Button
|
||
type="default"
|
||
size="small"
|
||
icon="ri-download-line"
|
||
className="mr-2"
|
||
onClick={downloadDocument}
|
||
>
|
||
下载
|
||
</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(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) => (
|
||
<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/list"
|
||
>
|
||
返回文档列表
|
||
</Button>
|
||
</div>
|
||
);
|
||
}
|