完善规则配置页交互细节

This commit is contained in:
2026-04-29 18:09:56 +08:00
committed by wren
parent 2eb40e8af6
commit 9e0909ab35
5 changed files with 195 additions and 55 deletions
+64 -26
View File
@@ -1,7 +1,7 @@
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 { useEffect, useMemo, useRef, useState } from 'react';
import { Button } from '~/components/ui/Button';
import { Card } from '~/components/ui/Card';
import { Table } from '~/components/ui/Table';
@@ -328,6 +328,7 @@ export default function RulesTestDetail() {
const [showValidation, setShowValidation] = useState(false);
const [showYamlPreview, setShowYamlPreview] = useState(false);
const [draftSaved, setDraftSaved] = useState(false);
const promptEditorRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
setRules(pack.rules);
@@ -448,6 +449,33 @@ export default function RulesTestDetail() {
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 saveRule = () => {
if (!editor || editor.kind !== 'rule') return;
const existingRule = editor.id ? rules.find(rule => rule.id === editor.id) : undefined;
@@ -488,7 +516,6 @@ export default function RulesTestDetail() {
render: (_: unknown, record: DependencyOption) => (
<div className="rule-name">
<strong>{record.label}</strong>
<span>YAML引用{record.value}</span>
</div>
)
},
@@ -540,7 +567,7 @@ export default function RulesTestDetail() {
{draftSaved && (
<div className="draft-tip">
<i className="ri-checkbox-circle-line"></i>
稿 OSS
稿
</div>
)}
</Card>
@@ -615,10 +642,16 @@ export default function RulesTestDetail() {
)}
</Card>
{(pack.fields.length > 0 || pack.subDocuments.length > 0 || pack.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>}
</>
)}
<Card
className="ant-card"
title={`依赖字段 (${currentRule.dependencies.length}项)`}
extra={<Button size="small" type="primary" icon="ri-add-line" onClick={() => openRuleEditor(currentRule)}></Button>}
>
<Table
className="rules-test-table"
@@ -629,6 +662,7 @@ export default function RulesTestDetail() {
/>
</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 && (
@@ -636,13 +670,12 @@ export default function RulesTestDetail() {
<div className="drawer-subsection-header">
<div>
<strong>{currentRule.type === 'rule_group' ? `子规则与逻辑(${currentRule.subRules.length}步)` : `规则步骤(${currentRule.subRules.length}步)`}</strong>
<span>{currentRule.type === 'rule_group' ? '规则组合只维护子规则编号、内容和逻辑表达式,不在此处维护字段库。' : '展示当前评查点 YAML stages 中的每一个检查步骤。'}</span>
</div>
</div>
<div className="subrule-list">
{currentRule.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<Tag color="gray" size="sm">{subRule.id}</Tag>
<span className="subrule-index">{subRule.id}</span>
<div>
<strong>
{checkTypeLabel(subRule.check)}
@@ -671,7 +704,6 @@ export default function RulesTestDetail() {
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<div className="subrule-list">
@@ -679,10 +711,10 @@ export default function RulesTestDetail() {
const referencedRule = rulesById.get(ruleId);
return (
<div key={ruleId} className="subrule-item">
<Tag color="gray" size="sm">{ruleId}</Tag>
<span className="subrule-index">{ruleId}</span>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</span>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '未找到对应规则内容'}</span>
</div>
</div>
);
@@ -702,7 +734,6 @@ export default function RulesTestDetail() {
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<pre className="rule-prompt-preview">{currentRule.prompt || '当前评查点尚未维护提示词。'}</pre>
@@ -732,7 +763,6 @@ export default function RulesTestDetail() {
<div className="rules-drawer-header">
<div>
<h3>{editor.mode === 'edit' ? '编辑评查点' : '新增评查点'}</h3>
<p></p>
</div>
<button type="button" className="drawer-close" onClick={() => setEditor(null)}><i className="ri-close-line"></i></button>
</div>
@@ -792,6 +822,7 @@ export default function RulesTestDetail() {
<label>
<span></span>
<textarea
ref={promptEditorRef}
className="prompt-editor"
value={ruleDraft.prompt}
onChange={event => setRuleDraft({ ...ruleDraft, prompt: event.target.value })}
@@ -804,13 +835,12 @@ export default function RulesTestDetail() {
<div className="drawer-subsection-header">
<div>
<strong></strong>
<span></span>
</div>
</div>
<div className="subrule-list">
{ruleDraft.subRules.length > 0 ? ruleDraft.subRules.map(subRule => (
<div key={subRule.id} className="subrule-item">
<Tag color="gray" size="sm">{subRule.id}</Tag>
<span className="subrule-index">{subRule.id}</span>
<div>
<strong>{checkTypeLabel(subRule.check)}</strong>
<span>{subRule.content}</span>
@@ -820,10 +850,10 @@ export default function RulesTestDetail() {
const referencedRule = rulesById.get(ruleId);
return (
<div key={ruleId} className="subrule-item">
<Tag color="gray" size="sm">{ruleId}</Tag>
<span className="subrule-index">{ruleId}</span>
<div>
<strong>{referencedRule?.name || '引用规则'}</strong>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</span>
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '未找到对应规则内容'}</span>
</div>
</div>
);
@@ -847,18 +877,27 @@ export default function RulesTestDetail() {
{selectedDependencyOptions.length === 0 ? (
<div className="drawer-empty"></div>
) : selectedDependencyOptions.map(option => (
<Tag
<span
key={option.value}
color="green"
size="sm"
closable
onClose={() => setRuleDraft({
...ruleDraft,
dependencies: ruleDraft.dependencies.filter(dependency => dependency !== option.value)
})}
className="dependency-variable-button"
>
{option.label}
</Tag>
<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>
@@ -880,7 +919,6 @@ export default function RulesTestDetail() {
<div className="dependency-dialog-header">
<div>
<h3></h3>
<p></p>
</div>
<button type="button" className="drawer-close" onClick={() => setDependencyDialogOpen(false)}><i className="ri-close-line"></i></button>
</div>
+27 -17
View File
@@ -104,12 +104,19 @@ export async function loader({ request }: LoaderFunctionArgs) {
? requestedFilters.subtype
: ''
};
const scopedPacks = packs.filter(pack =>
const scopedByMainTypePacks = packs.filter(pack =>
pack.documentType === scopedFilters.documentType &&
(!scopedFilters.mainType || pack.mainType === scopedFilters.mainType) &&
(!scopedFilters.subtype || pack.subtype === scopedFilters.subtype)
(!scopedFilters.mainType || pack.mainType === scopedFilters.mainType)
);
const ruleGroupOptions = unique(scopedPacks.flatMap(pack => pack.rules.map(rule => rule.group))).sort((a, b) => a.localeCompare(b, 'zh-CN'));
const subtypeOptions = unique(scopedByMainTypePacks.map(pack => pack.subtype));
const ruleGroupSourcePacks = scopedFilters.subtype
? scopedByMainTypePacks.filter(pack => pack.subtype === scopedFilters.subtype)
: subtypeOptions.length <= 1
? scopedByMainTypePacks
: [];
const ruleGroupOptions = unique(
ruleGroupSourcePacks.flatMap(pack => pack.rules.map(rule => rule.group))
).sort((a, b) => a.localeCompare(b, 'zh-CN'));
const filters = {
...scopedFilters,
ruleGroup: ruleGroupOptions.includes(requestedFilters.ruleGroup) ? requestedFilters.ruleGroup : ''
@@ -189,10 +196,7 @@ export async function loader({ request }: LoaderFunctionArgs) {
options: {
documentTypes,
mainTypes: unique(packs.filter(pack => pack.documentType === filters.documentType).map(pack => pack.mainType)),
subtypes: unique(packs.filter(pack =>
pack.documentType === filters.documentType &&
(!filters.mainType || pack.mainType === filters.mainType)
).map(pack => pack.subtype)),
subtypes: subtypeOptions,
ruleGroups: ruleGroupOptions
}
} satisfies LoaderData);
@@ -222,6 +226,10 @@ export default function RulesTestList() {
const handleFilterChange = (event: React.ChangeEvent<HTMLSelectElement>) => {
const { name, value } = event.target;
if (name === 'subtype') {
updateParams({ subtype: value, documentAttributeType: undefined, ruleGroup: undefined });
return;
}
updateParams({ [name]: value });
};
@@ -336,15 +344,6 @@ export default function RulesTestList() {
</Button>
}
>
<FilterSelect
label="所属规则组"
name="ruleGroup"
value={filters.ruleGroup}
options={options.ruleGroups.map(group => ({ value: group, label: group }))}
onChange={handleFilterChange}
className="mr-3 w-[22%]"
/>
<FilterSelect
label="子类型"
name="subtype"
@@ -354,6 +353,17 @@ export default function RulesTestList() {
className="mr-3 w-[18%]"
/>
<FilterSelect
label="所属规则组"
name="ruleGroup"
value={filters.ruleGroup}
options={options.ruleGroups.map(group => ({ value: group, label: group }))}
onChange={handleFilterChange}
className="mr-3 w-[22%]"
disabled={options.ruleGroups.length === 0}
placeholder={filters.subtype || options.subtypes.length <= 1 ? '全部' : '请先选择子类型'}
/>
<SearchFilter
key={filters.keyword}
label="搜索"