aec34d139a
Multiple locations in ReviewSettings.tsx call .includes() or .filter()
on variables that could theoretically be non-arrays:
1. availableFields.filter/.includes - added safeAvailableFields guard
2. newFields.map field.includes('_') - added typeof===string guard + filter
3. (prior fix) cfgAvailableFields includes/every in renderRuleConfig
4. (prior fix) selectedFields includes in renderFieldTags
These prevent TypeError crashes when config objects contain unexpected
types (e.g. {} instead of []) from stale API data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2255 lines
93 KiB
TypeScript
2255 lines
93 KiB
TypeScript
import React, { useState, useEffect, useContext, useCallback, useRef } from 'react';
|
||
import { SimpleCodeEditor } from './SimpleCodeEditor';
|
||
import { RuleContext } from '~/contexts/RuleContext';
|
||
import { processFieldNames, areArraysDifferent, getArrayDifference } from '~/utils';
|
||
|
||
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;
|
||
initialData?: {
|
||
rules?: RuleType[];
|
||
combinationLogic?: string;
|
||
customLogic?: string;
|
||
pass_message?: string;
|
||
fail_message?: string;
|
||
suggestion_message?: string;
|
||
suggestion_message_type?: string;
|
||
post_action?: string;
|
||
action_config?: string;
|
||
score?: number;
|
||
scoreDisplay?: string;
|
||
};
|
||
// 添加选项数据参数
|
||
ruleTypeOptions?: Array<{ value: string; label: string }>;
|
||
logicTypeOptions?: Array<{ value: string; label: string }>;
|
||
logicOperatorOptions?: Array<{ value: string; label: string }>;
|
||
compareMethodOptions?: Array<{ value: string; label: string }>;
|
||
formatTypeOptions?: Array<{ value: string; label: string }>;
|
||
comparisonOperatorOptions?: Array<{ value: string; label: string }>;
|
||
matchTypeOptions?: Array<{ value: string; label: string }>;
|
||
suggestionMessageTypeOptions?: Array<{ value: string; label: string }>;
|
||
postActionOptions?: Array<{ value: string; label: string }>;
|
||
}
|
||
|
||
export function ReviewSettings({
|
||
onChange,
|
||
initialData,
|
||
}: 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 [score, setScore] = useState<number>(0);
|
||
const [scoreDisplay, setScoreDisplay] = 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[]>(
|
||
// 初始化时就处理字段,去掉类型后缀
|
||
processFieldNames(extractionFields || [])
|
||
);
|
||
|
||
// 使用useRef跟踪是否已经初始化过
|
||
const initializedRef = useRef(false);
|
||
// 保存初始数据的引用,用于检测是否有实际变更
|
||
const initialDataRef = useRef<ReviewSettingsProps['initialData'] | null>(null);
|
||
|
||
// 生成评查配置并发送给父组件
|
||
const generateEvaluationConfig = useCallback(() => {
|
||
// 构建评查配置对象
|
||
const evaluationConfig = {
|
||
logicType: combinationLogic,
|
||
customLogic: customLogic,
|
||
rules: rules
|
||
.filter(rule => rule.type) // 过滤掉没有选择类型的规则
|
||
.map(rule => {
|
||
// 处理不同规则类型的特殊配置
|
||
const processedConfig = { ...rule.config };
|
||
|
||
switch(rule.type) {
|
||
case 'exists':
|
||
// 确保fields字段是数组
|
||
if (!Array.isArray(processedConfig.fields)) {
|
||
processedConfig.fields = [];
|
||
}
|
||
|
||
// 确保logic字段有值
|
||
if (!processedConfig.logic) {
|
||
processedConfig.logic = 'and';
|
||
}
|
||
break;
|
||
|
||
case 'consistency':
|
||
// 确保pairs字段是数组
|
||
if (!Array.isArray(processedConfig.pairs)) {
|
||
processedConfig.pairs = [];
|
||
}
|
||
|
||
// 确保logic字段有值
|
||
if (!processedConfig.logic) {
|
||
processedConfig.logic = 'and';
|
||
}
|
||
break;
|
||
|
||
case 'format':
|
||
// 确保field字段正确设置
|
||
if (processedConfig.checkField) {
|
||
processedConfig.field = processedConfig.checkField;
|
||
delete processedConfig.checkField;
|
||
}
|
||
|
||
// 确保field字段有值
|
||
if (!processedConfig.field) {
|
||
processedConfig.field = '';
|
||
}
|
||
|
||
if (processedConfig.formatParams) {
|
||
processedConfig.parameters = processedConfig.formatParams;
|
||
delete processedConfig.formatParams;
|
||
}
|
||
|
||
// 确保formatType字段有值
|
||
if (!processedConfig.formatType) {
|
||
processedConfig.formatType = '';
|
||
}
|
||
|
||
// 确保parameters字段有值
|
||
if (!processedConfig.parameters) {
|
||
processedConfig.parameters = '';
|
||
}
|
||
break;
|
||
|
||
case 'regex':
|
||
// 确保field和pattern字段正确设置
|
||
if (processedConfig.checkField) {
|
||
processedConfig.field = processedConfig.checkField;
|
||
delete processedConfig.checkField;
|
||
}
|
||
|
||
// 确保field字段有值
|
||
if (!processedConfig.field) {
|
||
processedConfig.field = '';
|
||
}
|
||
|
||
if (processedConfig.regexPattern) {
|
||
processedConfig.pattern = processedConfig.regexPattern;
|
||
delete processedConfig.regexPattern;
|
||
}
|
||
|
||
// 确保pattern字段有值
|
||
if (!processedConfig.pattern) {
|
||
processedConfig.pattern = '';
|
||
}
|
||
|
||
// 确保matchType字段有值
|
||
if (!processedConfig.matchType) {
|
||
processedConfig.matchType = 'match';
|
||
}
|
||
break;
|
||
|
||
case 'ai':
|
||
// 确保model字段有值
|
||
if (!processedConfig.model) {
|
||
processedConfig.model = 'qwen14b';
|
||
}
|
||
|
||
// 确保temperature字段是数字
|
||
if (typeof processedConfig.temperature !== 'number') {
|
||
processedConfig.temperature = 0.1;
|
||
}
|
||
|
||
// 确保prompt字段有值
|
||
if (!processedConfig.prompt) {
|
||
processedConfig.prompt = '';
|
||
}
|
||
break;
|
||
|
||
case 'code':
|
||
// 确保language字段有值
|
||
if (!processedConfig.language) {
|
||
processedConfig.language = 'javascript';
|
||
}
|
||
|
||
// 确保code字段有值
|
||
if (!processedConfig.code) {
|
||
processedConfig.code = '';
|
||
}
|
||
break;
|
||
}
|
||
|
||
// 移除辅助用的UI字段
|
||
delete processedConfig.availableFields;
|
||
|
||
return {
|
||
id: rule.id,
|
||
type: rule.type,
|
||
config: processedConfig
|
||
};
|
||
})
|
||
};
|
||
|
||
// 使用setTimeout避免连锁更新
|
||
setTimeout(() => {
|
||
if (onChange) {
|
||
// 仅将一个evaluation_config对象传递给父组件
|
||
onChange({ evaluation_config: evaluationConfig });
|
||
}
|
||
}, 0);
|
||
|
||
return evaluationConfig;
|
||
}, [rules, combinationLogic, customLogic, onChange]);
|
||
|
||
// 加载初始数据
|
||
useEffect(() => {
|
||
// 如果已经初始化过,则跳过此次处理
|
||
if (initializedRef.current) {
|
||
// console.log("ReviewSettings已初始化,跳过后续初始化处理");
|
||
return;
|
||
}
|
||
|
||
// 记录初始化处理
|
||
// console.log("ReviewSettings开始初始化,数据:", initialData);
|
||
|
||
// 保存初始数据引用,用于后续比较
|
||
initialDataRef.current = JSON.parse(JSON.stringify(initialData));
|
||
|
||
// 设置已初始化标记
|
||
initializedRef.current = true;
|
||
|
||
// 只有在有initialData时才进行初始化设置
|
||
if (initialData) {
|
||
// 处理初始规则数据
|
||
if (initialData.rules && Array.isArray(initialData.rules) && initialData.rules.length > 0) {
|
||
// console.log("设置初始规则数据:", initialData.rules);
|
||
|
||
const validRules = initialData.rules.map(rule => {
|
||
// 确保每个规则都有id
|
||
if (!rule.id) {
|
||
rule.id = `rule_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||
}
|
||
|
||
// 确保配置对象存在
|
||
if (!rule.config) {
|
||
rule.config = {};
|
||
}
|
||
|
||
// 添加可用字段
|
||
if (availableFields.length > 0) {
|
||
rule.config.availableFields = availableFields;
|
||
}
|
||
|
||
return rule;
|
||
});
|
||
|
||
// 如果没有规则或规则为空,添加一个默认规则
|
||
if (validRules.length === 0) {
|
||
validRules.push({ id: '1', type: '', config: { availableFields } });
|
||
}
|
||
|
||
setRules(validRules);
|
||
} else {
|
||
// 如果rules为空或不是数组,添加一个默认规则
|
||
setRules([{ id: '1', type: '', config: { availableFields } }]);
|
||
}
|
||
|
||
// 设置组合逻辑
|
||
if (initialData.combinationLogic) {
|
||
setCombinationLogic(initialData.combinationLogic);
|
||
if (initialData.combinationLogic === 'custom') {
|
||
setShowCustomLogic(true);
|
||
}
|
||
}
|
||
|
||
// 设置自定义逻辑
|
||
if (initialData.customLogic) {
|
||
setCustomLogic(initialData.customLogic);
|
||
}
|
||
|
||
// 设置通过/不通过消息
|
||
if (initialData.pass_message) {
|
||
setPassMessage(initialData.pass_message);
|
||
}
|
||
|
||
if (initialData.fail_message) {
|
||
setFailMessage(initialData.fail_message);
|
||
}
|
||
|
||
// 设置建议消息
|
||
if (initialData.suggestion_message) {
|
||
setSuggestMessage(initialData.suggestion_message);
|
||
}
|
||
|
||
if (initialData.suggestion_message_type) {
|
||
setSuggestionMessageType(initialData.suggestion_message_type);
|
||
}
|
||
|
||
// 设置后处理动作
|
||
if (initialData.post_action) {
|
||
setPostAction(initialData.post_action);
|
||
}
|
||
|
||
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, availableFields, generateEvaluationConfig]);
|
||
|
||
// 监听extractionFields的变化
|
||
useEffect(() => {
|
||
if (extractionFields && extractionFields.length > 0) {
|
||
// 使用工具函数处理字段
|
||
const uniqueFields = processFieldNames(extractionFields);
|
||
|
||
// 只在字段列表实际发生变化时更新
|
||
if (areArraysDifferent(uniqueFields, availableFields)) {
|
||
// 检查删除和新增的字段
|
||
const { removed } = getArrayDifference(uniqueFields, availableFields);
|
||
|
||
// 处理删除的字段
|
||
if (removed.length > 0) {
|
||
handleDeletedFields(removed);
|
||
}
|
||
|
||
// 更新可用字段
|
||
setAvailableFields(uniqueFields);
|
||
|
||
// 使用最新的字段更新规则
|
||
updateRulesWithNewFields(uniqueFields);
|
||
}
|
||
}
|
||
}, [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)];
|
||
|
||
// 检查是否有字段被删除(防御性:确保availableFields是数组)
|
||
const safeAvailableFields: string[] = Array.isArray(availableFields) ? availableFields : [];
|
||
const deletedFields = safeAvailableFields.filter(field => !uniqueFields.includes(field));
|
||
|
||
// 处理新增的字段
|
||
const newFields = uniqueFields.filter((field: string) => !safeAvailableFields.includes(field));
|
||
|
||
if (newFields.length > 0 || deletedFields.length > 0) {
|
||
// console.log('Updating fields in checkAndUpdateFields - deleted:', deletedFields, 'new:', newFields);
|
||
// 设置最新的可用字段列表
|
||
setAvailableFields(uniqueFields);
|
||
|
||
// 处理规则中已删除的字段
|
||
if (deletedFields.length > 0) {
|
||
handleDeletedFields(deletedFields);
|
||
}
|
||
|
||
// 使用最新的字段列表更新规则配置
|
||
updateRulesWithNewFields(uniqueFields);
|
||
|
||
return true; // 表示字段已更新
|
||
}
|
||
}
|
||
return false; // 表示字段未更新
|
||
};
|
||
|
||
// 初始化评查配置
|
||
useEffect(() => {
|
||
// 生成并更新评查配置
|
||
generateEvaluationConfig();
|
||
}, [generateEvaluationConfig]);
|
||
|
||
// 处理已删除字段的函数
|
||
const handleDeletedFields = (deletedFields: string[]) => {
|
||
// console.log("处理已删除字段:", deletedFields);
|
||
|
||
// 如果没有删除的字段,则直接返回
|
||
if (!deletedFields || deletedFields.length === 0) return;
|
||
|
||
setRules(prevRules => {
|
||
return prevRules.map(rule => {
|
||
const updatedConfig = { ...rule.config };
|
||
let configModified = false;
|
||
|
||
switch (rule.type) {
|
||
case 'exists':
|
||
// 处理存在性判断规则
|
||
if (Array.isArray(updatedConfig.fields)) {
|
||
const originalLength = (updatedConfig.fields as string[]).length;
|
||
// 从fields列表中移除已删除的字段
|
||
updatedConfig.fields = (updatedConfig.fields as string[]).filter(
|
||
field => !deletedFields.includes(field)
|
||
);
|
||
configModified = originalLength !== (updatedConfig.fields as string[]).length;
|
||
}
|
||
|
||
// 同时处理selectedFields字段(UI显示用)
|
||
if (Array.isArray(updatedConfig.selectedFields)) {
|
||
updatedConfig.selectedFields = (updatedConfig.selectedFields as string[]).filter(
|
||
field => !deletedFields.includes(field)
|
||
);
|
||
}
|
||
break;
|
||
|
||
case 'consistency':
|
||
// 处理一致性判断规则
|
||
if (Array.isArray(updatedConfig.pairs)) {
|
||
const originalLength = (updatedConfig.pairs as ComparisonPair[]).length;
|
||
// 从配对列表中移除包含已删除字段的配对
|
||
updatedConfig.pairs = (updatedConfig.pairs as ComparisonPair[]).filter(
|
||
pair => !deletedFields.includes(pair.sourceField) && !deletedFields.includes(pair.targetField)
|
||
);
|
||
configModified = originalLength !== (updatedConfig.pairs as ComparisonPair[]).length;
|
||
}
|
||
break;
|
||
|
||
case 'format':
|
||
// 处理格式判断规则
|
||
if (updatedConfig.field && deletedFields.includes(updatedConfig.field as string)) {
|
||
updatedConfig.field = '';
|
||
configModified = true;
|
||
}
|
||
|
||
if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) {
|
||
updatedConfig.checkField = '';
|
||
configModified = true;
|
||
}
|
||
break;
|
||
|
||
case 'regex':
|
||
// 处理正则判断规则
|
||
if (updatedConfig.field && deletedFields.includes(updatedConfig.field as string)) {
|
||
updatedConfig.field = '';
|
||
configModified = true;
|
||
}
|
||
|
||
if (updatedConfig.checkField && deletedFields.includes(updatedConfig.checkField as string)) {
|
||
updatedConfig.checkField = '';
|
||
configModified = true;
|
||
}
|
||
break;
|
||
|
||
case 'logic':
|
||
// 处理逻辑判断规则
|
||
if (Array.isArray(updatedConfig.conditions)) {
|
||
const originalLength = (updatedConfig.conditions as Condition[]).length;
|
||
// 从条件列表中移除使用已删除字段的条件
|
||
updatedConfig.conditions = (updatedConfig.conditions as Condition[]).filter(
|
||
condition => !deletedFields.includes(condition.field)
|
||
);
|
||
configModified = originalLength !== (updatedConfig.conditions as Condition[]).length;
|
||
}
|
||
break;
|
||
|
||
default:
|
||
break;
|
||
}
|
||
|
||
// 更新所有规则的可用字段列表
|
||
if (Array.isArray(updatedConfig.availableFields)) {
|
||
updatedConfig.availableFields = (updatedConfig.availableFields as string[]).filter(
|
||
field => !deletedFields.includes(field)
|
||
);
|
||
}
|
||
|
||
// 如果配置有实质性修改,记录日志
|
||
if (configModified) {
|
||
// console.log(`规则(ID: ${rule.id}, 类型: ${rule.type})已清除对已删除字段的引用`);
|
||
}
|
||
|
||
return {
|
||
...rule,
|
||
config: updatedConfig
|
||
};
|
||
});
|
||
});
|
||
|
||
// 在字段删除处理完毕后,触发一次评查配置更新
|
||
// 使用setTimeout确保状态更新完成后再生成配置
|
||
setTimeout(() => {
|
||
generateEvaluationConfig();
|
||
}, 10);
|
||
};
|
||
|
||
// 更新规则配置中的可用字段但保留已选择的字段和规则配置
|
||
const updateRulesWithNewFields = (newFields: string[]) => {
|
||
// 更新每个规则的可用字段列表,但保留现有配置
|
||
setRules(prevRules => {
|
||
return prevRules.map(rule => {
|
||
const updatedConfig = { ...rule.config };
|
||
|
||
// 对所有规则类型都更新availableFields字段
|
||
// 处理字段,只保留字段名,去掉类型后缀(防御性:确保field是字符串)
|
||
const processedFields = newFields
|
||
.filter(field => typeof field === 'string')
|
||
.map(field => {
|
||
if (field.includes('_')) {
|
||
return field.split('_')[0];
|
||
}
|
||
return field;
|
||
});
|
||
|
||
// 去重
|
||
const uniqueFields = [...new Set(processedFields)];
|
||
updatedConfig.availableFields = uniqueFields;
|
||
|
||
// 根据规则类型更新其他相关字段
|
||
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 updateData = {
|
||
combinationLogic: logic,
|
||
// 如果切换到自定义逻辑,同时传递自定义逻辑内容
|
||
customLogic: logic === 'custom' ? customLogic : ''
|
||
};
|
||
|
||
onChange(updateData);
|
||
|
||
// 生成完整的评查配置
|
||
setTimeout(() => generateEvaluationConfig(), 0);
|
||
}
|
||
};
|
||
|
||
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) {
|
||
// 查找原始规则以获取现有配置
|
||
const originalRule = rules.find(r => r.id === id);
|
||
const originalConfig = originalRule ? originalRule.config : {};
|
||
|
||
// 为新类型初始化配置
|
||
let initialConfig: Record<string, unknown> = {};
|
||
|
||
// 如果类型没变,保留原配置
|
||
if (type === rule.type) {
|
||
initialConfig = { ...originalConfig };
|
||
} else {
|
||
// 根据类型设置初始配置
|
||
switch(type) {
|
||
case 'exists':
|
||
initialConfig = {
|
||
fields: Array.isArray(originalConfig.fields) ? originalConfig.fields : [],
|
||
logic: originalConfig.logic || originalConfig.logicRelation || 'and',
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'consistency':
|
||
initialConfig = {
|
||
pairs: Array.isArray(originalConfig.pairs) ? originalConfig.pairs : [],
|
||
logic: originalConfig.logic || originalConfig.logicRelation || 'and',
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'format':
|
||
initialConfig = {
|
||
field: originalConfig.field || '',
|
||
checkField: originalConfig.checkField || originalConfig.field || '',
|
||
formatType: originalConfig.formatType || 'date',
|
||
parameters: originalConfig.parameters || '',
|
||
formatParams: originalConfig.formatParams || originalConfig.parameters || '',
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'logic':
|
||
initialConfig = {
|
||
conditions: Array.isArray(originalConfig.conditions) ? originalConfig.conditions : [],
|
||
logic: originalConfig.logic || originalConfig.logicRelation || 'and',
|
||
initialField: '',
|
||
initialOperator: 'eq',
|
||
initialValue: '',
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'regex':
|
||
initialConfig = {
|
||
field: originalConfig.field || '',
|
||
checkField: originalConfig.checkField || originalConfig.field || '',
|
||
pattern: originalConfig.pattern || '',
|
||
regexPattern: originalConfig.regexPattern || originalConfig.pattern || '',
|
||
matchType: originalConfig.matchType || 'match',
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'ai':
|
||
initialConfig = {
|
||
model: originalConfig.model || 'qwen14b',
|
||
temperature: typeof originalConfig.temperature === 'number' ? originalConfig.temperature : 0.1,
|
||
prompt: originalConfig.prompt || `请判断以下{字段}内容是否符合规范要求,仅回答"符合"或"不符合",并简要说明理由。
|
||
|
||
{字段内容}`,
|
||
availableFields: availableFields
|
||
};
|
||
break;
|
||
case 'code':
|
||
initialConfig = {
|
||
language: originalConfig.language || 'javascript',
|
||
code: originalConfig.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) {
|
||
// 处理特殊的字段映射,确保不同名称的字段保持同步
|
||
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;
|
||
});
|
||
|
||
setRules(newRules);
|
||
|
||
if (onChange) {
|
||
// 立即触发父组件的onChange回调,确保数据能保存到父组件
|
||
onChange({ rules: newRules });
|
||
}
|
||
};
|
||
|
||
// 渲染字段标签,确保已选择的字段即使在新的字段列表中不存在也会显示
|
||
const renderFieldTags = (ruleId: string, config: Record<string, unknown>) => {
|
||
// 获取规则的当前已选字段(防御性:确保总是数组)
|
||
const rawSelected = Array.isArray(config.fields)
|
||
? config.fields
|
||
: (Array.isArray(config.selectedFields) ? config.selectedFields : []);
|
||
const selectedFields: string[] = Array.isArray(rawSelected) ? rawSelected : [];
|
||
|
||
// 优先使用配置中存储的可用字段,如果没有则使用当前可用字段
|
||
const rawFieldsToRender = Array.isArray(config.availableFields)
|
||
? config.availableFields
|
||
: availableFields;
|
||
const fieldsToRender: string[] = Array.isArray(rawFieldsToRender) ? rawFieldsToRender : [];
|
||
|
||
return (
|
||
<div className="field-tags">
|
||
{fieldsToRender.map((field, index) => {
|
||
// 使用includes方法检查选中状态(selectedFields 总是数组,不会抛错)
|
||
const isSelected = selectedFields.includes(field);
|
||
|
||
return (
|
||
<div
|
||
key={`field-${ruleId}-${index}`}
|
||
className={`field-tag ${isSelected ? 'selected' : ''}`}
|
||
onClick={() => {
|
||
// 切换选中状态
|
||
const newSelectedFields = isSelected
|
||
? selectedFields.filter(f => f !== field)
|
||
: [...selectedFields, field];
|
||
|
||
// 更新规则配置
|
||
handleRuleConfigChange(ruleId, {
|
||
fields: newSelectedFields
|
||
});
|
||
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
onKeyDown={(e) => {
|
||
if (e.key === 'Enter' || e.key === ' ') {
|
||
const newSelectedFields = isSelected
|
||
? selectedFields.filter(f => f !== field)
|
||
: [...selectedFields, field];
|
||
|
||
handleRuleConfigChange(ruleId, {
|
||
fields: newSelectedFields
|
||
});
|
||
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}
|
||
}}
|
||
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不是最新的,则更新它
|
||
const cfgAvailableFields = Array.isArray(config.availableFields) ? config.availableFields as string[] : null;
|
||
if (type && config && (!cfgAvailableFields ||
|
||
!availableFields.every((field) => cfgAvailableFields.includes(field)) ||
|
||
!cfgAvailableFields.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={`logic-and-${id}`}>判断逻辑 <span className="required-mark">*</span></label>
|
||
<div className="form-radio-group">
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
id={`logic-and-${id}`}
|
||
name={`logic_${id}`}
|
||
className="form-radio"
|
||
value="and"
|
||
checked={!config.logic || config.logic === 'and'}
|
||
onChange={(e) => {
|
||
// console.log(`[调试] 选择判断逻辑 and,规则ID: ${id}, 当前值: ${config.logic}`);
|
||
handleRuleConfigChange(id, { logic: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>所有字段必须存在</span>
|
||
</label>
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
id={`logic-or-${id}`}
|
||
name={`logic_${id}`}
|
||
className="form-radio"
|
||
value="or"
|
||
checked={config.logic === 'or'}
|
||
onChange={(e) => {
|
||
// console.log(`[调试] 选择判断逻辑 or,规则ID: ${id}, 当前值: ${config.logic}`);
|
||
handleRuleConfigChange(id, { logic: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<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 });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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 });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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 });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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 });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={(config.initialSourceField as string) || ''}
|
||
onChange={(e) => {
|
||
// 创建新的比较对数组并存储当前选择的源字段
|
||
const sourceField = e.target.value;
|
||
handleRuleConfigChange(id, {
|
||
initialSourceField: sourceField,
|
||
pairs: [{ sourceField, targetField: '', compareMethod: '' }]
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={(config.initialTargetField as string) || ''}
|
||
onChange={(e) => {
|
||
// 获取当前选择的源字段和目标字段
|
||
const sourceField = (config.initialSourceField as string) || '';
|
||
const targetField = e.target.value;
|
||
|
||
handleRuleConfigChange(id, {
|
||
initialTargetField: targetField,
|
||
pairs: [{ sourceField, targetField, compareMethod: '' }]
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={(config.initialCompareMethod as string) || ''}
|
||
onChange={(e) => {
|
||
// 获取当前选择的源字段、目标字段和比较方式
|
||
const sourceField = (config.initialSourceField as string) || '';
|
||
const targetField = (config.initialTargetField as string) || '';
|
||
const compareMethod = e.target.value;
|
||
|
||
handleRuleConfigChange(id, {
|
||
initialCompareMethod: compareMethod,
|
||
pairs: [{ sourceField, targetField, compareMethod }]
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
checked={!config.logic && !config.logicRelation ? true : (config.logic === 'and' || config.logicRelation === 'and')}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
logicRelation: e.target.value,
|
||
logic: e.target.value
|
||
});
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>AND(所有条件都满足)</span>
|
||
</label>
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
name={`logicRelation_${id}`}
|
||
className="form-radio"
|
||
value="or"
|
||
checked={config.logic === 'or' || config.logicRelation === 'or'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
logicRelation: e.target.value,
|
||
logic: e.target.value
|
||
});
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<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 });
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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 });
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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 });
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
</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 });
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={(config.initialField as string) || ''}
|
||
onChange={(e) => {
|
||
// 直接初始化一个完整的条件对象
|
||
const field = e.target.value;
|
||
handleRuleConfigChange(id, {
|
||
initialField: field,
|
||
conditions: [{ field, operator: 'eq', value: '' }]
|
||
});
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={(config.initialOperator as string) || 'eq'}
|
||
onChange={(e) => {
|
||
// 获取field和operator的值
|
||
const field = (config.initialField as string) || '';
|
||
const operator = e.target.value;
|
||
|
||
// 如果field已经设置,则创建完整的条件
|
||
if (field) {
|
||
handleRuleConfigChange(id, {
|
||
initialOperator: operator,
|
||
conditions: [{ field, operator, value: '' }]
|
||
});
|
||
} else {
|
||
// 仅保存操作符值
|
||
handleRuleConfigChange(id, { initialOperator: operator });
|
||
}
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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="请输入比较值"
|
||
value={(config.initialValue as string) || ''}
|
||
onChange={(e) => {
|
||
// 获取field和operator的值
|
||
const field = (config.initialField as string) || '';
|
||
const operator = (config.initialOperator as string) || 'eq';
|
||
const value = e.target.value;
|
||
|
||
// 如果已经设置了字段,则创建条件
|
||
if (field) {
|
||
handleRuleConfigChange(id, {
|
||
initialValue: value,
|
||
conditions: [{ field, operator, value }]
|
||
});
|
||
} else {
|
||
// 否则只保存值
|
||
handleRuleConfigChange(id, { initialValue: value });
|
||
}
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
</div>
|
||
</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[])] : [];
|
||
|
||
// 如果数组为空,尝试从初始字段创建条件
|
||
if (conditions.length === 0 && config.initialField) {
|
||
const field = config.initialField as string;
|
||
const operator = (config.initialOperator as string) || 'eq';
|
||
const value = (config.initialValue as string) || '';
|
||
|
||
if (field) {
|
||
conditions.push({ field, operator, value });
|
||
}
|
||
}
|
||
|
||
// 创建新的空白条件
|
||
const newCondition = { field: '', operator: 'eq', value: '' };
|
||
conditions.push(newCondition);
|
||
|
||
// 更新配置
|
||
handleRuleConfigChange(id, { conditions });
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
checked={!config.logic && !config.logicRelation ? true : (config.logic === 'and' || config.logicRelation === 'and')}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
logicRelation: e.target.value,
|
||
logic: e.target.value
|
||
});
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>AND(所有条件都满足)</span>
|
||
</label>
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
name={`logicRelation_${id}`}
|
||
className="form-radio"
|
||
value="or"
|
||
checked={config.logic === 'or' || config.logicRelation === 'or'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
logicRelation: e.target.value,
|
||
logic: e.target.value
|
||
});
|
||
// 触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<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"
|
||
value={config.checkField as string || config.field as string || ''}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
checkField: e.target.value,
|
||
field: e.target.value // 同步更新内部字段
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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>
|
||
<input
|
||
type="text"
|
||
id={`regex-pattern-${id}`}
|
||
className="form-input"
|
||
placeholder="请输入正则表达式"
|
||
value={config.regexPattern as string || config.pattern as string || ''}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
regexPattern: e.target.value,
|
||
pattern: e.target.value // 同步更新内部字段
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<div className="form-tip">
|
||
输入标准正则表达式,例如: "^[a-zA-Z0-9]+$" 表示仅允许字母和数字
|
||
</div>
|
||
</div>
|
||
<div className="mb-4">
|
||
<label className="form-label" htmlFor={`match-type-${id}`}>匹配类型 <span className="required-mark">*</span></label>
|
||
<div className="form-radio-group">
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
id={`match-type-match-${id}`}
|
||
name={`match_type_${id}`}
|
||
className="form-radio"
|
||
value="match"
|
||
checked={!config.matchType || config.matchType === 'match'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { matchType: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>必须匹配(符合为通过)</span>
|
||
</label>
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
id={`match-type-not-match-${id}`}
|
||
name={`match_type_${id}`}
|
||
className="form-radio"
|
||
value="not_match"
|
||
checked={config.matchType === 'not_match'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { matchType: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>不得匹配(不符合为通过)</span>
|
||
</label>
|
||
</div>
|
||
</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"
|
||
value={config.model as string || 'qwen14b'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { model: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={typeof config.temperature === 'number' ? config.temperature : (config.temperature ? parseFloat(String(config.temperature)) : 0.1)}
|
||
min="0"
|
||
max="1"
|
||
step="0.1"
|
||
onChange={(e) => {
|
||
const value = e.target.value;
|
||
const numberValue = value === '' ? 0.1 : parseFloat(value);
|
||
handleRuleConfigChange(id, { temperature: numberValue });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
</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="请输入提示词,引导模型进行判断"
|
||
value={
|
||
typeof config.prompt === 'string' && config.prompt
|
||
? config.prompt
|
||
: ``
|
||
}
|
||
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"
|
||
checked={!config.language || config.language === 'javascript'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { language: e.target.value });
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<span>JavaScript</span>
|
||
</label>
|
||
<label className="form-radio-item">
|
||
<input
|
||
type="radio"
|
||
name={`codeLanguage_${id}`}
|
||
className="form-radio"
|
||
value="python"
|
||
checked={config.language === 'python'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { language: e.target.value });
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<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>
|
||
<div className="form-tip mb-2">编写返回true或false的评查函数,可以使用字段变量进行判断</div>
|
||
<SimpleCodeEditor
|
||
id={`code-editor-${id}`}
|
||
language={rule.config.language as 'javascript' | 'python' || 'javascript'}
|
||
initialValue={rule.config.code as string || ''}
|
||
onChange={(value) => {
|
||
handleRuleConfigChange(id, { code: value });
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
</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"
|
||
value={config.checkField as string || config.field as string || ''}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
checkField: e.target.value,
|
||
field: e.target.value // 同步更新内部字段
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<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"
|
||
value={config.formatType as string || 'date'}
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, { formatType: e.target.value });
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
>
|
||
<option value="">请选择格式类型</option>
|
||
<option value="date">日期格式</option>
|
||
<option value="number">数字格式</option>
|
||
<option value="phone">电话号码</option>
|
||
<option value="email">电子邮箱</option>
|
||
<option value="bank">银行卡号</option>
|
||
<option value="id">身份证号码</option>
|
||
<option value="postal">邮政编码</option>
|
||
<option value="credit">统一社会信用代码</option>
|
||
</select>
|
||
</div>
|
||
<div className="mb-4">
|
||
<label className="form-label" htmlFor={`format-parameters-${id}`}>格式参数</label>
|
||
<input
|
||
type="text"
|
||
id={`format-parameters-${id}`}
|
||
className="form-input"
|
||
value={config.formatParams as string || config.parameters as string || ''}
|
||
placeholder="输入格式参数,如日期格式YYYY-MM-DD"
|
||
onChange={(e) => {
|
||
handleRuleConfigChange(id, {
|
||
formatParams: e.target.value,
|
||
parameters: e.target.value
|
||
});
|
||
// 直接触发配置更新
|
||
generateEvaluationConfig();
|
||
}}
|
||
/>
|
||
<div className="form-tip">
|
||
根据格式类型传入特定参数,如日期格式可传入"YYYY-MM-DD"
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
|
||
default:
|
||
return (
|
||
<div className="p-4 bg-gray-50 rounded">
|
||
<p>已选择 {type} 类型规则,请继续配置。</p>
|
||
</div>
|
||
);
|
||
}
|
||
};
|
||
|
||
// 组件初次渲染后,主动发送一次完整配置数据
|
||
useEffect(() => {
|
||
// 如果有初始数据,在组件挂载后主动发送一次完整规则配置
|
||
if (initialDataRef.current && onChange) {
|
||
// console.log("组件挂载后发送初始完整配置");
|
||
setTimeout(() => generateEvaluationConfig(), 100);
|
||
}
|
||
}, [generateEvaluationConfig, onChange]);
|
||
|
||
// 处理评查结果消息变更
|
||
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) {
|
||
// 使用正确的字段名
|
||
const fieldName = type === 'pass' ? 'pass_message' :
|
||
type === 'fail' ? 'fail_message' :
|
||
'suggestion_message';
|
||
onChange({ [fieldName]: value });
|
||
}
|
||
};
|
||
|
||
// 处理严重程度变更
|
||
const handleSeverityChange = (value: string) => {
|
||
setSuggestionMessageType(value);
|
||
|
||
if (onChange) {
|
||
onChange({ suggestion_message_type: value });
|
||
}
|
||
};
|
||
|
||
// 处理分数变更
|
||
const handleScoreChange = (value: string) => {
|
||
// 保存用户输入的显示值
|
||
setScoreDisplay(value);
|
||
|
||
let scoreValue = 0;
|
||
// 只在值不为空时更新实际分数
|
||
if (value.trim() !== '') {
|
||
const numValue = parseFloat(value);
|
||
if (!isNaN(numValue)) {
|
||
scoreValue = Math.min(Math.max(numValue, 0), 100);
|
||
}
|
||
}
|
||
|
||
// 更新状态
|
||
setScore(scoreValue);
|
||
|
||
// 通知父组件
|
||
if (onChange) {
|
||
onChange({ score: scoreValue });
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="ant-card">
|
||
<style>
|
||
{`
|
||
.field-tags {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 8px;
|
||
margin-top: 8px;
|
||
}
|
||
|
||
.field-tag {
|
||
background-color: #f3f4f6;
|
||
border: 1px solid #e5e7eb;
|
||
border-radius: 4px;
|
||
padding: 4px 8px;
|
||
font-size: 0.875rem;
|
||
cursor: pointer;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.field-tag:hover {
|
||
background-color: #e5e7eb;
|
||
}
|
||
|
||
.field-tag.selected {
|
||
background-color: #1890ff;
|
||
border-color: #096dd9;
|
||
color: white;
|
||
}
|
||
`}
|
||
</style>
|
||
<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}
|
||
onBlur={() => {
|
||
// 确保在失去焦点时也触发更新
|
||
if (onChange) {
|
||
onChange({
|
||
combinationLogic: 'custom',
|
||
customLogic: customLogic
|
||
});
|
||
}
|
||
}}
|
||
></textarea>
|
||
<div className="form-tip">
|
||
使用规则编号和逻辑运算符(AND、OR、NOT)组合
|
||
</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">点击“添加规则”按钮开始创建评查规则</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>
|
||
|
||
{/* 分数设置 */}
|
||
<div className="mb-4">
|
||
<label className="form-label" htmlFor="score">
|
||
分数
|
||
</label>
|
||
<div className="flex items-center">
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
id="score"
|
||
placeholder="请输入分数 (0-100)"
|
||
value={scoreDisplay}
|
||
onChange={(e) => handleScoreChange(e.target.value)}
|
||
onBlur={() => {
|
||
// 在失去焦点时,如果显示值为空,则设置为0
|
||
if (scoreDisplay.trim() === '') {
|
||
setScoreDisplay('0');
|
||
setScore(0);
|
||
if (onChange) {
|
||
onChange({ score: 0 });
|
||
}
|
||
} else {
|
||
// 否则更新为实际分数值的字符串表示
|
||
setScoreDisplay(String(score));
|
||
}
|
||
}}
|
||
/>
|
||
<span className="ml-2 text-gray-600">分</span>
|
||
</div>
|
||
<div className="form-tip">该评查点的分值,范围0-100</div>
|
||
</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>
|
||
);
|
||
}
|