保存规则库 YAML 维护改造进展
This commit is contained in:
@@ -810,7 +810,7 @@ export default function DocumentsIndex() {
|
||||
if (typeof window !== 'undefined') {
|
||||
sessionStorage.setItem(SEARCH_PARAMS_STORAGE_KEY, searchParams.toString());
|
||||
}
|
||||
navigate(`/reviews?id=${fileId}&previousRoute=documents`);
|
||||
navigate(`/reviewsTest?id=${fileId}&previousRoute=documents`);
|
||||
};
|
||||
|
||||
// 处理附件追加文件选择
|
||||
@@ -1163,8 +1163,8 @@ export default function DocumentsIndex() {
|
||||
{/* 查看按钮 - 需要 document:document:view 权限 */}
|
||||
{canView && (
|
||||
<Link
|
||||
to={`/reviews?id=${historyDoc.id}&previousRoute=documents`}
|
||||
// to={`/reviews?id=${historyDoc.id}&previousRoute=documents`}
|
||||
to={`/reviewsTest?id=${historyDoc.id}&previousRoute=documents`}
|
||||
// to={`/reviewsTest?id=${historyDoc.id}&previousRoute=documents`}
|
||||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
@@ -1422,7 +1422,7 @@ export default function DocumentsIndex() {
|
||||
</Link>
|
||||
) : (
|
||||
<Link
|
||||
to={`/reviews?id=${record.id}&previousRoute=documents`}
|
||||
to={`/reviewsTest?id=${record.id}&previousRoute=documents`}
|
||||
className="text-xs px-2 py-1 h-7 mr-1 hover:underline"
|
||||
>
|
||||
<i className="ri-eye-line"></i>
|
||||
|
||||
@@ -2087,8 +2087,8 @@ export default function FilesUpload() {
|
||||
setTimeout(() => {
|
||||
try {
|
||||
if (isMountedRef.current) {
|
||||
// console.log(`【调试-handleViewFile】执行导航,URL: /reviews?id=${record.id}&previousRoute=filesUpload`);
|
||||
navigate(`/reviews?id=${record.id}&previousRoute=filesUpload`);
|
||||
// console.log(`【调试-handleViewFile】执行导航,URL: /reviewsTest?id=${record.id}&previousRoute=filesUpload`);
|
||||
navigate(`/reviewsTest?id=${record.id}&previousRoute=filesUpload`);
|
||||
} else {
|
||||
console.error('【调试-handleViewFile】组件已卸载,取消延迟导航');
|
||||
}
|
||||
@@ -3034,4 +3034,4 @@ export function ErrorBoundary({ error }: { error?: Error }) {
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -390,7 +390,7 @@ export default function Home() {
|
||||
<div className="shortcut-grid">
|
||||
<ShortcutItem icon="ri-upload-cloud-line" label="上传文件" to="/files/upload" />
|
||||
<ShortcutItem icon="ri-file-list-3-line" label="文档列表" to="/documents/list" />
|
||||
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules" />
|
||||
<ShortcutItem icon="ri-list-check-3" label="评查点列表" to="/rules/list" />
|
||||
<ShortcutItem icon="ri-folder-open-line" label="评查点分组" to="/rule-groups" />
|
||||
</div>
|
||||
</Card> */}
|
||||
|
||||
+300
-55
@@ -9,7 +9,7 @@ import { Switch } from '~/components/ui/Switch';
|
||||
import { TableRowSkeleton, LoadingIndicator, NumberSkeleton } from '~/components/ui/SkeletonScreen';
|
||||
import rulesStyles from "~/styles/pages/rules_index.css?url";
|
||||
import type { Rule, RuleType, RulePriority } from '~/models/rule';
|
||||
import { RULE_TYPE_COLORS, RULE_PRIORITY_LABELS, RULE_PRIORITY_COLORS } from '~/models/rule';
|
||||
import { RULE_PRIORITY_LABELS, RULE_PRIORITY_COLORS } from '~/models/rule';
|
||||
import type { TagColor } from '~/components/ui/Tag';
|
||||
import { Table } from '~/components/ui/Table';
|
||||
import { FilterPanel, FilterSelect, SearchFilter } from '~/components/ui/FilterPanel';
|
||||
@@ -22,12 +22,15 @@ import {
|
||||
deleteRule,
|
||||
getRuleTypes,
|
||||
getRuleGroupsByType,
|
||||
getAttributeTypes,
|
||||
batchUpdateRuleStatus,
|
||||
batchDeleteRules,
|
||||
updateEvaluationPoint,
|
||||
type RuleType as ApiRuleType,
|
||||
type RuleGroup
|
||||
type RuleGroup,
|
||||
type AttributeTypeOption
|
||||
} from '~/api/evaluation_points/rules';
|
||||
import { CONTRACT_TYPES } from '~/constants/contractTypes';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: "stylesheet", href: rulesStyles }
|
||||
@@ -79,6 +82,131 @@ interface ActionResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
type SubtypeOption = {
|
||||
code: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
const ADMIN_LICENSE_SUBTYPE_OPTIONS: SubtypeOption[] = [
|
||||
{ code: '新办', label: '新办' },
|
||||
{ code: '变更', label: '变更' },
|
||||
{ code: '延续', label: '延续' },
|
||||
{ code: '停业', label: '停业' },
|
||||
{ code: '歇业', label: '歇业' },
|
||||
{ code: '补办', label: '补办' },
|
||||
{ code: '恢复营业', label: '恢复营业' },
|
||||
{ code: '收回', label: '收回' },
|
||||
{ code: '注销', label: '注销' }
|
||||
];
|
||||
|
||||
const CONTRACT_SUBTYPE_OPTIONS: SubtypeOption[] = CONTRACT_TYPES.map(type => ({
|
||||
code: type.value === '通用' ? '通用' : type.label,
|
||||
label: type.label
|
||||
}));
|
||||
const SUBTYPE_DISCOVERY_PAGE_SIZE = 100;
|
||||
|
||||
function uniqueOptions(options: SubtypeOption[]): SubtypeOption[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter(option => {
|
||||
if (!option.code || seen.has(option.code)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.code);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function matchesOption(option: AttributeTypeOption, target: SubtypeOption): boolean {
|
||||
return [option.code, option.label].some(value =>
|
||||
value === target.code ||
|
||||
value === target.label ||
|
||||
value.replace(/合同$/, '') === target.code ||
|
||||
target.label.replace(/合同$/, '') === value
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSubtypeCode(value: string | undefined, options: SubtypeOption[]): string | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const matchedOption = options.find(option =>
|
||||
option.code === value ||
|
||||
option.label === value ||
|
||||
option.label.replace(/合同$/, '') === value
|
||||
);
|
||||
|
||||
return matchedOption?.code || value;
|
||||
}
|
||||
|
||||
function matchRuleTypeByName(ruleTypes: ApiRuleType[], ruleTypeName?: string | null): ApiRuleType | undefined {
|
||||
if (!ruleTypeName) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return ruleTypes.find(type =>
|
||||
type.name === ruleTypeName ||
|
||||
type.name.includes(ruleTypeName) ||
|
||||
ruleTypeName.includes(type.name)
|
||||
);
|
||||
}
|
||||
|
||||
function resolveCurrentRuleTypeId(
|
||||
ruleTypes: ApiRuleType[],
|
||||
ruleTypeId?: string | null,
|
||||
ruleTypeName?: string | null
|
||||
): string | undefined {
|
||||
if (ruleTypeId && ruleTypeId !== 'all') {
|
||||
return ruleTypeId;
|
||||
}
|
||||
|
||||
return matchRuleTypeByName(ruleTypes, ruleTypeName)?.id || ruleTypes[0]?.id;
|
||||
}
|
||||
|
||||
function resolveScopedSubtypeOptions(
|
||||
params: {
|
||||
documentTypeIds: number[];
|
||||
selectedModuleName: string;
|
||||
ruleTypeName?: string | null;
|
||||
apiOptions: AttributeTypeOption[];
|
||||
ruleOptions: SubtypeOption[];
|
||||
}
|
||||
): SubtypeOption[] {
|
||||
const { documentTypeIds, selectedModuleName, ruleTypeName, apiOptions, ruleOptions } = params;
|
||||
const isContractModule = selectedModuleName.includes('合同') || documentTypeIds.includes(1);
|
||||
const isCaseFileModule = selectedModuleName.includes('案卷') || selectedModuleName.includes('卷宗') || documentTypeIds.includes(2) || documentTypeIds.includes(3);
|
||||
|
||||
if (isContractModule) {
|
||||
const apiContractOptions = CONTRACT_SUBTYPE_OPTIONS.flatMap(target =>
|
||||
apiOptions
|
||||
.filter(option => matchesOption(option, target))
|
||||
.map(option => ({ code: option.code, label: option.label }))
|
||||
);
|
||||
|
||||
return uniqueOptions(apiContractOptions);
|
||||
}
|
||||
|
||||
if (isCaseFileModule && ruleTypeName?.includes('行政许可')) {
|
||||
const apiLicenseOptions = ADMIN_LICENSE_SUBTYPE_OPTIONS.flatMap(target =>
|
||||
apiOptions
|
||||
.filter(option => matchesOption(option, target))
|
||||
.map(option => ({ code: option.code, label: option.label }))
|
||||
);
|
||||
|
||||
return uniqueOptions(apiLicenseOptions);
|
||||
}
|
||||
|
||||
if (isCaseFileModule && ruleTypeName?.includes('行政处罚')) {
|
||||
const punishmentOptions = apiOptions
|
||||
.filter(option => option.code === '通用' || option.label === '通用')
|
||||
.map(option => ({ code: option.code, label: option.label }));
|
||||
|
||||
return uniqueOptions(punishmentOptions.length > 0 ? punishmentOptions : [{ code: '通用', label: '通用' }]);
|
||||
}
|
||||
|
||||
return uniqueOptions(ruleOptions);
|
||||
}
|
||||
|
||||
function mapApiRuleToModel(apiRule: ApiRule): Rule {
|
||||
// 🔑 清洗评查点编码:移除最后一个 '--' 及其后面的字符
|
||||
// 例如:'code-mis--mz' --> 'code-mis', 'code-mbs--alsi--gz' --> 'code-mbs--alsi'
|
||||
@@ -220,6 +348,7 @@ export default function RulesIndex() {
|
||||
const [filteredRules, setFilteredRules] = useState<Rule[]>(initialRules);
|
||||
const [filteredTotalCount, setFilteredTotalCount] = useState<number>(initialTotalCount);
|
||||
const [ruleTypes, setRuleTypes] = useState<ApiRuleType[]>(initialRuleTypes);
|
||||
const [attributeTypes, setAttributeTypes] = useState<Array<{ code: string; label: string }>>([]);
|
||||
|
||||
// 添加一个状态来跟踪是否执行了删除操作
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
@@ -235,9 +364,11 @@ export default function RulesIndex() {
|
||||
|
||||
// 获取当前的ruleType值
|
||||
const ruleTypeParam = searchParams.get('ruleType');
|
||||
const ruleTypeNameParam = searchParams.get('ruleTypeName');
|
||||
const selectedRuleTypeId = resolveCurrentRuleTypeId(ruleTypes, ruleTypeParam, ruleTypeNameParam) || '';
|
||||
|
||||
// 判断是否禁用规则组选择
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !ruleTypeParam || ruleGroups.length === 0;
|
||||
const isRuleGroupSelectDisabled = loadingGroups || !selectedRuleTypeId || ruleGroups.length === 0;
|
||||
|
||||
// 在组件渲染时初始化状态
|
||||
// useEffect(() => {
|
||||
@@ -267,6 +398,9 @@ export default function RulesIndex() {
|
||||
// 🔑 从 sessionStorage 获取 documentTypeIds
|
||||
const typeIdsStr = typeof window !== 'undefined' ? sessionStorage.getItem('documentTypeIds') : null;
|
||||
const documentTypeIds = typeIdsStr ? JSON.parse(typeIdsStr) : null;
|
||||
const selectedModuleName = typeof window !== 'undefined'
|
||||
? sessionStorage.getItem('selectedModuleName') || ''
|
||||
: '';
|
||||
|
||||
if (!documentTypeIds || documentTypeIds.length === 0) {
|
||||
console.warn('无法加载评查点数据:未找到 documentTypeIds');
|
||||
@@ -306,31 +440,139 @@ export default function RulesIndex() {
|
||||
console.error('加载评查点类型失败:', error);
|
||||
}
|
||||
|
||||
// 构建查询参数
|
||||
// 🔑 当选择"全部"或未选择评查点类型时,使用下拉框中所有评查点类型的 id 组合
|
||||
let finalRuleType: string | undefined = undefined;
|
||||
if (ruleTypeParam && ruleTypeParam !== 'all') {
|
||||
// 选择了具体的评查点类型
|
||||
finalRuleType = ruleTypeParam;
|
||||
} else if (loadedRuleTypes && loadedRuleTypes.length > 0) {
|
||||
// 选择"全部"或未选择,使用刚加载的评查点类型的 id
|
||||
finalRuleType = loadedRuleTypes.map(type => type.id).join(',');
|
||||
// console.log("📋 [fetchData] 选择全部类型,使用 loadedRuleTypes 的 id 组合:", finalRuleType);
|
||||
// 主类型筛选已从界面隐藏,未显式传参时默认使用当前模块的第一个主类型。
|
||||
const finalRuleType = resolveCurrentRuleTypeId(loadedRuleTypes, ruleTypeParam, ruleTypeNameParam);
|
||||
|
||||
let apiAttributeTypes: AttributeTypeOption[] = [];
|
||||
try {
|
||||
const attributeTypesResponse = await getAttributeTypes(loaderData.frontendJWT);
|
||||
if (attributeTypesResponse.data) {
|
||||
apiAttributeTypes = attributeTypesResponse.data;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载子类型枚举失败:', error);
|
||||
}
|
||||
|
||||
const queryParams = {
|
||||
let effectiveAttributeType = searchParams.get('documentAttributeType') || undefined;
|
||||
const presetScopedAttributeTypes = resolveScopedSubtypeOptions({
|
||||
documentTypeIds,
|
||||
selectedModuleName,
|
||||
ruleTypeName: ruleTypeNameParam,
|
||||
apiOptions: apiAttributeTypes,
|
||||
ruleOptions: []
|
||||
});
|
||||
|
||||
if (presetScopedAttributeTypes.length > 0) {
|
||||
setAttributeTypes(presetScopedAttributeTypes);
|
||||
const originalAttributeType = effectiveAttributeType;
|
||||
effectiveAttributeType = normalizeSubtypeCode(effectiveAttributeType, presetScopedAttributeTypes);
|
||||
|
||||
if (effectiveAttributeType && originalAttributeType !== effectiveAttributeType) {
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
nextParams.set('documentAttributeType', effectiveAttributeType);
|
||||
nextParams.set('page', '1');
|
||||
setSearchParams(nextParams);
|
||||
} else if (effectiveAttributeType && !presetScopedAttributeTypes.some(type => type.code === effectiveAttributeType)) {
|
||||
effectiveAttributeType = undefined;
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
nextParams.delete('documentAttributeType');
|
||||
nextParams.set('page', '1');
|
||||
setSearchParams(nextParams);
|
||||
}
|
||||
} else if (finalRuleType) {
|
||||
const attributeResponse = await getRulesList({
|
||||
ruleType: finalRuleType,
|
||||
page: 1,
|
||||
pageSize: SUBTYPE_DISCOVERY_PAGE_SIZE,
|
||||
token: loaderData.frontendJWT
|
||||
});
|
||||
|
||||
if (attributeResponse.data) {
|
||||
const ruleAttributeTypes = Array.from(
|
||||
new Set(
|
||||
attributeResponse.data.rules
|
||||
.map(rule => rule.documentAttributeType)
|
||||
.filter((value): value is string => Boolean(value))
|
||||
)
|
||||
).map(value => ({ code: value, label: value }));
|
||||
const scopedAttributeTypes = resolveScopedSubtypeOptions({
|
||||
documentTypeIds,
|
||||
selectedModuleName,
|
||||
ruleTypeName: ruleTypeNameParam,
|
||||
apiOptions: apiAttributeTypes,
|
||||
ruleOptions: ruleAttributeTypes
|
||||
});
|
||||
|
||||
setAttributeTypes(scopedAttributeTypes);
|
||||
const originalAttributeType = effectiveAttributeType;
|
||||
effectiveAttributeType = normalizeSubtypeCode(effectiveAttributeType, scopedAttributeTypes);
|
||||
|
||||
if (effectiveAttributeType && originalAttributeType !== effectiveAttributeType) {
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
nextParams.set('documentAttributeType', effectiveAttributeType);
|
||||
nextParams.set('page', '1');
|
||||
setSearchParams(nextParams);
|
||||
} else if (effectiveAttributeType && !scopedAttributeTypes.some(type => type.code === effectiveAttributeType)) {
|
||||
effectiveAttributeType = undefined;
|
||||
const nextParams = new URLSearchParams(searchParams);
|
||||
nextParams.delete('documentAttributeType');
|
||||
nextParams.set('page', '1');
|
||||
setSearchParams(nextParams);
|
||||
}
|
||||
} else {
|
||||
setAttributeTypes([]);
|
||||
effectiveAttributeType = undefined;
|
||||
}
|
||||
} else {
|
||||
setAttributeTypes([]);
|
||||
effectiveAttributeType = undefined;
|
||||
}
|
||||
|
||||
const baseQueryParams = {
|
||||
ruleType: finalRuleType,
|
||||
groupId: searchParams.get('groupId') || undefined,
|
||||
isActive: searchParams.get('isActive') ? searchParams.get('isActive') === 'true' : undefined,
|
||||
keyword: searchParams.get('keyword') || undefined,
|
||||
area: userArea, // 添加地区过滤
|
||||
page: currentPage,
|
||||
pageSize,
|
||||
token: loaderData.frontendJWT
|
||||
};
|
||||
|
||||
if (effectiveAttributeType) {
|
||||
const allRules: ApiRule[] = [];
|
||||
let totalCount = 0;
|
||||
let pageToFetch = 1;
|
||||
|
||||
do {
|
||||
const response = await getRulesList({
|
||||
...baseQueryParams,
|
||||
page: pageToFetch,
|
||||
pageSize: SUBTYPE_DISCOVERY_PAGE_SIZE
|
||||
});
|
||||
|
||||
if (!response.data) {
|
||||
break;
|
||||
}
|
||||
|
||||
allRules.push(...(response.data.rules as unknown as ApiRule[]));
|
||||
totalCount = response.data.totalCount || 0;
|
||||
pageToFetch += 1;
|
||||
} while (allRules.length < totalCount);
|
||||
|
||||
const subtypeRules = allRules.filter(rule => rule.documentAttributeType === effectiveAttributeType);
|
||||
const startIndex = (currentPage - 1) * pageSize;
|
||||
const pageRules = subtypeRules.slice(startIndex, startIndex + pageSize);
|
||||
|
||||
setFilteredRules(pageRules.map((apiRule: ApiRule) => mapApiRuleToModel(apiRule)));
|
||||
setFilteredTotalCount(subtypeRules.length);
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用 API 获取数据
|
||||
const response = await getRulesList(queryParams);
|
||||
const response = await getRulesList({
|
||||
...baseQueryParams,
|
||||
page: currentPage,
|
||||
pageSize
|
||||
});
|
||||
|
||||
if (response.data) {
|
||||
const apiRules = response.data.rules || [];
|
||||
@@ -347,12 +589,12 @@ export default function RulesIndex() {
|
||||
setLoading(false);
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
}, [ruleTypeParam, searchParams, currentPage, pageSize, loaderData.frontendJWT]);
|
||||
}, [ruleTypeParam, ruleTypeNameParam, searchParams, currentPage, pageSize, loaderData.frontendJWT, setSearchParams]);
|
||||
|
||||
// 当评查点类型变化时,加载对应的规则组
|
||||
useEffect(() => {
|
||||
// 如果选择了"全部"或未选择,则清空规则组
|
||||
if (!ruleTypeParam || ruleTypeParam === 'all') {
|
||||
if (!selectedRuleTypeId) {
|
||||
setRuleGroups([]);
|
||||
return;
|
||||
}
|
||||
@@ -361,7 +603,7 @@ export default function RulesIndex() {
|
||||
const loadRuleGroups = async () => {
|
||||
setLoadingGroups(true);
|
||||
try {
|
||||
const response = await getRuleGroupsByType(ruleTypeParam, loaderData.frontendJWT);
|
||||
const response = await getRuleGroupsByType(selectedRuleTypeId, loaderData.frontendJWT);
|
||||
if (response.data) {
|
||||
setRuleGroups(response.data);
|
||||
} else if (response.error) {
|
||||
@@ -377,7 +619,7 @@ export default function RulesIndex() {
|
||||
};
|
||||
|
||||
loadRuleGroups();
|
||||
}, [ruleTypeParam]);
|
||||
}, [selectedRuleTypeId, loaderData.frontendJWT]);
|
||||
|
||||
// 使用useEffect监听fetcher状态变化并显示Toast fetcher.state有以下几种状态: 通过fetcher提交数据后,action返回结果,fetcher.state会发生变化
|
||||
// idle: 空闲状态
|
||||
@@ -476,6 +718,7 @@ export default function RulesIndex() {
|
||||
if (value === '' || value === 'all') {
|
||||
setRuleGroups([]);
|
||||
}
|
||||
newParams.delete('ruleTypeName');
|
||||
}
|
||||
} else {
|
||||
newParams.delete(name);
|
||||
@@ -485,6 +728,9 @@ export default function RulesIndex() {
|
||||
newParams.delete('groupId');
|
||||
setRuleGroups([]);
|
||||
}
|
||||
if (name === 'ruleType') {
|
||||
newParams.delete('ruleTypeName');
|
||||
}
|
||||
}
|
||||
|
||||
// 切换筛选条件时,重置到第一页
|
||||
@@ -757,26 +1003,27 @@ export default function RulesIndex() {
|
||||
align: "left" as const,
|
||||
width: "12%"
|
||||
},
|
||||
{
|
||||
title: "评查点类型",
|
||||
key: "ruleType",
|
||||
align: "left" as const,
|
||||
width: "8%",
|
||||
render: (_: unknown, record: Rule) => {
|
||||
const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
|
||||
return (
|
||||
record.ruleType ? <Tag color={typeColor}>
|
||||
{record.ruleType}
|
||||
</Tag> : null
|
||||
);
|
||||
}
|
||||
},
|
||||
// 主类型已拆分到左侧菜单,列表不再重复展示评查点类型列。
|
||||
// {
|
||||
// title: "评查点类型",
|
||||
// key: "ruleType",
|
||||
// align: "left" as const,
|
||||
// width: "8%",
|
||||
// render: (_: unknown, record: Rule) => {
|
||||
// const typeColor = RULE_TYPE_COLORS[record.ruleType] as TagColor;
|
||||
// return (
|
||||
// record.ruleType ? <Tag color={typeColor}>
|
||||
// {record.ruleType}
|
||||
// </Tag> : null
|
||||
// );
|
||||
// }
|
||||
// },
|
||||
{
|
||||
title: "所属规则组",
|
||||
dataIndex: "groupName" as keyof Rule,
|
||||
key: "groupName",
|
||||
align: "left" as const,
|
||||
width: "8%"
|
||||
width: "12%"
|
||||
},
|
||||
{
|
||||
title: "地区",
|
||||
@@ -787,11 +1034,11 @@ export default function RulesIndex() {
|
||||
render: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
title: "属性类型",
|
||||
title: "子类型",
|
||||
dataIndex: "documentAttributeType" as keyof Rule,
|
||||
key: "documentAttributeType",
|
||||
align: "left" as const,
|
||||
width: "6%",
|
||||
width: "10%",
|
||||
render: (value: string) => value || '-'
|
||||
},
|
||||
{
|
||||
@@ -935,33 +1182,31 @@ export default function RulesIndex() {
|
||||
</>
|
||||
}
|
||||
>
|
||||
<FilterSelect
|
||||
label="评查点类型"
|
||||
name="ruleType"
|
||||
value={searchParams.get('ruleType') || ''}
|
||||
options={[
|
||||
...ruleTypes.map((type: ApiRuleType) => ({
|
||||
value: type.id,
|
||||
label: type.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[15%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="所属规则组"
|
||||
name="groupId"
|
||||
value={searchParams.get('groupId') || ''}
|
||||
options={[
|
||||
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择评查点类型" }] : []),
|
||||
...(isRuleGroupSelectDisabled ? [{ value: "", label: "请先选择主类型" }] : []),
|
||||
...ruleGroups.map(group => ({
|
||||
value: group.id,
|
||||
label: group.name
|
||||
}))
|
||||
]}
|
||||
onChange={handleFilterChange}
|
||||
className={`mr-3 w-[20%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
className={`mr-3 w-[22%] ${isRuleGroupSelectDisabled ? 'opacity-50' : ''}`}
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
label="子类型"
|
||||
name="documentAttributeType"
|
||||
value={searchParams.get('documentAttributeType') || ''}
|
||||
options={attributeTypes.map(type => ({
|
||||
value: type.code,
|
||||
label: type.label
|
||||
}))}
|
||||
onChange={handleFilterChange}
|
||||
className="mr-3 w-[18%]"
|
||||
/>
|
||||
|
||||
<FilterSelect
|
||||
@@ -1034,4 +1279,4 @@ export function ErrorBoundary() {
|
||||
<Button type="primary" to="/">返回首页</Button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ export default function RuleNew() {
|
||||
// API返回错误
|
||||
toastService.error(`获取评查点数据失败: ${response.error}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
navigate('/rules/list');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -366,7 +366,7 @@ export default function RuleNew() {
|
||||
console.error('JSON处理错误:', jsonError);
|
||||
toastService.error(`数据处理错误: ${jsonError instanceof Error ? jsonError.message : '未知错误'}`);
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
navigate('/rules/list');
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -374,7 +374,7 @@ export default function RuleNew() {
|
||||
toastService.error(`获取评查点数据失败: ${error instanceof Error ? error.message : '未知错误'}`);
|
||||
// 获取数据失败时返回上一页
|
||||
resetFormData();
|
||||
navigate('/rules');
|
||||
navigate('/rules/list');
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
@@ -909,7 +909,7 @@ export default function RuleNew() {
|
||||
} else {
|
||||
// 无法获取ID的情况
|
||||
toastService.warning(`评查点${isEditMode ? '更新' : '创建'}成功,但无法获取ID。正在返回列表页面。`);
|
||||
navigate('/rules');
|
||||
navigate('/rules/list');
|
||||
}
|
||||
} else {
|
||||
toastService.error('系统繁忙');
|
||||
|
||||
@@ -14,7 +14,7 @@ export const meta: MetaFunction = () => {
|
||||
|
||||
export const handle = {
|
||||
breadcrumb: "评查点列表",
|
||||
to: "/rules/list" // 指定面包屑点击后跳转的路径
|
||||
to: "/rulesTest/list" // 新版规则维护入口;旧版可从新版页面内返回
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -22,4 +22,4 @@ export const handle = {
|
||||
*/
|
||||
export default function RulesLayout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,948 @@
|
||||
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<RuleSummary, 'id' | 'ruleId' | 'name' | 'group' | 'risk' | 'score' | 'type' | 'logic' | 'subRules' | 'subRuleIds' | 'prompt' | 'description'> & {
|
||||
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 | undefined>): string[] {
|
||||
return Array.from(new Set(values.map(value => value?.trim()).filter(Boolean) as string[]));
|
||||
}
|
||||
|
||||
function uniqueDependencyOptions(options: DependencyOption[]): DependencyOption[] {
|
||||
const seen = new Set<string>();
|
||||
return options.filter(option => {
|
||||
if (!option.value || seen.has(option.value)) {
|
||||
return false;
|
||||
}
|
||||
seen.add(option.value);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function ruleKey(rule: Pick<RuleSummary, 'id' | 'ruleId'>): string {
|
||||
return rule.ruleId || rule.id;
|
||||
}
|
||||
|
||||
function ruleTypeLabel(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
deterministic: '确定性检查',
|
||||
ai_rule: '智能语义检查',
|
||||
rule_group: '规则组合',
|
||||
llm: '智能语义检查',
|
||||
manual: '人工复核'
|
||||
};
|
||||
return labels[type] ? `${labels[type]} (${type})` : type || '-';
|
||||
}
|
||||
|
||||
function checkTypeLabel(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
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<string, string> = {
|
||||
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<string, DependencyOption>): 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 <span key={index} className="yaml-line"> </span>;
|
||||
}
|
||||
|
||||
if (content.startsWith('#')) {
|
||||
return (
|
||||
<span key={index} className="yaml-line">
|
||||
<span className="yaml-value">{content}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (listMatch) {
|
||||
return (
|
||||
<span key={index} className="yaml-line">
|
||||
<span className="yaml-indent">{indent}</span>
|
||||
<span className="yaml-marker">{listMatch[1]}</span>
|
||||
<span className="yaml-key">{listMatch[2]}</span>
|
||||
<YamlValue value={listMatch[3]} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (keyMatch) {
|
||||
return (
|
||||
<span key={index} className="yaml-line">
|
||||
<span className="yaml-indent">{indent}</span>
|
||||
<span className="yaml-key">{keyMatch[1]}</span>
|
||||
<YamlValue value={keyMatch[2]} />
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<span key={index} className="yaml-line">
|
||||
<span className="yaml-indent">{indent}</span>
|
||||
<span>{content}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
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 <span className={className}>{value}</span>;
|
||||
}
|
||||
|
||||
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<typeof loader>() as LoaderData;
|
||||
const initialRuleKey = requestedRuleId || ruleKey(pack.rules[0] || { id: '', ruleId: '' });
|
||||
const [rules, setRules] = useState<RuleSummary[]>(pack.rules);
|
||||
const [selectedRuleKey, setSelectedRuleKey] = useState(initialRuleKey);
|
||||
const [editor, setEditor] = useState<EditorState>(null);
|
||||
const [ruleDraft, setRuleDraft] = useState<RuleDraft>(emptyRuleDraft(pack.rules[0]?.group));
|
||||
const [dependencyDialogOpen, setDependencyDialogOpen] = useState(false);
|
||||
const [dependencySearch, setDependencySearch] = useState('');
|
||||
const [dependencySelection, setDependencySelection] = useState<string[]>([]);
|
||||
const [expandedDependencyGroups, setExpandedDependencyGroups] = useState<string[]>([]);
|
||||
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<string, typeof filteredDependencyOptions>();
|
||||
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) => (
|
||||
<div className="rule-name">
|
||||
<strong>{record.label}</strong>
|
||||
<span>YAML引用:{record.value}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
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 (
|
||||
<div className="rules-test-page rules-page">
|
||||
<div className="yaml-layout-single">
|
||||
<Card className="ant-card config-toolbar-card">
|
||||
<div className="config-toolbar">
|
||||
<div>
|
||||
<div className="config-toolbar-title">{currentRule?.name || '未找到评查点'}</div>
|
||||
<div className="config-toolbar-desc">
|
||||
{pack.documentType} / {pack.mainType} / {pack.subtype} / {currentRule?.ruleId || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="config-toolbar-actions">
|
||||
<Link to={backLink} className="ant-btn ant-btn-default">
|
||||
<i className="ri-arrow-left-line mr-1.5"></i>返回列表
|
||||
</Link>
|
||||
<button type="button" className="ant-btn ant-btn-default" onClick={() => setShowValidation(current => !current)}>
|
||||
<i className="ri-shield-check-line mr-1.5"></i>校验评查点
|
||||
</button>
|
||||
<button type="button" className="ant-btn ant-btn-default" onClick={() => setShowYamlPreview(current => !current)}>
|
||||
<i className="ri-file-code-line mr-1.5"></i>{showYamlPreview ? '收起片段' : 'YAML 片段'}
|
||||
</button>
|
||||
<button type="button" className="ant-btn ant-btn-default" onClick={resetDraft}>
|
||||
<i className="ri-refresh-line mr-1.5"></i>重置修改
|
||||
</button>
|
||||
<button type="button" className="ant-btn ant-btn-primary" disabled={!currentRule} onClick={() => currentRule && openRuleEditor(currentRule)}>
|
||||
<i className="ri-edit-line mr-1.5"></i>编辑评查点
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{draftSaved && (
|
||||
<div className="draft-tip">
|
||||
<i className="ri-checkbox-circle-line"></i>
|
||||
草稿已保存到当前页面状态。本次验证不提交后端,也不会更新 OSS 文件。
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{showValidation && (
|
||||
<Card className="ant-card validation-card" title="评查点校验">
|
||||
<div className="validation-summary">
|
||||
<Tag color={hasErrors ? 'red' : 'green'}>{hasErrors ? '存在必改问题' : '可提交验证'}</Tag>
|
||||
<span>当前评查点发现 {validationIssues.length} 项提示,其中 {validationIssues.filter(issue => issue.severity === 'error').length} 项需要处理。</span>
|
||||
</div>
|
||||
<div className="validation-list">
|
||||
{validationIssues.length === 0 ? (
|
||||
<div className="empty-state">未发现配置问题。</div>
|
||||
) : validationIssues.map(issue => (
|
||||
<div key={issue.id} className={`validation-item ${issue.severity}`}>
|
||||
<Tag color={issueColor(issue.severity)}>{issue.severity === 'error' ? '必改' : '提醒'}</Tag>
|
||||
<strong>{issue.area}</strong>
|
||||
<span>{issue.target}</span>
|
||||
<p>{issue.message}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{showYamlPreview && currentRule && (
|
||||
<Card className="ant-card" title="当前评查点 YAML 片段">
|
||||
<pre className="yaml-source yaml-source-highlighted">
|
||||
<code>{yamlPreview.split('\n').map(renderYamlLine)}</code>
|
||||
</pre>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{currentRule ? (
|
||||
<>
|
||||
<Card className="ant-card" title="评查点定义">
|
||||
<div className="rule-detail-grid">
|
||||
<div className="info-box">
|
||||
<label>评查点编码</label>
|
||||
<div>{currentRule.ruleId || '-'}</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>规则组</label>
|
||||
<div>{currentRule.group || '-'}</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>检查方式</label>
|
||||
<div>{ruleTypeLabel(currentRule.type)}</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>业务阶段</label>
|
||||
<div>{currentRule.appliesIn.length > 0 ? currentRule.appliesIn.map(phaseLabel).join('、') : '全部阶段'}</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>风险等级</label>
|
||||
<div><Tag color={riskColor(currentRule.risk)} size="sm">{riskLabel(currentRule.risk)}</Tag></div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>分值</label>
|
||||
<div>{currentRule.score || '-'}</div>
|
||||
</div>
|
||||
<div className="info-box">
|
||||
<label>检查方法</label>
|
||||
<div>{currentRule.checkTypes.length > 0 ? currentRule.checkTypes.map(checkTypeLabel).join('、') : '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
{currentRule.description && (
|
||||
<div className="rule-description-block">
|
||||
<label>规则说明</label>
|
||||
<p>{currentRule.description}</p>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
<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"
|
||||
columns={dependencyColumns}
|
||||
dataSource={currentDependencyRows}
|
||||
rowKey="value"
|
||||
emptyText={<div className="empty-state">当前评查点暂未配置依赖字段。</div>}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card className="ant-card" title="评查规则">
|
||||
<div className="rule-content-stack">
|
||||
{currentRule.subRules.length > 0 && (
|
||||
<div className="drawer-subsection">
|
||||
<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>
|
||||
<div>
|
||||
<strong>
|
||||
{checkTypeLabel(subRule.check)}
|
||||
{currentRule.logic && (
|
||||
<Tag color={isStepReferenced(currentRule.logic, subRule.id) ? 'green' : 'orange'} size="sm">
|
||||
{isStepReferenced(currentRule.logic, subRule.id) ? '参与逻辑' : '未参与逻辑'}
|
||||
</Tag>
|
||||
)}
|
||||
</strong>
|
||||
<span>{subRule.content}</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
{currentRule.logic && (
|
||||
<div className="logic-expression">
|
||||
<label>逻辑运算式</label>
|
||||
<div>{currentRule.logic}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentRule.type === 'rule_group' && currentRule.subRules.length === 0 && (
|
||||
<div className="drawer-subsection">
|
||||
<div className="drawer-subsection-header">
|
||||
<div>
|
||||
<strong>子规则与逻辑</strong>
|
||||
<span>规则组合只维护子规则编号、内容和逻辑表达式,不在此处维护字段库。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="subrule-list">
|
||||
{currentRule.subRuleIds.length > 0 ? currentRule.subRuleIds.map(ruleId => {
|
||||
const referencedRule = rulesById.get(ruleId);
|
||||
return (
|
||||
<div key={ruleId} className="subrule-item">
|
||||
<Tag color="gray" size="sm">{ruleId}</Tag>
|
||||
<div>
|
||||
<strong>{referencedRule?.name || '引用规则'}</strong>
|
||||
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}) : (
|
||||
<div className="drawer-empty">当前规则组合还没有子规则内容。</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="logic-expression">
|
||||
<label>逻辑运算式</label>
|
||||
<div>{currentRule.logic || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{(currentRule.type === 'ai_rule' || currentRule.checkTypes.includes('ai')) && (
|
||||
<div className="drawer-subsection">
|
||||
<div className="drawer-subsection-header">
|
||||
<div>
|
||||
<strong>智能语义检查提示词</strong>
|
||||
<span>提示词属于当前评查点规则,不属于案卷文书或字段库。</span>
|
||||
</div>
|
||||
</div>
|
||||
<pre className="rule-prompt-preview">{currentRule.prompt || '当前评查点尚未维护提示词。'}</pre>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentRule.subRules.length === 0 && currentRule.type !== 'rule_group' && currentRule.type !== 'ai_rule' && !currentRule.checkTypes.includes('ai') && (
|
||||
<div className="rule-description-block compact">
|
||||
<label>规则内容</label>
|
||||
<p>{currentRule.description || '当前评查点没有额外规则内容。'}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<Card className="ant-card">
|
||||
<div className="empty-state">当前链接没有匹配到评查点,请返回列表重新进入。</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{editor && (
|
||||
<div className="rules-drawer-shell">
|
||||
<button className="rules-drawer-mask" type="button" aria-label="关闭编辑抽屉" onClick={() => setEditor(null)}></button>
|
||||
<aside className="rules-drawer" aria-label="评查点编辑">
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<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
|
||||
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>
|
||||
<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>
|
||||
<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">
|
||||
<Tag color="gray" size="sm">{ruleId}</Tag>
|
||||
<div>
|
||||
<strong>{referencedRule?.name || '引用规则'}</strong>
|
||||
<span>{referencedRule ? `${ruleTypeLabel(referencedRule.type)} / ${referencedRule.group}` : '当前 YAML 未找到对应规则内容'}</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 => (
|
||||
<Tag
|
||||
key={option.value}
|
||||
color="green"
|
||||
size="sm"
|
||||
closable
|
||||
onClose={() => setRuleDraft({
|
||||
...ruleDraft,
|
||||
dependencies: ruleDraft.dependencies.filter(dependency => dependency !== option.value)
|
||||
})}
|
||||
>
|
||||
{option.label}
|
||||
</Tag>
|
||||
))}
|
||||
<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>
|
||||
</aside>
|
||||
{dependencyDialogOpen && (
|
||||
<div className="dependency-dialog-shell" role="dialog" aria-modal="true" aria-label="追加依赖字段">
|
||||
<button className="dependency-dialog-mask" type="button" aria-label="关闭依赖字段选择" onClick={() => setDependencyDialogOpen(false)}></button>
|
||||
<div className="dependency-dialog">
|
||||
<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>
|
||||
<div className="dependency-dialog-search">
|
||||
<i className="ri-search-line"></i>
|
||||
<input value={dependencySearch} onChange={event => updateDependencySearch(event.target.value)} placeholder="搜索字段、文书、字段组" />
|
||||
</div>
|
||||
<div className="dependency-dialog-body">
|
||||
{dependencyGroups.length === 0 ? (
|
||||
<div className="drawer-empty">{dependencyDialogEmptyText}</div>
|
||||
) : dependencyGroups.map(([group, options]) => {
|
||||
const isExpanded = isDependencySearching || expandedDependencyGroups.includes(group);
|
||||
return (
|
||||
<div key={group} className={`dependency-option-group${isExpanded ? ' expanded' : ''}`}>
|
||||
<button
|
||||
type="button"
|
||||
className="dependency-option-group-title"
|
||||
aria-expanded={isExpanded}
|
||||
onClick={() => toggleDependencyGroup(group)}
|
||||
>
|
||||
<i className="ri-arrow-right-s-line"></i>
|
||||
<span>{group}</span>
|
||||
<em>{options.length} 项</em>
|
||||
</button>
|
||||
{isExpanded && (
|
||||
<div className="dependency-option-list">
|
||||
{options.map(option => (
|
||||
<label key={`${option.group}-${option.value}`} className="dependency-option">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={dependencySelection.includes(option.value)}
|
||||
onChange={event => {
|
||||
const nextSelection = event.target.checked
|
||||
? uniqueOptions([...dependencySelection, option.value])
|
||||
: dependencySelection.filter(value => value !== option.value);
|
||||
setDependencySelection(nextSelection);
|
||||
}}
|
||||
/>
|
||||
<span className="dependency-option-main">
|
||||
<span className="dependency-option-name">{option.label}</span>
|
||||
<em>{option.source}</em>
|
||||
</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="dependency-dialog-actions">
|
||||
<span>已选择 {dependencySelection.length} 项</span>
|
||||
<div>
|
||||
<Button type="default" onClick={() => setDependencyDialogOpen(false)}>取消</Button>
|
||||
<Button type="primary" onClick={applyDependencySelection}>确认追加</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
import { type LoaderFunctionArgs, type MetaFunction } from '@remix-run/node';
|
||||
import { Link, useLoaderData } from '@remix-run/react';
|
||||
import { Card } from '~/components/ui/Card';
|
||||
import { Table } from '~/components/ui/Table';
|
||||
import { Tag, type TagColor } from '~/components/ui/Tag';
|
||||
import { loadRuleYamlPacks, type RuleSummary, type RuleYamlPack } from '~/utils/rules-yaml-mock.server';
|
||||
import styles from '~/styles/pages/rules_test.css?url';
|
||||
|
||||
export const links = () => [
|
||||
{ rel: 'stylesheet', href: styles }
|
||||
];
|
||||
|
||||
export const meta: MetaFunction = () => [
|
||||
{ title: '规则 YAML 列表 - 智慧法务' }
|
||||
];
|
||||
|
||||
type RuleRow = RuleSummary & {
|
||||
rowId: string;
|
||||
packId: string;
|
||||
documentType: string;
|
||||
moduleType: string;
|
||||
mainType: string;
|
||||
subtype: string;
|
||||
yamlName: string;
|
||||
yamlStatus: RuleYamlPack['sourceStatus'];
|
||||
};
|
||||
|
||||
type LoaderData = {
|
||||
rows: RuleRow[];
|
||||
packs: RuleYamlPack[];
|
||||
filters: {
|
||||
documentType: string;
|
||||
mainType: string;
|
||||
subtype: string;
|
||||
ruleGroup: string;
|
||||
keyword: string;
|
||||
};
|
||||
options: {
|
||||
documentTypes: string[];
|
||||
mainTypes: string[];
|
||||
subtypes: string[];
|
||||
ruleGroups: string[];
|
||||
};
|
||||
};
|
||||
|
||||
function unique(values: string[]): string[] {
|
||||
return Array.from(new Set(values.filter(Boolean)));
|
||||
}
|
||||
|
||||
function riskColor(risk: string): TagColor {
|
||||
if (risk === 'high') return 'red';
|
||||
if (risk === 'medium') return 'orange';
|
||||
if (risk === 'low') return 'green';
|
||||
return 'gray';
|
||||
}
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
const url = new URL(request.url);
|
||||
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] || '';
|
||||
const requestedFilters = {
|
||||
documentType: url.searchParams.get('documentType') || '',
|
||||
mainType: requestedMainType,
|
||||
subtype: requestedSubtype,
|
||||
ruleGroup: requestedRuleGroup,
|
||||
keyword: url.searchParams.get('keyword') || ''
|
||||
};
|
||||
|
||||
const packs = await loadRuleYamlPacks();
|
||||
const documentTypes = unique(packs.map(pack => pack.documentType));
|
||||
const inferredDocumentType = packs.find(pack => pack.mainType === requestedFilters.mainType)?.documentType || '';
|
||||
const currentDocumentType = documentTypes.includes(requestedFilters.documentType)
|
||||
? requestedFilters.documentType
|
||||
: inferredDocumentType || documentTypes[0] || '';
|
||||
const scopedFilters = {
|
||||
...requestedFilters,
|
||||
documentType: currentDocumentType,
|
||||
mainType: packs.some(pack => pack.documentType === currentDocumentType && pack.mainType === requestedFilters.mainType)
|
||||
? requestedFilters.mainType
|
||||
: '',
|
||||
subtype: packs.some(pack =>
|
||||
pack.documentType === currentDocumentType &&
|
||||
(!requestedFilters.mainType || pack.mainType === requestedFilters.mainType) &&
|
||||
pack.subtype === requestedFilters.subtype
|
||||
)
|
||||
? requestedFilters.subtype
|
||||
: ''
|
||||
};
|
||||
const scopedPacks = packs.filter(pack =>
|
||||
pack.documentType === scopedFilters.documentType &&
|
||||
(!scopedFilters.mainType || pack.mainType === scopedFilters.mainType) &&
|
||||
(!scopedFilters.subtype || pack.subtype === scopedFilters.subtype)
|
||||
);
|
||||
const ruleGroupOptions = unique(scopedPacks.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 : ''
|
||||
};
|
||||
const visiblePacks = packs.filter(pack =>
|
||||
pack.documentType === filters.documentType &&
|
||||
(!filters.mainType || pack.mainType === filters.mainType) &&
|
||||
(!filters.subtype || pack.subtype === filters.subtype)
|
||||
);
|
||||
|
||||
const rows: RuleRow[] = visiblePacks.flatMap((pack): RuleRow[] => {
|
||||
if (pack.rules.length === 0) {
|
||||
return [{
|
||||
rowId: `${pack.id}-empty`,
|
||||
packId: pack.id,
|
||||
documentType: pack.documentType,
|
||||
moduleType: pack.moduleType,
|
||||
mainType: pack.mainType,
|
||||
subtype: pack.subtype,
|
||||
yamlName: pack.metadata.name || '待配置 YAML',
|
||||
yamlStatus: pack.sourceStatus,
|
||||
id: `${pack.id}-empty`,
|
||||
ruleId: '-',
|
||||
name: '暂无规则配置',
|
||||
group: '待配置',
|
||||
risk: '-',
|
||||
score: '-',
|
||||
type: '-',
|
||||
checkTypes: [],
|
||||
logic: '',
|
||||
subRules: [],
|
||||
subRuleIds: [],
|
||||
scope: [],
|
||||
dependencies: [],
|
||||
stageCount: 0,
|
||||
appliesIn: [],
|
||||
prompt: '',
|
||||
description: '当前文档类型已保留规则列表与 YAML 配置页流程,等待后续接入规则文件。'
|
||||
}];
|
||||
}
|
||||
|
||||
return pack.rules.map(rule => ({
|
||||
...rule,
|
||||
rowId: `${pack.id}-${rule.ruleId || rule.id}`,
|
||||
packId: pack.id,
|
||||
documentType: pack.documentType,
|
||||
moduleType: pack.moduleType,
|
||||
mainType: pack.mainType,
|
||||
subtype: pack.subtype,
|
||||
yamlName: pack.metadata.name,
|
||||
yamlStatus: pack.sourceStatus
|
||||
}));
|
||||
}).filter(row => {
|
||||
if (filters.ruleGroup && row.group !== filters.ruleGroup) {
|
||||
return false;
|
||||
}
|
||||
if (!filters.keyword) return true;
|
||||
return [row.ruleId, row.name, row.group, row.yamlName, row.subtype]
|
||||
.some(value => value.toLowerCase().includes(filters.keyword.toLowerCase()));
|
||||
});
|
||||
|
||||
return Response.json({
|
||||
rows,
|
||||
packs,
|
||||
filters,
|
||||
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)),
|
||||
ruleGroups: ruleGroupOptions
|
||||
}
|
||||
} satisfies LoaderData);
|
||||
}
|
||||
|
||||
export default function RulesTestList() {
|
||||
const { rows } = useLoaderData<typeof loader>() as LoaderData;
|
||||
|
||||
const columns = [
|
||||
{
|
||||
title: '规则',
|
||||
key: 'rule',
|
||||
width: '24%',
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<div className="rule-name">
|
||||
<strong>{record.name}</strong>
|
||||
<span>{record.ruleId}</span>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '子分类',
|
||||
key: 'subtype',
|
||||
width: '12%',
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<div className="inline-tags">
|
||||
<Tag color="blue" size="sm">{record.subtype}</Tag>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '规则组',
|
||||
dataIndex: 'group' as keyof RuleRow,
|
||||
key: 'group',
|
||||
width: '14%',
|
||||
align: 'center' as const
|
||||
},
|
||||
{
|
||||
title: '风险',
|
||||
key: 'risk',
|
||||
width: '8%',
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<Tag color={riskColor(record.risk)} size="sm">{record.risk}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '分值',
|
||||
key: 'score',
|
||||
width: '8%',
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<Tag color="gray" size="sm">{record.score}</Tag>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '依赖字段',
|
||||
key: 'dependencies',
|
||||
width: '20%',
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<span>{record.dependencies.length > 0 ? record.dependencies.slice(0, 3).join('、') : '-'}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'operation',
|
||||
width: '14%',
|
||||
align: 'center' as const,
|
||||
render: (_: unknown, record: RuleRow) => (
|
||||
<Link className="operation-btn" to={`/rulesTest/detail?packId=${encodeURIComponent(record.packId)}&ruleId=${encodeURIComponent(record.ruleId || record.id)}`}>
|
||||
<i className="ri-settings-3-line"></i> 配置
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="rules-test-page rules-page">
|
||||
<div className="page-shell">
|
||||
<Card className="ant-card">
|
||||
<Table
|
||||
className="rules-test-table rules-table"
|
||||
columns={columns}
|
||||
dataSource={rows}
|
||||
rowKey="rowId"
|
||||
emptyText={<div className="empty-state">暂无规则。当前类型的规则列表流程已保留,可进入配置页查看空 YAML 模板。</div>}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user