Files
leaudit-platform-frontend/app/routes/rulesTest.detail.tsx
T
2026-05-07 11:20:55 +08:00

1814 lines
77 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 { json, type ActionFunctionArgs, type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
import { Link, useFetcher, useLoaderData, useRevalidator } from '@remix-run/react';
import type React from 'react';
import { useEffect, useMemo, useRef, 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 { getUserSession } from '~/api/login/auth.server';
import { API_BASE_URL } from '~/config/api-config';
import { loadRuleConfigPack, loadRuleConfigPacks, loadRuleConfigVersions, type RuleVersionItem } from '~/utils/rules-config-packs.server';
import { buildRuleYamlPreview, buildYamlPreview, collectDependencyOptions, getDefaultExpandedDependencyGroups, prepareDraftYamlForSave, serializeEditableRuleConfig, validateEditableRuleConfig, type DependencyOption, type EditableRuleConfig, type ValidationIssue, type VisualElementSummary } from '~/utils/rules-config-editor';
import type { ExtractFieldSummary, RuleSummary, RuleYamlPack, SubDocumentSummary } from '~/utils/rules-yaml-mock.server';
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;
versions: RuleVersionItem[];
};
type ActionData = {
success: boolean;
message: string;
intent: 'save' | 'publish' | 'rollback';
versionId?: number;
versionNo?: string;
};
type EditorState =
| { kind: 'rule'; mode: 'create' | 'edit'; id?: string }
| { kind: 'field'; mode: 'create' | 'edit'; id?: string }
| { kind: 'document'; mode: 'create' | 'edit'; id?: string }
| { kind: 'visual'; 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[];
};
type FieldDraft = ExtractFieldSummary;
type DocumentDraft = SubDocumentSummary;
type VisualDraft = VisualElementSummary;
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 fieldTypeLabel(type: string): string {
const labels: Record<string, string> = {
verbatim: '原文',
string: '文本',
money: '金额',
date: '日期',
enum: '枚举',
number: '数字',
multi_entity: '多实体',
};
return labels[type] || type || '-';
}
function requiredFromLabel(value: string): string {
const labels: Record<string, string> = {
draft: '草稿阶段',
executed: '签署后阶段',
'-': '未限定',
true: '必需',
false: '可选',
conditional: '条件必需',
};
return labels[value] || value || '-';
}
function matchesCurrentRuleDependency(currentRule: RuleSummary | undefined, candidates: Array<string | undefined>): boolean {
const deps = new Set(currentRule?.dependencies || []);
if (deps.size === 0) return false;
return candidates.some((candidate) => {
const value = String(candidate || '').trim();
if (!value) return false;
if (deps.has(value)) return true;
return Array.from(deps).some((dependency) => dependency.startsWith(`${value}.`));
});
}
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 rewriteDependencyPrefix(dependency: string, from: string, to: string): string {
if (!from || !to || from === to) return dependency;
if (dependency === from) return to;
if (dependency.startsWith(`${from}.`)) return `${to}${dependency.slice(from.length)}`;
return dependency;
}
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 emptyFieldDraft(group = '基础信息'): FieldDraft {
return {
id: makeId('field'),
group,
name: '',
type: 'verbatim',
multipleEntities: false,
requiredFrom: 'draft',
description: '',
};
}
function emptyDocumentDraft(): DocumentDraft {
return {
id: makeId('document'),
name: '',
required: 'false',
fieldCount: 0,
groups: [],
description: '',
fields: [],
};
}
function emptyVisualDraft(type = '签章'): VisualDraft {
return {
id: makeId('visual'),
name: '',
type,
required: 'true',
signerRoles: [],
signatureTypes: [],
privateSealRestricted: false,
};
}
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 pack = packId ? await loadRuleConfigPack(request, packId) : undefined;
const resolvedPack = pack || (await loadRuleConfigPacks(request))[0];
if (!resolvedPack) {
throw new Response('未找到 YAML 配置', { status: 404 });
}
const versions = await loadRuleConfigVersions(request, resolvedPack.metadata.typeId || '');
return Response.json({ pack: resolvedPack, requestedRuleId, versions } satisfies LoaderData);
}
export async function action({ request }: ActionFunctionArgs) {
const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server');
await requireRoutePermission('/rulesTest/detail', userInfo?.role || '', frontendJWT || undefined);
if (!frontendJWT) {
return json<ActionData>({ success: false, intent: 'save', message: '登录已失效,请重新登录后再保存。' }, { status: 401 });
}
const formData = await request.formData();
const intent = String(formData.get('intent') || 'save').trim() as ActionData['intent'];
const ruleType = String(formData.get('ruleType') || '').trim();
const yamlText = String(formData.get('yamlText') || '');
const changeNote = String(formData.get('changeNote') || '').trim() || 'rulesTest.detail 保存评查点草稿';
const versionId = Number(formData.get('versionId') || 0);
if (!ruleType) {
return json<ActionData>({ success: false, intent, message: '当前规则类型缺失,无法保存。' }, { status: 400 });
}
if (intent === 'save' && !yamlText.trim()) {
return json<ActionData>({ success: false, intent, message: '当前 YAML 内容为空,无法保存。' }, { status: 400 });
}
if ((intent === 'publish' || intent === 'rollback') && (!Number.isFinite(versionId) || versionId <= 0)) {
return json<ActionData>({ success: false, intent, message: '目标版本缺失,无法继续操作。' }, { status: 400 });
}
try {
const apiPath = intent === 'save'
? `/api/rule-sets/${encodeURIComponent(ruleType)}/versions`
: `/api/rule-sets/${encodeURIComponent(ruleType)}/${intent}`;
const requestBody = intent === 'save'
? {
yamlText,
changeNote,
editorUserId: userInfo?.user_id ? Number(userInfo.user_id) : undefined,
}
: {
versionId,
operatorUserId: userInfo?.user_id ? Number(userInfo.user_id) : undefined,
};
const response = await fetch(`${API_BASE_URL}${apiPath}`, {
method: 'POST',
headers: {
Authorization: `Bearer ${frontendJWT}`,
'Content-Type': 'application/json',
Accept: 'application/json',
},
body: JSON.stringify(requestBody),
});
const payload = await response.json();
const data = payload?.data ?? null;
const message = String(payload?.message || payload?.msg || (response.ok ? '规则草稿保存成功' : '规则草稿保存失败'));
if (!response.ok || !data) {
return json<ActionData>({ success: false, intent, message }, { status: response.status || 500 });
}
return json<ActionData>({
success: true,
intent,
message,
versionId: Number(data.id),
versionNo: String(data.versionNo || ''),
});
} catch (error) {
return json<ActionData>({
success: false,
intent,
message: error instanceof Error ? error.message : '规则草稿保存失败',
}, { status: 500 });
}
}
export default function RulesTestDetail() {
const { pack, requestedRuleId, versions } = useLoaderData<typeof loader>() as LoaderData;
const saveFetcher = useFetcher<ActionData>();
const revalidator = useRevalidator();
const initialRuleKey = requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' });
const [rules, setRules] = useState<RuleSummary[]>(pack.rules);
const [fields, setFields] = useState<ExtractFieldSummary[]>(pack.fields);
const [subDocuments, setSubDocuments] = useState<SubDocumentSummary[]>(pack.subDocuments);
const [visualElements, setVisualElements] = useState<VisualElementSummary[]>(pack.visualElements);
const [selectedRuleKey, setSelectedRuleKey] = useState(initialRuleKey);
const [editor, setEditor] = useState<EditorState>(null);
const [ruleDraft, setRuleDraft] = useState<RuleDraft>(emptyRuleDraft(pack.rules[0]?.group));
const [fieldDraft, setFieldDraft] = useState<FieldDraft>(emptyFieldDraft(pack.fields[0]?.group || '基础信息'));
const [documentDraft, setDocumentDraft] = useState<DocumentDraft>(emptyDocumentDraft());
const [visualDraft, setVisualDraft] = useState<VisualDraft>(emptyVisualDraft(pack.visualElements[0]?.type || '签章'));
const [selectedRollbackVersionId, setSelectedRollbackVersionId] = useState('');
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);
const [saveMessage, setSaveMessage] = useState('');
const [saveError, setSaveError] = useState('');
const promptEditorRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setRules(pack.rules);
setFields(pack.fields);
setSubDocuments(pack.subDocuments);
setVisualElements(pack.visualElements);
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
setEditor(null);
setDependencyDialogOpen(false);
setDependencySearch('');
setDependencySelection([]);
setExpandedDependencyGroups([]);
setShowValidation(false);
setShowYamlPreview(false);
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
}, [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,
yamlSource: pack.yamlSource,
documentType: pack.documentType,
mainType: pack.mainType,
subtype: pack.subtype,
fields,
subDocuments,
visualElements,
rules
}), [fields, pack, rules, subDocuments, visualElements]);
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 currentRuleFields = useMemo(
() => fields.filter((field) => matchesCurrentRuleDependency(currentRule, [field.name])),
[currentRule, fields],
);
const currentRuleSubDocuments = useMemo(
() => subDocuments.filter((document) => matchesCurrentRuleDependency(currentRule, [document.name, document.id])),
[currentRule, subDocuments],
);
const currentRuleVisualElements = useMemo(
() => visualElements.filter((item) => matchesCurrentRuleDependency(currentRule, [
item.id,
item.name,
`visual.${item.id}`,
`visual.${item.name || item.id}`,
item.type,
])),
[currentRule, visualElements],
);
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 configValidationIssues = useMemo(() => validateEditableRuleConfig(editableConfig), [editableConfig]);
const hasConfigErrors = configValidationIssues.some(issue => issue.severity === 'error');
const serializedYamlResult = useMemo(() => {
try {
return { yamlText: serializeEditableRuleConfig(editableConfig), error: '' };
} catch (error) {
return {
yamlText: buildYamlPreview(editableConfig),
error: error instanceof Error ? error.message : '规则 YAML 生成失败',
};
}
}, [editableConfig]);
const fullYamlText = serializedYamlResult.yamlText;
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 saveButtonBusy = saveFetcher.state !== 'idle';
const latestDraftVersion = useMemo(
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
[versions],
);
const rollbackVersionOptions = useMemo(
() => versions,
[versions],
);
const rollbackOptions = useMemo(
() => rollbackVersionOptions.filter((item) => item.id !== pack.currentVersionId),
[rollbackVersionOptions, pack.currentVersionId],
);
const rollbackTargetVersion = useMemo(
() => rollbackOptions.find((item) => String(item.id) === selectedRollbackVersionId) || rollbackOptions[0] || null,
[rollbackOptions, selectedRollbackVersionId],
);
const packFilterMainType = pack.businessType || pack.mainType;
const currentResolvedVersion = useMemo(
() => {
if (pack.currentVersionId) {
return versions.find((item) => item.id === pack.currentVersionId) || null;
}
if (pack.fallbackVersionId) {
return versions.find((item) => item.id === pack.fallbackVersionId) || null;
}
return null;
},
[pack.currentVersionId, pack.fallbackVersionId, versions],
);
const versionStatusLabel = (status: string | undefined) => {
const normalized = String(status || '').trim().toLowerCase();
if (normalized === 'published') return '已发布';
if (normalized === 'rollback') return '回滚版本';
if (normalized === 'draft') return '草稿';
if (normalized === 'deprecated') return '已废弃';
return status || '-';
};
useEffect(() => {
setSelectedRollbackVersionId('');
}, [pack.id, pack.currentVersionId, versions.length]);
useEffect(() => {
if (!saveFetcher.data) return;
if (saveFetcher.data.success) {
setDraftSaved(saveFetcher.data.intent === 'save');
setSaveError('');
if (saveFetcher.data.intent === 'save') {
setSaveMessage(saveFetcher.data.versionNo
? `规则草稿已保存为版本 ${saveFetcher.data.versionNo}`
: saveFetcher.data.message || '规则草稿已保存');
} else {
setSaveMessage(saveFetcher.data.message || (saveFetcher.data.intent === 'publish' ? '规则版本已发布' : '规则版本已回滚'));
}
revalidator.revalidate();
return;
}
setDraftSaved(false);
setSaveMessage('');
setSaveError(saveFetcher.data.message || '规则草稿保存失败');
}, [revalidator, saveFetcher.data]);
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 insertDependencyVariable = (value: string) => {
const variable = `{{${value}}}`;
const textarea = promptEditorRef.current;
const prompt = ruleDraft.prompt || '';
const start = textarea?.selectionStart ?? prompt.length;
const end = textarea?.selectionEnd ?? prompt.length;
const prefix = prompt.slice(0, start);
const suffix = prompt.slice(end);
const nextPrompt = `${prefix}${variable}${suffix}`;
setRuleDraft({ ...ruleDraft, prompt: nextPrompt });
window.setTimeout(() => {
if (!promptEditorRef.current) return;
const nextCursor = start + variable.length;
promptEditorRef.current.focus();
promptEditorRef.current.setSelectionRange(nextCursor, nextCursor);
}, 0);
};
const removeDependency = (value: string) => {
setRuleDraft({
...ruleDraft,
dependencies: ruleDraft.dependencies.filter(dependency => dependency !== value)
});
};
const patchCurrentRuleDependencies = (
replacements: Array<{ from: string; to: string }>,
appendedDependencies: string[],
) => {
if (!currentRule) return;
setRules((current) => current.map((rule) => {
if (rule.id !== currentRule.id) return rule;
const rewritten = rule.dependencies.map((dependency) => (
replacements.reduce(
(nextValue, item) => rewriteDependencyPrefix(nextValue, item.from, item.to),
dependency,
)
));
const merged = Array.from(new Set([
...rewritten,
...appendedDependencies.filter(Boolean),
]));
return {
...rule,
dependencies: merged,
};
}));
};
const removeCurrentRuleDependencies = (targets: string[]) => {
if (!currentRule) return;
const normalizedTargets = targets.map((item) => String(item || '').trim()).filter(Boolean);
if (normalizedTargets.length === 0) return;
setRules((current) => current.map((rule) => {
if (rule.id !== currentRule.id) return rule;
return {
...rule,
dependencies: rule.dependencies.filter((dependency) => (
!normalizedTargets.some((target) => dependency === target || dependency.startsWith(`${target}.`))
)),
};
}));
};
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(false);
setSaveMessage('');
setSaveError('');
setEditor(null);
};
const openFieldEditor = (field?: ExtractFieldSummary) => {
setFieldDraft(field ? { ...field } : emptyFieldDraft(fields[0]?.group || '基础信息'));
setEditor({ kind: 'field', mode: field ? 'edit' : 'create', id: field?.id });
};
const saveField = () => {
if (!editor || editor.kind !== 'field') return;
const normalizedField: ExtractFieldSummary = {
...fieldDraft,
id: fieldDraft.id || makeId('field'),
group: fieldDraft.group || '未分组',
requiredFrom: fieldDraft.requiredFrom || 'draft',
type: fieldDraft.type || 'verbatim',
description: fieldDraft.description || '',
};
setFields((current) => editor.mode === 'edit'
? current.map((field) => (field.id === editor.id ? normalizedField : field))
: [...current, normalizedField]);
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
setEditor(null);
};
const removeField = (fieldId: string) => {
setFields((current) => current.filter((field) => field.id !== fieldId));
setDraftSaved(false);
};
const openDocumentEditor = (document?: SubDocumentSummary) => {
setDocumentDraft(document ? { ...document, fields: [...document.fields] } : emptyDocumentDraft());
setEditor({ kind: 'document', mode: document ? 'edit' : 'create', id: document?.id });
};
const saveDocument = () => {
if (!editor || editor.kind !== 'document') return;
const previousDocument = editor.mode === 'edit'
? subDocuments.find((document) => document.id === editor.id)
: undefined;
const normalizedDocument: SubDocumentSummary = {
...documentDraft,
id: documentDraft.id || makeId('document'),
name: documentDraft.name || documentDraft.id,
required: documentDraft.required || 'false',
fieldCount: (documentDraft.fields || []).length,
groups: Array.from(new Set((documentDraft.fields || []).map((field) => field.group || '未分组'))),
description: documentDraft.description || '',
fields: (documentDraft.fields || []).map((field) => ({
...field,
id: field.id || makeId('document-field'),
group: field.group || '未分组',
requiredFrom: field.requiredFrom || '-',
type: field.type || 'verbatim',
})),
};
setSubDocuments((current) => editor.mode === 'edit'
? current.map((document) => (document.id === editor.id ? normalizedDocument : document))
: [...current, normalizedDocument]);
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
patchCurrentRuleDependencies(
[
previousDocument ? { from: previousDocument.id, to: normalizedDocument.id } : null,
previousDocument ? { from: previousDocument.name, to: normalizedDocument.name } : null,
].filter(Boolean) as Array<{ from: string; to: string }>,
[normalizedDocument.id],
);
setEditor(null);
};
const removeDocument = (documentId: string) => {
const target = subDocuments.find((document) => document.id === documentId);
setSubDocuments((current) => current.filter((document) => document.id !== documentId));
removeCurrentRuleDependencies([
documentId,
target?.name || '',
]);
setDraftSaved(false);
};
const updateDocumentField = (fieldId: string, patch: Partial<ExtractFieldSummary>) => {
setDocumentDraft((current) => ({
...current,
fields: (current.fields || []).map((field) => (
field.id === fieldId ? { ...field, ...patch } : field
)),
}));
};
const addDocumentField = () => {
setDocumentDraft((current) => ({
...current,
fields: [
...(current.fields || []),
{
id: `${current.id || 'document'}-field-${Date.now()}`,
group: current.groups[0] || '未分组',
name: '',
type: 'verbatim',
multipleEntities: false,
requiredFrom: '-',
description: '',
},
],
}));
};
const removeDocumentField = (fieldId: string) => {
setDocumentDraft((current) => ({
...current,
fields: (current.fields || []).filter((field) => field.id !== fieldId),
}));
};
const openVisualEditor = (item?: VisualElementSummary) => {
setVisualDraft(item ? { ...item, signerRoles: [...(item.signerRoles || [])], signatureTypes: [...(item.signatureTypes || [])] } : emptyVisualDraft(visualElements[0]?.type || '签章'));
setEditor({ kind: 'visual', mode: item ? 'edit' : 'create', id: item?.id });
};
const saveVisual = () => {
if (!editor || editor.kind !== 'visual') return;
const previousVisual = editor.mode === 'edit'
? visualElements.find((item) => item.id === editor.id)
: undefined;
const normalizedVisual: VisualElementSummary = {
...visualDraft,
id: visualDraft.id || makeId('visual'),
name: visualDraft.name || visualDraft.id,
type: visualDraft.type || '签章',
required: visualDraft.required || 'true',
signerRoles: (visualDraft.signerRoles || []).filter(Boolean),
signatureTypes: (visualDraft.signatureTypes || []).filter(Boolean),
privateSealRestricted: Boolean(visualDraft.privateSealRestricted),
};
setVisualElements((current) => editor.mode === 'edit'
? current.map((item) => (item.id === editor.id ? normalizedVisual : item))
: [...current, normalizedVisual]);
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
patchCurrentRuleDependencies(
[
previousVisual ? { from: previousVisual.id, to: normalizedVisual.id } : null,
previousVisual ? { from: previousVisual.name, to: normalizedVisual.name } : null,
previousVisual ? { from: `visual.${previousVisual.id}`, to: `visual.${normalizedVisual.id}` } : null,
previousVisual ? { from: `visual.${previousVisual.name || previousVisual.id}`, to: `visual.${normalizedVisual.name || normalizedVisual.id}` } : null,
].filter(Boolean) as Array<{ from: string; to: string }>,
[`visual.${normalizedVisual.id}`],
);
setEditor(null);
};
const removeVisual = (visualId: string) => {
const target = visualElements.find((item) => item.id === visualId);
setVisualElements((current) => current.filter((item) => item.id !== visualId));
removeCurrentRuleDependencies([
visualId,
target?.name || '',
`visual.${visualId}`,
`visual.${target?.name || visualId}`,
]);
setDraftSaved(false);
};
const saveDraftToServer = () => {
if (hasConfigErrors) {
setShowValidation(true);
setSaveError('当前规则配置仍有必改问题,请先处理后再保存。');
setSaveMessage('');
return;
}
if (serializedYamlResult.error) {
setSaveError(serializedYamlResult.error);
setSaveMessage('');
return;
}
const formData = new FormData();
formData.append('ruleType', pack.metadata.typeId || '');
formData.append('yamlText', prepareDraftYamlForSave(fullYamlText));
formData.append('changeNote', `保存 ${pack.documentType}/${pack.mainType}/${pack.subtype} 规则配置`);
formData.append('intent', 'save');
saveFetcher.submit(formData, { method: 'post' });
};
const publishDraftVersion = () => {
if (!latestDraftVersion) {
setSaveError('当前没有可发布的新版本,请先保存规则配置。');
setSaveMessage('');
return;
}
const formData = new FormData();
formData.append('intent', 'publish');
formData.append('ruleType', pack.metadata.typeId || '');
formData.append('versionId', String(latestDraftVersion.id));
saveFetcher.submit(formData, { method: 'post' });
};
const rollbackRuleVersion = () => {
if (!rollbackTargetVersion) {
setSaveError('当前没有可回滚的历史可用版本。');
setSaveMessage('');
return;
}
const formData = new FormData();
formData.append('intent', 'rollback');
formData.append('ruleType', pack.metadata.typeId || '');
formData.append('versionId', String(rollbackTargetVersion.id));
saveFetcher.submit(formData, { method: 'post' });
};
const resetDraft = () => {
setRules(pack.rules);
setFields(pack.fields);
setSubDocuments(pack.subDocuments);
setVisualElements(pack.visualElements);
setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }));
setDependencyDialogOpen(false);
setDependencySearch('');
setDependencySelection([]);
setShowValidation(false);
setShowYamlPreview(false);
setDraftSaved(false);
setSaveMessage('');
setSaveError('');
};
const dependencyColumns = [
{
title: '依赖字段',
key: 'label',
width: '38%',
render: (_: unknown, record: DependencyOption) => (
<div className="rule-name">
<strong>{record.label}</strong>
</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(packFilterMainType)}&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 className="config-toolbar-desc">
{currentResolvedVersion?.versionNo || '-'} / {versionStatusLabel(currentResolvedVersion?.status)}
</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={saveButtonBusy} onClick={saveDraftToServer}>
<i className={`${saveButtonBusy ? 'ri-loader-4-line' : 'ri-save-line'} mr-1.5`}></i>
{saveButtonBusy ? '保存中...' : '保存规则配置'}
</button>
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !latestDraftVersion} onClick={publishDraftVersion}>
<i className="ri-upload-cloud-line mr-1.5"></i>
</button>
<select
className="rules-version-select"
value={rollbackTargetVersion ? String(rollbackTargetVersion.id) : selectedRollbackVersionId}
onChange={(event) => setSelectedRollbackVersionId(event.target.value)}
disabled={saveButtonBusy || rollbackVersionOptions.length === 0}
>
{rollbackVersionOptions.length === 0 ? (
<option value=""></option>
) : rollbackVersionOptions.map((item) => (
<option key={item.id} value={item.id} disabled={item.id === pack.currentVersionId}>
{item.versionNo} · {versionStatusLabel(item.status)}{item.id === pack.currentVersionId ? ' · 当前版本' : ''}
</option>
))}
</select>
<button type="button" className="ant-btn ant-btn-default" disabled={saveButtonBusy || !rollbackTargetVersion} onClick={rollbackRuleVersion}>
<i className="ri-history-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>
稿
</div>
)}
{saveMessage && (
<div className="draft-tip">
<i className="ri-checkbox-circle-line"></i>
{saveMessage}
</div>
)}
{saveError && (
<div className="draft-tip danger">
<i className="ri-error-warning-line"></i>
{saveError}
</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>
{(fields.length > 0 || subDocuments.length > 0 || visualElements.length > 0) && (
<>
{fields.length > 0 && <span id="fields" className="section-anchor" aria-hidden="true"></span>}
{subDocuments.length > 0 && <span id="sub-documents" className="section-anchor" aria-hidden="true"></span>}
{visualElements.length > 0 && <span id="visual-elements" className="section-anchor" aria-hidden="true"></span>}
</>
)}
<Card className="ant-card" title="抽取字段">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{currentRuleFields.length > 0 ? (
<div className="config-item-list">
{currentRuleFields.map((field) => (
<div key={field.id} className="config-item-card">
<div className="config-item-main">
<strong>{field.name}</strong>
<span>{field.group || '未分组'} / {fieldTypeLabel(field.type)}</span>
<span>{requiredFromLabel(field.requiredFrom || '-')}{field.description ? ` · ${field.description}` : ''}</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openFieldEditor(field)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeField(field.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"></div>
)}
</Card>
<Card className="ant-card" title="子文档 / 文书">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{currentRuleSubDocuments.length > 0 ? (
<div className="config-item-list">
{currentRuleSubDocuments.map((document) => (
<div key={document.id} className="config-item-card">
<div className="config-item-main">
<strong>{document.name}</strong>
<span>{document.id} / {requiredFromLabel(document.required || '-')}</span>
<span>{document.fields.length} {document.groups.length > 0 ? ` · ${document.groups.join('、')}` : ''}</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openDocumentEditor(document)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeDocument(document.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"> `文书.字段` </div>
)}
</Card>
<Card className="ant-card" title="视觉要素">
<div className="config-section-tools">
<span className="config-section-tip"></span>
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor()}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
{currentRuleVisualElements.length > 0 ? (
<div className="config-item-list">
{currentRuleVisualElements.map((item) => (
<div key={item.id} className="config-item-card">
<div className="config-item-main">
<strong>{item.name || item.id}</strong>
<span>{item.type} / {item.id}</span>
<span>{requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}</span>
</div>
<div className="config-item-actions">
<button type="button" className="ant-btn ant-btn-default" onClick={() => openVisualEditor(item)}></button>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeVisual(item.id)}></button>
</div>
</div>
))}
</div>
) : (
<div className="empty-state"> `visual.xxx` </div>
)}
</Card>
<Card
className="ant-card"
title={`依赖字段 (${currentRule.dependencies.length}项)`}
>
<Table
className="rules-test-table"
columns={dependencyColumns}
dataSource={currentDependencyRows}
rowKey="value"
emptyText={<div className="empty-state"></div>}
/>
</Card>
<span id="rules" className="section-anchor" aria-hidden="true"></span>
<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>
</div>
</div>
<div className="subrule-list">
{currentRule.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<span className="subrule-index">{subRule.id}</span>
<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>
</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">
<span className="subrule-index">{ruleId}</span>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '未找到对应规则内容'}</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>
</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.kind === 'rule' ? (editor.mode === 'edit' ? '编辑评查点' : '新增评查点') : ''}
{editor.kind === 'field' ? (editor.mode === 'edit' ? '编辑抽取字段' : '新增抽取字段') : ''}
{editor.kind === 'document' ? (editor.mode === 'edit' ? '编辑子文档' : '新增子文档') : ''}
{editor.kind === 'visual' ? (editor.mode === 'edit' ? '编辑视觉要素' : '新增视觉要素') : ''}
</h3>
</div>
<button type="button" className="drawer-close" onClick={() => setEditor(null)}><i className="ri-close-line"></i></button>
</div>
{editor.kind === 'rule' && (
<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
ref={promptEditorRef}
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>
</div>
</div>
<div className="subrule-list">
{ruleDraft.subRules.length > 0 ? ruleDraft.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<span className="subrule-index">{subRule.id}</span>
<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">
<span className="subrule-index">{ruleId}</span>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '未找到对应规则内容'}</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 => (
<span
key={option.value}
className="dependency-variable-button"
>
<button
type="button"
className="dependency-variable-main"
onClick={() => insertDependencyVariable(option.value)}
title={`插入变量 {{${option.value}}}`}
>
{option.label}
</button>
<button
type="button"
className="dependency-variable-remove"
onClick={() => removeDependency(option.value)}
aria-label={`移除依赖字段 ${option.label}`}
>
<i className="ri-close-line"></i>
</button>
</span>
))}
<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>
)}
{editor.kind === 'field' && (
<div className="drawer-form">
<label>
<span></span>
<input value={fieldDraft.name} onChange={(event) => setFieldDraft({ ...fieldDraft, name: event.target.value })} placeholder="如:合同金额" />
</label>
<label>
<span></span>
<input value={fieldDraft.group} onChange={(event) => setFieldDraft({ ...fieldDraft, group: event.target.value })} placeholder="如:基础信息" />
</label>
<div className="drawer-grid">
<label>
<span></span>
<select value={fieldDraft.type} onChange={(event) => setFieldDraft({ ...fieldDraft, type: event.target.value })}>
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => <option key={type} value={type}>{fieldTypeLabel(type)}</option>)}
</select>
</label>
<label>
<span></span>
<select value={fieldDraft.requiredFrom} onChange={(event) => setFieldDraft({ ...fieldDraft, requiredFrom: event.target.value })}>
{['draft', 'executed', '-'].map((phase) => <option key={phase} value={phase}>{requiredFromLabel(phase)}</option>)}
</select>
</label>
</div>
<label>
<span></span>
<textarea value={fieldDraft.description} onChange={(event) => setFieldDraft({ ...fieldDraft, description: event.target.value })} placeholder="描述字段如何抽取、给规则如何引用" />
</label>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveField}></Button>
</div>
</div>
)}
{editor.kind === 'document' && (
<div className="drawer-form">
<label>
<span></span>
<input value={documentDraft.id} onChange={(event) => setDocumentDraft({ ...documentDraft, id: event.target.value })} placeholder="如:处罚决定书" />
</label>
<label>
<span></span>
<input value={documentDraft.name} onChange={(event) => setDocumentDraft({ ...documentDraft, name: event.target.value })} placeholder="如:处罚决定书" />
</label>
<label>
<span></span>
<select value={documentDraft.required} onChange={(event) => setDocumentDraft({ ...documentDraft, required: event.target.value })}>
{['true', 'false', 'conditional'].map((value) => <option key={value} value={value}>{requiredFromLabel(value)}</option>)}
</select>
</label>
<label>
<span></span>
<div className="document-draft-fields">
{(documentDraft.fields || []).map((field) => (
<div key={field.id} className="document-draft-field-row">
<input
value={field.group || ''}
onChange={(event) => updateDocumentField(field.id, { group: event.target.value })}
placeholder="分组"
/>
<input
value={field.name || ''}
onChange={(event) => updateDocumentField(field.id, { name: event.target.value })}
placeholder="字段名称"
/>
<select
value={field.type || 'verbatim'}
onChange={(event) => updateDocumentField(field.id, { type: event.target.value })}
>
{['verbatim', 'string', 'money', 'date', 'enum', 'number'].map((type) => (
<option key={type} value={type}>{fieldTypeLabel(type)}</option>
))}
</select>
<input
value={field.description || ''}
onChange={(event) => updateDocumentField(field.id, { description: event.target.value })}
placeholder="字段说明"
/>
<button type="button" className="ant-btn ant-btn-default" onClick={() => removeDocumentField(field.id)}></button>
</div>
))}
<button type="button" className="ant-btn ant-btn-default" onClick={addDocumentField}>
<i className="ri-add-line mr-1.5"></i>
</button>
</div>
</label>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveDocument}></Button>
</div>
</div>
)}
{editor.kind === 'visual' && (
<div className="drawer-form">
<label>
<span></span>
<input value={visualDraft.id} onChange={(event) => setVisualDraft({ ...visualDraft, id: event.target.value })} placeholder="如:骑缝章" />
</label>
<label>
<span></span>
<input value={visualDraft.name} onChange={(event) => setVisualDraft({ ...visualDraft, name: event.target.value })} placeholder="如:合同骑缝章" />
</label>
<div className="drawer-grid">
<label>
<span></span>
<select value={visualDraft.type} onChange={(event) => setVisualDraft({ ...visualDraft, type: event.target.value })}>
{['签章', '签名', '骑缝章'].map((type) => <option key={type} value={type}>{type}</option>)}
</select>
</label>
<label>
<span></span>
<select value={visualDraft.required} onChange={(event) => setVisualDraft({ ...visualDraft, required: event.target.value })}>
{['true', 'false'].map((value) => <option key={value} value={value}>{requiredFromLabel(value)}</option>)}
</select>
</label>
</div>
<label>
<span></span>
<input
value={(visualDraft.signatureTypes || []).join('')}
onChange={(event) => setVisualDraft({ ...visualDraft, signatureTypes: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:合同专用章,公章"
/>
</label>
<label>
<span></span>
<input
value={(visualDraft.signerRoles || []).join('')}
onChange={(event) => setVisualDraft({ ...visualDraft, signerRoles: event.target.value.split(/[,]/).map((item) => item.trim()).filter(Boolean) })}
placeholder="如:甲方,乙方,承办人"
/>
</label>
<label className="drawer-checkbox-row">
<input
type="checkbox"
checked={Boolean(visualDraft.privateSealRestricted)}
onChange={(event) => setVisualDraft({ ...visualDraft, privateSealRestricted: event.target.checked })}
/>
<span> / </span>
</label>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveVisual}></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>
</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>
);
}