保存规则库 YAML 维护改造进展
This commit is contained in:
+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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user