From 6dc9b4e4689c2dd03438105d166476750ac16a6b Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Thu, 20 Nov 2025 15:26:11 +0800 Subject: [PATCH] =?UTF-8?q?feat:=201.=20=E5=AE=8C=E5=96=84=E6=96=87?= =?UTF-8?q?=E6=A1=A3=E5=88=97=E8=A1=A8=E7=9A=84=E6=98=BE=E7=A4=BA=E6=95=88?= =?UTF-8?q?=E6=9E=9C=EF=BC=8C=E6=95=B0=E6=8D=AE=E5=AF=B9=E6=8E=A5=E5=90=8E?= =?UTF-8?q?=E7=AB=AF=E6=8E=A5=E5=8F=A3=E8=BF=94=E5=9B=9E=E3=80=82=202.=20?= =?UTF-8?q?=E5=AF=B9=E8=AF=84=E6=9F=A5=E7=82=B9=E5=88=86=E7=BB=84=E5=92=8C?= =?UTF-8?q?=E6=96=87=E6=A1=A3=E7=B1=BB=E5=9E=8B=E7=9A=84=E7=BC=96=E8=BE=91?= =?UTF-8?q?=E5=88=A0=E9=99=A4=E6=96=B0=E5=A2=9E=E6=93=8D=E4=BD=9C=E8=BF=9B?= =?UTF-8?q?=E8=A1=8C=E9=99=90=E5=88=B6=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/document-types/document-types.ts | 61 +--- app/api/evaluation_points/rule-groups.ts | 57 ++-- app/api/files/documents.ts | 388 +++++++++++------------ app/components/layout/Sidebar.tsx | 30 +- app/routes/document-types._index.tsx | 43 ++- app/routes/document-types.new.tsx | 119 ++++--- app/routes/documents.list.tsx | 364 ++++++++++----------- app/routes/rule-groups._index.tsx | 18 +- app/routes/rule-groups.new.tsx | 35 +- app/routes/rules.list.tsx | 8 +- app/styles/main.css | 1 + 11 files changed, 549 insertions(+), 575 deletions(-) diff --git a/app/api/document-types/document-types.ts b/app/api/document-types/document-types.ts index dd63bbe..067ea6a 100644 --- a/app/api/document-types/document-types.ts +++ b/app/api/document-types/document-types.ts @@ -64,7 +64,7 @@ export interface DocumentTypeSearchParams { groupId?: string; page?: number; pageSize?: number; - reviewType?: string; + documentTypeIds?: number[]; // 文档类型 ID 数组 } @@ -254,18 +254,15 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = // 如果groupId存在,则将groupId作为子级评查点分组ID filter['evaluation_point_groups_ids'] = `cs.[${searchParams.groupId}]`; } - - // 根据 reviewType 添加过滤条件 - if (searchParams.reviewType) { - if (searchParams.reviewType === 'contract') { - // 如果是合同类型,只显示id=1的文档类型 - filter['id'] = 'eq.1'; - } else if (searchParams.reviewType === 'record') { - // 如果是卷宗类型,只显示id=2或id=3的文档类型 - filter['id'] = 'in.(2,3,155)'; - } + + // 🔑 根据 documentTypeIds 添加过滤条件 + if (searchParams.documentTypeIds && searchParams.documentTypeIds.length > 0) { + // 直接使用文档类型 ID 数组进行过滤 + const typeIdsStr = searchParams.documentTypeIds.join(','); + filter['id'] = `in.(${typeIdsStr})`; + // console.log('📋 [getDocumentTypes] 根据文档类型 IDs 过滤:', typeIdsStr); } - + params.filter = filter; // console.log('获取文档类型列表,参数:', params); @@ -280,44 +277,14 @@ export async function getDocumentTypes(searchParams: DocumentTypeSearchParams = const documentTypes = extractedData || []; // console.log('提取的文档类型数据:', documentTypes); - - // 为每个文档类型获取关联的分组 - const typesWithGroups = await Promise.all(documentTypes.map(async (type) => { - // 获取文档类型关联的分组IDs - let groupIds: number[] = []; - - try { - // 尝试解析evaluation_point_groups_ids - if (typeof type.evaluation_point_groups_ids === 'string') { - // 如果是JSON字符串,解析它 - groupIds = JSON.parse(type.evaluation_point_groups_ids as unknown as string); - } else if (Array.isArray(type.evaluation_point_groups_ids)) { - // 如果已经是数组,直接使用 - groupIds = type.evaluation_point_groups_ids; - } else if (type.evaluation_point_groups_ids) { - // 其他情况,尝试将其转换为数组 - groupIds = [type.evaluation_point_groups_ids as unknown as number]; - } - } catch (error) { - console.error('解析分组ID失败:', error, '原始值:', type.evaluation_point_groups_ids); - groupIds = []; - } - - // console.log(`文档类型 ${type.id} 的分组IDs:`, groupIds); - // 获取这些ID对应的分组信息 - const groupsResponse = await getEvaluationPointGroupsByIds(groupIds, frontendJWT); - - // 返回包含分组信息的文档类型 - return { - ...type, - groups: groupsResponse.data || [] - }; + // 🔧 优化:移除评查点分组查询(文档列表UI不需要此数据) + // 直接转换为UI类型,不查询关联的分组信息 + const uiTypes = documentTypes.map(type => ({ + ...convertToUIDocumentType(type), + groups: [] // 保持接口兼容性,但不填充数据 })); - // 转换为UI类型 - const uiTypes = typesWithGroups.map(convertToUIDocumentType); - // 获取总数 let totalCount = 0; const responseWithHeaders = response as { diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts index 2cef5bf..157d392 100644 --- a/app/api/evaluation_points/rule-groups.ts +++ b/app/api/evaluation_points/rule-groups.ts @@ -24,7 +24,6 @@ export interface ApiRuleGroup { code?: string; description?: string; is_enabled: boolean; - m_type?: number; // 文档类型:0=合同,1=其他 created_at?: string; updated_at?: string; } @@ -36,7 +35,6 @@ export interface RuleGroupCreateUpdateDto { pid: string | null; // 父分组ID,如果是一级分组则为null或'0' description?: string; is_enabled: boolean; - reviewType?: string; // 审核类型:'contract'=合同,其他为卷宗 } // 用于替换代码中的 any 类型 @@ -289,8 +287,8 @@ export async function getAllRuleGroups(token?: string): Promise<{data: RuleGroup })); } - // 3. 构建树形结构 - const parentGroups = allGroups.filter(group => group.pid === '0'); + // 3. 构建树形结构(pid为NULL表示顶级分组) + const parentGroups = allGroups.filter(group => !group.pid || group.pid === '0' || group.pid === null); // 4. 为每个父分组添加子分组 for (const parent of parentGroups) { @@ -396,8 +394,8 @@ export async function getRuleGroup(id: string, token?: string): Promise<{data: R return { error: '未找到指定分组', status: 404 }; } - // 如果是父分组,获取评查点数量 - if (group.pid === '0') { + // 如果是父分组(顶级分组,pid为NULL或'0'),获取评查点数量 + if (!group.pid || group.pid === '0' || group.pid === null) { const ruleCountParams: PostgrestParams = { select: 'id', filter: { @@ -436,30 +434,29 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token return { error: '分组名称和编码不能为空', status: 400 }; } - // 确保 pid 是合法值 - let pidValue: number; + // 🆕 确保 pid 是合法值(NULL表示顶级分组) + let pidValue: number | null; try { - pidValue = groupData.pid ? Number(groupData.pid) : 0; - if (isNaN(pidValue)) { - return { error: '父分组ID必须是有效的数字', status: 400 }; + if (!groupData.pid || groupData.pid === '0') { + pidValue = null; // 顶级分组 + } else { + pidValue = Number(groupData.pid); + if (isNaN(pidValue)) { + return { error: '父分组ID必须是有效的数字', status: 400 }; + } } } catch (error) { console.error('父分组ID转换失败:', error); return { error: '父分组ID格式错误', status: 400 }; } - // 根据 reviewType 确定 m_type 的值 - // contract -> 0, 其他 -> 1 - const mType = groupData.reviewType === 'contract' ? 0 : 1; - // 构建API请求数据 - 确保字段类型符合数据库要求 const apiGroup: ApiRuleGroup = { pid: pidValue, name: groupData.name.trim(), code: groupData.code.trim(), description: groupData.description || '', - is_enabled: groupData.is_enabled, - m_type: mType + is_enabled: groupData.is_enabled }; // console.log('创建评查点分组请求数据:', JSON.stringify(apiGroup, null, 2)); @@ -489,7 +486,7 @@ export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto, token // 构建返回对象 const createdGroup: RuleGroup = { id: apiResponse.id?.toString() || '', - pid: apiResponse.pid?.toString() || '0', // 兼容没有 pid 的情况 + pid: apiResponse.pid?.toString() || '', // 🆕 NULL 转换为空字符串(表示顶级分组) name: apiResponse.name || '', code: apiResponse.code?.toString() || '', // 处理可能的数字类型 description: apiResponse.description, @@ -521,24 +518,24 @@ export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto return { error: '分组名称和编码不能为空', status: 400 }; } - // 根据 reviewType 确定 m_type 的值 - // contract -> 0, 其他 -> 1 - const mType = data.reviewType === 'contract' ? 0 : 1; - // 构建符合数据库结构的对象 const apiGroup: Partial = { name: data.name.trim(), code: data.code.trim(), description: data.description || '', - is_enabled: data.is_enabled, - m_type: mType // 使用 m_type 而不是 reviewType + is_enabled: data.is_enabled }; - // 如果需要更新父分组,添加 pid + // 🆕 如果需要更新父分组,添加 pid(NULL表示顶级分组) if (data.pid !== undefined) { - const pidValue = data.pid ? Number(data.pid) : 0; - if (isNaN(pidValue)) { - return { error: '父分组ID必须是有效的数字', status: 400 }; + let pidValue: number | null; + if (!data.pid || data.pid === '0') { + pidValue = null; // 顶级分组 + } else { + pidValue = Number(data.pid); + if (isNaN(pidValue)) { + return { error: '父分组ID必须是有效的数字', status: 400 }; + } } apiGroup.pid = pidValue; } @@ -590,8 +587,8 @@ export async function deleteRuleGroup(id: string, token?: string): Promise<{succ return { success: false, error: '未找到指定分组' }; } - // 2. 如果是一级分组,需要先删除所有子分组 - if (group.pid === '0') { + // 2. 如果是一级分组(顶级分组,pid为NULL或'0'),需要先删除所有子分组 + if (!group.pid || group.pid === '0' || group.pid === null) { // 获取所有子分组 const childGroupsResponse = await getChildGroups(id, token); if (childGroupsResponse.error) { diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 70cb7df..24d76ff 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -1,6 +1,7 @@ import { postgrestGet, postgrestDelete, postgrestPut, postgrestPost } from '../postgrest-client'; import { getDocumentTypes } from '../document-types/document-types'; import { formatDate } from '../../utils'; +import { API_BASE_URL } from '~/config/api-config'; /** * 从不同格式的 API 响应中提取数据 @@ -23,25 +24,6 @@ function extractApiData(responseData: unknown): T | null { } -/** - * 查询参数 - */ -export interface DocumentSearchParams { - name?: string; - documentNumber?: string; - documentType?: string; - status?: string; - auditStatus?: string; - fileStatus?: string; - dateFrom?: string; - dateTo?: string; - page?: number; - pageSize?: number; - reviewType?: string; - userId?: string; // 添加用户ID筛选 - token?: string; // JWT token -} - /** * 数据库文档结构 */ @@ -93,9 +75,22 @@ export interface DocumentUI { updatedAt?: string; pageCount?: number; ocrResult?: unknown; + // 结果统计字段 + pass_count: number | null; // 通过数量 + warning_count: number | null; // 警告数量 + error_count: number | null; // 错误数量 + manual_count: number | null; // 人工审核数量 + // 消息详情字段 + warning_messages?: string[]; // 警告消息列表 + error_messages?: string[]; // 错误消息列表 + manual_messages?: string[]; // 人工审核消息列表 // 版本管理相关字段 historyCount?: number; // 历史版本数量(不含当前版本) previousIssues?: number | null; // 上一个版本的问题数量 + previous_pass_count?: number | null; // 上一版本通过数量 + previous_warning_count?: number | null; // 上一版本警告数量 + previous_error_count?: number | null; // 上一版本错误数量 + previous_manual_count?: number | null; // 上一版本人工数量 isExpanded?: boolean; // 是否展开历史版本(前端状态) historyVersions?: DocumentVersionUI[]; // 历史版本列表 } @@ -123,6 +118,15 @@ export interface DocumentVersionUI { pageCount?: number; ocrResult?: unknown; versionNumber?: number; // 版本号(v2, v3, v4...) + // 结果统计字段 + pass_count: number | null; // 通过数量 + warning_count: number | null; // 警告数量 + error_count: number | null; // 错误数量 + manual_count: number | null; // 人工审核数量 + previous_pass_count?: number | null; // 上一版本通过数量 + previous_warning_count?: number | null; // 上一版本警告数量 + previous_error_count?: number | null; // 上一版本错误数量 + previous_manual_count?: number | null; // 上一版本人工数量 } /** @@ -227,114 +231,6 @@ interface DocumentFromSQL { }; } -/** - * 获取文档列表 - * @param searchParams 搜索参数 - * @returns 文档列表和总数 - */ -export async function getDocuments(searchParams: DocumentSearchParams = {}): Promise<{ - data?: { documents: DocumentUI[], total: number }; - error?: string; - status?: number; -}> { - try { - // 准备RPC调用参数 - const { - page = 1, - pageSize = 10, - name, - documentNumber, - documentType, - auditStatus, - fileStatus, - dateFrom, - dateTo, - reviewType, - userId, - token - } = searchParams; - - let documentTypes: number[] | undefined; - if (documentType) { - documentTypes = [parseInt(documentType, 10)]; - } else if (reviewType) { - if (reviewType === 'contract') { - documentTypes = [1]; - } else if (reviewType === 'record') { - documentTypes = [2, 3, 155]; - } - } - - // 确保userId必须存在,如果不存在则抛出错误 - if (!userId) { - return { error: '用户身份验证失败,无法获取文档列表', status: 401 }; - } - - const rpcParams = { - search_name: name, - search_document_number: documentNumber, - search_document_types: documentTypes, - search_audit_status: auditStatus !== undefined ? parseInt(auditStatus, 10) : undefined, - search_file_status: fileStatus, - search_date_from: dateFrom, - search_date_to: dateTo, - search_user_id: parseInt(userId, 10), // 强制要求传递用户ID - }; - - // 并行执行获取数据和获取总数的请求 - const [documentsResponse, countResponse] = await Promise.all([ - postgrestPost('rpc/get_documents_with_filters', { ...rpcParams, page, page_size: pageSize }, token), - postgrestPost('rpc/count_documents_with_filters', rpcParams, token) - ]); - - // 处理获取文档列表的错误 - if (documentsResponse.error || !documentsResponse.data) { - return { error: documentsResponse.error || '获取文档数据失败', status: documentsResponse.status || 500 }; - } - - // 处理获取总数的错误 - if (countResponse.error || typeof countResponse.data !== 'number') { - // 如果计数失败,可以继续返回数据,但总数可能不准 - console.error('获取文档总数失败:', countResponse.error); - } - // console.log('countResponse.data', countResponse.data); - - const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0; - - // 将SQL返回的数据转换为UI格式 - const documents: DocumentUI[] = documentsResponse.data.map((doc: DocumentFromSQL) => ({ - id: doc.id, - name: doc.name, - documentNumber: doc.document_number, - type: doc.type_id.toString(), - typeName: doc.type_name || '未知类型', - size: doc.file_size, - auditStatus: doc.audit_status ?? 0, - fileStatus: doc.status || '', - issues: doc.false_count ?? null, - uploadTime: formatDate(doc.updated_at), - fileType: getFileExtension(doc.name), - path: doc.path, - isTest: doc.is_test_document, - updatedAt: formatDate(doc.updated_at), - pageCount: doc.ocr_result?.__meta?.page_count || 0, - ocrResult: doc.ocr_result - })); - - return { - data: { - documents, - total: totalCount - } - }; - } catch (error) { - console.error('获取文档列表失败:', error); - return { - error: error instanceof Error ? error.message : '获取文档列表失败', - status: 500 - }; - } -} /** * 删除文档 @@ -597,13 +493,25 @@ export async function updateDocument(id: string, document: Partial & } } + /** - * 获取文档列表(带版本信息)- 使用 RPC 函数 + * 获取文档列表(使用新的后端API) * @param searchParams 搜索参数 * @returns 文档列表和总数 */ -export async function getDocumentsWithVersionInfo(searchParams: DocumentSearchParams = {}): Promise<{ - data?: { documents: DocumentUI[], total: number }; +export async function getDocumentsListFromAPI(searchParams: { + page?: number; + pageSize?: number; + name?: string; + documentNumber?: string; + documentTypeIds?: number[]; // 文档类型ID数组 + auditStatus?: string; + fileStatus?: string; + dateFrom?: string; + dateTo?: string; + token: string; // JWT token (必填) +}): Promise<{ + data?: { documents: DocumentUI[], total: number, page: number, totalPages: number }; error?: string; status?: number; }> { @@ -613,104 +521,168 @@ export async function getDocumentsWithVersionInfo(searchParams: DocumentSearchPa pageSize = 10, name, documentNumber, - documentType, + documentTypeIds, auditStatus, fileStatus, dateFrom, dateTo, - reviewType, - userId, token } = searchParams; - // 确保userId必须存在 - if (!userId) { - return { error: '用户身份验证失败,无法获取文档列表', status: 401 }; - } - - // 处理文档类型 - let documentTypes: number[] | undefined; - if (documentType) { - documentTypes = [parseInt(documentType, 10)]; - } else if (reviewType) { - if (reviewType === 'contract') { - documentTypes = [1]; - } else if (reviewType === 'record') { - documentTypes = [2, 3, 155]; - } - } - - // 准备RPC调用参数 - const rpcParams = { - p_user_id: parseInt(userId, 10), - p_page: page, - p_page_size: pageSize, - p_search_name: name || null, - p_search_document_number: documentNumber || null, - p_search_document_types: documentTypes || null, - p_search_audit_status: auditStatus !== undefined ? parseInt(auditStatus, 10) : null, - p_search_file_status: fileStatus || null, - p_search_date_from: dateFrom || null, - p_search_date_to: dateTo || null + // 构建查询参数 + const params: Record = { + page, + page_size: pageSize }; - // 并行执行获取数据和获取总数的请求 - const [documentsResponse, countResponse] = await Promise.all([ - postgrestPost('rpc/documents_get_latest_documents_with_version_info', rpcParams, token), - postgrestPost('rpc/documents_count_latest_documents_with_filters', { - p_user_id: rpcParams.p_user_id, - p_search_name: rpcParams.p_search_name, - p_search_document_number: rpcParams.p_search_document_number, - p_search_document_types: rpcParams.p_search_document_types, - p_search_audit_status: rpcParams.p_search_audit_status, - p_search_file_status: rpcParams.p_search_file_status, - p_search_date_from: rpcParams.p_search_date_from, - p_search_date_to: rpcParams.p_search_date_to - }, token) - ]); + // 添加可选参数 + if (name) params.name = name; + if (documentNumber) params.document_number = documentNumber; + if (auditStatus) params.audit_status = parseInt(auditStatus, 10); + if (fileStatus) params.status = fileStatus; + if (dateFrom) params.start_time = dateFrom; + if (dateTo) params.end_time = dateTo; - // 处理获取文档列表的错误 - if (documentsResponse.error || !documentsResponse.data) { - return { error: documentsResponse.error || '获取文档数据失败', status: documentsResponse.status || 500 }; + // 处理文档类型ID数组 - 传递为数组或单个值 + if (documentTypeIds && documentTypeIds.length > 0) { + params.type_id = documentTypeIds; } - // 处理获取总数的错误 - if (countResponse.error || typeof countResponse.data !== 'number') { - console.error('获取文档总数失败:', countResponse.error); - } + console.log('📤 [getDocumentsListFromAPI] 请求参数:', params); - const totalCount = typeof countResponse.data === 'number' ? countResponse.data : 0; + // 调用后端API + const axios = await import('axios').then(m => m.default); + const response = await axios.get(`${API_BASE_URL}/admin/versions/documents-list`, { + params, + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + } + }); - // 将RPC返回的数据转换为UI格式 - const documents: DocumentUI[] = documentsResponse.data.map((doc: any) => ({ - id: doc.id, - name: doc.name, - documentNumber: doc.document_number, - type: doc.type_id.toString(), - typeName: doc.type_name || '未知类型', - size: doc.file_size, - auditStatus: doc.audit_status ?? 0, - fileStatus: doc.status || '', - issues: doc.false_count ?? null, - uploadTime: formatDate(doc.updated_at), - fileType: getFileExtension(doc.name), - path: doc.path, - isTest: doc.is_test_document, - updatedAt: formatDate(doc.updated_at), - pageCount: doc.ocr_result?.__meta?.page_count || 0, - ocrResult: doc.ocr_result, - historyCount: doc.history_count || 0, - previousIssues: doc.previous_issues - })); + const data = response.data; + const backendDocuments = data.documents || []; + const totalCount = data.total || 0; + const totalPages = data.total_pages || 0; + + console.log(`📥 [getDocumentsListFromAPI] 获取到 ${backendDocuments.length} 个文档,总数: ${totalCount}`); + + // 转换后端数据为前端 DocumentUI 格式 + const convertedDocuments: DocumentUI[] = backendDocuments.map((doc: any) => { + // 转换历史版本数据 + const historyVersions: DocumentVersionUI[] = (doc.history_versions || []).map((hv: any, index: number) => { + // 计算与下一个版本(更早的版本)的问题数量差异 + let issuesDiff: number | undefined; + let issuesDiffType: 'increase' | 'decrease' | 'same' | undefined; + + if (index < doc.history_versions.length - 1) { + const olderDoc = doc.history_versions[index + 1]; + if (hv.issue_count != null && olderDoc.issue_count != null) { + const diff = hv.issue_count - olderDoc.issue_count; + issuesDiff = Math.abs(diff); + if (diff > 0) { + issuesDiffType = 'increase'; + } else if (diff < 0) { + issuesDiffType = 'decrease'; + } else { + issuesDiffType = 'same'; + } + } + } + + // 获取前一个版本的统计数据(如果存在) + const prevVersion = index < doc.history_versions.length - 1 ? doc.history_versions[index + 1] : null; + + return { + id: hv.id, + name: hv.name || doc.name, + documentNumber: hv.document_number || doc.document_number || '', + type: hv.type_id?.toString() || doc.type_id?.toString() || '', + typeName: hv.type_name || doc.type_name || '未知类型', + size: hv.file_size || 0, + auditStatus: hv.audit_status ?? 0, + fileStatus: hv.status || 'Processed', + issues: hv.issue_count ?? null, + issuesDiff, + issuesDiffType, + uploadTime: formatDate(hv.created_at), + fileType: getFileExtension(hv.name || doc.name), + path: hv.path || '', + isTest: hv.is_test_document || false, + updatedAt: formatDate(hv.updated_at || hv.created_at), + pageCount: hv.ocr_result?.__meta?.page_count || 0, + ocrResult: hv.ocr_result, + versionNumber: hv.version_number, + // 结果统计字段 + pass_count: hv.pass_count ?? null, + warning_count: hv.warning_count ?? null, + error_count: hv.error_count ?? null, + manual_count: hv.manual_count ?? null, + // 前一版本统计(用于差异对比) + previous_pass_count: prevVersion?.pass_count ?? null, + previous_warning_count: prevVersion?.warning_count ?? null, + previous_error_count: prevVersion?.error_count ?? null, + previous_manual_count: prevVersion?.manual_count ?? null + }; + }); + + return { + id: doc.id, + name: doc.name, + documentNumber: doc.document_number || '', + type: doc.type_id?.toString() || '', + typeName: doc.type_name || '未知类型', + size: doc.file_size || 0, + auditStatus: doc.audit_status ?? 0, + fileStatus: doc.status || '', + issues: doc.issue_count ?? null, + uploadTime: formatDate(doc.upload_time || doc.created_at), + fileType: getFileExtension(doc.name), + path: doc.path || '', + isTest: doc.is_test_document || false, + updatedAt: formatDate(doc.updated_at || doc.created_at), + pageCount: doc.ocr_result?.__meta?.page_count || 0, + ocrResult: doc.ocr_result, + // 结果统计字段 + pass_count: doc.pass_count ?? null, + warning_count: doc.warning_count ?? null, + error_count: doc.error_count ?? null, + manual_count: doc.manual_count ?? null, + // 消息详情字段 + warning_messages: doc.warning_messages || [], + error_messages: doc.error_messages || [], + manual_messages: doc.manual_messages || [], + // 版本管理字段 + historyCount: (doc.total_versions || 1) - 1, // 总版本数 - 1 = 历史版本数 + previousIssues: doc.history_versions?.[0]?.issue_count ?? null, + previous_pass_count: doc.history_versions?.[0]?.pass_count ?? null, + previous_warning_count: doc.history_versions?.[0]?.warning_count ?? null, + previous_error_count: doc.history_versions?.[0]?.error_count ?? null, + previous_manual_count: doc.history_versions?.[0]?.manual_count ?? null, + historyVersions: historyVersions.length > 0 ? historyVersions : undefined + }; + }); return { data: { - documents, - total: totalCount + documents: convertedDocuments, + total: totalCount, + page: data.page || page, + totalPages } }; } catch (error) { - console.error('获取文档列表失败:', error); + console.error('❌ [getDocumentsListFromAPI] 获取文档列表失败:', error); + + // 处理axios错误 + if (error && typeof error === 'object' && 'response' in error) { + const axiosError = error as { response?: { data?: any; status?: number; statusText?: string } }; + return { + error: axiosError.response?.data?.message || axiosError.response?.statusText || '获取文档列表失败', + status: axiosError.response?.status || 500 + }; + } + return { error: error instanceof Error ? error.message : '获取文档列表失败', status: 500 diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 7ed4553..d7d87d4 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -17,6 +17,7 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid const [isLoadingRoutes, setIsLoadingRoutes] = useState(true); // 路由加载状态 const [isMobile, setIsMobile] = useState(false); // 移动端检测 const [selectedModuleName, setSelectedModuleName] = useState(''); // 当前选中的模块名称 + const [selectedModulePicPath, setSelectedModulePicPath] = useState(''); // 当前选中的模块图片路径 const navigate = useNavigate(); // 移动端检测 @@ -89,17 +90,24 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid fetchUserRoutes(); }, [userRole, frontendJWT, navigate]); - // 从 sessionStorage 读取当前选中的模块名称 + // 从 sessionStorage 读取当前选中的模块名称和图片路径 useEffect(() => { if (typeof window !== 'undefined') { try { const moduleName = sessionStorage.getItem('selectedModuleName'); + const modulePicPath = sessionStorage.getItem('selectedModulePicPath'); + if (moduleName) { setSelectedModuleName(moduleName); console.log('📌 [Sidebar] 当前选中模块:', moduleName); } + + if (modulePicPath) { + setSelectedModulePicPath(modulePicPath); + console.log('🖼️ [Sidebar] 模块图片路径:', modulePicPath); + } } catch (error) { - console.error('❌ [Sidebar] 读取 selectedModuleName 失败:', error); + console.error('❌ [Sidebar] 读取 sessionStorage 失败:', error); } } }, [location.pathname]); // 路由变化时重新读取 @@ -260,6 +268,24 @@ export function Sidebar({ onToggle, collapsed, userRole, frontendJWT = '' }: Sid + {/* 显示入口模块的名称 */} + {selectedModuleName && ( +
+
+ {selectedModulePicPath && ( + {selectedModuleName} + )} + {!collapsed && ( + {selectedModuleName} + )} +
+
+ )} +
{isLoadingRoutes ? ( // 加载中状态显示,保留菜单布局结构 diff --git a/app/routes/document-types._index.tsx b/app/routes/document-types._index.tsx index 386a81a..1fb862f 100644 --- a/app/routes/document-types._index.tsx +++ b/app/routes/document-types._index.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from "react"; -import { useSearchParams, useNavigate, useLoaderData } from "@remix-run/react"; +import { useSearchParams, useNavigate, useLoaderData, useRouteLoaderData } from "@remix-run/react"; import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Table } from "~/components/ui/Table"; import { Card } from "~/components/ui/Card"; @@ -138,9 +138,14 @@ export default function DocumentTypesList() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [isDeleting, setIsDeleting] = useState(false); - + // 获取加载器数据 const { types, total, error, ruleTypes, frontendJWT } = useLoaderData(); + + // 获取用户角色并判断权限 + const rootData = useRouteLoaderData("root") as { userRole: string }; + const userRole = rootData?.userRole || 'common'; + const hasEditPermission = userRole.toLowerCase().includes('provin'); // 状态管理 const [ruleGroups, setRuleGroups] = useState([]); @@ -369,15 +374,17 @@ export default function DocumentTypesList() { className="operation-btn text-primary" onClick={() => handleEdit(record.id)} > - 编辑 - - + {hasEditPermission && ( + + )} ) } @@ -395,13 +402,15 @@ export default function DocumentTypesList() {

文档类型管理

- + {hasEditPermission && ( + + )}
diff --git a/app/routes/document-types.new.tsx b/app/routes/document-types.new.tsx index 8d376b5..00ed7ca 100644 --- a/app/routes/document-types.new.tsx +++ b/app/routes/document-types.new.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from "react"; -import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react"; +import { Form, useActionData, useLoaderData, useNavigate, useSearchParams, useRouteLoaderData } from "@remix-run/react"; import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; @@ -230,9 +230,15 @@ export default function DocumentTypeNew() { evaluationTemplates, summaryTemplates } = useLoaderData(); - + const actionData = useActionData(); - + + // 获取用户角色并判断权限 + const rootData = useRouteLoaderData("root") as { userRole: string }; + const userRole = rootData?.userRole || 'common'; + const hasEditPermission = userRole.toLowerCase().includes('provin'); + const isReadOnly = !hasEditPermission; + // 状态管理 const [formData, setFormData] = useState({ id: documentType?.id || "", @@ -446,23 +452,25 @@ export default function DocumentTypeNew() {
{/* 页面头部 */}
-

{isEditMode ? "编辑文档类型" : "新增文档类型"}

+

{isEditMode ? (isReadOnly ? "查看文档类型" : "编辑文档类型") : "新增文档类型"}

- - + {!isReadOnly && ( + + )}
@@ -486,15 +494,16 @@ export default function DocumentTypeNew() { - {touchedFields.name && formErrors?.name && (
{formErrors.name}
@@ -505,14 +514,15 @@ export default function DocumentTypeNew() { {/* 类型描述 */}
-
@@ -521,12 +531,13 @@ export default function DocumentTypeNew() { {/* llm抽取提示词模板 */}
- {vlmExtractionTemplates.map((template: PromptTemplateUI) => ( @@ -567,12 +579,13 @@ export default function DocumentTypeNew() { {/* 评查提示词模板 */}
- {summaryTemplates.map((template: PromptTemplateUI) => ( @@ -625,7 +639,7 @@ export default function DocumentTypeNew() { {ruleGroups.map((group: RuleGroup) => ( {/* 父分组 */} -
- handleGroupCheckChange(group.id, e.target.checked)} - className="radio-input" + className="radio-input" + disabled={isReadOnly} />