Files
leaudit-platform-frontend/app/routes/rulesTest.detail.tsx
T

949 lines
40 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { 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>
);
}