评查点,增改逻辑完善
This commit is contained in:
@@ -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<string[]>([]);
|
||||
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<string | null>(null);
|
||||
// 添加定时器引用
|
||||
const fieldEditResetTimeoutRef = useRef<NodeJS.Timeout | null>(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<boolean>(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<string, number>();
|
||||
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<HTMLInputElement>, 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<HTMLInputElement>, 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<HTMLInputElement>, 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<HTMLSelectElement>, 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<HTMLTextAreaElement>, 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
|
||||
<div className="mt-2" id="regex-fields-container">
|
||||
{regexFields.map((field) => (
|
||||
<div
|
||||
className="regex-field-row flex items-start mb-2 border border-gray-200 rounded-md p-2 bg-gray-50"
|
||||
className={`regex-field-row flex items-start mb-2 border rounded-md p-2 ${fieldEditStatus[field.id] ? 'border-blue-300 bg-blue-50' : 'border-gray-200 bg-gray-50'}`}
|
||||
key={field.id}
|
||||
>
|
||||
<div className="w-3/10 mr-2">
|
||||
@@ -782,12 +1069,12 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input regex-field-name"
|
||||
className={`form-input regex-field-name ${!field.fieldName && !fieldEditStatus[field.id] ? 'border-yellow-300' : ''}`}
|
||||
id={`regex-field-name-${field.id}`}
|
||||
placeholder="如:合同编号"
|
||||
value={field.fieldName}
|
||||
onChange={(e) => 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')}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-7/10 mr-2">
|
||||
@@ -799,20 +1086,30 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
className="form-input regex-expression"
|
||||
className={`form-input regex-expression ${field.fieldName && !field.regex && !fieldEditStatus[field.id] ? 'border-yellow-300' : ''}`}
|
||||
id={`regex-expression-${field.id}`}
|
||||
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')}
|
||||
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 && (
|
||||
<div className="text-xs mt-1 text-blue-600 transition-opacity duration-300">
|
||||
{statusMessage.message}
|
||||
</div>
|
||||
)}
|
||||
{!statusMessage && field.fieldName && !field.regex && !fieldEditStatus[field.id] && (
|
||||
<div className="text-xs mt-1 text-yellow-600">
|
||||
未设置正则表达式,此字段将保留但不会执行抽取
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col justify-end pt-3">
|
||||
<button
|
||||
className="text-red-500 hover:text-red-700 remove-regex-field-row"
|
||||
type="button"
|
||||
aria-label="删除"
|
||||
onClick={() => removeRegexFieldRow(field.id)}
|
||||
onClick={() => field.id && removeRegexFieldRow(field.id)}
|
||||
>
|
||||
<i className="ri-delete-bin-line"></i>
|
||||
</button>
|
||||
@@ -857,6 +1154,35 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 在所有标签页外部添加统一的更新按钮和状态显示,这样在任何标签页都可见 */}
|
||||
<div className="border-t border-gray-200 pt-5 mt-5">
|
||||
<div className="flex flex-col items-center mb-2">
|
||||
{hasPendingChanges && !updateStatus && (
|
||||
<div className="text-center text-sm mb-3 p-2 rounded bg-yellow-100 text-yellow-700">
|
||||
您有未更新的字段变更,请点击下方的“更新全部字段”按钮提交所有标签页的字段变更
|
||||
</div>
|
||||
)}
|
||||
{!hasPendingChanges && !updateStatus && (
|
||||
<div className="text-center text-xs mb-2 text-gray-600">
|
||||
提示:请在完成所有标签页的字段编辑后,点击此按钮更新所有字段
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className={`ant-btn ${hasPendingChanges ? 'ant-btn-primary' : 'ant-btn-default'} text-base py-2 px-4 font-medium shadow-sm hover:shadow`}
|
||||
type="button"
|
||||
onClick={handleUpdateFields}
|
||||
>
|
||||
<i className="ri-refresh-line mr-1"></i>
|
||||
{hasPendingChanges ? '更新全部字段 (有未保存更改)' : '更新全部字段'}
|
||||
</button>
|
||||
</div>
|
||||
{updateStatus && (
|
||||
<div className={`text-center text-sm mt-2 p-2 rounded ${updateStatus.success ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{updateStatus.message}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user