diff --git a/app/api/evaluation_points/rules.ts b/app/api/evaluation_points/rules.ts index 830191e..558ee9d 100644 --- a/app/api/evaluation_points/rules.ts +++ b/app/api/evaluation_points/rules.ts @@ -1,6 +1,27 @@ import { postgrestGet, postgrestPost, postgrestPut, postgrestDelete, type PostgrestParams } from '../postgrest-client'; import dayjs from 'dayjs'; +/** + * 从不同格式的 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; +} + + /** * 评查点列表查询参数 */ @@ -927,4 +948,509 @@ export async function getRuleGroupsByType(typeId: string): Promise<{data: RuleGr status: 500 }; } +} + +// 定义提取配置类型 +interface ExtractionConfigType { + llm: { + fields: string[]; + prompt_setting: { + type: string; + template: string; + }; + }; + vlm: { + fields: Array; + prompt_setting: { + type: string; + template: string; + }; + }; + regex: { + fields: Array<{ field: string; pattern: string }>; + }; +} + +// 定义转换后的评查点数据类型 +interface FormattedEvaluationPoint { + id: number; // 修改为只接受 number 类型 + name: string; + code: string; + risk: string; + is_enabled: boolean; + description: string; + references_laws: Record | null; + evaluation_point_groups_pid: number | null; + evaluation_point_groups_id: number | null; + extraction_config: ExtractionConfigType; + evaluation_config: { + logicType: string; + customLogic: string; + rules: Array<{ + id: string; + type: string; + config: Record; + }>; + }; + pass_message: string; + fail_message: string; + suggestion_message: string; + suggestion_message_type: string; + post_action: string; + action_config: string; + score: number; +} + +/** + * 将 API 返回的数据格式转换为前端需要的格式 + * @param apiRule API 返回的评查点数据 + * @returns 转换后的前端格式数据 + */ +export function convertApiRuleToFormData(apiRule: ApiRule): FormattedEvaluationPoint { + // 提取旧格式的字段数据 + const extractFields = (): ExtractionConfigType => { + const fields: ExtractionConfigType = { + llm: { fields: [], prompt_setting: { type: 'system', template: '' } }, + vlm: { fields: [], prompt_setting: { type: 'system', template: '' } }, + regex: { fields: [] } + }; + + if (!apiRule.extraction_config) return fields; + + // 处理不同的字段类型 + if (apiRule.extraction_config.type === 'OCR+LLM' || apiRule.extraction_config.type === 'LLM') { + fields.llm.fields = apiRule.extraction_config.fields || []; + if (apiRule.extraction_config.prompt_setting) { + fields.llm.prompt_setting = { + type: apiRule.extraction_config.prompt_setting.type || 'system', + template: apiRule.extraction_config.prompt_setting.template || '' + }; + } + } else if (apiRule.extraction_config.type === 'VLM') { + fields.vlm.fields = apiRule.extraction_config.fields || []; + if (apiRule.extraction_config.prompt_setting) { + fields.vlm.prompt_setting = { + type: apiRule.extraction_config.prompt_setting.type || 'system', + template: apiRule.extraction_config.prompt_setting.template || '' + }; + } + } else if (apiRule.extraction_config.type === 'REGEX') { + // 将字符串字段转换为 regex 格式对象 + fields.regex.fields = apiRule.extraction_config.fields.map(field => ({ + field, + pattern: '' + })); + } + + return fields; + }; + + // 转换后的数据 + const formattedData: FormattedEvaluationPoint = { + id: typeof apiRule.id === 'string' ? parseInt(apiRule.id, 10) : apiRule.id, // 确保 id 是数字 + name: apiRule.name, + code: apiRule.code, + risk: apiRule.risk, + is_enabled: apiRule.is_enabled, + description: apiRule.description, + references_laws: apiRule.references_laws, + evaluation_point_groups_pid: apiRule.evaluation_point_groups?.first_name ? null : null, + evaluation_point_groups_id: apiRule.evaluation_point_groups_id, + extraction_config: extractFields(), + evaluation_config: { + logicType: apiRule.evaluation_config?.logicType || 'and', + customLogic: '', + rules: apiRule.evaluation_config?.rules || [] + }, + pass_message: apiRule.pass_message || '', + fail_message: apiRule.fail_message || '', + suggestion_message: apiRule.suggestion_message || '', + suggestion_message_type: apiRule.suggestion_message_type || 'warning', + post_action: apiRule.post_action || 'none', + action_config: apiRule.action_config || '', + score: 0 + }; + + return formattedData; +} + +/** + * 获取单个评查点数据 + * @param id 评查点ID + * @returns 评查点数据 + */ +export async function getEvaluationPoint(id: number): Promise<{ + data?: FormattedEvaluationPoint; + error?: string; + status?: number; +}> { + try { + console.log(`获取评查点数据,ID: ${id}`); + + // 使用 postgrestGet 替代直接调用 fetch + const postgrestParams: PostgrestParams = { + select: `*`, + filter: { + 'id': `eq.${id}` + } + }; + + const response = await postgrestGet<{code: number; msg: string; data: ApiRule[]} | ApiRule[]>('evaluation_points', postgrestParams); + + if (response.error) { + return { + error: response.error, + status: response.status + }; + } + + // 使用 extractApiData 统一处理响应数据 + const extractedData = extractApiData(response.data); + + if (extractedData && Array.isArray(extractedData) && extractedData.length > 0) { + // 转换数据为前端格式 + const formattedData = convertApiRuleToFormData(extractedData[0]); + return { data: formattedData }; + } else { + return { + error: '获取数据失败: 返回数据为空', + status: 404 + }; + } + } catch (error) { + console.error('获取评查点数据失败:', error); + return { + error: error instanceof Error ? error.message : '获取评查点数据失败', + status: 500 + }; + } +} + +/** + * 获取评查点组数据 + * @returns 评查点组列表 + */ +export async function getEvaluationPointGroups(): Promise<{ + data?: Array<{ + id: number; + pid: number; + code: string; + name: string; + description: string; + is_enabled: boolean; + created_at: string; + updated_at: string; + }>; + error?: string; + status?: number; +}> { + try { + console.log("获取评查点组数据"); + + // 使用 postgrestGet 替代直接调用 fetch + const postgrestParams: PostgrestParams = { + select: `*` + }; + + // 定义评查点组类型 + type EvaluationPointGroupType = { + id: number; + pid: number; + code: string; + name: string; + description: string; + is_enabled: boolean; + created_at?: string; + updated_at?: string; + }; + + const response = await postgrestGet<{code: number; msg: string; data: EvaluationPointGroupType[]} | EvaluationPointGroupType[]>('evaluation_point_groups', postgrestParams); + + if (response.error) { + return { + error: response.error, + status: response.status + }; + } + + // 使用 extractApiData 统一处理响应数据 + const extractedData = extractApiData(response.data); + + if (extractedData) { + // 确保每个组都有 created_at 和 updated_at 字段 + const currentTime = new Date().toISOString(); + const formattedGroups = extractedData.map(group => ({ + ...group, + created_at: group.created_at || currentTime, + updated_at: group.updated_at || currentTime + })); + + return { data: formattedGroups }; + } else { + return { + error: '获取评查点组数据失败: 返回数据为空', + status: 404 + }; + } + } catch (error) { + console.error('获取评查点组数据失败:', error); + return { + error: error instanceof Error ? error.message : '获取评查点组数据失败', + status: 500 + }; + } +} + +// 用于评查点输入的接口 +interface EvaluationPointInput { + id?: number | string; + name?: string; + code?: string; + risk?: string; + is_enabled?: boolean; + description?: string; + references_laws?: Record | null; + evaluation_point_groups_pid?: number | null; + evaluation_point_groups_id?: number | null; + extraction_config?: { + llm?: { + fields?: string[]; + prompt_setting?: { + type?: string; + template?: string; + }; + }; + vlm?: { + fields?: Array; + prompt_setting?: { + type?: string; + template?: string; + }; + }; + regex?: { + fields?: Array<{ field: string, pattern: string }>; + }; + }; + evaluation_config?: { + logicType?: string; + customLogic?: string; + rules?: Array<{ + id: string; + type: string; + config: Record; + }>; + }; + pass_message?: string; + fail_message?: string; + suggestion_message?: string; + suggestion_message_type?: string; + post_action?: string; + action_config?: string; + score?: number; +} + +/** + * 保存评查点数据 + * @param evaluationPoint 评查点数据 + * @param isEditMode 是否为编辑模式 + * @returns 保存结果 + */ +export async function saveEvaluationPoint(evaluationPoint: EvaluationPointInput, isEditMode: boolean): Promise<{ + data?: FormattedEvaluationPoint[]; + error?: string; + status?: number; +}> { + try { + console.log(`${isEditMode ? '更新' : '创建'}评查点数据`); + + // 创建一个符合数据库模式的数据副本 + const cleanedData = { + id: evaluationPoint.id, + name: evaluationPoint.name?.trim(), + code: evaluationPoint.code?.trim(), + risk: evaluationPoint.risk || 'low', + is_enabled: evaluationPoint.is_enabled !== undefined ? evaluationPoint.is_enabled : true, + description: evaluationPoint.description || '', + references_laws: evaluationPoint.references_laws || null, + evaluation_point_groups_pid: evaluationPoint.evaluation_point_groups_pid || null, + evaluation_point_groups_id: evaluationPoint.evaluation_point_groups_id || null, + extraction_config: { + llm: { + fields: Array.isArray(evaluationPoint.extraction_config?.llm?.fields) ? + [...evaluationPoint.extraction_config.llm.fields] : [], + prompt_setting: { + type: evaluationPoint.extraction_config?.llm?.prompt_setting?.type || 'system', + template: evaluationPoint.extraction_config?.llm?.prompt_setting?.template || '' + } + }, + vlm: { + fields: Array.isArray(evaluationPoint.extraction_config?.vlm?.fields) ? + [...evaluationPoint.extraction_config.vlm.fields] : [], + prompt_setting: { + type: evaluationPoint.extraction_config?.vlm?.prompt_setting?.type || 'system', + template: evaluationPoint.extraction_config?.vlm?.prompt_setting?.template || '' + } + }, + regex: { + fields: Array.isArray(evaluationPoint.extraction_config?.regex?.fields) ? + [...evaluationPoint.extraction_config.regex.fields] : [] + } + }, + evaluation_config: { + logicType: evaluationPoint.evaluation_config?.logicType || 'and', + customLogic: evaluationPoint.evaluation_config?.customLogic || '', + rules: Array.isArray(evaluationPoint.evaluation_config?.rules) ? + evaluationPoint.evaluation_config.rules.map((rule) => ({ + id: rule.id || '1', + type: rule.type || '', + config: rule.config || {} + })) : [] + }, + pass_message: evaluationPoint.pass_message || '文档检查通过,符合规范要求。', + fail_message: evaluationPoint.fail_message || '文档存在以下问题,请修改后重新提交。', + suggestion_message: evaluationPoint.suggestion_message || '', + suggestion_message_type: evaluationPoint.suggestion_message_type || 'warning', + post_action: evaluationPoint.post_action || 'none', + action_config: evaluationPoint.action_config || '', + score: evaluationPoint.score !== undefined ? Number(evaluationPoint.score) : 0 + }; + + // 如果是新建模式,则删除id字段 + if (!isEditMode) { + delete cleanedData.id; + } + + // 确保配置对象中的规则配置被正确处理 + if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) { + cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules + .filter(rule => rule && rule.type) // 确保规则有类型 + .map(rule => { + // 根据规则类型确保config中有必要的字段 + const config = { ...rule.config }; + + // 移除辅助字段(同rules.new.tsx中的逻辑) + switch (rule.type) { + case 'exists': + if (!Array.isArray(config.fields)) config.fields = []; + if (!config.logic) config.logic = 'and'; + delete config.availableFields; + delete config.selectedFields; + delete config.existsLogic; + break; + + case 'consistency': + if (!Array.isArray(config.pairs)) config.pairs = []; + if (!config.logic) config.logic = 'and'; + delete config.availableFields; + delete config.logicRelation; + delete config.initialSourceField; + delete config.initialTargetField; + delete config.initialCompareMethod; + break; + + case 'format': + if (!config.field) config.field = ''; + if (!config.formatType) config.formatType = 'date'; + if (!config.parameters) config.parameters = ''; + delete config.availableFields; + delete config.checkField; + delete config.formatParams; + break; + + case 'logic': + if (!Array.isArray(config.conditions)) config.conditions = []; + if (!config.logic) config.logic = 'and'; + delete config.availableFields; + delete config.logicRelation; + delete config.initialField; + delete config.initialOperator; + delete config.initialValue; + break; + + case 'regex': + if (!config.field) config.field = ''; + if (!config.pattern) config.pattern = ''; + if (!config.matchType) config.matchType = 'match'; + delete config.availableFields; + delete config.checkField; + delete config.regexPattern; + break; + + case 'ai': + if (!config.model) config.model = 'qwen14b'; + if (typeof config.temperature !== 'number') config.temperature = 0.1; + if (!config.prompt) config.prompt = ''; + delete config.availableFields; + break; + + case 'code': + if (!config.language) config.language = 'javascript'; + if (!config.code) config.code = ''; + delete config.availableFields; + break; + } + + return { + id: rule.id, + type: rule.type, + config + }; + }); + } + + console.log("准备发送到API的数据大小:", JSON.stringify(cleanedData).length, "字节"); + + // 使用 postgrest-client 替代直接 fetch 调用 + let response; + + if (isEditMode) { + // 更新操作 + response = await postgrestPut<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>( + `evaluation_points`, + cleanedData, + {id: cleanedData.id!} + ); + } else { + // 创建操作 + response = await postgrestPost<{code: number; msg: string; data: ApiRule} | ApiRule, typeof cleanedData>( + 'evaluation_points', + cleanedData + ); + } + + // 处理错误响应 + if (response.error) { + return { + error: response.error, + status: response.status + }; + } + + // 使用 extractApiData 统一处理响应数据 + const extractedData = extractApiData(response.data); + + if (extractedData) { + // 转换数据格式后返回 + if (Array.isArray(extractedData)) { + return { + data: extractedData.map(rule => convertApiRuleToFormData(rule)) + }; + } else { + return { + data: [convertApiRuleToFormData(extractedData)] + }; + } + } else { + return { + error: `${isEditMode ? '更新' : '创建'}评查点数据失败: 返回数据为空`, + status: 404 + }; + } + } 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/files.ts b/app/api/files.ts index c7feaed..a699beb 100644 --- a/app/api/files.ts +++ b/app/api/files.ts @@ -51,4 +51,83 @@ export async function uploadFileToServer( } catch (error) { return { success: false, error: error instanceof Error ? error.message : '上传失败' }; } +} + +/** + * 上传文件到文档审核系统 + */ +export async function uploadDocumentToServer( + binaryData: ArrayBuffer, + fileName: string, + fileType: string, + typeId: string | number, + priority: string, + documentNumber?: string | null, + remark?: string | null, + isTestDocument: boolean = false +): Promise<{ + success: boolean; + result?: { + id: number; + file_name: string; + file_size: number; + file_url: string; + type_id: number; + type_description: string; + document_number: string | null; + storage_type: string; + is_test_document: boolean; + remark: string | null; + background_processing: boolean; + evaluation_level: string; + }; + error: string | null; +}> { + try { + // 创建FormData对象 + const formData = new FormData(); + + // 将二进制数据转换为Blob并添加到FormData + const blob = new Blob([binaryData], { type: fileType }); + formData.append('file', blob, fileName); + + // 将信息添加到一个JSON对象中 + const uploadInfo = { + type_id: Number(typeId), + evaluation_level: priority, + document_number: documentNumber || null, + remark: remark || null, + is_test_document: isTestDocument + }; + + // 添加JSON字符串到FormData + formData.append('upload_info', JSON.stringify(uploadInfo)); + + // 发送请求 + const response = await fetch('http://172.16.0.55:8000/admin/documents/upload', { + method: 'POST', + headers: { + 'X-File-Name': encodeURIComponent(fileName) + }, + body: formData + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`上传失败 (${response.status}): ${errorText}`); + return { + success: false, + error: `上传失败: ${response.status} ${response.statusText}` + }; + } + + const data = await response.json(); + return data; + } catch (error) { + console.error('上传错误:', error); + return { + success: false, + error: error instanceof Error ? error.message : '上传失败' + }; + } } \ No newline at end of file diff --git a/app/api/postgrest-client.ts b/app/api/postgrest-client.ts index dbf6d38..053105d 100644 --- a/app/api/postgrest-client.ts +++ b/app/api/postgrest-client.ts @@ -503,7 +503,7 @@ function preprocessData(data: Record): Record export async function postgrestPut( endpoint: string, data: D, - filters?: Record + filters?: Record ): Promise<{data: T; error?: never} | {data?: never; error: string; status?: number}> { try { // 确保端点没有前导斜杠 diff --git a/app/components/layout/Sidebar.tsx b/app/components/layout/Sidebar.tsx index 51ad4b4..82099f3 100644 --- a/app/components/layout/Sidebar.tsx +++ b/app/components/layout/Sidebar.tsx @@ -84,7 +84,7 @@ export function Sidebar({ onToggle, collapsed }: SidebarProps) { { id: 'rule-new', title: '新增评查点', - path: '/rules/new', + path: '/rules-new', icon: 'ri-add-circle-line' }, { diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index 1b9ee29..de85a0b 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -238,7 +238,7 @@ export function ReviewSettings({ useEffect(() => { // 如果已经初始化过,则跳过此次处理 if (initializedRef.current) { - console.log("ReviewSettings已初始化,跳过后续初始化处理"); + // console.log("ReviewSettings已初始化,跳过后续初始化处理"); return; } @@ -1717,7 +1717,7 @@ export function ReviewSettings({ useEffect(() => { // 如果有初始数据,在组件挂载后主动发送一次完整规则配置 if (initialDataRef.current && onChange) { - console.log("组件挂载后发送初始完整配置"); + // console.log("组件挂载后发送初始完整配置"); setTimeout(() => generateEvaluationConfig(), 100); } }, [generateEvaluationConfig, onChange]); diff --git a/app/routes/files.upload.tsx b/app/routes/files.upload.tsx index 2c2d159..a3e5741 100644 --- a/app/routes/files.upload.tsx +++ b/app/routes/files.upload.tsx @@ -9,6 +9,7 @@ import { FileProgress} from "~/components/ui/FileProgress"; import { ProcessingSteps, Step } from "~/components/ui/ProcessingSteps"; import uploadStyles from "~/styles/pages/files_upload.css?url"; import { getTodayDocuments, getDocumentTypes, getDocumentsStatus, type Document, type DocumentType, DocumentStatus } from "~/api/files/files-upload"; +import { uploadFileToBinary, uploadDocumentToServer } from "~/api/files"; export function links() { return [ @@ -109,99 +110,34 @@ interface FileUploadResponse { error: string | null; } -// 将文件转换为二进制数据 -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 + priority: Priority, + documentNumber: string | null, + remark: string | null, + isTestDocument: boolean ): Promise { // 在实际应用中,这里会使用fetch或axios发送请求到后端API console.log(`[模拟API] 上传文件: ${fileName}, 大小: ${binaryData.byteLength} 字节`); - // 模拟网络延迟 - await new Promise(resolve => setTimeout(resolve, 500)); - try { - // 创建FormData对象,将文件和其他信息一起提交 - const formData = new FormData(); + // 使用封装的上传函数 + const response = await uploadDocumentToServer( + binaryData, + fileName, + fileType, + documentType, + PRIORITY_TO_CHINESE[priority], + documentNumber, + remark, + isTestDocument + ); - // 将二进制数据转换为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: { - // FormData会自动设置Content-Type为multipart/form-data - 'X-File-Name': encodeURIComponent(fileName) - }, - body: formData - }; - - // 打印 FormData 内容的正确方式 - console.log('[模拟API] 请求参数:', { - url: requestParams.url, - headers: requestParams.headers, - 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: 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 response; } catch (error) { console.error('[模拟API] 上传错误:', error); return { @@ -318,8 +254,11 @@ export default function FilesUpload() { const { documents, documentTypes } = useLoaderData(); // 状态管理 + const [isTestDocument, setIsTestDocument] = useState(false); const [fileType, setFileType] = useState(""); const [priority, setPriority] = useState(Priority.NORMAL); + const [documentNumber, setDocumentNumber] = useState(""); + const [remark, setRemark] = useState(""); const [currentFiles, setCurrentFiles] = useState([]); const [uploadProgress, setUploadProgress] = useState(0); const [uploadSpeed, setUploadSpeed] = useState("0KB/s"); @@ -506,7 +445,10 @@ export default function FilesUpload() { file.name, file.type, fileType as FileType, - priority + priority, + documentNumber || null, + remark || null, + isTestDocument ); if (!response.success || !response.result) { @@ -801,6 +743,8 @@ export default function FilesUpload() { return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; }; + + // 格式化文件大小显示 const formatFileSize = (bytes: number) => { @@ -920,7 +864,7 @@ export default function FilesUpload() {
{/* 文件类型选择 */} 选择文件类型} className="mb-4"> -
+
setDocumentNumber(e.target.value)} + disabled={uploadStage !== "idle"} + /> +
如无编号可留空,系统将自动识别
+
+
+
+
+ + +
@@ -975,6 +950,17 @@ export default function FilesUpload() { tipText="支持单个或批量上传,文件格式:PDF、Word、Excel、图片" shouldPreventFileSelect={!fileType} /> +
+ + 标记为测试文档(不计入正式统计) +
)} @@ -1082,6 +1068,8 @@ export default function FilesUpload() {
)} + +
diff --git a/app/routes/rule.new.tsx b/app/routes/rule.new.tsx deleted file mode 100644 index 482b356..0000000 --- a/app/routes/rule.new.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { type MetaFunction, LinksFunction } from "@remix-run/node"; -import { useState } from "react"; -import { BasicInfo } from "~/components/rules/new/BasicInfo"; -import { ExtractionSettings } from "~/components/rules/new/ExtractionSettings"; -import { ReviewSettings, RuleContext } from "~/components/rules/new/ReviewSettings"; -import { ActionButtons } from "~/components/rules/new/ActionButtons"; -import { PageHeader } from "~/components/rules/new/PageHeader"; -import rulesStyles from "~/styles/rules.css"; - -export const meta: MetaFunction = () => { - return [ - { title: "新增评查点 - 中国烟草AI合同及卷宗审核系统" }, - { - name: "description", - content: "创建新的评查点,设置规则参数" - } - ]; -}; - -export const links: LinksFunction = () => [ - { rel: "stylesheet", href: rulesStyles } -]; - -export const handle = { - breadcrumb: "新增评查点" -}; - -export default function RuleNew() { - // 用于保存抽取字段的状态,在抽取设置和评查设置组件之间共享 - const [extractionFields, setExtractionFields] = useState([]); - - const updateExtractionFields = (fields: string[]) => { - setExtractionFields(fields); - }; - - const handleSave = () => { - // 实现保存逻辑 - console.log('保存评查点'); - }; - - const handleSaveDraft = () => { - // 实现保存草稿逻辑 - console.log('保存为草稿'); - }; - - const handleExtractionChange = (data: Record) => { - // 使用合并后的所有字段列表 - if (data.allFields && Array.isArray(data.allFields)) { - updateExtractionFields(data.allFields); - return; - } - - // 旧版本兼容逻辑 - if (data.fields) { - // 尝试获取合并的字段列表 - if (Array.isArray(data.fields)) { - updateExtractionFields(data.fields); - } else { - const fieldData = data.fields as Record; - const currentMethod = data.extractionMethod as string; - - // 提取当前抽取方法的字段 - if (fieldData[currentMethod]) { - updateExtractionFields(fieldData[currentMethod]); - } - } - } else if (data.regexFields) { - // 处理正则字段情况 - const regexFields = data.regexFields as { id: string; fieldName: string; regex: string }[]; - const fieldNames = regexFields - .map(field => field.fieldName) - .filter(name => name.trim() !== ''); - - updateExtractionFields(fieldNames); - } - }; - - return ( - -
- -
- -
-
- - - - - - - -
-
-
- ); -} \ No newline at end of file diff --git a/app/routes/rules.new.tsx b/app/routes/rules-new.tsx similarity index 52% rename from app/routes/rules.new.tsx rename to app/routes/rules-new.tsx index 4731500..2515587 100644 --- a/app/routes/rules.new.tsx +++ b/app/routes/rules-new.tsx @@ -36,18 +36,14 @@ import rulesStyles from "~/styles/rules.css?url"; import { useNavigate, useLocation } from "@remix-run/react"; // 导入评查点模型定义和常量 import type { - EvaluationPoint, - LogicOperator, - CompareMethod, - FormatType, - ComparisonOperator, - MatchType, - ProgrammingLanguage + EvaluationPoint } from "~/models/evaluation_points"; import { EVALUATION_OPTIONS } from "~/models/evaluation_points"; import type { EvaluationPointGroup } from "~/models/evaluation_point_groups"; // 导入RuleContext上下文 import { RuleContext } from "~/contexts/RuleContext"; +// 导入API函数 +import { getEvaluationPoint, getEvaluationPointGroups, saveEvaluationPoint } from "~/api/evaluation_points/rules"; export const meta: MetaFunction = () => { return [ @@ -59,76 +55,14 @@ export const meta: MetaFunction = () => { ]; }; -export function links() { - return [{ rel: "stylesheet", href: rulesStyles }]; -} - export const handle = { breadcrumb: "评查点管理" }; -// 添加规则配置接口 -interface BaseRuleConfig { - availableFields?: string[]; +export function links() { + return [{ rel: "stylesheet", href: rulesStyles }]; } -interface ExistsRuleConfig extends BaseRuleConfig { - fields: string[]; - logic: LogicOperator; - selectedFields?: string[]; - existsLogic?: string; -} - -interface ConsistencyRuleConfig extends BaseRuleConfig { - pairs: Array<{sourceField: string; targetField: string; compareMethod: CompareMethod}>; - logic: LogicOperator; - logicRelation?: string; - initialSourceField?: string; - initialTargetField?: string; - initialCompareMethod?: string; -} - -interface FormatRuleConfig extends BaseRuleConfig { - field: string; - formatType: FormatType; - parameters: string; - checkField?: string; - formatParams?: string; -} - -interface LogicRuleConfig extends BaseRuleConfig { - conditions: { - field: string; - operator: ComparisonOperator; - value: string; - }[]; - logic: LogicOperator; - logicRelation?: string; - initialField?: string; - initialOperator?: string; - initialValue?: string; -} - -interface RegexRuleConfig extends BaseRuleConfig { - field: string; - pattern: string; - matchType: MatchType; - checkField?: string; - regexPattern?: string; -} - -interface AIRuleConfig extends BaseRuleConfig { - model: string; - temperature: number; - prompt: string; -} - -interface CodeRuleConfig extends BaseRuleConfig { - language: ProgrammingLanguage; - code: string; -} - -type RuleConfig = ExistsRuleConfig | ConsistencyRuleConfig | FormatRuleConfig | LogicRuleConfig | RegexRuleConfig | AIRuleConfig | CodeRuleConfig; export default function RuleNew() { const navigate = useNavigate(); @@ -235,33 +169,31 @@ export default function RuleNew() { try { setIsLoading(true); console.log(`获取评查点数据,ID: ${id}`); - const response = await fetch(`http://172.16.0.119:9000/admin/evaluation_points?id=eq.${id}`, { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - console.log(response); - if (response.ok) { - const data = await response.json(); - if (data.data && data.data[0]) { - setFormData(data.data[0]); - - // 初始化extractionFields - const extractedFields = extractFieldsFromFormData(data.data[0]); - setExtractionFields(extractedFields); - - // 设置编辑模式的实例键 - setInstanceKey(`edit_${id}_${Date.now()}`); - } else { - console.error('获取数据失败: 返回数据为空'); - alert('获取数据失败: 返回数据为空'); - resetFormData(); - navigate('/rules'); - } + + const response = await getEvaluationPoint(id); + + if (response.error) { + console.error('获取评查点数据失败:', response.error); + alert(`获取评查点数据失败: ${response.error}`); + resetFormData(); + navigate('/rules'); + return; + } + + if (response.data) { + setFormData(response.data as EvaluationPoint); + + // 初始化extractionFields + const extractedFields = extractFieldsFromFormData(response.data as EvaluationPoint); + setExtractionFields(extractedFields); + + // 设置编辑模式的实例键 + setInstanceKey(`edit_${id}_${Date.now()}`); } else { - throw new Error(`响应状态: ${response.status}`); + console.error('获取数据失败: 返回数据为空'); + alert('获取数据失败: 返回数据为空'); + resetFormData(); + navigate('/rules'); } } catch (error) { console.error('获取评查点数据失败:', error); @@ -281,17 +213,20 @@ export default function RuleNew() { const fetchEvaluationPointGroups = useCallback(async () => { try { console.log("获取评查点组数据"); - const response = await fetch("http://172.16.0.119:9000/admin/evaluation_point_groups", { - method: 'GET', - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } - }); - console.log(response); - if (response.ok) { - const data = await response.json(); - setEvaluationPointGroups(data.data); + + const response = await getEvaluationPointGroups(); + + if (response.error) { + console.error('获取评查点组数据失败:', response.error); + alert(`获取评查点组数据失败: ${response.error}\n将使用默认数据`); + return; + } + + if (response.data) { + setEvaluationPointGroups(response.data); + } else { + console.error('获取评查点组数据失败: 返回数据为空'); + alert('获取评查点组数据失败: 返回数据为空\n将使用默认数据'); } } catch (error) { console.error('获取评查点组数据失败:', error); @@ -317,198 +252,18 @@ export default function RuleNew() { // 显示保存中状态 setIsLoading(true); - // 根据模式决定是创建还是更新 - const apiMethod = isEditMode ? 'PATCH' : 'POST'; - const apiUrl = isEditMode - ? `http://172.16.0.119:9000/admin/evaluation_points?id=eq.${formData.id}` - : 'http://172.16.0.119:9000/admin/evaluation_points'; - - try { - // 创建一个符合数据库模式的数据副本 - const cleanedData = { - id: formData.id, - name: formData.name?.trim(), - code: formData.code?.trim(), - risk: formData.risk || 'low', - is_enabled: formData.is_enabled !== undefined ? formData.is_enabled : true, - description: formData.description || '', - references_laws: formData.references_laws || null, - evaluation_point_groups_pid: formData.evaluation_point_groups_pid || null, - evaluation_point_groups_id: formData.evaluation_point_groups_id || null, - extraction_config: { - llm: { - fields: Array.isArray(formData.extraction_config?.llm?.fields) ? - [...formData.extraction_config.llm.fields] : [], - prompt_setting: { - type: formData.extraction_config?.llm?.prompt_setting?.type || 'system', - template: formData.extraction_config?.llm?.prompt_setting?.template || '' - } - }, - vlm: { - fields: Array.isArray(formData.extraction_config?.vlm?.fields) ? - [...formData.extraction_config.vlm.fields] : [], - prompt_setting: { - type: formData.extraction_config?.vlm?.prompt_setting?.type || 'system', - template: formData.extraction_config?.vlm?.prompt_setting?.template || '' - } - }, - regex: { - fields: Array.isArray(formData.extraction_config?.regex?.fields) ? - [...formData.extraction_config.regex.fields] : [] - } - }, - evaluation_config: { - logicType: formData.evaluation_config?.logicType || 'and', - customLogic: formData.evaluation_config?.customLogic || '', - rules: Array.isArray(formData.evaluation_config?.rules) ? - formData.evaluation_config.rules.map(rule => ({ - id: rule.id || '1', - type: rule.type || '', - config: rule.config || {} - })) : [] - }, - pass_message: formData.pass_message || '文档检查通过,符合规范要求。', - fail_message: formData.fail_message || '文档存在以下问题,请修改后重新提交。', - suggestion_message: formData.suggestion_message || '', - suggestion_message_type: formData.suggestion_message_type || 'warning', - post_action: formData.post_action || 'none', - action_config: formData.action_config || '', - score: formData.score !== undefined ? Number(formData.score) : 0 - }; - - // 确保rules中的每个配置对象都被正确处理 - if (cleanedData.evaluation_config && Array.isArray(cleanedData.evaluation_config.rules)) { - cleanedData.evaluation_config.rules = cleanedData.evaluation_config.rules - .filter(rule => rule && rule.type) // 确保规则有类型 - .map(rule => { - // 根据规则类型确保config中有必要的字段 - const config = { ...rule.config } as RuleConfig; - - switch (rule.type) { - case 'exists': - if (!Array.isArray((config as ExistsRuleConfig).fields)) (config as ExistsRuleConfig).fields = []; - if (!(config as ExistsRuleConfig).logic) (config as ExistsRuleConfig).logic = 'and'; - // 删除不必要的字段 - delete (config as ExistsRuleConfig & {availableFields?: string}).availableFields; - delete (config as ExistsRuleConfig).selectedFields; - delete (config as ExistsRuleConfig).existsLogic; - break; - - case 'consistency': - if (!Array.isArray((config as ConsistencyRuleConfig).pairs)) (config as ConsistencyRuleConfig).pairs = []; - if (!(config as ConsistencyRuleConfig).logic) (config as ConsistencyRuleConfig).logic = 'and'; - delete (config as ConsistencyRuleConfig & {availableFields?: string}).availableFields; - delete (config as ConsistencyRuleConfig).logicRelation; - delete (config as ConsistencyRuleConfig).initialSourceField; - delete (config as ConsistencyRuleConfig).initialTargetField; - delete (config as ConsistencyRuleConfig).initialCompareMethod; - break; - - case 'format': - if (!(config as FormatRuleConfig).field) (config as FormatRuleConfig).field = ''; - if (!(config as FormatRuleConfig).formatType) (config as FormatRuleConfig).formatType = 'date'; - if (!(config as FormatRuleConfig).parameters) (config as FormatRuleConfig).parameters = ''; - delete (config as FormatRuleConfig & {availableFields?: string}).availableFields; - delete (config as FormatRuleConfig).checkField; - delete (config as FormatRuleConfig).formatParams; - break; - - case 'logic': - if (!Array.isArray((config as LogicRuleConfig).conditions)) (config as LogicRuleConfig).conditions = []; - if (!(config as LogicRuleConfig).logic) (config as LogicRuleConfig).logic = 'and'; - delete (config as LogicRuleConfig & {availableFields?: string}).availableFields; - delete (config as LogicRuleConfig).logicRelation; - delete (config as LogicRuleConfig).initialField; - delete (config as LogicRuleConfig).initialOperator; - delete (config as LogicRuleConfig).initialValue; - break; - - case 'regex': - if (!(config as RegexRuleConfig).field) (config as RegexRuleConfig).field = ''; - if (!(config as RegexRuleConfig).pattern) (config as RegexRuleConfig).pattern = ''; - if (!(config as RegexRuleConfig).matchType) (config as RegexRuleConfig).matchType = 'match'; - delete (config as RegexRuleConfig & {availableFields?: string}).availableFields; - delete (config as RegexRuleConfig).checkField; - delete (config as RegexRuleConfig).regexPattern; - break; - - case 'ai': - if (!(config as AIRuleConfig).model) (config as AIRuleConfig).model = 'qwen14b'; - if (typeof (config as AIRuleConfig).temperature !== 'number') (config as AIRuleConfig).temperature = 0.1; - if (!(config as AIRuleConfig).prompt) (config as AIRuleConfig).prompt = ''; - delete (config as AIRuleConfig & {availableFields?: string}).availableFields; - break; - - case 'code': - if (!(config as CodeRuleConfig).language) (config as CodeRuleConfig).language = 'javascript'; - if (!(config as CodeRuleConfig).code) (config as CodeRuleConfig).code = ''; - delete (config as CodeRuleConfig & {availableFields?: string}).availableFields; - break; - } - - return { - id: rule.id, - type: rule.type, - config - }; - }); - } - - // 如果是新建模式,则删除id字段 - if (!isEditMode) { - delete cleanedData.id; - } - - // 确保extraction_config和evaluation_config是有效的JSON对象 - // 通过先序列化再解析来验证 - try { - JSON.parse(JSON.stringify(cleanedData.extraction_config)); - } catch (e) { - throw new Error("extraction_config 格式无效"); - } - - try { - JSON.parse(JSON.stringify(cleanedData.evaluation_config)); - } catch (e) { - throw new Error("evaluation_config 格式无效"); - } - - // 检查JSON字符串长度,如果太长可能会被截断 - const jsonData = JSON.stringify(cleanedData); - const maxLength = 65536; // 通常PostgreSQL的jsonb列可以存储的最大长度 - - if (jsonData.length > maxLength) { - throw new Error(`数据大小超过限制 (${jsonData.length} > ${maxLength})`); - } - - console.log("准备提交到API的数据:", cleanedData); - console.log("JSON数据长度:", jsonData.length); - - // 发送数据到API - fetch(apiUrl, { - method: apiMethod, - headers: { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - }, - body: JSON.stringify(cleanedData) - }) - .then(async response => { - // 尝试解析响应内容 - let responseData; - try { - responseData = await response.json(); - } catch (e) { - responseData = { code: 500, msg: "响应解析失败" }; + // 调用API保存数据 + saveEvaluationPoint(formData, isEditMode) + .then(response => { + if (response.error) { + console.error("保存评查点失败:", response.error); + alert(`保存评查点失败: ${response.error}`); + return; } - // 根据响应码处理不同情况 - if (responseData.code === 0) { - // 成功情况 - console.log("保存成功:", responseData); - + if (response.data) { // 获取新创建或更新的评查点ID - const savedPointId = responseData.data[0]?.id; + const savedPointId = response.data[0]?.id; if (savedPointId) { // 显示成功消息 @@ -522,46 +277,17 @@ export default function RuleNew() { navigate('/rules'); } } else { - // 处理各种错误情况 - console.error("API错误:", responseData); - - // 根据错误码显示不同的错误消息 - switch (responseData.code) { - case 400: - alert(`参数错误: ${responseData.msg}`); - break; - case 401: - alert(`未授权: ${responseData.msg}`); - break; - case 403: - alert(`禁止访问: ${responseData.msg}`); - break; - case 404: - alert(`未找到资源: ${responseData.msg}`); - break; - case 409: - alert(`资源冲突: ${responseData.msg}`); - break; - case 500: - alert(`服务器错误: ${responseData.msg}`); - break; - default: - alert(`保存失败: ${responseData.msg || '未知错误'}`); - } + alert(`保存成功,但返回数据为空。正在返回列表页面。`); + navigate('/rules'); } }) .catch(error => { - console.error("网络或解析错误:", error); - alert(`网络或解析错误: ${error.message || '未知错误'}`); + console.error("保存评查点出错:", error); + alert(`保存评查点出错: ${error instanceof Error ? error.message : '未知错误'}`); }) .finally(() => { setIsLoading(false); }); - } catch (error) { - console.error("数据处理错误:", error); - alert(`数据处理错误: ${error instanceof Error ? error.message : '未知错误'}`); - setIsLoading(false); - } } const handleSaveDraft = () => { @@ -606,7 +332,7 @@ export default function RuleNew() { }; const handleReviewSettingsChange = (data: Record) => { - console.log("评查设置变更:", data); + // console.log("评查设置变更:", data); // 检查数据中是否包含evaluation_config对象 if (data.evaluation_config) { diff --git a/app/routes/rules.$rulesId.tsx b/app/routes/rules.$rulesId.tsx deleted file mode 100644 index 731fc2d..0000000 --- a/app/routes/rules.$rulesId.tsx +++ /dev/null @@ -1,418 +0,0 @@ -import React, { useState } from 'react'; -import { redirect, type MetaFunction, type ActionFunctionArgs, type LoaderFunctionArgs } from '@remix-run/node'; -import { useLoaderData, useActionData, Form, useSubmit, useNavigate } from '@remix-run/react'; -import { Button } from '~/components/ui/Button'; -import { Card } from '~/components/ui/Card'; -import { Breadcrumb } from '~/components/layout/Breadcrumb'; -import type { Rule, RuleType, RulePriority } from '~/models/rule'; - -export const meta: MetaFunction = () => { - return [ - { title: "中国烟草AI合同及卷宗审核系统 - 评查规则详情" }, - { name: "description", content: "评查规则详情编辑页面" } - ]; -}; - -export const handle = { - breadcrumb: '编辑评查点' -}; - -interface LoaderData { - rule: Rule; - ruleTypes: { label: string; value: RuleType }[]; - rulePriorities: { label: string; value: RulePriority }[]; - groupOptions: { label: string; value: string }[]; -} - -export async function loader({ params }: LoaderFunctionArgs) { - const { ruleId } = params; - - // 判断是否为新建规则 - const isNewRule = ruleId === 'new'; - - // 模拟数据,实际项目中应从API获取 - const rule: Rule = isNewRule ? { - id: '', - name: '', - description: '', - content: '', - type: 'text', - priority: 'medium', - groupId: '', - groupName: '', - isActive: true, - createdAt: '', - updatedAt: '' - } : { - id: ruleId, - name: '许可证编号格式检查', - description: '检查烟草专卖零售许可证编号是否符合"烟零许(年份)序号号"的标准格式', - content: '许可证编号应当符合"烟零许(年份)序号号"的标准格式,如"烟零许(2023)12345号"', - type: 'regex', - priority: 'high', - groupId: '1', - groupName: '专卖许可证规则组', - isActive: true, - createdAt: '2023-10-15 09:30', - updatedAt: '2023-12-10 14:20' - }; - - // 规则类型选项 - const ruleTypes = [ - { label: '文本匹配', value: 'text' }, - { label: '正则表达式', value: 'regex' }, - { label: '数值范围', value: 'range' }, - { label: '日期检查', value: 'date' }, - { label: 'AI智能检查', value: 'ai' } - ]; - - // 规则优先级选项 - const rulePriorities = [ - { label: '低', value: 'low' }, - { label: '中', value: 'medium' }, - { label: '高', value: 'high' }, - { label: '关键', value: 'critical' } - ]; - - // 规则组选项 - const groupOptions = [ - { label: '专卖许可证规则组', value: '1' }, - { label: '合同协议规则组', value: '2' }, - { label: '财务票据规则组', value: '3' }, - { label: '采购订单规则组', value: '4' }, - { label: '销售报表规则组', value: '5' } - ]; - - return Response.json({ - rule, - ruleTypes, - rulePriorities, - groupOptions - }); -} - -interface ActionData { - success?: boolean; - errors?: { - name?: string; - description?: string; - content?: string; - type?: string; - priority?: string; - groupId?: string; - general?: string; - }; -} - -export async function action({ request, params }: ActionFunctionArgs) { - const { ruleId } = params; - const formData = await request.formData(); - const isNewRule = ruleId === 'new'; - - // 获取表单数据 - const name = formData.get('name')?.toString() || ''; - const description = formData.get('description')?.toString() || ''; - const content = formData.get('content')?.toString() || ''; - const type = formData.get('type')?.toString() || ''; - const priority = formData.get('priority')?.toString() || ''; - const groupId = formData.get('groupId')?.toString() || ''; - const isActive = formData.get('isActive') === 'true'; - - // 表单验证 - const errors: ActionData['errors'] = {}; - - if (!name.trim()) { - errors.name = '规则名称不能为空'; - } - - if (!content.trim()) { - errors.content = '规则内容不能为空'; - } - - if (!type) { - errors.type = '必须选择规则类型'; - } - - if (!priority) { - errors.priority = '必须选择规则优先级'; - } - - if (!groupId) { - errors.groupId = '必须选择规则所属组'; - } - - if (Object.keys(errors).length > 0) { - return Response.json({ errors }); - } - - // 模拟API保存操作,实际项目中应调用API - try { - // 在这里调用API进行保存 - console.log('保存规则:', { - id: isNewRule ? 'new-id' : ruleId, - name, - description, - content, - type, - priority, - groupId, - isActive - }); - - // 成功后重定向到规则列表页 - return redirect('/rules'); - } catch (error) { - return Response.json({ - errors: { - general: '保存规则失败,请重试' - } - }); - } -} - -export default function RuleDetail() { - const { rule, ruleTypes, rulePriorities, groupOptions } = useLoaderData(); - const actionData = useActionData(); - const navigate = useNavigate(); - const submit = useSubmit(); - - const [formData, setFormData] = useState({ - name: rule.name, - description: rule.description, - content: rule.content, - type: rule.type, - priority: rule.priority, - groupId: rule.groupId, - isActive: rule.isActive - }); - - const isNewRule = !rule.id; - - const handleChange = (e: React.ChangeEvent) => { - const { name, value } = e.target; - setFormData(prev => ({ ...prev, [name]: value })); - }; - - const handleSwitchChange = (name: string, checked: boolean) => { - setFormData(prev => ({ ...prev, [name]: checked })); - }; - - const handleCancel = () => { - navigate('/rules'); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - - // 使用useSubmit提交表单 - const formElement = e.currentTarget; - submit(formElement, { method: 'post' }); - }; - - return ( -
- - -
-

{isNewRule ? '新增评查规则' : '编辑评查规则'}

-
- - -
- {actionData?.errors?.general && ( -
- - {actionData.errors.general} -
- )} - -
-

基本信息

- -
-
- - - {actionData?.errors?.name && ( -
{actionData.errors.name}
- )} -
- -
- - - {actionData?.errors?.groupId && ( -
{actionData.errors.groupId}
- )} -
-
- -
-
- - -
-
-
- -
-

规则设置

- -
-
- - - {actionData?.errors?.type && ( -
{actionData.errors.type}
- )} -
- -
- - - {actionData?.errors?.priority && ( -
{actionData.errors.priority}
- )} -
- -
- -
- - - {formData.isActive ? '启用' : '禁用'} -
-
-
- -
-
- - - {actionData?.errors?.content && ( -
{actionData.errors.content}
- )} - {formData.type === 'regex' && ( -
- - 输入正则表达式,用于匹配文档内容 -
- )} - {formData.type === 'ai' && ( -
- - 请使用自然语言描述规则检查的要求,AI将自动理解并执行检查 -
- )} -
-
-
- -
-

测试工具

- -
-
- - -
- -
- -
-
-
- -
- - -
-
-
-
- ); -} \ No newline at end of file diff --git a/app/routes/rules._index.tsx b/app/routes/rules._index.tsx index 9aa84ca..7c33c69 100644 --- a/app/routes/rules._index.tsx +++ b/app/routes/rules._index.tsx @@ -448,7 +448,7 @@ export default function RulesIndex() { width: "10%", render: (_: unknown, record: Rule) => (
- + 编辑