diff --git a/app/api/client.ts b/app/api/client.ts index 996f718..df02e01 100644 --- a/app/api/client.ts +++ b/app/api/client.ts @@ -14,7 +14,8 @@ export type ApiResponse = { export type QueryParams = Record; // 获取 API 基础 URL -const API_BASE_URL = '172.16.0.119:9000/admin'; +const API_BASE_URL = '172.18.0.100:3000'; +// const API_BASE_URL = '172.16.0.119:9000/admin'; // 是否使用模拟数据(开发环境使用) const USE_MOCK_DATA = false; // 设置为true使用模拟数据,避免API连接问题 diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index dc6922a..830191e 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -7,8 +7,8 @@ import dayjs from 'dayjs'; export interface RulesQueryParams { page?: number; pageSize?: number; - ruleType?: string; - groupId?: string; + ruleType?: string; // 评查点类型ID + groupId?: string; // 规则组ID isActive?: boolean; keyword?: string; orderBy?: string; @@ -30,12 +30,13 @@ export interface ApiRule { id: number; code: string; name: string; - evaluation_point_groups_id: number; + evaluation_point_groups_id: number | null; risk: string; description: string; is_enabled: boolean; evaluation_point_groups?: { - name: string; + first_name: string; + second_name: string; }; references_laws: Record; extraction_config: { @@ -94,23 +95,32 @@ function mapApiRuleToFrontendModel(apiRule: ApiRule): Rule { '低': 'low' }; - // 规则类型映射(这里根据实际业务逻辑设置一个默认值) - const ruleType = 'essential'; // 实际应用中可能需要从其他字段推断 + //评查点类型映射 - // 优先使用关联查询获取的分组名称,如果不存在则使用默认值 - const groupName = apiRule.evaluation_point_groups?.name || `${apiRule.evaluation_point_groups_id}`; + // 规则类型映射(这里根据实际业务逻辑设置一个默认值) +// const ruleType = 'essential'; // 实际应用中可能需要从其他字段推断 +// console.log("apiRule.evaluation_point_groups",apiRule); + + // 如果evaluation_point_groups_id为null或空,则不显示ruleType和groupName + const isGroupIdEmpty = !apiRule.evaluation_point_groups_id; + + // 规则类型只在有分组ID时才显示 + const ruleType = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups?.first_name || ''); + + // 规则组名称只在有分组ID时才显示 + const groupName = isGroupIdEmpty ? '' : (apiRule.evaluation_point_groups?.second_name || `${apiRule.evaluation_point_groups_id}`); return { - id: apiRule.id.toString(), - code: apiRule.code, - name: apiRule.name, + id: apiRule.id ? apiRule.id.toString() : '', // 添加空值检查 + code: apiRule.code || '', + name: apiRule.name || '', ruleType: ruleType, - groupId: apiRule.evaluation_point_groups_id.toString(), + groupId: isGroupIdEmpty ? '' : apiRule.evaluation_point_groups_id?.toString() || '', groupName: groupName, priority: priorityMap[apiRule.risk] || 'medium', - description: apiRule.description, - isActive: apiRule.is_enabled, - createdAt: apiRule.created_at, + description: apiRule.description || '', + isActive: apiRule.is_enabled === undefined ? false : apiRule.is_enabled, + createdAt: apiRule.created_at , updatedAt: apiRule.updated_at }; } @@ -141,14 +151,12 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul // 构建PostgrestParams参数 const postgrestParams: PostgrestParams = { - // 使用PostgREST资源嵌入语法获取关联数据 - // 这里使用外键关系自动关联evaluation_point_groups表 + // 修改select语句,不使用嵌入查询语法 select: ` id, code, name, evaluation_point_groups_id, - evaluation_point_groups(name), risk, description, is_enabled, @@ -171,7 +179,7 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul } }; - // 添加精确匹配过滤 + // 添加精确匹配过滤:规则组ID if (groupId) { postgrestParams.filter!['evaluation_point_groups_id'] = `eq.${groupId}`; } @@ -180,6 +188,47 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul postgrestParams.filter!['is_enabled'] = `eq.${isActive}`; } + // 如果指定了评查点类型ID,需要先查询该类型下的所有规则组ID + if (ruleType) { + try { + // 先获取该类型下的所有规则组 + const groupsParams: PostgrestParams = { + select: 'id', + filter: { + 'pid': `eq.${ruleType}` + } + }; + + const groupsResponse = await postgrestGet<{code: number; msg: string; data: Array<{id: number}>}>('evaluation_point_groups', groupsParams); + + if (groupsResponse.error) { + console.error('获取规则组列表失败:', groupsResponse.error); + } else { + let groupIds: number[] = []; + + // 处理不同API响应格式 + if (groupsResponse.data && 'code' in groupsResponse.data && groupsResponse.data.data) { + if (Array.isArray(groupsResponse.data.data) && groupsResponse.data.data.length > 0) { + groupIds = groupsResponse.data.data.map(group => group.id); + } + } else if (Array.isArray(groupsResponse.data) && groupsResponse.data.length > 0) { + groupIds = groupsResponse.data.map(group => group.id); + } + + // 使用in过滤条件,如果找到规则组 + if (groupIds.length > 0) { + postgrestParams.filter!['evaluation_point_groups_id'] = `in.(${groupIds.join(',')})`; + } + if (groupId) { + postgrestParams.filter!['evaluation_point_groups_id'] = `eq.${groupId}`; + } + } + } catch (error) { + console.error('获取规则组ID出错:', error); + // 错误不中断流程,继续使用其他筛选条件 + } + } + // 添加模糊搜索 if (keyword) { // 使用PostgREST的or条件查询 @@ -198,66 +247,90 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul return { error: response.error, status: response.status }; } - // 确保响应数据存在且符合预期格式 - if (!response.data || !response.data.data || !Array.isArray(response.data.data)) { + // 处理不同的API响应格式 + let apiRules: ApiRule[] = []; + let totalCount = 0; + + // 9000端口格式 {code: number, msg: string, data: ApiRule[]} + if (response.data && 'code' in response.data && response.data.data) { + if (Array.isArray(response.data.data)) { + apiRules = response.data.data; + } else { + return { error: '接口返回数据格式不正确', status: 500 }; + } + } + // 3000端口格式 ApiRule[] + else if (Array.isArray(response.data)) { + apiRules = response.data; + } + // 不支持的格式 + else { return { error: '接口返回数据格式不正确', status: 500 }; } - // 获取API返回的所有评查点 - const apiRules = response.data.data; - - // 从响应头中获取总记录数(如果存在) - // 注意:这里假设总记录数会在接口响应的某个位置返回 - // 实际使用时,需要根据 PostgREST 的响应格式进行调整 - let totalCount = 0; - // 尝试从响应中获取总数 - if (response.headers && response.headers['content-range']) { + let rangeHeader = ''; + + // 安全地检查头信息是否存在 + if (response && 'headers' in response && response.headers && typeof response.headers === 'object') { + rangeHeader = (response.headers as Record)['content-range'] || ''; + } + + if (rangeHeader) { // 例如 Content-Range: 0-9/42 表示总共有 42 条记录 - const range = response.headers['content-range']; - const total = range.split('/')[1]; + const total = rangeHeader.split('/')[1]; if (total !== '*') { // '*' 表示未知总数 totalCount = parseInt(total, 10); } } else { // 如果没有响应头,则使用当前返回的数据长度作为默认值 - // 这种情况下分页可能不准确 totalCount = apiRules.length; - console.warn('未能从响应中获取总记录数,使用当前页数据长度作为默认值'); } // 打印第一个数据项来查看结构(仅用于调试) - if (apiRules.length > 0) { - console.log('评查点数据示例:', JSON.stringify(apiRules[0], null, 2)); - } + // if (apiRules.length > 0) { + // console.log('评查点数据示例:', JSON.stringify(apiRules[0], null, 2)); + // } - // 收集所有分组ID (多进行一步查找表的操作) + + // 收集所有规则分组ID(实际上是二级分组) (多进行一步查找表的操作) const groupIds = [...new Set(apiRules.map(rule => rule.evaluation_point_groups_id))]; - - // 如果有分组ID,查询分组信息 - const groupsMap: Record = {}; - + // 如果有分组ID,查询分组信息 - 更新类型定义 + const groupsMap: Record = {}; if (groupIds.length > 0) { try { - // 构建查询参数 - const groupsParams: PostgrestParams = { - select: 'id,name', - filter: { - 'id': `in.(${groupIds.join(',')})` - } - }; + // 过滤null和空值 + const validGroupIds = groupIds.filter(id => id != null && id.toString().trim() !== ''); - // 查询评查点分组表 - const groupsResponse = await postgrestGet<{code: number; msg: string; data: {id: number; name: string}[]}>('evaluation_point_groups', groupsParams); - - if (groupsResponse.data?.data) { - // 创建ID到名称的映射 - groupsResponse.data.data.forEach(group => { - groupsMap[group.id.toString()] = group.name; - }); + if (validGroupIds.length > 0) { + // 使用Promise.all并行查询所有分组信息 - 使用正确的函数名 + const groupPromises = validGroupIds.map(id => + postgrestGet<{code: number; msg: string; data: {id: number; pid: number; name: string; first_name: string; second_name: string}[]}>( + `rpc/get_evaluation_point_group_with_pid?input_id=${id}` + ) + ); - // 打印分组数据(仅用于调试) - console.log('分组数据:', groupsMap); + const groupResponses = await Promise.all(groupPromises); + // console.log("groupResponsesdddddddddddddddddd",groupResponses); + // 处理响应,填充groupsMap + groupResponses.forEach((response) => { + if (!response.error && response.data) { + // 处理不同API响应格式 + if (Array.isArray(response.data.data) && response.data.data.length > 0) { + const groupid = response.data.data[1]?.id || ""; + groupsMap[groupid] = { + first_name: response.data.data[0]?.name || "", + second_name: response.data.data[1]?.name || "" + }; + }else if(Array.isArray(response.data) && response.data.length > 0){ + const groupid = response.data[1]?.id || ""; + groupsMap[groupid] = { + first_name: response.data[0]?.name || "", + second_name: response.data[1]?.name || "" + }; + } + } + }); } } catch (error) { console.error('获取分组数据失败:', error); @@ -265,33 +338,26 @@ export async function getRulesList(params: RulesQueryParams): Promise<{data: Rul } } + // 应用本地过滤 - 只在API不支持的情况下使用 - let filteredRules = [...apiRules]; - - // 如果有ruleType过滤(API暂不支持),在本地过滤 - if (ruleType) { - // 注意:这是本地过滤,实际情况下最好在API层面支持 - filteredRules = filteredRules.filter(() => { - // 实现一个映射逻辑,比如根据其他字段推导ruleType - const derivedType = 'essential'; // 此处应为实际推导逻辑 - return derivedType === ruleType; - }); - } - + const filteredRules = [...apiRules]; + // 不再需要本地过滤,因为已经在API层面添加了评查点类型过滤 // 如果进行了本地过滤,则需要调整总记录数 - if (ruleType) { - totalCount = filteredRules.length; - } + // if (ruleType) { + // totalCount = filteredRules.length; + // } + // 将API返回的数据映射到前端模型,并附加分组名称 + console.log("groupsMap",groupsMap); const mappedRules = filteredRules.map(apiRule => { // 创建修改版的apiRule,添加从分组映射获取的名称 const enhancedApiRule = { ...apiRule, // 从映射中获取分组名称,如果不存在则使用默认值 evaluation_point_groups: { - name: groupsMap[apiRule.evaluation_point_groups_id.toString()] || `${apiRule.evaluation_point_groups_id}` - // name: apiRule.evaluation_point_groups?.name || `${apiRule.evaluation_point_groups_id}` + first_name: groupsMap[apiRule.evaluation_point_groups_id?.toString() || '']?.first_name || `${apiRule.evaluation_point_groups_id}`, + second_name: groupsMap[apiRule.evaluation_point_groups_id?.toString() || '']?.second_name || `${apiRule.evaluation_point_groups_id}` } }; @@ -383,7 +449,10 @@ export async function getRule(id: string): Promise<{data: Rule; error?: never} | if (groupResponse.data?.data && groupResponse.data.data.length > 0) { // 将分组信息添加到评查点数据中 const group = groupResponse.data.data[0]; - apiRule.evaluation_point_groups = { name: group.name }; + apiRule.evaluation_point_groups = { + first_name: group.name, + second_name: group.name + }; } } } catch (error) { @@ -535,23 +604,103 @@ export async function updateRule(id: string, ruleData: Partial { try { + console.log(`开始删除评查点, ID: ${id}`); + + // 使用 PostgREST 语法,通过查询参数指定要删除的行 + const postgrestParams: PostgrestParams = { + filter: { + 'id': `eq.${id}` + }, + headers: { + 'Prefer': 'return=representation' // 请求返回被删除的记录 + } + }; + // 使用postgrestDelete删除评查点 - const response = await postgrestDelete<{code: number; msg: string; data: ApiRule}>(`evaluation_points/${id}`); + const response = await postgrestDelete('evaluation_points', postgrestParams); + + console.log('删除请求响应:', JSON.stringify(response, null, 2)); // 检查是否有错误响应 if (response.error) { + console.error('删除评查点API返回错误:', response.error); return { error: response.error, status: response.status }; } - // 确保响应数据存在且符合预期格式 - if (!response.data || !response.data.data) { - return { error: '接口返回数据格式不正确', status: 500 }; + // 确保响应数据存在 + if (!response.data) { + console.error('API响应缺少数据字段'); + return { error: 'API返回数据为空', status: 500 }; } - // 将API返回的数据映射到前端模型 - const rule = mapApiRuleToFrontendModel(response.data.data); + // 创建一个模拟的成功删除结果 + const createMockSuccessRule = (): Rule => { + return { + id: id, + code: '', + name: '', + ruleType: '', + groupId: '', + groupName: '', + priority: 'medium', + description: '', + isActive: false, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString() + }; + }; - return { data: rule }; + // 处理9000端口响应格式 + if (typeof response.data === 'object' && response.data !== null && 'code' in response.data) { + const apiResponse = response.data as {code: number; msg: string; data?: ApiRule | ApiRule[]}; + + // 检查响应的code - 如果code为0则表示操作成功 + if (apiResponse.code === 0) { + // 如果data不存在或是空数组,返回模拟数据 + if (!apiResponse.data || (Array.isArray(apiResponse.data) && apiResponse.data.length === 0)) { + return { data: createMockSuccessRule() }; + } + + // 处理存在的data + let apiRule: ApiRule; + if (Array.isArray(apiResponse.data)) { + apiRule = apiResponse.data[0]; + } else { + apiRule = apiResponse.data; + } + + // 将API返回的数据映射到前端模型 + const rule = mapApiRuleToFrontendModel(apiRule); + return { data: rule }; + } + + // 如果code不为0,则返回错误信息 + return { + error: apiResponse.msg || '删除失败,服务器返回错误', + status: 500 + }; + } + // 处理3000端口响应格式 (直接返回数据) + else { + // 处理数组响应 + if (Array.isArray(response.data)) { + if (response.data.length === 0) { + // 空数组表示成功但没有返回数据 + return { data: createMockSuccessRule() }; + } else { + // 返回数组中的第一个元素 + const apiRule = response.data[0] as ApiRule; + const rule = mapApiRuleToFrontendModel(apiRule); + return { data: rule }; + } + } + // 处理单一对象响应 + else { + const apiRule = response.data as ApiRule; + const rule = mapApiRuleToFrontendModel(apiRule); + return { data: rule }; + } + } } catch (error) { console.error('删除评查点出错:', error); return { @@ -600,4 +749,182 @@ export async function duplicateRule(id: string): Promise<{data: Rule; error?: ne status: 500 }; } +} + +/** + * 评查点类型 + */ +export interface RuleType { + id: string; + name: string; + description?: string; + isEnabled: boolean; +} + +/** + * 规则组 + */ +export interface RuleGroup { + id: string; + name: string; + description?: string; + isEnabled: boolean; + typeId?: string; // 关联的评查点类型ID +} + +/** + * 获取评查点类型列表 + * @returns 评查点类型列表 + */ +export async function getRuleTypes(): Promise<{data: RuleType[]; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 构建PostgrestParams参数 + const postgrestParams: PostgrestParams = { + select: ` + id, + pid, + code, + name, + description, + is_enabled + `, + // 查询父ID为0的类型(顶级类型) + filter: { + 'pid': 'eq.0' + } + }; + + // 发送请求获取评查点类型列表 + const response = await postgrestGet<{code: number; msg: string; data: Array<{ + id: number; + pid: number; + code: string; + name: string; + description: string; + is_enabled: boolean; + }>; + }>('evaluation_point_groups', postgrestParams); + + // 检查是否有错误响应 + if (response.error) { + return { error: response.error, status: response.status }; + } + + if(response.data && 'code' in response.data && response.data.data){ + if(Array.isArray(response.data.data) && response.data.data.length > 0){ + // 将API返回的数据映射到前端模型 + const ruleTypes = response.data.data.map(item => ({ + id: item.id.toString(), + pid: item.pid.toString(), + code: item.code, + name: item.name, + description: item.description, + isEnabled: item.is_enabled + })); + return { data: ruleTypes }; + }else{ + return { error: '9000接口返回数据格式不正确', status: 500 }; + } + }else if(Array.isArray(response.data) && response.data.length > 0){ + console.log("评查点类型列表",response.data); + const ruleTypes = response.data.map(item => ({ + id: item.id.toString(), + pid: item.pid.toString(), + code: item.code, + name: item.name, + description: item.description, + isEnabled: item.is_enabled + })); + return { data: ruleTypes }; + }else{ + return { error: '3000接口返回数据格式不正确', status: 500 }; + } + } catch (error) { + console.error('获取评查点类型出错:', error); + return { + error: error instanceof Error ? error.message : '获取评查点类型失败', + status: 500 + }; + } +} + +/** + * 根据评查点类型ID获取规则组列表 + * @param typeId 评查点类型ID + * @returns 规则组列表 + */ +export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGroup[]; error?: never} | {data?: never; error: string; status?: number}> { + try { + // 如果typeId为空或为"全部",则返回空数组 + if (!typeId || typeId === 'all') { + return { data: [] }; + } + + // 构建PostgrestParams参数 + const postgrestParams: PostgrestParams = { + select: ` + id, + pid, + code, + name, + description, + is_enabled + `, + // 查询指定类型ID的规则组 + filter: { + 'pid': `eq.${typeId}` + } + }; + + // 发送请求获取规则组列表 + const response = await postgrestGet<{code: number; msg: string; data: Array<{ + id: number; + pid: number; + code: string; + name: string; + description: string; + is_enabled: boolean; + }>; + }>('evaluation_point_groups', postgrestParams); + + // 检查是否有错误响应 + if (response.error) { + return { error: response.error, status: response.status }; + } + + // 确保响应数据存在且符合预期格式 + if(response.data && 'code' in response.data && response.data.data){ + if(Array.isArray(response.data.data) && response.data.data.length > 0){ + // 将API返回的数据映射到前端模型 + const ruleGroups = response.data.data.map(item => ({ + id: item.id.toString(), + name: item.name, + description: item.description, + isEnabled: item.is_enabled, + code: item.code + })); + return { data: ruleGroups }; + }else{ + return { error: '9000接口返回数据格式不正确', status: 500 }; + } + }else if(Array.isArray(response.data) && response.data.length > 0){ + console.log("评查点类型列表",response.data); + const ruleGroups = response.data.map(item => ({ + id: item.id.toString(), + name: item.name, + description: item.description, + isEnabled: item.is_enabled, + code: item.code + })); + return { data: ruleGroups }; + }else{ + return { error: '3000接口返回数据格式不正确', status: 500 }; + } + } catch (error) { + console.error('获取规则组出错:', error); + return { + error: error instanceof Error ? error.message : '获取规则组失败', + status: 500 + }; + } } \ No newline at end of file diff --git a/app/api/postgrest-client.ts b/app/api/postgrest-client.ts index 024a5c1..f374d66 100644 --- a/app/api/postgrest-client.ts +++ b/app/api/postgrest-client.ts @@ -24,7 +24,8 @@ export interface PostgrestParams { */ function logPostgrestQuery(endpoint: string, params?: QueryParams): void { if (process.env.NODE_ENV !== 'production') { - const baseUrl = 'http://172.16.0.119:9000/admin'; + // const baseUrl = 'http://172.16.0.119:9000/admin'; + const baseUrl = 'http://172.18.0.100:3000'; console.log('\n📦 PostgREST 查询日志 ========================'); console.log(`📦 API 端点: ${baseUrl}/${endpoint}`); @@ -355,24 +356,38 @@ export async function postgrestPut>(endpoint: str /** * 发送 DELETE 请求到 PostgresREST 接口 * @param endpoint 端点 + * @param params 查询参数,用于指定要删除的记录 * @returns 响应数据 */ -export async function postgrestDelete(endpoint: string): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { +export async function postgrestDelete(endpoint: string, params?: PostgrestParams): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 const apiEndpoint = endpoint.startsWith('/') ? endpoint.substring(1) : endpoint; - // 打印查询信息(DELETE请求只打印端点) - logPostgrestQuery(apiEndpoint); + // 转换查询参数 + const queryParams = params ? transformParams(params) : {}; + + // 提取并移除自定义头部参数 + const headers: Record = { + 'Prefer': 'return=representation', // 默认请求返回被删除的记录 + ...(params?.headers || {}) + }; + + // 清除查询参数中的headers属性,避免将其作为URL参数 + if (queryParams.headers) { + delete queryParams.headers; + } + + // 打印查询信息 + logPostgrestQuery(apiEndpoint, queryParams); const response = await apiRequest( apiEndpoint, { method: 'DELETE', - headers: { - 'Prefer': 'return=representation' - } - } + headers + }, + queryParams ); if (response.error) { diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 19f4c67..2c59ef3 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -1,5 +1,6 @@ import { useState, useEffect, useRef, useCallback } from "react"; -import { MetaFunction, ActionFunctionArgs } from "@remix-run/node"; +import { MetaFunction, ActionFunctionArgs, json } from "@remix-run/node"; +import { Form } from "@remix-run/react"; import { Card } from "~/components/ui/Card"; import { Button } from "~/components/ui/Button"; import { Table } from "~/components/ui/Table"; @@ -82,27 +83,132 @@ export interface UploadedFile { }; } +// 文件上传响应接口 +interface FileUploadResponse { + success: boolean; + fileId?: string; + message?: string; + error?: string; +} + +// 将文件转换为二进制数据 +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('读取文件失败')); + }; + + // 读取文件为 ArrayBuffer (二进制格式) + reader.readAsArrayBuffer(file); + }); +} + +// 模拟上传文件到服务器的API +async function uploadFileToServer( + binaryData: ArrayBuffer, + fileName: string, + fileType: string, + documentType: FileType, + priority: Priority +): Promise { + // 在实际应用中,这里会使用fetch或axios发送请求到后端API + console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); + + // 模拟网络延迟 + await new Promise(resolve => setTimeout(resolve, 500)); + + try { + // 创建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 + }, + body: binaryData // 二进制数据作为请求体 + }; + + console.log('[模拟API] 请求参数:', { + url: requestParams.url, + headers: requestParams.headers, + bodySize: binaryData.byteLength + }); + + // 实际API调用 - 在生产环境中实现 + const response = await fetch(requestParams.url, { + method: requestParams.method, + headers: requestParams.headers, + body: binaryData + }); + + if (!response.ok) { + 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 { + success: false, + error: error instanceof Error ? error.message : '上传失败' + }; + } +} + // action处理文件上传请求 export async function action({ request }: ActionFunctionArgs) { try { const formData = await request.formData(); - // 由于无法直接从Remix的action中处理文件上传, - // 实际环境中应使用FormData将文件发送到后端API - // 这里我们模拟处理过程,创建一个响应对象 - + // 获取文件和其他字段 + const fileUpload = formData.get("file") as File | null; const fileType = formData.get("fileType") as FileType; const priority = formData.get("priority") as Priority; if (!fileType) { - return Response.json( + return json( { success: false, error: "请选择文件类型" }, { status: 400 } ); } + if (!fileUpload) { + return json( + { success: false, error: "未找到上传的文件" }, + { status: 400 } + ); + } + + // 获取文件信息 + console.log(`接收到文件: ${fileUpload.name}, 大小: ${fileUpload.size}, 类型: ${fileUpload.type}`); + + // 注意: 在实际的Remix action中,我们无法直接处理文件内容 + // 这里的代码仅用于模拟。在前端组件中,我们将实现实际的文件处理逻辑。 + // 模拟文件上传成功响应 - return Response.json({ + return json({ success: true, message: "文件上传请求已接收", fileId: `file_${Date.now()}`, @@ -111,7 +217,7 @@ export async function action({ request }: ActionFunctionArgs) { }); } catch (error) { console.error("文件上传失败:", error); - return Response.json( + return json( { success: false, error: "文件上传失败,请重试" }, { status: 500 } ); @@ -186,6 +292,9 @@ export default function FilesUpload() { // UploadArea组件引用 const uploadAreaRef = useRef(null); + // 表单提交引用 + const formRef = useRef(null); + // 清理定时器 useEffect(() => { return () => { @@ -198,6 +307,28 @@ export default function FilesUpload() { }; }, []); + // 处理表单提交 + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (!currentFile || !fileType) { + alert('请选择文件和文件类型'); + return; + } + + try { + // 设置上传状态 + setUploadStage('uploading'); + + // 实际上传 - 通过我们自定义的二进制上传方法 + await startUpload(currentFile); + + } catch (error) { + console.error('提交表单错误:', error); + alert(`提交表单失败: ${error instanceof Error ? error.message : '未知错误'}`); + } + }; + // 处理文件选择 const handleFilesSelected = useCallback((selectedFiles: FileList) => { console.log("selectedFiles", selectedFiles); @@ -213,46 +344,91 @@ export default function FilesUpload() { } setCurrentFile(selectedFiles[0]); - startUpload(selectedFiles[0]); - }, [fileType, currentFile]); + // 不再立即上传,而是等待表单提交 + }, [fileType]); // 开始上传文件 - const startUpload = (file: File) => { - setUploadStage("uploading"); - setUploadProgress(0); - - // 更新步骤状态 - const updatedSteps = [...processingSteps]; - updatedSteps[0].status = "active"; - updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`; - setProcessingSteps(updatedSteps); - - // 模拟上传进度 - if (progressIntervalRef.current) { - clearInterval(progressIntervalRef.current); - } - - progressIntervalRef.current = setInterval(() => { - setUploadProgress(prev => { - const newProgress = prev + 5; - // 根据文件大小调整上传速度 - const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5); - setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`); - - if (newProgress >= 100) { - if (progressIntervalRef.current) { - clearInterval(progressIntervalRef.current); - setUploadSpeed("完成"); - - // 完成上传后开始处理流程 - startProcessing(file); + const startUpload = async (file: File) => { + try { + setUploadStage("uploading"); + setUploadProgress(0); + + // 更新步骤状态 + const updatedSteps = [...processingSteps]; + updatedSteps[0].status = "active"; + updatedSteps[0].description = `正在上传文件"${file.name}"到服务器...`; + setProcessingSteps(updatedSteps); + + // 转换文件为二进制格式 + console.log("开始转换文件到二进制格式..."); + + // 模拟上传进度 + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + + progressIntervalRef.current = setInterval(() => { + setUploadProgress(prev => { + const newProgress = prev + 2; + // 根据文件大小调整上传速度 + const speedFactor = Math.min(file.size / (1024 * 1024) + 1, 5); + setUploadSpeed(`${Math.floor(Math.random() * 100 * speedFactor) + 50}KB/s`); + + if (newProgress >= 50) { // 只模拟到50%,剩下的50%留给实际上传 + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + return 50; } - return 100; - } - - return newProgress; - }); - }, 200); + + return newProgress; + }); + }, 100); + + // 实际执行二进制转换 + const binaryData = await uploadFileToBinary(file); + console.log(`文件转换为二进制完成,大小: ${binaryData.byteLength} 字节`); + + // 模拟实际上传 + setUploadProgress(60); // 转换完成,进度到60% + setUploadSpeed(`${Math.floor(Math.random() * 200) + 100}KB/s`); + + console.log("开始上传文件到服务器..."); + const response = await uploadFileToServer( + binaryData, + file.name, + file.type, + fileType as FileType, + priority + ); + + if (!response.success) { + throw new Error(response.error || "上传失败"); + } + + console.log("文件上传成功:", response); + setUploadProgress(100); + setUploadSpeed("完成"); + + // 完成上传后开始处理流程 + startProcessing(file); + } catch (error) { + console.error("文件上传错误:", error); + + // 更新步骤状态为错误 + const errorSteps = [...processingSteps]; + errorSteps[0].status = "error"; + errorSteps[0].description = `上传文件"${file.name}"失败: ${error instanceof Error ? error.message : '未知错误'}`; + setProcessingSteps(errorSteps); + + // 清除进度定时器 + if (progressIntervalRef.current) { + clearInterval(progressIntervalRef.current); + } + + alert(`文件上传失败: ${error instanceof Error ? error.message : '未知错误'}`); + resetUpload(); + } }; // 开始处理文件 @@ -308,13 +484,6 @@ export default function FilesUpload() { setProcessingSteps(nextUpdatedSteps); currentStepIndex++; - - // 如果这是最后一个步骤,确保完成 - // if (currentStepIndex >= processingSteps.length) { - // setTimeout(() => { - // completeProcessing(); - // }, 1000); - // } }, 2000); }, 2500); @@ -514,157 +683,176 @@ export default function FilesUpload() {

待审核文件上传

- {/* 文件类型选择 */} - 选择文件类型} className="mb-4"> -
-
- - -
不同类型的文档将应用不同的审核规则
-
-
- - -
优先级影响文档在队列中的处理顺序
-
-
-
- - {/* 文件上传区域 */} - 文件上传} className="mb-4"> - {/* 初始上传区域 */} - {uploadStage === "idle" && ( - - )} - - {/* 上传进度显示 */} - {uploadStage !== "completed" && currentFile && ( - - )} - - {/* 处理步骤显示 */} - {(uploadStage === "processing" || uploadStage === "completed") && ( -
- -
- )} - - {/* 完成后的文件信息 */} - {uploadStage === "completed" && completedFile && ( -
-
-
- - 评查成功 -
-

文件已成功上传并评查完成,请查看结果

+ {/* 文件类型选择和上传表单 */} +
+ {/* 文件类型选择 */} + 选择文件类型} className="mb-4"> +
+
+ + +
不同类型的文档将应用不同的审核规则
- -
-
-

文件信息

-
    -
  • - 文件名: - {completedFile.name} -
  • -
  • - 文件大小: - {formatFileSize(completedFile.size)} -
  • -
  • - 上传时间: - {completedFile.uploadTime} -
  • -
  • - 文件类型: - {FILE_TYPE_LABELS[completedFile.fileType]} -
  • -
  • - 审核规则: - 系统自动选择 -
  • -
+
+ + +
优先级影响文档在队列中的处理顺序
+
+
+ + + {/* 文件上传区域 */} + 文件上传} className="mb-4"> + {/* 初始上传区域 */} + {uploadStage === "idle" && ( + <> + + + {currentFile && ( +
+ +
+ )} + + )} + + {/* 上传进度显示 */} + {uploadStage !== "completed" && currentFile && ( + + )} + + {/* 处理步骤显示 */} + {(uploadStage === "processing" || uploadStage === "completed") && ( +
+ +
+ )} + + {/* 完成后的文件信息 */} + {uploadStage === "completed" && completedFile && ( +
+
+
+ + 评查成功 +
+

文件已成功上传并评查完成,请查看结果

-
-

解析结果预览

-
-
- 合同编号: - XS-2023-1025-001 -
-
- 合同名称: - 烟草制品销售合同 -
-
- 签约日期: - 2023年10月20日 -
-
- 合同金额: - ¥ 1,580,000.00 -
-
- 当事人: - 甲方:XX烟草公司,乙方:YY贸易有限公司 -
+
+
+

文件信息

+
    +
  • + 文件名: + {completedFile.name} +
  • +
  • + 文件大小: + {formatFileSize(completedFile.size)} +
  • +
  • + 上传时间: + {completedFile.uploadTime} +
  • +
  • + 文件类型: + {FILE_TYPE_LABELS[completedFile.fileType]} +
  • +
  • + 审核规则: + 系统自动选择 +
  • +
-
- - +
+

解析结果预览

+
+
+ 合同编号: + XS-2023-1025-001 +
+
+ 合同名称: + 烟草制品销售合同 +
+
+ 签约日期: + 2023年10月20日 +
+
+ 合同金额: + ¥ 1,580,000.00 +
+
+ 当事人: + 甲方:XX烟草公司,乙方:YY贸易有限公司 +
+
+ +
+ + +
-
- )} - + )} + + {/* 上传队列 */} [ { rel: "stylesheet", href: rulesStyles } @@ -34,6 +42,7 @@ export type LoaderData = { currentPage: number; pageSize: number; totalPages: number; + ruleTypes: ApiRuleType[]; // 添加评查点类型 }; // API返回的数据映射到前端模型 @@ -83,6 +92,15 @@ export async function loader({ request }: LoaderFunctionArgs) { }; try { + // 获取评查点类型列表 + const typeResponse = await getRuleTypes(); + + if (typeResponse.error) { + console.error('获取评查点类型失败:', typeResponse.error); + } + + const ruleTypes = typeResponse.error ? [] : typeResponse.data; + // 使用API调用获取数据 const response = await getRulesList(params); @@ -114,7 +132,8 @@ export async function loader({ request }: LoaderFunctionArgs) { totalCount, currentPage: params.page, pageSize: params.pageSize, - totalPages + totalPages, + ruleTypes }, { headers: { "Cache-Control": "max-age=60, s-maxage=180" @@ -138,41 +157,57 @@ export async function action({ request }: LoaderFunctionArgs) { try { if (_action === 'delete') { - // 实际项目中应调用API删除评查点 + // 调用API删除评查点 console.log(`删除评查点 ${ruleId}`); - // 模拟API调用 - // const response = await fetch(`/api/rules/${ruleId}`, { - // method: 'DELETE', - // }); - - // if (!response.ok) { - // throw new Error(`删除失败: ${response.status}`); - // } - - return json({ success: true }); + try { + const deleteResponse = await deleteRule(ruleId as string); + + if (deleteResponse.error) { + throw new Error(deleteResponse.error); + } + + // 删除成功,获取当前URL + const url = new URL(request.url); + // 返回重定向响应,以刷新页面数据 + return redirect(`${url.pathname}${url.search}`); + } catch (error) { + console.error('删除评查点失败:', error); + return Response.json({ + success: false, + error: error instanceof Error ? error.message : "删除失败" + }, { status: 500 }); + } } if (_action === 'duplicate') { - // 实际项目中应调用API复制评查点 + // 复制评查点 console.log(`复制评查点 ${ruleId}`); - // 模拟API调用 - // const response = await fetch(`/api/rules/${ruleId}/duplicate`, { - // method: 'POST', - // }); - - // if (!response.ok) { - // throw new Error(`复制失败: ${response.status}`); - // } - - return json({ success: true }); + try { + const duplicateResponse = await duplicateRule(ruleId as string); + + if (duplicateResponse.error) { + throw new Error(duplicateResponse.error); + } + + // 复制成功,获取当前URL + const url = new URL(request.url); + // 返回重定向响应,以刷新页面数据 + return redirect(`${url.pathname}${url.search}`); + } catch (error) { + console.error('复制评查点失败:', error); + return Response.json({ + success: false, + error: error instanceof Error ? error.message : "复制失败" + }, { status: 500 }); + } } - return json({ success: false, error: "未知操作" }, { status: 400 }); + return Response.json({ success: false, error: "未知操作" }, { status: 400 }); } catch (error) { console.error('操作评查点失败:', error); - return json({ success: false, error: "操作失败" }, { status: 500 }); + return Response.json({ success: false, error: "操作失败" }, { status: 500 }); } } @@ -186,15 +221,7 @@ export function ErrorBoundary() { ); } -// 规则类型和优先级的描述标签映射 -const typeLabels = { - 'essential': '基本要素类', - 'content': '内容合规类', - 'legal': '法律风险类', - 'format': '格式规范类', - 'business': '业务专项类' -}; - +// 规则优先级的描述标签映射 const priorityLabels = { 'high': '高', 'medium': '中', @@ -202,22 +229,81 @@ const priorityLabels = { }; export default function RulesIndex() { - const { rules, totalCount, currentPage, pageSize } = useLoaderData(); + const loaderData = useLoaderData(); + const { rules, totalCount, currentPage, pageSize } = loaderData; + const ruleTypes = loaderData.ruleTypes || []; // 添加默认空数组避免undefined const [searchParams, setSearchParams] = useSearchParams(); const submit = useSubmit(); // 状态管理 const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); const [ruleToDelete, setRuleToDelete] = useState(null); + const [ruleGroups, setRuleGroups] = useState([]); + const [loadingGroups, setLoadingGroups] = useState(false); + + // 判断是否禁用规则组选择 + const isRuleGroupSelectDisabled = loadingGroups || !searchParams.get('ruleType') || ruleGroups.length === 0; + + // 当评查点类型变化时,加载对应的规则组 + useEffect(() => { + const selectedType = searchParams.get('ruleType'); + + // 如果选择了"全部"或未选择,则清空规则组 + if (!selectedType || selectedType === 'all') { + setRuleGroups([]); + return; + } + + // 加载当前类型的规则组 + const loadRuleGroups = async () => { + setLoadingGroups(true); + try { + const response = await getRuleGroupsByType(selectedType); + if (response.data) { + setRuleGroups(response.data); + } else if (response.error) { + console.error('加载规则组失败:', response.error); + setRuleGroups([]); + } + } catch (error) { + console.error('加载规则组出错:', error); + setRuleGroups([]); + } finally { + setLoadingGroups(false); + } + }; + + loadRuleGroups(); + }, [searchParams.get('ruleType')]); const handleFilterChange = (e: React.ChangeEvent) => { const { name, value } = e.target; const newParams = new URLSearchParams(searchParams); + // 如果是规则组选择,但是当前应该被禁用,则不处理 + if (name === 'groupId' && isRuleGroupSelectDisabled) { + return; + } + if (value) { newParams.set(name, value); + + // 如果是评查点类型变更,清空规则组选择 + if (name === 'ruleType') { + newParams.delete('groupId'); + // 如果选择了"全部"或空值,也清空规则组选择 + if (value === '' || value === 'all') { + setRuleGroups([]); + } + } } else { newParams.delete(name); + + // 如果清除评查点类型,也清除规则组 + if (name === 'ruleType') { + newParams.delete('groupId'); + setRuleGroups([]); + } } // 切换筛选条件时,重置到第一页 @@ -241,6 +327,7 @@ export default function RulesIndex() { }; const handleDeleteClick = (rule: Rule) => { + console.log("handleDELETEclick",rule) setRuleToDelete(rule); setShowDeleteConfirm(true); }; @@ -311,9 +398,9 @@ export default function RulesIndex() { render: (_: unknown, record: Rule) => { const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor; return ( - - {typeLabels[record.ruleType as keyof typeof typeLabels] || RULE_TYPE_LABELS[record.ruleType]} - + record.ruleType ? + {record.ruleType} + : null ); } }, @@ -393,11 +480,10 @@ export default function RulesIndex() { name="ruleType" value={searchParams.get('ruleType') || ''} options={[ - { value: "essential", label: "基本要素类" }, - { value: "content", label: "内容合规类" }, - { value: "format", label: "格式规范类" }, - { value: "legal", label: "法律风险类" }, - { value: "business", label: "业务专项类" } + ...ruleTypes.map((type: ApiRuleType) => ({ + value: type.id, + label: type.name + })) ]} onChange={handleFilterChange} className="mr-3 w-60 " @@ -408,14 +494,14 @@ export default function RulesIndex() { name="groupId" value={searchParams.get('groupId') || ''} options={[ - { value: "1", label: "合同基本要素类检查" }, - { value: "2", label: "销售合同专项检查" }, - { value: "3", label: "采购合同专项检查" }, - { value: "4", label: "专卖许可证审核规则" }, - { value: "5", label: "行政处罚规范性检查" } + ...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []), + ...ruleGroups.map(group => ({ + value: group.id, + label: group.name + })) ]} onChange={handleFilterChange} - className="mr-3 w-60" + className={`mr-3 w-60 ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`} />