From 0079786b254c57b43bb2850dc8f7ed801d02df1d Mon Sep 17 00:00:00 2001 From: awen Date: Fri, 28 Mar 2025 14:45:54 +0800 Subject: [PATCH 01/23] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/rules/new/ActionButtons.tsx | 34 + app/components/rules/new/BasicInfo.tsx | 260 ++++ app/components/rules/new/CodeEditor.tsx | 128 ++ .../rules/new/ExtractionSettings.tsx | 1010 +++++++++++++ app/components/rules/new/PageHeader.tsx | 24 + app/components/rules/new/ReviewSettings.tsx | 1309 +++++++++++++++++ app/components/rules/new/SimpleCodeEditor.tsx | 145 ++ app/root.tsx | 17 +- app/routes/rule.new.tsx | 92 ++ app/routes/rules.new.tsx | 57 + app/styles/rules.css | 412 ++++++ html/评查点-新增.html | 2 +- package-lock.json | 230 ++- package.json | 4 + 14 files changed, 3718 insertions(+), 6 deletions(-) create mode 100644 app/components/rules/new/ActionButtons.tsx create mode 100644 app/components/rules/new/BasicInfo.tsx create mode 100644 app/components/rules/new/CodeEditor.tsx create mode 100644 app/components/rules/new/ExtractionSettings.tsx create mode 100644 app/components/rules/new/PageHeader.tsx create mode 100644 app/components/rules/new/ReviewSettings.tsx create mode 100644 app/components/rules/new/SimpleCodeEditor.tsx create mode 100644 app/routes/rule.new.tsx create mode 100644 app/routes/rules.new.tsx create mode 100644 app/styles/rules.css diff --git a/app/components/rules/new/ActionButtons.tsx b/app/components/rules/new/ActionButtons.tsx new file mode 100644 index 0000000..221d166 --- /dev/null +++ b/app/components/rules/new/ActionButtons.tsx @@ -0,0 +1,34 @@ +import React from 'react'; + +interface ActionButtonsProps { + onSave?: () => void; + onSaveDraft?: () => void; +} + +export function ActionButtons({ onSave, onSaveDraft }: ActionButtonsProps) { + return ( +
+ + + +
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/BasicInfo.tsx b/app/components/rules/new/BasicInfo.tsx new file mode 100644 index 0000000..3afd207 --- /dev/null +++ b/app/components/rules/new/BasicInfo.tsx @@ -0,0 +1,260 @@ +import { useState } from 'react'; + +interface BasicInfoProps { + onChange?: (data: Record) => void; +} + +export function BasicInfo({ onChange }: BasicInfoProps) { + const [isDescriptionExpanded, setIsDescriptionExpanded] = useState(false); + const [formData, setFormData] = useState({ + name: '', + code: '', + riskLevel: 'medium', + type: '', + group: 'contract-base', + enabled: true, + description: '', + lawName: '', + lawArticles: '', + lawContent: '' + }); + + const toggleDescription = () => { + setIsDescriptionExpanded(!isDescriptionExpanded); + }; + + const handleInputChange = (e: React.ChangeEvent) => { + const { id, value } = e.target; + let fieldName = id; + + // 映射id到表单字段名 + switch(id) { + case 'checkpoint-name': fieldName = 'name'; break; + case 'checkpoint-code': fieldName = 'code'; break; + case 'risk-level': fieldName = 'riskLevel'; break; + case 'checkpointType': fieldName = 'type'; break; + case 'rule-group': fieldName = 'group'; break; + case 'is-enabled': fieldName = 'enabled'; break; + case 'checkpoint-description': fieldName = 'description'; break; + case 'law-name': fieldName = 'lawName'; break; + case 'law-articles': fieldName = 'lawArticles'; break; + case 'law-content': fieldName = 'lawContent'; break; + } + + const newData = { + ...formData, + [fieldName]: id === 'is-enabled' ? value === 'true' : value + }; + + setFormData(newData); + + if (onChange) { + onChange(newData); + } + }; + + return ( +
+
+

基本信息

+
+
+
+
+ + +
请使用简洁明了的名称,不超过30个字符
+
+
+ + +
用于系统标识的唯一编码
+
+
+ + +
请定义评查点的风险等级
+
+
+ + +
评查点类型用于分类管理,便于规则统一调用
+
+
+ + +
+
+ + +
创建后是否立即启用此评查点
+
+
+
{ + if (e.key === 'Enter' || e.key === ' ') { + toggleDescription(); + } + }} + aria-expanded={isDescriptionExpanded} + aria-controls="description-section" + > +

评查点描述与法律依据

+ +
+ +
+
+ + +
详细描述有助于其他用户了解该评查点的用途
+
+ + {/* 引用法典输入区域 */} +
+ + +
+ + +
+ +
+ + +
多个条款用逗号分隔,将自动转换为数组格式
+
+ +
+ + +
+ +
+ 引用的法律条文将在评查结果中显示,帮助用户理解评查规则的法律依据 +
+ + {/* 预览区域 */} +
+
预览效果
+
+
+ {formData.lawName || '《中华人民共和国民法典》'} +
+
+ {formData.lawArticles ? formData.lawArticles.split(',').map((article, index) => ( + {article.trim()} + )) : ( + <> + 第五百八十五条 + 第五百八十六条 + + )} +
+
+ {formData.lawContent || '当事人应当按照约定全面履行自己的义务。'} +
+
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/CodeEditor.tsx b/app/components/rules/new/CodeEditor.tsx new file mode 100644 index 0000000..4131729 --- /dev/null +++ b/app/components/rules/new/CodeEditor.tsx @@ -0,0 +1,128 @@ +import React, { useState, useEffect } from 'react'; +import CodeMirror from '@uiw/react-codemirror'; +import { javascript } from '@codemirror/lang-javascript'; +import { oneDark } from '@codemirror/theme-one-dark'; + +interface CodeEditorProps { + id: string; + initialValue?: string; + language?: 'javascript' | 'python'; + onChange?: (value: string) => void; +} + +export function CodeEditor({ + id, + initialValue = '', + language = 'javascript', + onChange +}: CodeEditorProps) { + const [code, setCode] = useState(initialValue); + const [copySuccess, setCopySuccess] = useState(false); + + // 当语言变化时更新编辑器 + const extensions = [language === 'javascript' ? javascript() : javascript()]; + + // 处理代码变化 + const handleChange = (value: string) => { + setCode(value); + if (onChange) { + onChange(value); + } + }; + + // 复制代码到剪贴板 + const copyToClipboard = () => { + navigator.clipboard.writeText(code).then(() => { + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + }); + }; + + // 初始示例代码 + const getDefaultCode = (lang: string) => { + if (lang === 'javascript') { + return `// 示例代码 +function checkRule(data) { + // data 包含抽取的字段 + try { + // 在此编写检查逻辑 + if (data.fieldName && condition) { + return { + pass: true, + message: "检查通过" + }; + } else { + return { + pass: false, + message: "检查不通过,原因:..." + }; + } + } catch (error) { + return { + pass: false, + message: "执行出错:" + error.message + }; + } +}`; + } else { + return `# 示例代码 +def check_rule(data): + # data 包含抽取的字段 + try: + # 在此编写检查逻辑 + if 'field_name' in data and condition: + return { + 'pass': True, + 'message': "检查通过" + } + else: + return { + 'pass': False, + 'message': "检查不通过,原因:..." + } + except Exception as error: + return { + 'pass': False, + 'message': f"执行出错:{str(error)}" + }`; + } + }; + + // 如果初始值为空,则使用默认示例代码 + useEffect(() => { + if (!initialValue) { + setCode(getDefaultCode(language)); + } + }, [language, initialValue]); + + return ( +
+
+
+
{language === 'javascript' ? 'script.js' : 'script.py'}
+
+ +
+
+ +
+ {copySuccess && ( +
+ 代码已复制到剪贴板 +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx new file mode 100644 index 0000000..8f6cef6 --- /dev/null +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -0,0 +1,1010 @@ +import { useState, KeyboardEvent, FormEvent, useContext, useEffect } from 'react'; +import { RuleContext } from './ReviewSettings'; + +interface ExtractionSettingsProps { + onChange?: (data: Record) => void; +} + +interface RegexField { + id: string; + fieldName: string; + regex: string; +} + +interface PromptTemplate { + id: number; + template_name: string; + template_type: string; + template_content: string; +} + +export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { + // 使用RuleContext获取全局状态 + const ruleContext = useContext(RuleContext); + + const [currentTab, setCurrentTab] = useState('llm_ocr'); + const [fields, setFields] = useState<{[key: string]: string[]}>({ + llm_ocr: [], + llm: [] + }); + const [inputValue, setInputValue] = useState({ + llm_ocr: '', + llm: '' + }); + const [selectedFieldType, setSelectedFieldType] = useState('default'); + const [regexFields, setRegexFields] = useState([ + { id: '1', fieldName: '', regex: '' } + ]); + + // 提示词相关状态 + const [promptType, setPromptType] = useState({ + llm_ocr: 'system', + llm: 'system' + }); + const [promptContent, setPromptContent] = useState({ + llm_ocr: '', + llm: '' + }); + const [selectedTemplate, setSelectedTemplate] = useState({ + llm_ocr: '', + llm: '' + }); + + // 在组件初始化时,如果Context中已有字段数据,则使用Context数据初始化 + useEffect(() => { + if (ruleContext && ruleContext.extractionFields.length > 0) { + // 将Context中的字段数据添加到当前激活的抽取方式中 + setFields(prevFields => ({ + ...prevFields, + [currentTab]: [...ruleContext.extractionFields] + })); + } + }, []); + + const handleTabChange = (tab: string) => { + setCurrentTab(tab); + + // 当切换抽取方法时,更新全局Context中的字段 + if (ruleContext && fields[tab]) { + ruleContext.updateExtractionFields(fields[tab]); + } + + if (onChange) { + onChange({ extractionMethod: tab }); + } + }; + + const handleFieldInputChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + setInputValue({ + ...inputValue, + [type]: e.currentTarget.value + }); + }; + + const handleFieldTypeChange = (e: FormEvent) => { + setSelectedFieldType(e.currentTarget.value); + }; + + const addField = (type: 'llm_ocr' | 'llm') => { + if (inputValue[type].trim()) { + let newFields: string[] = []; + + // OCR+LLM模式下,支持多个字段同时添加(用逗号、顿号或空格分隔) + if (type === 'llm_ocr') { + newFields = [ + ...fields[type], + ...inputValue[type].split(/[\s、,]+/).map(f => f.trim()).filter(f => f !== '') + ]; + } else { + // 多模态抽取模式下,一次只添加一个字段(带类型) + newFields = [...fields[type], `${inputValue[type].trim()}_${selectedFieldType}`]; + } + + setFields({ + ...fields, + [type]: newFields + }); + + setInputValue({ + ...inputValue, + [type]: '' + }); + + if (type === 'llm') { + setSelectedFieldType('default'); + } + + // 更新全局Context中的字段 + if (ruleContext) { + ruleContext.updateExtractionFields(newFields); + } + + if (onChange) { + onChange({ + extractionMethod: currentTab, + fields: { + ...fields, + [type]: newFields + } + }); + } + + // 触发自定义事件,通知字段已更新(兼容非Context的实现) + const event = new CustomEvent('extraction-fields-updated', { + detail: { fields: newFields } + }); + document.dispatchEvent(event); + } + }; + + const handleKeyDown = (e: KeyboardEvent, type: 'llm_ocr' | 'llm') => { + if (e.key === 'Enter') { + e.preventDefault(); + addField(type); + } + }; + + const removeField = (type: 'llm_ocr' | 'llm', index: number) => { + const newFields = [...fields[type]]; + newFields.splice(index, 1); + + setFields({ + ...fields, + [type]: newFields + }); + + // 更新全局Context中的字段 + if (ruleContext) { + ruleContext.updateExtractionFields(newFields); + } + + if (onChange) { + onChange({ + extractionMethod: currentTab, + fields: { + ...fields, + [type]: newFields + } + }); + } + + // 触发自定义事件,通知字段已更新(兼容非Context的实现) + const event = new CustomEvent('extraction-fields-updated', { + detail: { fields: newFields } + }); + document.dispatchEvent(event); + }; + + // 添加正则表达式字段行 + const addRegexFieldRow = () => { + const newId = `${regexFields.length + 1}`; + setRegexFields([...regexFields, { id: newId, fieldName: '', regex: '' }]); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + regexFields: [...regexFields, { id: newId, fieldName: '', regex: '' }] + }); + } + + // 添加字段时不触发事件,因为此时字段名称尚未填写 + }; + + // 删除正则表达式字段行 + const removeRegexFieldRow = (id: string) => { + // 至少保留一行 + if (regexFields.length <= 1) { + return; + } + + const newRegexFields = regexFields.filter(field => field.id !== id); + setRegexFields(newRegexFields); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + regexFields: newRegexFields + }); + } + }; + + // 更新正则表达式字段值 + const updateRegexField = (id: string, key: 'fieldName' | 'regex', value: string) => { + const newRegexFields = regexFields.map(field => + field.id === id ? { ...field, [key]: value } : field + ); + + setRegexFields(newRegexFields); + + // 如果是字段名更新且当前抽取方法是正则抽取,则更新Context + if (key === 'fieldName' && currentTab === 'ocr_regex' && ruleContext) { + const fieldNames = newRegexFields + .map(field => field.fieldName) + .filter(name => name.trim() !== ''); + + ruleContext.updateExtractionFields(fieldNames); + } + + if (onChange) { + onChange({ + extractionMethod: currentTab, + regexFields: newRegexFields + }); + } + + // 如果更新的是字段名,触发自定义事件通知字段已更新 + if (key === 'fieldName' && value.trim()) { + const allFieldNames = [...fields[currentTab], ...newRegexFields.map(f => f.fieldName).filter(f => f)]; + + // 触发自定义事件,通知字段已更新(兼容非Context的实现) + const event = new CustomEvent('extraction-fields-updated', { + detail: { fields: allFieldNames } + }); + document.dispatchEvent(event); + } + }; + + // 应用正则模板 + const applyRegexTemplate = (regex: string) => { + // 找到当前正在编辑的行,或者最后一行 + const lastField = regexFields[regexFields.length - 1]; + updateRegexField(lastField.id, 'regex', regex); + }; + + // 从字段字符串中提取字段名和类型(用于多模态抽取) + const getFieldInfo = (field: string) => { + const [fieldName, fieldType = 'default'] = field.split('_'); + const typeName = { + 'default': '默认', + 'seal': '印章', + 'cross-seal': '骑缝章', + 'handwriting': '手写体', + 'print': '印刷体', + 'english': '英文', + 'number': '数字', + 'currency': '货币' + }[fieldType] || '默认'; + + const badgeClass = { + 'default': 'bg-blue-100 text-blue-800', + 'seal': 'bg-red-100 text-red-800', + 'cross-seal': 'bg-red-100 text-red-800', + 'handwriting': 'bg-yellow-100 text-yellow-800', + 'print': 'bg-purple-100 text-purple-800', + 'english': 'bg-indigo-100 text-indigo-800', + 'number': 'bg-gray-100 text-gray-800', + 'currency': 'bg-green-100 text-green-800' + }[fieldType] || 'bg-blue-100 text-blue-800'; + + return { fieldName, fieldType, typeName, badgeClass }; + }; + + // 处理提示词类型切换 + const handlePromptTypeChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const value = e.currentTarget.value; + setPromptType({ + ...promptType, + [type]: value + }); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + promptSettings: { + ...promptType, + [type]: value + } + }); + } + }; + + // 处理提示词模板选择 + const handleTemplateChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const value = e.currentTarget.value; + setSelectedTemplate({ + ...selectedTemplate, + [type]: value + }); + + if (value) { + const templateData = getPromptTemplateById(Number(value)); + if (templateData) { + // 基础模板内容 + let content = templateData.template_content; + + // 替换字段列表变量 + if (content.includes('{fieldsList}') && fields[type].length > 0) { + let fieldListStr = ''; + + if (type === 'llm_ocr') { + // 普通字段列表 + fieldListStr = fields[type].map((field, idx) => `${idx+1}. ${field}`).join('\n'); + } else if (type === 'llm') { + // 带类型的字段列表 + fieldListStr = fields[type].map((field, idx) => { + const { fieldName, typeName } = getFieldInfo(field); + return `${idx+1}. ${fieldName} (${typeName})`; + }).join('\n'); + } + + content = content.replace('{fieldsList}', fieldListStr); + } + + setPromptContent({ + ...promptContent, + [type]: content + }); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + promptSettings: { + type: promptType[type], + template: value, + content: content + } + }); + } + } + } else { + // 清空内容 + setPromptContent({ + ...promptContent, + [type]: '' + }); + } + }; + + // 处理提示词内容变更 + const handlePromptContentChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const value = e.currentTarget.value; + setPromptContent({ + ...promptContent, + [type]: value + }); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + promptSettings: { + type: promptType[type], + template: selectedTemplate[type], + content: value + } + }); + } + }; + + // 应用变量标签到提示词 + const applyVariableToPrompt = (variable: string, type: 'llm_ocr' | 'llm') => { + const textarea = document.getElementById(type === 'llm_ocr' ? 'llm-prompt-content' : 'multimodal-prompt-content') as HTMLTextAreaElement; + if (textarea) { + const start = textarea.selectionStart; + const end = textarea.selectionEnd; + const text = textarea.value; + const newText = text.substring(0, start) + `{${variable}}` + text.substring(end); + + setPromptContent({ + ...promptContent, + [type]: newText + }); + + // 使焦点回到文本框并设置光标位置 + setTimeout(() => { + textarea.focus(); + textarea.setSelectionRange(start + variable.length + 2, start + variable.length + 2); + }, 0); + + if (onChange) { + onChange({ + extractionMethod: currentTab, + promptSettings: { + type: promptType[type], + template: selectedTemplate[type], + content: newText + } + }); + } + } + }; + + // 模拟获取提示词模板 + const getPromptTemplateById = (id: number): PromptTemplate | null => { + // 模拟的模板数据,实际应用中应从服务器获取 + const templates: Record = { + 1: { + id: 1, + template_name: '行政处罚-抽取通用模板', + template_type: 'Extraction', + template_content: `你是一个专业的文档信息抽取助手。请从以下{docType}文档中抽取关键信息: + +{fieldsList} + +请将结果以JSON格式输出,包含以上字段。如果某个字段在文档中未找到,则该字段的值设为null。` + }, + 4: { + id: 4, + template_name: '采购合同-乙方资质抽取', + template_type: 'Extraction', + template_content: `你是一个专业的合同信息抽取助手。请从以下{docType}中抽取乙方的资质信息: + +需要抽取的信息包括: +{fieldsList} + +{companyName}要求所有供应商必须提供完整的资质信息。请将结果以JSON格式输出,包含以上字段。` + }, + 5: { + id: 5, + template_name: '合同-关键条款抽取', + template_type: 'Extraction', + template_content: `请作为{industry}行业的专业合同审核员,从提供的{docType}中提取以下关键条款信息: + +{fieldsList} + +文档ID: {documentId} +审核日期: {date} + +请以JSON格式输出结果,对于未明确指定的条款需标记为"未明确约定"。` + }, + 6: { + id: 6, + template_name: '烟草许可证-信息抽取', + template_type: 'Extraction', + template_content: `请从下列烟草专卖许可证文件中抽取以下关键信息: + +{fieldsList} + +这些信息将用于{companyName}内部数据库更新。请确保许可证编号和有效期格式准确无误。` + }, + 7: { + id: 7, + template_name: '多模态-印章识别模板', + template_type: 'Multimodal', + template_content: `请识别并提取文档中的所有印章信息,包括: + +{fieldsList} + +文档类型: {docType} +页面范围: {pageRange} + +请注意区分公章、法人章和合同专用章,并分析印章的清晰度和完整性。` + }, + 8: { + id: 8, + template_name: '多模态-表格抽取模板', + template_type: 'Multimodal', + template_content: `请从文档中的表格提取以下信息: + +{fieldsList} + +文档类型: {docType} +表格可能跨页,请确保完整提取所有内容。表格中的数值需保留原始精度。` + }, + 9: { + id: 9, + template_name: '多模态-手写内容识别模板', + template_type: 'Multimodal', + template_content: `请识别文档中的手写内容,特别关注: + +{fieldsList} + +文档类型: {docType} +内容类型: {contentType} + +对于难以辨认的手写内容,请标注为"[难以辨认]"并尽可能给出可能的解读。` + } + }; + + return templates[id] || null; + }; + + return ( +
+
+

抽取设置

+
+
+
+ {/* 切换按钮 */} +
+ + + +
+
+ + {/* 大模型抽取配置 */} +
+
+
+ +
+ handleFieldInputChange(e, 'llm_ocr')} + onKeyDown={(e) => handleKeyDown(e, 'llm_ocr')} + /> + +
+
+ {fields.llm_ocr.map((field, index) => ( +
+ {field} + removeField('llm_ocr', index)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + removeField('llm_ocr', index); + } + }} + role="button" + tabIndex={0} + aria-label={`删除字段 ${field}`} + >× +
+ ))} +
+
支持一次输入多个字段
+
+
+ +
+
+ +
+ + +
+ +
+ 系统将根据评查点类型和抽取目标自动生成适合的提示词,您无需额外配置。 +
+ +
+
+ + +
+
+ + +
+

支持的变量(点击变量将其添加到提示词中):

+
+ + + + + + + +
+
+
+
+
+
+
+ + {/* 多模态抽取配置 */} +
+
+
+ +
+ handleFieldInputChange(e, 'llm')} + onKeyDown={(e) => handleKeyDown(e, 'llm')} + /> + + +
+
+ {fields.llm.map((field, index) => { + const { fieldName, fieldType, typeName, badgeClass } = getFieldInfo(field); + return ( +
+ {fieldName} + {typeName} + removeField('llm', index)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + removeField('llm', index); + } + }} + role="button" + tabIndex={0} + aria-label={`删除字段 ${fieldName}`} + >× +
+ ); + })} +
+
请为每个字段选择适当的抽取类型,有助于提高识别准确率
+
+
+ +
+
+ +
+ + +
+
+ 系统将根据评查点类型和抽取目标自动生成适合的提示词,支持图表、印章等图像内容抽取。 +
+ +
+
+ + +
+
+ + +
+

支持的变量(点击变量将其添加到提示词中):

+
+ + + + + + + + + + +
+
+
+
+
+
+
+ + {/* 正则抽取配置 */} +
+
+
+
+
+ + +
+ +
+ {/* 字段-正则表达式配置行 */} + {regexFields.map((field) => ( +
+
+ + updateRegexField(field.id, 'fieldName', e.target.value)} + /> +
+
+ + updateRegexField(field.id, 'regex', e.target.value)} + /> +
+
+ +
+
+ ))} +
+
+
+ +
+
applyRegexTemplate("\\d{4}[-/年](0?[1-9]|1[0-2])[-/月](0?[1-9]|[12][0-9]|3[01])[日]?")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + applyRegexTemplate("\\d{4}[-/年](0?[1-9]|1[0-2])[-/月](0?[1-9]|[12][0-9]|3[01])[日]?"); + } + }} + >日期格式:yyyy-mm-dd
+
applyRegexTemplate("[A-Z]{2,5}-\\d{4,10}")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + applyRegexTemplate("[A-Z]{2,5}-\\d{4,10}"); + } + }} + >合同编号格式
+
applyRegexTemplate("(人民币|RMB)?\\s?(\\d{1,3}(,\\d{3})*(\\.\\d{2})?)\\s?[万元]?")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + applyRegexTemplate("(人民币|RMB)?\\s?(\\d{1,3}(,\\d{3})*(\\.\\d{2})?)\\s?[万元]?"); + } + }} + >金额格式
+
applyRegexTemplate("\\d{3}-\\d{8}|\\d{4}-\\d{7,8}")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + applyRegexTemplate("\\d{3}-\\d{8}|\\d{4}-\\d{7,8}"); + } + }} + >座机号码格式
+
applyRegexTemplate("1[3-9]\\d{9}")} + role="button" + tabIndex={0} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + applyRegexTemplate("1[3-9]\\d{9}"); + } + }} + >手机号码格式
+
+
+
+
+
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/PageHeader.tsx b/app/components/rules/new/PageHeader.tsx new file mode 100644 index 0000000..f1b907e --- /dev/null +++ b/app/components/rules/new/PageHeader.tsx @@ -0,0 +1,24 @@ +import React from 'react'; +import { Link } from '@remix-run/react'; + +interface PageHeaderProps { + title: string; + onSave?: () => void; +} + +export function PageHeader({ title, onSave }: PageHeaderProps) { + return ( +
+

{title}

+
+ +
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx new file mode 100644 index 0000000..399c705 --- /dev/null +++ b/app/components/rules/new/ReviewSettings.tsx @@ -0,0 +1,1309 @@ +import React, { useState, useEffect, useContext, createContext } from 'react'; +import { SimpleCodeEditor } from './SimpleCodeEditor'; + +interface RuleType { + id: string; + type: string; + config: Record; +} + +// 为配置项添加类型定义 +interface ComparisonPair { + sourceField: string; + targetField: string; + compareMethod: string; +} + +interface ReviewSettingsProps { + onChange?: (data: Record) => void; +} + +// 创建全局上下文以便在不同组件间共享数据 +interface RuleContextType { + extractionFields: string[]; + updateExtractionFields: (fields: string[]) => void; +} + +// 创建全局Context对象 +export const RuleContext = createContext({ + extractionFields: [], + updateExtractionFields: () => {} +}); + +export function ReviewSettings({ onChange }: ReviewSettingsProps) { + const [rules, setRules] = useState([ + { id: '1', type: '', config: {} } + ]); + const [combinationLogic, setCombinationLogic] = useState('and'); + const [customLogic, setCustomLogic] = useState(''); + const [showCustomLogic, setShowCustomLogic] = useState(false); + // 添加评查后动作相关状态 + const [actionType, setActionType] = useState('none'); + const [actionDescription, setActionDescription] = useState(''); + + // 获取抽取字段的上下文 + const { extractionFields } = useContext(RuleContext); + + // 初始化评查通过/不通过/建议信息 + const [passMessage, setPassMessage] = useState('文档检查通过,符合规范要求。'); + const [failMessage, setFailMessage] = useState('文档存在以下问题,请修改后重新提交。'); + const [suggestMessage, setSuggestMessage] = useState(''); + + // 错误严重程度 + const [errorSeverity, setErrorSeverity] = useState('error'); + + // 保存最近一次可用的字段列表 + const [availableFields, setAvailableFields] = useState(extractionFields || []); + + // 监听抽取设置中的字段变化 + useEffect(() => { + // 当Context中的字段发生变化时,立即更新可用字段 + if (extractionFields.length > 0) { + setAvailableFields(extractionFields); + + // 同时更新已有规则中的可选字段状态 + updateRulesWithNewFields(extractionFields); + return; + } + + // 获取抽取字段函数 - 当Context不可用时的备选方案 + const getExtractedFields = (): string[] => { + const extractedFields: string[] = []; + + // 查找当前活跃的抽取设置容器 + const activeExtractConfig = document.querySelector('.extraction-config:not(.hidden)'); + if (!activeExtractConfig) { + // 如果没有找到活跃的配置,返回空数组 + return []; + } + + // 查找字段容器 + const fieldsContainer = activeExtractConfig.querySelector('.chips-container'); + if (fieldsContainer) { + // 获取所有字段芯片 + const chips = fieldsContainer.querySelectorAll('.chip'); + chips.forEach(chip => { + const fieldName = chip.textContent?.replace('×', '').trim(); + if (fieldName) { + extractedFields.push(fieldName); + } + }); + } + + // 查找正则字段 + const regexFields = document.querySelectorAll('.regex-field-row'); + regexFields.forEach(row => { + const fieldNameInput = row.querySelector('input[placeholder="字段名称"]'); + if (fieldNameInput instanceof HTMLInputElement && fieldNameInput.value.trim()) { + extractedFields.push(fieldNameInput.value.trim()); + } + }); + + return extractedFields; + }; + + // 初始化获取字段 - 仅在Context不可用时使用 + setAvailableFields(getExtractedFields()); + + // 监听抽取设置的变化 - 仅在Context不可用时使用 + const handleExtractionChange = (event: Event) => { + if (event instanceof CustomEvent && event.detail && Array.isArray(event.detail.fields)) { + setAvailableFields(event.detail.fields); + } else { + setAvailableFields(getExtractedFields()); + } + }; + + // 添加事件监听器,监听抽取设置中的字段变化 + document.addEventListener('extraction-fields-updated', handleExtractionChange); + + // 组件卸载时移除事件监听 + return () => { + document.removeEventListener('extraction-fields-updated', handleExtractionChange); + }; + }, [extractionFields]); + + // 当可用字段发生变化时,更新已有规则的配置 + const updateRulesWithNewFields = (newFields: string[]) => { + // 更新已有规则中的字段选择,保留已选择的字段 + const updatedRules = rules.map(rule => { + if (rule.type === 'exists' || rule.type === 'consistency' || rule.type === 'logic' || rule.type === 'regex') { + // 检查和更新字段相关的配置 + const currentSelectedFields = Array.isArray(rule.config.selectedFields) + ? rule.config.selectedFields as string[] + : []; + + // 使用已选中的字段和新字段的交集作为更新后的选中字段 + const validSelectedFields = currentSelectedFields.filter(field => + newFields.includes(field) || field.trim() !== '' + ); + + return { + ...rule, + config: { + ...rule.config, + selectedFields: validSelectedFields + } + }; + } + return rule; + }); + + setRules(updatedRules); + + if (onChange) { + onChange({ rules: updatedRules }); + } + }; + + const handleLogicChange = (logic: string) => { + setCombinationLogic(logic); + if (logic === 'custom') { + setShowCustomLogic(true); + } else { + setShowCustomLogic(false); + } + + if (onChange) { + onChange({ combinationLogic: logic, showCustomLogic: logic === 'custom', customLogic }); + } + }; + + const handleCustomLogicChange = (e: React.ChangeEvent) => { + setCustomLogic(e.target.value); + + if (onChange) { + onChange({ customLogic: e.target.value }); + } + }; + + const handleActionTypeChange = (type: string) => { + setActionType(type); + + if (onChange) { + onChange({ actionType: type }); + } + }; + + const handleAddRule = () => { + const newId = `${rules.length + 1}`; + const newRule = { id: newId, type: '', config: {} }; + setRules([...rules, newRule]); + + if (onChange) { + onChange({ rules: [...rules, newRule] }); + } + }; + + const handleRemoveRule = (id: string) => { + // 如果只有一个规则,不允许删除 + if (rules.length <= 1) { + return; + } + + const newRules = rules.filter(rule => rule.id !== id); + setRules(newRules); + + // 重新编号规则 + const reindexedRules = newRules.map((rule, index) => ({ + ...rule, + id: `${index + 1}` + })); + + setRules(reindexedRules); + + if (onChange) { + onChange({ rules: reindexedRules }); + } + }; + + const handleRuleTypeChange = (id: string, type: string) => { + const updatedRules = rules.map(rule => + rule.id === id ? { ...rule, type, config: {} } : rule + ); + + setRules(updatedRules); + + if (onChange) { + onChange({ rules: updatedRules }); + } + }; + + const handleRuleConfigChange = (id: string, config: Record) => { + const updatedRules = rules.map(rule => + rule.id === id ? { ...rule, config: { ...rule.config, ...config } } : rule + ); + + setRules(updatedRules); + + if (onChange) { + onChange({ rules: updatedRules }); + } + }; + + // 处理字段选择 + const handleFieldSelection = (id: string, fieldName: string, selected: boolean) => { + const updatedRules = rules.map(rule => { + if (rule.id === id) { + // 获取当前已选字段 + const selectedFields = Array.isArray(rule.config.selectedFields) + ? rule.config.selectedFields as string[] + : []; + + // 添加或删除字段 + const newSelectedFields = selected + ? [...selectedFields, fieldName] + : selectedFields.filter(f => f !== fieldName); + + return { + ...rule, + config: { ...rule.config, selectedFields: newSelectedFields } + }; + } + return rule; + }); + + setRules(updatedRules); + + if (onChange) { + onChange({ rules: updatedRules }); + } + }; + + // 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示 + const renderFieldTags = (ruleId: string, ruleConfig: Record) => { + const selectedFields = Array.isArray(ruleConfig.selectedFields) + ? ruleConfig.selectedFields as string[] + : []; + + // 合并已选择的字段和可用字段,以确保已选择但不再可用的字段仍然显示 + const allFieldsToRender = [...new Set([...availableFields, ...selectedFields])]; + + return allFieldsToRender.map((field, index) => { + const isSelected = selectedFields.includes(field); + const isAvailable = availableFields.includes(field); + const buttonId = `field-tag-${ruleId}-${index}`; + + // 如果字段不再可用但已被选中,用特殊样式显示 + const fieldClass = !isAvailable && isSelected ? 'field-tag selected unavailable' : + isSelected ? 'field-tag selected' : 'field-tag'; + + // 只显示可用的字段或已选择的字段 + if (!isAvailable && !isSelected) { + return null; + } + + return ( + + ); + }).filter(Boolean); // 过滤掉null值 + }; + + // 获取规则类型的Badge样式 + const getRuleTypeBadgeClass = (type: string) => { + switch(type) { + case 'exists': return 'bg-green-500'; + case 'consistency': return 'bg-blue-500'; + case 'format': return 'bg-purple-500'; + case 'logic': return 'bg-yellow-500'; + case 'regex': return 'bg-red-500'; + case 'ai': return 'bg-indigo-500'; + case 'code': return 'bg-gray-700'; + default: return 'bg-primary'; + } + }; + + // 渲染规则配置区域 + const renderRuleConfig = (rule: RuleType) => { + const { id, type, config } = rule; + + if (!type) { + return ( +
+ +

请先选择评查类型

+
+ ); + } + + switch(type) { + case 'exists': + return ( +
+
+ +
+ {renderFieldTags(id, config)} +
+
点击选择需要判断是否存在的字段,已选中的字段会高亮显示
+
+
+ +
+ + +
+
+
+ ); + + case 'consistency': + return ( +
+
+ +
+ {Array.isArray(config.pairs) && config.pairs.length > 0 ? ( + config.pairs.map((pair, pairIndex) => ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+ )) + ) : ( +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ )} +
+
+ +
+
+
+
+ 逻辑关系 * +
+ + +
+
+
+
+ ); + + case 'logic': + return ( +
+
+ +
+
+
+
+ + +
+
+ + +
+
+ + { + const currentConditions = Array.isArray(config.conditions) ? [...config.conditions] : []; + currentConditions[0] = { ...currentConditions[0], value: e.target.value }; + handleRuleConfigChange(id, { conditions: currentConditions }); + }} + /> +
+
+
+ +
+
+
+
+ +
+
+
+
+ 逻辑关系 * +
+ + +
+
+
+
+ ); + + case 'regex': + return ( +
+
+ + +
+
+ + +
+
+
+ 匹配类型 * +
+ + +
+
+
+
+ ); + + case 'ai': + return ( +
+
+
+ + +
+
+ + handleRuleConfigChange(id, { temperature: e.target.value })} + /> +
+
+ + +
+
+ {availableFields.map((field, idx) => ( + + ))} +
+
+
+ ); + + case 'code': + return ( +
+
+
+ 代码语言 * +
+ + +
+
+
+
+ + handleRuleConfigChange(id, { code: value })} + /> +
+
+ ); + + case 'format': + return ( +
+
+ + +
+
+ + +
+
+ + handleRuleConfigChange(id, { formatParams: e.target.value })} + /> +
+
+ ); + + default: + return ( +
+

已选择 {type} 类型规则,请继续配置。

+
+ ); + } + }; + + // 处理评查结果消息变更 + const handleMessageChange = (type: string, value: string) => { + switch(type) { + case 'pass': + setPassMessage(value); + break; + case 'fail': + setFailMessage(value); + break; + case 'suggest': + setSuggestMessage(value); + break; + } + + if (onChange) { + onChange({ [`${type}Message`]: value }); + } + }; + + // 处理严重程度变更 + const handleSeverityChange = (value: string) => { + setErrorSeverity(value); + + if (onChange) { + onChange({ errorSeverity: value }); + } + }; + + return ( +
+
+

评查设置

+
+
+
+
+
+ +
+
+
+ + + +
+ + {showCustomLogic && ( +
+ + +
+ 使用规则编号和逻辑运算符(AND、OR、NOT)组合 +
+
+ )} +
+
+ +
+
+ +
+
+
+
已添加 {rules.length} 条规则
+ +
+ + {rules.length === 0 ? ( +
+
+ +

尚未添加任何规则

+

点击“添加规则”按钮开始创建评查规则

+
+
+ ) : ( +
+ {rules.map((rule) => ( +
+
+ + 规则 #{rule.id} + + +
+ +
+ + +
选择评查类型后将显示对应的配置项
+
+ +
+ {renderRuleConfig(rule)} +
+
+ ))} +
+ )} +
+
+ +
+
+ +
+ +
+
+ + + +
+ +
+
+ + +
建议在消息中说明问题出现的原因及建议的解决方案
+
+ + {actionType === 'notification' && ( +
+ + + +
+ +
+ + + +
+
+
+ )} +
+
+
+ +
+ + {/* 评查结果提示信息 */} +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + {/* 不通过提示类别 */} +
+ +
+ + + + + +
+
不同类别会影响问题的展示方式和处理流程
+
+ + {/* 评查后动作 */} +
+ + +
+ + {/* 动作描述区域 */} + {actionType && actionType !== 'none' && ( +
+ + +
+ )} +
+
+
+ ); +} \ No newline at end of file diff --git a/app/components/rules/new/SimpleCodeEditor.tsx b/app/components/rules/new/SimpleCodeEditor.tsx new file mode 100644 index 0000000..5a91656 --- /dev/null +++ b/app/components/rules/new/SimpleCodeEditor.tsx @@ -0,0 +1,145 @@ +import { useState, useEffect } from 'react'; + +interface SimpleCodeEditorProps { + id: string; + initialValue?: string; + language?: 'javascript' | 'python'; + onChange?: (value: string) => void; +} + +export function SimpleCodeEditor({ + id, + initialValue = '', + language = 'javascript', + onChange +}: SimpleCodeEditorProps) { + const [code, setCode] = useState(initialValue || getDefaultCode(language)); + const [copySuccess, setCopySuccess] = useState(false); + + // 复制代码到剪贴板 + const copyToClipboard = () => { + navigator.clipboard.writeText(code).then(() => { + setCopySuccess(true); + setTimeout(() => setCopySuccess(false), 2000); + }); + }; + + // 处理代码变化 + const handleChange = (e: React.ChangeEvent) => { + const value = e.target.value; + setCode(value); + if (onChange) { + onChange(value); + } + }; + + // 初始示例代码 + function getDefaultCode(lang: string) { + if (lang === 'javascript') { + return `// 示例代码 +function checkRule(data) { + // data 包含抽取的字段 + try { + // 在此编写检查逻辑 + if (data.fieldName && condition) { + return { + pass: true, + message: "检查通过" + }; + } else { + return { + pass: false, + message: "检查不通过,原因:..." + }; + } + } catch (error) { + return { + pass: false, + message: "执行出错:" + error.message + }; + } +}`; + } else { + return `# 示例代码 +def check_rule(data): + # data 包含抽取的字段 + try: + # 在此编写检查逻辑 + if 'field_name' in data and condition: + return { + 'pass': True, + 'message': "检查通过" + } + else: + return { + 'pass': False, + 'message': "检查不通过,原因:..." + } + except Exception as error: + return { + 'pass': False, + 'message': f"执行出错:{str(error)}" + }`; + } + } + + // 当语言变化时更新代码 + useEffect(() => { + if (!initialValue) { + setCode(getDefaultCode(language)); + } + }, [language, initialValue]); + + return ( +
+
+
+
{language === 'javascript' ? 'script.js' : 'script.py'}
+
+ +
+
+
+ +
+
+ {copySuccess && ( +
+ 代码已复制到剪贴板 +
+ )} +
+ ); +} \ No newline at end of file diff --git a/app/root.tsx b/app/root.tsx index 0c813a8..f4eddbf 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -14,7 +14,7 @@ import { Layout } from "~/components/layout/Layout"; import { ErrorBoundary as AppErrorBoundary } from "~/components/error/ErrorBoundary"; import "remixicon/fonts/remixicon.css"; // 导入样式 -import styles from "~/styles/main.css?url"; +import mainStyles from "~/styles/main.css?url"; // 添加客户端hydration错误处理 // if (typeof window !== "undefined") { @@ -42,7 +42,7 @@ export const meta: MetaFunction = () => { // 使用links函数为应用加载CSS和其他资源 export function links() { return [ - { rel: "stylesheet", href: styles }, + { rel: "stylesheet", href: mainStyles }, { rel: "preconnect", href: "https://fonts.googleapis.com" }, { rel: "preconnect", href: "https://fonts.gstatic.com", crossOrigin: "anonymous" }, { rel: "stylesheet", href: "https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap" } @@ -55,6 +55,19 @@ export default function App() { +

评查设置

@@ -1411,6 +1696,15 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { placeholder="请输入自定义组合逻辑,例如:(规则1 AND 规则2) OR 规则3" value={customLogic} onChange={handleCustomLogicChange} + onBlur={() => { + // 确保在失去焦点时也触发更新 + if (onChange) { + onChange({ + combinationLogic: 'custom', + customLogic: customLogic + }); + } + }} >
使用规则编号和逻辑运算符(AND、OR、NOT)组合 @@ -1675,6 +1969,25 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) {
+ {/* 分数设置 */} +
+ +
+ handleScoreChange(e.target.value)} + /> + +
+
该评查点的分值,范围0-100
+
+ {/* 动作描述区域 */} {post_action && post_action !== 'none' && (
diff --git a/app/contexts/RuleContext.tsx b/app/contexts/RuleContext.tsx new file mode 100644 index 0000000..6369a9e --- /dev/null +++ b/app/contexts/RuleContext.tsx @@ -0,0 +1,26 @@ +import { createContext } from 'react'; + +/** + * 规则上下文类型 + * 用于在抽取设置和评查设置之间共享数据 + */ +export interface RuleContextType { + /** + * 抽取的字段列表 + */ + extractionFields: string[]; + + /** + * 更新字段列表的函数 + */ + updateFields: (fields: string[]) => void; +} + +/** + * 创建规则上下文 + * 用于在抽取设置和评查设置组件之间共享字段数据 + */ +export const RuleContext = createContext({ + extractionFields: [], + updateFields: () => {} +}); \ No newline at end of file diff --git a/app/entry.client.tsx b/app/entry.client.tsx index 94d5dc0..30656b8 100644 --- a/app/entry.client.tsx +++ b/app/entry.client.tsx @@ -5,14 +5,12 @@ */ import { RemixBrowser } from "@remix-run/react"; -import { startTransition, StrictMode } from "react"; +import { startTransition } from "react"; import { hydrateRoot } from "react-dom/client"; startTransition(() => { hydrateRoot( document, - - - + ); }); diff --git a/app/routes/rules.new.tsx b/app/routes/rules.new.tsx index 486faef..4459efb 100644 --- a/app/routes/rules.new.tsx +++ b/app/routes/rules.new.tsx @@ -2,7 +2,8 @@ import { type MetaFunction } from "@remix-run/node"; import { useState, useEffect } 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 { ReviewSettings } from "~/components/rules/new/ReviewSettings"; +import { RuleContext } from "~/contexts/RuleContext"; import { ActionButtons } from "~/components/rules/new/ActionButtons"; import { PageHeader } from "~/components/rules/new/PageHeader"; import rulesStyles from "~/styles/rules.css?url"; @@ -119,7 +120,10 @@ interface FormDataType { suggestion_message_type: string; post_action: string; action_config: string; - type?: string; + type: string; + evaluation_point_groups_pid: number | null; + score: number; + scoreDisplay?: string; } interface ApiPointData { @@ -135,6 +139,7 @@ interface ApiPointData { content: string; }; evaluation_point_groups_id: number | null; + evaluation_point_groups_pid?: number | null; extraction_config: ApiExtactionConfigType; evaluation_config: { logicType?: string; @@ -152,6 +157,21 @@ interface ApiPointData { post_action: string; action_config: string; type?: string; + meta?: { + type: string; + pid?: number; + [key: string]: unknown; + }; + score?: number; + scoreDisplay?: string; +} + +// API响应数据类型 +interface ApiResponse { + code?: number; + msg?: string; + data?: Array> | Record; + [key: string]: unknown; } export default function RuleNew() { @@ -160,6 +180,7 @@ export default function RuleNew() { const [extractionFields, setExtractionFields] = useState([]); const [isEditMode, setIsEditMode] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [evaluationPointGroups, setEvaluationPointGroups] = useState>([]); const [formData, setFormData] = useState({ // 基本信息字段 name: '', @@ -174,6 +195,7 @@ export default function RuleNew() { }, evaluation_point_groups_id: null, type: '', + evaluation_point_groups_pid: null, // 抽取设置 extraction_config: { @@ -211,7 +233,10 @@ export default function RuleNew() { // 评查后动作 post_action: 'none', - action_config: '' + action_config: '', + + // 分数 + score: 0 }); // 页面加载时检查URL中是否有ID参数,如果有则为编辑模式 @@ -223,7 +248,95 @@ export default function RuleNew() { setIsEditMode(true); fetchEvaluationPoint(parseInt(id)); } - }, [location]); + + // 获取评查点组数据 + fetchEvaluationPointGroups(); + }, [location.search]); + + // 获取评查点组数据 + const fetchEvaluationPointGroups = async () => { + try { + console.log("获取评查点组数据"); + const response = await fetch("http://127.0.0.1:9000/admin/evaluation_point_groups", { + method: 'GET', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json' + } + }); + + if (!response.ok) { + const errorText = await response.text(); + console.error(`API响应错误: ${response.status}`, errorText); + throw new Error(`获取评查点组数据失败: ${response.status} - ${errorText}`); + } + + const responseData = await response.json(); + console.log("评查点组数据原始响应:", responseData); + + // 适配API响应格式 + let groupsData: Array<{id: number, pid: number, code: string, name: string, is_enabled: boolean}> = []; + if (responseData && typeof responseData === 'object') { + if (responseData.code === 0 && responseData.data) { + // 新API格式 + groupsData = Array.isArray(responseData.data) ? responseData.data : [responseData.data]; + } else if (Array.isArray(responseData)) { + // 旧API格式 + groupsData = responseData; + } else if (responseData.data && Array.isArray(responseData.data)) { + // 旧API格式 + groupsData = responseData.data; + } + } + + // 确保所有项都有必需的字段 + groupsData = groupsData.filter(item => + item && typeof item === 'object' && + 'id' in item && + 'pid' in item && + 'code' in item && + 'name' in item + ); + + console.log("处理后的评查点组数据:", groupsData); + console.log("根级评查点类型:", groupsData.filter(group => group.pid === 0)); + setEvaluationPointGroups(groupsData); + + // 如果表单数据已加载但类型未设置,尝试根据evaluation_point_groups_pid设置类型 + if (formData.id && !formData.type && formData.evaluation_point_groups_pid) { + console.log("评查点组数据加载后更新类型,当前pid:", formData.evaluation_point_groups_pid); + + const typeGroup = groupsData.find(group => group.id === formData.evaluation_point_groups_pid); + if (typeGroup) { + console.log("找到对应的类型组:", typeGroup); + setFormData(prevData => ({ + ...prevData, + type: typeGroup.code + })); + console.log("根据评查点类型ID更新类型为:", typeGroup.code); + } else if (formData.evaluation_point_groups_id) { + // 通过规则组查找类型 + const selectedGroup = groupsData.find(group => group.id === formData.evaluation_point_groups_id); + if (selectedGroup && selectedGroup.pid !== 0) { + const parentTypeGroup = groupsData.find(group => group.id === selectedGroup.pid); + if (parentTypeGroup) { + console.log("通过规则组找到类型组:", parentTypeGroup); + setFormData(prevData => ({ + ...prevData, + type: parentTypeGroup.code, + evaluation_point_groups_pid: parentTypeGroup.id + })); + console.log("根据规则组更新类型为:", parentTypeGroup.code); + } + } + } + } + } catch (error) { + console.error('获取评查点组数据失败:', error); + // 显示错误提示但不影响应用继续使用 + alert(`获取评查点组数据失败: ${error instanceof Error ? error.message : '未知错误'}\n将使用默认数据`); + } + }; // 获取评查点数据 const fetchEvaluationPoint = async (id: number) => { @@ -233,7 +346,8 @@ export default function RuleNew() { const response = await fetch(`http://127.0.0.1:9000/admin/evaluation_points?id=eq.${id}`, { method: 'GET', headers: { - 'Accept': 'application/json' + 'Accept': 'application/json', + 'Content-Type': 'application/json' } }); @@ -246,18 +360,36 @@ export default function RuleNew() { const responseData = await response.json(); console.log("API响应数据:", responseData); - // 处理多种可能的响应格式 - if (Array.isArray(responseData)) { - if (responseData.length > 0) { - console.log("数组格式响应,使用第一项"); - processPointData(responseData[0]); - } else { - console.error("数组为空,未找到数据"); - throw new Error(`未找到ID为${id}的评查点数据`); - } - } else if (responseData && typeof responseData === 'object') { - // 处理可能的包装对象格式 - if (responseData.data) { + // 新的API响应格式适配 + if (responseData && typeof responseData === 'object') { + if (responseData.code === 0 && responseData.data) { + // 符合新的API响应格式 + if (Array.isArray(responseData.data)) { + if (responseData.data.length > 0) { + console.log("处理数组数据的第一项"); + processPointData(responseData.data[0]); + } else { + console.error("数据数组为空"); + throw new Error(`未找到ID为${id}的评查点数据`); + } + } else if (typeof responseData.data === 'object') { + console.log("处理单个对象数据"); + processPointData(responseData.data); + } + } else if (responseData.code !== 0) { + // API返回错误 + throw new Error(responseData.msg || `获取数据失败,错误码: ${responseData.code}`); + } else if (Array.isArray(responseData)) { + // 处理旧API格式:数组形式 + if (responseData.length > 0) { + console.log("数组格式响应,使用第一项"); + processPointData(responseData[0]); + } else { + console.error("数组为空,未找到数据"); + throw new Error(`未找到ID为${id}的评查点数据`); + } + } else if (responseData.data) { + // 处理旧API格式:包含data字段的对象 console.log("包装对象格式响应,使用data字段"); if (Array.isArray(responseData.data)) { if (responseData.data.length > 0) { @@ -273,7 +405,7 @@ export default function RuleNew() { throw new Error('数据格式不正确: data字段不是预期的格式'); } } else if (responseData.id) { - // 可能是直接返回的单个对象 + // 处理旧API格式:直接返回的对象 console.log("单对象格式响应"); processPointData(responseData); } else { @@ -287,6 +419,8 @@ export default function RuleNew() { } catch (error) { console.error('获取评查点数据失败:', error); alert(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`); + // 获取数据失败时返回上一页 + navigate(-1); } finally { setIsLoading(false); } @@ -308,6 +442,7 @@ export default function RuleNew() { console.log("提取配置:", extractionConfig); console.log("评查配置:", evaluationConfig); + console.log("分数:", pointData.score); // 提取字段列表,用于规则设置 const extractedFields: string[] = []; @@ -335,8 +470,36 @@ export default function RuleNew() { console.log("提取的字段:", extractedFields); setExtractionFields(extractedFields); + // 提取评查点类型ID + let pointGroupPid: number | null = null; + if (pointData.evaluation_point_groups_pid !== undefined && pointData.evaluation_point_groups_pid !== null) { + // 直接使用字段值 + pointGroupPid = typeof pointData.evaluation_point_groups_pid === 'number' + ? pointData.evaluation_point_groups_pid + : null; + console.log("从evaluation_point_groups_pid获取类型ID:", pointGroupPid); + } else if (pointData.meta && typeof pointData.meta === 'object' && pointData.meta.pid !== undefined) { + // 从meta中提取 + pointGroupPid = typeof pointData.meta.pid === 'number' + ? pointData.meta.pid + : null; + console.log("从meta.pid获取类型ID:", pointGroupPid); + } + + console.log("最终确定的类型ID:", pointGroupPid); + + // 处理分数 + let pointScore = 0; + if (pointData.score !== undefined && pointData.score !== null) { + pointScore = typeof pointData.score === 'number' ? pointData.score : 0; + console.log("从score字段获取分数:", pointScore); + } else if (pointData.meta && typeof pointData.meta === 'object' && pointData.meta.score !== undefined) { + pointScore = typeof pointData.meta.score === 'number' ? pointData.meta.score : 0; + console.log("从meta.score获取分数:", pointScore); + } + // 构建表单数据 - const newFormData = { + const newFormData: FormDataType = { id: pointData.id, name: pointData.name || '', code: pointData.code || '', @@ -349,7 +512,8 @@ export default function RuleNew() { content: '' }, evaluation_point_groups_id: pointData.evaluation_point_groups_id || null, - type: pointData.type || '', + evaluation_point_groups_pid: pointGroupPid, + type: '', // 先置空,稍后根据pid推断 // 将API数据格式转换为内部使用的格式 extraction_config: { @@ -456,11 +620,76 @@ export default function RuleNew() { suggestion_message: pointData.suggestion_message || '', suggestion_message_type: pointData.suggestion_message_type || 'warning', post_action: pointData.post_action || 'none', - action_config: pointData.action_config || '' + action_config: pointData.action_config || '', + score: pointScore, + scoreDisplay: pointData.scoreDisplay }; console.log("设置表单数据:", newFormData); setFormData(newFormData); + + // 根据evaluation_point_groups_pid查找对应的评查点类型 + if (evaluationPointGroups.length > 0) { + console.log("开始根据pid查找对应的评查点类型, pid:", newFormData.evaluation_point_groups_pid); + console.log("当前评查点组数据:", evaluationPointGroups); + + // 如果有evaluation_point_groups_pid,直接查找对应的类型组 + if (newFormData.evaluation_point_groups_pid) { + const typeGroup = evaluationPointGroups.find(group => group.id === newFormData.evaluation_point_groups_pid); + if (typeGroup) { + console.log("找到对应的类型组:", typeGroup); + const updatedFormData = { + ...newFormData, + type: typeGroup.code + }; + console.log("根据评查点类型ID设置类型:", typeGroup.code); + setFormData(updatedFormData); + return; + } + } + + // 如果评查点组ID存在,尝试查找对应的类型 + if (newFormData.evaluation_point_groups_id) { + console.log("通过规则组ID查找对应的类型组"); + const selectedGroup = evaluationPointGroups.find(group => group.id === newFormData.evaluation_point_groups_id); + console.log("找到的规则组:", selectedGroup); + + if (selectedGroup && selectedGroup.pid !== 0) { + const typeGroup = evaluationPointGroups.find(group => group.id === selectedGroup.pid); + console.log("找到的类型组:", typeGroup); + + if (typeGroup && typeGroup.code) { + // 更新类型和评查点类型ID + const updatedFormData = { + ...newFormData, + type: typeGroup.code, + evaluation_point_groups_pid: typeGroup.id + }; + console.log("根据规则组ID设置类型:", typeGroup.code, "和评查点类型ID:", typeGroup.id); + setFormData(updatedFormData); + } + } + } + } else { + console.log("无评查点组数据,无法推断类型"); + + // 尝试从meta或type字段获取类型信息 + if (pointData.type) { + console.log("从type字段获取类型:", pointData.type); + const updatedFormData = { + ...newFormData, + type: pointData.type + }; + setFormData(updatedFormData); + } else if (pointData.meta && typeof pointData.meta === 'object' && 'type' in pointData.meta) { + console.log("从meta.type字段获取类型:", pointData.meta.type); + const updatedFormData = { + ...newFormData, + type: pointData.meta.type as string + }; + setFormData(updatedFormData); + } + } } catch (error) { console.error("处理评查点数据时出错:", error); throw new Error(`处理评查点数据时出错: ${error instanceof Error ? error.message : '未知错误'}`); @@ -484,9 +713,14 @@ export default function RuleNew() { const handleExtractionSettingsChange = (data: Record) => { setFormData(prevData => { // 获取数据 - const extractionMethod = data.extractionMethod as string; const regexFields = data.regexFields as Array<{ id: string; fieldName: string; regex: string }>; const fields = data.fields as Record; + const allFields = data.allFields as string[]; + + if (allFields && allFields.length > 0) { + // 更新抽取字段列表,用于在规则设置中使用 + updateExtractionFields(allFields); + } // 根据抽取方法更新对应字段 const updatedExtractionConfig = { ...prevData.extraction_config }; @@ -514,15 +748,22 @@ export default function RuleNew() { // 更新提示词设置 if (data.promptSettings) { - const promptSettings = data.promptSettings as Record; + const promptSettings = data.promptSettings as Record>; - // 确定当前是处理哪种类型的提示词 - if (extractionMethod === 'llm_ocr') { - updatedExtractionConfig.llm_ocr.prompt_setting.type = promptSettings.type as string || 'system'; - updatedExtractionConfig.llm_ocr.prompt_setting.template = promptSettings.content as string || ''; - } else if (extractionMethod === 'llm') { - updatedExtractionConfig.llm_vl.prompt_setting.type = promptSettings.type as string || 'system'; - updatedExtractionConfig.llm_vl.prompt_setting.template = promptSettings.content as string || ''; + // 更新大模型抽取提示词设置 + if (promptSettings.llm_ocr) { + updatedExtractionConfig.llm_ocr.prompt_setting = { + type: promptSettings.llm_ocr.type as string || 'system', + template: promptSettings.llm_ocr.content as string || '' + }; + } + + // 更新多模态抽取提示词设置 + if (promptSettings.llm) { + updatedExtractionConfig.llm_vl.prompt_setting = { + type: promptSettings.llm.type as string || 'system', + template: promptSettings.llm.content as string || '' + }; } } else { // 兼容旧的API @@ -547,6 +788,13 @@ export default function RuleNew() { } } + // 记录状态更新到控制台以便调试 + console.log("抽取设置更新:", { + fields: fields, + regexFields: regexFields, + extractionConfig: updatedExtractionConfig + }); + return { ...prevData, extraction_config: updatedExtractionConfig @@ -559,6 +807,9 @@ export default function RuleNew() { setFormData(prevData => { const updatedData = { ...prevData }; + // 记录所有收到的数据 + console.log("评查设置更新数据:", data); + // 更新规则 if (data.rules) { updatedData.evaluation_config.rules = data.rules as Rule[]; @@ -600,6 +851,17 @@ export default function RuleNew() { updatedData.action_config = data.action_config as string; } + // 更新分数 + if (data.score !== undefined) { + const scoreValue = parseFloat(data.score as string); + updatedData.score = isNaN(scoreValue) ? 0 : scoreValue; + } + + // 更新分数显示值 + if (data.scoreDisplay !== undefined) { + updatedData.scoreDisplay = data.scoreDisplay as string; + } + return updatedData; }); }; @@ -633,12 +895,14 @@ export default function RuleNew() { } }, regex: { - fields: (formData.extraction_config.ocr_regex.fields || []).map((field: RegexField) => { - return { - field: field.fieldName || '', - pattern: field.regex || '' - }; - }) + fields: (formData.extraction_config.ocr_regex.fields || []) + .filter(field => field.fieldName && field.fieldName.trim() !== '') + .map((field: RegexField) => { + return { + field: field.fieldName || '', + pattern: field.regex || '' + }; + }) } }; @@ -646,66 +910,72 @@ export default function RuleNew() { const evaluationConfig = { logicType: formData.evaluation_config.logicType || 'and', customLogic: formData.evaluation_config.customLogic || '', - rules: (formData.evaluation_config.rules || []).map((rule: Rule) => { - let config = {}; - - // 根据不同的规则类型生成对应的配置 - switch (rule.type) { - case 'exists': - config = { - fields: rule.config?.selectedFields || [], - logic: rule.config?.logicRelation || 'and' - }; - break; - case 'consistency': - config = { - pairs: rule.config?.pairs || [], - logic: rule.config?.logicRelation || 'and' - }; - break; - case 'format': - config = { - field: rule.config?.field || '', - formatType: rule.config?.formatType || '', - parameters: rule.config?.parameters || '' - }; - break; - case 'logic': - config = { - conditions: rule.config?.conditions || [], - logic: rule.config?.logicRelation || 'and' - }; - break; - case 'regex': - config = { - field: rule.config?.field || '', - pattern: rule.config?.pattern || '', - matchType: rule.config?.matchType || 'match' - }; - break; - case 'ai': - config = { - model: rule.config?.model || 'qwen14b', - temperature: rule.config?.temperature || 0.1, - prompt: rule.config?.prompt || '' - }; - break; - case 'code': - config = { - language: rule.config?.language || 'javascript', - code: rule.config?.code || '' - }; - break; - default: - config = rule.config || {}; - } - - return { - id: rule.id, - type: rule.type, - config - }; - }) + rules: (formData.evaluation_config.rules || []) + .filter((rule: Rule) => rule.type && rule.type.trim() !== '') + .map((rule: Rule) => { + let config = {}; + + // 根据不同的规则类型生成对应的配置 + switch (rule.type) { + case 'exists': + config = { + fields: rule.config?.fields || [], + logic: rule.config?.logicRelation || 'and' + }; + break; + case 'consistency': + config = { + pairs: rule.config?.pairs || [], + logic: rule.config?.logicRelation || 'and' + }; + break; + case 'format': + config = { + field: rule.config?.field || '', + formatType: rule.config?.formatType || '', + parameters: rule.config?.parameters || '' + }; + break; + case 'logic': + config = { + conditions: rule.config?.conditions || [], + logic: rule.config?.logicRelation || 'and' + }; + break; + case 'regex': + config = { + field: rule.config?.field || '', + pattern: rule.config?.pattern || '', + matchType: rule.config?.matchType || 'match' + }; + break; + case 'ai': + config = { + model: rule.config?.model || 'qwen14b', + temperature: rule.config?.temperature || 0.1, + prompt: rule.config?.prompt || '' + }; + break; + case 'code': + config = { + language: rule.config?.language || 'javascript', + code: rule.config?.code || '' + }; + break; + default: + config = { ...rule.config }; + // 清除辅助字段,避免发送无效数据 + if (typeof config === 'object' && config !== null) { + delete (config as Record).availableFields; + } + } + + return { + id: rule.id, + type: rule.type, + config + }; + }) }; // 构建完整的评查点数据 @@ -713,18 +983,20 @@ export default function RuleNew() { code: formData.code, name: formData.name, evaluation_point_groups_id: formData.evaluation_point_groups_id, + evaluation_point_groups_pid: formData.evaluation_point_groups_pid, risk: formData.risk, description: formData.description, is_enabled: isDraft ? false : formData.is_enabled, references_laws: formData.references_laws, extraction_config: extractionConfig, evaluation_config: evaluationConfig, - pass_message: formData.pass_message, - fail_message: formData.fail_message, - suggestion_message: formData.suggestion_message, - suggestion_message_type: formData.suggestion_message_type, - post_action: formData.post_action, - action_config: formData.action_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: Number(formData.score) // 确保是数字类型 }; // 如果是编辑模式,添加ID @@ -738,29 +1010,67 @@ export default function RuleNew() { return evaluationPointData; }; + // 确定使用的HTTP方法和URL + const getEndpointAndMethod = (id?: number) => { + let method = 'POST'; + let endpoint = 'http://127.0.0.1:9000/admin/evaluation_points'; + + // 如果是编辑模式,使用PATCH更新现有记录 + if (id) { + method = 'PATCH'; + endpoint = `http://127.0.0.1:9000/admin/evaluation_points?id=eq.${id}`; + } + + return { method, endpoint }; + }; + // 保存评查点 const handleSave = async () => { try { setIsLoading(true); + + // 基本验证 + if (!formData.name || !formData.code) { + alert("请填写评查点名称和编码,这些是必填项"); + setIsLoading(false); + return; + } + + // 检查评查点类型 + if (!formData.type) { + alert("请选择评查点类型"); + setIsLoading(false); + return; + } + + // 检查所属规则组 + if (!formData.evaluation_point_groups_id) { + alert("请选择所属规则组"); + setIsLoading(false); + return; + } + + // 检查评查规则 + if (!formData.evaluation_config.rules || formData.evaluation_config.rules.length === 0 || + !formData.evaluation_config.rules.some(rule => rule.type && rule.type.trim() !== '')) { + alert("请至少添加一条有效的评查规则"); + setIsLoading(false); + return; + } + const evaluationPointData = formatDataForApi(formData); console.log("保存数据:", evaluationPointData); - // 确定使用的HTTP方法和URL - let method = 'POST'; - let endpoint = 'http://127.0.0.1:9000/admin/evaluation_points'; - - - // 如果是编辑模式,使用PATCH更新现有记录 - if (isEditMode && formData.id) { - method = 'PATCH'; - endpoint = `http://127.0.0.1:9000/admin/evaluation_points?id=eq.${formData.id}`; - } - + const { method, endpoint } = getEndpointAndMethod(formData.id); console.log(`发送${method}请求到`, endpoint); // 发送数据到API const response = await fetch(endpoint, { method: method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, body: JSON.stringify(evaluationPointData) }); @@ -774,10 +1084,10 @@ export default function RuleNew() { // 尝试解析错误响应为JSON const errorJson = JSON.parse(errorText); console.error("解析的错误JSON:", errorJson); - throw new Error(`API响应错误: ${errorJson.msg || response.statusText}`); + throw new Error(`保存失败: ${errorJson.msg || response.statusText}`); } catch (parseError) { // 如果无法解析为JSON,使用原始文本 - throw new Error(`API响应错误: ${response.status} - ${errorText}`); + throw new Error(`保存失败: ${response.status} - ${errorText}`); } } @@ -789,40 +1099,8 @@ export default function RuleNew() { const responseData = await response.json(); console.log("API响应数据:", responseData); - // 处理多种可能的响应格式 - if (Array.isArray(responseData)) { - if (responseData.length > 0) { - console.log("保存成功 (数组响应):", responseData[0]); - alert("保存成功!"); - navigate('/rules'); - } else { - console.warn("响应数组为空"); - alert("操作已完成,但服务器未返回数据"); - navigate('/rules'); - } - } else if (responseData && typeof responseData === 'object') { - if (responseData.data) { - console.log("保存成功 (带data字段):", responseData.data); - alert("保存成功!"); - navigate('/rules'); - } else if (responseData.id) { - console.log("保存成功 (直接对象):", responseData); - alert("保存成功!"); - navigate('/rules'); - } else if (responseData.code === 0) { - console.log("保存成功 (带code字段):", responseData); - alert("保存成功!"); - navigate('/rules'); - } else { - console.warn("响应对象格式不符合预期", responseData); - alert("操作已完成,但返回的数据格式不符合预期"); - navigate('/rules'); - } - } else { - console.warn("响应不是数组或对象", responseData); - alert("操作已完成,但返回的数据格式不符合预期"); - navigate('/rules'); - } + // 处理响应 + await handleApiResponse(responseData, isEditMode); } else { // 非JSON响应 const text = await response.text(); @@ -842,32 +1120,59 @@ export default function RuleNew() { const handleSaveDraft = async () => { try { setIsLoading(true); + + // 基本验证 + if (!formData.name || !formData.code) { + alert("请填写评查点名称和编码,这些是必填项"); + setIsLoading(false); + return; + } + + // 检查评查点类型 + if (!formData.type) { + alert("请选择评查点类型"); + setIsLoading(false); + return; + } + + // 检查所属规则组 + if (!formData.evaluation_point_groups_id) { + alert("请选择所属规则组"); + setIsLoading(false); + return; + } + const draftData = formatDataForApi(formData, true); console.log("保存草稿数据:", draftData); - // 确定使用的HTTP方法和URL - let method = 'POST'; - let endpoint = 'http://127.0.0.1:9000/admin/evaluation_points'; - - - // 如果是编辑模式,使用PATCH更新现有记录 - if (isEditMode && formData.id) { - method = 'PATCH'; - endpoint = `http://127.0.0.1:9000/admin/evaluation_points?id=eq.${formData.id}`; - } - + const { method, endpoint } = getEndpointAndMethod(formData.id); console.log(`发送${method}请求到`, endpoint); // 发送数据到API const response = await fetch(endpoint, { method: method, + headers: { + 'Content-Type': 'application/json', + 'Accept': 'application/json' + }, body: JSON.stringify(draftData) }); + // 输出完整响应信息 + console.log("响应状态:", response.status, response.statusText); + if (!response.ok) { const errorText = await response.text(); console.error(`API响应错误: ${response.status}`, errorText); - throw new Error(`API响应错误: ${response.status} - ${errorText}`); + try { + // 尝试解析错误响应为JSON + const errorJson = JSON.parse(errorText); + console.error("解析的错误JSON:", errorJson); + throw new Error(`保存失败: ${errorJson.msg || response.statusText}`); + } catch (parseError) { + // 如果无法解析为JSON,使用原始文本 + throw new Error(`保存失败: ${response.status} - ${errorText}`); + } } const contentType = response.headers.get('content-type'); @@ -878,45 +1183,13 @@ export default function RuleNew() { const responseData = await response.json(); console.log("API响应数据:", responseData); - // 处理多种可能的响应格式 - if (Array.isArray(responseData)) { - if (responseData.length > 0) { - console.log("保存草稿成功 (数组响应):", responseData[0]); - alert("草稿保存成功!"); - navigate('/rules'); - } else { - console.warn("响应数组为空"); - alert("操作已完成,但服务器未返回数据"); - navigate('/rules'); - } - } else if (responseData && typeof responseData === 'object') { - if (responseData.data) { - console.log("保存草稿成功 (带data字段):", responseData.data); - alert("草稿保存成功!"); - navigate('/rules'); - } else if (responseData.id) { - console.log("保存草稿成功 (直接对象):", responseData); - alert("草稿保存成功!"); - navigate('/rules'); - } else if (responseData.code === 0) { - console.log("保存草稿成功 (带code字段):", responseData); - alert("草稿保存成功!"); - navigate('/rules'); - } else { - console.warn("响应对象格式不符合预期", responseData); - alert("操作已完成,但返回的数据格式不符合预期"); - navigate('/rules'); - } - } else { - console.warn("响应不是数组或对象", responseData); - alert("操作已完成,但返回的数据格式不符合预期"); - navigate('/rules'); - } + // 处理响应 + await handleApiResponse(responseData, isEditMode, true); } else { // 非JSON响应 const text = await response.text(); console.log("非JSON响应:", text); - alert("操作已完成!"); + alert("草稿已保存!"); navigate('/rules'); } } catch (error) { @@ -927,6 +1200,158 @@ export default function RuleNew() { } }; + // 处理API响应 + const handleApiResponse = async ( + responseData: + | ApiResponse + | Array<{ id: number; [key: string]: unknown }>, + isEditMode: boolean, + isDraft: boolean = false + ) => { + // 适配新的API响应格式 + if (responseData && typeof responseData === 'object') { + // 符合新的API规范 + if ('code' in responseData && responseData.code === 0) { + console.log(`${isDraft ? '草稿' : ''}保存成功 (新API格式):`, responseData.data); + alert(`${isDraft ? '草稿' : ''}保存成功!`); + + // 根据操作类型重定向 + if (isEditMode) { + // 编辑模式,保留在当前编辑页面 + navigate(`/rules/new?id=${formData.id}`); + } else { + // 创建模式,跳转到新创建数据的编辑页面 + let newId: number | undefined; + + if (responseData.data && Array.isArray(responseData.data) && responseData.data.length > 0) { + newId = responseData.data[0].id as number; + } else if (responseData.data && typeof responseData.data === 'object' && 'id' in responseData.data) { + newId = responseData.data.id as number; + } + + if (newId) { + navigate(`/rules/new?id=${newId}`); + } else { + // 无法获取ID,返回列表页 + navigate('/rules'); + } + } + return; + } else if ('code' in responseData && responseData.code !== 0) { + // API返回错误 + console.warn("API返回错误:", responseData.msg); + throw new Error(responseData.msg as string || "操作失败"); + } + } + + // 兼容处理旧的响应格式 + if (Array.isArray(responseData)) { + if (responseData.length > 0) { + console.log(`${isDraft ? '草稿' : ''}保存成功 (数组响应):`, responseData[0]); + alert(`${isDraft ? '草稿' : ''}保存成功!`); + + // 如果是创建,跳转到编辑页面 + if (!isEditMode && responseData[0].id) { + navigate(`/rules/new?id=${responseData[0].id}`); + } else { + navigate('/rules'); + } + } else { + console.warn("响应数组为空"); + alert("操作已完成,但服务器未返回数据"); + navigate('/rules'); + } + } else if (responseData && typeof responseData === 'object') { + if ('data' in responseData && responseData.data) { + console.log(`${isDraft ? '草稿' : ''}保存成功 (带data字段):`, responseData.data); + alert(`${isDraft ? '草稿' : ''}保存成功!`); + + let newId: number | undefined; + if (Array.isArray(responseData.data) && responseData.data.length > 0 && 'id' in responseData.data[0]) { + newId = responseData.data[0].id as number; + } else if (typeof responseData.data === 'object' && 'id' in responseData.data) { + newId = responseData.data.id as number; + } + + if (!isEditMode && newId) { + navigate(`/rules/new?id=${newId}`); + } else { + navigate('/rules'); + } + } else if ('id' in responseData && responseData.id) { + console.log(`${isDraft ? '草稿' : ''}保存成功 (直接对象):`, responseData); + alert(`${isDraft ? '草稿' : ''}保存成功!`); + + if (!isEditMode) { + navigate(`/rules/new?id=${responseData.id as number}`); + } else { + navigate('/rules'); + } + } else { + console.warn("响应对象格式不符合预期", responseData); + alert("操作已完成,但返回的数据格式不符合预期"); + navigate('/rules'); + } + } else { + console.warn("响应不是数组或对象", responseData); + alert("操作已完成,但返回的数据格式不符合预期"); + navigate('/rules'); + } + }; + + // 当评查点组数据和表单数据都加载完成后,确保类型信息被正确设置 + useEffect(() => { + // 仅在编辑模式下,且表单数据已加载,评查点组数据也已加载的情况下执行 + if ( + isEditMode && + formData.id && + evaluationPointGroups.length > 0 && + (!formData.type || formData.type === '') + ) { + console.log("检测到编辑模式下类型未设置,尝试自动设置类型"); + + // 首先尝试通过evaluation_point_groups_pid设置类型 + if (formData.evaluation_point_groups_pid) { + const typeGroup = evaluationPointGroups.find( + group => group.id === formData.evaluation_point_groups_pid + ); + + if (typeGroup) { + console.log("通过评查点类型ID找到类型组:", typeGroup); + setFormData(prevData => ({ + ...prevData, + type: typeGroup.code + })); + console.log("自动设置类型为:", typeGroup.code); + return; + } + } + + // 如果无法通过evaluation_point_groups_pid设置,尝试通过evaluation_point_groups_id设置 + if (formData.evaluation_point_groups_id) { + const ruleGroup = evaluationPointGroups.find( + group => group.id === formData.evaluation_point_groups_id + ); + + if (ruleGroup && ruleGroup.pid && ruleGroup.pid !== 0) { + const parentGroup = evaluationPointGroups.find( + group => group.id === ruleGroup.pid + ); + + if (parentGroup) { + console.log("通过规则组找到父级类型组:", parentGroup); + setFormData(prevData => ({ + ...prevData, + type: parentGroup.code, + evaluation_point_groups_pid: parentGroup.id + })); + console.log("自动设置类型为:", parentGroup.code); + } + } + } + } + }, [isEditMode, formData.id, formData.type, formData.evaluation_point_groups_id, formData.evaluation_point_groups_pid, evaluationPointGroups]); + return (
- +
@@ -971,7 +1400,9 @@ export default function RuleNew() { suggestion_message: formData.suggestion_message, suggestion_message_type: formData.suggestion_message_type, post_action: formData.post_action, - action_config: formData.action_config + action_config: formData.action_config, + score: formData.score, + scoreDisplay: formData.scoreDisplay }} /> diff --git a/app/utils.ts b/app/utils.ts new file mode 100644 index 0000000..9975548 --- /dev/null +++ b/app/utils.ts @@ -0,0 +1,68 @@ +/** + * 工具函数集合 + * 包含字段处理、防抖等通用功能 + */ + +/** + * 处理字段名,去除类型后缀 + * 例如: "字段名_类型" -> "字段名" + */ +export function processFieldName(field: string): string { + if (field.includes('_')) { + return field.split('_')[0]; // 只保留类型前面的字段名 + } + return field; +} + +/** + * 处理字段数组,去除类型后缀并去重 + */ +export function processFieldNames(fields: string[]): string[] { + // 处理字段,去掉类型后缀 + const processedFields = fields.map(processFieldName); + + // 去重并返回 + return [...new Set(processedFields)]; +} + +/** + * 创建防抖函数 + * @param fn 要执行的函数 + * @param delay 延迟时间(毫秒) + */ +export function debounce unknown>( + fn: T, + delay: number +): (...args: Parameters) => void { + let timer: NodeJS.Timeout | null = null; + + return function(...args: Parameters) { + if (timer) { + clearTimeout(timer); + } + + timer = setTimeout(() => { + fn(...args); + timer = null; + }, delay); + }; +} + +/** + * 比较两个数组是否有实质性不同 + * 用于避免不必要的状态更新 + */ +export function areArraysDifferent(arr1: T[], arr2: T[]): boolean { + return JSON.stringify(arr1) !== JSON.stringify(arr2); +} + +/** + * 查找两个数组之间的差异项 + * @returns 包含新增和删除项的对象 + */ +export function getArrayDifference(current: T[], previous: T[]): { added: T[], removed: T[] } { + const added = current.filter(item => !previous.includes(item)); + const removed = previous.filter(item => !current.includes(item)); + + return { added, removed }; +} \ No newline at end of file From a5cad46a844b76d401f0d2b42d891792aac53ba6 Mon Sep 17 00:00:00 2001 From: awen Date: Mon, 7 Apr 2025 20:24:52 +0800 Subject: [PATCH 12/23] =?UTF-8?q?=E7=BC=96=E8=BE=91=E6=83=85=E5=86=B5?= =?UTF-8?q?=E4=BC=98=E5=8C=96-=E9=9C=80=E8=A6=81=E7=BB=A7=E7=BB=AD?= =?UTF-8?q?=E5=AE=8C=E5=96=84-=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/components/rules/new/ReviewSettings.tsx | 131 ++++++++++--------- app/routes/rules.new.tsx | 132 +++++++++++++++++--- 2 files changed, 188 insertions(+), 75 deletions(-) diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index f8603cd..7f8485a 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -73,59 +73,60 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { // 使用useRef跟踪是否已经初始化过 const initializedRef = useRef(false); + // 保存初始数据的引用,用于检测是否有实际变更 + const initialDataRef = useRef(null); // 加载初始数据 useEffect(() => { // 如果已经初始化过,则跳过此次处理 - if (initialData && !initializedRef.current) { - initializedRef.current = true; - console.log('加载初始数据(首次):', initialData); - - // 设置规则 - if (initialData.rules && initialData.rules.length > 0) { - // 确保每个规则都有完整的配置 - const enhancedRules = initialData.rules.map(rule => { - // 根据规则类型,确保config包含必要的字段 - let config = { ...rule.config }; - - // 确保有用于展示的字段 - if (rule.type === 'format' && config.field && !config.checkField) { - config.checkField = config.field; + if (initializedRef.current) { + console.log("ReviewSettings已初始化,跳过后续初始化处理"); + return; + } + + // 记录初始化处理 + console.log("ReviewSettings开始初始化,数据:", initialData); + + // 保存初始数据引用,用于后续比较 + initialDataRef.current = JSON.parse(JSON.stringify(initialData)); + + // 设置已初始化标记 + initializedRef.current = true; + + // 只有在有initialData时才进行初始化设置 + if (initialData) { + // 处理初始规则数据 + if (initialData.rules && Array.isArray(initialData.rules) && initialData.rules.length > 0) { + console.log("设置初始规则数据:", initialData.rules); + + const validRules = initialData.rules.map(rule => { + // 确保每个规则都有id + if (!rule.id) { + rule.id = `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } - if (rule.type === 'regex') { - if (config.field && !config.checkField) { - config.checkField = config.field; - } - if (config.pattern && !config.regexPattern) { - config.regexPattern = config.pattern; - } + // 确保配置对象存在 + if (!rule.config) { + rule.config = {}; } - // 确保存在字段有正确的内部表示 - if (rule.type === 'exists' && config.fields && !config.selectedFields) { - config.selectedFields = config.fields; - config.existsLogic = config.logic || 'all'; + // 添加可用字段 + if (availableFields.length > 0) { + rule.config.availableFields = availableFields; } - // 对于条件逻辑规则 - if ((rule.type === 'consistency' || rule.type === 'logic') && config.logic && !config.logicRelation) { - config.logicRelation = config.logic; - } - - // 确保所有规则都有可用字段列表 - if (!config.availableFields) { - config.availableFields = availableFields; - } - - return { - ...rule, - config - }; + return rule; }); - console.log('增强后的规则(首次):', enhancedRules); - setRules(enhancedRules); + // 如果没有规则或规则为空,添加一个默认规则 + if (validRules.length === 0) { + validRules.push({ id: '1', type: '', config: { availableFields } }); + } + + setRules(validRules); + } else { + // 如果rules为空或不是数组,添加一个默认规则 + setRules([{ id: '1', type: '', config: { availableFields } }]); } // 设置组合逻辑 @@ -1520,7 +1521,7 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { break; } - // 移除只在UI使用的字段 + // 移除辅助用的UI字段 delete processedConfig.availableFields; return { @@ -1528,30 +1529,40 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { type: rule.type, config: processedConfig }; - }) + }).filter(rule => rule.type && rule.type.trim() !== '') }; - if (onChange) { - const updateData = { - rules: config.rules, - combinationLogic: config.logicType, - customLogic: config.customLogic, - pass_message: pass_message, - fail_message: fail_message, - suggestion_message: suggestion_message, - suggestion_message_type: suggestion_message_type, - post_action: post_action, - action_config: action_config, - score: score, - scoreDisplay: scoreDisplay - }; - - onChange(updateData); - } + // 使用setTimeout避免连锁更新 + setTimeout(() => { + if (onChange) { + onChange({ + rules: config.rules, + combinationLogic: config.logicType, + customLogic: config.customLogic, + pass_message: pass_message, + fail_message: fail_message, + suggestion_message: suggestion_message, + suggestion_message_type: suggestion_message_type, + post_action: post_action, + action_config: action_config, + score: score, + scoreDisplay: scoreDisplay + }); + } + }, 0); return config; }, [rules, combinationLogic, customLogic, pass_message, fail_message, suggestion_message, suggestion_message_type, post_action, action_config, score, scoreDisplay, onChange]); + // 组件初次渲染后,主动发送一次完整配置数据 + useEffect(() => { + // 如果有初始数据,在组件挂载后主动发送一次完整规则配置 + if (initialDataRef.current && onChange) { + console.log("组件挂载后发送初始完整配置"); + setTimeout(() => generateEvaluationConfig(), 100); + } + }, [generateEvaluationConfig, onChange]); + // 处理评查结果消息变更 const handleMessageChange = (type: string, value: string) => { switch(type) { diff --git a/app/routes/rules.new.tsx b/app/routes/rules.new.tsx index 4459efb..369f95c 100644 --- a/app/routes/rules.new.tsx +++ b/app/routes/rules.new.tsx @@ -174,6 +174,20 @@ interface ApiResponse { [key: string]: unknown; } +// 深拷贝工具函数 +function deepClone(obj: T): T { + if (obj === null || obj === undefined || typeof obj !== 'object') { + return obj; + } + + try { + return JSON.parse(JSON.stringify(obj)); + } catch (err) { + console.error('深拷贝对象失败:', err); + return obj; + } +} + export default function RuleNew() { const navigate = useNavigate(); const location = useLocation(); @@ -442,7 +456,7 @@ export default function RuleNew() { console.log("提取配置:", extractionConfig); console.log("评查配置:", evaluationConfig); - console.log("分数:", pointData.score); + console.log("评查规则详细信息:", JSON.stringify(evaluationConfig.rules || [], null, 2)); // 提取字段列表,用于规则设置 const extractedFields: string[] = []; @@ -548,7 +562,7 @@ export default function RuleNew() { evaluation_config: { logicType: evaluationConfig.logicType || 'and', customLogic: evaluationConfig.customLogic || '', - rules: (evaluationConfig.rules || []).map((rule) => { + rules: validateAndFixRules((evaluationConfig.rules || []).map((rule) => { // 将API规则格式转换为UI使用的格式 let config: RuleConfigType = {}; @@ -607,12 +621,17 @@ export default function RuleNew() { config = rule.config || {}; } + // 保存原始规则数据以便后续处理 + if (typeof config === 'object' && config !== null) { + config._originalData = JSON.parse(JSON.stringify(rule.config || {})); + } + return { id: rule.id, type: rule.type, config }; - }) + })) }, pass_message: pointData.pass_message || '文档检查通过,符合规范要求。', @@ -810,9 +829,12 @@ export default function RuleNew() { // 记录所有收到的数据 console.log("评查设置更新数据:", data); - // 更新规则 + // 更新规则 - 只有当明确提供了rules数据时才更新 if (data.rules) { - updatedData.evaluation_config.rules = data.rules as Rule[]; + // 验证并修复规则数据 + const validatedRules = validateAndFixRules(data.rules as Rule[]); + console.log("规则数据验证后:", validatedRules); + updatedData.evaluation_config.rules = validatedRules; } // 更新组合逻辑 @@ -866,8 +888,49 @@ export default function RuleNew() { }); }; + // 检查并修复评查规则数据,确保数据的完整性 + const validateAndFixRules = (rules: Rule[] | undefined): Rule[] => { + if (!rules || !Array.isArray(rules)) { + console.log("规则数据无效或为空,返回空数组"); + return []; + } + + // 过滤无效规则,确保每个规则都有type字段 + const validRules = rules.filter(rule => { + if (!rule || typeof rule !== 'object') { + console.log("发现无效规则对象:", rule); + return false; + } + + if (!rule.type || typeof rule.type !== 'string' || rule.type.trim() === '') { + console.log("发现无效规则类型:", rule); + return false; + } + + if (!rule.id || typeof rule.id !== 'string') { + console.log("发现缺少ID的规则:", rule); + // 为缺少ID的规则自动生成ID + rule.id = `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + } + + // 确保config存在 + if (!rule.config || typeof rule.config !== 'object') { + console.log("规则缺少配置对象,自动创建:", rule); + rule.config = {}; + } + + return true; + }); + + console.log(`规则验证结果: 输入${rules.length}条,有效${validRules.length}条`); + return validRules; + }; + // 格式化数据,准备提交到接口 const formatDataForApi = (formData: FormDataType, isDraft: boolean = false) => { + // 记录规则处理前的数据 + console.log("格式化前的评查规则数据:", formData.evaluation_config?.rules || []); + // 转换提取配置为符合数据库格式的JSON const extractionConfig = { llm: { @@ -906,11 +969,15 @@ export default function RuleNew() { } }; + // 确保规则数据有效 + const validatedRules = validateAndFixRules(formData.evaluation_config.rules || []); + console.log("验证后的评查规则数量:", validatedRules.length); + // 转换评查配置为符合数据库格式的JSON const evaluationConfig = { logicType: formData.evaluation_config.logicType || 'and', customLogic: formData.evaluation_config.customLogic || '', - rules: (formData.evaluation_config.rules || []) + rules: validatedRules .filter((rule: Rule) => rule.type && rule.type.trim() !== '') .map((rule: Rule) => { let config = {}; @@ -919,14 +986,14 @@ export default function RuleNew() { switch (rule.type) { case 'exists': config = { - fields: rule.config?.fields || [], - logic: rule.config?.logicRelation || 'and' + fields: rule.config?.fields || rule.config?.selectedFields || [], + logic: rule.config?.logic || rule.config?.logicRelation || 'and' }; break; case 'consistency': config = { pairs: rule.config?.pairs || [], - logic: rule.config?.logicRelation || 'and' + logic: rule.config?.logic || rule.config?.logicRelation || 'and' }; break; case 'format': @@ -939,7 +1006,7 @@ export default function RuleNew() { case 'logic': config = { conditions: rule.config?.conditions || [], - logic: rule.config?.logicRelation || 'and' + logic: rule.config?.logic || rule.config?.logicRelation || 'and' }; break; case 'regex': @@ -963,10 +1030,16 @@ export default function RuleNew() { }; break; default: - config = { ...rule.config }; + // 如果有保存的原始数据,优先使用 + if (rule.config?._originalData) { + config = { ...rule.config._originalData }; + } else { + config = { ...rule.config }; + } // 清除辅助字段,避免发送无效数据 if (typeof config === 'object' && config !== null) { delete (config as Record).availableFields; + delete (config as Record)._originalData; } } @@ -977,6 +1050,9 @@ export default function RuleNew() { }; }) }; + + // 记录规则处理后的数据 + console.log("处理后的评查规则数据:", evaluationConfig.rules); // 构建完整的评查点数据 const evaluationPointData: Record = { @@ -1029,6 +1105,9 @@ export default function RuleNew() { try { setIsLoading(true); + // 记录当前评查规则状态 + console.log("保存前评查规则状态:", formData.evaluation_config.rules); + // 基本验证 if (!formData.name || !formData.code) { alert("请填写评查点名称和编码,这些是必填项"); @@ -1050,14 +1129,33 @@ export default function RuleNew() { return; } - // 检查评查规则 - if (!formData.evaluation_config.rules || formData.evaluation_config.rules.length === 0 || - !formData.evaluation_config.rules.some(rule => rule.type && rule.type.trim() !== '')) { + // 检查评查规则 - 只在新增模式下检查规则是否为空 + if (!isEditMode && + (!formData.evaluation_config.rules || + formData.evaluation_config.rules.length === 0 || + !formData.evaluation_config.rules.some(rule => rule.type && rule.type.trim() !== ''))) { + console.log("规则验证失败,当前规则:", formData.evaluation_config.rules); + console.log("规则数量:", formData.evaluation_config.rules?.length || 0); + console.log("规则有效性:", formData.evaluation_config.rules?.some(rule => rule.type && rule.type.trim() !== '')); alert("请至少添加一条有效的评查规则"); setIsLoading(false); return; } + // 编辑模式下,只有当规则数组不为空时才验证规则有效性 + if (isEditMode && + formData.evaluation_config.rules && + formData.evaluation_config.rules.length > 0 && + !formData.evaluation_config.rules.some(rule => rule.type && rule.type.trim() !== '')) { + console.log("编辑模式下规则无效,当前规则:", formData.evaluation_config.rules); + console.log("编辑模式-规则数量:", formData.evaluation_config.rules.length); + console.log("编辑模式-规则详情:", JSON.stringify(formData.evaluation_config.rules, null, 2)); + console.log("编辑模式-规则有效性:", formData.evaluation_config.rules.some(rule => rule.type && rule.type.trim() !== '')); + alert("请至少添加一条有效的评查规则或清空规则列表"); + setIsLoading(false); + return; + } + const evaluationPointData = formatDataForApi(formData); console.log("保存数据:", evaluationPointData); @@ -1142,6 +1240,9 @@ export default function RuleNew() { return; } + // 编辑模式下不检查评查规则是否为空 + // 草稿模式下规则可以为空 + const draftData = formatDataForApi(formData, true); console.log("保存草稿数据:", draftData); @@ -1390,9 +1491,10 @@ export default function RuleNew() {
Date: Tue, 8 Apr 2025 16:52:16 +0800 Subject: [PATCH 13/23] =?UTF-8?q?=E6=9A=82=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../rules/new/ExtractionSettings.tsx | 660 +++++++++----- app/components/rules/new/ReviewSettings.tsx | 37 +- app/routes/rules.new.tsx | 853 +++++++++--------- 3 files changed, 894 insertions(+), 656 deletions(-) diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index 464b8ea..d433f1c 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -2,6 +2,18 @@ import { useState, KeyboardEvent, FormEvent, useContext, useEffect, useCallback, import { RuleContext } from '~/contexts/RuleContext'; import { processFieldName } from '~/utils'; +// 定义通知函数的类型 +type NotifyFn = (data: Record) => void; + +// 添加防抖工具函数,使用简单的函数类型 +const debounce = (fn: NotifyFn, ms = 300): NotifyFn => { + let timeoutId: ReturnType; + return function(data: Record): void { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => fn(data), ms); + }; +}; + /** * ExtractionSettings 组件 * @@ -23,9 +35,13 @@ import { processFieldName } from '~/utils'; */ interface RegexField { - id: string; - fieldName: string; - regex: string; + field: string; + pattern: string; +} + +interface VlmField { + name: string; + type: string; } interface PromptTemplate { @@ -38,21 +54,21 @@ interface PromptTemplate { interface ExtractionSettingsProps { onChange?: (data: Record) => void; initialData?: { - llm_ocr?: { + llm?: { fields?: string[]; prompt_setting?: { type?: string; template?: string; }; }; - llm_vl?: { - fields?: string[]; + vlm?: { + fields?: VlmField[] | string[]; prompt_setting?: { type?: string; template?: string; }; }; - ocr_regex?: { + regex?: { fields?: RegexField[]; }; }; @@ -63,21 +79,23 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const lastUpdateTimeRef = useRef(0); // 添加一个ref来记录上次更新时间 const lastEventFieldsRef = useRef([]); const ignoreEmptyFieldsRef = useRef(false); + // 添加对防抖通知函数的引用,使用具体的函数类型 + const debouncedNotifyParentRef = useRef(null); - const [currentTab, setCurrentTab] = useState('llm_ocr'); - const [fields, setFields] = useState<{ [key: string]: string[] }>({ - llm_ocr: [], + const [currentTab, setCurrentTab] = useState('llm'); + const [fields, setFields] = useState<{ [key: string]: Array }>({ llm: [], + vlm: [], }); const [inputValue, setInputValue] = useState({ - llm_ocr: '', llm: '', + vlm: '', }); const [selectedFieldType, setSelectedFieldType] = useState('default'); - const [regexFields, setRegexFields] = useState([{ id: '1', fieldName: '', regex: '' }]); - const [promptType, setPromptType] = useState({ llm_ocr: 'system', llm: 'system' }); - const [promptContent, setPromptContent] = useState({ llm_ocr: '', llm: '' }); - const [selectedTemplate, setSelectedTemplate] = useState({ llm_ocr: '', llm: '' }); + const [regexFields, setRegexFields] = useState([{ field: '', pattern: '' }]); + const [promptType, setPromptType] = useState({ llm: 'system', vlm: 'system' }); + const [promptContent, setPromptContent] = useState({ llm: '', vlm: '' }); + const [selectedTemplate, setSelectedTemplate] = useState({ llm: '', vlm: '' }); // 添加状态记录每个字段的编辑状态 const [fieldEditStatus, setFieldEditStatus] = useState<{[id: string]: boolean}>({}); @@ -95,27 +113,42 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 加载初始数据 useEffect(() => { if (initialData) { - const newFields = { - llm_ocr: initialData.llm_ocr?.fields || [], - llm: initialData.llm_vl?.fields || [], + const newFields: { [key: string]: Array } = { + llm: initialData.llm?.fields || [], + vlm: [], }; + + // 处理vlm字段 + if (initialData.vlm?.fields) { + // 处理两种可能的格式:字符串数组或对象数组 + if (Array.isArray(initialData.vlm.fields)) { + if (initialData.vlm.fields.length > 0) { + if (typeof initialData.vlm.fields[0] === 'string') { + // 如果是字符串数组,直接使用 + newFields.vlm = initialData.vlm.fields as string[]; + } else { + // 如果是对象数组,转换为字符串数组 (name_type 格式) + newFields.vlm = (initialData.vlm.fields as VlmField[]).map(field => + field.type && field.type !== 'default' + ? `${field.name}_${field.type}` + : field.name + ); + } + } + } + } + setFields(newFields); setPromptType({ - llm_ocr: initialData.llm_ocr?.prompt_setting?.type || 'system', - llm: initialData.llm_vl?.prompt_setting?.type || 'system', + llm: initialData.llm?.prompt_setting?.type || 'system', + vlm: initialData.vlm?.prompt_setting?.type || 'system', }); setPromptContent({ - llm_ocr: initialData.llm_ocr?.prompt_setting?.template || '', - llm: initialData.llm_vl?.prompt_setting?.template || '', + llm: initialData.llm?.prompt_setting?.template || '', + vlm: initialData.vlm?.prompt_setting?.template || '', }); - if (initialData.ocr_regex?.fields?.length) { - setRegexFields( - initialData.ocr_regex.fields.map((field: RegexField, index: number) => ({ - id: String(index + 1), - fieldName: field.fieldName || '', - regex: field.regex || '', - })) - ); + if (initialData.regex?.fields?.length) { + setRegexFields(initialData.regex.fields); } } }, [initialData]); // 只依赖 initialData,避免 ruleContext 导致频繁触发 @@ -142,22 +175,22 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 获取所有字段(使用 useCallback 稳定函数引用) const getAllFields = useCallback(() => { // 1. 收集大模型抽取字段 - const llm_ocr_fields = fields.llm_ocr || []; + const llm_fields = fields.llm || []; // 2. 收集多模态抽取字段(去掉类型后缀) - const llm_fields = (fields.llm || []).map((field) => { + const vlm_fields = (fields.vlm || []).map((field) => { // 从字段名_类型格式中提取字段名部分 return field.split('_')[0]; }); // 3. 收集正则抽取字段(仅保留有效字段) const regex_fields = regexFields - .filter((field) => field.fieldName && field.fieldName.trim() !== '') - .map((field) => field.fieldName.trim()); + .filter((field) => field.field && field.field.trim() !== '') + .map((field) => field.field.trim()); // 4. 合并所有字段并确保唯一性(使用Set去重) // 这样即使用户在不同标签页添加了同名字段,最终也只会保留一个 - return [...new Set([...llm_ocr_fields, ...llm_fields, ...regex_fields])]; + return [...new Set([...llm_fields, ...vlm_fields, ...regex_fields])]; }, [fields, regexFields]); // 检查字段名是否存在 @@ -169,19 +202,19 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const fieldNameLower = fieldNameTrimmed.toLowerCase(); // 获取所有字段(不包括regexFields,这部分单独处理) - const llm_ocr_fields = fields.llm_ocr || []; - const llm_fields = (fields.llm || []).map(processFieldName); + const llm_fields = fields.llm || []; + const vlm_fields = (fields.vlm || []).map(processFieldName); // 检查是否在其他类型字段中存在 - if (llm_ocr_fields.some(f => f.toLowerCase() === fieldNameLower) || - llm_fields.some(f => f.toLowerCase() === fieldNameLower)) { + if (llm_fields.some(f => f.toLowerCase() === fieldNameLower) || + vlm_fields.some(f => f.toLowerCase() === fieldNameLower)) { return true; } // 检查是否在其他正则字段中存在(排除当前正在编辑的字段) const otherRegexFields = regexFields - .filter((f) => !excludeId || f.id !== excludeId) - .map((f) => f.fieldName ? f.fieldName.trim() : ''); + .filter((f) => !excludeId || f.field !== excludeId) + .map((f) => f.field ? f.field.trim() : ''); return otherRegexFields.some(f => f.toLowerCase() === fieldNameLower); }, @@ -193,7 +226,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings try { // 收集所有三种类型的字段,无论当前在哪个标签页 // 验证正则字段,只需要字段名有值即可,不要求正则表达式必须有值 - const validRegexFields = regexFields.filter(field => field.fieldName && field.fieldName.trim() !== ''); + const validRegexFields = regexFields.filter(field => field.field && field.field.trim() !== ''); // 检查字段名称是否重复 const fieldNames = new Map(); @@ -202,9 +235,9 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 收集所有字段名 - 不受当前标签页影响,始终收集所有类型的字段 const allFieldNamesList = [ - ...fields.llm_ocr, - ...fields.llm.map(f => processFieldName(f)), - ...validRegexFields.map(f => f.fieldName.trim()) + ...fields.llm, + ...fields.vlm.map(f => processFieldName(f)), + ...validRegexFields.map(f => f.field.trim()) ].filter(name => name); // 过滤空值 allFieldNamesList.forEach(name => { @@ -228,8 +261,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const allFields = getAllFields(); // 创建摘要信息,显示所有类型的字段数量 - const llmOcrCount = fields.llm_ocr.length; - const llmVlCount = fields.llm.length; + const llmCount = fields.llm.length; + const vlmCount = fields.vlm.length; const regexCount = validRegexFields.length; const totalCount = allFields.length; @@ -242,8 +275,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings if (onChange) { onChange({ fields: { - llm_ocr: fields.llm_ocr, - llm: fields.llm + llm: fields.llm, + vlm: fields.vlm }, regexFields: validRegexFields, allFields, @@ -263,7 +296,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 生成更详细的成功消息,列出每种类型的字段数量 setUpdateStatus({ success: true, - message: `已成功更新${totalCount}个字段(大模型字段: ${llmOcrCount},多模态字段: ${llmVlCount},正则字段: ${regexCount})` + message: `已成功更新${totalCount}个字段(大模型字段: ${llmCount},多模态字段: ${vlmCount},正则字段: ${regexCount})` }); // 3秒后清除更新状态 @@ -282,62 +315,42 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } }, [fields, regexFields, getAllFields, ruleContext, onChange]); - // 处理更新字段按钮点击 - const handleUpdateFields = () => { - // 只有在点击更新按钮时,才执行验证和向父组件提交数据 - if (validateAndUpdateFields()) { - // 当更新成功时,才传递字段数据到父组件 - const validRegexFields = regexFields.filter(field => field.fieldName && field.fieldName.trim() !== ''); - if (onChange) { - onChange({ - fields: { - llm_ocr: fields.llm_ocr, - llm: fields.llm - }, - regexFields: validRegexFields, - allFields: getAllFields(), - pendingUpdate: false, // 标记已完成更新 - - // 同时提交提示词设置 - promptType, - promptContent, - promptSettings: { - llm_ocr: { - type: promptType.llm_ocr, - content: promptContent.llm_ocr, - template: selectedTemplate.llm_ocr - }, - llm: { - type: promptType.llm, - content: promptContent.llm, - template: selectedTemplate.llm - } - } - }); - } + // 初始化防抖函数 + useEffect(() => { + if (onChange) { + debouncedNotifyParentRef.current = debounce((data: Record) => { + onChange(data); + }, 500); // 500ms的防抖延迟 } - }; - - const handleTabChange = (tab: string) => { - setCurrentTab(tab); - // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 - // onChange?.({ extractionMethod: tab }); - }; + return () => { + // 组件卸载时清理 + debouncedNotifyParentRef.current = null; + }; + }, [onChange]); - const handleFieldInputChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { - setInputValue({ ...inputValue, [type]: e.currentTarget.value }); - }; + // 通知父组件的包装函数,使用防抖 + const notifyParent = useCallback((data: Record, immediate = false) => { + if (!onChange) return; + + if (immediate) { + // 对于需要立即响应的操作,直接调用onChange + onChange(data); + } else if (debouncedNotifyParentRef.current) { + // 对于可以延迟处理的操作,使用防抖函数 + debouncedNotifyParentRef.current(data); + } + }, [onChange]); - const handleFieldTypeChange = (e: FormEvent) => { - setSelectedFieldType(e.currentTarget.value); - }; - - const addField = (type: 'llm_ocr' | 'llm') => { + // 修改addField函数,使用防抖通知 + const addField = (type: 'llm' | 'vlm') => { const value = inputValue[type].trim(); if (!value) return; - if (type === 'llm_ocr') { + const newFields = { ...fields }; + + if (type === 'llm') { + // 大模型抽取支持一次性添加多个字段 const fieldsToAdd = value .split(/[\s、,]+/) .map((f) => f.trim()) @@ -346,39 +359,100 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings alert('所有字段名已存在,请确保字段名称唯一'); return; } - setFields((prev) => ({ ...prev, [type]: [...prev[type], ...fieldsToAdd] })); + newFields[type] = [...fields[type], ...fieldsToAdd]; } else { + // 多模态抽取需要添加字段类型后缀 if (isFieldNameExists(value)) { alert(`字段名 "${value}" 已存在,请确保字段名称唯一`); return; } - setFields((prev) => ({ ...prev, [type]: [...prev[type], `${value}_${selectedFieldType}`] })); - setSelectedFieldType('default'); + newFields[type] = [...fields[type], `${value}_${selectedFieldType}`]; } + + setFields(newFields); setInputValue((prev) => ({ ...prev, [type]: '' })); // 标记有未保存的更改 setHasPendingChanges(true); - }; - - const handleKeyDown = (e: KeyboardEvent, type: 'llm_ocr' | 'llm') => { - if (e.key === 'Enter') { - e.preventDefault(); - addField(type); - } - }; - - const removeField = (type: 'llm_ocr' | 'llm', index: number) => { - setFields((prev) => { - const newFields = [...prev[type]]; - newFields.splice(index, 1); - return { ...prev, [type]: newFields }; + + // 添加字段后通知父组件,使用防抖 + notifyParent({ + fields: newFields, + pendingUpdate: true, + allFields: getFieldsWithNewAddition(newFields, type === 'llm' ? value : `${value}_${selectedFieldType}`) }); + }; + + // 新增辅助函数,计算包含新添加字段的完整字段列表 + const getFieldsWithNewAddition = (fieldsObj: {[key: string]: string[]}, newField: string) => { + // 收集大模型抽取字段 + const llm_fields = fieldsObj.llm || []; + + // 收集多模态抽取字段(去掉类型后缀) + const vlm_fields = (fieldsObj.vlm || []).map((field) => { + // 从字段名_类型格式中提取字段名部分 + return field.split('_')[0]; + }); + + // 收集正则抽取字段(仅保留有效字段) + const regex_fields = regexFields + .filter((field) => field.field && field.field.trim() !== '') + .map((field) => field.field.trim()); + + // 添加新字段(处理新字段格式) + const newFieldName = newField.split('_')[0]; + + // 合并所有字段并确保唯一性(使用Set去重) + return [...new Set([...llm_fields, ...vlm_fields, ...regex_fields, newFieldName])]; + }; + + // 修改removeField函数,使用防抖通知 + const removeField = (type: 'llm' | 'vlm', index: number) => { + const newFields = { ...fields }; + const tempFields = [...fields[type]]; + // 保存被删除的字段,以便从allFields中移除 + const removedField = tempFields[index]; + tempFields.splice(index, 1); + newFields[type] = tempFields; + + setFields(newFields); // 标记有未保存的更改 setHasPendingChanges(true); + + // 删除字段后通知父组件,使用防抖 + notifyParent({ + fields: newFields, + pendingUpdate: true, + allFields: getFieldsWithRemoval(newFields, type === 'llm' ? removedField : removedField.split('_')[0]) + }); }; + // 新增辅助函数,计算移除字段后的完整字段列表 + const getFieldsWithRemoval = (fieldsObj: {[key: string]: string[]}, removedField: string) => { + // 收集大模型抽取字段 + const llm_fields = fieldsObj.llm || []; + + // 收集多模态抽取字段(去掉类型后缀) + const vlm_fields = (fieldsObj.vlm || []).map((field) => { + // 从字段名_类型格式中提取字段名部分 + return field.split('_')[0]; + }); + + // 收集正则抽取字段(仅保留有效字段) + const regex_fields = regexFields + .filter((field) => field.field && field.field.trim() !== '') + .map((field) => field.field.trim()); + + // 移除字段处理:如果是多模态字段,提取字段名部分 + const fieldToRemove = removedField.split('_')[0]; + + // 合并所有字段并确保唯一性(使用Set去重) + const allFields = [...new Set([...llm_fields, ...vlm_fields, ...regex_fields])]; + return allFields.filter(field => field !== fieldToRemove); + }; + + // 修改addRegexFieldRow函数,使用防抖通知 const addRegexFieldRow = () => { // 使用时间戳和随机数生成唯一ID const newId = `regex_${Date.now()}_${Math.floor(Math.random() * 100000)}`; @@ -392,7 +466,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 添加空字段但不会立即触发验证和更新 setRegexFields(prev => { - const newFields = [...prev, { id: newId, fieldName: '', regex: '' }]; + const newFields = [...prev, { field: '', pattern: '' }]; // 延迟聚焦到新添加的字段 setTimeout(() => { @@ -401,32 +475,56 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings input.focus(); } - // 5秒后重置标记,恢复正常字段检查 + // 设置一个非常长的超时时间,确保字段不会因为空值而被自动删除 + // 用户需要手动点击更新按钮才会处理这些字段 setTimeout(() => { ignoreEmptyFieldsRef.current = false; - }, 5000); // 设置为5秒,给用户足够的时间填写 + }, 3600000); // 设置为1小时,基本上确保用户有足够的时间完成编辑 }, 50); return newFields; }); + + // 手动触发一次onChange,确保父组件知道我们添加了新字段 + // 但不触发完整的字段验证和更新,此处立即通知,不使用防抖 + notifyParent({ + regexFields: [...regexFields, { field: '', pattern: '' }], + pendingUpdate: true // 标记有待更新的内容 + }, true); }; const removeRegexFieldRow = (id: string) => { if (regexFields.length <= 1) return; - setRegexFields((prev) => prev.filter((field) => field.id !== id)); + + // 先保存更新前的状态,以便通知父组件 + const updatedRegexFields = regexFields.filter((field) => field.field !== id); + + setRegexFields(updatedRegexFields); // 标记有未保存的更改 setHasPendingChanges(true); + + // 删除字段后通知父组件,使用立即通知模式确保立即删除 + notifyParent({ + regexFields: updatedRegexFields, + pendingUpdate: true, + allFields: getAllFields().filter(field => { + // 找到被删除的字段名 + const deletedField = regexFields.find(f => f.field === id); + return deletedField ? field !== deletedField.field.trim() : true; + }) + }, true); // 使用立即通知,确保字段立即删除 }; - const updateRegexField = (id: string, key: 'fieldName' | 'regex', value: string) => { + // 修改updateRegexField函数,使用防抖通知 + const updateRegexField = (id: string, key: 'field' | 'pattern', value: string) => { // 标记此字段为正在编辑状态 setFieldEditStatus(prev => ({ ...prev, [id]: true })); // 记录当前活动字段ID activeFieldRef.current = id; setRegexFields((prev) => - prev.map((field) => (field.id === id ? { ...field, [key]: value } : field)) + prev.map((field) => (field.field === id ? { ...field, [key]: value } : field)) ); // 标记有未保存的更改 @@ -447,18 +545,18 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 设置一个更长的超时时间,给用户充分的编辑时间 fieldEditResetTimeoutRef.current = setTimeout(() => { // 检查字段是否已填写完成 - const currentField = regexFields.find(f => f.id === id); + const currentField = regexFields.find(f => f.field === id); if (currentField) { // 只有当字段名有值时,才考虑将字段标记为完成状态 - if (currentField.fieldName && currentField.fieldName.trim() !== '') { + if (currentField.field && currentField.field.trim() !== '') { // 即使正则为空,也不要自动删除字段,只是更新编辑状态 setFieldEditStatus(prev => ({ ...prev, [id]: false })); if (activeFieldRef.current === id) { activeFieldRef.current = null; } - // 如果正则为空,提示用户填写 - if (!currentField.regex || currentField.regex.trim() === '') { + // 如果正则为空,提示用户填写,但不删除字段 + if (!currentField.pattern || currentField.pattern.trim() === '') { setStatusMessage({ id, message: "正则表达式为空,此字段会保留但不会执行抽取。" @@ -471,34 +569,57 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } } } - }, 180000); // 给用户3分钟的编辑时间,大幅延长以避免过早丢失编辑状态 + }, 3600000); // 设置为1小时,确保用户有足够时间完成编辑 + + // 每次字段更新都触发onChange,确保父组件知道字段状态变化,使用防抖 + notifyParent({ + regexFields: regexFields.map(field => + field.field === id ? { ...field, [key]: value } : field + ), + pendingUpdate: true + }); }; - const handleRegexFieldBlur = (id: string, key: 'fieldName' | 'regex') => { + // 修改handleRegexFieldBlur函数,使用防抖通知 + const handleRegexFieldBlur = (id: string, key: 'field' | 'pattern') => { // 如果用户从正则表达式字段离开并且字段名和正则都已填写,则标记字段编辑完成 - const field = regexFields.find((f) => f.id === id); + const field = regexFields.find((f) => f.field === id); if (!field) return; - if (key === 'fieldName') { + if (key === 'field') { // 如果字段名为空,不进行任何操作,保留字段 - if (!field.fieldName || field.fieldName.trim() === '') { + if (!field.field || field.field.trim() === '') { return; } // 检查重复字段 - if (isFieldNameExists(field.fieldName, id)) { - alert(`字段名 "${field.fieldName.trim()}" 已存在,请确保字段名称唯一`); + if (isFieldNameExists(field.field, id)) { + alert(`字段名 "${field.field.trim()}" 已存在,请确保字段名称唯一`); setRegexFields((prev) => - prev.map((f) => (f.id === id ? { ...f, fieldName: '' } : f)) + prev.map((f) => (f.field === id ? { ...f, field: '' } : f)) ); + + // 通知父组件字段已更新,此处立即通知,不使用防抖 + notifyParent({ + regexFields: regexFields.map(f => + f.field === id ? { ...f, field: '' } : f + ), + pendingUpdate: true + }, true); + } else if (field.field.trim() !== '') { + // 如果字段名不为空且不重复,通知父组件字段已更新,使用防抖 + notifyParent({ + regexFields, + pendingUpdate: true + }); } - } else if (key === 'regex') { + } else if (key === 'pattern') { // 如果字段名和正则都已填写,标记为完成状态 - if (field.fieldName && field.fieldName.trim() !== '') { + if (field.field && field.field.trim() !== '') { // 即使正则为空,也不要自动删除字段 setTimeout(() => { - // 正则有内容,标记为完成状态 - if (field.regex && field.regex.trim() !== '') { + // 只有当正则不为空时,才显示完成提示 + if (field.pattern && field.pattern.trim() !== '') { setFieldEditStatus(prev => ({ ...prev, [id]: false })); if (activeFieldRef.current === id) { activeFieldRef.current = null; @@ -514,7 +635,24 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings setTimeout(() => { setStatusMessage(current => current?.id === id ? null : current); }, 2000); + } else { + // 正则为空时,显示提示但不删除字段 + setStatusMessage({ + id, + message: "未设置正则表达式,此字段会保留但不会执行抽取。" + }); + + // 5秒后自动隐藏提示 + setTimeout(() => { + setStatusMessage(current => current?.id === id ? null : current); + }, 5000); } + + // 不管正则是否为空,都通知父组件字段已更新,使用防抖 + notifyParent({ + regexFields, + pendingUpdate: true + }); }, 200); } } @@ -523,7 +661,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const applyRegexTemplate = (regex: string) => { const lastField = regexFields[regexFields.length - 1]; if (lastField) { - updateRegexField(lastField.id, 'regex', regex); + updateRegexField(lastField.field, 'pattern', regex); } }; @@ -554,14 +692,14 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings return { fieldName, fieldType, typeName, badgeClass }; }; - const handlePromptTypeChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handlePromptTypeChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setPromptType((prev) => ({ ...prev, [type]: value })); // 标记有未保存的更改,但不触发onChange setHasPendingChanges(true); }; - const handleTemplateChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handleTemplateChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setSelectedTemplate((prev) => ({ ...prev, [type]: value })); @@ -571,7 +709,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings let content = templateData.template_content; if (content.includes('{fieldsList}') && fields[type].length > 0) { const fieldListStr = - type === 'llm_ocr' + type === 'llm' ? fields[type].map((field, idx) => `${idx + 1}. ${field}`).join('\n') : fields[type] .map((field, idx) => { @@ -592,16 +730,16 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } }; - const handlePromptContentChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { + const handlePromptContentChange = (e: FormEvent, type: 'llm' | 'vlm') => { const value = e.currentTarget.value; setPromptContent((prev) => ({ ...prev, [type]: value })); // 标记有未保存的更改,但不触发onChange setHasPendingChanges(true); }; - const applyVariableToPrompt = (variable: string, type: 'llm_ocr' | 'llm') => { + const applyVariableToPrompt = (variable: string, type: 'llm' | 'vlm') => { const textarea = document.getElementById( - type === 'llm_ocr' ? 'llm-prompt-content' : 'multimodal-prompt-content' + type === 'llm' ? 'llm-prompt-content' : 'multimodal-prompt-content' ) as HTMLTextAreaElement; if (textarea) { const start = textarea.selectionStart; @@ -666,6 +804,86 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings return templates[id] || null; }; + // 修复缺失的handleKeyDown函数 + const handleKeyDown = (e: KeyboardEvent, type: 'llm' | 'vlm') => { + if (e.key === 'Enter') { + e.preventDefault(); + addField(type); + } + }; + + // 修改handleUpdateFields函数,使用立即通知模式 + const handleUpdateFields = () => { + // 只有在点击更新按钮时,才执行验证和向父组件提交数据 + if (validateAndUpdateFields()) { + // 当更新成功时,才传递字段数据到父组件 + // 保留所有有字段名的正则字段,包括那些正则表达式为空的字段 + const validRegexFields = regexFields.filter(field => field.field && field.field.trim() !== ''); + + if (onChange) { + // 更新按钮点击时使用立即通知,不使用防抖 + notifyParent({ + fields: { + llm: fields.llm, + vlm: fields.vlm + }, + regexFields: validRegexFields, + allFields: getAllFields(), + pendingUpdate: false, // 标记已完成更新 + + // 同时提交提示词设置 + promptType, + promptContent, + promptSettings: { + llm: { + type: promptType.llm, + content: promptContent.llm, + template: selectedTemplate.llm + }, + vlm: { + type: promptType.vlm, + content: promptContent.vlm, + template: selectedTemplate.vlm + } + } + }, true); + + // 更新完成后,取消所有编辑状态 + setFieldEditStatus({}); + // 清除活动字段引用 + activeFieldRef.current = null; + // 重置待更新状态 + setHasPendingChanges(false); + + // 显示成功提示,包含字段数量统计 + setUpdateStatus({ + success: true, + message: `更新成功!共更新字段 ${getAllFields().length} 个 (大模型: ${fields.llm.length}, 多模态: ${fields.vlm.length}, 正则: ${validRegexFields.length})` + }); + + // 5秒后自动隐藏成功提示 + setTimeout(() => { + setUpdateStatus(null); + }, 5000); + } + } + }; + + const handleTabChange = (tab: string) => { + setCurrentTab(tab); + + // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 + // onChange?.({ extractionMethod: tab }); + }; + + const handleFieldInputChange = (e: FormEvent, type: 'llm' | 'vlm') => { + setInputValue({ ...inputValue, [type]: e.currentTarget.value }); + }; + + const handleFieldTypeChange = (e: FormEvent) => { + setSelectedFieldType(e.currentTarget.value); + }; + return (
@@ -675,22 +893,22 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
-
+
-