From 3c253a3031f7396a101af6bae796e6cf72d29628 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Fri, 11 Apr 2025 21:05:10 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E6=96=87=E6=A1=A3=E5=88=97?= =?UTF-8?q?=E8=A1=A8=E7=9A=84=E4=BF=AE=E6=94=B9=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/documents.ts | 110 +++++++++++- app/routes/documents._index.tsx | 33 +++- app/routes/documents.download.tsx | 48 ++++++ app/routes/documents.edit.tsx | 257 ++++++++++++---------------- app/styles/pages/documents_edit.css | 2 +- 5 files changed, 291 insertions(+), 159 deletions(-) create mode 100644 app/routes/documents.download.tsx diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 61ab079..74ccb9c 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -1,4 +1,4 @@ -import { postgrestGet, postgrestDelete, type PostgrestParams } from '../postgrest-client'; +import { postgrestGet, postgrestDelete, postgrestPut, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; import { getDocumentTypes } from '../document-types/document-types'; @@ -315,4 +315,112 @@ export async function getDocument(id: string): Promise<{ status: 500 }; } +} + +/** + * 获取文件下载链接 + * @param filePath 文件路径 + * @returns 下载链接 + */ +export async function getFileDownloadUrl(filePath: string): Promise<{ + data?: { downloadUrl: string }; + error?: string; + status?: number; +}> { + try { + if (!filePath) { + return { error: '文件路径不能为空', status: 400 }; + } + + // 构建API请求参数 + const params: PostgrestParams = { + filter: { + 'path': `eq.${filePath}` + } + }; + + // 这里应该调用获取文件下载链接的API + // 假设后端有这样的端点:/api/files/generate-download-url?path=xxx + // 实际项目中需要根据你的后端API调整 + + // 临时解决方案:返回Remix路由路径 + // 这将通过Remix服务器代理对文件的访问 + return { + data: { + downloadUrl: `/documents/download?path=${encodeURIComponent(filePath)}` + } + }; + + } catch (error) { + console.error('获取文件下载链接失败:', error); + return { + error: error instanceof Error ? error.message : '获取文件下载链接失败', + status: 500 + }; + } +} + +/** + * 更新文档信息 + * @param id 文档ID + * @param document 部分文档数据 + * @returns 更新结果 + */ +export async function updateDocument(id: string, document: Partial & { remark?: string }): Promise<{ + data?: DocumentUI; + error?: string; + status?: number; +}> { + try { + if (!id) { + return { error: '文档ID不能为空', status: 400 }; + } + + // 准备API数据 - 将UI数据转换为API格式 + const apiDocument: Partial = {}; + + if (document.documentNumber !== undefined) { + apiDocument.document_number = document.documentNumber; + } + + if (document.type !== undefined) { + apiDocument.type_id = parseInt(document.type); + } + + if (document.status !== undefined) { + apiDocument.status = document.status as 'pass' | 'warning' | 'waiting' | 'processing' | 'fail'; + } + + if (document.isTest !== undefined) { + apiDocument.is_test_document = document.isTest; + } + + if (document.remark !== undefined) { + apiDocument.remark = document.remark; + } + + console.log('更新文档API数据:', apiDocument); + + const response = await postgrestPut>( + 'documents', + apiDocument, + { id: parseInt(id) } + ); + + if (response.error) { + console.error('更新文档API错误:', response.error); + return { error: response.error, status: response.status }; + } + + // 获取更新后的完整文档数据 + const updatedResponse = await getDocument(id); + + return updatedResponse; + } catch (error) { + console.error('更新文档信息失败:', error); + return { + error: error instanceof Error ? error.message : '更新文档信息失败', + status: 500 + }; + } } \ No newline at end of file diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 09b9350..342038e 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -10,7 +10,7 @@ import { FileTypeTag } from "~/components/ui/FileTypeTag"; import { FileTag } from "~/components/ui/FileTag"; import { FilterPanel, FilterSelect, SearchFilter, DateRangeFilter } from "~/components/ui/FilterPanel"; import documentsIndexStyles from "~/styles/pages/documents_index.css?url"; -import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents"; +import { getDocuments, deleteDocument, type DocumentUI, getFileDownloadUrl } from "~/api/files/documents"; import { getDocumentTypes } from "~/api/document-types/document-types"; // 导入样式 @@ -288,15 +288,30 @@ export default function DocumentsIndex() { }; // 下载文档 - const handleDownload = (path: string, fileName: string) => { + const handleDownload = async (path: string, fileName: string) => { console.log('handleDownload',path,fileName) - // 创建一个隐藏的a标签并点击它 - const a = document.createElement('a'); - a.href = path; - a.download = fileName; // 设置下载的文件名 - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); + try { + // 使用API获取授权的下载链接 + // const { data, error } = await getFileDownloadUrl(path); + + // if (error || !data?.downloadUrl) { + // console.error('获取下载链接失败:', error); + // alert('获取下载链接失败: ' + (error || '未知错误')); + // return; + // } + + // 创建一个隐藏的a标签并点击它 + const a = document.createElement('a'); + // a.href = data.downloadUrl; + a.href = path; + a.download = fileName; // 设置下载的文件名 + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + } catch (err) { + console.error('下载文件失败:', err); + alert('下载文件失败: ' + (err instanceof Error ? err.message : '未知错误')); + } }; // 删除文档 diff --git a/app/routes/documents.download.tsx b/app/routes/documents.download.tsx new file mode 100644 index 0000000..e8c9dac --- /dev/null +++ b/app/routes/documents.download.tsx @@ -0,0 +1,48 @@ +import { LoaderFunctionArgs } from "@remix-run/node"; +import { postgrestGet } from "~/api/postgrest-client"; + +/** + * 文档下载路由 - 处理文档下载请求 + * 通过重定向到带有授权的连接来允许下载文件 + */ +export async function loader({ request }: LoaderFunctionArgs) { + try { + // 获取文件路径参数 + const url = new URL(request.url); + const filePath = url.searchParams.get("path"); + + if (!filePath) { + return new Response("缺少文件路径参数", { status: 400 }); + } + + // 调用Minio API获取带有授权的预签名URL + // 这里假设后端有一个生成预签名URL的API + const response = await postgrestGet<{ presignedUrl: string }>( + '/minio/presign', + { + filter: { + 'object_path': `eq.${filePath}`, + 'expires_in': 'eq.300' // 5分钟有效期 + } + } + ); + + if (response.error) { + console.error("获取文件下载链接失败:", response.error); + return new Response("获取文件下载链接失败", { status: 500 }); + } + + if (!response.data?.presignedUrl) { + return new Response("无法获取文件下载链接", { status: 404 }); + } + + // 重定向到预签名URL,这样浏览器就能直接下载文件 + return Response.redirect(response.data.presignedUrl); + } catch (error) { + console.error("文件下载处理失败:", error); + return new Response( + "文件下载处理失败: " + (error instanceof Error ? error.message : "未知错误"), + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index 6b32f84..f51a633 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -4,6 +4,9 @@ import { redirect, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFu 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 }]; @@ -18,7 +21,7 @@ export const meta: MetaFunction = () => { // 文档状态定义 enum DocumentStatus { - PENDING = "pending", + WAITING = "waiting", PROCESSING = "processing", PASS = "pass", WARNING = "warning", @@ -27,124 +30,13 @@ enum DocumentStatus { // 文档状态对应的中文标签 const STATUS_LABELS: Record = { - [DocumentStatus.PENDING]: "待审核", + [DocumentStatus.WAITING]: "待审核", [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 { - // 这里应该是实际API调用 - return [ - { id: "1", name: "销售合同" }, - { id: "2", name: "采购合同" }, - { id: "3", name: "专卖许可证" }, - { id: "4", name: "行政处罚决定书" }, - { id: "5", name: "承包协议" } - ]; -} - -// 模拟API获取文档详情 -async function getDocument(id: string): Promise { - // 这里应该是实际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): Promise { - // 这里应该是实际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"; @@ -168,12 +60,23 @@ export async function loader({ request }: LoaderFunctionArgs) { } // 并行获取文档详情和文档类型列表 - const [document, documentTypes] = await Promise.all([ + const [documentResponse, documentTypesResponse] = await Promise.all([ getDocument(id), getDocumentTypes() ]); - return Response.json({ document, documentTypes }); + 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 }); @@ -194,19 +97,19 @@ export async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); // 从表单数据中提取字段 - const type_id = formData.get("type_id") as string; - const document_number = formData.get("document_number") as string; + const type = formData.get("type_id") as string; + const documentNumber = formData.get("document_number") as string; const status = formData.get("status") as DocumentStatus; - const is_test_document = formData.get("is_test_document") === "on"; + const isTest = formData.get("is_test_document") === "on"; const remark = formData.get("remark") as string; // 验证必填字段 - if (!type_id || !status) { + if (!type || !status) { return Response.json( { error: "缺少必填字段", fieldErrors: { - type_id: !type_id ? "文档类型不能为空" : null, + type_id: !type ? "文档类型不能为空" : null, status: !status ? "状态不能为空" : null } }, @@ -214,20 +117,33 @@ export async function action({ request }: ActionFunctionArgs) { ); } + console.log('提交更新:', { type, documentNumber, status, isTest, remark }); + // 更新文档 - await updateDocument(id, { - type_id, - document_number: document_number || null, + const updateResponse = await updateDocument(id, { + type, + documentNumber, status, - is_test_document, - remark: remark || null + 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: "更新文档失败" }, { status: 500 }); + return Response.json({ + error: "更新文档失败", + message: error instanceof Error ? error.message : "发生未知错误" + }, { status: 500 }); } } @@ -237,8 +153,14 @@ export default function DocumentEdit() { const actionData = useActionData(); const navigate = useNavigate(); + // 定义类型 + interface DocType { + id: string | number; + name: string; + } + // 状态 - const [localStatus, setLocalStatus] = useState(document.status); + const [localStatus, setLocalStatus] = useState(document.status as DocumentStatus); // 处理状态变更 const handleStatusChange = (e: React.ChangeEvent) => { @@ -247,23 +169,31 @@ export default function DocumentEdit() { // 获取文档类型名称 const getDocumentTypeName = (typeId: string): string => { - const docType = documentTypes.find((type: DocumentType) => type.id === typeId); - return docType ? docType.name : "未知类型"; + const docType = documentTypes.find((type) => (type as any).id.toString() === typeId); + return docType ? (docType as any).name : "未知类型"; }; // 渲染状态徽章 - const renderStatusBadge = (status: DocumentStatus) => { - const statusClasses: Record = { - [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" + const renderStatusBadge = (status: string) => { + const statusClasses: Record = { + "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 = { + "waiting": "待审核", + "processing": "审核中", + "pass": "通过", + "warning": "警告", + "fail": "不通过" }; return ( - - {STATUS_LABELS[status]} + + {statusLabel[status] || status} ); }; @@ -296,22 +226,28 @@ export default function DocumentEdit() {
- +
{document.name}
- {getDocumentTypeName(document.type_id)} + {getDocumentTypeName(document.type)}
- {document.upload_time} + {document.uploadTime}
- {formatFileSize(document.file_size)} + {formatFileSize(document.size)}
{renderStatusBadge(document.status)} @@ -332,10 +268,10 @@ export default function DocumentEdit() { id="type-id" name="type_id" className="form-select" - defaultValue={document.type_id} + defaultValue={document.type} required > - {documentTypes.map((type: DocumentType) => ( + {documentTypes.map(type => ( ))} @@ -353,7 +289,7 @@ export default function DocumentEdit() { name="document_number" className="form-input" placeholder="请输入合同编号、许可证号等" - defaultValue={document.document_number || ""} + defaultValue={document.documentNumber || ""} />
如无编号可留空
@@ -368,7 +304,7 @@ export default function DocumentEdit() { onChange={handleStatusChange} required > - + @@ -388,7 +324,7 @@ export default function DocumentEdit() { type="checkbox" id="is-test-document" name="is_test_document" - defaultChecked={document.is_test_document} + defaultChecked={document.isTest} /> 标记为测试文档 @@ -454,7 +390,32 @@ export default function DocumentEdit() { {/* 修改历史 */}
- {document.history.map((item: HistoryItem, index: number) => ( + {[ + { + 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) => (
{item.time}
diff --git a/app/styles/pages/documents_edit.css b/app/styles/pages/documents_edit.css index bfff123..4277577 100644 --- a/app/styles/pages/documents_edit.css +++ b/app/styles/pages/documents_edit.css @@ -22,7 +22,7 @@ .document-edit-page .form-textarea, .document-edit-page .form-select { @apply w-full rounded-md border border-gray-300 shadow-sm px-3 py-2; - @apply focus:outline-none focus:ring-1 focus:ring-primary-500 focus:border-primary-500; + @apply focus:outline-none focus:border-[var(--primary-color)] focus:shadow-[0_0_0_2px_rgba(0,104,74,0.2)]; } .document-edit-page .text-secondary {