diff --git a/app/components/rules/new/BasicInfo.tsx b/app/components/rules/new/BasicInfo.tsx index 174d5df..a532ee3 100644 --- a/app/components/rules/new/BasicInfo.tsx +++ b/app/components/rules/new/BasicInfo.tsx @@ -2,7 +2,8 @@ import React, { useState, useEffect } from 'react'; interface BasicInfoProps { onChange?: (data: Record) => void; - initialData?: Record; + initialData?: FormDataType; + evaluationPointGroups?: Array<{id: number, pid: number, code: string, name: string, is_enabled: boolean}>; } // 定义表单数据类型 @@ -18,10 +19,12 @@ interface FormDataType { content: string; }; evaluation_point_groups_id: number | null; + evaluation_point_groups_pid: number | null; type: string; + id?: number; } -export function BasicInfo({ onChange, initialData }: BasicInfoProps) { +export function BasicInfo({ onChange, initialData, evaluationPointGroups = [] }: BasicInfoProps) { const [formData, setFormData] = useState({ name: '', code: '', @@ -34,11 +37,13 @@ export function BasicInfo({ onChange, initialData }: BasicInfoProps) { content: '' }, evaluation_point_groups_id: null, + evaluation_point_groups_pid: null, type: '' }); const [isDescExpanded, setIsDescExpanded] = useState(false); const [lawArticlesInput, setLawArticlesInput] = useState(''); + const [filteredRuleGroups, setFilteredRuleGroups] = useState>([]); // 当initialData变化时更新表单数据 useEffect(() => { @@ -55,6 +60,7 @@ export function BasicInfo({ onChange, initialData }: BasicInfoProps) { content: '' }, evaluation_point_groups_id: initialData.evaluation_point_groups_id || null, + evaluation_point_groups_pid: initialData.evaluation_point_groups_pid || null, type: initialData.type || '' }; @@ -76,6 +82,113 @@ export function BasicInfo({ onChange, initialData }: BasicInfoProps) { } }, [initialData]); + // 当评查点类型或评查点组数据变化时,过滤规则组列表 + useEffect(() => { + if (evaluationPointGroups && evaluationPointGroups.length > 0) { + console.log("评查点组数据更新,当前类型:", formData.type); + console.log("评查点组数据:", evaluationPointGroups); + + if (formData.type) { + // 获取所选评查点类型的组ID + const typeGroup = evaluationPointGroups.find(group => + group.pid === 0 && + group.code === formData.type + ); + + console.log("找到的类型组:", typeGroup); + + if (typeGroup) { + // 更新评查点类型组ID + if (formData.evaluation_point_groups_pid !== typeGroup.id) { + const newData = { + ...formData, + evaluation_point_groups_pid: typeGroup.id + }; + setFormData(newData); + if (onChange) onChange(newData); + } + + // 过滤出属于该类型的规则组(pid等于类型组ID的项) + const groups = evaluationPointGroups.filter(group => + group.pid === typeGroup.id && + group.is_enabled + ); + console.log("过滤后的规则组:", groups); + setFilteredRuleGroups(groups); + + // 如果当前选择的规则组不在过滤结果中,重置选择 + if (formData.evaluation_point_groups_id && + !groups.some(group => group.id === formData.evaluation_point_groups_id)) { + console.log("当前选择的规则组不在过滤结果中,重置选择"); + const newData = { + ...formData, + evaluation_point_groups_id: null + }; + setFormData(newData); + if (onChange) onChange(newData); + } + } else { + console.log("未找到对应的类型组"); + setFilteredRuleGroups([]); + + // 重置评查点类型组ID + if (formData.evaluation_point_groups_pid !== null) { + const newData = { + ...formData, + evaluation_point_groups_pid: null + }; + setFormData(newData); + if (onChange) onChange(newData); + } + } + } else { + console.log("未选择评查点类型"); + setFilteredRuleGroups([]); + + // 重置评查点类型组ID + if (formData.evaluation_point_groups_pid !== null) { + const newData = { + ...formData, + evaluation_point_groups_pid: null + }; + setFormData(newData); + if (onChange) onChange(newData); + } + } + } + }, [formData.type, evaluationPointGroups, formData.evaluation_point_groups_id, formData.evaluation_point_groups_pid, onChange]); + + // 获取评查点类型选项(pid=0的数据) + const getCheckpointTypeOptions = () => { + if (!evaluationPointGroups || evaluationPointGroups.length === 0) { + console.log("无评查点组数据,使用默认类型选项"); + return ( + <> + + + + + + + + ); + } + + const typeGroups = evaluationPointGroups.filter(group => group.pid === 0 && group.is_enabled); + console.log("可用的评查点类型:", typeGroups); + + return ( + <> + + {typeGroups.map(group => ( + + ))} + + ); + }; + const handleToggleDescription = () => { setIsDescExpanded(!isDescExpanded); }; @@ -115,6 +228,19 @@ export function BasicInfo({ onChange, initialData }: BasicInfoProps) { break; case 'checkpoint-type': newData.type = value; + // 重置规则组选择 + newData.evaluation_point_groups_id = null; + + // 设置评查点类型组ID + if (value) { + const typeGroup = evaluationPointGroups.find(group => + group.pid === 0 && + group.code === value + ); + newData.evaluation_point_groups_pid = typeGroup ? typeGroup.id : null; + } else { + newData.evaluation_point_groups_pid = null; + } break; } @@ -207,32 +333,37 @@ export function BasicInfo({ onChange, initialData }: BasicInfoProps) { value={formData.type} onChange={handleInputChange} > - - - - - - + {getCheckpointTypeOptions()}
评查点类型用于分类管理,便于规则统一调用
+
+ {!formData.type ? "请先选择评查点类型" : + filteredRuleGroups.length === 0 ? "该类型下暂无可用规则组" : + "选择评查点所属的规则组"} +
diff --git a/app/components/rules/new/ExtractionSettings.tsx b/app/components/rules/new/ExtractionSettings.tsx index 2b45903..464b8ea 100644 --- a/app/components/rules/new/ExtractionSettings.tsx +++ b/app/components/rules/new/ExtractionSettings.tsx @@ -1,5 +1,6 @@ import { useState, KeyboardEvent, FormEvent, useContext, useEffect, useCallback, useRef } from 'react'; -import { RuleContext } from './ReviewSettings'; +import { RuleContext } from '~/contexts/RuleContext'; +import { processFieldName } from '~/utils'; /** * ExtractionSettings 组件 @@ -169,7 +170,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 获取所有字段(不包括regexFields,这部分单独处理) const llm_ocr_fields = fields.llm_ocr || []; - const llm_fields = (fields.llm || []).map((field) => field.split('_')[0]); + const llm_fields = (fields.llm || []).map(processFieldName); // 检查是否在其他类型字段中存在 if (llm_ocr_fields.some(f => f.toLowerCase() === fieldNameLower) || @@ -202,7 +203,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings // 收集所有字段名 - 不受当前标签页影响,始终收集所有类型的字段 const allFieldNamesList = [ ...fields.llm_ocr, - ...fields.llm.map(f => f.split('_')[0]), + ...fields.llm.map(f => processFieldName(f)), ...validRegexFields.map(f => f.fieldName.trim()) ].filter(name => name); // 过滤空值 @@ -250,20 +251,7 @@ export function ExtractionSettings({ onChange, initialData }: ExtractionSettings }); } - // 分发自定义事件,确保更新到所有依赖此事件的组件 - document.dispatchEvent( - new CustomEvent('extraction-fields-updated', { - detail: { - fields: allFields, - fieldsData: { - llm_ocr: fields.llm_ocr || [], - llm: fields.llm || [], - regex: validRegexFields.map(f => f.fieldName.trim()) - }, - timestamp: Date.now() // 添加时间戳确保事件能被识别为新事件 - }, - }) - ); + // 不再使用自定义事件,统一通过Context共享数据 // 更新上次发送的字段列表和时间 lastEventFieldsRef.current = [...allFields]; diff --git a/app/components/rules/new/ReviewSettings.tsx b/app/components/rules/new/ReviewSettings.tsx index f98b6ba..f8603cd 100644 --- a/app/components/rules/new/ReviewSettings.tsx +++ b/app/components/rules/new/ReviewSettings.tsx @@ -1,5 +1,7 @@ -import React, { useState, useEffect, useContext, createContext } from 'react'; +import React, { useState, useEffect, useContext, useCallback, useRef } from 'react'; import { SimpleCodeEditor } from './SimpleCodeEditor'; +import { RuleContext } from '~/contexts/RuleContext'; +import { processFieldNames, areArraysDifferent, getArrayDifference, debounce } from '~/utils'; interface RuleType { id: string; @@ -33,21 +35,11 @@ interface ReviewSettingsProps { suggestion_message_type?: string; post_action?: string; action_config?: string; + score?: number; + scoreDisplay?: string; }; } -// 创建全局上下文以便在不同组件间共享数据 -interface RuleContextType { - extractionFields: string[]; - updateFields: (fields: string[]) => void; -} - -// 创建全局Context对象 -export const RuleContext = createContext({ - extractionFields: [], - updateFields: () => {} -}); - export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const [rules, setRules] = useState([ { id: '1', type: '', config: {} } @@ -58,6 +50,9 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { // 添加评查后动作相关状态 const [post_action, setPostAction] = useState('none'); const [action_config, setActionConfig] = useState(''); + // 添加分数状态 + const [score, setScore] = useState(0); + const [scoreDisplay, setScoreDisplay] = useState(''); // 获取抽取字段的上下文 const { extractionFields } = useContext(RuleContext); @@ -71,14 +66,66 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const [suggestion_message_type, setSuggestionMessageType] = useState('warning'); // 保存最近一次可用的字段列表 - const [availableFields, setAvailableFields] = useState(extractionFields || []); - + const [availableFields, setAvailableFields] = useState( + // 初始化时就处理字段,去掉类型后缀 + processFieldNames(extractionFields || []) + ); + + // 使用useRef跟踪是否已经初始化过 + const initializedRef = useRef(false); + // 加载初始数据 useEffect(() => { - if (initialData) { + // 如果已经初始化过,则跳过此次处理 + if (initialData && !initializedRef.current) { + initializedRef.current = true; + console.log('加载初始数据(首次):', initialData); + // 设置规则 if (initialData.rules && initialData.rules.length > 0) { - setRules(initialData.rules); + // 确保每个规则都有完整的配置 + const enhancedRules = initialData.rules.map(rule => { + // 根据规则类型,确保config包含必要的字段 + let config = { ...rule.config }; + + // 确保有用于展示的字段 + if (rule.type === 'format' && config.field && !config.checkField) { + config.checkField = config.field; + } + + if (rule.type === 'regex') { + if (config.field && !config.checkField) { + config.checkField = config.field; + } + if (config.pattern && !config.regexPattern) { + config.regexPattern = config.pattern; + } + } + + // 确保存在字段有正确的内部表示 + if (rule.type === 'exists' && config.fields && !config.selectedFields) { + config.selectedFields = config.fields; + config.existsLogic = config.logic || 'all'; + } + + // 对于条件逻辑规则 + if ((rule.type === 'consistency' || rule.type === 'logic') && config.logic && !config.logicRelation) { + config.logicRelation = config.logic; + } + + // 确保所有规则都有可用字段列表 + if (!config.availableFields) { + config.availableFields = availableFields; + } + + return { + ...rule, + config + }; + }); + + console.log('增强后的规则(首次):', enhancedRules); + setRules(enhancedRules); } // 设置组合逻辑 @@ -120,104 +167,77 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { if (initialData.action_config) { setActionConfig(initialData.action_config); } + + // 设置分数 + if (initialData.score !== undefined) { + setScore(initialData.score); + } + + // 设置分数显示值 + if (initialData.scoreDisplay) { + setScoreDisplay(initialData.scoreDisplay); + } else if (initialData.score !== undefined && initialData.score > 0) { + setScoreDisplay(String(initialData.score)); + } + + // 数据加载完成后,生成一次完整的评查配置 + setTimeout(() => { + generateEvaluationConfig(); + }, 0); } + // 移除availableFields依赖,避免死循环 }, [initialData]); // 监听extractionFields的变化 useEffect(() => { if (extractionFields && extractionFields.length > 0) { - const newFields = [...extractionFields]; + // 使用工具函数处理字段 + const uniqueFields = processFieldNames(extractionFields); + // 只在字段列表实际发生变化时更新 - if (JSON.stringify(newFields) !== JSON.stringify(availableFields)) { - setAvailableFields(newFields); + if (areArraysDifferent(uniqueFields, availableFields)) { + // 检查删除和新增的字段 + const { added, removed } = getArrayDifference(uniqueFields, availableFields); + + // 处理删除的字段 + if (removed.length > 0) { + handleDeletedFields(removed); + } + + // 更新可用字段 + setAvailableFields(uniqueFields); + + // 使用最新的字段更新规则 + updateRulesWithNewFields(uniqueFields); } } - }, [extractionFields]); - - // 监听抽取设置中的字段变化 - useEffect(() => { - // 用于防抖的变量 - 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(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); - } - }; - - // 添加事件监听器,监听抽取设置中的字段变化 - document.addEventListener('extraction-fields-updated', handleExtractionChange); - - // 组件卸载时移除事件监听 - return () => { - document.removeEventListener('extraction-fields-updated', handleExtractionChange); - if (fieldUpdateTimeout) { - clearTimeout(fieldUpdateTimeout); - } - }; - }, [availableFields]); + }, [extractionFields, availableFields]); // 检查并更新字段(仍然保留此函数供需要时手动触发) // eslint-disable-next-line @typescript-eslint/no-unused-vars const checkAndUpdateFields = () => { if (extractionFields.length > 0) { + // 处理字段,去掉类型后缀 + const processedFields = extractionFields.map(field => { + if (field.includes('_')) { + return field.split('_')[0]; // 只保留类型前面的字段名 + } + return field; + }); + + // 去重 + const uniqueFields = [...new Set(processedFields)]; + // 检查是否有字段被删除 - const deletedFields = availableFields.filter(field => !extractionFields.includes(field)); + const deletedFields = availableFields.filter(field => !uniqueFields.includes(field)); // 处理新增的字段 - const newFields = extractionFields.filter((field: string) => !availableFields.includes(field)); + const newFields = uniqueFields.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); + setAvailableFields(uniqueFields); // 处理规则中已删除的字段 if (deletedFields.length > 0) { @@ -225,7 +245,7 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { } // 使用最新的字段列表更新规则配置 - updateRulesWithNewFields(extractionFields); + updateRulesWithNewFields(uniqueFields); return true; // 表示字段已更新 } @@ -300,7 +320,17 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const updatedConfig = { ...rule.config }; // 对所有规则类型都更新availableFields字段 - updatedConfig.availableFields = newFields; + // 处理字段,只保留字段名,去掉类型后缀 + const processedFields = newFields.map(field => { + if (field.includes('_')) { + return field.split('_')[0]; // 只保留类型前面的字段名 + } + return field; + }); + + // 去重 + const uniqueFields = [...new Set(processedFields)]; + updatedConfig.availableFields = uniqueFields; // 根据规则类型更新其他相关字段 if (rule.type) { @@ -348,8 +378,17 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { setShowCustomLogic(logic === 'custom'); if (onChange) { - const updatedData: Record = { logicType: logic }; - onChange(updatedData); + // 确保将完整的数据传递给父组件 + const updateData = { + combinationLogic: logic, + // 如果切换到自定义逻辑,同时传递自定义逻辑内容 + customLogic: logic === 'custom' ? customLogic : '' + }; + + onChange(updateData); + + // 生成完整的评查配置 + setTimeout(() => generateEvaluationConfig(), 0); } }; @@ -409,38 +448,41 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { switch(type) { case 'exists': initialConfig = { - fields: [], - logic: 'and', + selectedFields: [], + existsLogic: 'all', // 默认为全部必须存在 availableFields: availableFields }; break; case 'consistency': initialConfig = { pairs: [], - logic: 'and', + logicRelation: 'and', // 默认为AND逻辑 availableFields: availableFields }; break; case 'format': initialConfig = { - field: '', + field: '', // 用于内部运算 + checkField: '', // 用于UI展示 formatType: '', - parameters: '', + formatParams: '', availableFields: availableFields }; break; case 'logic': initialConfig = { conditions: [], - logic: 'and', + logicRelation: 'and', // 默认为AND逻辑 availableFields: availableFields }; break; case 'regex': initialConfig = { - field: '', + field: '', // 用于内部运算 + checkField: '', // 用于UI展示 pattern: '', - matchType: 'match', + regexPattern: '', // 用于UI展示 + matchType: 'match', // 默认为必须匹配 availableFields: availableFields }; break; @@ -489,7 +531,25 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const handleRuleConfigChange = (id: string, configChanges: Record) => { const newRules = rules.map(rule => { if (rule.id === id) { - return { ...rule, config: { ...rule.config, ...configChanges } }; + // 处理特殊的字段映射,确保不同名称的字段保持同步 + const processedChanges = { ...configChanges }; + + // 对于格式判断,确保checkField和field字段同步 + if (rule.type === 'format' && 'checkField' in configChanges) { + processedChanges.field = configChanges.checkField; + } + + // 对于正则判断,确保字段名和模式保持同步 + if (rule.type === 'regex') { + if ('checkField' in configChanges) { + processedChanges.field = configChanges.checkField; + } + if ('regexPattern' in configChanges) { + processedChanges.pattern = configChanges.regexPattern; + } + } + + return { ...rule, config: { ...rule.config, ...processedChanges } }; } return rule; }); @@ -497,11 +557,9 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { setRules(newRules); if (onChange) { + // 立即触发父组件的onChange回调,确保数据能保存到父组件 onChange({ rules: newRules }); } - - // 更新评查配置 - generateEvaluationConfig(); }; // 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示 @@ -516,37 +574,49 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { return (
- {fieldsToRender.map((field, index) => ( -
{ - // 切换选中状态 - const newSelectedFields = selectedFields.includes(field) - ? selectedFields.filter(f => f !== field) - : [...selectedFields, field]; - - handleRuleConfigChange(ruleId, { - selectedFields: newSelectedFields - }); - }} - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ' ') { - const newSelectedFields = selectedFields.includes(field) + {fieldsToRender.map((field, index) => { + // 使用includes方法检查选中状态 + const isSelected = selectedFields.includes(field); + + return ( +
{ + // 切换选中状态 + const newSelectedFields = isSelected ? selectedFields.filter(f => f !== field) : [...selectedFields, field]; + // 更新规则配置 handleRuleConfigChange(ruleId, { selectedFields: newSelectedFields }); - } - }} - role="button" - tabIndex={0} - > - {field} -
- ))} + + // 直接触发配置更新 + generateEvaluationConfig(); + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + const newSelectedFields = isSelected + ? selectedFields.filter(f => f !== field) + : [...selectedFields, field]; + + handleRuleConfigChange(ruleId, { + selectedFields: newSelectedFields + }); + + // 直接触发配置更新 + generateEvaluationConfig(); + } + }} + role="button" + tabIndex={0} + > + {field} +
+ ); + })}
); }; @@ -612,8 +682,12 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { name={`existsLogic_${id}`} className="form-radio" value="all" - defaultChecked - onChange={(e) => handleRuleConfigChange(id, { existsLogic: e.target.value })} + checked={!config.existsLogic || config.existsLogic === 'all'} + onChange={(e) => { + handleRuleConfigChange(id, { existsLogic: e.target.value }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} /> 所有字段必须存在 @@ -624,7 +698,12 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { name={`existsLogic_${id}`} className="form-radio" value="any" - onChange={(e) => handleRuleConfigChange(id, { existsLogic: e.target.value })} + checked={config.existsLogic === 'any'} + onChange={(e) => { + handleRuleConfigChange(id, { existsLogic: e.target.value }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} /> 任一字段存在即可 @@ -653,6 +732,8 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const updatedPairs = [...(config.pairs as ComparisonPair[])]; updatedPairs[pairIndex] = {...pair, sourceField: e.target.value}; handleRuleConfigChange(id, { pairs: updatedPairs }); + // 直接触发配置更新 + generateEvaluationConfig(); }} > @@ -671,6 +752,8 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const updatedPairs = [...(config.pairs as ComparisonPair[])]; updatedPairs[pairIndex] = {...pair, targetField: e.target.value}; handleRuleConfigChange(id, { pairs: updatedPairs }); + // 直接触发配置更新 + generateEvaluationConfig(); }} > @@ -689,6 +772,8 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const updatedPairs = [...(config.pairs as ComparisonPair[])]; updatedPairs[pairIndex] = {...pair, compareMethod: e.target.value}; handleRuleConfigChange(id, { pairs: updatedPairs }); + // 直接触发配置更新 + generateEvaluationConfig(); }} > @@ -706,6 +791,8 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { const updatedPairs = [...(config.pairs as ComparisonPair[])]; updatedPairs.splice(pairIndex, 1); handleRuleConfigChange(id, { pairs: updatedPairs }); + // 直接触发配置更新 + generateEvaluationConfig(); }} > 删除 @@ -721,10 +808,16 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { { - // 获取sourceField的值 - const sourceField = document.getElementById(`source-field-${id}-0`) ? - (document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : ''; + // 获取当前选择的源字段和目标字段 + const sourceField = (config.initialSourceField as string) || ''; + const targetField = e.target.value; - const firstPair = { sourceField, targetField: e.target.value, compareMethod: '' }; - handleRuleConfigChange(id, { pairs: [firstPair] }); + handleRuleConfigChange(id, { + initialTargetField: targetField, + pairs: [{ sourceField, targetField, compareMethod: '' }] + }); + // 直接触发配置更新 + generateEvaluationConfig(); }} > @@ -758,15 +856,19 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { { // 直接初始化一个完整的条件对象 - const firstCondition = { field: e.target.value, operator: 'eq', value: '' }; - handleRuleConfigChange(id, { conditions: [firstCondition] }); + const field = e.target.value; + handleRuleConfigChange(id, { + initialField: field, + conditions: [{ field, operator: 'eq', value: '' }] + }); + // 触发配置更新 + generateEvaluationConfig(); }} > @@ -953,13 +1070,18 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { handleRuleConfigChange(id, { checkField: e.target.value })} + value={config.checkField as string || ''} + onChange={(e) => { + handleRuleConfigChange(id, { + checkField: e.target.value, + field: e.target.value // 同步更新内部字段 + }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} > {availableFields.map((field, idx) => ( @@ -1100,7 +1216,15 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { id={`regex-pattern-${id}`} className="form-textarea" placeholder="请输入正则表达式" - onChange={(e) => handleRuleConfigChange(id, { regexPattern: e.target.value })} + value={config.regexPattern as string || ''} + onChange={(e) => { + handleRuleConfigChange(id, { + regexPattern: e.target.value, + pattern: e.target.value // 同步更新内部字段 + }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} >
@@ -1113,7 +1237,7 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { name={`regexMatchType_${id}`} className="form-radio" value="match" - defaultChecked + checked={!config.matchType || config.matchType === 'match'} onChange={(e) => handleRuleConfigChange(id, { matchType: e.target.value })} /> 必须匹配(符合为通过) @@ -1124,6 +1248,7 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { name={`regexMatchType_${id}`} className="form-radio" value="not_match" + checked={config.matchType === 'not_match'} onChange={(e) => handleRuleConfigChange(id, { matchType: e.target.value })} /> 不得匹配(不符合为通过) @@ -1262,7 +1387,15 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { handleRuleConfigChange(id, { formatType: e.target.value })} + value={config.formatType as string || ''} + onChange={(e) => { + handleRuleConfigChange(id, { formatType: e.target.value }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} > @@ -1295,7 +1433,15 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { id={`format-params-${id}`} className="form-input" placeholder="请输入参数设置" - onChange={(e) => handleRuleConfigChange(id, { formatParams: e.target.value })} + value={config.formatParams as string || ''} + onChange={(e) => { + handleRuleConfigChange(id, { + formatParams: e.target.value, + parameters: e.target.value // 同步更新内部参数字段 + }); + // 直接触发配置更新 + generateEvaluationConfig(); + }} />
@@ -1311,23 +1457,100 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { }; // 生成完整的评查配置数据并在提交保存时使用 - const generateEvaluationConfig = () => { + const generateEvaluationConfig = useCallback(() => { const config = { logicType: combinationLogic, customLogic: combinationLogic === 'custom' ? customLogic : '', - rules: rules.map(rule => ({ - id: rule.id, - type: rule.type, - config: rule.config - })) + rules: rules.map(rule => { + // 创建一个深拷贝以避免修改原始对象 + const processedConfig = JSON.parse(JSON.stringify(rule.config || {})); + + // 根据规则类型处理特定的字段映射 + switch(rule.type) { + case 'exists': + // 将UI字段名映射为API字段名 + if (processedConfig.selectedFields) { + processedConfig.fields = processedConfig.selectedFields; + delete processedConfig.selectedFields; + } + if (processedConfig.existsLogic) { + processedConfig.logic = processedConfig.existsLogic; + delete processedConfig.existsLogic; + } + break; + + case 'consistency': + case 'logic': + // 将UI字段名映射为API字段名 + if (processedConfig.logicRelation) { + processedConfig.logic = processedConfig.logicRelation; + delete processedConfig.logicRelation; + } + // 删除用于UI的临时字段 + delete processedConfig.initialSourceField; + delete processedConfig.initialTargetField; + delete processedConfig.initialCompareMethod; + delete processedConfig.initialField; + delete processedConfig.initialOperator; + delete processedConfig.initialValue; + break; + + case 'format': + // 确保field字段正确设置 + if (processedConfig.checkField) { + processedConfig.field = processedConfig.checkField; + delete processedConfig.checkField; + } + if (processedConfig.formatParams) { + processedConfig.parameters = processedConfig.formatParams; + delete processedConfig.formatParams; + } + break; + + case 'regex': + // 确保field和pattern字段正确设置 + if (processedConfig.checkField) { + processedConfig.field = processedConfig.checkField; + delete processedConfig.checkField; + } + if (processedConfig.regexPattern) { + processedConfig.pattern = processedConfig.regexPattern; + delete processedConfig.regexPattern; + } + break; + } + + // 移除只在UI使用的字段 + delete processedConfig.availableFields; + + return { + id: rule.id, + type: rule.type, + config: processedConfig + }; + }) }; if (onChange) { - onChange({ evaluation_config: config }); + 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); } return config; - }; + }, [rules, combinationLogic, customLogic, pass_message, fail_message, suggestion_message, suggestion_message_type, post_action, action_config, score, scoreDisplay, onChange]); // 处理评查结果消息变更 const handleMessageChange = (type: string, value: string) => { @@ -1345,6 +1568,8 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { if (onChange) { onChange({ [`${type}_message`]: value }); + // 触发完整配置生成以确保数据保存 + generateEvaluationConfig(); } }; @@ -1354,11 +1579,71 @@ export function ReviewSettings({ onChange, initialData }: ReviewSettingsProps) { if (onChange) { onChange({ suggestion_message_type: value }); + // 触发完整配置生成以确保数据保存 + generateEvaluationConfig(); + } + }; + + // 处理分数变更 + const handleScoreChange = (value: string) => { + // 保存用户输入的显示值 + setScoreDisplay(value); + + // 只在值不为空时更新实际分数 + if (value.trim() !== '') { + const numValue = parseFloat(value); + if (!isNaN(numValue)) { + const validScore = Math.min(Math.max(numValue, 0), 100); + setScore(validScore); + } else { + setScore(0); + } + } else { + setScore(0); + } + + if (onChange) { + if (value.trim() === '') { + // 空值处理 + onChange({ score: 0, scoreDisplay: '' }); + } else { + onChange({ score: score, scoreDisplay: value }); + } } }; return (
+

评查设置

@@ -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