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 & { 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[] { return Array.from(new Set(values.map(value => value?.trim()).filter(Boolean) as string[])); } function uniqueDependencyOptions(options: DependencyOption[]): DependencyOption[] { const seen = new Set(); return options.filter(option => { if (!option.value || seen.has(option.value)) { return false; } seen.add(option.value); return true; }); } function ruleKey(rule: Pick): string { return rule.ruleId || rule.id; } function ruleTypeLabel(type: string): string { const labels: Record = { deterministic: '确定性检查', ai_rule: '智能语义检查', rule_group: '规则组合', llm: '智能语义检查', manual: '人工复核' }; return labels[type] ? `${labels[type]} (${type})` : type || '-'; } function checkTypeLabel(type: string): string { const labels: Record = { 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 = { draft: '草稿', executed: '已执行' }; return labels[phase] ? `${labels[phase]} (${phase})` : phase; } function fieldTypeLabel(type: string): string { const labels: Record = { verbatim: '原文', string: '文本', money: '金额', date: '日期', enum: '枚举', number: '数字', multi_entity: '多实体', }; return labels[type] || type || '-'; } function requiredFromLabel(value: string): string { const labels: Record = { draft: '草稿阶段', executed: '签署后阶段', '-': '未限定', true: '必需', false: '可选', conditional: '条件必需', }; return labels[value] || value || '-'; } function matchesCurrentRuleDependency(currentRule: RuleSummary | undefined, candidates: Array): 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): 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  ; } if (content.startsWith('#')) { return ( {content} ); } if (listMatch) { return ( {indent} {listMatch[1]} {listMatch[2]} ); } if (keyMatch) { return ( {indent} {keyMatch[1]} ); } return ( {indent} {content} ); } 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 {value}; } 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({ 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({ success: false, intent, message: '当前规则类型缺失,无法保存。' }, { status: 400 }); } if (intent === 'save' && !yamlText.trim()) { return json({ success: false, intent, message: '当前 YAML 内容为空,无法保存。' }, { status: 400 }); } if ((intent === 'publish' || intent === 'rollback') && (!Number.isFinite(versionId) || versionId <= 0)) { return json({ 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({ success: false, intent, message }, { status: response.status || 500 }); } return json({ success: true, intent, message, versionId: Number(data.id), versionNo: String(data.versionNo || ''), }); } catch (error) { return json({ success: false, intent, message: error instanceof Error ? error.message : '规则草稿保存失败', }, { status: 500 }); } } export default function RulesTestDetail() { const { pack, requestedRuleId, versions } = useLoaderData() as LoaderData; const saveFetcher = useFetcher(); const revalidator = useRevalidator(); const initialRuleKey = requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }); const [rules, setRules] = useState(pack.rules); const [fields, setFields] = useState(pack.fields); const [subDocuments, setSubDocuments] = useState(pack.subDocuments); const [visualElements, setVisualElements] = useState(pack.visualElements); const [selectedRuleKey, setSelectedRuleKey] = useState(initialRuleKey); const [editor, setEditor] = useState(null); const [ruleDraft, setRuleDraft] = useState(emptyRuleDraft(pack.rules[0]?.group)); const [fieldDraft, setFieldDraft] = useState(emptyFieldDraft(pack.fields[0]?.group || '基础信息')); const [documentDraft, setDocumentDraft] = useState(emptyDocumentDraft()); const [visualDraft, setVisualDraft] = useState(emptyVisualDraft(pack.visualElements[0]?.type || '签章')); const [selectedRollbackVersionId, setSelectedRollbackVersionId] = useState(''); const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false); const [dependencySearch, setDependencySearch] = useState(''); const [dependencySelection, setDependencySelection] = useState([]); const [expandedDependencyGroups, setExpandedDependencyGroups] = useState([]); 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(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(); 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) => { 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) => (
{record.label}
) }, { 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 (
{currentRule?.name || '未找到评查点'}
{pack.documentType} / {pack.mainType} / {pack.subtype} / {currentRule?.ruleId || '-'}
当前版本:{currentResolvedVersion?.versionNo || '-'} / 当前状态:{versionStatusLabel(currentResolvedVersion?.status)}
返回列表
{draftSaved && (
草稿已保存。
)} {saveMessage && (
{saveMessage}
)} {saveError && (
{saveError}
)}
{showValidation && (
{hasErrors ? '存在必改问题' : '可提交验证'} 当前评查点发现 {validationIssues.length} 项提示,其中 {validationIssues.filter(issue => issue.severity === 'error').length} 项需要处理。
{validationIssues.length === 0 ? (
未发现配置问题。
) : validationIssues.map(issue => (
{issue.severity === 'error' ? '必改' : '提醒'} {issue.area} {issue.target}

{issue.message}

))}
)} {showYamlPreview && currentRule && (
              {yamlPreview.split('\n').map(renderYamlLine)}
            
)} {currentRule ? ( <>
{currentRule.ruleId || '-'}
{currentRule.group || '-'}
{ruleTypeLabel(currentRule.type)}
{currentRule.appliesIn.length > 0 ? currentRule.appliesIn.map(phaseLabel).join('、') : '全部阶段'}
{riskLabel(currentRule.risk)}
{currentRule.score || '-'}
{currentRule.checkTypes.length > 0 ? currentRule.checkTypes.map(checkTypeLabel).join('、') : '-'}
{currentRule.description && (

{currentRule.description}

)}
{(fields.length > 0 || subDocuments.length > 0 || visualElements.length > 0) && ( <> {fields.length > 0 && } {subDocuments.length > 0 && } {visualElements.length > 0 && } )}
这里只显示当前评查点实际引用到的抽取字段。
{currentRuleFields.length > 0 ? (
{currentRuleFields.map((field) => (
{field.name} {field.group || '未分组'} / {fieldTypeLabel(field.type)} {requiredFromLabel(field.requiredFrom || '-')}{field.description ? ` · ${field.description}` : ''}
))}
) : (
当前评查点没有引用抽取字段;新增后需在该评查点“依赖字段”中引用,才会显示在这里。
)}
这里只显示当前评查点实际引用到的子文档;新增或编辑后会自动挂到当前评查点依赖。
{currentRuleSubDocuments.length > 0 ? (
{currentRuleSubDocuments.map((document) => (
{document.name} {document.id} / {requiredFromLabel(document.required || '-')} {document.fields.length} 个字段{document.groups.length > 0 ? ` · ${document.groups.join('、')}` : ''}
))}
) : (
当前评查点没有引用任何子文档;在依赖字段中引用文书名称或 `文书.字段` 后,这里才会显示。
)}
这里只显示当前评查点实际引用到的视觉要素;新增或编辑后会自动挂到当前评查点依赖。
{currentRuleVisualElements.length > 0 ? (
{currentRuleVisualElements.map((item) => (
{item.name || item.id} {item.type} / {item.id} {requiredFromLabel(String(item.required))}{item.signatureTypes && item.signatureTypes.length > 0 ? ` · ${item.signatureTypes.join('、')}` : ''}
))}
) : (
当前评查点没有引用视觉要素;在依赖字段中引用印章、签名、骑缝章或 `visual.xxx` 后,这里才会显示。
)}
当前评查点暂未配置依赖字段。} />
{currentRule.subRules.length > 0 && (
{currentRule.type === 'rule_group' ? `子规则与逻辑(${currentRule.subRules.length}步)` : `规则步骤(${currentRule.subRules.length}步)`}
{currentRule.subRules.map(subRule => (
{subRule.id}
{checkTypeLabel(subRule.check)} {currentRule.logic && ( {isStepReferenced(currentRule.logic, subRule.id) ? '参与逻辑' : '未参与逻辑'} )} {subRule.content}
))}
{currentRule.logic && (
{currentRule.logic}
)}
)} {currentRule.type === 'rule_group' && currentRule.subRules.length === 0 && (
子规则与逻辑
{currentRule.subRuleIds.length > 0 ? currentRule.subRuleIds.map(ruleId => { const referencedRule = rulesById.get(ruleId); return (
{ruleId}
{referencedRule?.name || '引用规则'} {referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '未找到对应规则内容'}
); }) : (
当前规则组合还没有子规则内容。
)}
{currentRule.logic || '-'}
)} {(currentRule.type === 'ai_rule' || currentRule.checkTypes.includes('ai')) && (
智能语义检查提示词
{currentRule.prompt || '当前评查点尚未维护提示词。'}
)} {currentRule.subRules.length === 0 && currentRule.type !== 'rule_group' && currentRule.type !== 'ai_rule' && !currentRule.checkTypes.includes('ai') && (

{currentRule.description || '当前评查点没有额外规则内容。'}

)}
) : (
当前链接没有匹配到评查点,请返回列表重新进入。
)} {editor && (