From 5cf05eca40bee012482b933c1a189d6039101a05 Mon Sep 17 00:00:00 2001 From: yorn <1057707203@qq.com> Date: Mon, 7 Apr 2025 22:40:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E8=AF=84=E6=9F=A5=E7=82=B9?= =?UTF-8?q?=E5=88=86=E7=BB=84=E7=9A=84=E5=A2=9E=E5=88=A0=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/client.ts | 95 ++++- app/api/evaluation_points/rule-groups.ts | 499 +++++++++++++++++++++-- app/api/files.ts | 54 +++ app/api/postgrest-client.ts | 236 +++++++++-- app/api/system_setting/config-lists.ts | 0 app/components/ui/UploadArea.tsx | 20 +- app/routes/files.upload.tsx | 112 +++-- app/routes/rule-groups._index.tsx | 383 ++++++++++++++--- app/routes/rule-groups.new.tsx | 182 ++++----- app/types/enums.ts | 47 +++ app/types/files.ts | 33 ++ json.txt | 109 ++++- 12 files changed, 1496 insertions(+), 274 deletions(-) create mode 100644 app/api/files.ts create mode 100644 app/api/system_setting/config-lists.ts create mode 100644 app/types/enums.ts create mode 100644 app/types/files.ts diff --git a/app/api/client.ts b/app/api/client.ts index bc48c7e..66263e5 100644 --- a/app/api/client.ts +++ b/app/api/client.ts @@ -156,7 +156,26 @@ export async function apiRequest( headers.set('Accept', 'application/json'); } - // 发送请求,5秒超时 + // 针对 PostgREST 的额外处理 + if (endpoint.includes('evaluation_point_groups') && (options.method === 'POST' || options.method === 'PATCH')) { + console.log('使用 PostgREST 特定配置处理请求'); + // 确保请求体是有效的 JSON 对象 + if (options.body && typeof options.body === 'string') { + try { + JSON.parse(options.body); // 验证 JSON 是否有效 + } catch (e) { + console.error('请求体不是有效的 JSON:', options.body); + throw new Error('请求体必须是有效的 JSON'); + } + } + } + + console.log(`发送 ${options.method || 'GET'} 请求到: ${url}`); + if (options.body) { + console.log(`请求体: ${options.body}`); + } + + // 发送请求,10秒超时 const response = await fetchWithTimeout(url, { ...options, headers @@ -164,9 +183,23 @@ export async function apiRequest( // 解析响应 let data = null; - const contentType = response.headers.get('content-type'); - if (contentType && contentType.includes('application/json') && response.status !== 204) { - data = await response.json(); + let responseText = ''; + + try { + const contentType = response.headers.get('content-type'); + responseText = await response.text(); + + if (contentType && contentType.includes('application/json') && response.status !== 204 && responseText) { + try { + data = JSON.parse(responseText); + } catch (e) { + console.error('响应不是有效的 JSON:', responseText); + throw new Error('服务器返回无效的 JSON 响应'); + } + } + } catch (e) { + console.error('解析响应失败:', e); + console.log('原始响应:', responseText); } // 收集响应头信息 @@ -175,6 +208,51 @@ export async function apiRequest( responseHeaders[key] = value; }); + // 打印响应信息 + // console.log(`响应状态: ${response.status} ${response.statusText}`); + // console.log(`响应头:`, responseHeaders); + // console.log(`响应体:`, data || responseText); + + // 检查 PostgREST 特定错误 + if (!response.ok) { + if (response.status === 400) { + console.error('PostgREST 错误 - 无效请求:', data || responseText); + return { + error: '无效的请求格式,请检查数据格式是否正确', + status: response.status, + headers: responseHeaders + }; + } else if (response.status === 401) { + console.error('PostgREST 错误 - 未授权:', data || responseText); + return { + error: '未授权,请检查认证信息', + status: response.status, + headers: responseHeaders + }; + } else if (response.status === 403) { + console.error('PostgREST 错误 - 禁止访问:', data || responseText); + return { + error: '没有权限执行此操作', + status: response.status, + headers: responseHeaders + }; + } else if (response.status === 404) { + console.error('PostgREST 错误 - 资源不存在:', data || responseText); + return { + error: '请求的资源不存在', + status: response.status, + headers: responseHeaders + }; + } else { + console.error(`HTTP请求失败: ${response.status} - ${url}`, data || responseText); + return { + error: data?.msg || `请求失败: ${response.status}`, + status: response.status, + headers: responseHeaders + }; + } + } + // 检查API返回的状态码 if (data && 'code' in data && data.code !== 0) { console.error(`API请求失败: ${data.msg || '未知错误'} - ${url}`); @@ -185,15 +263,6 @@ export async function apiRequest( }; } - if (!response.ok) { - console.error(`HTTP请求失败: ${response.status} - ${url}`); - return { - error: data?.msg || `请求失败: ${response.status}`, - status: response.status, - headers: responseHeaders - }; - } - return { data, status: response.status, diff --git a/app/api/evaluation_points/rule-groups.ts b/app/api/evaluation_points/rule-groups.ts index 9f3c15b..ac6a8db 100644 --- a/app/api/evaluation_points/rule-groups.ts +++ b/app/api/evaluation_points/rule-groups.ts @@ -1,4 +1,5 @@ -import { postgrestGet, type PostgrestParams } from '../postgrest-client'; +import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client'; +import dayjs from 'dayjs'; /** * 评查点分组接口 @@ -7,9 +8,75 @@ export interface RuleGroup { id: string; pid: string; name: string; - status: boolean; + code?: string; // 添加分组编码字段 + is_enabled: boolean; ruleCount?: number; // 评查点数量 children?: RuleGroup[]; // 子分组 + createdAt?: string; // 添加创建时间字段 + description?: string; // 描述 +} + +// API请求模型 +export interface ApiRuleGroup { + id?: number; + pid: number; + name: string; + code?: string; + description?: string; + is_enabled: boolean; + created_at?: string; + updated_at?: string; +} + +// 创建或更新分组请求参数 +export interface RuleGroupCreateUpdateDto { + name: string; + code: string; + pid: string | null; // 父分组ID,如果是一级分组则为null或'0' + description?: string; + is_enabled: boolean; +} + +// 用于替换代码中的 any 类型 +interface ApiResponse { + code: number; + msg: string; + data: T; +} + +/** + * 格式化日期 + * @param dateString 日期字符串 + * @returns 格式化后的日期字符串 + */ +function formatDate(dateString: string): string { + if (!dateString) return ''; + try { + return dayjs(dateString).format('YYYY-MM-DD HH:mm:ss'); + } catch (error) { + console.error('日期格式化失败:', error); + return dateString; + } +} + +/** + * 从不同格式的 API 响应中提取数据 + * @param responseData API 响应数据 + * @returns 提取后的数据或 null + */ +function extractApiData(responseData: unknown): T | null { + if (!responseData) return null; + + // 格式1: { code: number, msg: string, data: T } + if (typeof responseData === 'object' && responseData !== null && + 'code' in responseData && + 'data' in responseData && + (responseData as { data: unknown }).data) { + return (responseData as { data: T }).data; + } + + // 格式2: 直接是数据对象 + return responseData as T; } /** @@ -18,53 +85,62 @@ export interface RuleGroup { */ export async function getRuleGroups(): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { try { - // 1. 获取所有一级分组(pid=0) - const parentGroupsParams: PostgrestParams = { + const params: PostgrestParams = { select: ` id, pid, name, - status + code, + description, + is_enabled, + created_at `, filter: { 'pid': 'eq.0' } }; - const parentGroupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{ + const response = await postgrestGet<{code: number; msg: string; data: Array<{ id: number; pid: number; name: string; - status: boolean; - }>}>('evaluation_point_groups', parentGroupsParams); + code?: string; + description?: string; + is_enabled: boolean; + created_at?: string; + }>}>('evaluation_point_groups', params); - if (parentGroupsResponse.error) { - return { error: parentGroupsResponse.error, status: parentGroupsResponse.status }; + if (response.error) { + return { error: response.error, status: response.status }; } // 处理响应数据 - let parentGroups: RuleGroup[] = []; - if (parentGroupsResponse.data && 'code' in parentGroupsResponse.data && parentGroupsResponse.data.data) { - parentGroups = parentGroupsResponse.data.data.map(group => ({ + let groups: RuleGroup[] = []; + if (response.data && 'code' in response.data && response.data.data) { + groups = response.data.data.map(group => ({ id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, - children: [] // 初始化子分组数组 + code: group.code, + description: group.description, + is_enabled: group.is_enabled, + createdAt: group.created_at ? formatDate(group.created_at) : undefined })); - } else if (Array.isArray(parentGroupsResponse.data)) { - parentGroups = parentGroupsResponse.data.map(group => ({ + } else if (Array.isArray(response.data)) { + groups = response.data.map(group => ({ id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, - children: [] // 初始化子分组数组 + code: group.code, + description: group.description, + is_enabled: group.is_enabled, + createdAt: group.created_at ? formatDate(group.created_at) : undefined })); } - return { data: parentGroups }; + return { data: groups }; } catch (error) { - console.error('获取评查点分组列表出错:', error); + console.error('获取评查点分组列表失败:', error); return { error: error instanceof Error ? error.message : '获取评查点分组列表失败', status: 500 @@ -85,7 +161,9 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup id, pid, name, - status + code, + is_enabled, + created_at `, filter: { 'pid': `eq.${parentId}` @@ -96,7 +174,9 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup id: number; pid: number; name: string; - status: boolean; + code?: string; + is_enabled: boolean; + created_at?: string; }>}>('evaluation_point_groups', childGroupsParams); if (childGroupsResponse.error) { @@ -115,18 +195,18 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup } }; - const ruleCountResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_points', ruleCountParams); + const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); return { id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, + code: group.code, + is_enabled: group.is_enabled, + createdAt: group.created_at ? formatDate(group.created_at) : undefined, ruleCount: ruleCountResponse.data && 'code' in ruleCountResponse.data - ? ruleCountResponse.data.data?.length || 0 - : Array.isArray(ruleCountResponse.data) - ? ruleCountResponse.data.length - : 0 + ? (ruleCountResponse.data.data && Array.isArray(ruleCountResponse.data.data) ? ruleCountResponse.data.data.length : 0) + : (Array.isArray(ruleCountResponse.data) ? (ruleCountResponse.data as unknown[]).length : 0) }; })); } else if (Array.isArray(childGroupsResponse.data)) { @@ -139,18 +219,18 @@ export async function getChildGroups(parentId: string): Promise<{data: RuleGroup } }; - const ruleCountResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_points', ruleCountParams); + const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); return { id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, + code: group.code, + is_enabled: group.is_enabled, + createdAt: group.created_at ? formatDate(group.created_at) : undefined, ruleCount: ruleCountResponse.data && 'code' in ruleCountResponse.data - ? ruleCountResponse.data.data?.length || 0 - : Array.isArray(ruleCountResponse.data) - ? ruleCountResponse.data.length - : 0 + ? (ruleCountResponse.data.data && Array.isArray(ruleCountResponse.data.data) ? ruleCountResponse.data.data.length : 0) + : (Array.isArray(ruleCountResponse.data) ? (ruleCountResponse.data as unknown[]).length : 0) }; })); } @@ -177,7 +257,7 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne id, pid, name, - status + is_enabled ` }; @@ -185,7 +265,7 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne id: number; pid: number; name: string; - status: boolean; + is_enabled: boolean; }>}>('evaluation_point_groups', allGroupsParams); if (allGroupsResponse.error) { @@ -199,7 +279,7 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, + is_enabled: group.is_enabled, children: [] })); } else if (Array.isArray(allGroupsResponse.data)) { @@ -207,7 +287,7 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne id: group.id.toString(), pid: group.pid.toString(), name: group.name, - status: group.status, + is_enabled: group.is_enabled, children: [] })); } @@ -228,13 +308,11 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne } }; - const ruleCountResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_points', ruleCountParams); + const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); child.ruleCount = ruleCountResponse.data && 'code' in ruleCountResponse.data - ? ruleCountResponse.data.data?.length || 0 - : Array.isArray(ruleCountResponse.data) - ? ruleCountResponse.data.length - : 0; + ? (ruleCountResponse.data.data && Array.isArray(ruleCountResponse.data.data) ? ruleCountResponse.data.data.length : 0) + : (Array.isArray(ruleCountResponse.data) ? (ruleCountResponse.data as unknown[]).length : 0) } } @@ -246,4 +324,337 @@ export async function getAllRuleGroups(): Promise<{data: RuleGroup[]; error?: ne status: 500 }; } +} + +/** + * 获取单个评查点分组详情 + * @param id 分组ID + * @returns 分组详情 + */ +export async function getRuleGroup(id: string): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { + try { + if (!id) { + return { error: '分组ID不能为空', status: 400 }; + } + + const params: PostgrestParams = { + select: ` + id, + pid, + name, + code, + description, + is_enabled, + created_at + `, + filter: { + 'id': `eq.${id}` + } + }; + + const response = await postgrestGet<{code: number; msg: string; data: Array<{ + id: number; + pid: number; + name: string; + code?: string; + description?: string; + is_enabled: boolean; + created_at?: string; + }>}>('evaluation_point_groups', params); + + if (response.error) { + return { error: response.error, status: response.status }; + } + + let group: RuleGroup | null = null; + + if (response.data && 'code' in response.data && response.data.data && response.data.data.length > 0) { + const apiGroup = response.data.data[0]; + group = { + id: apiGroup.id.toString(), + pid: apiGroup.pid.toString(), + name: apiGroup.name, + code: apiGroup.code, + description: apiGroup.description, + is_enabled: apiGroup.is_enabled, + createdAt: apiGroup.created_at ? formatDate(apiGroup.created_at) : undefined + }; + } else if (Array.isArray(response.data) && response.data.length > 0) { + const apiGroup = response.data[0]; + group = { + id: apiGroup.id.toString(), + pid: apiGroup.pid.toString(), + name: apiGroup.name, + code: apiGroup.code, + description: apiGroup.description, + is_enabled: apiGroup.is_enabled, + createdAt: apiGroup.created_at ? formatDate(apiGroup.created_at) : undefined + }; + } + + if (!group) { + return { error: '未找到指定分组', status: 404 }; + } + + // 如果是父分组,获取评查点数量 + if (group.pid === '0') { + const ruleCountParams: PostgrestParams = { + select: 'id', + filter: { + 'evaluation_point_groups_id': `eq.${group.id}` + } + }; + + const ruleCountResponse = await postgrestGet>>('evaluation_points', ruleCountParams); + + group.ruleCount = ruleCountResponse.data && 'code' in ruleCountResponse.data + ? (ruleCountResponse.data.data && Array.isArray(ruleCountResponse.data.data) ? ruleCountResponse.data.data.length : 0) + : (Array.isArray(ruleCountResponse.data) ? (ruleCountResponse.data as unknown[]).length : 0); + } + + return { data: group }; + } catch (error) { + console.error('获取评查点分组详情失败:', error); + return { + error: error instanceof Error ? error.message : '获取评查点分组详情失败', + status: 500 + }; + } +} + +/** + * 创建评查点分组 + * @param groupData 分组数据 + * @returns 创建的分组 + */ +export async function createRuleGroup(groupData: RuleGroupCreateUpdateDto): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 验证必填字段 + if (!groupData.name || !groupData.code) { + return { error: '分组名称和编码不能为空', status: 400 }; + } + + // 确保 pid 是合法值 + let pidValue: number; + try { + pidValue = groupData.pid ? Number(groupData.pid) : 0; + if (isNaN(pidValue)) { + return { error: '父分组ID必须是有效的数字', status: 400 }; + } + } catch (error) { + console.error('父分组ID转换失败:', error); + return { error: '父分组ID格式错误', status: 400 }; + } + + // 构建API请求数据 - 确保字段类型符合数据库要求 + const apiGroup: ApiRuleGroup = { + pid: pidValue, + name: groupData.name.trim(), + code: groupData.code.trim(), + description: groupData.description || '', + is_enabled: groupData.is_enabled + }; + + console.log('创建评查点分组请求数据:', JSON.stringify(apiGroup, null, 2)); + + // 直接发送到 PostgreSQL 表 + const response = await postgrestPost | ApiRuleGroup, ApiRuleGroup>( + 'evaluation_point_groups', // 表名 + apiGroup + ); + + if (response.error) { + console.error('创建评查点分组API返回错误:', response.error, '状态码:', response.status); + return { error: response.error, status: response.status }; + } + + console.log('创建评查点分组响应数据:', JSON.stringify(response.data, null, 2)); + + // 处理响应数据 - 适配不同的API响应格式 + const apiResponse = extractApiData(response.data); + + if (!apiResponse) { + console.error('创建分组成功但返回数据格式异常:', response.data); + return { error: '创建分组失败,返回数据格式错误', status: 500 }; + } + + // 构建返回对象 + const createdGroup: RuleGroup = { + id: apiResponse.id?.toString() || '', + pid: apiResponse.pid?.toString() || '0', // 兼容没有 pid 的情况 + name: apiResponse.name || '', + code: apiResponse.code?.toString() || '', // 处理可能的数字类型 + description: apiResponse.description, + is_enabled: apiResponse.is_enabled !== undefined ? apiResponse.is_enabled : true, + createdAt: apiResponse.created_at ? formatDate(apiResponse.created_at) : undefined + }; + + return { data: createdGroup }; + } catch (error) { + console.error('创建评查点分组失败:', error); + return { + error: error instanceof Error ? error.message : '创建评查点分组失败', + status: 500 + }; + } +} + +/** + * 更新评查点分组 + * @param id 分组ID + * @param data 更新的分组数据 + * @returns 更新后的分组 + */ +export async function updateRuleGroup(id: string, data: RuleGroupCreateUpdateDto): Promise<{data: RuleGroup; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 使用新的filters参数 + const response = await postgrestPut | RuleGroup, RuleGroupCreateUpdateDto>( + 'evaluation_point_groups', + data, + { id } + ); + + if (response.error) { + return { error: response.error, status: response.status }; + } + + // 使用辅助函数提取数据 + const extractedData = extractApiData(response.data); + + if (!extractedData) { + return { error: '更新成功但未返回数据' }; + } + + return { data: extractedData }; + } catch (error) { + console.error('更新评查点分组失败:', error); + return { + error: error instanceof Error ? error.message : '更新评查点分组失败' + }; + } +} + +/** + * 删除评查点分组 + * @param id 分组ID + * @returns 删除结果 + */ +export async function deleteRuleGroup(id: string): Promise<{success: boolean; error?: string}> { + try { + // 1. 首先获取分组信息,判断是一级还是二级分组 + const groupResponse = await getRuleGroup(id); + if (groupResponse.error) { + return { success: false, error: groupResponse.error }; + } + + const group = groupResponse.data; + if (!group) { + return { success: false, error: '未找到指定分组' }; + } + + // 2. 如果是一级分组,需要先删除所有子分组 + if (group.pid === '0') { + // 获取所有子分组 + const childGroupsResponse = await getChildGroups(id); + if (childGroupsResponse.error) { + return { success: false, error: childGroupsResponse.error }; + } + + const childGroups = childGroupsResponse.data || []; + + // 遍历删除每个子分组 + for (const childGroup of childGroups) { + const deleteChildResult = await deleteChildGroup(childGroup.id); + if (!deleteChildResult.success) { + return deleteChildResult; + } + } + } + + // 3. 删除分组下的所有评查点 + const deletePointsResult = await deleteEvaluationPointsByGroupId(id); + if (!deletePointsResult.success) { + return deletePointsResult; + } + + // 4. 最后删除分组本身 + const response = await postgrestDelete>('evaluation_point_groups', { + filter: { + 'id': `eq.${id}` + } + }); + + if (response.error) { + return { success: false, error: response.error }; + } + + return { success: true }; + } catch (error) { + console.error('删除评查点分组失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '删除评查点分组失败' + }; + } +} + +/** + * 删除子分组及其相关数据 + * @param id 子分组ID + * @returns 删除结果 + */ +async function deleteChildGroup(id: string): Promise<{success: boolean; error?: string}> { + try { + // 1. 删除子分组下的所有评查点 + const deletePointsResult = await deleteEvaluationPointsByGroupId(id); + if (!deletePointsResult.success) { + return deletePointsResult; + } + + // 2. 删除子分组本身 + const response = await postgrestDelete>('evaluation_point_groups', { + filter: { + 'id': `eq.${id}` + } + }); + + if (response.error) { + return { success: false, error: response.error }; + } + + return { success: true }; + } catch (error) { + console.error('删除子分组失败:', error); + return { + success: false, + error: error instanceof Error ? error.message : '删除子分组失败' + }; + } +} + +/** + * 删除指定分组下的所有评查点 + * @param groupId 分组ID + * @returns 删除结果 + */ +async function deleteEvaluationPointsByGroupId(groupId: string): Promise<{success: boolean; error?: string}> { + try { + const response = await postgrestDelete>('evaluation_points', { + filter: { + 'evaluation_point_groups_id': `eq.${groupId}` + } + }); + + if (response.error) { + return { success: false, error: response.error }; + } + + return { success: true }; + } 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.ts b/app/api/files.ts new file mode 100644 index 0000000..c7feaed --- /dev/null +++ b/app/api/files.ts @@ -0,0 +1,54 @@ +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 : '上传失败' }; + } +} \ No newline at end of file diff --git a/app/api/postgrest-client.ts b/app/api/postgrest-client.ts index 0727e41..dbf6d38 100644 --- a/app/api/postgrest-client.ts +++ b/app/api/postgrest-client.ts @@ -21,8 +21,9 @@ export interface PostgrestParams { * 打印 PostgREST 查询日志 * @param endpoint 端点 * @param params 参数 + * @param method HTTP 方法 */ -function logPostgrestQuery(endpoint: string, params?: QueryParams): void { +function logPostgrestQuery(endpoint: string, params?: QueryParams, method: string = 'GET'): void { if (process.env.NODE_ENV !== 'production') { // const baseUrl = 'http://172.16.0.119:9000/admin'; // const baseUrl = 'http://172.18.0.100:3000'; @@ -32,6 +33,7 @@ function logPostgrestQuery(endpoint: string, params?: QueryParams): void { const normalizedEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; console.log('\n📦 PostgREST 查询日志 ========================'); + console.log(`📦 HTTP 方法: ${method}`); console.log(`📦 API 端点: ${baseUrl}/${normalizedEndpoint}`); if (params && Object.keys(params).length > 0) { @@ -74,7 +76,7 @@ function logPostgrestQuery(endpoint: string, params?: QueryParams): void { console.log(`\n📦 可读URL: ${baseUrl}/${normalizedEndpoint}${readableQueryString ? '?' + readableQueryString : ''}`); // 格式化查询为 PostgreSQL 风格的查询 - let postgrestQuery = `SELECT `; + let postgrestQuery = `${method.toUpperCase()} `; if (params.select && typeof params.select === 'string') { postgrestQuery += params.select.replace(/\s+/g, ' ').trim(); @@ -251,7 +253,7 @@ export async function postgrestGet(endpoint: string, params?: PostgrestParams const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 打印查询信息 - logPostgrestQuery(apiEndpoint, queryParams); + logPostgrestQuery(apiEndpoint, queryParams, 'GET'); // 提取并移除自定义头部参数 const headers: Record = params?.headers || {}; @@ -285,9 +287,96 @@ export async function postgrestGet(endpoint: string, params?: PostgrestParams } } + /** + * 处理 PostgreSQL 特定错误 + * @param error 错误对象或消息 + * @param responseText 原始响应文本 + * @returns 格式化的错误信息 + */ +function handlePostgresError(error: unknown, responseText?: string): { message: string, status?: number } { + let errorMessage = error instanceof Error ? error.message : String(error); + let statusCode: number | undefined = undefined; + + // 如果有原始响应文本,尝试从中提取更详细的错误信息 + if (responseText) { + try { + const errorData = JSON.parse(responseText); + + // 处理 PostgreSQL 错误码 + if (errorData.code) { + switch (errorData.code) { + case '23505': // 唯一约束冲突 + errorMessage = '记录已存在,无法创建重复数据'; + if (errorData.details) { + // 尝试提取冲突的字段名 + const match = errorData.details.match(/Key \((.+?)\)=/); + if (match && match[1]) { + const field = match[1]; + errorMessage = `${field} 已存在,请使用不同的值`; + } + } + statusCode = 409; // Conflict + break; + + case '23503': // 外键约束失败 + errorMessage = '引用的记录不存在'; + if (errorData.details) { + // 尝试提取外键字段 + const match = errorData.details.match(/Key \((.+?)\)=/); + if (match && match[1]) { + const field = match[1]; + errorMessage = `所引用的 ${field} 不存在或无效`; + } + } + statusCode = 400; // Bad Request + break; + + case '42P01': // 表不存在 + errorMessage = '所请求的数据表不存在'; + statusCode = 404; // Not Found + break; + + case '42703': // 列不存在 + errorMessage = '所引用的字段不存在'; + if (errorData.details) { + const match = errorData.details.match(/column "(.+?)"/); + if (match && match[1]) { + const column = match[1]; + errorMessage = `字段 "${column}" 不存在`; + } + } + statusCode = 400; // Bad Request + break; + + default: + // 使用 PostgreSQL 提供的错误消息 + if (errorData.message) { + errorMessage = errorData.message; + } + break; + } + } + + // 处理 HTTP 状态码 + if (errorData.status) { + statusCode = errorData.status; + } + + } catch (e) { + console.error('解析错误响应失败:', e); + // 如果解析失败,尝试直接使用响应文本 + if (responseText && responseText.length < 200) { + errorMessage = responseText; + } + } + } + + return { message: errorMessage, status: statusCode }; +} + /** * 发送 POST 请求到 PostgresREST 接口 - * @param endpoint 端点 + * @param endpoint 端点(表名) * @param data 请求体数据 * @returns 响应数据 */ @@ -297,46 +386,143 @@ export async function postgrestPost>(endpoint: st const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; // 打印查询信息(POST请求只打印端点) - logPostgrestQuery(apiEndpoint); + logPostgrestQuery(apiEndpoint, undefined, 'POST'); - const response = await apiRequest( - apiEndpoint, - { - method: 'POST', - body: JSON.stringify(data), - headers: { - 'Prefer': 'return=representation' + // 预处理数据,确保所有字段类型符合 PostgreSQL 要求 + const processedData = preprocessData(data as Record); + + // 确保数据是合法的JSON对象 + const requestBody = JSON.stringify(processedData); + console.log(`准备发送 PostgreSQL 插入请求到: ${apiEndpoint}`); + console.log(`请求体: ${requestBody}`); + + try { + const response = await apiRequest( + apiEndpoint, + { + method: 'POST', + body: requestBody, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'Prefer': 'return=representation' + } } + ); + + if (response.error) { + console.error(`POST请求失败: ${response.error}`); + throw new Error(response.error); } - ); - - if (response.error) { - throw new Error(response.error); + + console.log(`POST请求成功,响应: `, response.data); + return { data: response.data as T }; + } catch (error) { + // 捕获并处理 API 请求错误 + let errorText = ''; + + // 如果错误对象中包含响应文本,尝试提取更详细的错误信息 + if (error instanceof Error && 'responseText' in error) { + errorText = (error as {responseText: string}).responseText; + } + + // 处理 PostgreSQL 特定错误 + const pgError = handlePostgresError(error, errorText); + + console.error(`POST请求错误: ${pgError.message}`); + return { + error: pgError.message, + status: pgError.status || 500 + }; } - - return { data: response.data as T }; } catch (error) { + console.error(`POST请求处理错误: ${error instanceof Error ? error.message : String(error)}`); const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; } } +/** + * 预处理数据,确保类型与 PostgreSQL 兼容 + * @param data 原始数据 + * @returns 处理后的数据 + */ +function preprocessData(data: Record): Record { + const processed: Record = {}; + + for (const [key, value] of Object.entries(data)) { + // 确保 null 值被正确处理 + if (value === null) { + processed[key] = null; + continue; + } + + // 处理字符串可能被错误解析为数字的情况 + if (typeof value === 'string' && /^\d+$/.test(value) && key !== 'code' && !key.endsWith('_id')) { + // 对于可能需要保持字符串格式的值,我们不进行数字转换 + processed[key] = value; + } + // 将字符串 'true'/'false' 转为布尔值 + else if (typeof value === 'string' && (value.toLowerCase() === 'true' || value.toLowerCase() === 'false')) { + processed[key] = value.toLowerCase() === 'true'; + } + // 尝试转换 '0'/'1' 为 boolean (如果字段名暗示是布尔值) + else if (typeof value === 'string' && (value === '0' || value === '1') && + (key.startsWith('is_') || key.startsWith('has_') || key.endsWith('_enabled'))) { + processed[key] = value === '1'; + } + // 对于ID字段确保使用正确的类型 + else if ((key === 'id' || key.endsWith('_id') || key === 'pid') && value !== undefined) { + try { + const numValue = Number(value); + if (!isNaN(numValue)) { + processed[key] = numValue; + } else { + processed[key] = value; + } + } catch { + processed[key] = value; + } + } + // 其他值保持不变 + else { + processed[key] = value; + } + } + + return processed; +} + /** * 发送 PUT 请求到 PostgresREST 接口 * @param endpoint 端点 * @param data 请求体数据 + * @param filters 过滤条件 * @returns 响应数据 */ -export async function postgrestPut>(endpoint: string, data: D): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { +export async function postgrestPut( + endpoint: string, + data: D, + filters?: Record +): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; + // 构建完整的URL,包含过滤条件 + let fullEndpoint = apiEndpoint; + if (filters) { + const filterString = Object.entries(filters) + .map(([key, value]) => `${key}=eq.${value}`) + .join('&'); + fullEndpoint = `${apiEndpoint}?${filterString}`; + } + // 打印查询信息(PUT请求只打印端点) - logPostgrestQuery(apiEndpoint); + logPostgrestQuery(fullEndpoint, undefined, 'PATCH'); const response = await apiRequest( - apiEndpoint, + fullEndpoint, { method: 'PATCH', body: JSON.stringify(data), @@ -350,7 +536,11 @@ export async function postgrestPut>(endpoint: str throw new Error(response.error); } - return { data: response.data as T }; + if (!response.data) { + throw new Error('更新成功但未返回数据'); + } + + return { data: response.data }; } catch (error) { const apiError = handleApiError(error); return { error: apiError.message, status: apiError.status }; @@ -383,7 +573,7 @@ export async function postgrestDelete(endpoint: string, params?: PostgrestPar } // 打印查询信息 - logPostgrestQuery(apiEndpoint, queryParams); + logPostgrestQuery(apiEndpoint, queryParams, 'DELETE'); const response = await apiRequest( apiEndpoint, diff --git a/app/api/system_setting/config-lists.ts b/app/api/system_setting/config-lists.ts new file mode 100644 index 0000000..e69de29 diff --git a/app/components/ui/UploadArea.tsx b/app/components/ui/UploadArea.tsx index 1f1837b..84c01a9 100644 --- a/app/components/ui/UploadArea.tsx +++ b/app/components/ui/UploadArea.tsx @@ -12,6 +12,7 @@ interface UploadAreaProps { mainText?: string; tipText?: ReactNode; disabled?: boolean; + shouldPreventFileSelect?: boolean; } export interface UploadAreaRef { @@ -31,7 +32,8 @@ export const UploadArea = forwardRef(({ buttonText = "选择文件", mainText = "点击或拖拽文件到此区域上传", tipText = "", - disabled = false + disabled = false, + shouldPreventFileSelect = false }, ref) => { const fileInputRef = useRef(null); const [isDragOver, setIsDragOver] = useState(false); @@ -48,10 +50,10 @@ export const UploadArea = forwardRef(({ })); const handleClick = useCallback(() => { - if (!disabled && fileInputRef.current) { + if (!disabled && !shouldPreventFileSelect && fileInputRef.current) { fileInputRef.current.click(); } - }, [disabled]); + }, [disabled, shouldPreventFileSelect]); const handleFileChange = useCallback(() => { if (fileInputRef.current?.files?.length) { @@ -61,10 +63,10 @@ export const UploadArea = forwardRef(({ const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); - if (!disabled) { + if (!disabled && !shouldPreventFileSelect) { setIsDragOver(true); } - }, [disabled]); + }, [disabled, shouldPreventFileSelect]); const handleDragLeave = useCallback(() => { setIsDragOver(false); @@ -74,17 +76,17 @@ export const UploadArea = forwardRef(({ e.preventDefault(); setIsDragOver(false); - if (!disabled && e.dataTransfer.files.length > 0) { + if (!disabled && !shouldPreventFileSelect && e.dataTransfer.files.length > 0) { onFilesSelected(e.dataTransfer.files); } - }, [disabled, onFilesSelected]); + }, [disabled, shouldPreventFileSelect, onFilesSelected]); const handleKeyDown = useCallback((e: React.KeyboardEvent) => { - if ((e.key === 'Enter' || e.key === ' ') && !disabled) { + if ((e.key === 'Enter' || e.key === ' ') && !disabled && !shouldPreventFileSelect) { e.preventDefault(); handleClick(); } - }, [handleClick, disabled]); + }, [handleClick, disabled, shouldPreventFileSelect]); return (
{ // 文件类型定义 export enum FileType { - CONTRACT = "contract", - LICENSE = "license", - PUNISHMENT = "punishment", - OTHER = "other" + CONTRACT = "1", + LICENSE = "2", + PUNISHMENT = "3", + OTHER = "4" } // 文件类型标签映射 @@ -59,6 +59,13 @@ export const PRIORITY_LABELS: Record = { [Priority.URGENT]: "紧急" }; +// 优先级中文映射 +const PRIORITY_TO_CHINESE: Record = { + [Priority.NORMAL]: "普通", + [Priority.HIGH]: "优先", + [Priority.URGENT]: "紧急" +}; + // 处理状态定义 export enum ProcessingStatus { WAITING = "waiting", @@ -128,46 +135,62 @@ async function uploadFileToServer( await new Promise(resolve => setTimeout(resolve, 500)); try { - // 创建HTTP请求的参数(实际环境中会使用这些参数发送请求) + // 创建FormData对象,将文件和其他信息一起提交 + const formData = new FormData(); + + // 将二进制数据转换为Blob并添加到FormData + const blob = new Blob([binaryData], { type: fileType }); + formData.append('file', blob, fileName); + + // 将 type_id 和 priority 添加到一个JSON对象中 + const uploadInfo = { + type_id: Number(documentType), // 确保 type_id 是数字值 + evaluation_level: PRIORITY_TO_CHINESE[priority] // 转换为中文优先级 + }; + + // 添加 JSON 字符串到 FormData + formData.append('upload_info', JSON.stringify(uploadInfo)); + + // 创建HTTP请求的参数 const requestParams = { method: 'POST', url: 'http://172.16.0.55:8000/admin/documents/upload', headers: { - 'Content-Type': 'application/octet-stream', - 'X-File-Name': encodeURIComponent(fileName), - 'X-File-Type': fileType, - 'X-Document-Type': documentType, - 'X-Priority': priority + // FormData会自动设置Content-Type为multipart/form-data + 'X-File-Name': encodeURIComponent(fileName) }, - body: binaryData // 二进制数据作为请求体 + body: formData }; + // 打印 FormData 内容的正确方式 console.log('[模拟API] 请求参数:', { url: requestParams.url, headers: requestParams.headers, - bodySize: binaryData.byteLength + uploadInfo, // 直接打印原始对象更有帮助 + formDataEntries: Array.from(formData.entries()).map(([key, value]) => { + if (!(value instanceof Blob)) { + return { key, value }; + } + }), + fileName }); // 实际API调用 - 在生产环境中实现 const response = await fetch(requestParams.url, { method: requestParams.method, headers: requestParams.headers, - body: binaryData + body: requestParams.body }); if (!response.ok) { + // 获取更多错误信息 + const errorText = await response.text(); + console.error(`上传失败 (${response.status}): ${errorText}`); throw new Error(`上传失败: ${response.status} ${response.statusText}`); } const data = await response.json(); return data; - - // 模拟成功响应 (实际应用中这是服务器返回的) - return { - success: true, - fileId: `file_${Date.now()}`, - message: '文件上传成功' - }; } catch (error) { console.error('[模拟API] 上传错误:', error); return { @@ -344,7 +367,8 @@ export default function FilesUpload() { } setCurrentFile(selectedFiles[0]); - // 不再立即上传,而是等待表单提交 + // 立即开始上传 + startUpload(selectedFiles[0]); }, [fileType]); // 开始上传文件 @@ -410,6 +434,25 @@ export default function FilesUpload() { setUploadProgress(100); setUploadSpeed("完成"); + // 创建新的文件对象并添加到队列 + const newFile: UploadedFile = { + id: `file_${Date.now()}`, + name: file.name, + size: file.size, + type: file.type, + fileType: fileType as FileType, + priority, + status: ProcessingStatus.PROCESSING, + uploadTime: getCurrentTime(), + processingInfo: { + progress: 0, + currentStep: 0 + } + }; + + // 添加到队列中 + setQueueFiles(prev => [newFile, ...prev]); + // 完成上传后开始处理流程 startProcessing(file); } catch (error) { @@ -699,10 +742,10 @@ export default function FilesUpload() { disabled={uploadStage !== "idle"} > - - - - + + + +
不同类型的文档将应用不同的审核规则
@@ -716,9 +759,9 @@ export default function FilesUpload() { onChange={(e) => setPriority(e.target.value as Priority)} disabled={uploadStage !== "idle"} > - - - + + +
优先级影响文档在队列中的处理顺序
@@ -736,19 +779,8 @@ export default function FilesUpload() { multiple={false} accept=".pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png" tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片" + shouldPreventFileSelect={!fileType} /> - - {currentFile && ( -
- -
- )} )} diff --git a/app/routes/rule-groups._index.tsx b/app/routes/rule-groups._index.tsx index 000b468..99a6371 100644 --- a/app/routes/rule-groups._index.tsx +++ b/app/routes/rule-groups._index.tsx @@ -1,4 +1,4 @@ -import { json, type MetaFunction } from "@remix-run/node"; +import { type MetaFunction } from "@remix-run/node"; import { useLoaderData, Link, useNavigate, useSearchParams } from "@remix-run/react"; import { useState, useEffect } from "react"; import indexStyles from "~/styles/pages/rule-groups_index.css?url"; @@ -8,7 +8,7 @@ import { StatusDot } from "~/components/ui/StatusDot"; import { Table } from "~/components/ui/Table"; import { FilterPanel, FilterSelect, SearchFilter } from "~/components/ui/FilterPanel"; import { Pagination } from "~/components/ui/Pagination"; -import { getRuleGroups, getChildGroups, type RuleGroup } from "~/api/evaluation_points/rule-groups"; +import { getRuleGroups, getChildGroups, type RuleGroup, deleteRuleGroup } from "~/api/evaluation_points/rule-groups"; export function links() { return [{ rel: "stylesheet", href: indexStyles }]; @@ -27,10 +27,10 @@ export async function loader() { if (response.error) { throw new Error(response.error); } - return json({ groups: response.data }); + return Response.json({ groups: response.data }); } catch (error) { console.error('加载评查点分组失败:', error); - return json({ groups: [] }); + return Response.json({ groups: [] }); } } @@ -39,8 +39,72 @@ export default function RuleGroupsIndex() { const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); const [expandedGroups, setExpandedGroups] = useState([]); - const [groups, setGroups] = useState(initialGroups); + const [groups, setGroups] = useState(initialGroups || []); const [loading, setLoading] = useState>({}); + const [filteredChildrenMap, setFilteredChildrenMap] = useState>({}); + const [initialLoading, setInitialLoading] = useState(true); + + // 初始加载时自动加载所有子分组 + useEffect(() => { + const loadAllChildGroups = async () => { + if (!initialGroups || initialGroups.length === 0) { + setInitialLoading(false); + return; + } + + try { + // 创建一个加载状态对象,标记所有父分组正在加载 + const loadingState: Record = {}; + initialGroups.forEach((group: RuleGroup) => { + loadingState[group.id] = true; + }); + setLoading(loadingState); + + // 并行加载所有父分组的子分组 + const promises = initialGroups.map(async (group: RuleGroup) => { + try { + const response = await getChildGroups(group.id); + if (response.error) { + console.error(`加载分组 ${group.id} 的子分组失败:`, response.error); + return { parentId: group.id, children: [] }; + } + return { parentId: group.id, children: response.data }; + } catch (error) { + console.error(`加载分组 ${group.id} 的子分组出错:`, error); + return { parentId: group.id, children: [] }; + } + }); + + const results = await Promise.all(promises); + + // 更新分组数据 + setGroups(prev => { + const newGroups = [...prev]; + results.forEach(({ parentId, children }) => { + const parentIndex = newGroups.findIndex(g => g.id === parentId); + if (parentIndex !== -1) { + newGroups[parentIndex] = { + ...newGroups[parentIndex], + children + }; + } + }); + return newGroups; + }); + + // 展开所有父分组 + setExpandedGroups(initialGroups.map((group: RuleGroup) => group.id)); + } catch (error) { + console.error('自动加载所有子分组失败:', error); + } finally { + // 清除所有加载状态 + setLoading({}); + setInitialLoading(false); + } + }; + + loadAllChildGroups(); + }, [initialGroups]); // 处理展开/收起 const toggleGroup = async (groupId: string) => { @@ -53,6 +117,26 @@ export default function RuleGroupsIndex() { // 展开分组 setLoading(prev => ({ ...prev, [groupId]: true })); try { + const parentGroup = groups.find(g => g.id === groupId); + + // 如果已经加载过子分组,直接展开 + if (parentGroup && parentGroup.children && parentGroup.children.length > 0) { + setExpandedGroups(prev => [...prev, groupId]); + + // 应用现有的过滤条件到已加载的子分组 + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + + if ((nameFilter || codeFilter || statusFilter) && parentGroup.children) { + applyFiltersToChildren(groupId, parentGroup.children); + } + + setLoading(prev => ({ ...prev, [groupId]: false })); + return; + } + + // 否则加载子分组 const response = await getChildGroups(groupId); if (response.error) { throw new Error(response.error); @@ -70,6 +154,15 @@ export default function RuleGroupsIndex() { })); setExpandedGroups(prev => [...prev, groupId]); + + // 应用现有的过滤条件到新加载的子分组 + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + + if ((nameFilter || codeFilter || statusFilter) && response.data) { + applyFiltersToChildren(groupId, response.data); + } } catch (error) { console.error('加载子分组失败:', error); } finally { @@ -83,23 +176,46 @@ export default function RuleGroupsIndex() { // 展开所有分组 const expandedIds = groups.map(g => g.id); setExpandedGroups(expandedIds); - - // 加载所有子分组 - for (const group of groups) { - if (!group.children || group.children.length === 0) { - await toggleGroup(group.id); - } - } } else { setExpandedGroups([]); } }; // 处理删除分组 - const handleDeleteGroup = (groupId: string) => { + const handleDeleteGroup = async (groupId: string) => { if (confirm("确定要删除该分组吗?此操作将同时删除该分组下的所有评查点,且不可恢复。")) { - console.log('删除分组ID:', groupId); - // TODO: 实现删除分组的API调用 + try { + const result = await deleteRuleGroup(groupId); + if (result.success) { + // 从本地状态中移除被删除的分组 + setGroups(prev => { + // 如果是一级分组,直接过滤掉 + const filteredGroups = prev.filter(g => g.id !== groupId); + + // 如果是二级分组,需要从父分组的 children 中移除 + return filteredGroups.map(group => { + if (group.children) { + return { + ...group, + children: group.children.filter(child => child.id !== groupId) + }; + } + return group; + }); + }); + + // 如果被删除的分组当前是展开状态,从展开列表中移除 + setExpandedGroups(prev => prev.filter(id => id !== groupId)); + + // 显示成功消息 + alert('删除成功'); + } else { + alert(`删除失败: ${result.error}`); + } + } catch (error) { + console.error('删除分组失败:', error); + alert('删除分组失败,请稍后重试'); + } } }; @@ -132,9 +248,9 @@ export default function RuleGroupsIndex() { const { value } = e.target; const newParams = new URLSearchParams(searchParams); if (value) { - newParams.set('status', value); + newParams.set('is_enabled', value); } else { - newParams.delete('status'); + newParams.delete('is_enabled'); } newParams.set('page', '1'); setSearchParams(newParams); @@ -143,10 +259,145 @@ export default function RuleGroupsIndex() { // 处理重置筛选 const handleReset = () => { setSearchParams(new URLSearchParams()); + setFilteredChildrenMap({}); + + // 清空输入框内容(通过DOM操作) + const nameInput = document.querySelector('input[placeholder="请输入分组名称"]') as HTMLInputElement; + const codeInput = document.querySelector('input[placeholder="请输入分组编码"]') as HTMLInputElement; + const statusSelect = document.querySelector('select[name="is_enabled"]') as HTMLSelectElement; + + if (nameInput) nameInput.value = ''; + if (codeInput) codeInput.value = ''; + if (statusSelect) statusSelect.value = ''; }; + // 应用筛选条件到子分组 + const applyFiltersToChildren = (parentId: string, children: RuleGroup[]) => { + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + + + if (!nameFilter && !codeFilter && !statusFilter) { + setFilteredChildrenMap(prev => ({...prev, [parentId]: []})); + return; + } + + const filteredChildren = children.filter(child => { + // 筛选名称 + if (nameFilter && !child.name.toLowerCase().includes(nameFilter.toLowerCase())) { + return false; + } + + // 筛选编码 + if (codeFilter && (!child.code || !child.code.toLowerCase().includes(codeFilter.toLowerCase()))) { + return false; + } + + // 筛选状态 + if (statusFilter) { + const isEnabled = statusFilter === 'true'; + if (child.is_enabled !== isEnabled) { + return false; + } + } + + return true; + }); + + setFilteredChildrenMap(prev => ({...prev, [parentId]: filteredChildren})); + }; + + // 当筛选条件变化时,重新计算过滤结果 + useEffect(() => { + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + + if (!nameFilter && !codeFilter && !statusFilter) { + setFilteredChildrenMap({}); + return; + } + + // 检查所有已加载的子分组 + groups.forEach(group => { + if (group.children && group.children.length > 0) { + applyFiltersToChildren(group.id, group.children); + } + }); + + // 查找匹配的子分组,自动展开它们的父级 + const parentsToExpand: string[] = []; + + groups.forEach(group => { + if (group.children && group.children.length > 0) { + const hasMatchingChild = group.children.some(child => { + const nameMatch = !nameFilter || child.name.toLowerCase().includes(nameFilter.toLowerCase()); + const codeMatch = !codeFilter || (child.code && child.code.toLowerCase().includes(codeFilter.toLowerCase())); + const statusMatch = !statusFilter || (statusFilter === 'true' ? child.is_enabled : !child.is_enabled); + + return nameMatch && codeMatch && statusMatch; + }); + + if (hasMatchingChild && !expandedGroups.includes(group.id)) { + parentsToExpand.push(group.id); + } + } + }); + + if (parentsToExpand.length > 0) { + setExpandedGroups(prev => [...prev, ...parentsToExpand]); + } + }, [searchParams, groups]); + + // 检查父级分组是否匹配筛选条件 + const parentMatchesFilter = (group: RuleGroup): boolean => { + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + + // 筛选名称 + if (nameFilter && !group.name.toLowerCase().includes(nameFilter.toLowerCase())) { + return false; + } + + // 筛选编码 + if (codeFilter && (!group.code || !group.code.toLowerCase().includes(codeFilter.toLowerCase()))) { + return false; + } + + // 筛选状态 + if (statusFilter) { + const isEnabled = statusFilter === 'true'; + if (group.is_enabled !== isEnabled) { + return false; + } + } + + return true; + }; + + // 检查父级分组是否有匹配的子分组 + const hasMatchingChildren = (parentId: string): boolean => { + const filteredChildren = filteredChildrenMap[parentId]; + return filteredChildren && filteredChildren.length > 0; + }; + // 处理表格数据,包括父子级关系 const processedData = groups.flatMap(group => { + const parentMatches = parentMatchesFilter(group); + const hasMatched = hasMatchingChildren(group.id); + + // 如果有筛选条件但父级和子级都不匹配,则不显示 + const nameFilter = searchParams.get('name') || ''; + const codeFilter = searchParams.get('code') || ''; + const statusFilter = searchParams.get('is_enabled') || ''; + const hasFilters = nameFilter || codeFilter || statusFilter; + + if (hasFilters && !parentMatches && !hasMatched) { + return []; + } + // 先添加父级分组 const result: (RuleGroup & { isParent?: boolean, parentId?: string })[] = [ { ...group, isParent: true } @@ -154,14 +405,39 @@ export default function RuleGroupsIndex() { // 如果有子级分组并且当前已展开,则添加子级分组 if (group.children && expandedGroups.includes(group.id)) { - group.children.forEach(child => { - result.push({ ...child, parentId: group.id }); - }); + // 如果有筛选条件并且有过滤后的子分组,则显示过滤后的子分组 + if (hasFilters && filteredChildrenMap[group.id]) { + filteredChildrenMap[group.id].forEach(child => { + result.push({ ...child, parentId: group.id }); + }); + } + // 否则显示所有子分组 + else if (!hasFilters) { + group.children.forEach(child => { + result.push({ ...child, parentId: group.id }); + }); + } } return result; }); + // 计算一级分组的评查点数量(累加其所有二级分组的评查点数量) + const calculateTotalRuleCount = (record: RuleGroup & { isParent?: boolean, parentId?: string }) => { + // 如果是二级分组,直接返回其评查点数量 + if (!record.isParent) { + return record.ruleCount || 0; + } + + // 如果是一级分组,累加其所有子分组的评查点数量 + let totalCount = 0; + if (record.children && record.children.length > 0) { + totalCount = record.children.reduce((sum, child) => sum + (child.ruleCount || 0), 0); + } + + return totalCount; + }; + // 定义表格列配置 const columns = [ { @@ -205,24 +481,26 @@ export default function RuleGroupsIndex() { { title: "分组编码", key: "code", - render: (_: unknown, record: RuleGroup) => record.code + render: (_: unknown, record: RuleGroup) => ( + {record.code || '-'} + ) }, { title: "评查点数量", key: "ruleCount", - render: (_: unknown, record: RuleGroup) => ( + render: (_: unknown, record: RuleGroup & { isParent?: boolean, parentId?: string }) => ( - {record.ruleCount || 0} + {calculateTotalRuleCount(record)} ) }, { title: "状态", - key: "status", + key: "is_enabled", render: (_: unknown, record: RuleGroup) => ( ) @@ -230,7 +508,9 @@ export default function RuleGroupsIndex() { { title: "创建时间", key: "createdAt", - render: (_: unknown, record: RuleGroup) => record.createdAt + render: (_: unknown, record: RuleGroup) => ( + {record.createdAt || '-'} + ) }, { title: "操作", @@ -296,9 +576,9 @@ export default function RuleGroupsIndex() { - + */} } noActionDivider={true} @@ -323,8 +603,8 @@ export default function RuleGroupsIndex() { - - - {/* 分页 - 使用Pagination组件 */} - {}} - showTotal={true} - /> + {initialLoading ? ( +
+ + 正在加载分组数据... +
+ ) : ( + <> +
+ + {/* 分页 - 使用Pagination组件 */} + {}} + showTotal={true} + /> + + )} ); diff --git a/app/routes/rule-groups.new.tsx b/app/routes/rule-groups.new.tsx index e0332a0..cc61991 100644 --- a/app/routes/rule-groups.new.tsx +++ b/app/routes/rule-groups.new.tsx @@ -5,6 +5,14 @@ import { useEffect, useState } from "react"; import { Button } from "~/components/ui/Button"; import { Card } from "~/components/ui/Card"; import ruleGroupsNewStyles from "~/styles/pages/rule-groups_new.css?url"; +import { + getRuleGroups, + getRuleGroup, + createRuleGroup, + updateRuleGroup, + type RuleGroup as ApiRuleGroup, + type RuleGroupCreateUpdateDto +} from "~/api/evaluation_points/rule-groups"; // 类型定义 interface RuleGroup { @@ -65,6 +73,19 @@ export const meta: MetaFunction = ({ location }) => { ]; }; +// 将API分组转换为前端分组模型 +function mapApiToFrontend(apiGroup: ApiRuleGroup): RuleGroup { + return { + id: apiGroup.id, + name: apiGroup.name, + code: apiGroup.code || '', + description: apiGroup.description, + status: apiGroup.is_enabled ? 'active' : 'inactive', + parentId: apiGroup.pid === '0' ? null : apiGroup.pid, + sortOrder: 0 // API中不存在sortOrder字段,使用默认值 + }; +} + // 数据加载器 export async function loader({ request }: LoaderFunctionArgs) { console.log("rule-groups.new loader被调用,URL:", request.url); @@ -74,85 +95,48 @@ export async function loader({ request }: LoaderFunctionArgs) { console.log("获取到的ID参数:", id); // 获取一级分组列表 (用于选择父级分组) - const parentGroups: ParentGroup[] = [ - { id: "1", name: "合同基本要素检查" }, - { id: "4", name: "销售合同专项检查" }, - { id: "5", name: "行政处罚规范性检查" } - ]; - - // 简化这里,初始时不获取数据,避免可能的错误 - let group: RuleGroup | undefined = undefined; - - // 如果有ID才尝试获取数据 - if (id) { - // 简化的模拟数据 - const demoData: Record = { - "1": { - id: "1", - name: "合同基本要素检查", - code: "contract-base", - description: "检查合同基本要素是否完整规范", - status: "active", - parentId: null, - sortOrder: 0 - }, - "2": { - id: "2", - name: "必备要素检查", - code: "essential-elements", - description: "检查合同中的必要信息项是否齐全", - status: "active", - parentId: "1", - sortOrder: 1 - }, - "3": { - id: "3", - name: "合同主体检查", - code: "contract-parties", - description: "检查合同主体信息是否规范", - status: "active", - parentId: "1", - sortOrder: 2 - }, - "4": { - id: "4", - name: "销售合同专项检查", - code: "contract-sales", - description: "针对销售合同的专项合规检查", - status: "active", - parentId: null, - sortOrder: 3 - }, - "5": { - id: "5", - name: "行政处罚规范性检查", - code: "punishment", - description: "对行政处罚文书的合规性检查", - status: "inactive", - parentId: null, - sortOrder: 4 - } - }; - - group = demoData[id]; - console.log("找到的group数据:", group); + const parentGroupsResponse = await getRuleGroups(); + if (parentGroupsResponse.error) { + console.error("获取父分组列表失败:", parentGroupsResponse.error); + throw new Error(parentGroupsResponse.error); } - // 返回简化的数据结构 - return json({ + const parentGroups: ParentGroup[] = parentGroupsResponse.data ? parentGroupsResponse.data.map(group => ({ + id: group.id, + name: group.name + })) : []; + + // 初始化分组数据 + let group: RuleGroup | undefined = undefined; + + // 如果有ID,获取分组详情 + if (id) { + const groupResponse = await getRuleGroup(id); + if (groupResponse.error) { + console.error("获取分组详情失败:", groupResponse.error); + throw new Error(groupResponse.error); + } + + if (groupResponse.data) { + group = mapApiToFrontend(groupResponse.data); + } + } + + // 返回加载的数据 + return Response.json({ group, parentGroups, isEdit: !!group, - error: undefined // 显式返回error字段,无错误时为undefined + error: undefined }); } catch (error) { console.error("loader函数出错:", error); // 返回一个基本的响应,避免500错误 - return json({ + return Response.json({ group: undefined, parentGroups: [], isEdit: false, - error: "加载数据时出错" + error: error instanceof Error ? error.message : "加载数据时出错" }); } } @@ -169,7 +153,6 @@ export async function action({ request }: ActionFunctionArgs) { const status = formData.get("status") as string || "active"; const groupType = formData.get("groupType") as string; const parentId = groupType === "secondary" ? formData.get("parentId") as string : null; - const sortOrder = parseInt(formData.get("sortOrder") as string || "0", 10); // 表单验证 const errors: ActionData["errors"] = {}; @@ -180,8 +163,8 @@ export async function action({ request }: ActionFunctionArgs) { if (!code || code.trim() === "") { errors.code = "分组编码不能为空"; - } else if (!/^[a-zA-Z0-9-]+$/.test(code)) { - errors.code = "分组编码只能包含字母、数字和连字符"; + } else if (!/^[a-zA-Z0-9-_]+$/.test(code)) { + errors.code = "分组编码只能包含字母、数字、连字符和下划线"; } if (groupType === "secondary" && (!parentId || parentId.trim() === "")) { @@ -189,38 +172,41 @@ export async function action({ request }: ActionFunctionArgs) { } if (Object.keys(errors).length > 0) { - return json({ + return Response.json({ errors, values: Object.fromEntries(formData) as Record }); } // 构建保存数据 - const saveData = { - id, + const saveData: RuleGroupCreateUpdateDto = { name: name.trim(), code: code.trim(), description: description?.trim() || "", - status, - parentId, - sortOrder + is_enabled: status === "active", + pid: parentId === null ? "0" : parentId }; try { - // 实际应用中应调用API - console.log("保存分组数据:", saveData); + // 根据是否有ID决定是创建还是更新 + let response; + if (id) { + response = await updateRuleGroup(id, saveData); + } else { + response = await createRuleGroup(saveData); + } - // const response = await fetch(`${process.env.API_BASE_URL}/api/rule-groups${id ? `/${id}` : ''}`, { - // method: id ? "PUT" : "POST", - // headers: { - // "Content-Type": "application/json", - // }, - // body: JSON.stringify(saveData), - // }); - // - // if (!response.ok) { - // throw new Error(`保存失败: ${response.status}`); - // } + // 处理API响应 + if (response.error) { + console.error("保存分组失败:", response.error); + return json({ + success: false, + errors: { + general: response.error + }, + values: Object.fromEntries(formData) as Record + }); + } // 保存成功,重定向到列表页 return redirect("/rule-groups"); @@ -229,7 +215,7 @@ export async function action({ request }: ActionFunctionArgs) { return json({ success: false, errors: { - general: "保存分组失败,请稍后重试" + general: error instanceof Error ? error.message : "保存分组失败,请稍后重试" }, values: Object.fromEntries(formData) as Record }); @@ -384,11 +370,13 @@ export default function RuleGroupNew() { defaultValue={group?.parentId || ""} > - {parentGroups.map((parent) => ( - - ))} + {parentGroups + .filter((parent: ParentGroup) => !group?.id || parent.id !== group.id) // 过滤掉当前编辑的分组 + .map((parent: ParentGroup) => ( + + ))} {actionData?.errors?.parentId && (
{actionData.errors.parentId}
@@ -415,7 +403,7 @@ export default function RuleGroupNew() { {actionData?.errors?.code && (
{actionData.errors.code}
)} -
编码只能包含字母、数字和连字符,且必须唯一
+
编码只能包含字母、数字、连字符和下划线,且必须唯一
@@ -477,7 +465,7 @@ export default function RuleGroupNew() {
{/* 排序 */} -
+
= { + [FileType.CONTRACT]: "合同", + [FileType.LICENSE]: "许可证", + [FileType.PUNISHMENT]: "处罚", + [FileType.OTHER]: "其他" +}; + +/** + * 优先级标签 + */ +export const PRIORITY_LABELS: Record = { + [Priority.NORMAL]: "普通", + [Priority.HIGH]: "高", + [Priority.URGENT]: "紧急" +}; \ No newline at end of file diff --git a/app/types/files.ts b/app/types/files.ts new file mode 100644 index 0000000..75b4687 --- /dev/null +++ b/app/types/files.ts @@ -0,0 +1,33 @@ +import { ProcessingStatus } from './enums'; + +/** + * 上传文件信息 + */ +export interface UploadedFile { + id: string; + name: string; + size: number; + type: string; + fileType: string; + priority: string; + status: ProcessingStatus; + uploadTime: string; + processingInfo?: { + currentStep: number; + totalSteps: number; + currentStepName: string; + }; +} + +/** + * 处理步骤信息 + */ +export interface ProcessingStep { + id: number; + name: string; + description: string; + status: ProcessingStatus; + startTime?: string; + endTime?: string; + error?: string; +} \ No newline at end of file diff --git a/json.txt b/json.txt index 02c3b02..951bce2 100644 --- a/json.txt +++ b/json.txt @@ -1 +1,108 @@ -{"code":0,"msg":"成功","data":[{"id":5,"code":"LIAN_WEN_SHU_WAN_ZHENG_XING_JIAN_CHA","name":"立案文书完整性检查","evaluation_point_groups_id":1,"risk":"高","description":"须立案而没有立案文书的(2分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+LLM","fields":["立案报告表-负责人意见"]},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-负责人意见"]}},{"id":"规则2","type":"regex","config":{"field":"立案报告表-负责人意见","pattern":"同意","matchType":"match"}}],"logicType":"and"},"pass_message":"立案文件合格,负责人意见已填写且同意。","fail_message":"立案文件不合格,负责人意见没有内容,请核对检查。","suggestion_message":"请检查立案报告表中的负责人意见栏,确保已填写且同意。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":6,"code":"AN_JIAN_LAI_YUAN_YI_ZHI_XING_JIAO_YAN","name":"案件来源一致性校验","evaluation_point_groups_id":1,"risk":"中","description":"没有记载案件来源或案件来源与其他文书不一致的(0.5分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+LLM","fields":["立案报告表-案件来源1111","案件处理审批表-案件来源","案件调查终结报告-案件来源"]},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-案件来源","案件处理审批表-案件来源","案件调查终结报告-案件来源"]}},{"id":"规则2","type":"consistency","config":{"logic":"and","pairs":[{"sourceField":"立案报告表-案件来源","targetField":"案件处理审批表-案件来源","compareMethod":"semantic"},{"sourceField":"立案报告表-案件来源","targetField":"案件调查终结报告-案件来源","compareMethod":"semantic"}]}}],"logicType":"and"},"pass_message":"案件来源完整","fail_message":"案件来源信息不一致或缺失,请核对。","suggestion_message":"请检查立案报告表、案件处理审批表和案件调查终结报告中的案件来源信息,确保一致。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":7,"code":"AN_YOU_FA_AN_SHI_JIAN_HE_FA_AN_DI_DIAN_JI_ZAI_ZHUN_QUE_XING_YOU_WU","name":"案由、发案时间和发案地点记载准确性-有无","evaluation_point_groups_id":1,"risk":"高","description":"没有记载或错误记载案由、发案时间和发案地点的(1分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+LLM","fields":["立案报告表-案由","立案报告表-案件来源","案件处理审批表-案件来源","案件调查终结报告-案件来源"],"prompt_setting":{"type":"custom","template":"从立案报告表中抽取案由信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-案由"]}}],"logicType":"and"},"pass_message":"案由、发案时间和发案地点记录准确。","fail_message":"案由、发案时间和发案地点记录有误或缺失,请核对。","suggestion_message":"请检查立案报告表中的案由信息,确保已填写。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":8,"code":"AN_YOU_FA_AN_SHI_JIAN_HE_FA_AN_DI_DIAN_JI_ZAI_ZHUN_QUE_XING_YI_ZHI","name":"案由、发案时间和发案地点记载准确性-一致","evaluation_point_groups_id":1,"risk":"高","description":"案由、发案时间和发案地点记载不一致的(1分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"LLM-VL","fields":["立案报告表-案发时间","立案报告表-案发地址","现场笔录-检查时间","现场笔录-检查地点"],"prompt_setting":{"type":"system","template":"从立案报告表和现场笔录中抽取案发时间、案发地址、检查时间和检查地点信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"consistency","config":{"logic":"and","pairs":[{"sourceField":"立案报告表-案发时间","targetField":"现场笔录-检查时间","compareMethod":"exact"},{"sourceField":"立案报告表-案发地址","targetField":"现场笔录-检查地点","compareMethod":"semantic"}]}}],"logicType":"and"},"pass_message":"案由、发案时间和发案地点记录准确。","fail_message":"案由、发案时间和发案地点记录有误或缺失,请核对。","suggestion_message":"请检查立案报告表和现场笔录中的案发时间和地点信息,确保一致。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":9,"code":"DANG_SHI_REN_JI_BEN_QING_KUANG_JI_ZAI_WAN_ZHENG_ZHUN_QUE","name":"当事人基本情况记载完整、准确","evaluation_point_groups_id":1,"risk":"高","description":"没有记载当事人的基本情况,或错误记载当事人姓名、有效证件号码和地址的(1分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+Regex","fields":["立案报告表-当事人-姓名","立案报告表-当事人-身份证号码","立案报告表-当事人-住址","证据复制(提取)单-姓名","证据复制(提取)单-身份证号码","证据复制(提取)单-住址"],"prompt_setting":{"type":"system","template":"从立案报告表和证据复制(提取)单中抽取当事人姓名、身份证号码和住址信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-当事人-姓名","立案报告表-当事人-身份证号码","立案报告表-当事人-住址"]}},{"id":"规则2","type":"consistency","config":{"logic":"and","pairs":[{"sourceField":"立案报告表-当事人-姓名","targetField":"证据复制(提取)单-姓名","compareMethod":"exact"},{"sourceField":"立案报告表-当事人-身份证号码","targetField":"证据复制(提取)单-身份证号码","compareMethod":"exact"},{"sourceField":"立案报告表-当事人-住址","targetField":"证据复制(提取)单-住址","compareMethod":"semantic"}]}}],"logicType":"and"},"pass_message":"当事人基本情况记录完整,与身份证信息一致。","fail_message":"当事人基本情况记录有误或缺失,请核对。","suggestion_message":"请检查立案报告表和证据复制(提取)单中的当事人信息,确保一致。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":10,"code":"AN_JIAN_QING_KUANG_BIAO_SHU_QING_XI_DU","name":"案件情况表述清晰度","evaluation_point_groups_id":1,"risk":"高","description":"没有记载案件情况或案件情况表述不清晰的(1分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+LLM","fields":["立案报告表-案情摘要-时间","立案报告表-案情摘要-查获物","立案报告表-案情摘要-案由","立案报告表-案情摘要-引用条款"],"prompt_setting":{"type":"custom","template":"从立案报告表中抽取案情摘要中的时间、查获物、案由和引用条款信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-案情摘要-时间","立案报告表-案情摘要-查获物","立案报告表-案情摘要-案由","立案报告表-案情摘要-引用条款"]}}],"logicType":"and"},"pass_message":"案件情况描述清晰。","fail_message":"案件情况记录不清晰或缺失,请核对。","suggestion_message":"请检查立案报告表中的案情摘要,确保包含时间、查获物、案由和引用条款。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":11,"code":"CHENG_BAN_REN_HE_CHENG_BAN_BU_MEN_YI_JIAN_JI_QIAN_MING","name":"承办人和承办部门意见及签名","evaluation_point_groups_id":1,"risk":"中","description":"没有记载承办人、承办部门意见、签字和日期的(0.5分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"LLM-VL","fields":["立案报告表-承办部门意见-意见","立案报告表-承办部门意见-签名","立案报告表-承办部门意见-日期"],"prompt_setting":{"type":"custom","template":"从立案报告表中抽取承办部门意见、签名和日期信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-承办部门意见-意见","立案报告表-承办部门意见-签名","立案报告表-承办部门意见-日期"]}}],"logicType":"and"},"pass_message":"承办人和承办部门意见及签名完整。","fail_message":"承办人和承办部门意见及签名存在缺失,请核对。","suggestion_message":"请检查立案报告表中的承办部门意见、签名和日期,确保已填写。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"},{"id":12,"code":"XING_ZHENG_JI_GUAN_FU_ZE_REN_MING_QUE_YI_JIAN_QIAN_ZI_HE_RI_QI","name":"行政机关负责人明确意见、签字和日期","evaluation_point_groups_id":1,"risk":"中","description":"没有记载行政机关负责人明确意见、签字和日期的(0.5分)","is_enabled":true,"references_laws":{},"extraction_config":{"type":"OCR+LLM","fields":["立案报告表-负责人意见-意见","立案报告表-负责人意见-签名","立案报告表-负责人意见-日期"],"prompt_setting":{"type":"system","template":"从立案报告表中抽取负责人意见、签名和日期信息。"}},"evaluation_config":{"rules":[{"id":"规则1","type":"exists","config":{"logic":"and","fields":["立案报告表-负责人意见-意见","立案报告表-负责人意见-签名","立案报告表-负责人意见-日期"]}}],"logicType":"and"},"pass_message":"行政机关负责人意见、签字和日期完整。","fail_message":"行政机关负责人意见、签字和日期缺失,请核对。","suggestion_message":"请检查立案报告表中的负责人意见、签名和日期,确保已填写。","suggestion_message_type":"warning","post_action":"none","action_config":"","created_at":"2023-10-01T00:00:00+00:00","updated_at":"2023-10-01T00:00:00+00:00"}]} \ No newline at end of file +[ + { + "id": 1, + "template_name": "行政处罚-抽取通用模板", + "template_type": "Extraction", + "description": "本模板用于抽取行政处罚决定书编号等信息", + "template_content": "你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息:\n\n1. 处罚决定书编号\n2. 处罚对象名称\n3. 处罚事由\n4. 处罚依据\n5. 处罚内容\n6. 处罚金额\n7. 发文日期\n\n请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。", + "variables": { + "docType": "文档类型" + }, + "status": 2, + "version": "v1.0", + "created_by": 1, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 2, + "template_name": "销售合同-甲方信息评估", + "template_type": "Evaluation", + "description": "评估销售合同中甲方信息是否完整", + "template_content": "你是一个专业的合同审核助手。请评估以下{docType}中甲方信息的完整性:\n\n请检查以下要素是否存在且完整:\n1. 甲方全称\n2. 注册地址\n3. 统一社会信用代码\n4. 法定代表人\n5. 联系方式\n\n请给出评估结果,并标明缺失或不完整的信息。", + "variables": { + "docType": "文档类型" + }, + "status": 1, + "version": "v1.2", + "created_by": 2, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 3, + "template_name": "专卖许可证-摘要模板", + "template_type": "Summary", + "description": "生成专卖许可证申请文件的内容摘要", + "template_content": "你是一个专业的文档摘要助手。请为以下{docType}生成一份简洁的摘要:\n\n摘要应包含以下要点:\n1. 申请人基本信息\n2. 许可证类型\n3. 申请事项\n4. 经营范围\n5. 申请日期\n\n请控制摘要在200字以内,保留关键信息。", + "variables": { + "docType": "文档类型" + }, + "status": 1, + "version": "v1.0", + "created_by": 2, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 4, + "template_name": "采购合同-乙方资质抽取", + "template_type": "Extraction", + "description": "抽取采购合同中乙方的资质信息", + "template_content": "你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息:\n\n需要抽取的信息包括:\n1. 乙方全称\n2. 资质证书类型\n3. 资质证书编号\n4. 资质等级\n5. 证书有效期\n\n请将结果以JSON格式输出,包含以上字段。", + "variables": { + "docType": "文档类型" + }, + "status": 0, + "version": "v1.1", + "created_by": 3, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 5, + "template_name": "合同通用-关键条款评估", + "template_type": "Evaluation", + "description": "评估合同中关键条款是否明确、合规", + "template_content": "你是一个专业的{industry}行业合同审核助手。请评估以下合同中的关键条款是否明确、合规:\n\n请重点关注以下条款:\n1. 合同标的\n2. 价格条款\n3. 付款条件\n4. 交付方式\n5. 违约责任\n6. 争议解决\n\n请对每一项给出评估结果,并指出不明确或存在风险的条款。", + "variables": { + "docType": "文档类型", + "industry": "行业类型" + }, + "status": 1, + "version": "v2.0", + "created_by": 4, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 6, + "template_name": "LLM通用抽取Prompt", + "template_type": "Extraction", + "description": "", + "template_content": "上面的文本为{{文档名称}}文档,请帮我结构化抽取下列信息:\n{{提取字段}}\n\n以 下面的json 结构输出抽取结果\n**输出格式**:\n - 以JSON格式输出结果:\n{\n \"{{文档名称}}\": {\n \"案件来源\": \"字段值\", \n \"案由\": \"字段值\"\n }\n}", + "variables": { + + }, + "status": 1, + "version": "v2.0", + "created_by": 4, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + }, + { + "id": 7, + "template_name": "VLM通用抽取Prompt", + "template_type": "Extraction", + "description": null, + "template_content": "上面的文本为{{文档名称}}文档,请帮我结构化抽取下列信息:\n{{提取字段}}\n\n以 下面的json 结构输出抽取结果\n**输出格式**:\n - 以JSON格式输出结果:\n{\n \"{{文档名称}}\": {\n \"案件来源\": \"字段值\", \n \"案由\": \"字段值\"\n }\n}", + "variables": { + + }, + "status": 1, + "version": "v2.0", + "created_by": 4, + "created_at": "2025-03-26T01:23:58.549908", + "updated_at": "2025-03-26T01:23:58.549908" + } +] \ No newline at end of file