保存规则库 YAML 维护改造进展

This commit is contained in:
2026-04-28 22:00:00 +08:00
parent 7b86293263
commit dce5ac0c9a
96 changed files with 36801 additions and 615 deletions
+300 -55
View File
@@ -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>
);
}
}