完善规则配置页交互细节
This commit is contained in:
@@ -13,12 +13,14 @@ interface FilterSelectProps {
|
||||
options: FilterOption[];
|
||||
onChange: (e: React.ChangeEvent<HTMLSelectElement>) => void;
|
||||
className?: string;
|
||||
disabled?: boolean;
|
||||
placeholder?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选下拉选择框组件
|
||||
*/
|
||||
const FilterSelect = ({ label, name, value, options, onChange, className = '' }: FilterSelectProps) => (
|
||||
const FilterSelect = ({ label, name, value, options, onChange, className = '', disabled = false, placeholder = '全部' }: FilterSelectProps) => (
|
||||
<div className={`filter-item ${className}`}>
|
||||
<label className="filter-label">{label}</label>
|
||||
<select
|
||||
@@ -26,8 +28,9 @@ const FilterSelect = ({ label, name, value, options, onChange, className = '' }:
|
||||
name={name}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<option value="">全部</option>
|
||||
<option value="">{placeholder}</option>
|
||||
{options.map(option => (
|
||||
<option key={option.value} value={option.value}>{option.label}</option>
|
||||
))}
|
||||
@@ -201,4 +204,4 @@ export function DateRangeFilter({
|
||||
}
|
||||
|
||||
// 导出筛选下拉框组件和日期范围筛选组件
|
||||
export { FilterSelect };
|
||||
export { FilterSelect };
|
||||
|
||||
@@ -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="搜索"
|
||||
|
||||
@@ -9,6 +9,14 @@
|
||||
scroll-margin-top: 100px;
|
||||
}
|
||||
|
||||
.rules-test-page .tag.tag-sm {
|
||||
min-height: 20px;
|
||||
padding: 2px 8px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rules-test-page #fields,
|
||||
.rules-test-page #sub-documents,
|
||||
.rules-test-page #visual-elements,
|
||||
@@ -844,6 +852,63 @@
|
||||
background: #fbfdfc;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-button {
|
||||
display: inline-flex;
|
||||
align-items: stretch;
|
||||
min-height: 32px;
|
||||
overflow: hidden;
|
||||
border: 1px solid #00684a;
|
||||
border-radius: 6px;
|
||||
background: #00684a;
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-button:hover {
|
||||
border-color: #005a3f;
|
||||
background: #005a3f;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-main,
|
||||
.rules-test-page .dependency-variable-remove {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: inherit;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-main {
|
||||
max-width: 260px;
|
||||
padding: 7px 10px 7px 12px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-remove {
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.28);
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-remove:hover {
|
||||
background: rgba(255, 255, 255, 0.16);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rules-test-page .dependency-variable-main:focus-visible,
|
||||
.rules-test-page .dependency-variable-remove:focus-visible {
|
||||
outline: 2px solid rgba(0, 104, 74, 0.18);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.rules-test-page .subrule-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
@@ -851,7 +916,7 @@
|
||||
|
||||
.rules-test-page .subrule-item {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
grid-template-columns: 34px minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 10px;
|
||||
padding: 9px 10px;
|
||||
@@ -860,6 +925,22 @@
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.rules-test-page .subrule-index {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
min-width: 30px;
|
||||
border-radius: 50%;
|
||||
background: #00684a;
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 650;
|
||||
line-height: 1;
|
||||
box-shadow: 0 2px 6px rgba(0, 104, 74, 0.18);
|
||||
}
|
||||
|
||||
.rules-test-page .subrule-item div {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -882,6 +963,12 @@
|
||||
line-height: 1.45;
|
||||
}
|
||||
|
||||
.rules-test-page .subrule-item > .subrule-index {
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.rules-test-page .drawer-field-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -54,12 +54,13 @@ export function getDefaultExpandedDependencyGroups(options: DependencyOption[],
|
||||
|
||||
export function collectDependencyOptions(config: Pick<EditableRuleConfig, 'fields' | 'subDocuments' | 'visualElements'>): DependencyOption[] {
|
||||
const topLevelFields = config.fields.flatMap(field => {
|
||||
const source = field.group ? `字段抽取 / ${field.group}` : '字段抽取';
|
||||
const source = '字段抽取';
|
||||
const group = field.group || '未分组';
|
||||
const options = [{
|
||||
value: field.name,
|
||||
label: field.name,
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
}];
|
||||
if (field.group === '派生字段') {
|
||||
options.push({
|
||||
@@ -74,7 +75,7 @@ export function collectDependencyOptions(config: Pick<EditableRuleConfig, 'field
|
||||
value: field.name.replace('[*].', '.'),
|
||||
label: field.name.replace('[*].', ' / '),
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
});
|
||||
}
|
||||
return options;
|
||||
@@ -105,31 +106,32 @@ export function collectDependencyOptions(config: Pick<EditableRuleConfig, 'field
|
||||
|
||||
const visualElements = config.visualElements.flatMap(item => {
|
||||
const label = item.name || item.id;
|
||||
const source = `视觉要素 / ${item.type}`;
|
||||
const source = '视觉要素';
|
||||
const group = item.type || '未分组';
|
||||
return [
|
||||
{
|
||||
value: item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
},
|
||||
{
|
||||
value: item.name || item.id,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
},
|
||||
{
|
||||
value: `visual.${item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
},
|
||||
{
|
||||
value: `visual.${item.name || item.id}`,
|
||||
label,
|
||||
source,
|
||||
group: source
|
||||
group
|
||||
},
|
||||
{
|
||||
value: item.type,
|
||||
|
||||
Reference in New Issue
Block a user