diff --git a/app/api/document-types/document-types.ts b/app/api/document-types/document-types.ts index dcd95d8..b3b751a 100644 --- a/app/api/document-types/document-types.ts +++ b/app/api/document-types/document-types.ts @@ -1,659 +1,258 @@ import { apiRequest } from '../axios-client'; -import { formatDate } from '../../utils'; -import { getAllEvaluationPointGroups, type RuleGroup } from '../evaluation_points/rule-groups'; +import { API_BASE_URL } from '../../config/api-config'; +import axios from 'axios'; + +// ==================== 类型定义 ==================== -// 定义文档类型接口 export interface DocumentType { id: number; name: string; - code?: string | null; + code: string; description: string | null; - evaluation_point_groups_ids: number[]; // 评查点分组ID数组 - entry_module_id?: number | null; - entry_module?: { - id: number; - name: string; - } | null; - groups?: DocumentTypeGroup[]; - llm_extraction_template_id?: number | null; - vlm_extraction_template_id?: number | null; - created_at: string; - updated_at: string; + entryModuleId: number | null; + isEnabled: boolean; + ruleSetIds: number[]; + createdAt?: string; + updatedAt?: string; } -// 定义用于UI展示的文档类型接口 -export interface DocumentTypeUI { - id: number; - name: string; - code?: string | null; - description: string; - groups: DocumentTypeGroup[]; - entry_module?: { - id: number; - name: string; - } | null; - llm_extraction_template_id?: number | null; - vlm_extraction_template_id?: number | null; - created_at: string; - updated_at: string; -} - -// 文档类型创建接口 export interface DocumentTypeCreateDTO { + code: string; name: string; description?: string; - group_ids: number[]; // 改为 number[] 数组 - code: string | null; - entry_module_id?: number | null; - llm_extraction_template_id?: number | null; - vlm_extraction_template_id?: number | null; + entryModuleId?: number | null; + isEnabled?: boolean; + sortOrder?: number; + ruleSetIds?: number[]; } -// 文档类型更新接口 -export interface DocumentTypeUpdateDTO extends DocumentTypeCreateDTO { +export interface DocumentTypeUpdateDTO { + name?: string; + description?: string; + entryModuleId?: number | null; + isEnabled?: boolean; + sortOrder?: number; + ruleSetIds?: number[]; +} + +export interface RuleSetOption { id: number; + ruleType: string; + ruleName: string; + status: string; } -// 文档类型分组关系 -export interface DocumentTypeGroup { - id: string | number; +export interface EntryModuleOption { + id: number; name: string; - code?: string; } -// 搜索参数 export interface DocumentTypeSearchParams { name?: string; - group_id?: number; // 按评查点分组ID筛选 - entry_module_id?: number; // 按入口模块ID筛选 - ids?: number[]; // 按ID列表筛选 + entry_module_id?: number; + ids?: number[]; page?: number; pageSize?: number; } -// API 响应格式 interface ApiResponse { code: number; - msg: string; + message: string; data: T; } -// 列表响应数据 -interface ListResponseData { - total: number; - page: number; - page_size: number; - items: DocumentType[]; +// ==================== API 调用 ==================== + +function authHeaders(token?: string): Record { + const headers: Record = {}; + if (token) headers['Authorization'] = `Bearer ${token}`; + return headers; } -// 选项数据 -interface OptionsResponseData { - items: Array<{ id: number; name: string; code?: string }>; +function extractData(response: any): T | null { + return response?.data?.data ?? response?.data ?? null; } /** * 获取文档类型列表 - * @param searchParams 搜索参数 - * @param frontendJWT JWT token - * @returns 文档类型列表和总数 */ export async function getDocumentTypes( searchParams: DocumentTypeSearchParams = {}, - frontendJWT?: string -): Promise<{ - data?: { types: DocumentTypeUI[], total: number }; - error?: string; - status?: number; -}> { + token?: string +): Promise<{ data?: { types: DocumentType[]; total: number }; error?: string; status?: number }> { try { - const page = searchParams.page || 1; - const pageSize = searchParams.pageSize || 10; - - // 构建查询参数 - const params: Record = { - page, - page_size: pageSize, + const params: Record = { + page: searchParams.page || 1, + pageSize: searchParams.pageSize || 50, }; + if (searchParams.ids) params.ids = searchParams.ids.join(","); - if (searchParams.name) { - params.name = searchParams.name; - } + const response = await axios.get(`${API_BASE_URL}/api/document-types`, { + params, + headers: authHeaders(token), + }); - if (searchParams.group_id) { - params.group_id = searchParams.group_id; - } + const items = extractData(response) || []; + const types: DocumentType[] = items.map((item: any) => ({ + id: item.id, + name: item.name, + code: item.code, + description: item.description || null, + entryModuleId: item.entryModuleId || null, + isEnabled: item.isEnabled !== false, + ruleSetIds: item.ruleSetIds || [], + })); - if (searchParams.entry_module_id) { - params.entry_module_id = searchParams.entry_module_id; - } - - if (searchParams.ids) { - params.ids = searchParams.ids; - } - - const response = await apiRequest>( - '/api/v3/document-types', - { - method: 'GET', - headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined, - }, - params - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - const apiData = response.data?.data; - if (!apiData) { - return { error: '获取文档类型列表失败', status: 500 }; - } - - // 转换为UI类型 - const uiTypes = apiData.items.map(type => convertToUIDocumentType(type)); - - return { - data: { - types: uiTypes, - total: apiData.total - } - }; + return { data: { types, total: types.length } }; } catch (error) { - console.error('获取文档类型列表失败:', error); - return { - error: error instanceof Error ? error.message : '获取文档类型列表失败', - status: 500 - }; + console.error("获取文档类型列表失败:", error); + return { error: error instanceof Error ? error.message : "获取文档类型列表失败", status: 500 }; } } /** * 获取文档类型详情 - * @param id 文档类型ID - * @param frontendJWT JWT token - * @returns 文档类型详情 */ export async function getDocumentType( - id: string, - frontendJWT?: string -): Promise<{ - data?: DocumentTypeUI; - error?: string; - status?: number; -}> { + id: number, + token?: string +): Promise<{ data?: DocumentType; error?: string; status?: number }> { try { - if (!id) { - return { error: '文档类型ID不能为空', status: 400 }; - } + const response = await axios.get(`${API_BASE_URL}/api/document-types/${id}`, { + headers: authHeaders(token), + }); + const item = extractData(response); + if (!item) return { error: "文档类型不存在", status: 404 }; - const response = await apiRequest>( - `/api/v3/document-types/${id}`, - { - method: 'GET', - headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined, - } - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - const documentType = response.data?.data; - if (!documentType) { - return { error: '未找到文档类型', status: 404 }; - } - - return { data: convertToUIDocumentType(documentType) }; - } catch (error) { - console.error('获取文档类型详情失败:', error); return { - error: error instanceof Error ? error.message : '获取文档类型详情失败', - status: 500 + data: { + id: item.id, + name: item.name, + code: item.code, + description: item.description || null, + entryModuleId: item.entryModuleId || null, + isEnabled: item.isEnabled !== false, + ruleSetIds: item.ruleSetIds || [], + }, }; + } catch (error) { + return { error: error instanceof Error ? error.message : "获取文档类型失败", status: 500 }; } } /** * 创建文档类型 - * @param documentType 文档类型数据 - * @param frontendJWT JWT token - * @returns 创建结果 */ export async function createDocumentType( - documentType: DocumentTypeCreateDTO, - frontendJWT?: string -): Promise<{ - data?: DocumentTypeUI; - error?: string; - status?: number; -}> { + dto: DocumentTypeCreateDTO, + token?: string +): Promise<{ data?: DocumentType; error?: string; status?: number }> { try { - // 验证必填字段 - if (!documentType.name) { - return { error: '文档类型名称不能为空', status: 400 }; - } + const response = await axios.post(`${API_BASE_URL}/api/document-types`, dto, { + headers: { ...authHeaders(token), "Content-Type": "application/json" }, + }); + const item = extractData(response); + if (!item) return { error: "创建失败", status: 500 }; - if (!documentType.group_ids || documentType.group_ids.length === 0) { - return { error: '请至少选择一个关联的评查点分组', status: 400 }; - } - - // 构建请求数据 - const requestData = { - name: documentType.name.trim(), - description: documentType.description || '', - entry_module_id: documentType.entry_module_id || null, - code: documentType.code || null, - group_ids: documentType.group_ids, - llm_extraction_template_id: documentType.llm_extraction_template_id || null, - vlm_extraction_template_id: documentType.vlm_extraction_template_id || null, - }; - - const response = await apiRequest>( - '/api/v3/document-types', - { - method: 'POST', - headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined, - data: requestData, - } - ); - - if (response.error) { - console.error('创建文档类型API返回错误:', response.error, '状态码:', response.status); - return { error: response.error, status: response.status }; - } - - const newDocumentType = response.data?.data; - if (!newDocumentType) { - return { error: '创建文档类型失败: 无法获取新创建的数据', status: 500 }; - } - - return { data: convertToUIDocumentType(newDocumentType) }; - } catch (error) { - console.error('创建文档类型失败:', error); return { - error: error instanceof Error ? error.message : '创建文档类型失败', - status: 500 + data: { + id: item.id, name: item.name, code: item.code, + description: item.description || null, + entryModuleId: item.entryModuleId || null, + isEnabled: item.isEnabled !== false, + ruleSetIds: item.ruleSetIds || [], + }, }; + } catch (error) { + const msg = (error as any)?.response?.data?.message || (error instanceof Error ? error.message : "创建失败"); + return { error: msg, status: (error as any)?.response?.status || 500 }; } } /** * 更新文档类型 - * @param id 文档类型ID - * @param documentType 文档类型数据 - * @param frontendJWT JWT token - * @returns 更新结果 */ export async function updateDocumentType( - id: string, - documentType: DocumentTypeUpdateDTO, - frontendJWT?: string -): Promise<{ - data?: DocumentTypeUI; - error?: string; - status?: number; -}> { + id: number, + dto: DocumentTypeUpdateDTO, + token?: string +): Promise<{ data?: DocumentType; error?: string; status?: number }> { try { - // 验证必填字段 - if (!id) { - return { error: '文档类型ID不能为空', status: 400 }; - } + const response = await axios.put(`${API_BASE_URL}/api/document-types/${id}`, dto, { + headers: { ...authHeaders(token), "Content-Type": "application/json" }, + }); + const item = extractData(response); + if (!item) return { error: "更新失败", status: 500 }; - if (!documentType.name) { - return { error: '文档类型名称不能为空', status: 400 }; - } - - if (!documentType.group_ids || documentType.group_ids.length === 0) { - return { error: '请至少选择一个关联的评查点分组', status: 400 }; - } - - // 构建请求数据 - const requestData = { - name: documentType.name.trim(), - description: documentType.description || '', - entry_module_id: documentType.entry_module_id || null, - code: documentType.code || null, - group_ids: documentType.group_ids, - llm_extraction_template_id: documentType.llm_extraction_template_id || null, - vlm_extraction_template_id: documentType.vlm_extraction_template_id || null, - }; - - const response = await apiRequest>( - `/api/v3/document-types/${id}`, - { - method: 'PUT', - headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined, - data: requestData, - } - ); - - if (response.error) { - console.error('更新文档类型API返回错误:', response.error, '状态码:', response.status); - return { error: response.error, status: response.status }; - } - - const updatedDocumentType = response.data?.data; - if (!updatedDocumentType) { - return { error: '更新文档类型失败: 无法获取更新后的数据', status: 500 }; - } - - return { data: convertToUIDocumentType(updatedDocumentType) }; - } catch (error) { - console.error('更新文档类型失败:', error); return { - error: error instanceof Error ? error.message : '更新文档类型失败', - status: 500 + data: { + id: item.id, name: item.name, code: item.code, + description: item.description || null, + entryModuleId: item.entryModuleId || null, + isEnabled: item.isEnabled !== false, + ruleSetIds: item.ruleSetIds || [], + }, }; + } catch (error) { + const msg = (error as any)?.response?.data?.message || (error instanceof Error ? error.message : "更新失败"); + return { error: msg, status: (error as any)?.response?.status || 500 }; } } /** * 删除文档类型 - * @param id 文档类型ID - * @param frontendJWT JWT token - * @returns 删除结果 */ export async function deleteDocumentType( - id: string, - frontendJWT?: string -): Promise<{ - success?: boolean; - error?: string; - status?: number; -}> { + id: number, + token?: string +): Promise<{ success?: boolean; error?: string; status?: number }> { try { - if (!id) { - return { error: '文档类型ID不能为空', status: 400 }; - } - - const response = await apiRequest>( - `/api/v3/document-types/${id}`, - { - method: 'DELETE', - headers: frontendJWT ? { 'Authorization': `Bearer ${frontendJWT}` } : undefined, - } - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - + await axios.delete(`${API_BASE_URL}/api/document-types/${id}`, { + headers: authHeaders(token), + }); return { success: true }; } catch (error) { - console.error('删除文档类型失败:', error); - return { - error: error instanceof Error ? error.message : '删除文档类型失败', - status: 500 - }; + return { error: error instanceof Error ? error.message : "删除失败", status: 500 }; } } /** * 获取入口模块选项 - * @param token JWT token - * @returns 入口模块列表 */ export async function getEntryModules( token?: string -): Promise<{ - data?: Array<{ id: number; name: string }>; - error?: string; - status?: number; -}> { +): Promise<{ data?: EntryModuleOption[]; error?: string }> { try { - const response = await apiRequest>( - '/api/v3/document-types/options/entry-modules', - { - method: 'GET', - headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, - } - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - const items = response.data?.data?.items; - if (!items) { - return { data: [] }; - } - - return { data: items }; + const response = await axios.get(`${API_BASE_URL}/api/v3/entry-modules`, { + headers: authHeaders(token), + }); + const items = extractData(response) || []; + return { data: items.map((m: any) => ({ id: m.id, name: m.name })) }; } catch (error) { - console.error('获取入口模块失败:', error); - return { error: error instanceof Error ? error.message : '获取入口模块失败' }; + return { error: error instanceof Error ? error.message : "获取入口模块失败" }; } } /** - * 将扁平的分组列表转换为树形结构 - * @param flatList 扁平列表,每个元素包含 id 和 pid - * @returns 树形结构数组 + * 获取规则集选项 */ -function buildGroupTree(flatList: any[]): RuleGroup[] { - const map = new Map(); - const roots: RuleGroup[] = []; - - // 第一遍:创建所有节点的映射 - flatList.forEach(item => { - const node: RuleGroup = { - id: item.id.toString(), - pid: item.pid !== null && item.pid !== undefined ? item.pid.toString() : '0', - name: item.name || '', - code: item.code, - is_enabled: item.is_enabled !== undefined ? item.is_enabled : true, - ruleCount: item.ruleCount || item.rule_count || 0, - children: [], - createdAt: item.created_at || item.createdAt, - description: item.description - }; - map.set(item.id, node); - }); - - // 第二遍:建立父子关系 - flatList.forEach(item => { - const node = map.get(item.id); - if (!node) return; - - const pid = item.pid !== null && item.pid !== undefined ? item.pid : 0; - - // pid 为 0 或 null 的是根节点 - if (pid === 0 || pid === '0' || pid === null) { - roots.push(node); - } else { - // 找到父节点并添加到其 children 中 - const parent = map.get(pid); - if (parent) { - if (!parent.children) { - parent.children = []; - } - parent.children.push(node); - } else { - // 如果找不到父节点,作为根节点处理 - roots.push(node); - } - } - }); - - return roots; -} - -/** - * 获取所有评查点分组(使用 FastAPI v3 接口) - * @param token JWT token - * @returns 评查点分组列表(树形结构) - */ -export async function getAllEvaluationPointGroups_ForDocTypes( +export async function getRuleSets( token?: string -): Promise<{ - data?: RuleGroup[]; - error?: string; - status?: number; -}> { - // 调用原始方法获取扁平数据 - const result = await getAllEvaluationPointGroups(false, false, token); // 第二个参数改为 false,不自动构建树 - - if (result.error || !result.data) { - return result; - } - - // 构建树形结构 - const treeData = buildGroupTree(result.data); - - return { - data: treeData, - error: result.error - }; -} - -/** - * 获取一级评查点分组(pid=0 的分组) - * @param token JWT token - * @returns 一级评查点分组列表 - */ -export async function getRootEvaluationPointGroups_ForDocTypes( - token?: string -): Promise<{ - data?: RuleGroup[]; - error?: string; - status?: number; -}> { +): Promise<{ data?: RuleSetOption[]; error?: string }> { try { - // 导入 getEvaluationPointGroups 函数 - const { getEvaluationPointGroups } = await import('../evaluation_points/rule-groups'); - - // 获取一级分组(pid='0' 或 pid=null) - const result = await getEvaluationPointGroups( - { - pid: '0', - pageSize: 1000, // 设置较大的页面大小以获取所有一级分组 - }, - token - ); - - if (result.error) { - return { error: result.error, status: result.status }; - } - + const response = await axios.get(`${API_BASE_URL}/api/rule-sets`, { + headers: authHeaders(token), + }); + const items = extractData(response) || []; return { - data: result.data || [], - error: result.error + data: items.map((r: any) => ({ + id: r.id, + ruleType: r.ruleType, + ruleName: r.ruleName, + status: r.status, + })), }; } catch (error) { - console.error('获取一级评查点分组失败:', error); - return { error: error instanceof Error ? error.message : '获取一级评查点分组失败' }; + return { error: error instanceof Error ? error.message : "获取规则集失败" }; } } - -/** - * 获取父级评查点分组(pid=0的分组) - * @param token JWT token - * @returns 父级评查点分组列表 - */ -export async function getParentEvaluationPointGroups( - token?: string -): Promise<{ - data?: DocumentTypeGroup[]; - error?: string; - status?: number; -}> { - try { - const response = await apiRequest>( - '/api/v3/document-types/options/evaluation-point-groups', - { - method: 'GET', - headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, - }, - { root_only: true } - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - // console.log('文档类型返回的评查点分组父级数据', response.data) - const items = response.data?.data?.items; - if (!items) { - return { data: [] }; - } - - // 转换为DocumentTypeGroup格式 - const groups: DocumentTypeGroup[] = items.map(item => ({ - id: item.id, - name: item.name, - code: item.code - })); - - return { data: groups }; - } catch (error) { - console.error('获取父级评查点分组失败:', error); - return { error: error instanceof Error ? error.message : '获取父级评查点分组失败' }; - } -} - -/** - * 获取提示词模板选项 - * @param templateType 模板类型(LLM_Extraction, VLM_Extraction 等) - * @param token JWT token - * @returns 提示词模板列表 - */ -export async function getPromptTemplateOptions( - templateType?: string, - token?: string -): Promise<{ - data?: Array<{ id: number; template_name: string; template_code: string; template_type: string }>; - error?: string; - status?: number; -}> { - try { - const params: Record = {}; - if (templateType) { - params.template_type = templateType; - } - - const response = await apiRequest>( - '/api/v3/document-types/options/prompt-templates', - { - method: 'GET', - headers: token ? { 'Authorization': `Bearer ${token}` } : undefined, - }, - Object.keys(params).length > 0 ? params : undefined - ); - - if (response.error) { - return { error: response.error, status: response.status }; - } - - const items = response.data?.data?.items; - if (!items) { - return { data: [] }; - } - - return { data: items as any }; - } catch (error) { - console.error('获取提示词模板选项失败:', error); - return { error: error instanceof Error ? error.message : '获取提示词模板选项失败' }; - } -} - -/** - * 将API返回的文档类型转换为UI文档类型 - */ -function convertToUIDocumentType(type: DocumentType): DocumentTypeUI { - return { - id: type.id, - name: type.name, - code: type.code, - description: type.description || '', - groups: type.groups?.map(g => ({ - id: g.id, - name: g.name, - code: g.code - })) || [], - entry_module: type.entry_module || null, - llm_extraction_template_id: type.llm_extraction_template_id, - vlm_extraction_template_id: type.vlm_extraction_template_id, - created_at: formatDate(type.created_at), - updated_at: formatDate(type.updated_at), - }; -} diff --git a/app/api/files/documents.ts b/app/api/files/documents.ts index 467ec91..34c2ed8 100644 --- a/app/api/files/documents.ts +++ b/app/api/files/documents.ts @@ -2,7 +2,7 @@ import { postgrestGet, postgrestDelete, postgrestPut, postgrestPost } from '../p import { getDocumentTypes } from '../document-types/document-types'; import { formatDate } from '../../utils'; import { API_BASE_URL } from '~/config/api-config'; -import type { DocumentType } from './files-upload'; +import type { DocumentType } from '../document-types/document-types'; /** * 从不同格式的 API 响应中提取数据 diff --git a/app/routes/document-types._index.tsx b/app/routes/document-types._index.tsx index 8beee3a..83e21f7 100644 --- a/app/routes/document-types._index.tsx +++ b/app/routes/document-types._index.tsx @@ -1,502 +1,152 @@ -import { useState, useEffect, useRef } from "react"; -import { useSearchParams, useNavigate, useLoaderData, useFetcher, useRevalidator } from "@remix-run/react"; -import { ActionFunctionArgs, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; -import { Table } from "~/components/ui/Table"; +import { useState, useEffect } from "react"; +import { useNavigate, useLoaderData } from "@remix-run/react"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; -import { Pagination } from "~/components/ui/Pagination"; -import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel"; import { toastService } from "~/components/ui/Toast"; -import { messageService } from "~/components/ui/MessageModal"; -import { usePermission } from "~/hooks/usePermission"; import { getDocumentTypes, deleteDocumentType, - type DocumentTypeUI, - type DocumentTypeSearchParams, - type DocumentTypeGroup, - getParentEvaluationPointGroups + getEntryModules, + type DocumentType, + type EntryModuleOption, } from "~/api/document-types/document-types"; import documentTypesStyles from "~/styles/pages/document-types_index.css?url"; - -// 引入CSS样式 export function links() { - return [ - { rel: "stylesheet", href: documentTypesStyles } - ]; + return [{ rel: "stylesheet", href: documentTypesStyles }]; } -// 页面元数据 export const meta: MetaFunction = () => { return [ { title: "文档类型管理 - 中国烟草AI合同及卷宗审核系统" }, - { name: "description", content: "管理文档类型,包括查看、编辑和删除文档类型" }, + { name: "description", content: "管理文档类型及其规则集绑定" }, ]; }; -// 定义加载器返回的数据类型 interface LoaderData { - types: DocumentTypeUI[]; - total: number; - pageSize: number; - currentPage: number; - error?: string; - parentGroups: DocumentTypeGroup[]; + types: DocumentType[]; + entryModules: EntryModuleOption[]; frontendJWT?: string | null; -} - -// 定义 action 返回的数据类型 -interface ActionResponse { - success?: boolean; error?: string; } -// 加载函数 - 获取文档类型列表 export async function loader({ request }: LoaderFunctionArgs) { try { - // 获取用户会话信息 const { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); - const url = new URL(request.url); - const name = url.searchParams.get('name') || undefined; - const groupId = url.searchParams.get('group_id') || undefined; - const entryModuleId = url.searchParams.get('entry_module_id') || undefined; - const page = parseInt(url.searchParams.get('page') || '1', 10); - const pageSize = parseInt(url.searchParams.get('pageSize') || '10', 10); - - // 构建搜索参数 - const searchParams: DocumentTypeSearchParams = { - name, - group_id: groupId ? parseInt(groupId, 10) : undefined, - entry_module_id: entryModuleId ? parseInt(entryModuleId, 10) : undefined, - page, - pageSize - }; - - // 并行获取文档类型数据和父级评查点分组 - const [parentGroupsResponse, typesResponse] = await Promise.all([ - getParentEvaluationPointGroups(frontendJWT), - getDocumentTypes(searchParams, frontendJWT) + const [typesRes, modulesRes] = await Promise.all([ + getDocumentTypes({}, frontendJWT), + getEntryModules(frontendJWT), ]); - // 如果获取父级评查点分组失败,返回空数组(不阻塞页面加载) - if(parentGroupsResponse.error){ - console.error("获取父级评查点分组失败:", parentGroupsResponse.error); - } - const parentGroups = parentGroupsResponse.error ? [] : (parentGroupsResponse.data || []); - - // 如果获取文档类型失败(如403无权限),返回空数组和错误信息 - if(typesResponse.error){ - console.error("获取文档类型失败:", typesResponse.error); - return Response.json({ - types: [], - total: 0, - pageSize, - currentPage: page, - parentGroups: [], - frontendJWT, - error: typesResponse.error - }); - } - - const typesResult = typesResponse.data?.types || []; - - return Response.json({ - types: typesResult, - total: typesResponse.data?.total || typesResult.length, - pageSize, - currentPage: page, - parentGroups, - frontendJWT - }); + return { + types: typesRes.data?.types || [], + entryModules: modulesRes.data || [], + frontendJWT, + }; } catch (error) { - console.error("加载文档类型列表失败:", error); - const errorMessage = error instanceof Error ? error.message : "加载文档类型列表失败"; - return Response.json({ - types: [], - total: 0, - pageSize: 10, - currentPage: 1, - parentGroups: [], - frontendJWT: null, - error: errorMessage - }); + return { types: [], entryModules: [], error: "加载失败" }; } } -// 动作函数 - 处理删除请求 -export async function action({ request }: ActionFunctionArgs) { - // 获取表单数据 - const formData = await request.formData(); - const id = formData.get("id") as string; - const intent = formData.get("intent") as string; - const { getUserSession } = await import("~/api/login/auth.server"); - const { frontendJWT } = await getUserSession(request); - - if (intent === "delete" && id) { - try { - const result = await deleteDocumentType(id, frontendJWT || undefined); - - if (result.error) { - return Response.json({ success: false, error: result.error }, { status: result.status || 500 }); - } - - return Response.json({ success: true }); - } catch (error) { - return Response.json( - { success: false, error: error instanceof Error ? error.message : "删除文档类型失败" }, - { status: 500 } - ); - } - } - - return Response.json({ success: false, error: "无效的操作" }, { status: 400 }); -} - -// 文档类型列表组件 -export default function DocumentTypesList() { +export default function DocumentTypesIndex() { const navigate = useNavigate(); - const [searchParams, setSearchParams] = useSearchParams(); - const fetcher = useFetcher(); - const revalidator = useRevalidator(); - const processedResponseRef = useRef(null); + const loaderData = useLoaderData(); + const [types, setTypes] = useState(loaderData.types || []); + const entryModules = loaderData.entryModules || []; - // 获取加载器数据 - const { types, total, error, parentGroups, frontendJWT } = useLoaderData(); - - // 处理 fetcher 响应 useEffect(() => { - // 为每个响应生成唯一标识,避免重复处理 - const responseKey = fetcher.data ? JSON.stringify(fetcher.data) : null; + setTypes(loaderData.types || []); + }, [loaderData.types]); - if (responseKey && responseKey !== processedResponseRef.current) { - if (fetcher.data?.success) { - toastService.success('删除成功!'); - processedResponseRef.current = responseKey; - // 只重新验证数据,不刷新整个页面 - revalidator.revalidate(); - } else if (fetcher.data?.error) { - toastService.error(`删除失败: ${fetcher.data.error}`); - processedResponseRef.current = responseKey; - } - } - }, [fetcher.data, revalidator]); + const getEntryModuleName = (id: number | null) => { + if (!id) return "—"; + return entryModules.find((m) => m.id === id)?.name || `#${id}`; + }; - // 权限控制 - const { canCreate, canUpdate, canDelete, canView } = usePermission(); - const canCreateType = canCreate('document_type'); - const canUpdateType = canUpdate('document_type'); - const canDeleteType = canDelete('document_type'); - // console.log('document_type---canDeleteType',canDeleteType) - const canViewType = canView('document_type'); + const handleDelete = async (type: DocumentType) => { + const confirmed = window.confirm(`确定要删除文档类型「${type.name}」吗?`); + if (!confirmed) return; - // 获取搜索参数 - const name = searchParams.get('name') || ''; - const currentPage = parseInt(searchParams.get('page') || String(1), 10); - const pageSize = parseInt(searchParams.get('pageSize') || String(10), 10); - - // 处理测试loader返回的信息 - useEffect(() => { - // console.log('返回的父级评查点分组数据',parentGroups) - }, [parentGroups]) - - // 处理loader加载数据的时候的错误 - useEffect(() => { - if(error){ - // 如果是无权限错误,显示友好提示 - if(error.includes('Permission denied') || error.includes('无权限') || error.includes('权限不足')){ - toastService.error('无权限访问文档类型管理,请联系系统管理员'); - } else { - toastService.error(error); - } - } - }, [error]); - - - // 处理名称搜索 - const handleNameSearch = (value: string) => { - const newParams = new URLSearchParams(searchParams); - if (value) { - newParams.set('name', value); + const result = await deleteDocumentType(type.id, loaderData.frontendJWT ?? undefined); + if (result.success) { + toastService.success("文档类型已删除"); + setTypes((prev) => prev.filter((t) => t.id !== type.id)); } else { - newParams.delete('name'); + toastService.error(result.error || "删除失败"); } - newParams.set('page', '1'); - setSearchParams(newParams); - }; - - // 处理筛选变更 - const handleFilterChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - const newParams = new URLSearchParams(searchParams); - - if (value) { - newParams.set(name, value); - } else { - newParams.delete(name); - } - - // 切换筛选条件时,重置到第一页 - newParams.set('page', '1'); - - setSearchParams(newParams); - }; - - // 处理重置筛选 - const handleReset = () => { - const nameInput = document.querySelector('input[placeholder="请输入文档类型名称"]'); - if (nameInput) { - (nameInput as HTMLInputElement).value = ''; - } - - // 重置所有筛选条件 - setSearchParams(new URLSearchParams()); - }; - - // 处理删除文档类型 - const handleDelete = (id: number) => { - // 权限检查 - if (!canDeleteType) { - toastService.warning('您没有删除权限'); - return; - } - - // 检查是否正在加载 - if (fetcher.state === 'submitting') { - return; - } - - messageService.show({ - title: "确认删除", - message: "确定要删除该文档类型吗?此操作不会影响关联的评查点分组,但可能会影响使用该类型的文档评查。", - type: "warning", - confirmText: "删除", - cancelText: "取消", - confirmDelay: 4, - onConfirm: () => { - const formData = new FormData(); - formData.append('id', id.toString()); - formData.append('intent', 'delete'); - - // 使用 useFetcher 提交请求 - fetcher.submit(formData, { method: 'post' }); - } - }); - }; - - // 处理编辑文档类型 - const handleEdit = (id: number) => { - navigate(`/document-types/new?id=${id}`); - }; - - // 处理分页变更 - const handlePageChange = (page: number) => { - const newParams = new URLSearchParams(searchParams); - newParams.set('page', page.toString()); - setSearchParams(newParams); - }; - - // 处理每页条数变更 - const handlePageSizeChange = (size: number) => { - const newParams = new URLSearchParams(searchParams); - newParams.set('pageSize', size.toString()); - newParams.set('page', '1'); - setSearchParams(newParams); }; - // 定义表格列配置 - const columns = [ - { - title: "文档类型名称", - key: "name", - width: "220px", - render: (_: unknown, record: DocumentTypeUI) => ( -
- - {record.name} -
- ) - }, - { - title: "描述", - key: "description", - width: "260px", - render: (_: unknown, record: DocumentTypeUI) => ( -
- {record.description || '-'} -
- ) - }, - { - title: "入口模块", - key: "entry_module", - width: "150px", - render: (_: unknown, record: DocumentTypeUI) => ( -
- {record.entry_module ? ( - {record.entry_module.name} - ) : ( - - - )} -
- ) - }, - { - title: "关联的评查点分组", - key: "groups", - render: (_: unknown, record: DocumentTypeUI) => ( -
- {record.groups && record.groups.length > 0 ? ( - record.groups.map(group => ( - - {group.name} - - )) - ) : ( - - - )} -
- ) - }, - { - title: "创建时间", - key: "created_at", - width: "160px", - render: (_: unknown, record: DocumentTypeUI) => record.created_at || '-' - }, - { - title: "更新时间", - key: "updated_at", - width: "160px", - render: (_: unknown, record: DocumentTypeUI) => record.updated_at || '-' - }, - { - title: "操作", - key: "operation", - width: "160px", - render: (_: unknown, record: DocumentTypeUI) => ( -
- {canViewType && ( - <> - - - )} - {canDeleteType && ( - - )} - {!canViewType && !canDeleteType && ( - - - )} -
- ) - } - ]; - return (
- {error && ( -
-

{error}

-
- )} - - {/* 页面头部 */}
-

文档类型管理

-
- {canCreateType && ( - - )} -
+

+ + 文档类型管理 +

+
- - {/* 搜索栏 */} - - - - } - noActionDivider={true} - > - ({ - value: group.id.toString(), - label: group.name - })) - ]} - onChange={handleFilterChange} - className="mr-3 w-[20%]" - /> - - - - - {/* 数据表格 */} - - - - {/* 分页 */} -
- -
+ + {types.length === 0 ? ( +
+ +

暂无文档类型

+
+ ) : ( +
+ + + + + + + + + + + + {types.map((type) => ( + + + + + + + + + ))} + +
编码名称入口模块规则集状态操作
{type.code}{type.name}{getEntryModuleName(type.entryModuleId)} + {type.ruleSetIds?.length || 0} 个规则集 + + + {type.isEnabled ? "启用" : "禁用"} + + +
+ + +
+
+ )}
); } - diff --git a/app/routes/document-types.new.tsx b/app/routes/document-types.new.tsx index 4d90c68..4f832d4 100644 --- a/app/routes/document-types.new.tsx +++ b/app/routes/document-types.new.tsx @@ -1,841 +1,229 @@ -import React, { useState, useEffect, useRef } from "react"; -import { Form, useActionData, useLoaderData, useNavigate, useSearchParams } from "@remix-run/react"; -import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from "@remix-run/node"; +import { useState, useEffect } from "react"; +import { useNavigate, useSearchParams, useLoaderData } from "@remix-run/react"; +import { LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; -import documentTypesNewStyles from "~/styles/pages/document-types_new.css?url"; -import { type RuleGroup } from "~/api/evaluation_points/rule-groups"; +import { toastService } from "~/components/ui/Toast"; import { getDocumentType, createDocumentType, updateDocumentType, getEntryModules, - getRootEvaluationPointGroups_ForDocTypes, - getPromptTemplateOptions + getRuleSets, + type DocumentType, + type DocumentTypeCreateDTO, + type DocumentTypeUpdateDTO, + type EntryModuleOption, + type RuleSetOption, } from "~/api/document-types/document-types"; -import { getEvaluationPointGroupChildren } from "~/api/evaluation_points/rule-groups"; -import { toastService } from "~/components/ui/Toast"; -import { usePermission } from "~/hooks/usePermission"; +import newStyles from "~/styles/pages/document-types_new.css?url"; export function links() { - return [{ rel: "stylesheet", href: documentTypesNewStyles }]; + return [{ rel: "stylesheet", href: newStyles }]; } -export const handle = { - breadcrumb: (data:LoaderData) => { - if (data.isEdit) { - return "编辑文档类型"; - } else { - return "新增文档类型"; - } - } +export const meta: MetaFunction = () => { + return [{ title: "新建/编辑文档类型 - 中国烟草AI合同及卷宗审核系统" }]; }; interface LoaderData { - isEdit: boolean; + entryModules: EntryModuleOption[]; + ruleSets: RuleSetOption[]; + editType: DocumentType | null; + frontendJWT?: string | null; } -export const meta: MetaFunction = ({ location }) => { - const isEdit = new URLSearchParams(location.search).has("id"); - return [ - { title: `${isEdit ? "编辑" : "新增"}文档类型 - 中国烟草AI合同及卷宗审核系统` }, - { name: "description", content: `${isEdit ? "编辑" : "新增"}文档类型,设置文档类型名称、描述和关联的评查点分组` } - ]; -}; - -// 定义模板类型 -const TEMPLATE_TYPES = { - LLM_EXTRACTION: "LLM_Extraction", - VLM_EXTRACTION: "VLM_Extraction" -}; - -// 定义动作返回的数据类型 -interface ActionData { - result?: boolean; - errors?: { - name?: string; - groups?: string; - general?: string; - llmExtractionTemplate?: string; - vlmExtractionTemplate?: string; - }; - values?: Record; -} - - -// 获取数据 export async function loader({ request }: LoaderFunctionArgs) { - try { - // 获取用户会话信息 - const { getUserSession } = await import("~/api/login/auth.server"); - const { frontendJWT } = await getUserSession(request); - - const url = new URL(request.url); - const id = url.searchParams.get("id"); - const isEdit = id ? true : false; - - // 1. 获取一级评查点分组(后续通过点击展开按钮动态加载子分组) - const ruleGroupsResponse = await getRootEvaluationPointGroups_ForDocTypes(frontendJWT); - if (ruleGroupsResponse.error) { - console.error("获取一级评查点分组失败:", ruleGroupsResponse.error); - } - const rootGroups = ruleGroupsResponse.error ? [] : ruleGroupsResponse.data || []; - - // 2. 获取入口模块列表 - const entryModulesResponse = await getEntryModules(frontendJWT); - if (entryModulesResponse.error) { - console.error("获取入口模块失败:", entryModulesResponse.error); - } - const entryModules = entryModulesResponse.error ? [] : (entryModulesResponse.data || []); - - // 3. 获取提示词模板(只获取 LLM 和 VLM) - const [llmExtractionTemplatesResponse, vlmExtractionTemplatesResponse] = - await Promise.all([ - getPromptTemplateOptions(TEMPLATE_TYPES.LLM_EXTRACTION, frontendJWT), - getPromptTemplateOptions(TEMPLATE_TYPES.VLM_EXTRACTION, frontendJWT) - ]); - - // 4. 如果是编辑模式,获取文档类型详情 - let documentType = undefined; - if (id) { - const typeResponse = await getDocumentType(id, frontendJWT); - if (typeResponse.data) { - documentType = typeResponse.data; - } - } - - return Response.json({ - isEdit, - documentType, - ruleGroups: rootGroups, - entryModules, - llmExtractionTemplates: llmExtractionTemplatesResponse.data || [], - vlmExtractionTemplates: vlmExtractionTemplatesResponse.data || [] - }); - } catch (error) { - console.error("加载数据失败:", error); - return Response.json({ - isEdit: false, - documentType: undefined, - ruleGroups: [], - entryModules: [], - llmExtractionTemplates: [], - vlmExtractionTemplates: [], - error: error || "加载数据失败" - }); - } -} - -// 处理表单提交 -export async function action({ request }: ActionFunctionArgs) { const { getUserSession } = await import("~/api/login/auth.server"); const { frontendJWT } = await getUserSession(request); + const url = new URL(request.url); + const editId = url.searchParams.get("id"); - const formData = await request.formData(); - const id = formData.get("id") as string | null; - const name = formData.get("name") as string; - const code = formData.get("code") as string; - const description = formData.get("description") as string; - const entryModuleId = formData.get("entry_module_id") as string; - const llmExtractionTemplateId = formData.get("llm_extraction_template") as string; - const vlmExtractionTemplateId = formData.get("vlm_extraction_template") as string; + const [modulesRes, setsRes] = await Promise.all([ + getEntryModules(frontendJWT), + getRuleSets(frontendJWT), + ]); - // 获取选中的评查点分组ID列表 - const selectedGroups = formData.getAll("checkpoint_group_ids") as string[]; - - // 表单验证 - const errors: ActionData["errors"] = {}; - - // 收集所有错误 - if (!name || name.trim() === "") { - errors.name = "文档类型名称不能为空"; + let editType: DocumentType | null = null; + if (editId) { + const res = await getDocumentType(parseInt(editId), frontendJWT); + editType = res.data || null; } - // 验证:如果入口模块为"合同管理",文档类型名称必须包含"合同" - if (entryModuleId && name) { - // 获取入口模块列表以验证模块名称 - const entryModulesResponse = await getEntryModules(frontendJWT); - if (!entryModulesResponse.error && entryModulesResponse.data) { - const selectedModule = entryModulesResponse.data.find( - (m: { id: number; name: string }) => m.id.toString() === entryModuleId - ); - if (selectedModule?.name === '合同管理' && !name.includes('合同')) { - errors.name = '入口模块为"合同管理"时,文档类型名称必须包含"合同"'; - } - } - } - - if (selectedGroups.length === 0) { - errors.groups = "请至少选择一个关联的评查点分组"; - } - - if (!llmExtractionTemplateId) { - errors.llmExtractionTemplate = "请选择llm抽取提示词模板"; - } - - if (!vlmExtractionTemplateId) { - errors.vlmExtractionTemplate = "请选择vlm抽取提示词模板"; - } - - // 如果有错误,返回错误信息 - if (Object.keys(errors).length > 0) { - return Response.json({ errors, result: false }); - } - - try { - // 构建文档类型数据 - group_ids 转换为 number[] - const documentTypeData = { - name, - code: code || null, - description, - group_ids: selectedGroups.map(id => parseInt(id, 10)), - entry_module_id: entryModuleId ? parseInt(entryModuleId) : null, - llm_extraction_template_id: llmExtractionTemplateId ? parseInt(llmExtractionTemplateId) : null, - vlm_extraction_template_id: vlmExtractionTemplateId ? parseInt(vlmExtractionTemplateId) : null - }; - - // 调用API创建或更新文档类型 - let response; - if (id) { - // 更新文档类型 - response = await updateDocumentType(id, { - ...documentTypeData, - id: parseInt(id), - }, frontendJWT); - } else { - // 创建新文档类型 - response = await createDocumentType(documentTypeData, frontendJWT); - } - - if (response.error) { - console.error("保存/更新文档类型失败:", response.error); - throw new Error(response.error); - } - - // 操作成功,重定向到列表页 - return redirect("/document-types"); - } catch (error) { - console.error("保存文档类型失败:", error); - return Response.json({ - result: false, - errors: { - general: error instanceof Error ? error.message : "保存文档类型失败" - } - }); - } + return { entryModules: modulesRes.data || [], ruleSets: setsRes.data || [], editType, frontendJWT }; } export default function DocumentTypeNew() { const navigate = useNavigate(); - const [searchParams] = useSearchParams(); - const isEditMode = searchParams.has("id"); + const loaderData = useLoaderData(); + const editType = loaderData.editType; + const isEdit = !!editType; - const { - documentType, - ruleGroups, - entryModules, - llmExtractionTemplates, - vlmExtractionTemplates - } = useLoaderData(); + const [code, setCode] = useState(""); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [entryModuleId, setEntryModuleId] = useState(null); + const [selectedRuleSetIds, setSelectedRuleSetIds] = useState([]); + const [saving, setSaving] = useState(false); + const [errors, setErrors] = useState>({}); - const actionData = useActionData(); - - // 权限控制 - const { canCreate, canUpdate } = usePermission(); - const canCreateType = canCreate('document_type'); - const canUpdateType = canUpdate('document_type'); - - const urlMode = searchParams.get('mode'); - const isViewMode = urlMode === 'view'; - - const hasEditPermission = isEditMode ? canUpdateType : canCreateType; - const isReadOnly = isViewMode || !hasEditPermission; - - // 状态管理 - const [formData, setFormData] = useState({ - id: documentType?.id || "", - name: documentType?.name || "", - code: documentType?.code || "", - description: documentType?.description || "", - entryModuleId: documentType?.entry_module?.id?.toString() || "", - llmExtractionTemplateId: documentType?.llm_extraction_template_id?.toString() || "", - vlmExtractionTemplateId: documentType?.vlm_extraction_template_id?.toString() || "", - selectedGroups: documentType?.groups?.map((g: { id: string | number }) => g.id.toString()) || [] - }); - - // 添加本地验证错误状态 - const [formErrors, setFormErrors] = useState({} as ActionData["errors"]); - - // 表单引用 - const formRef = useRef(null); - - // 字段是否被触摸过(用于确定何时显示错误) - const [touchedFields, setTouchedFields] = useState({ - name: false, - llmExtractionTemplate: false, - vlmExtractionTemplate: false, - groups: false - }); - - // 客户端调试信息ruleGroups useEffect(() => { - console.log('返回的评查点分组数据',ruleGroups) - }, [ruleGroups]) - - // 新增模式下,设置模板的默认值(选择第一个选项) - useEffect(() => { - if (!isEditMode) { - setFormData(prev => ({ - ...prev, - llmExtractionTemplateId: prev.llmExtractionTemplateId || (llmExtractionTemplates[0]?.id?.toString() || ""), - vlmExtractionTemplateId: prev.vlmExtractionTemplateId || (vlmExtractionTemplates[0]?.id?.toString() || "") - })); + if (editType) { + setCode(editType.code || ""); + setName(editType.name || ""); + setDescription(editType.description || ""); + setEntryModuleId(editType.entryModuleId); + setSelectedRuleSetIds(editType.ruleSetIds || []); } - }, [isEditMode, llmExtractionTemplates, vlmExtractionTemplates]); - - // 从actionData初始化本地错误 - useEffect(() => { - if (!actionData?.result) { - setFormErrors(actionData?.errors); - if (actionData?.errors?.general) { - toastService.error(actionData?.errors?.general || "保存文档类型失败"); + }, [editType]); + + const validate = (): boolean => { + const errs: Record = {}; + if (!code.trim()) errs.code = "编码不能为空"; + else if (!/^[a-zA-Z][a-zA-Z0-9_.]*$/.test(code.trim())) errs.code = "编码格式:字母开头,可含字母数字._"; + if (!name.trim()) errs.name = "名称不能为空"; + setErrors(errs); + return Object.keys(errs).length === 0; + }; + + const toggleRuleSet = (id: number) => { + setSelectedRuleSetIds((prev) => + prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id] + ); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!validate()) return; + + setSaving(true); + try { + if (isEdit && editType) { + const dto: DocumentTypeUpdateDTO = { + name: name.trim(), + description: description.trim(), + entryModuleId, + ruleSetIds: selectedRuleSetIds, + }; + const res = await updateDocumentType(editType.id, dto, loaderData.frontendJWT ?? undefined); + if (res.error) { toastService.error(res.error); return; } + toastService.success("文档类型已更新"); + } else { + const dto: DocumentTypeCreateDTO = { + code: code.trim(), + name: name.trim(), + description: description.trim(), + entryModuleId, + ruleSetIds: selectedRuleSetIds, + }; + const res = await createDocumentType(dto, loaderData.frontendJWT ?? undefined); + if (res.error) { toastService.error(res.error); return; } + toastService.success("文档类型已创建"); } - } - }, [actionData]); - - // 分组展开状态 - const [expandedGroups, setExpandedGroups] = useState>({}); - - // 动态加载的子分组数据(groupId -> children[]) - const [groupChildrenMap, setGroupChildrenMap] = useState>({}); - - // 子分组加载状态 - const [loadingChildren, setLoadingChildren] = useState>({}); - - // 当文档类型数据加载完成时更新表单 - useEffect(() => { - if (documentType) { - console.log('documentType', documentType) - setFormData({ - id: documentType.id, - name: documentType.name, - code: documentType.code || "", - description: documentType.description, - entryModuleId: documentType.entry_module?.id?.toString() || "", - llmExtractionTemplateId: documentType.llm_extraction_template_id?.toString() || "", - vlmExtractionTemplateId: documentType.vlm_extraction_template_id?.toString() || "", - selectedGroups: documentType.groups.map((g: { id: string | number }) => g.id.toString()) - }); - - // 初始化展开状态 - 如果选中的是一级分组,需要加载子分组并展开 - const loadInitialChildren = async () => { - const newExpandedGroups: Record = {}; - const newChildrenMap: Record = {}; - - // 获取 frontendJWT - let frontendJWT: string | undefined = undefined; - if (typeof window !== 'undefined') { - const userInfoStr = localStorage.getItem('user_info'); - if (userInfoStr) { - try { - const userInfo = JSON.parse(userInfoStr); - frontendJWT = userInfo.frontendJWT; - } catch (e) { - console.error('解析用户信息失败:', e); - } - } - } - - // 遍历所有一级分组,检查是否被选中 - for (const parentGroup of ruleGroups) { - const isParentSelected = documentType.groups.some((g: { id: string | number }) => - g.id.toString() === parentGroup.id.toString() - ); - - if (isParentSelected) { - // 标记为展开 - newExpandedGroups[parentGroup.id] = true; - - // 加载子分组 - try { - const response = await getEvaluationPointGroupChildren( - parentGroup.id, - { pageSize: 1000 }, - frontendJWT - ); - - if (!response.error && response.data) { - newChildrenMap[parentGroup.id] = response.data; - } - } catch (error) { - console.error(`加载分组 ${parentGroup.id} 的子分组失败:`, error); - } - } - } - - setExpandedGroups(newExpandedGroups); - setGroupChildrenMap(newChildrenMap); - }; - - loadInitialChildren(); - } - }, [documentType, ruleGroups]); - - // 验证表单字段 - const validateField = (field: string, value: string | string[], allFormData?: typeof formData): string => { - switch (field) { - case 'name': - if (!value || (typeof value === 'string' && value.trim() === "")) { - return "文档类型名称不能为空"; - } - // 检查入口模块是否为"合同管理",如果是则名称必须包含"合同" - const currentEntryModuleId = allFormData?.entryModuleId || formData.entryModuleId; - const selectedModule = entryModules.find((m: { id: number; name: string }) => m.id.toString() === currentEntryModuleId); - if (selectedModule?.name === '合同管理' && typeof value === 'string' && !value.includes('合同')) { - return '入口模块为"合同管理"时,文档类型名称必须包含"合同"'; - } - return ""; - case 'llmExtractionTemplate': - return !value || (typeof value === 'string' && value.trim() === "") ? "请选择llm抽取提示词模板" : ""; - case 'vlmExtractionTemplate': - return !value || (typeof value === 'string' && value.trim() === "") ? "请选择vlm抽取提示词模板" : ""; - case 'groups': - return Array.isArray(value) && value.length === 0 ? "请至少选择一个关联的评查点分组" : ""; - default: - return ""; + navigate("/document-types"); + } catch (error) { + toastService.error(error instanceof Error ? error.message : "保存失败"); + } finally { + setSaving(false); } }; - - // 处理分组勾选 - const handleGroupCheckChange = ( - groupId: string, - isChecked: boolean - ) => { - // 多选模式:添加或移除选中的分组 - let newSelectedGroups: string[] = []; - if (isChecked) { - // 添加当前选中的分组(避免重复) - newSelectedGroups = [...formData.selectedGroups, groupId]; - } else { - // 移除取消选中的分组 - newSelectedGroups = formData.selectedGroups.filter(id => id !== groupId); - } - - setFormData(prev => ({ ...prev, selectedGroups: newSelectedGroups })); - - // 标记字段为已触摸 - setTouchedFields(prev => ({...prev, groups: true})); - - // 实时验证 - const error = validateField('groups', newSelectedGroups); - setFormErrors(prev => ({...prev, groups: error})); - }; - - // 修复展开/折叠功能 - 动态加载子分组 - const handleGroupExpand = async (groupId: string, event: React.MouseEvent) => { - // 阻止事件冒泡,避免触发checkbox选中 - event.stopPropagation(); - - const isCurrentlyExpanded = expandedGroups[groupId]; - - // 如果当前是折叠状态,准备展开 - if (!isCurrentlyExpanded) { - // 检查是否已经加载过子分组 - if (!groupChildrenMap[groupId]) { - // 还未加载,开始加载 - setLoadingChildren(prev => ({ ...prev, [groupId]: true })); - - try { - // 获取用户 token - let frontendJWT: string | undefined = undefined; - if (typeof window !== 'undefined') { - const userInfoStr = localStorage.getItem('user_info'); - if (userInfoStr) { - try { - const userInfo = JSON.parse(userInfoStr); - frontendJWT = userInfo.frontendJWT; - } catch (e) { - console.error('解析用户信息失败:', e); - } - } - } - - // 调用 API 获取子分组 - const response = await getEvaluationPointGroupChildren( - groupId, - { pageSize: 1000 }, // 获取所有子分组 - frontendJWT - ); - - if (response.error) { - console.error('获取子分组失败:', response.error); - toastService.error('获取子分组失败'); - setLoadingChildren(prev => ({ ...prev, [groupId]: false })); - return; - } - - // 保存子分组数据 - setGroupChildrenMap(prev => ({ - ...prev, - [groupId]: response.data || [] - })); - - setLoadingChildren(prev => ({ ...prev, [groupId]: false })); - } catch (error) { - console.error('获取子分组异常:', error); - toastService.error('获取子分组失败'); - setLoadingChildren(prev => ({ ...prev, [groupId]: false })); - return; - } - } - } - - // 切换展开/折叠状态 - setExpandedGroups(prev => ({ - ...prev, - [groupId]: !isCurrentlyExpanded - })); - }; - - // 处理表单输入变化 - const handleInputChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - - // 根据name属性映射到对应的formData字段 - let fieldName = name; - - if (name === 'llm_extraction_template') { - fieldName = 'llmExtractionTemplateId'; - setTouchedFields(prev => ({...prev, llmExtractionTemplate: true})); - } else if (name === 'vlm_extraction_template') { - fieldName = 'vlmExtractionTemplateId'; - setTouchedFields(prev => ({...prev, vlmExtractionTemplate: true})); - } else if (name === 'entry_module_id') { - fieldName = 'entryModuleId'; - } else if (name === 'name') { - setTouchedFields(prev => ({...prev, name: true})); - } - - // 先更新 formData - const updatedFormData = { ...formData, [fieldName]: value }; - setFormData(updatedFormData); - - // 实时验证 - if (name === 'name') { - const error = validateField(name, value, updatedFormData); - setFormErrors(prev => ({...prev, name: error})); - } else if (name === 'llm_extraction_template') { - const error = validateField('llmExtractionTemplate', value); - setFormErrors(prev => ({...prev, llmExtractionTemplate: error})); - } else if (name === 'vlm_extraction_template') { - const error = validateField('vlmExtractionTemplate', value); - setFormErrors(prev => ({...prev, vlmExtractionTemplate: error})); - } else if (name === 'entry_module_id') { - // 入口模块变更时,重新验证名称字段(如果名称已被触摸过) - if (touchedFields.name) { - const nameError = validateField('name', updatedFormData.name, updatedFormData); - setFormErrors(prev => ({...prev, name: nameError})); - } - } - }; - - // 处理表单提交前验证 - const handleBeforeSubmit = (e: React.FormEvent) => { - // 权限检查 - if (isEditMode && !canUpdateType) { - toastService.warning('您没有修改权限,无法保存更改'); - e.preventDefault(); - return; - } - if (!isEditMode && !canCreateType) { - toastService.warning('您没有创建权限,无法新增文档类型'); - e.preventDefault(); - return; - } - - // 标记所有字段为已触摸 - setTouchedFields({ - name: true, - llmExtractionTemplate: true, - vlmExtractionTemplate: true, - groups: true - }); - - // 验证所有字段 - const errors = { - name: validateField('name', formData.name, formData), - llmExtractionTemplate: validateField('llmExtractionTemplate', formData.llmExtractionTemplateId), - vlmExtractionTemplate: validateField('vlmExtractionTemplate', formData.vlmExtractionTemplateId), - groups: validateField('groups', formData.selectedGroups) - }; - - setFormErrors(errors); - - // 如果有错误,阻止提交 - if (errors.name || errors.llmExtractionTemplate || errors.vlmExtractionTemplate || errors.groups) { - e.preventDefault(); - } - }; - return (
- {/* 页面头部 */}
-

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

-
- - {!isReadOnly && ( - - )} -
+

+ + {isEdit ? `编辑文档类型 — ${editType?.code}` : "新建文档类型"} +

- - {/* 表单内容 */} + -
- {/* 如果是编辑模式,添加隐藏的ID字段 */} - {formData.id && } - -
- {/* 错误提示 */} - {formErrors?.general && ( -
- - {formErrors.general} -
- )} - - {/* 文档类型名称 */} + +
- + { setCode(e.target.value); setErrors({ ...errors, code: "" }); }} + disabled={isEdit} /> - {touchedFields.name && formErrors?.name && ( -
{formErrors.name}
- )} -
例如:销售合同、采购合同、专卖许可证等
+ {errors.code && {errors.code}} + {isEdit && 编码创建后不可修改}
- - {/* 文档类型编码 */}
- + { setName(e.target.value); setErrors({ ...errors, name: "" }); }} /> -
用于系统内部识别的唯一编码(可选)
-
- - {/* 入口模块 */} -
- - -
选择此文档类型对应的入口模块(可选)
-
- - {/* 类型描述 */} -
- - -
- - {/* 提示词模板选择区域 */} -
- {/* llm抽取提示词模板 */} -
- - - {touchedFields.llmExtractionTemplate && formErrors?.llmExtractionTemplate && ( -
{formErrors.llmExtractionTemplate}
- )} -
选择用于从此类文档中抽取信息的llm提示词模板
-
- - {/* vlm抽取提示词模板 */} -
- - - {touchedFields.vlmExtractionTemplate && formErrors?.vlmExtractionTemplate && ( -
{formErrors.vlmExtractionTemplate}
- )} -
选择用于从此类文档中抽取信息的vlm提示词模板
-
-
- - {/* 关联评查点分组 */} -
-
- - 关联评查点分组 * - -
- {ruleGroups.map((group: RuleGroup) => ( - - {/* 父分组 */} -
- - handleGroupCheckChange(group.id, e.target.checked)} - className="checkbox-input" - disabled={isReadOnly} - /> - -
- - {/* 子分组 - 动态加载并展示 */} - {expandedGroups[group.id] && ( - <> - {loadingChildren[group.id] ? ( -
- - - 加载中... - -
- ) : ( - groupChildrenMap[group.id]?.map((child: RuleGroup) => ( -
- - - {child.name} - 二级分组 - -
- )) - )} - - )} -
- ))} -
- {touchedFields.groups && formErrors?.groups && ( -
{formErrors.groups}
- )} -
选择与此文档类型关联的评查点分组,文档上传后将应用这些分组中的评查点进行审核
-
+ {errors.name && {errors.name}}
- + +
+ +