import { type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node'; import { Link, useLoaderData } from '@remix-run/react'; import type React from 'react'; import { useEffect, useMemo, useState } from 'react'; import { Button } from '~/components/ui/Button'; import { Card } from '~/components/ui/Card'; import { Table } from '~/components/ui/Table'; import { Tag, type TagColor } from '~/components/ui/Tag'; import { loadRuleYamlPack, loadRuleYamlPacks, type RuleSummary, type RuleYamlPack } from '~/utils/rules-yaml-mock.server'; import { buildRuleYamlPreview, collectDependencyOptions, getDefaultExpandedDependencyGroups, type DependencyOption, type EditableRuleConfig, type ValidationIssue } from '~/utils/rules-config-editor'; import styles from '~/styles/pages/rules_test.css?url'; export const links = () => [ { rel: 'stylesheet', href: styles } ]; export const meta: MetaFunction = () => [ { title: '评查点详情 - 智慧法务' } ]; type LoaderData = { pack: RuleYamlPack; requestedRuleId: string; }; type EditorState = { kind: 'rule'; mode: 'create' | 'edit'; id?: string } | null; type RuleDraft = Pick & { checkTypes: string[]; dependencies: string[]; }; function riskColor(risk: string): TagColor { if (risk === 'high') return 'red'; if (risk === 'medium') return 'orange'; if (risk === 'low') return 'green'; return 'gray'; } function riskLabel(risk: string): string { if (risk === 'high') return '高'; if (risk === 'medium') return '中'; if (risk === 'low') return '低'; return risk || '-'; } function uniqueOptions(values: Array): string[] { 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 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 emptyRuleDraft(group = '未分组'): RuleDraft { return { id: makeId('rule'), ruleId: '', name: '', group, risk: 'medium', score: '1', type: 'deterministic', checkTypes: [], logic: '', subRules: [], subRuleIds: [], prompt: '', description: '', dependencies: [] }; } function issueColor(severity: ValidationIssue['severity']): TagColor { return severity === 'error' ? 'red' : 'orange'; } function renderYamlLine(line: string, index: number) { const indent = line.match(/^\s*/)?.[0] || ''; const content = line.slice(indent.length); const listMatch = content.match(/^(-\s+)([^:]+:)(.*)$/); const keyMatch = content.match(/^([^:]+:)(.*)$/); if (!content) { return  ; } 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 packs = await loadRuleYamlPacks(); const pack = (packId ? await loadRuleYamlPack(packId) : undefined) || packs[0]; if (!pack) { throw new Response('未找到 YAML 配置', { status: 404 }); } return Response.json({ pack, requestedRuleId } satisfies LoaderData); } export default function RulesTestDetail() { const { pack, requestedRuleId } = useLoaderData() as LoaderData; const initialRuleKey = requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' }); const [rules, setRules] = useState(pack.rules); const [selectedRuleKey, setSelectedRuleKey] = useState(initialRuleKey); const [editor, setEditor] = useState(null); const [ruleDraft, setRuleDraft] = useState(emptyRuleDraft(pack.rules[0]?.group)); 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); useEffect(() => { setRules(pack.rules); setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' })); setEditor(null); setDependencyDialogOpen(false); setDependencySearch(''); setDependencySelection([]); setExpandedDependencyGroups([]); setShowValidation(false); setShowYamlPreview(false); setDraftSaved(false); }, [pack.id, requestedRuleId]); const currentRule = useMemo(() => { return rules.find(rule => rule.id === selectedRuleKey || rule.ruleId === selectedRuleKey) || rules[0]; }, [rules, selectedRuleKey]); const editableConfig: EditableRuleConfig = useMemo(() => ({ metadata: pack.metadata, documentType: pack.documentType, mainType: pack.mainType, subtype: pack.subtype, fields: pack.fields, subDocuments: pack.subDocuments, visualElements: pack.visualElements, rules }), [pack, rules]); const dependencyOptions = useMemo(() => collectDependencyOptions(editableConfig), [editableConfig]); const dependencyOptionMap = useMemo(() => new Map(dependencyOptions.map(option => [option.value, option])), [dependencyOptions]); const validationIssues = useMemo(() => validateRule(currentRule, dependencyOptions), [currentRule, dependencyOptions]); const yamlPreview = useMemo(() => currentRule ? buildRuleYamlPreview(editableConfig, currentRule) : '', [currentRule, editableConfig]); const ruleGroups = useMemo(() => Array.from(new Set(rules.map(rule => rule.group || '未分组'))), [rules]); const ruleTypeOptions = useMemo(() => uniqueOptions([ ...rules.map(rule => rule.type), 'deterministic', 'ai_rule', 'rule_group' ]), [rules]); const selectedDependencyOptions = useMemo(() => { return ruleDraft.dependencies.map(value => dependencyOptionMap.get(value) || fallbackDependencyOption(value, dependencyOptionMap)); }, [dependencyOptionMap, ruleDraft.dependencies]); const currentDependencyRows = useMemo(() => { return (currentRule?.dependencies || []).map(value => dependencyOptionMap.get(value) || fallbackDependencyOption(value, dependencyOptionMap)); }, [currentRule, dependencyOptionMap]); const dialogDependencyOptions = useMemo(() => { const selectedValues = new Set(ruleDraft.dependencies); return uniqueDependencyOptions([ ...selectedDependencyOptions, ...dependencyOptions ]).sort((left, right) => { const selectedDelta = Number(selectedValues.has(right.value)) - Number(selectedValues.has(left.value)); if (selectedDelta !== 0) return selectedDelta; return left.label.localeCompare(right.label, 'zh-CN'); }); }, [dependencyOptions, ruleDraft.dependencies, selectedDependencyOptions]); const filteredDependencyOptions = useMemo(() => { const keyword = dependencySearch.trim().toLowerCase(); return dialogDependencyOptions.filter(option => { if (!keyword) return true; return [option.value, option.label, option.source, option.group] .some(text => text.toLowerCase().includes(keyword)); }); }, [dialogDependencyOptions, dependencySearch]); const dependencyGroups = useMemo(() => { const groups = new Map(); filteredDependencyOptions.forEach(option => { const current = groups.get(option.group) || []; current.push(option); groups.set(option.group, current); }); return Array.from(groups.entries()); }, [filteredDependencyOptions]); const isDependencySearching = Boolean(dependencySearch.trim()); const defaultExpandedDependencyGroups = useMemo(() => { return getDefaultExpandedDependencyGroups(dialogDependencyOptions, dependencySelection); }, [dialogDependencyOptions, dependencySelection]); const dependencyDialogEmptyText = dependencySearch.trim() ? '没有匹配的字段。' : '当前文档类型暂无可追加字段。'; const hasErrors = validationIssues.some(issue => issue.severity === 'error'); const isSmartRuleDraft = ruleDraft.type === 'ai_rule' || ruleDraft.checkTypes.includes('ai'); const isRuleGroupDraft = ruleDraft.type === 'rule_group'; const rulesById = useMemo(() => new Map(rules.map(rule => [rule.ruleId || rule.id, rule])), [rules]); const openRuleEditor = (rule?: RuleSummary) => { setRuleDraft(rule ? { ...rule } : emptyRuleDraft(ruleGroups[0])); setDependencyDialogOpen(false); setDependencySearch(''); setDependencySelection(rule?.dependencies || []); setExpandedDependencyGroups([]); setEditor({ kind: 'rule', mode: rule ? 'edit' : 'create', id: rule?.id }); }; const openDependencyDialog = () => { setDependencySelection(ruleDraft.dependencies); setDependencySearch(''); setExpandedDependencyGroups(getDefaultExpandedDependencyGroups(dialogDependencyOptions, ruleDraft.dependencies)); setDependencyDialogOpen(true); }; const updateDependencySearch = (value: string) => { setDependencySearch(value); if (!value.trim()) { setExpandedDependencyGroups(defaultExpandedDependencyGroups); } }; const toggleDependencyGroup = (group: string) => { setExpandedDependencyGroups(current => ( current.includes(group) ? current.filter(item => item !== group) : [...current, group] )); }; const applyDependencySelection = () => { setRuleDraft({ ...ruleDraft, dependencies: dependencySelection }); setDependencyDialogOpen(false); }; const saveRule = () => { if (!editor || editor.kind !== 'rule') return; const existingRule = editor.id ? rules.find(rule => rule.id === editor.id) : undefined; const normalizedRule: RuleSummary = { ...ruleDraft, id: ruleDraft.id || makeId('rule'), ruleId: ruleDraft.ruleId || ruleDraft.id, group: ruleDraft.group || '未分组', checkTypes: ruleDraft.type === 'ai_rule' ? uniqueOptions([...ruleDraft.checkTypes, 'ai']) : ruleDraft.checkTypes, appliesIn: existingRule?.appliesIn || [], scope: existingRule?.scope || [], stageCount: existingRule?.stageCount || ruleDraft.subRules.length }; setRules(current => editor.mode === 'edit' ? current.map(rule => rule.id === editor.id ? normalizedRule : rule) : [...current, normalizedRule]); setSelectedRuleKey(ruleKey(normalizedRule)); setDraftSaved(true); setEditor(null); }; const resetDraft = () => { setRules(pack.rules); setSelectedRuleKey(requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' })); setDependencyDialogOpen(false); setDependencySearch(''); setDependencySelection([]); setShowValidation(false); setShowYamlPreview(false); setDraftSaved(false); }; const dependencyColumns = [ { title: '依赖字段', key: 'label', width: '38%', render: (_: unknown, record: DependencyOption) => (
{record.label} YAML引用:{record.value}
) }, { title: '来源', dataIndex: 'source' as keyof DependencyOption, key: 'source', width: '26%' }, { title: '分组', dataIndex: 'group' as keyof DependencyOption, key: 'group', width: '36%' } ]; const backLink = `/rulesTest/list?documentType=${encodeURIComponent(pack.documentType)}&mainType=${encodeURIComponent(pack.mainType)}&subtype=${encodeURIComponent(pack.subtype)}`; return (
{currentRule?.name || '未找到评查点'}
{pack.documentType} / {pack.mainType} / {pack.subtype} / {currentRule?.ruleId || '-'}
返回列表
{draftSaved && (
草稿已保存到当前页面状态。本次验证不提交后端,也不会更新 OSS 文件。
)}
{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}

)}
openRuleEditor(currentRule)}>维护依赖} > 当前评查点暂未配置依赖字段。} />
{currentRule.subRules.length > 0 && (
{currentRule.type === 'rule_group' ? `子规则与逻辑(${currentRule.subRules.length}步)` : `规则步骤(${currentRule.subRules.length}步)`} {currentRule.type === 'rule_group' ? '规则组合只维护子规则编号、内容和逻辑表达式,不在此处维护字段库。' : '展示当前评查点 YAML stages 中的每一个检查步骤。'}
{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}` : '当前 YAML 未找到对应规则内容'}
); }) : (
当前规则组合还没有子规则内容。
)}
{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 && (