Files
leaudit-platform-frontend/app/components/rules/new/ReviewSettings.tsx
T
2025-04-02 18:13:12 +08:00

1625 lines
69 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useState, useEffect, useContext, createContext } from 'react';
import { SimpleCodeEditor } from './SimpleCodeEditor';
interface RuleType {
id: string;
type: string;
config: Record<string, unknown>;
}
// 为配置项添加类型定义
interface ComparisonPair {
sourceField: string;
targetField: string;
compareMethod: string;
}
// 添加逻辑条件接口
interface Condition {
field: string;
operator: string;
value: string;
}
interface ReviewSettingsProps {
onChange?: (data: Record<string, unknown>) => void;
}
// 创建全局上下文以便在不同组件间共享数据
interface RuleContextType {
extractionFields: string[];
updateFields: (fields: string[]) => void;
}
// 创建全局Context对象
export const RuleContext = createContext<RuleContextType>({
extractionFields: [],
updateFields: () => {}
});
export function ReviewSettings({ onChange }: ReviewSettingsProps) {
const [rules, setRules] = useState<RuleType[]>([
{ id: '1', type: '', config: {} }
]);
const [combinationLogic, setCombinationLogic] = useState<string>('and');
const [customLogic, setCustomLogic] = useState<string>('');
const [showCustomLogic, setShowCustomLogic] = useState<boolean>(false);
// 添加评查后动作相关状态
const [post_action, setPostAction] = useState<string>('none');
const [action_config, setActionConfig] = useState<string>('');
// 获取抽取字段的上下文
const { extractionFields } = useContext(RuleContext);
// 初始化评查通过/不通过/建议信息
const [pass_message, setPassMessage] = useState('文档检查通过,符合规范要求。');
const [fail_message, setFailMessage] = useState('文档存在以下问题,请修改后重新提交。');
const [suggestion_message, setSuggestMessage] = useState('');
// 提示类型
const [suggestion_message_type, setSuggestionMessageType] = useState('warning');
// 保存最近一次可用的字段列表
const [availableFields, setAvailableFields] = useState<string[]>(extractionFields || []);
// 监听抽取设置中的字段变化
useEffect(() => {
// 当Context中的字段发生变化时,更新可用字段但保留已有配置
if (extractionFields.length > 0) {
console.log('extractionFields updated in ReviewSettings:', extractionFields);
// 检查是否有字段被删除
const deletedFields = availableFields.filter(field => !extractionFields.includes(field));
// 处理新增的字段
const newFields = extractionFields.filter((field: string) => !availableFields.includes(field));
console.log('New fields:', newFields);
console.log('Deleted fields:', deletedFields);
if (newFields.length > 0 || deletedFields.length > 0) {
// 设置最新的可用字段列表
setAvailableFields(extractionFields);
// 处理规则中已删除的字段
if (deletedFields.length > 0) {
handleDeletedFields(deletedFields);
}
// 使用最新的字段列表更新规则配置
updateRulesWithNewFields(extractionFields);
}
}
// 监听抽取设置的变化 - 用于捕获非Context更新的情况
const handleExtractionChange = (event: Event) => {
if (event instanceof CustomEvent && event.detail && Array.isArray(event.detail.fields)) {
const incomingFields = event.detail.fields;
console.log('Received extraction fields update:', incomingFields);
// 检查是否有字段被删除
const deletedFields = availableFields.filter(field => !incomingFields.includes(field));
// 识别新增的字段
const newFields = incomingFields.filter((field: string) => !availableFields.includes(field));
console.log('Deleted fields:', deletedFields);
console.log('New fields:', newFields);
if (newFields.length > 0 || deletedFields.length > 0) {
// 设置最新的可用字段列表
setAvailableFields(incomingFields);
// 处理规则中已删除的字段
if (deletedFields.length > 0) {
handleDeletedFields(deletedFields);
}
// 使用最新的字段列表更新规则配置
updateRulesWithNewFields(incomingFields);
}
}
};
// 添加事件监听器,监听抽取设置中的字段变化
document.addEventListener('extraction-fields-updated', handleExtractionChange);
// 组件卸载时移除事件监听
return () => {
document.removeEventListener('extraction-fields-updated', handleExtractionChange);
};
}, [extractionFields]);
// 检查并更新字段(仍然保留此函数供需要时手动触发)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const checkAndUpdateFields = () => {
if (extractionFields.length > 0) {
// 检查是否有字段被删除
const deletedFields = availableFields.filter(field => !extractionFields.includes(field));
// 处理新增的字段
const newFields = extractionFields.filter((field: string) => !availableFields.includes(field));
if (newFields.length > 0 || deletedFields.length > 0) {
console.log('Updating fields in checkAndUpdateFields - deleted:', deletedFields, 'new:', newFields);
// 设置最新的可用字段列表
setAvailableFields(extractionFields);
// 处理规则中已删除的字段
if (deletedFields.length > 0) {
handleDeletedFields(deletedFields);
}
// 使用最新的字段列表更新规则配置
updateRulesWithNewFields(extractionFields);
return true; // 表示字段已更新
}
}
return false; // 表示字段未更新
};
// 初始化评查配置
useEffect(() => {
// 生成并更新评查配置
generateEvaluationConfig();
}, []);
// 处理已删除字段的函数
const handleDeletedFields = (deletedFields: string[]) => {
setRules(prevRules => {
return prevRules.map(rule => {
const updatedConfig = { ...rule.config };
switch (rule.type) {
case 'exists':
case 'logic':
case 'regex':
// 从已选字段中移除被删除的字段
if (Array.isArray(updatedConfig.selectedFields)) {
updatedConfig.selectedFields = (updatedConfig.selectedFields as string[]).filter(
field => !deletedFields.includes(field)
);
}
break;
case 'consistency':
// 从配对字段中移除被删除的字段
if (Array.isArray(updatedConfig.pairs)) {
updatedConfig.pairs = (updatedConfig.pairs as ComparisonPair[]).filter(
pair => !deletedFields.includes(pair.sourceField) && !deletedFields.includes(pair.targetField)
);
}
break;
case 'format':
// 如果判断字段被删除,则清空字段
if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) {
updatedConfig.checkField = '';
}
break;
default:
break;
}
// 更新可用字段列表,移除被删除的字段
if (Array.isArray(updatedConfig.availableFields)) {
updatedConfig.availableFields = (updatedConfig.availableFields as string[]).filter(
field => !deletedFields.includes(field)
);
}
return {
...rule,
config: updatedConfig
};
});
});
};
// 更新规则配置中的可用字段但保留已选择的字段和规则配置
const updateRulesWithNewFields = (newFields: string[]) => {
// 更新每个规则的可用字段列表,但保留现有配置
setRules(prevRules => {
return prevRules.map(rule => {
const updatedConfig = { ...rule.config };
// 对所有规则类型都更新availableFields字段
updatedConfig.availableFields = newFields;
// 根据规则类型更新其他相关字段
if (rule.type) {
switch (rule.type) {
case 'field_validation':
// 保留已有的字段选择,只添加新字段
if (!updatedConfig.fields) {
updatedConfig.fields = [];
}
break;
case 'field_comparison':
// 保留已配置的比较项
if (!updatedConfig.pairs) {
updatedConfig.pairs = [];
}
break;
case 'field_regex':
// 保留正则表达式配置
break;
case 'custom_code':
break;
default:
// 对于所有类型规则,确保selectedFields字段存在
if (!updatedConfig.selectedFields) {
updatedConfig.selectedFields = [];
}
break;
}
}
return {
...rule,
config: updatedConfig
};
});
});
};
const handleLogicChange = (logic: string) => {
setCombinationLogic(logic);
setShowCustomLogic(logic === 'custom');
if (onChange) {
const updatedData: Record<string, unknown> = { logicType: logic };
onChange(updatedData);
}
};
const handleCustomLogicChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target.value;
setCustomLogic(value);
if (onChange) {
onChange({ customLogic: value });
}
};
const handleAddRule = () => {
const newId = `${rules.length + 1}`;
const newRule = { id: newId, type: '', config: {} };
setRules([...rules, newRule]);
if (onChange) {
onChange({ rules: [...rules, newRule] });
}
};
const handleRemoveRule = (id: string) => {
// 如果只有一个规则,不允许删除
if (rules.length <= 1) {
return;
}
const newRules = rules.filter(rule => rule.id !== id);
setRules(newRules);
// 重新编号规则
const reindexedRules = newRules.map((rule, index) => ({
...rule,
id: `${index + 1}`
}));
setRules(reindexedRules);
if (onChange) {
onChange({ rules: reindexedRules });
}
};
// 处理规则类型变更
const handleRuleTypeChange = (id: string, type: string) => {
const newRules = rules.map(rule => {
if (rule.id === id) {
// 为新类型初始化配置
let initialConfig: Record<string, unknown> = {};
// 如果类型没变,保留原配置
if (type === rule.type) {
initialConfig = { ...rule.config };
} else {
// 根据类型设置初始配置
switch(type) {
case 'exists':
initialConfig = {
fields: [],
logic: 'and',
availableFields: availableFields
};
break;
case 'consistency':
initialConfig = {
pairs: [],
logic: 'and',
availableFields: availableFields
};
break;
case 'format':
initialConfig = {
field: '',
formatType: '',
parameters: '',
availableFields: availableFields
};
break;
case 'logic':
initialConfig = {
conditions: [],
logic: 'and',
availableFields: availableFields
};
break;
case 'regex':
initialConfig = {
field: '',
pattern: '',
matchType: 'match',
availableFields: availableFields
};
break;
case 'ai':
initialConfig = {
model: 'qwen14b',
temperature: 0.1,
prompt: '',
availableFields: availableFields
};
break;
case 'code':
initialConfig = {
language: 'javascript',
code: '',
availableFields: availableFields
};
break;
default:
initialConfig = {
availableFields: availableFields
};
}
}
return {
...rule,
type,
config: initialConfig
};
}
return rule;
});
setRules(newRules);
if (onChange) {
onChange({ rules: newRules });
}
// 更新评查配置
generateEvaluationConfig();
};
// 处理规则配置变更
const handleRuleConfigChange = (id: string, configChanges: Record<string, unknown>) => {
const newRules = rules.map(rule => {
if (rule.id === id) {
return { ...rule, config: { ...rule.config, ...configChanges } };
}
return rule;
});
setRules(newRules);
if (onChange) {
onChange({ rules: newRules });
}
// 更新评查配置
generateEvaluationConfig();
};
// 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示
const renderFieldTags = (ruleId: string, config: Record<string, unknown>) => {
// 获取规则的当前已选字段
const selectedFields = Array.isArray(config.selectedFields) ? config.selectedFields as string[] : [];
// 优先使用配置中存储的可用字段,如果没有则使用当前可用字段
const fieldsToRender = Array.isArray(config.availableFields) ?
config.availableFields as string[] :
availableFields;
return (
<div className="field-tags">
{fieldsToRender.map((field, index) => (
<div
key={`field-${ruleId}-${index}`}
className={`field-tag ${selectedFields.includes(field) ? 'selected' : ''}`}
onClick={() => {
// 切换选中状态
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)
? selectedFields.filter(f => f !== field)
: [...selectedFields, field];
handleRuleConfigChange(ruleId, {
selectedFields: newSelectedFields
});
}
}}
role="button"
tabIndex={0}
>
{field}
</div>
))}
</div>
);
};
// 获取规则类型的Badge样式
const getRuleTypeBadgeClass = (type: string) => {
switch(type) {
case 'exists': return 'bg-green-500';
case 'consistency': return 'bg-blue-500';
case 'format': return 'bg-purple-500';
case 'logic': return 'bg-yellow-500';
case 'regex': return 'bg-red-500';
case 'ai': return 'bg-indigo-500';
case 'code': return 'bg-gray-700';
default: return 'bg-primary';
}
};
// 渲染规则配置区域
const renderRuleConfig = (rule: RuleType) => {
const { id, type, config } = rule;
// 如果规则中的availableFields不是最新的,则更新它
if (type && config && (!config.availableFields ||
(Array.isArray(config.availableFields) &&
!availableFields.every((field) => (config.availableFields as string[]).includes(field)) ||
!(config.availableFields as string[]).every((field) => availableFields.includes(field))))) {
// 延迟更新以避免在渲染过程中修改状态
setTimeout(() => {
console.log('Updating rule config with new available fields:', availableFields);
const updatedConfig = { ...config, availableFields: availableFields };
handleRuleConfigChange(id, updatedConfig);
}, 0);
}
if (!type) {
return (
<div className="rule-placeholder text-center py-6 text-secondary">
<i className="ri-settings-3-line text-4xl mb-2 block"></i>
<p></p>
</div>
);
}
switch(type) {
case 'exists':
return (
<div className="config-section">
<div className="mb-4">
<label className="form-label" htmlFor={`exists-fields-${id}`}> <span className="required-mark">*</span></label>
<div className="field-tags-container exists-fields-container" id={`exists-fields-container-${id}`}>
{renderFieldTags(id, config)}
</div>
<div className="form-tip mt-2"></div>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`existsLogic-all-${id}`}> <span className="required-mark">*</span></label>
<div className="form-radio-group">
<label className="form-radio-item">
<input
type="radio"
id={`existsLogic-all-${id}`}
name={`existsLogic_${id}`}
className="form-radio"
value="all"
defaultChecked
onChange={(e) => handleRuleConfigChange(id, { existsLogic: e.target.value })}
/>
<span></span>
</label>
<label className="form-radio-item">
<input
type="radio"
id={`existsLogic-any-${id}`}
name={`existsLogic_${id}`}
className="form-radio"
value="any"
onChange={(e) => handleRuleConfigChange(id, { existsLogic: e.target.value })}
/>
<span></span>
</label>
</div>
</div>
</div>
);
case 'consistency':
return (
<div className="config-section">
<div className="mb-4">
<label className="form-label" htmlFor={`consistency-fields-container-${id}`}> <span className="required-mark">*</span></label>
<div id={`consistency-fields-container-${id}`}>
{Array.isArray(config.pairs) && config.pairs.length > 0 ? (
config.pairs.map((pair, pairIndex) => (
<div key={`pair-${id}-${pairIndex}`} className="bg-gray-50 p-4 rounded-md mb-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div>
<label className="form-label text-sm" htmlFor={`source-field-${id}-${pairIndex}`}></label>
<select
id={`source-field-${id}-${pairIndex}`}
className="form-select"
value={pair.sourceField || ''}
onChange={(e) => {
const updatedPairs = [...(config.pairs as ComparisonPair[])];
updatedPairs[pairIndex] = {...pair, sourceField: e.target.value};
handleRuleConfigChange(id, { pairs: updatedPairs });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`src-${pairIndex}-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`target-field-${id}-${pairIndex}`}></label>
<select
id={`target-field-${id}-${pairIndex}`}
className="form-select"
value={pair.targetField || ''}
onChange={(e) => {
const updatedPairs = [...(config.pairs as ComparisonPair[])];
updatedPairs[pairIndex] = {...pair, targetField: e.target.value};
handleRuleConfigChange(id, { pairs: updatedPairs });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`tgt-${pairIndex}-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`compare-method-${id}-${pairIndex}`}> <span className="required-mark">*</span></label>
<select
id={`compare-method-${id}-${pairIndex}`}
className="form-select"
value={pair.compareMethod || ''}
onChange={(e) => {
const updatedPairs = [...(config.pairs as ComparisonPair[])];
updatedPairs[pairIndex] = {...pair, compareMethod: e.target.value};
handleRuleConfigChange(id, { pairs: updatedPairs });
}}
>
<option value=""></option>
<option value="exact"></option>
<option value="contains"></option>
<option value="semantic"></option>
</select>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
className="text-red-500 hover:text-red-700"
onClick={() => {
const updatedPairs = [...(config.pairs as ComparisonPair[])];
updatedPairs.splice(pairIndex, 1);
handleRuleConfigChange(id, { pairs: updatedPairs });
}}
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
</div>
))
) : (
<div className="bg-gray-50 p-4 rounded-md mb-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div>
<label className="form-label text-sm" htmlFor={`source-field-${id}-0`}></label>
<select
id={`source-field-${id}-0`}
className="form-select"
onChange={(e) => {
// 直接初始化一个完整的比较对数组
const firstPair = { sourceField: e.target.value, targetField: '', compareMethod: '' };
handleRuleConfigChange(id, { pairs: [firstPair] });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`src-default-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`target-field-${id}-0`}></label>
<select
id={`target-field-${id}-0`}
className="form-select"
onChange={(e) => {
// 获取sourceField的值
const sourceField = document.getElementById(`source-field-${id}-0`) ?
(document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : '';
const firstPair = { sourceField, targetField: e.target.value, compareMethod: '' };
handleRuleConfigChange(id, { pairs: [firstPair] });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`tgt-default-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`compare-method-${id}-0`}> <span className="required-mark">*</span></label>
<select
id={`compare-method-${id}-0`}
className="form-select"
onChange={(e) => {
// 获取sourceField和targetField的值
const sourceField = document.getElementById(`source-field-${id}-0`) ?
(document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : '';
const targetField = document.getElementById(`target-field-${id}-0`) ?
(document.getElementById(`target-field-${id}-0`) as HTMLSelectElement).value : '';
const firstPair = { sourceField, targetField, compareMethod: e.target.value };
handleRuleConfigChange(id, { pairs: [firstPair] });
}}
>
<option value=""></option>
<option value="exact"></option>
<option value="contains"></option>
<option value="semantic"></option>
</select>
</div>
</div>
</div>
)}
</div>
<div>
<button
type="button"
className="ant-btn ant-btn-default"
onClick={() => {
// 添加新的比较对
// 直接获取当前的pairs数组,或初始化为空数组
const pairs = Array.isArray(config.pairs) ? [...(config.pairs as ComparisonPair[])] : [];
// 创建新的空白比较对
const newPair = { sourceField: '', targetField: '', compareMethod: '' };
// 如果数组为空,确保先初始化第一个条目
if (pairs.length === 0) {
// 如果界面上已有值,则添加两行:一行是当前值,一行是新的空行
const sourceField = document.getElementById(`source-field-${id}-0`) ?
(document.getElementById(`source-field-${id}-0`) as HTMLSelectElement).value : '';
const targetField = document.getElementById(`target-field-${id}-0`) ?
(document.getElementById(`target-field-${id}-0`) as HTMLSelectElement).value : '';
const compareMethod = document.getElementById(`compare-method-${id}-0`) ?
(document.getElementById(`compare-method-${id}-0`) as HTMLSelectElement).value : '';
// 将第一行设置为当前值(如果有)
pairs.push({ sourceField, targetField, compareMethod });
}
// 无论如何,都添加一个新的空白行
pairs.push(newPair);
// 更新配置
handleRuleConfigChange(id, { pairs });
}}
>
<i className="ri-add-line mr-1"></i>
</button>
</div>
</div>
<div className="mb-4">
<fieldset>
<legend className="form-label"> <span className="required-mark">*</span></legend>
<div className="form-radio-group">
<label className="form-radio-item">
<input
type="radio"
name={`logicRelation_${id}`}
className="form-radio"
value="and"
defaultChecked
onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })}
/>
<span>AND</span>
</label>
<label className="form-radio-item">
<input
type="radio"
name={`logicRelation_${id}`}
className="form-radio"
value="or"
onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })}
/>
<span>OR</span>
</label>
</div>
</fieldset>
</div>
</div>
);
case 'logic':
return (
<div className="config-section">
<div className="mb-4">
<label className="form-label" htmlFor={`conditions-container-${id}`}> <span className="required-mark">*</span></label>
<div className="conditions-container" id={`conditions-container-${id}`}>
{Array.isArray(config.conditions) && config.conditions.length > 0 ? (
config.conditions.map((condition, conditionIndex) => (
<div key={`condition-${id}-${conditionIndex}`} className="condition-row bg-gray-50 p-4 rounded-md mb-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div>
<label className="form-label text-sm" htmlFor={`field-${id}-${conditionIndex}`}></label>
<select
id={`field-${id}-${conditionIndex}`}
className="form-select"
value={condition.field || ''}
onChange={(e) => {
const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], field: e.target.value };
handleRuleConfigChange(id, { conditions: currentConditions });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`field-${conditionIndex}-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`operator-${id}-${conditionIndex}`}></label>
<select
id={`operator-${id}-${conditionIndex}`}
className="form-select"
value={condition.operator || 'eq'}
onChange={(e) => {
const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], operator: e.target.value };
handleRuleConfigChange(id, { conditions: currentConditions });
}}
>
<option value="eq"> (=)</option>
<option value="neq"> ()</option>
<option value="gt"> {`>`}</option>
<option value="gte"> {``}</option>
<option value="lt"> {`<`}</option>
<option value="lte"> {``}</option>
<option value="contains"></option>
<option value="not_contains"></option>
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`value-${id}-${conditionIndex}`}></label>
<input
type="text"
id={`value-${id}-${conditionIndex}`}
className="form-input"
value={condition.value || ''}
placeholder="请输入比较值"
onChange={(e) => {
const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
currentConditions[conditionIndex] = { ...currentConditions[conditionIndex], value: e.target.value };
handleRuleConfigChange(id, { conditions: currentConditions });
}}
/>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
className="text-red-500 hover:text-red-700"
onClick={() => {
const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
currentConditions.splice(conditionIndex, 1);
handleRuleConfigChange(id, { conditions: currentConditions });
}}
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
</div>
))
) : (
<div className="condition-row bg-gray-50 p-4 rounded-md mb-2">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-2">
<div>
<label className="form-label text-sm" htmlFor={`field-${id}-0`}></label>
<select
id={`field-${id}-0`}
className="form-select"
onChange={(e) => {
// 直接初始化一个完整的条件对象
const firstCondition = { field: e.target.value, operator: 'eq', value: '' };
handleRuleConfigChange(id, { conditions: [firstCondition] });
}}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`field-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`operator-${id}-0`}></label>
<select
id={`operator-${id}-0`}
className="form-select"
onChange={(e) => {
// 获取field的值
const field = document.getElementById(`field-${id}-0`) ?
(document.getElementById(`field-${id}-0`) as HTMLSelectElement).value : '';
const firstCondition = { field, operator: e.target.value, value: '' };
handleRuleConfigChange(id, { conditions: [firstCondition] });
}}
>
<option value="eq"> (=)</option>
<option value="neq"> ()</option>
<option value="gt"> {`>`}</option>
<option value="gte"> {``}</option>
<option value="lt"> {`<`}</option>
<option value="lte"> {``}</option>
<option value="contains"></option>
<option value="not_contains"></option>
</select>
</div>
<div>
<label className="form-label text-sm" htmlFor={`value-${id}-0`}></label>
<input
type="text"
id={`value-${id}-0`}
className="form-input"
placeholder="请输入比较值"
onChange={(e) => {
// 获取field和operator的值
const field = document.getElementById(`field-${id}-0`) ?
(document.getElementById(`field-${id}-0`) as HTMLSelectElement).value : '';
const operator = document.getElementById(`operator-${id}-0`) ?
(document.getElementById(`operator-${id}-0`) as HTMLSelectElement).value : 'eq';
const firstCondition = { field, operator, value: e.target.value };
handleRuleConfigChange(id, { conditions: [firstCondition] });
}}
/>
</div>
</div>
<div className="flex justify-end">
<button
type="button"
className="text-red-500 hover:text-red-700"
onClick={() => {
const currentConditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
if (currentConditions.length > 1) {
currentConditions.splice(0, 1);
handleRuleConfigChange(id, { conditions: currentConditions });
}
}}
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
</div>
)}
</div>
<div className="mt-2">
<button
type="button"
className="ant-btn ant-btn-default"
onClick={() => {
// 添加新的条件
// 直接获取当前的conditions数组,或初始化为空数组
const conditions = Array.isArray(config.conditions) ? [...(config.conditions as Condition[])] : [];
// 创建新的空白条件
const newCondition = { field: '', operator: 'eq', value: '' };
// 如果数组为空,确保先初始化第一个条目
if (conditions.length === 0) {
// 如果界面上已有值,则添加两行:一行是当前值,一行是新的空行
const field = document.getElementById(`field-${id}-0`) ?
(document.getElementById(`field-${id}-0`) as HTMLSelectElement).value : '';
const operator = document.getElementById(`operator-${id}-0`) ?
(document.getElementById(`operator-${id}-0`) as HTMLSelectElement).value : 'eq';
const value = document.getElementById(`value-${id}-0`) ?
(document.getElementById(`value-${id}-0`) as HTMLInputElement).value : '';
// 将第一行设置为当前值(如果有)
conditions.push({ field, operator, value });
}
// 无论如何,都添加一个新的空白行
conditions.push(newCondition);
// 更新配置
handleRuleConfigChange(id, { conditions });
}}
>
<i className="ri-add-line mr-1"></i>
</button>
</div>
</div>
<div className="mb-4">
<fieldset>
<legend className="form-label"> <span className="required-mark">*</span></legend>
<div className="form-radio-group">
<label className="form-radio-item">
<input
type="radio"
name={`logicRelation_${id}`}
className="form-radio"
value="and"
defaultChecked
onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })}
/>
<span>AND</span>
</label>
<label className="form-radio-item">
<input
type="radio"
name={`logicRelation_${id}`}
className="form-radio"
value="or"
onChange={(e) => handleRuleConfigChange(id, { logicRelation: e.target.value })}
/>
<span>OR</span>
</label>
</div>
</fieldset>
</div>
</div>
);
case 'regex':
return (
<div className="config-section">
<div className="mb-4">
<label className="form-label" htmlFor={`regex-field-${id}`}> <span className="required-mark">*</span></label>
<select
id={`regex-field-${id}`}
className="form-select"
onChange={(e) => handleRuleConfigChange(id, { checkField: e.target.value })}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`regex-field-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`regex-pattern-${id}`}> <span className="required-mark">*</span></label>
<textarea
id={`regex-pattern-${id}`}
className="form-textarea"
placeholder="请输入正则表达式"
onChange={(e) => handleRuleConfigChange(id, { regexPattern: e.target.value })}
></textarea>
</div>
<div className="mb-4">
<fieldset>
<legend className="form-label"> <span className="required-mark">*</span></legend>
<div className="form-radio-group">
<label className="form-radio-item">
<input
type="radio"
name={`regexMatchType_${id}`}
className="form-radio"
value="match"
defaultChecked
onChange={(e) => handleRuleConfigChange(id, { matchType: e.target.value })}
/>
<span></span>
</label>
<label className="form-radio-item">
<input
type="radio"
name={`regexMatchType_${id}`}
className="form-radio"
value="not_match"
onChange={(e) => handleRuleConfigChange(id, { matchType: e.target.value })}
/>
<span></span>
</label>
</div>
</fieldset>
</div>
</div>
);
case 'ai':
return (
<div className="config-section">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label className="form-label" htmlFor={`ai-model-${id}`}>
<span className="required-mark">*</span>
</label>
<select
id={`ai-model-${id}`}
className="form-select"
onChange={(e) => handleRuleConfigChange(id, { model: e.target.value })}
>
<option value="deepseek">DeepSeek</option>
<option value="qwen72b">Qwen72B-VL</option>
<option value="qwen14b">Qwen14B</option>
</select>
</div>
<div>
<label className="form-label" htmlFor={`ai-temp-${id}`}></label>
<input
type="number"
id={`ai-temp-${id}`}
className="form-input"
placeholder="0.1"
defaultValue="0.1"
min="0"
max="1"
step="0.1"
onChange={(e) => handleRuleConfigChange(id, { temperature: e.target.value })}
/>
</div>
<div className="col-span-1 md:col-span-2">
<label className="form-label" htmlFor={`ai-prompt-${id}`}> Prompt <span className="required-mark">*</span></label>
<textarea
id={`ai-prompt-${id}`}
className="form-textarea"
placeholder="请输入提示词,引导模型进行判断"
defaultValue={`请判断以下{字段}内容是否符合规范要求,仅回答"符合"或"不符合",并简要说明理由。
{字段内容}`}
onChange={(e) => handleRuleConfigChange(id, { prompt: e.target.value })}
></textarea>
</div>
<div className="col-span-1 md:col-span-2 flex flex-wrap gap-2 mt-2">
{availableFields.map((field, idx) => (
<button
key={`tag-${idx}`}
type="button"
className="ant-btn ant-btn-default tag-button"
onClick={() => {
// 将标签插入到文本区域
const textArea = document.getElementById(`ai-prompt-${id}`) as HTMLTextAreaElement;
if (textArea) {
const startPos = textArea.selectionStart;
const endPos = textArea.selectionEnd;
const textBefore = textArea.value.substring(0, startPos);
const textAfter = textArea.value.substring(endPos);
const tagText = `{${field}}`;
textArea.value = textBefore + tagText + textAfter;
textArea.focus();
textArea.selectionStart = startPos + tagText.length;
textArea.selectionEnd = startPos + tagText.length;
// 更新配置
handleRuleConfigChange(id, { prompt: textArea.value });
}
}}
>
{field}
</button>
))}
</div>
</div>
</div>
);
case 'code':
return (
<div className="config-section">
<div className="mb-4">
<fieldset>
<legend className="form-label"> <span className="required-mark">*</span></legend>
<div className="form-radio-group">
<label className="form-radio-item">
<input
type="radio"
name={`codeLanguage_${id}`}
className="form-radio"
value="javascript"
defaultChecked
onChange={(e) => handleRuleConfigChange(id, { language: e.target.value })}
/>
<span>JavaScript</span>
</label>
<label className="form-radio-item">
<input
type="radio"
name={`codeLanguage_${id}`}
className="form-radio"
value="python"
onChange={(e) => handleRuleConfigChange(id, { language: e.target.value })}
/>
<span>Python</span>
</label>
</div>
</fieldset>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`code-editor-${id}`}> <span className="required-mark">*</span></label>
<SimpleCodeEditor
id={`code-editor-${id}`}
language={rule.config.language as 'javascript' | 'python' || 'javascript'}
onChange={(value) => handleRuleConfigChange(id, { code: value })}
/>
</div>
</div>
);
case 'format':
return (
<div className="config-section">
<div className="mb-4">
<label className="form-label" htmlFor={`format-field-${id}`}> <span className="required-mark">*</span></label>
<select
id={`format-field-${id}`}
className="form-select"
onChange={(e) => handleRuleConfigChange(id, { checkField: e.target.value })}
>
<option value=""></option>
{availableFields.map((field, idx) => (
<option key={`format-field-${idx}`} value={field}>{field}</option>
))}
</select>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`format-type-${id}`}> <span className="required-mark">*</span></label>
<select
id={`format-type-${id}`}
className="form-select format-type"
onChange={(e) => handleRuleConfigChange(id, { formatType: e.target.value })}
>
<option value=""></option>
<option value="date"></option>
<option value="number"></option>
<option value="phone"></option>
<option value="email"></option>
<option value="bankcard"></option>
<option value="idcard"></option>
<option value="zipcode"></option>
<option value="uscc"></option>
</select>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`format-params-${id}`}></label>
<input
type="text"
id={`format-params-${id}`}
className="form-input"
placeholder="请输入参数设置"
onChange={(e) => handleRuleConfigChange(id, { formatParams: e.target.value })}
/>
</div>
</div>
);
default:
return (
<div className="p-4 bg-gray-50 rounded">
<p> {type} </p>
</div>
);
}
};
// 生成完整的评查配置数据并在提交保存时使用
const generateEvaluationConfig = () => {
const config = {
logicType: combinationLogic,
customLogic: combinationLogic === 'custom' ? customLogic : '',
rules: rules.map(rule => ({
id: rule.id,
type: rule.type,
config: rule.config
}))
};
if (onChange) {
onChange({ evaluation_config: config });
}
return config;
};
// 处理评查结果消息变更
const handleMessageChange = (type: string, value: string) => {
switch(type) {
case 'pass':
setPassMessage(value);
break;
case 'fail':
setFailMessage(value);
break;
case 'suggest':
setSuggestMessage(value);
break;
}
if (onChange) {
onChange({ [`${type}_message`]: value });
}
};
// 处理严重程度变更
const handleSeverityChange = (value: string) => {
setSuggestionMessageType(value);
if (onChange) {
onChange({ suggestion_message_type: value });
}
};
return (
<div className="ant-card">
<div className="ant-card-header">
<h3></h3>
</div>
<div className="ant-card-body">
<div className="mb-6">
<div className="form-section mb-6">
<div className="form-section-title mb-2">
<label className="form-label" htmlFor="logic-section"> <span className="required-mark">*</span></label>
</div>
<div id="logic-section">
<div className="form-radio-group mb-3">
<label className="form-radio-item">
<input
type="radio"
name="combination-logic"
className="form-radio"
checked={combinationLogic === 'and'}
onChange={() => handleLogicChange('and')}
/>
<span>AND</span>
</label>
<label className="form-radio-item">
<input
type="radio"
name="combination-logic"
className="form-radio"
checked={combinationLogic === 'or'}
onChange={() => handleLogicChange('or')}
/>
<span>OR</span>
</label>
<label className="form-radio-item">
<input
type="radio"
name="combination-logic"
className="form-radio"
checked={combinationLogic === 'custom'}
onChange={() => handleLogicChange('custom')}
/>
<span></span>
</label>
</div>
{showCustomLogic && (
<div className="mt-3 mb-4">
<label className="form-label" htmlFor="custom-logic-expression"> <span className="required-mark">*</span></label>
<textarea
id="custom-logic-expression"
className="form-textarea"
placeholder="请输入自定义组合逻辑,例如:(规则1 AND 规则2) OR 规则3"
value={customLogic}
onChange={handleCustomLogicChange}
></textarea>
<div className="form-tip">
使(ANDORNOT)
</div>
</div>
)}
</div>
</div>
<div className="form-section mb-6">
<div className="form-section-title mb-2">
<label className="form-label" htmlFor="rules-container"></label>
</div>
<div id="rules-container">
<div className="flex items-center justify-between mb-2">
<div> <span className="text-blue-600 font-medium">{rules.length}</span> </div>
</div>
{rules.length === 0 ? (
<div className="border border-dashed border-gray-300 rounded-md p-6 text-center text-gray-500 bg-gray-50">
<div className="flex flex-col items-center justify-center">
<i className="ri-file-list-3-line text-4xl mb-2"></i>
<p></p>
<p className="text-xs mt-1">&ldquo;&rdquo;</p>
</div>
</div>
) : (
<div id="rule-items-container">
{rules.map((rule) => (
<div
key={rule.id}
className="rule-item border border-dashed border-gray-300 rounded-md p-6 mb-6 relative"
>
<div className="absolute top-3 right-3 flex gap-2">
<span className={`badge rounded-pill ${getRuleTypeBadgeClass(rule.type)} text-white px-2 py-1`}>
#{rule.id}
</span>
<button
className={`${rule.id === '1' && rules.length === 1 ? 'text-gray-400' : 'text-red-500 hover:text-red-700'}`}
onClick={() => handleRemoveRule(rule.id)}
disabled={rule.id === '1' && rules.length === 1}
>
<i className="ri-delete-bin-line"></i>
</button>
</div>
<div className="mb-4">
<label className="form-label" htmlFor={`rule-type-${rule.id}`}>
<span className="required-mark">*</span>
</label>
<select
id={`rule-type-${rule.id}`}
className="form-select rule-type-select"
value={rule.type}
onChange={(e) => handleRuleTypeChange(rule.id, e.target.value)}
>
<option value=""></option>
<option value="exists"></option>
<option value="consistency"></option>
<option value="format"></option>
<option value="logic"></option>
<option value="regex"></option>
<option value="ai"></option>
<option value="code"></option>
</select>
<div className="form-tip"></div>
</div>
<div className="rule-config-container">
{renderRuleConfig(rule)}
</div>
</div>
))}
</div>
)}
</div>
</div>
<div className="flex justify-center">
<button
type="button"
className="ant-btn ant-btn-default"
onClick={handleAddRule}
>
<i className="ri-add-line mr-1"></i>
</button>
</div>
<div className="divider"></div>
{/* 评查结果提示信息 */}
<div className="mb-4">
<h4 className="form-label"></h4>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label className="form-label text-sm" htmlFor="pass-message"></label>
<textarea
id="pass-message"
className="form-textarea"
style={{ height: '80px', minHeight: '60px' }}
placeholder="请输入评查通过时的提示信息"
value={pass_message}
onChange={(e) => handleMessageChange('pass', e.target.value)}
></textarea>
</div>
<div>
<label className="form-label text-sm" htmlFor="fail-message"></label>
<textarea
id="fail-message"
className="form-textarea"
style={{ height: '80px', minHeight: '60px' }}
placeholder="请输入评查不通过时的提示信息"
value={fail_message}
onChange={(e) => handleMessageChange('fail', e.target.value)}
></textarea>
</div>
<div>
<label className="form-label text-sm" htmlFor="suggest-message"></label>
<textarea
id="suggest-message"
className="form-textarea"
style={{ height: '80px', minHeight: '60px' }}
placeholder="请输入对用户的建议信息"
value={suggestion_message}
onChange={(e) => handleMessageChange('suggest', e.target.value)}
></textarea>
</div>
</div>
</div>
{/* 建议信息类别 */}
<div className="mb-4">
<h4 className="form-label"></h4>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 mt-2">
<div
className={`flex items-center cursor-pointer border-l-4 border-blue-500 ${suggestion_message_type === 'info'
? 'bg-blue-100 shadow-md ring-2 ring-blue-200'
: 'bg-blue-50 hover:bg-blue-100'} p-3 rounded-r-md transition-all duration-200`}
onClick={() => handleSeverityChange('info')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSeverityChange('info');
}
}}
role="radio"
aria-checked={suggestion_message_type === 'info'}
tabIndex={0}
>
<input
type="radio"
name="suggestion_message_type"
id="severity-info"
className="severity-radio hidden"
value="info"
checked={suggestion_message_type === 'info'}
onChange={() => handleSeverityChange('info')}
/>
<i className={`ri-information-line text-blue-500 text-xl mr-3 ${suggestion_message_type === 'info' ? 'animate-pulse' : ''}`}></i>
<div>
<div className="font-medium"> (Info)</div>
<div className="text-sm text-gray-500"></div>
</div>
{suggestion_message_type === 'info' && (
<div className="ml-auto">
<i className="ri-check-line text-blue-500 text-lg"></i>
</div>
)}
</div>
<div
className={`flex items-center cursor-pointer border-l-4 border-yellow-500 ${suggestion_message_type === 'warning'
? 'bg-yellow-100 shadow-md ring-2 ring-yellow-200'
: 'bg-yellow-50 hover:bg-yellow-100'} p-3 rounded-r-md transition-all duration-200`}
onClick={() => handleSeverityChange('warning')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSeverityChange('warning');
}
}}
role="radio"
aria-checked={suggestion_message_type === 'warning'}
tabIndex={0}
>
<input
type="radio"
name="suggestion_message_type"
id="severity-warning"
className="severity-radio hidden"
value="warning"
checked={suggestion_message_type === 'warning'}
onChange={() => handleSeverityChange('warning')}
/>
<i className={`ri-alert-line text-yellow-500 text-xl mr-3 ${suggestion_message_type === 'warning' ? 'animate-pulse' : ''}`}></i>
<div>
<div className="font-medium"> (Warning)</div>
<div className="text-sm text-gray-500"></div>
</div>
{suggestion_message_type === 'warning' && (
<div className="ml-auto">
<i className="ri-check-line text-yellow-500 text-lg"></i>
</div>
)}
</div>
<div
className={`flex items-center cursor-pointer border-l-4 border-red-500 ${suggestion_message_type === 'error'
? 'bg-red-100 shadow-md ring-2 ring-red-200'
: 'bg-red-50 hover:bg-red-100'} p-3 rounded-r-md transition-all duration-200`}
onClick={() => handleSeverityChange('error')}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleSeverityChange('error');
}
}}
role="radio"
aria-checked={suggestion_message_type === 'error'}
tabIndex={0}
>
<input
type="radio"
name="suggestion_message_type"
id="severity-error"
className="severity-radio hidden"
value="error"
checked={suggestion_message_type === 'error'}
onChange={() => handleSeverityChange('error')}
/>
<i className={`ri-error-warning-line text-red-500 text-xl mr-3 ${suggestion_message_type === 'error' ? 'animate-pulse' : ''}`}></i>
<div>
<div className="font-medium"> (Error)</div>
<div className="text-sm text-gray-500"></div>
</div>
{suggestion_message_type === 'error' && (
<div className="ml-auto">
<i className="ri-check-line text-red-500 text-lg"></i>
</div>
)}
</div>
</div>
<div className="form-tip mt-2"></div>
</div>
{/* 评查后动作 */}
<div className="mb-4">
<label className="form-label" htmlFor="post-action">
<span className="required-mark">*</span>
</label>
<select
className="form-select"
id="post-action"
value={post_action || 'none'}
onChange={(e) => {
setPostAction(e.target.value);
if (onChange) {
onChange({ post_action: e.target.value });
}
}}
>
<option value="none"></option>
<option value="manual"></option>
<option value="replace"></option>
</select>
</div>
{/* 动作描述区域 */}
{post_action && post_action !== 'none' && (
<div className="mb-4" id="action-config-container">
<label className="form-label" htmlFor="action-config"></label>
<textarea
className="form-textarea"
id="action-config"
placeholder="请输入动作描述,说明评查通过或未通过时的处理方式"
value={action_config}
onChange={(e) => {
setActionConfig(e.target.value);
if (onChange) {
onChange({ action_config: e.target.value });
}
}}
></textarea>
</div>
)}
</div>
</div>
</div>
);
}