diff --git a/.gitignore b/.gitignore index 80ec311..d836877 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,6 @@ node_modules /.cache /build .env + +.idea + diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index 26c3d64..aee74af 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -50,23 +50,6 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { llm: '' }); - // 获取所有可用字段(合并大模型、多模态和正则抽取的字段) - const getAllFields = (): string[] => { - const llm_ocr_fields = fields.llm_ocr || []; - // 从多模态字段中提取基本字段名(去除类型后缀) - const llm_fields = (fields.llm || []).map(field => { - const [fieldName] = field.split('_'); - return fieldName; - }); - // 获取正则字段名 - const regex_fields = regexFields - .map(field => field.fieldName) - .filter(name => name.trim() !== ''); - - // 合并并去重 - return [...new Set([...llm_ocr_fields, ...llm_fields, ...regex_fields])]; - }; - // 在组件初始化时,如果Context中已有字段数据,则使用Context数据初始化 useEffect(() => { if (ruleContext && ruleContext.extractionFields.length > 0) { @@ -78,6 +61,79 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { } }, []); + // 当组件首次加载时更新字段 + useEffect(() => { + updateAllFields(); + }, []); + + // 获取所有可用字段(合并大模型、多模态和正则抽取的字段) + const getAllFields = (): string[] => { + // 从大模型OCR抽取中获取字段 + const llm_ocr_fields = fields.llm_ocr || []; + + // 从多模态字段中提取基本字段名(去除类型后缀) + const llm_fields = (fields.llm || []).map(field => { + const [fieldName] = field.split('_'); + return fieldName; + }); + + // 获取正则字段名 + const regex_fields = regexFields + .map(field => field.fieldName) + .filter(name => name.trim() !== ''); + + // 合并并去重 + const allFields = [...new Set([...llm_ocr_fields, ...llm_fields, ...regex_fields])]; + console.log("所有可用字段:", allFields); + return allFields; + }; + + // 检查字段名是否存在(精确匹配) + const isFieldNameExists = (fieldName: string, excludeId?: string): boolean => { + // 获取所有字段名称(不转换为小写) + const existingFields = getAllFields(); + + // 检查精确匹配(区分大小写) + for (const existingField of existingFields) { + // 严格相等比较,确保完全匹配而不是部分匹配 + if (existingField === fieldName) { + console.log(`字段名 '${fieldName}' 在现有字段中存在(严格匹配)`); + return true; + } + } + + // 检查正则字段组中的其他字段(精确匹配) + // 排除当前正在编辑的字段ID + const otherRegexFields = regexFields + .filter(f => !excludeId || f.id !== excludeId) + .map(f => f.fieldName); + + for (const regexField of otherRegexFields) { + // 严格相等比较 + if (regexField === fieldName) { + console.log(`字段名 '${fieldName}' 在正则字段中存在(严格匹配)`); + return true; + } + } + + // 不区分大小写的检查(保留这部分功能,但仍然是精确匹配) + const fieldNameLower = fieldName.toLowerCase(); + const existingFieldsLower = existingFields.map(f => f.toLowerCase()); + const otherRegexFieldsLower = otherRegexFields.map(f => f.toLowerCase()); + + if (existingFieldsLower.includes(fieldNameLower)) { + console.log(`字段名 '${fieldName}' 在现有字段中存在(不区分大小写)`); + return true; + } + + if (otherRegexFieldsLower.includes(fieldNameLower)) { + console.log(`字段名 '${fieldName}' 在正则字段中存在(不区分大小写)`); + return true; + } + + return false; + }; + // 更新所有抽取字段到Context const updateAllFields = () => { const allFields = getAllFields(); @@ -111,10 +167,13 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { } }; - // 在所有字段集合变化时自动更新 + // 使用useEffect监听字段变化并更新Context useEffect(() => { + // 立即更新字段列表 updateAllFields(); - }, [fields, regexFields]); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [fields.llm_ocr, fields.llm, regexFields]); const handleTabChange = (tab: string) => { setCurrentTab(tab); @@ -141,19 +200,40 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { // OCR+LLM模式下,支持多个字段同时添加(用逗号、顿号或空格分隔) if (type === 'llm_ocr') { - newFields = [ - ...fields[type], - ...inputValue[type].split(/[\s、,]+/).map(f => f.trim()).filter(f => f !== '') - ]; + const fieldsToAdd = inputValue[type].split(/[\s、,]+/) + .map(f => f.trim()) + .filter(f => f !== ''); + + console.log(`添加OCR字段:`, fieldsToAdd); + + // 仅添加不存在的字段 + const uniqueFields = fieldsToAdd.filter(field => !isFieldNameExists(field)); + + if (uniqueFields.length === 0) { + // 如果没有唯一字段可添加,显示提示并返回 + alert("所有字段名已存在,请确保字段名称唯一"); + return; + } + + newFields = [...fields[type], ...uniqueFields]; } else { - // 多模态抽取模式下,一次只添加一个字段(带类型) - newFields = [...fields[type], `${inputValue[type].trim()}_${selectedFieldType}`]; + // 多模态抽取模式下,处理字段名称唯一性 + const fieldName = inputValue[type].trim(); + console.log(`添加多模态字段:${fieldName}`); + + // 检查字段名是否已存在 + if (isFieldNameExists(fieldName)) { + alert(`字段名 "${fieldName}" 已存在,请确保字段名称唯一`); + return; + } + + newFields = [...fields[type], `${fieldName}_${selectedFieldType}`]; } - setFields({ - ...fields, + setFields(prevFields => ({ + ...prevFields, [type]: newFields - }); + })); setInputValue({ ...inputValue, @@ -163,9 +243,6 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { if (type === 'llm') { setSelectedFieldType('default'); } - - // 立即触发字段更新事件,通知评查设置组件 - setTimeout(() => updateAllFields(), 0); } }; @@ -180,13 +257,18 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { const newFields = [...fields[type]]; newFields.splice(index, 1); - setFields({ - ...fields, - [type]: newFields + // 使用新的方式更新,确保状态立即更新并触发后续操作 + setFields(prevFields => { + const updatedFields = { + ...prevFields, + [type]: newFields + }; + + // 状态更新后立即触发字段更新事件 + Promise.resolve().then(() => updateAllFields()); + + return updatedFields; }); - - // 立即触发字段更新事件,通知评查设置组件 - setTimeout(() => updateAllFields(), 0); }; // 添加正则表达式字段行 @@ -223,19 +305,20 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { } }; - // 更新正则表达式字段值 + // 更新正则表达式字段 const updateRegexField = (id: string, key: 'fieldName' | 'regex', value: string) => { - const newRegexFields = regexFields.map(field => - field.id === id ? { ...field, [key]: value } : field - ); + // 更新字段值 + const newRegexFields = regexFields.map(field => { + if (field.id === id) { + return { ...field, [key]: value }; + } + return field; + }); + // 仅更新状态,不触发其他事件 setRegexFields(newRegexFields); - // 如果更新的是字段名,则触发字段更新事件 - if (key === 'fieldName') { - setTimeout(() => updateAllFields(), 0); - } - + // 更新onChange回调 if (onChange) { onChange({ extractionMethod: currentTab, @@ -244,6 +327,74 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { } }; + // 处理正则字段失去焦点事件,检查唯一性并更新字段列表 + const handleRegexFieldBlur = (id: string, key: 'fieldName' | 'regex') => { + // 只有在修改字段名时需要检查唯一性并更新字段列表 + if (key === 'fieldName') { + const currentField = regexFields.find(field => field.id === id); + if (currentField && currentField.fieldName.trim() !== '') { + const fieldName = currentField.fieldName.trim(); + console.log(`检查正则字段 '${fieldName}' 的唯一性,ID: ${id}`); + + // 检查当前正则字段组中是否有重名(排除自身) + const duplicateInRegex = regexFields + .filter(f => f.id !== id) + .find(f => f.fieldName === fieldName); + + if (duplicateInRegex) { + console.log(`字段名 '${fieldName}' 在正则字段中存在重复,ID: ${duplicateInRegex.id}`); + alert(`字段名 "${fieldName}" 已存在,请确保字段名称唯一`); + + // 重置为空字段名 + const resetFields = regexFields.map(field => { + if (field.id === id) { + return { ...field, fieldName: '' }; + } + return field; + }); + + setRegexFields(resetFields); + return; + } + + // 检查其他抽取方法中的字段(不区分大小写) + const otherExtractFields = [ + ...fields.llm_ocr.map(f => f.toLowerCase()), + ...fields.llm.map(f => { + const [name] = f.split('_'); + return name.toLowerCase(); + }) + ]; + + const fieldNameLower = fieldName.toLowerCase(); + const duplicateInOtherMethods = otherExtractFields.includes(fieldNameLower); + + if (duplicateInOtherMethods) { + console.log(`字段名 '${fieldName}' 在其他抽取方法中存在(不区分大小写)`); + alert(`字段名 "${fieldName}" 已存在,请确保字段名称唯一`); + + // 重置为空字段名 + const resetFields = regexFields.map(field => { + if (field.id === id) { + return { ...field, fieldName: '' }; + } + return field; + }); + + setRegexFields(resetFields); + return; + } + + // 字段名有效,更新字段列表 + console.log(`字段名 '${fieldName}' 检查通过,更新字段列表`); + updateAllFields(); + } + } else { + // 对于regex字段,只需更新字段列表 + updateAllFields(); + } + }; + // 应用正则模板 const applyRegexTemplate = (regex: string) => { // 找到当前正在编辑的行,或者最后一行 @@ -909,6 +1060,7 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { placeholder="如:合同编号" value={field.fieldName} onChange={(e) => updateRegexField(field.id, 'fieldName', e.target.value)} + onBlur={() => handleRegexFieldBlur(field.id, 'fieldName')} />
@@ -920,6 +1072,7 @@ export function ExtractionSettings({ onChange }: ExtractionSettingsProps) { placeholder="如:\\d{4}[-/年](0?[1-9]|1[0-2])[-/月](0?[1-9]|[12][0-9]|3[01])[日]?" value={field.regex} onChange={(e) => updateRegexField(field.id, 'regex', e.target.value)} + onBlur={() => handleRegexFieldBlur(field.id, 'regex')} />
diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index 2023881..91b2e9e 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -14,6 +14,13 @@ interface ComparisonPair { compareMethod: string; } +// 添加逻辑条件接口 +interface Condition { + field: string; + operator: string; + value: string; +} + interface ReviewSettingsProps { onChange?: (data: Record) => void; } @@ -59,12 +66,16 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { useEffect(() => { // 当Context中的字段发生变化时,更新可用字段但保留已有配置 if (extractionFields.length > 0) { + console.log('extractionFields updated in ReviewSettings:', extractionFields); // 检查是否有字段被删除 const deletedFields = availableFields.filter(field => !extractionFields.includes(field)); // 处理新增的字段 const newFields = extractionFields.filter((field: string) => !availableFields.includes(field)); + console.log('New fields:', newFields); + console.log('Deleted fields:', deletedFields); + if (newFields.length > 0 || deletedFields.length > 0) { // 设置最新的可用字段列表 setAvailableFields(extractionFields); @@ -83,6 +94,7 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { const handleExtractionChange = (event: Event) => { if (event instanceof CustomEvent && event.detail && Array.isArray(event.detail.fields)) { const incomingFields = event.detail.fields; + console.log('Received extraction fields update:', incomingFields); // 检查是否有字段被删除 const deletedFields = availableFields.filter(field => !incomingFields.includes(field)); @@ -90,6 +102,9 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { // 识别新增的字段 const newFields = incomingFields.filter((field: string) => !availableFields.includes(field)); + console.log('Deleted fields:', deletedFields); + console.log('New fields:', newFields); + if (newFields.length > 0 || deletedFields.length > 0) { // 设置最新的可用字段列表 setAvailableFields(incomingFields); @@ -112,7 +127,36 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { return () => { document.removeEventListener('extraction-fields-updated', handleExtractionChange); }; - }, [extractionFields, availableFields]); + }, [extractionFields]); + + // 检查并更新字段(仍然保留此函数供需要时手动触发) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const checkAndUpdateFields = () => { + 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) { + console.log('Updating fields in checkAndUpdateFields - deleted:', deletedFields, 'new:', newFields); + // 设置最新的可用字段列表 + setAvailableFields(extractionFields); + + // 处理规则中已删除的字段 + if (deletedFields.length > 0) { + handleDeletedFields(deletedFields); + } + + // 使用最新的字段列表更新规则配置 + updateRulesWithNewFields(extractionFields); + + return true; // 表示字段已更新 + } + } + return false; // 表示字段未更新 + }; // 初始化评查配置 useEffect(() => { @@ -453,9 +497,11 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { // 如果规则中的availableFields不是最新的,则更新它 if (type && config && (!config.availableFields || (Array.isArray(config.availableFields) && - !availableFields.every((field) => (config.availableFields as string[]).includes(field))))) { + !availableFields.every((field) => (config.availableFields as string[]).includes(field)) || + !(config.availableFields as string[]).every((field) => availableFields.includes(field))))) { // 延迟更新以避免在渲染过程中修改状态 setTimeout(() => { + console.log('Updating rule config with new available fields:', availableFields); const updatedConfig = { ...config, availableFields: availableFields }; handleRuleConfigChange(id, updatedConfig); }, 0); @@ -601,6 +647,7 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { id={`source-field-${id}-0`} className="form-select" onChange={(e) => { + // 直接初始化一个完整的比较对数组 const firstPair = { sourceField: e.target.value, targetField: '', compareMethod: '' }; handleRuleConfigChange(id, { pairs: [firstPair] }); }} @@ -617,9 +664,12 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { id={`target-field-${id}-0`} className="form-select" onChange={(e) => { - const pairs = Array.isArray(config.pairs) ? [...config.pairs] : [{ sourceField: '', compareMethod: '' }]; - pairs[0] = { ...pairs[0], targetField: e.target.value }; - handleRuleConfigChange(id, { pairs }); + // 获取sourceField的值 + const sourceField = document.getElementById(`source-field-${id}-0`) ? + (document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : ''; + + const firstPair = { sourceField, targetField: e.target.value, compareMethod: '' }; + handleRuleConfigChange(id, { pairs: [firstPair] }); }} > @@ -634,9 +684,14 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { id={`compare-method-${id}-0`} className="form-select" onChange={(e) => { - const pairs = Array.isArray(config.pairs) ? [...config.pairs] : [{ sourceField: '', targetField: '' }]; - pairs[0] = { ...pairs[0], compareMethod: e.target.value }; - handleRuleConfigChange(id, { pairs }); + // 获取sourceField和targetField的值 + const sourceField = document.getElementById(`source-field-${id}-0`) ? + (document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : ''; + const targetField = document.getElementById(`target-field-${id}-0`) ? + (document.getElementById(`target-field-${id}-0`) as HTMLSelectElement).value : ''; + + const firstPair = { sourceField, targetField, compareMethod: e.target.value }; + handleRuleConfigChange(id, { pairs: [firstPair] }); }} > @@ -655,9 +710,31 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) { className="ant-btn ant-btn-default" onClick={() => { // 添加新的比较对 - const currentPairs = Array.isArray(config.pairs) ? config.pairs : []; + // 直接获取当前的pairs数组,或初始化为空数组 + const pairs = Array.isArray(config.pairs) ? [...(config.pairs as ComparisonPair[])] : []; + + // 创建新的空白比较对 const newPair = { sourceField: '', targetField: '', compareMethod: '' }; - handleRuleConfigChange(id, { pairs: [...currentPairs, newPair] }); + + // 如果数组为空,确保先初始化第一个条目 + if (pairs.length === 0) { + // 如果界面上已有值,则添加两行:一行是当前值,一行是新的空行 + const sourceField = document.getElementById(`source-field-${id}-0`) ? + (document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : ''; + const targetField = document.getElementById(`target-field-${id}-0`) ? + (document.getElementById(`target-field-${id}-0`) as HTMLSelectElement).value : ''; + const compareMethod = document.getElementById(`compare-method-${id}-0`) ? + (document.getElementById(`compare-method-${id}-0`) as HTMLSelectElement).value : ''; + + // 将第一行设置为当前值(如果有) + pairs.push({ sourceField, targetField, compareMethod }); + } + + // 无论如何,都添加一个新的空白行 + pairs.push(newPair); + + // 更新配置 + handleRuleConfigChange(id, { pairs }); }} > 添加比较对 @@ -701,76 +778,194 @@ export function ReviewSettings({ onChange }: ReviewSettingsProps) {
-
-
-
- - { + const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : []; + currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], field: e.target.value }; + handleRuleConfigChange(id, { conditions: currentConditions }); + }} + > + + {availableFields.map((field, idx) => ( + + ))} + +
+
+ + +
+
+ + { + const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : []; + currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], value: e.target.value }; + handleRuleConfigChange(id, { conditions: currentConditions }); + }} + /> +
+
+
+ +
+
+ )) + ) : ( +
+
+
+ + +
+
+ + +
+
+ + { + // 获取field和operator的值 + const field = document.getElementById(`field-${id}-0`) ? + (document.getElementById(`field-${id}-0`) as HTMLSelectElement).value : ''; + const operator = document.getElementById(`operator-${id}-0`) ? + (document.getElementById(`operator-${id}-0`) as HTMLSelectElement).value : 'eq'; + + const firstCondition = { field, operator, value: e.target.value }; + handleRuleConfigChange(id, { conditions: [firstCondition] }); + }} + /> +
+
+
+
-
- - -
-
- - { - const currentConditions = Array.isArray(config.conditions) ? [...config.conditions] : []; - currentConditions[0] = { ...currentConditions[0], value: e.target.value }; - handleRuleConfigChange(id, { conditions: currentConditions }); - }} - /> + 删除 +
-
- -
-
+ )}