From cb52bf81790afc2a5797fb1f8aa8cf382d9f325f Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Sun, 13 Apr 2025 18:49:43 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/client.ts | 2 +- app/api/files.ts | 133 -------------------------- app/api/files/documents.ts | 19 ++-- app/api/files/files-upload.ts | 128 +++++++++++++++++++++++++ app/api/filesApi.ts | 88 ----------------- app/api/rulesApi.ts | 75 --------------- app/routes/documents._index.tsx | 57 +++++++++-- app/routes/documents.edit.tsx | 126 +++++++++++++----------- app/routes/files.upload.tsx | 153 +++++++++++++++++++++++------- app/styles/pages/files_upload.css | 22 +++++ 10 files changed, 396 insertions(+), 407 deletions(-) delete mode 100644 app/api/files.ts delete mode 100644 app/api/filesApi.ts delete mode 100644 app/api/rulesApi.ts diff --git a/app/api/client.ts b/app/api/client.ts index 66263e5..c9b6575 100644 --- a/app/api/client.ts +++ b/app/api/client.ts @@ -16,7 +16,7 @@ export type QueryParams = Record; // 获取 API 基础 URL // const API_BASE_URL = '172.18.0.100:3000'; // const API_BASE_URL = '172.16.0.119:9000/admin'; -const API_BASE_URL = 'http://nas.7bm.co:3000'; +export const API_BASE_URL = 'http://nas.7bm.co:3000'; // 是否使用模拟数据(开发环境使用) const USE_MOCK_DATA = false; // 设置为true使用模拟数据,避免API连接问题 diff --git a/app/api/files.ts b/app/api/files.ts deleted file mode 100644 index a699beb..0000000 --- a/app/api/files.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { apiRequest } from './client'; -import { FileType, Priority } from '~/types/enums'; - -/** - * 将文件转换为二进制数据 - */ -export async function uploadFileToBinary(file: File): Promise { - return new Promise((resolve, reject) => { - const reader = new FileReader(); - reader.onload = () => { - if (reader.result instanceof ArrayBuffer) { - resolve(reader.result); - } else { - reject(new Error('文件读取失败')); - } - }; - reader.onerror = () => reject(new Error('文件读取失败')); - reader.readAsArrayBuffer(file); - }); -} - -/** - * 上传文件到服务器 - */ -export async function uploadFileToServer( - binaryData: ArrayBuffer, - fileName: string, - fileType: string, - documentType: FileType, - priority: Priority -): Promise<{ success: boolean; fileId?: string; message?: string; error?: string }> { - try { - const formData = new FormData(); - formData.append('file', new Blob([binaryData], { type: fileType }), fileName); - formData.append('documentType', documentType); - formData.append('priority', priority); - - const response = await apiRequest<{ success: boolean; fileId?: string; message?: string }>( - '/api/files/upload', - { - method: 'POST', - body: formData - } - ); - - if (response.error) { - return { success: false, error: response.error }; - } - - return { success: true, fileId: response.data?.fileId, message: response.data?.message }; - } catch (error) { - return { success: false, error: error instanceof Error ? error.message : '上传失败' }; - } -} - -/** - * 上传文件到文档审核系统 - */ -export async function uploadDocumentToServer( - binaryData: ArrayBuffer, - fileName: string, - fileType: string, - typeId: string | number, - priority: string, - documentNumber?: string | null, - remark?: string | null, - isTestDocument: boolean = false -): Promise<{ - success: boolean; - result?: { - id: number; - file_name: string; - file_size: number; - file_url: string; - type_id: number; - type_description: string; - document_number: string | null; - storage_type: string; - is_test_document: boolean; - remark: string | null; - background_processing: boolean; - evaluation_level: string; - }; - error: string | null; -}> { - try { - // 创建FormData对象 - const formData = new FormData(); - - // 将二进制数据转换为Blob并添加到FormData - const blob = new Blob([binaryData], { type: fileType }); - formData.append('file', blob, fileName); - - // 将信息添加到一个JSON对象中 - const uploadInfo = { - type_id: Number(typeId), - evaluation_level: priority, - document_number: documentNumber || null, - remark: remark || null, - is_test_document: isTestDocument - }; - - // 添加JSON字符串到FormData - formData.append('upload_info', JSON.stringify(uploadInfo)); - - // 发送请求 - const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { - method: 'POST', - headers: { - 'X-File-Name': encodeURIComponent(fileName) - }, - body: formData - }); - - if (!response.ok) { - const errorText = await response.text(); - console.error(`上传失败 (${response.status}): ${errorText}`); - return { - success: false, - error: `上传失败: ${response.status} ${response.statusText}` - }; - } - - const data = await response.json(); - return data; - } catch (error) { - console.error('上传错误:', error); - return { - success: false, - error: error instanceof Error ? error.message : '上传失败' - }; - } -} \ No newline at end of file diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 74ccb9c..05c6017 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -67,6 +67,8 @@ export interface Document { is_test_document: boolean; evaluation_level: string; status: 'pass' | 'warning' | 'waiting' | 'processing' | 'fail'; + file_status: 'Waiting' | 'Cutting' | 'Extractioning' | 'Evaluationing' | 'Processed'; + audit_status: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中 ocr_result: unknown; extracted_results: unknown; summary: unknown; @@ -85,7 +87,8 @@ export interface DocumentUI { type: string; typeName: string; size: number; - status: string; + auditStatus: number; // -1: 不通过, 0: 待审核, 1: 通过, 2: 警告, 3: 审核中 + fileStatus: string; // Waiting, Cutting, Extractioning, Evaluationing, Processed issues: number | null; uploadTime: string; fileType: string; @@ -119,7 +122,8 @@ async function convertToUIDocument(doc: Document): Promise { type: doc.type_id.toString(), typeName: docType?.name || '未知类型', size: doc.file_size, - status: doc.status, + auditStatus: doc.audit_status, + fileStatus: doc.file_status || 'Processed', // 默认为已处理 issues: 0, // 固定为0 uploadTime: formatDate(doc.updated_at), fileType: getFileExtension(doc.name), @@ -332,13 +336,6 @@ export async function getFileDownloadUrl(filePath: string): Promise<{ return { error: '文件路径不能为空', status: 400 }; } - // 构建API请求参数 - const params: PostgrestParams = { - filter: { - 'path': `eq.${filePath}` - } - }; - // 这里应该调用获取文件下载链接的API // 假设后端有这样的端点:/api/files/generate-download-url?path=xxx // 实际项目中需要根据你的后端API调整 @@ -387,8 +384,8 @@ export async function updateDocument(id: string, document: Partial & apiDocument.type_id = parseInt(document.type); } - if (document.status !== undefined) { - apiDocument.status = document.status as 'pass' | 'warning' | 'waiting' | 'processing' | 'fail'; + if (document.auditStatus !== undefined) { + apiDocument.audit_status = document.auditStatus; } if (document.isTest !== undefined) { diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index c317975..f6ebb8c 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -1,5 +1,6 @@ import { postgrestGet, postgrestPut, postgrestPost, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; +// import { API_BASE_URL } from '../client'; /** * 格式化日期 @@ -39,6 +40,7 @@ function extractApiData(responseData: unknown): T | null { // 文档状态枚举 export enum DocumentStatus { + WAITING = "waiting", CUTTING = "Cutting", EXTRACTIONING = "extractioning", REVIEWING = "reviewing", @@ -80,6 +82,132 @@ export interface Document { remark?: string; } +// 文件上传响应接口 +export interface FileUploadResponse { + success: boolean; + result?: { + id: number; + file_name: string; + file_size: number; + file_url: string; + type_id: number; + type_description: string; + document_number: string | null; + storage_type: string; + is_test_document: boolean; + remark: string | null; + background_processing: boolean; + evaluation_level: string; + }; + error: string | null; +} + +/** + * 将文件转换为二进制数据 + */ +export async function uploadFileToBinary(file: File): Promise { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (reader.result instanceof ArrayBuffer) { + resolve(reader.result); + } else { + reject(new Error('文件读取失败')); + } + }; + reader.onerror = () => reject(new Error('文件读取失败')); + reader.readAsArrayBuffer(file); + }); +} + +/** + * 上传文件到文档审核系统 + * @param binaryData 文件的二进制数据 + * @param fileName 文件名 + * @param fileType 文件类型 + * @param typeId 文档类型ID + * @param priority 优先级 + * @param documentNumber 文档编号(可选) + * @param remark 备注信息(可选) + * @param isTestDocument 是否为测试文档 + * @returns 上传结果 + */ +export async function uploadDocumentToServer( + binaryData: ArrayBuffer, + fileName: string, + fileType: string, + typeId: string | number, + priority: string, + documentNumber?: string | null, + remark?: string | null, + isTestDocument: boolean = false +): Promise<{data: FileUploadResponse; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 创建FormData对象 + const formData = new FormData(); + + // 将二进制数据转换为Blob并添加到FormData + const blob = new Blob([binaryData], { type: fileType }); + formData.append('file', blob, fileName); + + // 将信息添加到一个JSON对象中 + const uploadInfo = { + type_id: Number(typeId), + evaluation_level: priority, + document_number: documentNumber || null, + remark: remark || null, + is_test_document: isTestDocument + }; + + // 添加JSON字符串到FormData + formData.append('upload_info', JSON.stringify(uploadInfo)); + + console.log('上传信息:', { + fileName, + fileType, + typeId: Number(typeId), + priority, + documentNumber: documentNumber || null, + remark: remark || null, + isTestDocument + }); + + // 发送请求 + // const response = await fetch(`${API_BASE_URL}/admin/documents/upload`, { + const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { + method: 'POST', + headers: { + 'X-File-Name': encodeURIComponent(fileName) + }, + body: formData + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`上传失败 (${response.status}): ${errorText}`); + return { + error: `上传失败: ${response.status} ${response.statusText}`, + status: response.status + }; + } + + const responseData = await response.json(); + const extractedData = extractApiData(responseData); + + if (!extractedData) { + return { error: '处理上传响应失败', status: 500 }; + } + + return { data: extractedData }; + } catch (error) { + console.error('上传错误:', error); + return { + error: error instanceof Error ? error.message : '上传失败', + status: 500 + }; + } +} + /** * 获取当天的文档列表 * @returns 文档列表 diff --git a/app/api/filesApi.ts b/app/api/filesApi.ts deleted file mode 100644 index b7f2340..0000000 --- a/app/api/filesApi.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { apiRequest, buildUrl, type PaginatedResponse } from './base'; -import type { File, DocumentType } from '~/models/file'; - -/** - * 文件API服务 - */ - -interface FileFilterParams { - documentTypeId?: string; - status?: string; - reviewStatus?: string; - keyword?: string; - startDate?: string; - endDate?: string; - page?: number; - pageSize?: number; -} - -// 获取文件列表 -export async function getFiles(params?: FileFilterParams): Promise> { - const url = buildUrl('/api/files', params); - return apiRequest>(url); -} - -// 获取单个文件 -export async function getFile(id: string): Promise { - const url = buildUrl(`/api/files/${id}`); - return apiRequest(url); -} - -// 上传文件 -export async function uploadFile(formData: FormData): Promise { - const url = buildUrl('/api/files/upload'); - - const response = await fetch(url, { - method: 'POST', - body: formData, - }); - - if (!response.ok) { - const error = await response.json(); - throw new Error(error.message || '文件上传失败'); - } - - return response.json().then(data => data.data); -} - -// 删除文件 -export async function deleteFile(id: string): Promise { - const url = buildUrl(`/api/files/${id}`); - return apiRequest(url, 'DELETE'); -} - -// 获取文档类型列表 -export async function getDocumentTypes(): Promise { - const url = buildUrl('/api/document-types'); - return apiRequest(url); -} - -// 获取单个文档类型 -export async function getDocumentType(id: string): Promise { - const url = buildUrl(`/api/document-types/${id}`); - return apiRequest(url); -} - -// 创建文档类型 -// 请使用 ~/api/document-types/document-types.ts 中的实现 -/* -export async function createDocumentType(documentType: Omit): Promise { - const url = buildUrl('/api/document-types'); - return apiRequest(url, 'POST', documentType); -} -*/ - -// 更新文档类型 -// 请使用 ~/api/document-types/document-types.ts 中的实现 -/* -export async function updateDocumentType(id: string, documentType: Partial>): Promise { - const url = buildUrl(`/api/document-types/${id}`); - return apiRequest(url, 'PUT', documentType); -} -*/ - -// 删除文档类型 -export async function deleteDocumentType(id: string): Promise { - const url = buildUrl(`/api/document-types/${id}`); - return apiRequest(url, 'DELETE'); -} \ No newline at end of file diff --git a/app/api/rulesApi.ts b/app/api/rulesApi.ts deleted file mode 100644 index 8b8d3ed..0000000 --- a/app/api/rulesApi.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { apiRequest, buildUrl, type PaginatedResponse } from './base'; -import type { Rule, RuleGroup } from '~/models/rule'; - -/** - * 评查规则API服务 - */ - -interface RuleFilterParams { - ruleType?: string; - groupId?: string; - isActive?: boolean; - keyword?: string; - page?: number; - pageSize?: number; -} - -// 获取评查点列表 -export async function getRules(params?: RuleFilterParams): Promise> { - const url = buildUrl('/api/rules', params); - return apiRequest>(url); -} - -// 获取单个评查点 -export async function getRule(id: string): Promise { - const url = buildUrl(`/api/rules/${id}`); - return apiRequest(url); -} - -// 创建评查点 -export async function createRule(rule: Omit): Promise { - const url = buildUrl('/api/rules'); - return apiRequest(url, 'POST', rule); -} - -// 更新评查点 -export async function updateRule(id: string, rule: Partial>): Promise { - const url = buildUrl(`/api/rules/${id}`); - return apiRequest(url, 'PUT', rule); -} - -// 删除评查点 -export async function deleteRule(id: string): Promise { - const url = buildUrl(`/api/rules/${id}`); - return apiRequest(url, 'DELETE'); -} - -// 获取评查点分组列表 -export async function getRuleGroups(): Promise { - const url = buildUrl('/api/rule-groups'); - return apiRequest(url); -} - -// 获取单个评查点分组 -export async function getRuleGroup(id: string): Promise { - const url = buildUrl(`/api/rule-groups/${id}`); - return apiRequest(url); -} - -// 创建评查点分组 -export async function createRuleGroup(group: Omit): Promise { - const url = buildUrl('/api/rule-groups'); - return apiRequest(url, 'POST', group); -} - -// 更新评查点分组 -export async function updateRuleGroup(id: string, group: Partial>): Promise { - const url = buildUrl(`/api/rule-groups/${id}`); - return apiRequest(url, 'PUT', group); -} - -// 删除评查点分组 -export async function deleteRuleGroup(id: string): Promise { - const url = buildUrl(`/api/rule-groups/${id}`); - return apiRequest(url, 'DELETE'); -} \ No newline at end of file diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 342038e..19ff77f 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -5,12 +5,11 @@ import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { Table } from "~/components/ui/Table"; import { Pagination } from "~/components/ui/Pagination"; -import { StatusBadge } from "~/components/ui/StatusBadge"; 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, getFileDownloadUrl } from "~/api/files/documents"; +import { getDocuments, deleteDocument, type DocumentUI } from "~/api/files/documents"; import { getDocumentTypes } from "~/api/document-types/document-types"; // 导入样式 @@ -122,6 +121,24 @@ const documentStatusOptions = [ { value: "fail", label: "不通过" }, ]; +// 文件处理状态选项 +const fileProcessingStatusOptions = [ + { value: "Waiting", label: "上传中", icon: "ri-loader-line" }, + { value: "Cutting", label: "切分中", icon: "ri-loader-line" }, + { value: "Extractioning", label: "抽取中", icon: "ri-loader-line" }, + { value: "Evaluationing", label: "评查中", icon: "ri-loader-line" }, + { value: "Processed", label: "已完成", icon: "ri-check-line" }, +]; + +// 审核状态选项及样式 +const auditStatusMapping: Record = { + "-1": { label: "不通过", color: "red", icon: "ri-close-line" }, + "0": { label: "待审核", color: "blue", icon: "ri-time-line" }, + "1": { label: "通过", color: "green", icon: "ri-check-line" }, + "2": { label: "警告", color: "yellow", icon: "ri-alert-line" }, + "3": { label: "审核中", color: "purple", icon: "ri-search-line" }, +}; + // 格式化文件大小 const formatFileSize = (bytes: number) => { if (bytes === 0) return "0 Bytes"; @@ -417,12 +434,34 @@ export default function DocumentsIndex() { width: "100px", render: (_: unknown, record: DocumentUI) => formatFileSize(record.size) }, + { + title: "文件状态", + key: "fileStatus", + render: (_: unknown, record: DocumentUI) => { + const status = fileProcessingStatusOptions.find(s => s.value === record.fileStatus) || + fileProcessingStatusOptions[0]; + const isSpinning = record.fileStatus !== "Processed"; + return ( +
+ + {status.label} +
+ ); + } + }, { title: "审核状态", - key: "status", - render: (_: unknown, record: DocumentUI) => ( - - ) + key: "auditStatus", + render: (_: unknown, record: DocumentUI) => { + const statusKey = record.auditStatus.toString(); + const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"]; + return ( +
+ + {statusInfo.label} +
+ ); + } }, { title: "问题数量", @@ -443,7 +482,7 @@ export default function DocumentsIndex() { width: "280px", render: (_: unknown, record: DocumentUI) => (
- {record.status === "waiting" ? ( + {record.auditStatus === 0 ? ( 开始审核 - ) : record.status === "processing" ? ( + ) : record.auditStatus === 3 ? ( 上传文档 diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index f51a633..29b5db0 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -19,22 +19,31 @@ export const meta: MetaFunction = () => { ]; }; -// 文档状态定义 -enum DocumentStatus { - WAITING = "waiting", - PROCESSING = "processing", - PASS = "pass", - WARNING = "warning", - FAIL = "fail" +// 文档审核状态定义 +enum DocumentAuditStatus { + FAIL = -1, + WAITING = 0, + PASS = 1, + WARNING = 2, + PROCESSING = 3 } // 文档状态对应的中文标签 -const STATUS_LABELS: Record = { - [DocumentStatus.WAITING]: "待审核", - [DocumentStatus.PROCESSING]: "审核中", - [DocumentStatus.PASS]: "通过", - [DocumentStatus.WARNING]: "警告", - [DocumentStatus.FAIL]: "不通过" +const STATUS_LABELS: Record = { + [DocumentAuditStatus.FAIL]: "不通过", + [DocumentAuditStatus.WAITING]: "待审核", + [DocumentAuditStatus.PASS]: "通过", + [DocumentAuditStatus.WARNING]: "警告", + [DocumentAuditStatus.PROCESSING]: "审核中" +}; + +// 文档状态样式配置 +const STATUS_STYLES: Record = { + [-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" } }; // 格式化文件大小 @@ -99,31 +108,31 @@ export async function action({ request }: ActionFunctionArgs) { // 从表单数据中提取字段 const type = formData.get("type_id") as string; const documentNumber = formData.get("document_number") as string; - const status = formData.get("status") as DocumentStatus; + 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 || !status) { + if (!type || auditStatus === undefined || isNaN(auditStatus)) { return Response.json( { error: "缺少必填字段", fieldErrors: { type_id: !type ? "文档类型不能为空" : null, - status: !status ? "状态不能为空" : null + audit_status: (auditStatus === undefined || isNaN(auditStatus)) ? "审核状态不能为空" : null } }, { status: 400 } ); } - console.log('提交更新:', { type, documentNumber, status, isTest, remark }); + console.log('提交更新:', { type, documentNumber, auditStatus, isTest, remark }); // 更新文档 const updateResponse = await updateDocument(id, { type, documentNumber, - status, + auditStatus, isTest, remark }); @@ -160,43 +169,38 @@ export default function DocumentEdit() { } // 状态 - const [localStatus, setLocalStatus] = useState(document.status as DocumentStatus); + const [localStatus, setLocalStatus] = useState(document.auditStatus); // 处理状态变更 const handleStatusChange = (e: React.ChangeEvent) => { - setLocalStatus(e.target.value as DocumentStatus); + setLocalStatus(parseInt(e.target.value)); }; // 获取文档类型名称 const getDocumentTypeName = (typeId: string): string => { - const docType = documentTypes.find((type) => (type as any).id.toString() === typeId); - return docType ? (docType as any).name : "未知类型"; + const docType = documentTypes.find((type: DocType) => type.id.toString() === typeId); + return docType ? docType.name : "未知类型"; }; // 渲染状态徽章 - 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": "不通过" - }; + const renderStatusBadge = (status: number) => { + const style = STATUS_STYLES[status] || STATUS_STYLES[0]; + const label = STATUS_LABELS[status as DocumentAuditStatus] || STATUS_LABELS[DocumentAuditStatus.WAITING]; return ( - - {statusLabel[status] || status} + + + {label} ); }; + + // 在新窗口打开文档预览 + const openPreview = () => { + // 假设有一个预览URL的格式,比如 /preview?path=xxx + const previewUrl = `/preview?path=${encodeURIComponent(document.path)}&name=${encodeURIComponent(document.name)}`; + window.open(previewUrl, '_blank'); + }; return (
@@ -250,7 +254,7 @@ export default function DocumentEdit() { {formatFileSize(document.size)}
- {renderStatusBadge(document.status)} + {renderStatusBadge(document.auditStatus)}
@@ -271,7 +275,7 @@ export default function DocumentEdit() { defaultValue={document.type} required > - {documentTypes.map(type => ( + {documentTypes.map((type: DocType) => ( ))} @@ -295,24 +299,24 @@ export default function DocumentEdit() {
- +
更改状态可能会影响此文档在列表中的显示和排序
- {actionData?.fieldErrors?.status && ( -
{actionData.fieldErrors.status}
+ {actionData?.fieldErrors?.audit_status && ( +
{actionData.fieldErrors.audit_status}
)}
@@ -356,7 +360,7 @@ export default function DocumentEdit() {
- + {document.name}
@@ -364,21 +368,31 @@ export default function DocumentEdit() { type="default" size="small" icon="ri-download-line" + className="mr-2" > 下载 +
- +

预览功能暂不可用

-

PDF文件需要外部查看器支持

+

点击"在新窗口打开"查看完整文档

@@ -388,7 +402,7 @@ export default function DocumentEdit() { {/* 修改历史 */} - +
{[ { diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index a3e5741..75620ad 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -1,5 +1,5 @@ import { useState, useEffect, useRef } from "react"; -import { MetaFunction, ActionFunctionArgs, json } from "@remix-run/node"; +import { MetaFunction, ActionFunctionArgs } from "@remix-run/node"; import { Form, useActionData, useLoaderData } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; @@ -8,8 +8,17 @@ import { UploadArea, UploadAreaRef } from "~/components/ui/UploadArea"; import { FileProgress} from "~/components/ui/FileProgress"; import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps"; import uploadStyles from "~/styles/pages/files_upload.css?url"; -import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload"; -import { uploadFileToBinary, uploadDocumentToServer } from "~/api/files"; +import { + getTodayDocuments, + getDocumentTypes, + getDocumentsStatus, + uploadFileToBinary, + uploadDocumentToServer, + type Document, + type DocumentType, + type FileUploadResponse, + DocumentStatus +} from "~/api/files/files-upload"; export function links() { return [ @@ -74,6 +83,20 @@ export enum StepStatus { COMPLETED = "completed" } +// 模拟API支持的存储类型 +const STORAGE_TYPES = [ + { id: "minio", name: "MinIO对象存储" }, + { id: "local", name: "本地文件系统" }, + { id: "s3", name: "Amazon S3" } +]; + +// 文件上传完成后的操作选项 +const AFTER_UPLOAD_OPTIONS = [ + { id: "list", name: "返回文档列表" }, + { id: "stay", name: "留在当前页面" }, + { id: "audit", name: "立即开始审核" } +]; + // 上传的文件信息接口 export interface UploadedFile { id: number; @@ -90,26 +113,6 @@ export interface UploadedFile { }; } -// 文件上传响应接口 -interface FileUploadResponse { - success: boolean; - result?: { - id: number; - file_name: string; - file_size: number; - file_url: string; - type_id: number; - type_description: string; - document_number: string | null; - storage_type: string; - is_test_document: boolean; - remark: string | null; - background_processing: boolean; - evaluation_level: string; - }; - error: string | null; -} - // 模拟上传文件到服务器的API async function uploadFileToServer( binaryData: ArrayBuffer, @@ -122,7 +125,7 @@ async function uploadFileToServer( isTestDocument: boolean ): Promise { // 在实际应用中,这里会使用fetch或axios发送请求到后端API - console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); + console.log(`[API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); try { // 使用封装的上传函数 @@ -137,9 +140,26 @@ async function uploadFileToServer( isTestDocument ); - return response; + if (response.error) { + console.error('[API] 上传错误:', response.error); + return { + success: false, + error: response.error + }; + } + + // 确保返回有效的FileUploadResponse对象 + if (response.data) { + return response.data; + } + + // 如果没有数据,则返回错误 + return { + success: false, + error: '上传失败,未获取到响应数据' + }; } catch (error) { - console.error('[模拟API] 上传错误:', error); + console.error('[API] 上传错误:', error); return { success: false, error: error instanceof Error ? error.message : '上传失败' @@ -183,7 +203,7 @@ export async function action({ request }: ActionFunctionArgs) { // 如果有错误,返回错误信息 if (Object.keys(errors).length > 0) { - return json({ errors }); + return Response.json({ errors }); } // 获取文件信息 @@ -195,7 +215,7 @@ export async function action({ request }: ActionFunctionArgs) { // 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。 // 模拟文件上传成功响应 - return json({ + return Response.json({ success: true, message: "文件上传请求已接收", fileId: `file_${Date.now()}`, @@ -204,7 +224,7 @@ export async function action({ request }: ActionFunctionArgs) { }); } catch (error) { console.error("文件上传失败:", error); - return json( + return Response.json( { success: false, error: "文件上传失败,请重试" }, { status: 500 } ); @@ -254,6 +274,8 @@ export default function FilesUpload() { const { documents, documentTypes } = useLoaderData(); // 状态管理 + // 高级上传设置 + const [showAdvancedOptions, setShowAdvancedOptions] = useState(false); const [isTestDocument, setIsTestDocument] = useState(false); const [fileType, setFileType] = useState(""); const [priority, setPriority] = useState(Priority.NORMAL); @@ -262,7 +284,7 @@ export default function FilesUpload() { const [currentFiles, setCurrentFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); - const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed">("idle"); + const [uploadStage, setUploadStage] = useState<"idle" | "uploading" | "processing" | "completed" | "hadden">("idle"); const [processingSteps, setProcessingSteps] = useState([ { title: "文件上传", description: "等待上传文件到服务器...", status: "waiting" }, { title: "文档转换拆分", description: "转换文档格式,拆分文档内容", status: "waiting" }, @@ -310,7 +332,8 @@ export default function FilesUpload() { useEffect(() => { console.log('设置上传队列状态检查定时器'); - // 设置定时器检查队列中文件的状态 + // 设置定时器检查队列中文件的状态,初始先加载一次查询 + checkQueueStatus(); statusCheckIntervalRef.current = setInterval(checkQueueStatus, 10000); // 只在组件卸载时清除 @@ -745,7 +768,6 @@ export default function FilesUpload() { }; - // 格式化文件大小显示 const formatFileSize = (bytes: number) => { if (bytes === 0) return '0 Bytes'; @@ -805,6 +827,11 @@ export default function FilesUpload() { let statusText = ""; switch(record.status) { + case DocumentStatus.WAITING: + statusClass = "status-processing"; + statusIcon = "ri-loader-4-line"; + statusText = "等待中"; + break; case DocumentStatus.CUTTING: statusClass = "status-processing"; statusIcon = "ri-loader-4-line"; @@ -950,7 +977,9 @@ export default function FilesUpload() { tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片" shouldPreventFileSelect={!fileType} /> -
+ + {/* 测试文档标记 */} +
+ + {/* 高级上传设置 */} + { showAdvancedOptions && ( +
+
setShowAdvancedOptions(!showAdvancedOptions)} + > + 高级上传设置 + +
+ +
+
+
+ + +
选择文档的存储位置
+
+ +
+ + +
上传完成后自动执行的操作
+
+
+
+
+ + )} )} @@ -982,7 +1066,8 @@ export default function FilesUpload() { )} {/* 文件信息显示 - 上传完成后显示 */} - {uploadStage !== "idle" && completedFiles.length > 0 && ( + {/* {uploadStage !== "idle" && completedFiles.length > 0 && ( */} + {uploadStage === "hadden" && completedFiles.length > 0 && (

文件信息

diff --git a/app/styles/pages/files_upload.css b/app/styles/pages/files_upload.css index 7cd382d..c9d2562 100644 --- a/app/styles/pages/files_upload.css +++ b/app/styles/pages/files_upload.css @@ -58,6 +58,28 @@ @apply transform translate-x-5; } +/* 高级上传设置 */ +/* 高级选项区域 */ +.file-upload-page .advanced-options { + @apply mt-4; +} + +.file-upload-page .advanced-options-toggle { + @apply text-[var(--primary-color)] cursor-pointer inline-flex items-center text-sm; +} + +.file-upload-page .advanced-options-toggle i { + @apply ml-1 transition-transform duration-200; +} + +.file-upload-page .advanced-options-toggle.open i { + @apply rotate-180; +} + +.file-upload-page .advanced-options-content { + @apply mt-3 p-3 bg-gray-50 border border-gray-200 rounded-md hidden; +} + .file-upload-page .form-label { @apply block text-sm font-medium text-gray-700 mb-2; } From 5573724390b7f70903f56f16f763c47407015be0 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 14 Apr 2025 16:47:26 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E5=88=97=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/files/documents.ts | 31 +++-- app/api/files/files-upload.ts | 9 +- app/routes/documents._index.tsx | 209 ++++++++++++++++++++------------ app/routes/documents.edit.tsx | 7 +- app/routes/files.upload.tsx | 41 +++---- 5 files changed, 182 insertions(+), 115 deletions(-) diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 05c6017..f2d9ea0 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -45,6 +45,8 @@ export interface DocumentSearchParams { documentNumber?: string; documentType?: string; status?: string; + auditStatus?: string; + fileStatus?: string; dateFrom?: string; dateTo?: string; page?: number; @@ -123,7 +125,7 @@ async function convertToUIDocument(doc: Document): Promise { typeName: docType?.name || '未知类型', size: doc.file_size, auditStatus: doc.audit_status, - fileStatus: doc.file_status || 'Processed', // 默认为已处理 + fileStatus: doc.status || '', // 默认为'' issues: 0, // 固定为0 uploadTime: formatDate(doc.updated_at), fileType: getFileExtension(doc.name), @@ -173,20 +175,35 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro filter['type_id'] = `eq.${searchParams.documentType}`; } - if (searchParams.status) { - filter['status'] = `eq.${searchParams.status}`; + if (searchParams.auditStatus) { + filter['audit_status'] = `eq.${searchParams.auditStatus}`; + } + + if (searchParams.fileStatus) { + filter['status'] = `eq.${searchParams.fileStatus}`; } // 处理日期范围 if (searchParams.dateFrom) { - filter['updated_at'] = `gte.${searchParams.dateFrom}`; + // 添加当天开始时间 00:00:00 + filter['created_at'] = `gte.'${searchParams.dateFrom} 00:00:00'`; } if (searchParams.dateTo) { - const dateToKey = searchParams.dateFrom ? 'and.updated_at.lte' : 'updated_at'; - filter[dateToKey] = `lte.${searchParams.dateTo}`; + // 如果有开始日期,使用and条件;否则直接设置结束日期 + const dateToKey = searchParams.dateFrom ? 'and' : 'created_at'; + + // 添加当天结束时间 23:59:59 + if (dateToKey === 'and') { + delete filter['created_at']; + // 使用OR操作符连接两个条件 + filter[dateToKey] = `(created_at.gte.'${dayjs(searchParams.dateFrom).format()}',created_at.lte.'${dayjs(searchParams.dateTo).format()}')`; + } else { + filter['created_at'] = `lte.'${dayjs(searchParams.dateTo).format()}'`; + } } + console.log('filter-----', filter); params.filter = filter; // 发送请求 @@ -204,7 +221,7 @@ export async function getDocuments(searchParams: DocumentSearchParams = {}): Pro // 转换为UI格式 const documents = await Promise.all(extractedData.map(convertToUIDocument)); - + // console.log('documentsItem',documents) // 获取总数 let totalCount = 0; const responseWithHeaders = response as { diff --git a/app/api/files/files-upload.ts b/app/api/files/files-upload.ts index f6ebb8c..0f9e0d6 100644 --- a/app/api/files/files-upload.ts +++ b/app/api/files/files-upload.ts @@ -40,11 +40,12 @@ function extractApiData(responseData: unknown): T | null { // 文档状态枚举 export enum DocumentStatus { - WAITING = "waiting", + WAITING = "Waiting", CUTTING = "Cutting", - EXTRACTIONING = "extractioning", - REVIEWING = "reviewing", - COMPLETED = "completed" + EXTRACTIONING = "Extractioning", + EVALUATIONING = "Evaluationing", + FAILED = "Failed", + PROCESSED = "Processed" } // 文档类型接口 diff --git a/app/routes/documents._index.tsx b/app/routes/documents._index.tsx index 19ff77f..01ae7e8 100644 --- a/app/routes/documents._index.tsx +++ b/app/routes/documents._index.tsx @@ -33,8 +33,9 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { const url = new URL(request.url); const search = url.searchParams.get("search") || ""; const documentType = url.searchParams.get("documentType") || ""; - const status = url.searchParams.get("status") || ""; + const auditStatus = url.searchParams.get("auditStatus") || ""; const documentNumber = url.searchParams.get("documentNumber") || ""; + const fileStatus = url.searchParams.get("fileStatus") || ""; const dateFrom = url.searchParams.get("dateFrom") || ""; const dateTo = url.searchParams.get("dateTo") || ""; const page = parseInt(url.searchParams.get("page") || "1", 10); @@ -45,7 +46,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { name: search || undefined, documentNumber: documentNumber || undefined, documentType: documentType || undefined, - status: status || undefined, + auditStatus: auditStatus || undefined, + fileStatus: fileStatus || undefined, dateFrom: dateFrom || undefined, dateTo: dateTo || undefined, page, @@ -58,8 +60,8 @@ export const loader = async ({ request }: LoaderFunctionArgs) => { throw new Error(documentsResponse.error); } - // 获取文档类型列表,用于筛选条件 - const typesResponse = await getDocumentTypes(); + // 获取文档类型列表,用于筛选条件,设置较大的pageSize确保获取所有数据 + const typesResponse = await getDocumentTypes({ pageSize: 500 }); const documentTypes = typesResponse.data?.types || []; const documentTypeOptions = documentTypes.map(type => ({ value: type.id, @@ -112,22 +114,33 @@ export const action = async ({ request }: ActionFunctionArgs) => { return Response.json({ success: false, message: "未知操作" }, { status: 400 }); }; -// 文档状态选项 -const documentStatusOptions = [ - { value: "waiting", label: "待审核" }, - { value: "processing", label: "审核中" }, - { value: "pass", label: "通过" }, - { value: "warning", label: "警告" }, - { value: "fail", label: "不通过" }, +// 审核状态筛选选项 +const auditStatusOptions = [ + // { value: "", label: "全部" }, + { value: "-1", label: "不通过" }, + { value: "0", label: "待审核" }, + { value: "1", label: "通过" }, + { value: "2", label: "警告" }, + { value: "3", label: "审核中" }, ]; // 文件处理状态选项 const fileProcessingStatusOptions = [ - { value: "Waiting", label: "上传中", icon: "ri-loader-line" }, - { value: "Cutting", label: "切分中", icon: "ri-loader-line" }, - { value: "Extractioning", label: "抽取中", icon: "ri-loader-line" }, - { value: "Evaluationing", label: "评查中", icon: "ri-loader-line" }, - { value: "Processed", label: "已完成", icon: "ri-check-line" }, + { value: "Waiting", label: "上传中", icon: "ri-loader-line", color: "blue" }, + { value: "Cutting", label: "切分中", icon: "ri-loader-line", color: "purple" }, + { value: "Extractioning", label: "抽取中", icon: "ri-loader-line", color: "cyan" }, + { value: "Evaluationing", label: "评查中", icon: "ri-loader-line", color: "teal" }, + { value: "Processed", label: "已完成", icon: "ri-check-line", color: "green" }, +]; + +// 文件状态筛选选项 +const fileStatusOptions = [ + // { value: "", label: "全部" }, + { value: "Waiting", label: "上传中" }, + { value: "Cutting", label: "切分中" }, + { value: "Extractioning", label: "抽取中" }, + { value: "Evaluationing", label: "评查中" }, + { value: "Processed", label: "已完成" }, ]; // 审核状态选项及样式 @@ -171,8 +184,9 @@ export default function DocumentsIndex() { // 从URL获取当前筛选条件 const search = searchParams.get("search") || ""; const documentType = searchParams.get("documentType") || ""; - const status = searchParams.get("status") || ""; + const auditStatus = searchParams.get("auditStatus") || ""; const documentNumber = searchParams.get("documentNumber") || ""; + const fileStatus = searchParams.get("fileStatus") || ""; const dateFrom = searchParams.get("dateFrom") || ""; const dateTo = searchParams.get("dateTo") || ""; const currentPage = parseInt(searchParams.get("page") || "1", 10); @@ -234,9 +248,9 @@ export default function DocumentsIndex() { const handleStatusChange = (e: React.ChangeEvent) => { const params = new URLSearchParams(searchParams); if (e.target.value) { - params.set("status", e.target.value); + params.set("auditStatus", e.target.value); } else { - params.delete("status"); + params.delete("auditStatus"); } params.set("page", "1"); // 重置页码 setSearchParams(params); @@ -332,7 +346,11 @@ export default function DocumentsIndex() { }; // 删除文档 - const handleDelete = (id: string, name: string) => { + const handleDelete = (id: string, name: string, fileStatus: string) => { + if (fileStatus !== 'Processed') { + alert('文档正在处理中,不能删除'); + return; + } if (window.confirm(`确认删除文档 "${name}"?`)) { // 使用fetcher提交表单 const formData = new FormData(); @@ -353,6 +371,16 @@ export default function DocumentsIndex() { return; } + // 检查是否有正在处理中的文件 + const hasProcessingFiles = documents.some((doc: DocumentUI) => + selectedRowKeys.includes(doc.id.toString()) && doc.fileStatus !== 'Processed' + ); + + if (hasProcessingFiles) { + alert('存在服务器未处理完成的文件,请重新选择需要删除的文件'); + return; + } + if (window.confirm(`确认删除选中的 ${selectedRowKeys.length} 个文档?`)) { // 使用fetcher提交表单 const formData = new FormData(); @@ -370,6 +398,18 @@ export default function DocumentsIndex() { } }; + // 处理文件状态变更 + const handleFileStatusChange = (e: React.ChangeEvent) => { + const params = new URLSearchParams(searchParams); + if (e.target.value) { + params.set("fileStatus", e.target.value); + } else { + params.delete("fileStatus"); + } + params.set("page", "1"); // 重置页码 + setSearchParams(params); + }; + // 表格列定义 const columns = [ { @@ -393,6 +433,7 @@ export default function DocumentsIndex() { { title: "文档名称", key: "name", + width:'20%', render: (_: unknown, record: DocumentUI) => (
-
- {record.name} +
+ {record.name}
( {record.documentNumber} ) @@ -431,18 +473,22 @@ export default function DocumentsIndex() { { title: "文件大小", key: "size", - width: "100px", + width: "10%", render: (_: unknown, record: DocumentUI) => formatFileSize(record.size) }, { title: "文件状态", key: "fileStatus", + width:'10%', render: (_: unknown, record: DocumentUI) => { - const status = fileProcessingStatusOptions.find(s => s.value === record.fileStatus) || - fileProcessingStatusOptions[0]; - const isSpinning = record.fileStatus !== "Processed"; + // 处理fileStatus为null或undefined的情况 + // console.log('fileStatus',record.fileStatus) + const fileStatus = record.fileStatus || "Processed"; + const status = fileProcessingStatusOptions.find(s => s.value === fileStatus) || + fileProcessingStatusOptions[0]; + const isSpinning = fileStatus !== "Processed"; return ( -
+
{status.label}
@@ -452,8 +498,11 @@ export default function DocumentsIndex() { { title: "审核状态", key: "auditStatus", + width:"10%", render: (_: unknown, record: DocumentUI) => { - const statusKey = record.auditStatus.toString(); + // 处理auditStatus为null或undefined的情况,默认为0(待审核) + const auditStatus = record.auditStatus != null ? record.auditStatus : 0; + const statusKey = auditStatus.toString(); const statusInfo = auditStatusMapping[statusKey] || auditStatusMapping["0"]; return (
@@ -466,7 +515,7 @@ export default function DocumentsIndex() { { title: "问题数量", key: "issues", - width:"60px", + width:"10%", render: (_: unknown, record: DocumentUI) => ( record.issues === null ? "-" : record.issues ) @@ -474,15 +523,16 @@ export default function DocumentsIndex() { { title: "上传时间", key: "uploadTime", + width:"10%", render: (_: unknown, record: DocumentUI) => record.uploadTime }, { title: "操作", key: "actions", - width: "280px", + width: "20%", render: (_: unknown, record: DocumentUI) => (
- {record.auditStatus === 0 ? ( + {(record.auditStatus === 0 || record.auditStatus == null) ? ( handleDelete(record.id.toString(), record.name)} + onClick={() => handleDelete(record.id.toString(), record.name, record.fileStatus)} > 删除 @@ -578,51 +628,56 @@ export default function DocumentsIndex() { } noActionDivider={true} > - +
+ - - - - - - - handleDateChange('dateFrom', value)} - onEndDateChange={(value) => handleDateChange('dateTo', value)} - className="flex-1" - simple={true} - /> + + + + + + + + + handleDateChange('dateFrom', value)} + onEndDateChange={(value) => handleDateChange('dateTo', value)} + simple={true} + /> +
diff --git a/app/routes/documents.edit.tsx b/app/routes/documents.edit.tsx index 29b5db0..2324fbc 100644 --- a/app/routes/documents.edit.tsx +++ b/app/routes/documents.edit.tsx @@ -71,7 +71,7 @@ export async function loader({ request }: LoaderFunctionArgs) { // 并行获取文档详情和文档类型列表 const [documentResponse, documentTypesResponse] = await Promise.all([ getDocument(id), - getDocumentTypes() + getDocumentTypes({ pageSize: 500 }) ]); if (documentResponse.error) { @@ -198,6 +198,8 @@ export default function DocumentEdit() { // 在新窗口打开文档预览 const openPreview = () => { // 假设有一个预览URL的格式,比如 /preview?path=xxx + console.log('documentstest', document); + const previewUrl = `/preview?path=${encodeURIComponent(document.path)}&name=${encodeURIComponent(document.name)}`; window.open(previewUrl, '_blank'); }; @@ -268,11 +270,12 @@ export default function DocumentEdit() {
-