保存规则库 YAML 维护改造进展

This commit is contained in:
2026-04-28 22:00:00 +08:00
parent 7b86293263
commit dce5ac0c9a
96 changed files with 36801 additions and 615 deletions
+948
View File
@@ -0,0 +1,948 @@
import { type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { Link, useLoaderData } from '@remix-run/react';
import type React from 'react';
import { useEffect, useMemo, useState } from 'react';
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Table } from '~/components/ui/Table';
import { Tag, type TagColor } from '~/components/ui/Tag';
import { loadRuleYamlPack, loadRuleYamlPacks, type RuleSummary, type RuleYamlPack } from '~/utils/rules-yaml-mock.server';
import { buildRuleYamlPreview, collectDependencyOptions, getDefaultExpandedDependencyGroups, type DependencyOption, type EditableRuleConfig, type ValidationIssue } from '~/utils/rules-config-editor';
import styles from '~/styles/pages/rules_test.css?url';
export const links = () => [
{ rel: 'stylesheet', href: styles }
];
export const meta: MetaFunction = () => [
{ title: '评查点详情 - 智慧法务' }
];
type LoaderData = {
pack: RuleYamlPack;
requestedRuleId: string;
};
type EditorState = { kind: 'rule'; mode: 'create' | 'edit'; id?: string } | null;
type RuleDraft = Pick<RuleSummary, 'id' | 'ruleId' | 'name' | 'group' | 'risk' | 'score' | 'type' | 'logic' | 'subRules' | 'subRuleIds' | 'prompt' | 'description'> & {
checkTypes: string[];
dependencies: string[];
};
function riskColor(risk: string): TagColor {
if (risk === 'high') return 'red';
if (risk === 'medium') return 'orange';
if (risk === 'low') return 'green';
return 'gray';
}
function riskLabel(risk: string): string {
if (risk === 'high') return '高';
if (risk === 'medium') return '中';
if (risk === 'low') return '低';
return risk || '-';
}
function uniqueOptions(values: Array<string | undefined>): string[] {
return Array.from(new Set(values.map(value => value?.trim()).filter(Boolean) as string[]));
}
function uniqueDependencyOptions(options: DependencyOption[]): DependencyOption[] {
const seen = new Set<string>();
return options.filter(option => {
if (!option.value || seen.has(option.value)) {
return false;
}
seen.add(option.value);
return true;
});
}
function ruleKey(rule: Pick<RuleSummary, 'id' | 'ruleId'>): string {
return rule.ruleId || rule.id;
}
function ruleTypeLabel(type: string): string {
const labels: Record<string, string> = {
deterministic: '确定性检查',
ai_rule: '智能语义检查',
rule_group: '规则组合',
llm: '智能语义检查',
manual: '人工复核'
};
return labels[type] ? `${labels[type]} (${type})` : type || '-';
}
function checkTypeLabel(type: string): string {
const labels: Record<string, string> = {
required: '必填',
ai: '智能判断',
contains: '包含',
match: '匹配',
format: '格式',
compare: '比较',
amount_match: '金额一致',
visual: '视觉要素',
assert: '断言'
};
return labels[type] ? `${labels[type]} (${type})` : type;
}
function phaseLabel(phase: string): string {
const labels: Record<string, string> = {
draft: '草稿',
executed: '已执行'
};
return labels[phase] ? `${labels[phase]} (${phase})` : phase;
}
function isStepReferenced(logic: string, stepId: string): boolean {
if (!logic.trim()) return false;
return new RegExp(`(^|[^\\w-])${stepId}([^\\w-]|$)`).test(logic);
}
function fallbackDependencyOption(value: string, optionMap?: Map<string, DependencyOption>): DependencyOption {
if (/^-?\d+(\.\d+)?$/.test(value)) {
return { value, label: value, source: '常量', group: '常量' };
}
if (value.startsWith('derived.')) {
return { value, label: value.replace(/^derived\./, ''), source: '派生字段', group: '派生字段' };
}
if (value.startsWith('visual.')) {
return { value, label: value.replace(/^visual\./, ''), source: '视觉要素引用', group: '视觉要素' };
}
if (value.includes('[*].')) {
return { value, label: value, source: '多实体字段', group: value.split('[*].')[0] };
}
const prefix = value.split('.')[0];
const parent = value.includes('.') ? optionMap?.get(prefix) : undefined;
if (parent) {
return { value, label: value, source: `${parent.source} / 子项未显式定义`, group: parent.group };
}
return {
value,
label: value,
source: '未匹配',
group: '未匹配'
};
}
function makeId(prefix: string): string {
return `${prefix}-${Date.now()}`;
}
function emptyRuleDraft(group = '未分组'): RuleDraft {
return {
id: makeId('rule'),
ruleId: '',
name: '',
group,
risk: 'medium',
score: '1',
type: 'deterministic',
checkTypes: [],
logic: '',
subRules: [],
subRuleIds: [],
prompt: '',
description: '',
dependencies: []
};
}
function issueColor(severity: ValidationIssue['severity']): TagColor {
return severity === 'error' ? 'red' : 'orange';
}
function renderYamlLine(line: string, index: number) {
const indent = line.match(/^\s*/)?.[0] || '';
const content = line.slice(indent.length);
const listMatch = content.match(/^(-\s+)([^:]+:)(.*)$/);
const keyMatch = content.match(/^([^:]+:)(.*)$/);
if (!content) {
return <span key={index} className="yaml-line">&nbsp;</span>;
}
if (content.startsWith('#')) {
return (
<span key={index} className="yaml-line">
<span className="yaml-value">{content}</span>
</span>
);
}
if (listMatch) {
return (
<span key={index} className="yaml-line">
<span className="yaml-indent">{indent}</span>
<span className="yaml-marker">{listMatch[1]}</span>
<span className="yaml-key">{listMatch[2]}</span>
<YamlValue value={listMatch[3]} />
</span>
);
}
if (keyMatch) {
return (
<span key={index} className="yaml-line">
<span className="yaml-indent">{indent}</span>
<span className="yaml-key">{keyMatch[1]}</span>
<YamlValue value={keyMatch[2]} />
</span>
);
}
return (
<span key={index} className="yaml-line">
<span className="yaml-indent">{indent}</span>
<span>{content}</span>
</span>
);
}
function YamlValue({ value }: { value: string }) {
const trimmed = value.trim();
const className = /^'.*'$|^".*"$/.test(trimmed)
? 'yaml-string'
: /^(true|false|null)$/i.test(trimmed)
? 'yaml-boolean'
: /^-?\d+(\.\d+)?$/.test(trimmed)
? 'yaml-number'
: 'yaml-value';
return <span className={className}>{value}</span>;
}
function validateRule(rule: RuleSummary | undefined, dependencyOptions: DependencyOption[]): ValidationIssue[] {
if (!rule) {
return [{
id: 'rule-missing',
severity: 'error',
area: '评查规则',
target: '未找到评查点',
message: '当前链接没有匹配到评查点,请从规则列表重新进入。'
}];
}
const issues: ValidationIssue[] = [];
const dependencyValues = new Set(dependencyOptions.map(option => option.value));
const hasKnownDependency = (dependency: string) => {
if (/^-?\d+(\.\d+)?$/.test(dependency)) return true;
if (dependencyValues.has(dependency)) return true;
const prefix = dependency.split('.')[0];
return dependency.includes('.') && dependencyValues.has(prefix);
};
if (!rule.name.trim()) {
issues.push({
id: `rule-name-${rule.id}`,
severity: 'error',
area: '评查规则',
target: rule.ruleId || rule.id,
message: '评查点名称不能为空。'
});
}
if (!rule.group.trim()) {
issues.push({
id: `rule-group-${rule.id}`,
severity: 'error',
area: '评查规则',
target: rule.name || rule.ruleId || rule.id,
message: '评查点必须选择规则组。'
});
}
if (!rule.score.trim() || rule.score === '-') {
issues.push({
id: `rule-score-${rule.id}`,
severity: 'error',
area: '评查规则',
target: rule.name || rule.ruleId || rule.id,
message: '评查点必须设置分值。'
});
}
if ((rule.type === 'ai_rule' || rule.checkTypes.includes('ai')) && !rule.prompt.trim()) {
issues.push({
id: `rule-prompt-${rule.id}`,
severity: 'warning',
area: '评查规则',
target: rule.name || rule.ruleId || rule.id,
message: '智能语义检查建议维护提示词。'
});
}
if (rule.type === 'rule_group' && !rule.logic.trim()) {
issues.push({
id: `rule-group-logic-${rule.id}`,
severity: 'error',
area: '评查规则',
target: rule.name || rule.ruleId || rule.id,
message: '规则组合必须维护逻辑运算式。'
});
}
rule.dependencies.forEach(dependency => {
if (!hasKnownDependency(dependency)) {
issues.push({
id: `rule-dependency-${rule.id}-${dependency}`,
severity: 'warning',
area: '评查规则',
target: rule.name || rule.ruleId || rule.id,
message: `依赖字段【${dependency}】未在当前 YAML 的字段配置或视觉要素中找到。`
});
}
});
return issues;
}
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const packId = url.searchParams.get('packId') || url.searchParams.get('id') || '';
const requestedRuleId = url.searchParams.get('ruleId') || '';
const packs = await loadRuleYamlPacks();
const pack = (packId ? await loadRuleYamlPack(packId) : undefined) || packs[0];
if (!pack) {
throw new Response('未找到 YAML 配置', { status: 404 });
}
return Response.json({ pack, requestedRuleId } satisfies LoaderData);
}
export default function RulesTestDetail() {
const { pack, requestedRuleId } = useLoaderData<typeof loader>() as LoaderData;
const initialRuleKey = requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' });
const [rules, setRules] = useState<RuleSummary[]>(pack.rules);
const [selectedRuleKey, setSelectedRuleKey] = useState(initialRuleKey);
const [editor, setEditor] = useState<EditorState>(null);
const [ruleDraft, setRuleDraft] = useState<RuleDraft>(emptyRuleDraft(pack.rules[0]?.group));
const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false);
const [dependencySearch, setDependencySearch] = useState('');
const [dependencySelection, setDependencySelection] = useState<string[]>([]);
const [expandedDependencyGroups, setExpandedDependencyGroups] = useState<string[]>([]);
const [showValidation, setShowValidation] = useState(false);
const [showYamlPreview, setShowYamlPreview] = useState(false);
const [draftSaved, setDraftSaved] = useState(false);
useEffect(() => {
setRules(pack.rules);
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
setEditor(null);
setDependencyDialogOpen(false);
setDependencySearch('');
setDependencySelection([]);
setExpandedDependencyGroups([]);
setShowValidation(false);
setShowYamlPreview(false);
setDraftSaved(false);
}, [pack.id, requestedRuleId]);
const currentRule = useMemo(() => {
return rules.find(rule => rule.id === selectedRuleKey || rule.ruleId === selectedRuleKey) || rules[0];
}, [rules, selectedRuleKey]);
const editableConfig: EditableRuleConfig = useMemo(() => ({
metadata: pack.metadata,
documentType: pack.documentType,
mainType: pack.mainType,
subtype: pack.subtype,
fields: pack.fields,
subDocuments: pack.subDocuments,
visualElements: pack.visualElements,
rules
}), [pack, rules]);
const dependencyOptions = useMemo(() => collectDependencyOptions(editableConfig), [editableConfig]);
const dependencyOptionMap = useMemo(() => new Map(dependencyOptions.map(option => [option.value, option])), [dependencyOptions]);
const validationIssues = useMemo(() => validateRule(currentRule, dependencyOptions), [currentRule, dependencyOptions]);
const yamlPreview = useMemo(() => currentRule ? buildRuleYamlPreview(editableConfig, currentRule) : '', [currentRule, editableConfig]);
const ruleGroups = useMemo(() => Array.from(new Set(rules.map(rule => rule.group || '未分组'))), [rules]);
const ruleTypeOptions = useMemo(() => uniqueOptions([
...rules.map(rule => rule.type),
'deterministic',
'ai_rule',
'rule_group'
]), [rules]);
const selectedDependencyOptions = useMemo(() => {
return ruleDraft.dependencies.map(value => dependencyOptionMap.get(value) || fallbackDependencyOption(value, dependencyOptionMap));
}, [dependencyOptionMap, ruleDraft.dependencies]);
const currentDependencyRows = useMemo(() => {
return (currentRule?.dependencies || []).map(value => dependencyOptionMap.get(value) || fallbackDependencyOption(value, dependencyOptionMap));
}, [currentRule, dependencyOptionMap]);
const dialogDependencyOptions = useMemo(() => {
const selectedValues = new Set(ruleDraft.dependencies);
return uniqueDependencyOptions([
...selectedDependencyOptions,
...dependencyOptions
]).sort((left, right) => {
const selectedDelta = Number(selectedValues.has(right.value)) - Number(selectedValues.has(left.value));
if (selectedDelta !== 0) return selectedDelta;
return left.label.localeCompare(right.label, 'zh-CN');
});
}, [dependencyOptions, ruleDraft.dependencies, selectedDependencyOptions]);
const filteredDependencyOptions = useMemo(() => {
const keyword = dependencySearch.trim().toLowerCase();
return dialogDependencyOptions.filter(option => {
if (!keyword) return true;
return [option.value, option.label, option.source, option.group]
.some(text => text.toLowerCase().includes(keyword));
});
}, [dialogDependencyOptions, dependencySearch]);
const dependencyGroups = useMemo(() => {
const groups = new Map<string, typeof filteredDependencyOptions>();
filteredDependencyOptions.forEach(option => {
const current = groups.get(option.group) || [];
current.push(option);
groups.set(option.group, current);
});
return Array.from(groups.entries());
}, [filteredDependencyOptions]);
const isDependencySearching = Boolean(dependencySearch.trim());
const defaultExpandedDependencyGroups = useMemo(() => {
return getDefaultExpandedDependencyGroups(dialogDependencyOptions, dependencySelection);
}, [dialogDependencyOptions, dependencySelection]);
const dependencyDialogEmptyText = dependencySearch.trim() ? '没有匹配的字段。' : '当前文档类型暂无可追加字段。';
const hasErrors = validationIssues.some(issue => issue.severity === 'error');
const isSmartRuleDraft = ruleDraft.type === 'ai_rule' || ruleDraft.checkTypes.includes('ai');
const isRuleGroupDraft = ruleDraft.type === 'rule_group';
const rulesById = useMemo(() => new Map(rules.map(rule => [rule.ruleId || rule.id, rule])), [rules]);
const openRuleEditor = (rule?: RuleSummary) => {
setRuleDraft(rule ? { ...rule } : emptyRuleDraft(ruleGroups[0]));
setDependencyDialogOpen(false);
setDependencySearch('');
setDependencySelection(rule?.dependencies || []);
setExpandedDependencyGroups([]);
setEditor({ kind: 'rule', mode: rule ? 'edit' : 'create', id: rule?.id });
};
const openDependencyDialog = () => {
setDependencySelection(ruleDraft.dependencies);
setDependencySearch('');
setExpandedDependencyGroups(getDefaultExpandedDependencyGroups(dialogDependencyOptions, ruleDraft.dependencies));
setDependencyDialogOpen(true);
};
const updateDependencySearch = (value: string) => {
setDependencySearch(value);
if (!value.trim()) {
setExpandedDependencyGroups(defaultExpandedDependencyGroups);
}
};
const toggleDependencyGroup = (group: string) => {
setExpandedDependencyGroups(current => (
current.includes(group)
? current.filter(item => item !== group)
: [...current, group]
));
};
const applyDependencySelection = () => {
setRuleDraft({ ...ruleDraft, dependencies: dependencySelection });
setDependencyDialogOpen(false);
};
const saveRule = () => {
if (!editor || editor.kind !== 'rule') return;
const existingRule = editor.id ? rules.find(rule => rule.id === editor.id) : undefined;
const normalizedRule: RuleSummary = {
...ruleDraft,
id: ruleDraft.id || makeId('rule'),
ruleId: ruleDraft.ruleId || ruleDraft.id,
group: ruleDraft.group || '未分组',
checkTypes: ruleDraft.type === 'ai_rule' ? uniqueOptions([...ruleDraft.checkTypes, 'ai']) : ruleDraft.checkTypes,
appliesIn: existingRule?.appliesIn || [],
scope: existingRule?.scope || [],
stageCount: existingRule?.stageCount || ruleDraft.subRules.length
};
setRules(current => editor.mode === 'edit'
? current.map(rule => rule.id === editor.id ? normalizedRule : rule)
: [...current, normalizedRule]);
setSelectedRuleKey(ruleKey(normalizedRule));
setDraftSaved(true);
setEditor(null);
};
const resetDraft = () => {
setRules(pack.rules);
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
setDependencyDialogOpen(false);
setDependencySearch('');
setDependencySelection([]);
setShowValidation(false);
setShowYamlPreview(false);
setDraftSaved(false);
};
const dependencyColumns = [
{
title: '依赖字段',
key: 'label',
width: '38%',
render: (_: unknown, record: DependencyOption) => (
<div className="rule-name">
<strong>{record.label}</strong>
<span>YAML引用{record.value}</span>
</div>
)
},
{
title: '来源',
dataIndex: 'source' as keyof DependencyOption,
key: 'source',
width: '26%'
},
{
title: '分组',
dataIndex: 'group' as keyof DependencyOption,
key: 'group',
width: '36%'
}
];
const backLink = `/rulesTest/list?documentType=${encodeURIComponent(pack.documentType)}&mainType=${encodeURIComponent(pack.mainType)}&subtype=${encodeURIComponent(pack.subtype)}`;
return (
<div className="rules-test-page rules-page">
<div className="yaml-layout-single">
<Card className="ant-card config-toolbar-card">
<div className="config-toolbar">
<div>
<div className="config-toolbar-title">{currentRule?.name || '未找到评查点'}</div>
<div className="config-toolbar-desc">
{pack.documentType} / {pack.mainType} / {pack.subtype} / {currentRule?.ruleId || '-'}
</div>
</div>
<div className="config-toolbar-actions">
<Link to={backLink} className="ant-btn ant-btn-default">
<i className="ri-arrow-left-line mr-1.5"></i>
</Link>
<button type="button" className="ant-btn ant-btn-default" onClick={() => setShowValidation(current => !current)}>
<i className="ri-shield-check-line mr-1.5"></i>
</button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => setShowYamlPreview(current => !current)}>
<i className="ri-file-code-line mr-1.5"></i>{showYamlPreview ? '收起片段' : 'YAML 片段'}
</button>
<button type="button" className="ant-btn ant-btn-default" onClick={resetDraft}>
<i className="ri-refresh-line mr-1.5"></i>
</button>
<button type="button" className="ant-btn ant-btn-primary" disabled={!currentRule} onClick={() => currentRule && openRuleEditor(currentRule)}>
<i className="ri-edit-line mr-1.5"></i>
</button>
</div>
</div>
{draftSaved && (
<div className="draft-tip">
<i className="ri-checkbox-circle-line"></i>
稿 OSS
</div>
)}
</Card>
{showValidation && (
<Card className="ant-card validation-card" title="评查点校验">
<div className="validation-summary">
<Tag color={hasErrors ? 'red' : 'green'}>{hasErrors ? '存在必改问题' : '可提交验证'}</Tag>
<span> {validationIssues.length} {validationIssues.filter(issue => issue.severity === 'error').length} </span>
</div>
<div className="validation-list">
{validationIssues.length === 0 ? (
<div className="empty-state"></div>
) : validationIssues.map(issue => (
<div key={issue.id} className={`validation-item ${issue.severity}`}>
<Tag color={issueColor(issue.severity)}>{issue.severity === 'error' ? '必改' : '提醒'}</Tag>
<strong>{issue.area}</strong>
<span>{issue.target}</span>
<p>{issue.message}</p>
</div>
))}
</div>
</Card>
)}
{showYamlPreview && currentRule && (
<Card className="ant-card" title="当前评查点 YAML 片段">
<pre className="yaml-source yaml-source-highlighted">
<code>{yamlPreview.split('\n').map(renderYamlLine)}</code>
</pre>
</Card>
)}
{currentRule ? (
<>
<Card className="ant-card" title="评查点定义">
<div className="rule-detail-grid">
<div className="info-box">
<label></label>
<div>{currentRule.ruleId || '-'}</div>
</div>
<div className="info-box">
<label></label>
<div>{currentRule.group || '-'}</div>
</div>
<div className="info-box">
<label></label>
<div>{ruleTypeLabel(currentRule.type)}</div>
</div>
<div className="info-box">
<label></label>
<div>{currentRule.appliesIn.length > 0 ? currentRule.appliesIn.map(phaseLabel).join('、') : '全部阶段'}</div>
</div>
<div className="info-box">
<label></label>
<div><Tag color={riskColor(currentRule.risk)} size="sm">{riskLabel(currentRule.risk)}</Tag></div>
</div>
<div className="info-box">
<label></label>
<div>{currentRule.score || '-'}</div>
</div>
<div className="info-box">
<label></label>
<div>{currentRule.checkTypes.length > 0 ? currentRule.checkTypes.map(checkTypeLabel).join('、') : '-'}</div>
</div>
</div>
{currentRule.description && (
<div className="rule-description-block">
<label></label>
<p>{currentRule.description}</p>
</div>
)}
</Card>
<Card
className="ant-card"
title={`依赖字段 (${currentRule.dependencies.length}项)`}
extra={<Button size="small" type="primary" icon="ri-add-line" onClick={() => openRuleEditor(currentRule)}></Button>}
>
<Table
className="rules-test-table"
columns={dependencyColumns}
dataSource={currentDependencyRows}
rowKey="value"
emptyText={<div className="empty-state"></div>}
/>
</Card>
<Card className="ant-card" title="评查规则">
<div className="rule-content-stack">
{currentRule.subRules.length > 0 && (
<div className="drawer-subsection">
<div className="drawer-subsection-header">
<div>
<strong>{currentRule.type === 'rule_group' ? `子规则与逻辑(${currentRule.subRules.length}步)` : `规则步骤(${currentRule.subRules.length}步)`}</strong>
<span>{currentRule.type === 'rule_group' ? '规则组合只维护子规则编号、内容和逻辑表达式,不在此处维护字段库。' : '展示当前评查点 YAML stages 中的每一个检查步骤。'}</span>
</div>
</div>
<div className="subrule-list">
{currentRule.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<Tag color="gray" size="sm">{subRule.id}</Tag>
<div>
<strong>
{checkTypeLabel(subRule.check)}
{currentRule.logic && (
<Tag color={isStepReferenced(currentRule.logic, subRule.id) ? 'green' : 'orange'} size="sm">
{isStepReferenced(currentRule.logic, subRule.id) ? '参与逻辑' : '未参与逻辑'}
</Tag>
)}
</strong>
<span>{subRule.content}</span>
</div>
</div>
))}
</div>
{currentRule.logic && (
<div className="logic-expression">
<label></label>
<div>{currentRule.logic}</div>
</div>
)}
</div>
)}
{currentRule.type === 'rule_group' && currentRule.subRules.length === 0 && (
<div className="drawer-subsection">
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<div className="subrule-list">
{currentRule.subRuleIds.length > 0 ? currentRule.subRuleIds.map(ruleId => {
const referencedRule = rulesById.get(ruleId);
return (
<div key={ruleId} className="subrule-item">
<Tag color="gray" size="sm">{ruleId}</Tag>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</span>
</div>
</div>
);
}) : (
<div className="drawer-empty"></div>
)}
</div>
<div className="logic-expression">
<label></label>
<div>{currentRule.logic || '-'}</div>
</div>
</div>
)}
{(currentRule.type === 'ai_rule' || currentRule.checkTypes.includes('ai')) && (
<div className="drawer-subsection">
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<pre className="rule-prompt-preview">{currentRule.prompt || '当前评查点尚未维护提示词。'}</pre>
</div>
)}
{currentRule.subRules.length === 0 && currentRule.type !== 'rule_group' && currentRule.type !== 'ai_rule' && !currentRule.checkTypes.includes('ai') && (
<div className="rule-description-block compact">
<label></label>
<p>{currentRule.description || '当前评查点没有额外规则内容。'}</p>
</div>
)}
</div>
</Card>
</>
) : (
<Card className="ant-card">
<div className="empty-state"></div>
</Card>
)}
</div>
{editor && (
<div className="rules-drawer-shell">
<button className="rules-drawer-mask" type="button" aria-label="关闭编辑抽屉" onClick={() => setEditor(null)}></button>
<aside className="rules-drawer" aria-label="评查点编辑">
<div className="rules-drawer-header">
<div>
<h3>{editor.mode === 'edit' ? '编辑评查点' : '新增评查点'}</h3>
<p></p>
</div>
<button type="button" className="drawer-close" onClick={() => setEditor(null)}><i className="ri-close-line"></i></button>
</div>
<div className="drawer-form">
<label>
<span></span>
<input value={ruleDraft.name} onChange={event => setRuleDraft({ ...ruleDraft, name: event.target.value })} placeholder="如:合同金额不得为空" />
</label>
<label>
<span></span>
<input value={ruleDraft.ruleId} onChange={event => setRuleDraft({ ...ruleDraft, ruleId: event.target.value })} placeholder="如:CONTRACT-001" />
</label>
<label>
<span></span>
<select value={ruleDraft.group} onChange={event => setRuleDraft({ ...ruleDraft, group: event.target.value })}>
{uniqueOptions([ruleDraft.group, ...ruleGroups, '未分组']).map(group => (
<option key={group} value={group}>{group}</option>
))}
</select>
</label>
<div className="drawer-grid">
<label>
<span></span>
<select value={ruleDraft.risk} onChange={event => setRuleDraft({ ...ruleDraft, risk: event.target.value })}>
<option value="high"></option>
<option value="medium"></option>
<option value="low"></option>
</select>
</label>
<label>
<span></span>
<input value={ruleDraft.score} onChange={event => setRuleDraft({ ...ruleDraft, score: event.target.value })} />
</label>
</div>
<label>
<span></span>
<select
value={ruleDraft.type}
onChange={event => {
const nextType = event.target.value;
setRuleDraft({
...ruleDraft,
type: nextType,
checkTypes: nextType === 'ai_rule'
? uniqueOptions([...ruleDraft.checkTypes, 'ai'])
: ruleDraft.checkTypes.filter(type => type !== 'ai')
});
}}
>
{uniqueOptions([ruleDraft.type, ...ruleTypeOptions]).map(type => (
<option key={type} value={type}>{ruleTypeLabel(type)}</option>
))}
</select>
</label>
{isSmartRuleDraft && (
<label>
<span></span>
<textarea
className="prompt-editor"
value={ruleDraft.prompt}
onChange={event => setRuleDraft({ ...ruleDraft, prompt: event.target.value })}
placeholder="用自然语言描述智能语义检查的判断标准,可引用 {{字段名}} 或 {{文书名称.字段名}}。"
/>
</label>
)}
{isRuleGroupDraft && (
<div className="drawer-subsection">
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<div className="subrule-list">
{ruleDraft.subRules.length > 0 ? ruleDraft.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<Tag color="gray" size="sm">{subRule.id}</Tag>
<div>
<strong>{checkTypeLabel(subRule.check)}</strong>
<span>{subRule.content}</span>
</div>
</div>
)) : ruleDraft.subRuleIds.length > 0 ? ruleDraft.subRuleIds.map(ruleId => {
const referencedRule = rulesById.get(ruleId);
return (
<div key={ruleId} className="subrule-item">
<Tag color="gray" size="sm">{ruleId}</Tag>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</span>
</div>
</div>
);
}) : (
<div className="drawer-empty"></div>
)}
</div>
<label>
<span></span>
<input
value={ruleDraft.logic}
onChange={event => setRuleDraft({ ...ruleDraft, logic: event.target.value })}
placeholder="如:1 AND 2,或 JK-002 AND JK-005"
/>
</label>
</div>
)}
<label>
<span></span>
<div className="selected-dependencies">
{selectedDependencyOptions.length === 0 ? (
<div className="drawer-empty"></div>
) : selectedDependencyOptions.map(option => (
<Tag
key={option.value}
color="green"
size="sm"
closable
onClose={() => setRuleDraft({
...ruleDraft,
dependencies: ruleDraft.dependencies.filter(dependency => dependency !== option.value)
})}
>
{option.label}
</Tag>
))}
<Button size="small" type="default" icon="ri-add-line" onClick={openDependencyDialog}></Button>
</div>
</label>
<label>
<span></span>
<textarea value={ruleDraft.description} onChange={event => setRuleDraft({ ...ruleDraft, description: event.target.value })} placeholder="用业务语言描述该评查点如何判断" />
</label>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveRule}></Button>
</div>
</div>
</aside>
{dependencyDialogOpen && (
<div className="dependency-dialog-shell" role="dialog" aria-modal="true" aria-label="追加依赖字段">
<button className="dependency-dialog-mask" type="button" aria-label="关闭依赖字段选择" onClick={() => setDependencyDialogOpen(false)}></button>
<div className="dependency-dialog">
<div className="dependency-dialog-header">
<div>
<h3></h3>
<p></p>
</div>
<button type="button" className="drawer-close" onClick={() => setDependencyDialogOpen(false)}><i className="ri-close-line"></i></button>
</div>
<div className="dependency-dialog-search">
<i className="ri-search-line"></i>
<input value={dependencySearch} onChange={event => updateDependencySearch(event.target.value)} placeholder="搜索字段、文书、字段组" />
</div>
<div className="dependency-dialog-body">
{dependencyGroups.length === 0 ? (
<div className="drawer-empty">{dependencyDialogEmptyText}</div>
) : dependencyGroups.map(([group, options]) => {
const isExpanded = isDependencySearching || expandedDependencyGroups.includes(group);
return (
<div key={group} className={`dependency-option-group${isExpanded ? ' expanded' : ''}`}>
<button
type="button"
className="dependency-option-group-title"
aria-expanded={isExpanded}
onClick={() => toggleDependencyGroup(group)}
>
<i className="ri-arrow-right-s-line"></i>
<span>{group}</span>
<em>{options.length} </em>
</button>
{isExpanded && (
<div className="dependency-option-list">
{options.map(option => (
<label key={`${option.group}-${option.value}`} className="dependency-option">
<input
type="checkbox"
checked={dependencySelection.includes(option.value)}
onChange={event => {
const nextSelection = event.target.checked
? uniqueOptions([...dependencySelection, option.value])
: dependencySelection.filter(value => value !== option.value);
setDependencySelection(nextSelection);
}}
/>
<span className="dependency-option-main">
<span className="dependency-option-name">{option.label}</span>
<em>{option.source}</em>
</span>
</label>
))}
</div>
)}
</div>
);
})}
</div>
<div className="dependency-dialog-actions">
<span> {dependencySelection.length} </span>
<div>
<Button type="default" onClick={() => setDependencyDialogOpen(false)}></Button>
<Button type="primary" onClick={applyDependencySelection}></Button>
</div>
</div>
</div>
</div>
)}
</div>
)}
</div>
);
}