fix: stabilize rules detail editor flow

This commit is contained in:
wren
2026-05-07 09:59:01 +08:00
parent e7bac9a33f
commit 71476fc919
7 changed files with 1071 additions and 236 deletions
+657 -157
View File
@@ -9,8 +9,8 @@ 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, validateEditableRuleConfig, type DependencyOption, type EditableRuleConfig, type ValidationIssue } from '~/utils/rules-config-editor';
import type { RuleSummary, RuleYamlPack } from '~/utils/rules-yaml-mock.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 = () => [
@@ -35,13 +35,22 @@ type ActionData = {
versionNo?: string;
};
type EditorState = { kind: 'rule'; mode: 'create' | 'edit'; id?: string } | null;
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';
@@ -109,6 +118,40 @@ function phaseLabel(phase: string): string {
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();
return value ? deps.has(value) : false;
});
}
function isStepReferenced(logic: string, stepId: string): boolean {
if (!logic.trim()) return false;
return new RegExp(`(^|[^\\w-])${stepId}([^\\w-]|$)`).test(logic);
@@ -163,6 +206,42 @@ function emptyRuleDraft(group = '未分组'): RuleDraft {
};
}
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';
}
@@ -314,21 +393,18 @@ function validateRule(rule: RuleSummary | undefined, dependencyOptions: Dependen
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import('~/api/auth/check-route-permission.server');
await requireRoutePermission('/rulesTest/detail', userInfo?.role || '', frontendJWT || undefined);
const packId = url.searchParams.get('packId') || url.searchParams.get('id') || '';
const requestedRuleId = url.searchParams.get('ruleId') || '';
const packs = await loadRuleConfigPacks(request);
const pack = (packId ? await loadRuleConfigPack(request, packId) : undefined) || packs[0];
const pack = packId ? await loadRuleConfigPack(request, packId) : undefined;
const resolvedPack = pack || (await loadRuleConfigPacks(request))[0];
if (!pack) {
if (!resolvedPack) {
throw new Response('未找到 YAML 配置', { status: 404 });
}
const versions = await loadRuleConfigVersions(request, pack.metadata.typeId || '');
const versions = await loadRuleConfigVersions(request, resolvedPack.metadata.typeId || '');
return Response.json({ pack, requestedRuleId, versions } satisfies LoaderData);
return Response.json({ pack: resolvedPack, requestedRuleId, versions } satisfies LoaderData);
}
export async function action({ request }: ActionFunctionArgs) {
@@ -413,9 +489,16 @@ export default function RulesTestDetail() {
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[]>([]);
@@ -429,6 +512,9 @@ export default function RulesTestDetail() {
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);
@@ -448,14 +534,15 @@ export default function RulesTestDetail() {
const editableConfig: EditableRuleConfig = useMemo(() => ({
metadata: pack.metadata,
yamlSource: pack.yamlSource,
documentType: pack.documentType,
mainType: pack.mainType,
subtype: pack.subtype,
fields: pack.fields,
subDocuments: pack.subDocuments,
visualElements: pack.visualElements,
fields,
subDocuments,
visualElements,
rules
}), [pack, rules]);
}), [fields, pack, rules, subDocuments, visualElements]);
const dependencyOptions = useMemo(() => collectDependencyOptions(editableConfig), [editableConfig]);
const dependencyOptionMap = useMemo(() => new Map(dependencyOptions.map(option => [option.value, option])), [dependencyOptions]);
@@ -474,6 +561,24 @@ export default function RulesTestDetail() {
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([
@@ -510,7 +615,17 @@ export default function RulesTestDetail() {
const hasErrors = validationIssues.some(issue => issue.severity === 'error');
const configValidationIssues = useMemo(() => validateEditableRuleConfig(editableConfig), [editableConfig]);
const hasConfigErrors = configValidationIssues.some(issue => issue.severity === 'error');
const fullYamlText = useMemo(() => buildYamlPreview(editableConfig), [editableConfig]);
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]);
@@ -519,10 +634,14 @@ export default function RulesTestDetail() {
() => versions.find((item) => !['published', 'rollback'].includes(item.status)),
[versions],
);
const rollbackTargetVersion = useMemo(
() => versions.find((item) => ['published', 'rollback'].includes(item.status) && item.id !== pack.currentVersionId),
const rollbackOptions = useMemo(
() => versions.filter((item) => item.id !== pack.currentVersionId),
[versions, 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(
() => versions.find((item) => item.id === pack.currentVersionId || item.id === pack.fallbackVersionId) || null,
@@ -537,6 +656,10 @@ export default function RulesTestDetail() {
return status || '-';
};
useEffect(() => {
setSelectedRollbackVersionId('');
}, [pack.id, pack.currentVersionId, versions.length]);
useEffect(() => {
if (!saveFetcher.data) return;
if (saveFetcher.data.success) {
@@ -643,6 +766,137 @@ export default function RulesTestDetail() {
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 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('');
setEditor(null);
};
const removeDocument = (documentId: string) => {
setSubDocuments((current) => current.filter((document) => document.id !== documentId));
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 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('');
setEditor(null);
};
const removeVisual = (visualId: string) => {
setVisualElements((current) => current.filter((item) => item.id !== visualId));
setDraftSaved(false);
};
const saveDraftToServer = () => {
if (hasConfigErrors) {
setShowValidation(true);
@@ -650,10 +904,15 @@ export default function RulesTestDetail() {
setSaveMessage('');
return;
}
if (serializedYamlResult.error) {
setSaveError(serializedYamlResult.error);
setSaveMessage('');
return;
}
const formData = new FormData();
formData.append('ruleType', pack.metadata.typeId || '');
formData.append('yamlText', fullYamlText);
formData.append('yamlText', prepareDraftYamlForSave(fullYamlText));
formData.append('changeNote', `保存 ${pack.documentType}/${pack.mainType}/${pack.subtype} 规则配置`);
formData.append('intent', 'save');
saveFetcher.submit(formData, { method: 'post' });
@@ -689,6 +948,9 @@ export default function RulesTestDetail() {
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('');
@@ -761,6 +1023,20 @@ export default function RulesTestDetail() {
<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 || rollbackOptions.length === 0}
>
{rollbackOptions.length === 0 ? (
<option value=""></option>
) : rollbackOptions.map((item) => (
<option key={item.id} value={item.id}>
{item.versionNo} · {versionStatusLabel(item.status)}
</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>
@@ -859,13 +1135,97 @@ export default function RulesTestDetail() {
)}
</Card>
{(pack.fields.length > 0 || pack.subDocuments.length > 0 || pack.visualElements.length > 0) && (
{(fields.length > 0 || subDocuments.length > 0 || visualElements.length > 0) && (
<>
{pack.fields.length > 0 && <span id="fields" className="section-anchor" aria-hidden="true"></span>}
{pack.subDocuments.length > 0 && <span id="sub-documents" className="section-anchor" aria-hidden="true"></span>}
{pack.visualElements.length > 0 && <span id="visual-elements" className="section-anchor" aria-hidden="true"></span>}
{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}项)`}
@@ -979,155 +1339,295 @@ export default function RulesTestDetail() {
<aside className="rules-drawer" aria-label="评查点编辑">
<div className="rules-drawer-header">
<div>
<h3>{editor.mode === 'edit' ? '编辑评查点' : '新增评查点'}</h3>
<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>
<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">
{editor.kind === 'rule' && (
<div className="drawer-form">
<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>
<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>
<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>
<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>
)) : 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>
)}
))}
<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>
<input
value={ruleDraft.logic}
onChange={event => setRuleDraft({ ...ruleDraft, logic: event.target.value })}
placeholder="如:1 AND 2,或 JK-002 AND JK-005"
/>
<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>
<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>
<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>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveVisual}></Button>
</div>
</label>
<label>
<span></span>
<textarea value={ruleDraft.description} onChange={event => setRuleDraft({ ...ruleDraft, description: event.target.value })} placeholder="用业务语言描述该评查点如何判断" />
</label>
<div className="drawer-actions">
<Button type="default" onClick={() => setEditor(null)}></Button>
<Button type="primary" onClick={saveRule}></Button>
</div>
</div>
)}
</aside>
{dependencyDialogOpen && (
<div className="dependency-dialog-shell" role="dialog" aria-modal="true" aria-label="追加依赖字段">
-4
View File
@@ -84,10 +84,6 @@ function riskColor(risk: string): TagColor {
export async function loader({ request }: LoaderFunctionArgs) {
const url = new URL(request.url);
const { getUserSession } = await import("~/api/login/auth.server");
const { frontendJWT, userInfo } = await getUserSession(request);
const { requireRoutePermission } = await import("~/api/auth/check-route-permission.server");
await requireRoutePermission("/rulesTest/list", userInfo?.role || "", frontendJWT || undefined);
const requestedMainType = url.searchParams.get('mainType') || url.searchParams.get('ruleTypeName') || '';
const requestedSubtype = url.searchParams.get('subtype') || url.searchParams.get('documentAttributeType') || '';
const requestedRuleGroup = url.searchParams.get('ruleGroup') || url.searchParams.getAll('ruleGroups')[0] || '';