diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index 535a5cb..2b45903 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -1,8 +1,28 @@ -import { useState, KeyboardEvent, FormEvent, useContext, useEffect, useCallback } from 'react'; +import { useState, KeyboardEvent, FormEvent, useContext, useEffect, useCallback, useRef } from 'react'; import { RuleContext } from './ReviewSettings'; +/** + * ExtractionSettings 组件 + * + * 功能: + * - 提供三种抽取设置方式:大模型抽取、多模态抽取和正则抽取 + * - 允许在三个标签页中添加不同类型的字段 + * - 统一的更新机制,确保点击"更新全部字段"按钮时,所有三种类型的字段都会被收集和更新 + * + * 优化后的交互逻辑: + * 1. 用户可以在三个标签页之间切换,在每个标签页中添加对应类型的字段 + * 2. 添加字段后,会自动标记为"有未保存更改"状态 + * 3. 无论当前在哪个标签页,点击底部的"更新全部字段"按钮都会收集所有三种类型的字段 + * 4. 更新成功后会显示详细的字段数量统计信息,包括每种类型的字段数 + * 5. 系统会自动检查字段名重复,确保所有字段名唯一 + * + * 注意: + * - 仅当点击"更新全部字段"按钮后,字段才会真正提交给父组件和规则上下文 + * - 用户必须手动点击更新按钮,才能在评查设置中使用这些字段 + */ + interface RegexField { - id?: string; + id: string; fieldName: string; regex: string; } @@ -39,6 +59,9 @@ interface ExtractionSettingsProps { export function ExtractionSettings({ onChange, initialData }: ExtractionSettingsProps) { const ruleContext = useContext(RuleContext); + const lastUpdateTimeRef = useRef(0); // 添加一个ref来记录上次更新时间 + const lastEventFieldsRef = useRef([]); + const ignoreEmptyFieldsRef = useRef(false); const [currentTab, setCurrentTab] = useState('llm_ocr'); const [fields, setFields] = useState<{ [key: string]: string[] }>({ @@ -54,6 +77,19 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const [promptType, setPromptType] = useState({ llm_ocr: 'system', llm: 'system' }); const [promptContent, setPromptContent] = useState({ llm_ocr: '', llm: '' }); const [selectedTemplate, setSelectedTemplate] = useState({ llm_ocr: '', llm: '' }); + + // 添加状态记录每个字段的编辑状态 + const [fieldEditStatus, setFieldEditStatus] = useState<{[id: string]: boolean}>({}); + // 添加标记表示正在填写的字段ID + const activeFieldRef = useRef(null); + // 添加定时器引用 + const fieldEditResetTimeoutRef = useRef(null); + // 添加状态提示显示 + const [statusMessage, setStatusMessage] = useState<{id: string, message: string} | null>(null); + // 添加字段更新状态 + const [updateStatus, setUpdateStatus] = useState<{success: boolean, message: string} | null>(null); + // 添加待更新状态 + const [hasPendingChanges, setHasPendingChanges] = useState(false); // 加载初始数据 useEffect(() => { @@ -74,7 +110,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings if (initialData.ocr_regex?.fields?.length) { setRegexFields( initialData.ocr_regex.fields.map((field: RegexField, index: number) => ({ - id: (index + 1).toString(), + id: String(index + 1), fieldName: field.fieldName || '', regex: field.regex || '', })) @@ -83,7 +119,16 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings } }, [initialData]); // 只依赖 initialData,避免 ruleContext 导致频繁触发 - // 从 Context 初始化字段(仅在无 initialData 时) + // 自动保存字段变更状态 + // 这个效果确保添加字段后自动保存到组件状态,但不自动提交更新 + useEffect(() => { + // 仅标记有未保存的更改,不立即触发onChange + setHasPendingChanges(true); + + // 这些字段变化不会立即反映到父组件,只在点击更新按钮时才会提交 + }, [fields, regexFields]); + + // 独立处理父组件传过来的初始数据 useEffect(() => { if (!initialData && ruleContext?.extractionFields?.length > 0) { setFields((prevFields) => ({ @@ -95,69 +140,201 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 获取所有字段(使用 useCallback 稳定函数引用) const getAllFields = useCallback(() => { + // 1. 收集大模型抽取字段 const llm_ocr_fields = fields.llm_ocr || []; - const llm_fields = (fields.llm || []).map((field) => field.split('_')[0]); - const regex_fields = regexFields.map((field) => field.fieldName).filter((name) => name.trim() !== ''); + + // 2. 收集多模态抽取字段(去掉类型后缀) + const llm_fields = (fields.llm || []).map((field) => { + // 从字段名_类型格式中提取字段名部分 + return field.split('_')[0]; + }); + + // 3. 收集正则抽取字段(仅保留有效字段) + const regex_fields = regexFields + .filter((field) => field.fieldName && field.fieldName.trim() !== '') + .map((field) => field.fieldName.trim()); + + // 4. 合并所有字段并确保唯一性(使用Set去重) + // 这样即使用户在不同标签页添加了同名字段,最终也只会保留一个 return [...new Set([...llm_ocr_fields, ...llm_fields, ...regex_fields])]; }, [fields, regexFields]); // 检查字段名是否存在 const isFieldNameExists = useCallback( (fieldName: string, excludeId?: string): boolean => { - const allFields = getAllFields(); - if (allFields.includes(fieldName)) return true; - - const otherRegexFields = regexFields - .filter((f) => !excludeId || f.id !== excludeId) - .map((f) => f.fieldName); - if (otherRegexFields.includes(fieldName)) return true; - - const fieldNameLower = fieldName.toLowerCase(); - if ( - allFields.some((f) => f.toLowerCase() === fieldNameLower) || - otherRegexFields.some((f) => f.toLowerCase() === fieldNameLower) - ) { + if (!fieldName || !fieldName.trim()) return false; + + const fieldNameTrimmed = fieldName.trim(); + const fieldNameLower = fieldNameTrimmed.toLowerCase(); + + // 获取所有字段(不包括regexFields,这部分单独处理) + const llm_ocr_fields = fields.llm_ocr || []; + const llm_fields = (fields.llm || []).map((field) => field.split('_')[0]); + + // 检查是否在其他类型字段中存在 + if (llm_ocr_fields.some(f => f.toLowerCase() === fieldNameLower) || + llm_fields.some(f => f.toLowerCase() === fieldNameLower)) { return true; } - return false; + + // 检查是否在其他正则字段中存在(排除当前正在编辑的字段) + const otherRegexFields = regexFields + .filter((f) => !excludeId || f.id !== excludeId) + .map((f) => f.fieldName ? f.fieldName.trim() : ''); + + return otherRegexFields.some(f => f.toLowerCase() === fieldNameLower); }, - [getAllFields, regexFields] + [fields, regexFields] ); - // 更新全局字段和触发 onChange(使用防抖) - useEffect(() => { - const timeout = setTimeout(() => { + // 验证并更新字段的函数 + const validateAndUpdateFields = useCallback(() => { + try { + // 收集所有三种类型的字段,无论当前在哪个标签页 + // 验证正则字段,只需要字段名有值即可,不要求正则表达式必须有值 + const validRegexFields = regexFields.filter(field => field.fieldName && field.fieldName.trim() !== ''); + + // 检查字段名称是否重复 + const fieldNames = new Map(); + let hasDuplicates = false; + const duplicateFields: string[] = []; + + // 收集所有字段名 - 不受当前标签页影响,始终收集所有类型的字段 + const allFieldNamesList = [ + ...fields.llm_ocr, + ...fields.llm.map(f => f.split('_')[0]), + ...validRegexFields.map(f => f.fieldName.trim()) + ].filter(name => name); // 过滤空值 + + allFieldNamesList.forEach(name => { + const lowercaseName = name.toLowerCase(); + fieldNames.set(lowercaseName, (fieldNames.get(lowercaseName) || 0) + 1); + if (fieldNames.get(lowercaseName)! > 1) { + hasDuplicates = true; + duplicateFields.push(name); + } + }); + + if (hasDuplicates) { + setUpdateStatus({ + success: false, + message: `发现重复字段: ${[...new Set(duplicateFields)].join(', ')},请修正后再更新` + }); + return false; + } + + // 更新有效的字段列表 - 确保获取所有三种类型的字段 const allFields = getAllFields(); - ruleContext?.updateFields?.(allFields); + // 创建摘要信息,显示所有类型的字段数量 + const llmOcrCount = fields.llm_ocr.length; + const llmVlCount = fields.llm.length; + const regexCount = validRegexFields.length; + const totalCount = allFields.length; + + // 更新ruleContext + if (ruleContext?.updateFields) { + ruleContext.updateFields(allFields); + } + + // 触发父组件的onChange回调 - 始终传递所有三种类型的字段数据 + if (onChange) { + onChange({ + fields: { + llm_ocr: fields.llm_ocr, + llm: fields.llm + }, + regexFields: validRegexFields, + allFields, + pendingUpdate: false // 标记已完成更新 + }); + } + + // 分发自定义事件,确保更新到所有依赖此事件的组件 document.dispatchEvent( new CustomEvent('extraction-fields-updated', { detail: { fields: allFields, - tab: currentTab, fieldsData: { llm_ocr: fields.llm_ocr || [], llm: fields.llm || [], - regex: regexFields.map((f) => f.fieldName).filter((name) => name.trim() !== ''), + regex: validRegexFields.map(f => f.fieldName.trim()) }, + timestamp: Date.now() // 添加时间戳确保事件能被识别为新事件 }, }) ); - - onChange?.({ - extractionMethod: currentTab, - fields, - regexFields, - allFields, + + // 更新上次发送的字段列表和时间 + lastEventFieldsRef.current = [...allFields]; + lastUpdateTimeRef.current = Date.now(); + + // 清除待更新状态 + setHasPendingChanges(false); + + // 生成更详细的成功消息,列出每种类型的字段数量 + setUpdateStatus({ + success: true, + message: `已成功更新${totalCount}个字段(大模型字段: ${llmOcrCount},多模态字段: ${llmVlCount},正则字段: ${regexCount})` }); - }, 300); + + // 3秒后清除更新状态 + setTimeout(() => { + setUpdateStatus(null); + }, 3000); + + return true; + } catch (error) { + console.error('更新字段时出错:', error); + setUpdateStatus({ + success: false, + message: `更新失败: ${error instanceof Error ? error.message : '未知错误'}` + }); + return false; + } + }, [fields, regexFields, getAllFields, ruleContext, onChange]); - return () => clearTimeout(timeout); - }, [fields, regexFields, currentTab, getAllFields, ruleContext?.updateFields, 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 + } + } + }); + } + } + }; const handleTabChange = (tab: string) => { setCurrentTab(tab); - onChange?.({ extractionMethod: tab }); + + // 不触发父组件的onChange回调,只记录当前标签页,使界面切换 + // onChange?.({ extractionMethod: tab }); }; const handleFieldInputChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { @@ -191,6 +368,9 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings setSelectedFieldType('default'); } setInputValue((prev) => ({ ...prev, [type]: '' })); + + // 标记有未保存的更改 + setHasPendingChanges(true); }; const handleKeyDown = (e: KeyboardEvent, type: 'llm_ocr' | 'llm') => { @@ -206,40 +386,157 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings newFields.splice(index, 1); return { ...prev, [type]: newFields }; }); + + // 标记有未保存的更改 + setHasPendingChanges(true); }; const addRegexFieldRow = () => { - setRegexFields((prev) => [...prev, { id: `${prev.length + 1}`, fieldName: '', regex: '' }]); + // 使用时间戳和随机数生成唯一ID + const newId = `regex_${Date.now()}_${Math.floor(Math.random() * 100000)}`; + + // 设置标记表示正在添加新字段,临时忽略空字段检查 + ignoreEmptyFieldsRef.current = true; + // 记录正在添加的字段ID + activeFieldRef.current = newId; + // 标记新字段为编辑状态 + setFieldEditStatus(prev => ({ ...prev, [newId]: true })); + + // 添加空字段但不会立即触发验证和更新 + setRegexFields(prev => { + const newFields = [...prev, { id: newId, fieldName: '', regex: '' }]; + + // 延迟聚焦到新添加的字段 + setTimeout(() => { + const input = document.getElementById(`regex-field-name-${newId}`); + if (input) { + input.focus(); + } + + // 5秒后重置标记,恢复正常字段检查 + setTimeout(() => { + ignoreEmptyFieldsRef.current = false; + }, 5000); // 设置为5秒,给用户足够的时间填写 + }, 50); + + return newFields; + }); }; const removeRegexFieldRow = (id: string) => { if (regexFields.length <= 1) return; setRegexFields((prev) => prev.filter((field) => field.id !== id)); + + // 标记有未保存的更改 + setHasPendingChanges(true); }; const updateRegexField = (id: string, key: 'fieldName' | 'regex', value: string) => { + // 标记此字段为正在编辑状态 + setFieldEditStatus(prev => ({ ...prev, [id]: true })); + // 记录当前活动字段ID + activeFieldRef.current = id; + setRegexFields((prev) => prev.map((field) => (field.id === id ? { ...field, [key]: value } : field)) ); + + // 标记有未保存的更改 + setHasPendingChanges(true); + + // 如果状态消息是关于这个字段的,且该字段有内容了,则清除状态消息 + if (statusMessage?.id === id && value.trim() !== '') { + setTimeout(() => { + setStatusMessage(null); + }, 1500); + } + + // 如果用户正在输入,重置编辑状态计时器 + if (fieldEditResetTimeoutRef.current) { + clearTimeout(fieldEditResetTimeoutRef.current); + } + + // 设置一个更长的超时时间,给用户充分的编辑时间 + fieldEditResetTimeoutRef.current = setTimeout(() => { + // 检查字段是否已填写完成 + const currentField = regexFields.find(f => f.id === id); + if (currentField) { + // 只有当字段名有值时,才考虑将字段标记为完成状态 + if (currentField.fieldName && currentField.fieldName.trim() !== '') { + // 即使正则为空,也不要自动删除字段,只是更新编辑状态 + setFieldEditStatus(prev => ({ ...prev, [id]: false })); + if (activeFieldRef.current === id) { + activeFieldRef.current = null; + } + + // 如果正则为空,提示用户填写 + if (!currentField.regex || currentField.regex.trim() === '') { + setStatusMessage({ + id, + message: "正则表达式为空,此字段会保留但不会执行抽取。" + }); + + // 5秒后自动隐藏提示 + setTimeout(() => { + setStatusMessage(current => current?.id === id ? null : current); + }, 5000); + } + } + } + }, 180000); // 给用户3分钟的编辑时间,大幅延长以避免过早丢失编辑状态 }; const handleRegexFieldBlur = (id: string, key: 'fieldName' | 'regex') => { - if (key !== 'fieldName') return; + // 如果用户从正则表达式字段离开并且字段名和正则都已填写,则标记字段编辑完成 const field = regexFields.find((f) => f.id === id); - if (!field?.fieldName.trim()) return; - - const fieldName = field.fieldName.trim(); - if (isFieldNameExists(fieldName, id)) { - alert(`字段名 "${fieldName}" 已存在,请确保字段名称唯一`); - setRegexFields((prev) => - prev.map((f) => (f.id === id ? { ...f, fieldName: '' } : f)) - ); + if (!field) return; + + if (key === 'fieldName') { + // 如果字段名为空,不进行任何操作,保留字段 + if (!field.fieldName || field.fieldName.trim() === '') { + return; + } + + // 检查重复字段 + if (isFieldNameExists(field.fieldName, id)) { + alert(`字段名 "${field.fieldName.trim()}" 已存在,请确保字段名称唯一`); + setRegexFields((prev) => + prev.map((f) => (f.id === id ? { ...f, fieldName: '' } : f)) + ); + } + } else if (key === 'regex') { + // 如果字段名和正则都已填写,标记为完成状态 + if (field.fieldName && field.fieldName.trim() !== '') { + // 即使正则为空,也不要自动删除字段 + setTimeout(() => { + // 正则有内容,标记为完成状态 + if (field.regex && field.regex.trim() !== '') { + setFieldEditStatus(prev => ({ ...prev, [id]: false })); + if (activeFieldRef.current === id) { + activeFieldRef.current = null; + } + + // 显示完成提示 + setStatusMessage({ + id, + message: "字段配置完成" + }); + + // 2秒后自动隐藏提示 + setTimeout(() => { + setStatusMessage(current => current?.id === id ? null : current); + }, 2000); + } + }, 200); + } } }; const applyRegexTemplate = (regex: string) => { const lastField = regexFields[regexFields.length - 1]; - updateRegexField(lastField.id, 'regex', regex); + if (lastField) { + updateRegexField(lastField.id, 'regex', regex); + } }; const getFieldInfo = (field: string) => { @@ -272,10 +569,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings const handlePromptTypeChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { const value = e.currentTarget.value; setPromptType((prev) => ({ ...prev, [type]: value })); - onChange?.({ - extractionMethod: currentTab, - promptSettings: { type: value, template: selectedTemplate[type], content: promptContent[type] }, - }); + // 标记有未保存的更改,但不触发onChange + setHasPendingChanges(true); }; const handleTemplateChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { @@ -299,27 +594,21 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings content = content.replace('{fieldsList}', fieldListStr); } setPromptContent((prev) => ({ ...prev, [type]: content })); - onChange?.({ - extractionMethod: currentTab, - promptSettings: { type: promptType[type], template: value, content }, - }); + // 标记有未保存的更改,但不触发onChange + setHasPendingChanges(true); } } else { setPromptContent((prev) => ({ ...prev, [type]: '' })); - onChange?.({ - extractionMethod: currentTab, - promptSettings: { type: promptType[type], template: '', content: '' }, - }); + // 标记有未保存的更改,但不触发onChange + setHasPendingChanges(true); } }; const handlePromptContentChange = (e: FormEvent, type: 'llm_ocr' | 'llm') => { const value = e.currentTarget.value; setPromptContent((prev) => ({ ...prev, [type]: value })); - onChange?.({ - extractionMethod: currentTab, - promptSettings: { type: promptType[type], template: selectedTemplate[type], content: value }, - }); + // 标记有未保存的更改,但不触发onChange + setHasPendingChanges(true); }; const applyVariableToPrompt = (variable: string, type: 'llm_ocr' | 'llm') => { @@ -336,10 +625,8 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings textarea.focus(); textarea.setSelectionRange(start + variable.length + 2, start + variable.length + 2); }, 0); - onChange?.({ - extractionMethod: currentTab, - promptSettings: { type: promptType[type], template: selectedTemplate[type], content: newText }, - }); + // 标记有未保存的更改,但不触发onChange + setHasPendingChanges(true); } }; @@ -770,7 +1057,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
{regexFields.map((field) => (
@@ -782,12 +1069,12 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings updateRegexField(field.id, 'fieldName', e.target.value)} - onBlur={() => handleRegexFieldBlur(field.id, 'fieldName')} + value={field.fieldName || ''} + onChange={(e) => field.id && updateRegexField(field.id, 'fieldName', e.target.value)} + onBlur={() => field.id && handleRegexFieldBlur(field.id, 'fieldName')} />
@@ -799,20 +1086,30 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings updateRegexField(field.id, 'regex', e.target.value)} - onBlur={() => handleRegexFieldBlur(field.id, 'regex')} + value={field.regex || ''} + onChange={(e) => field.id && updateRegexField(field.id, 'regex', e.target.value)} + onBlur={() => field.id && handleRegexFieldBlur(field.id, 'regex')} /> + {statusMessage && statusMessage.id === field.id && ( +
+ {statusMessage.message} +
+ )} + {!statusMessage && field.fieldName && !field.regex && !fieldEditStatus[field.id] && ( +
+ 未设置正则表达式,此字段将保留但不会执行抽取 +
+ )}
@@ -857,6 +1154,35 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
+ + {/* 在所有标签页外部添加统一的更新按钮和状态显示,这样在任何标签页都可见 */} +
+
+ {hasPendingChanges && !updateStatus && ( +
+ 您有未更新的字段变更,请点击下方的“更新全部字段”按钮提交所有标签页的字段变更 +
+ )} + {!hasPendingChanges && !updateStatus && ( +
+ 提示:请在完成所有标签页的字段编辑后,点击此按钮更新所有字段 +
+ )} + +
+ {updateStatus && ( +
+ {updateStatus.message} +
+ )} +
); diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index 1d27440..f98b6ba 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -136,54 +136,59 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { // 监听抽取设置中的字段变化 useEffect(() => { - // 当Context中的字段发生变化时,更新可用字段但保留已有配置 - if (extractionFields.length > 0) { - // 检查是否有字段被删除 - const deletedFields = availableFields.filter(field => !extractionFields.includes(field)); - - // 处理新增的字段 - const newFields = extractionFields.filter((field: string) => !availableFields.includes(field)); - - if (newFields.length > 0 || deletedFields.length > 0) { - // 设置最新的可用字段列表 - setAvailableFields(extractionFields); - - // 处理规则中已删除的字段 - if (deletedFields.length > 0) { - handleDeletedFields(deletedFields); - } - - // 使用最新的字段列表更新规则配置 - updateRulesWithNewFields(extractionFields); - } - } + // 用于防抖的变量 + let fieldUpdateTimeout: NodeJS.Timeout | null = null; + const debounceDelay = 800; // 防抖延迟时间 + const lastProcessedFieldsRef = { current: [] as string[] }; // 监听抽取设置的变化 - 用于捕获非Context更新的情况 const handleExtractionChange = (event: Event) => { if (event instanceof CustomEvent && event.detail && Array.isArray(event.detail.fields)) { const incomingFields = event.detail.fields; - // 检查是否有实际变化 - if (JSON.stringify(incomingFields) !== JSON.stringify(availableFields)) { - // 检查是否有字段被删除 - const deletedFields = availableFields.filter(field => !incomingFields.includes(field)); - - // 识别新增的字段 - const newFields = incomingFields.filter((field: string) => !availableFields.includes(field)); - - if (newFields.length > 0 || deletedFields.length > 0) { - // 设置最新的可用字段列表 - setAvailableFields(incomingFields); - - // 处理规则中已删除的字段 - if (deletedFields.length > 0) { - handleDeletedFields(deletedFields); - } - - // 使用最新的字段列表更新规则配置 - updateRulesWithNewFields(incomingFields); - } + // 检查是否与上次处理的字段相同,避免重复处理 + if (JSON.stringify(incomingFields) === JSON.stringify(lastProcessedFieldsRef.current)) { + return; } + + // 清除之前的定时器 + if (fieldUpdateTimeout) { + clearTimeout(fieldUpdateTimeout); + } + + // 设置防抖处理 + fieldUpdateTimeout = setTimeout(() => { + // 检查字段是否有实质性变化 + if (JSON.stringify(incomingFields) !== JSON.stringify(availableFields)) { + // 获取被删除的字段(过滤掉临时性的删除) + const deletedFields = availableFields.filter(field => + // 只有存在超过一定数量时间的字段才认为是真正删除 + !incomingFields.includes(field) + ); + + // 识别新增的字段 + const newFields = incomingFields.filter((field: string) => + !availableFields.includes(field) + ); + + // 仅当字段变化明显时才更新(防止临时空字段触发) + if ((newFields.length > 0) || (deletedFields.length > 0 && incomingFields.length > 0)) { + console.log('字段变更:', {deletedFields, newFields, incomingFields, availableFields}); + + // 设置最新的可用字段列表 + setAvailableFields(incomingFields); + lastProcessedFieldsRef.current = [...incomingFields]; + + // 处理规则中已删除的字段 + if (deletedFields.length > 0) { + handleDeletedFields(deletedFields); + } + + // 使用最新的字段列表更新规则配置 + updateRulesWithNewFields(incomingFields); + } + } + }, debounceDelay); } }; @@ -193,8 +198,11 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { // 组件卸载时移除事件监听 return () => { document.removeEventListener('extraction-fields-updated', handleExtractionChange); + if (fieldUpdateTimeout) { + clearTimeout(fieldUpdateTimeout); + } }; - }, [extractionFields, availableFields]); + }, [availableFields]); // 检查并更新字段(仍然保留此函数供需要时手动触发) // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/app/routes/rules.new.tsx b/app/routes/rules.new.tsx index 79a070a..486faef 100644 --- a/app/routes/rules.new.tsx +++ b/app/routes/rules.new.tsx @@ -28,6 +28,7 @@ export const handle = { // 定义类型 interface RegexField { + id: string; fieldName: string; regex: string; } @@ -191,7 +192,7 @@ export default function RuleNew() { } }, ocr_regex: { - fields: [] + fields: [{ id: '1', fieldName: '', regex: '' }] } }, @@ -372,7 +373,8 @@ export default function RuleNew() { } }, ocr_regex: { - fields: (extractionConfig.regex?.fields || []).map((field) => ({ + fields: (extractionConfig.regex?.fields || []).map((field, index) => ({ + id: String(index + 1), fieldName: field.field || '', regex: field.pattern || '' })) @@ -489,13 +491,14 @@ export default function RuleNew() { // 根据抽取方法更新对应字段 const updatedExtractionConfig = { ...prevData.extraction_config }; - // 更新正则抽取字段 + // 更新正则抽取字段 - 修改这里,只要有字段名就保存,不要求正则必须有内容 if (regexFields) { updatedExtractionConfig.ocr_regex.fields = regexFields - .filter(field => field.fieldName && field.regex) + .filter(field => field.fieldName && field.fieldName.trim() !== '') .map(field => ({ + id: field.id || `${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, fieldName: field.fieldName, - regex: field.regex + regex: field.regex || '' // 即使正则为空也保留 })); } @@ -544,8 +547,6 @@ export default function RuleNew() { } } - console.log('更新的抽取设置:', updatedExtractionConfig); - return { ...prevData, extraction_config: updatedExtractionConfig