完善规则配置页交互细节
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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="搜索"
|
||||
|
||||
Reference in New Issue
Block a user