feat(gxmu): 模块下拉改由模块注册表驱动,审批链每级支持审批人规则

- 新增模块注册表 api 与进程内缓存(首次使用拉一次、并发共用同一个请求),
  启用清单驱动「可新建审批链」的模块下拉与审核列表模块筛选
- reviewFlowConfigEdit 不再维护硬编码模块清单;停用模块的历史配置按
  「标题(已停用)」回显,避免下拉空白
- module → detailKey 的映射收敛到 detailKeyOf 一处,moduleDetailConfig
  未命中注册表时回退 module 本身(停用模块的详情仍可打开)
- 审核流每级新增「审批人规则」输入(USER:<userId> / COLLEGE_ROLE:<roleCode>,
  分号拼装多段,留空沿用「指定人」),随该级提交 reviewerRule 并回显
- 新增 scripts/selfcheck-module-registry.mjs:前端无单测基建时验证下拉过滤、
  detailKey 回退与停用模块回显
This commit is contained in:
2026-11-03 22:40:43 +08:00
parent 0444846422
commit 74f801a081
11 changed files with 396 additions and 29 deletions
@@ -63,6 +63,12 @@
@focus="loadReviewUsers(item.organizationId)"
@update:value="updateReviewUser(index, $event)"
/>
<a-input
allow-clear
:value="item.reviewerRule"
:placeholder="reviewerRulePlaceholder"
@update:value="updateReviewerRule(index, $event)"
/>
</div>
<a-space :size="4">
<a-button
@@ -109,6 +115,8 @@
interface ReviewFlowItem {
organizationId?: number;
reviewUserId?: number;
// 审批人规则:';' 分隔可拼装多段,USER:<userId> / COLLEGE_ROLE:<roleCode>
reviewerRule?: string;
}
interface ReviewFlowForm extends Omit<ReviewFlow, 'reviewUserId'> {
@@ -148,6 +156,9 @@
const reviewUserOptionsMap = ref<Record<number, User[]>>({});
const reviewUserLoadingMap = ref<Record<number, boolean>>({});
const reviewUserNameMap = ref<Record<number, string>>({});
// 审批人规则语法见 docs/adr/0006-approval-chain-convergence.md
const reviewerRulePlaceholder =
'审批人规则:USER:12 或 COLLEGE_ROLE:COLLEGE_LEAGUE_SECRETARY,可用 ; 拼装多段,留空沿用「指定人」';
// 用户信息
const form = reactive<ReviewFlowForm>({
@@ -190,9 +201,13 @@
},
{
validator: async () => {
const invalid = form.flowItems.some((item) => !item.reviewUserId);
const invalid = form.flowItems.some(
(item) => !item.reviewUserId && !item.reviewerRule?.trim()
);
if (invalid) {
return Promise.reject(new Error('请为每一级选择审核人'));
return Promise.reject(
new Error('请为每一级选择审核人或填写审批人规则')
);
}
return Promise.resolve();
},
@@ -357,6 +372,12 @@
syncOrganizationIds();
};
const updateReviewerRule = (index: number, reviewerRule?: string) => {
const items = [...form.flowItems];
items[index] = { ...items[index], reviewerRule };
form.flowItems = items;
};
const getSortedFlows = () => {
if (props.data?.flows?.length) {
return [...props.data.flows].sort(
@@ -399,6 +420,7 @@
title: form.title,
organizationId: item.organizationId,
reviewUserId: item.reviewUserId,
reviewerRule: item.reviewerRule?.trim() || undefined,
level: index + 1
};
delete (formData as ReviewFlow & { organizationIds?: number[] })
@@ -452,7 +474,8 @@
.filter((item) => typeof item.organizationId === 'number')
.map((item) => ({
organizationId: item.organizationId,
reviewUserId: item.reviewUserId
reviewUserId: item.reviewUserId,
reviewerRule: item.reviewerRule
}));
syncOrganizationIds();
await preloadReviewUsers();
@@ -503,7 +526,10 @@
}
.flow-step-reviewer {
width: 220px;
display: flex;
flex-direction: column;
gap: 6px;
width: 320px;
flex-shrink: 0;
}
</style>
@@ -55,6 +55,7 @@
allow-clear
show-search
:options="moduleOptions"
:loading="moduleLoading"
:filter-option="filterSelectOption"
placeholder="请选择模块"
v-model:value="form.module"
@@ -81,6 +82,12 @@
import type { ReviewFlow } from '@/api/gxmu/reviewFlow/model';
import { listOrganizations } from '@/api/system/organization';
import type { Organization } from '@/api/system/organization/model';
import {
enabledModuleOptions,
loadModuleRegistry,
withLegacyModuleOption
} from '@/utils/module-registry';
import type { ModuleOption } from '@/utils/module-registry-core';
// 是否是修改
const isUpdate = ref(false);
@@ -111,18 +118,9 @@
const reviewFlowOptions = ref<{ label: string; value: number }[]>([]);
const organizationLoading = ref(false);
const organizationTreeData = ref<Organization[]>([]);
const moduleOptions = [
{ label: '稿件', value: 'cms_manuscript' },
{ label: '十佳青年岗位能手', value: 'gxmu_sjqn' },
{ label: '十佳团支部书记', value: 'gxmu_sjtbzbsj' },
{ label: '五四红旗团委', value: 'gxmu_wshqtw' },
{ label: '五四红旗团支部', value: 'gxmu_wshqtzb' },
{ label: '优秀共青团干部', value: 'gxmu_yxgqtdgb' },
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
{ label: '青马工程', value: 'gxmu_qmgc_form' },
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
];
// 模块下拉来自模块注册表(只含启用模块),不再维护前端硬编码清单
const moduleLoading = ref(false);
const moduleOptions = ref<ModuleOption[]>([]);
// 用户信息
const form = reactive<ReviewFlowConfig>({
@@ -204,6 +202,28 @@
}
};
/**
* 加载模块下拉。
*
* 注册表接口只返回启用模块,编辑停用模块的历史审批链时把当前值补成
* 「标题(已停用)」,保证回显不空白。
*/
const loadModuleOptions = async (module?: string, moduleName?: string) => {
moduleLoading.value = true;
try {
await loadModuleRegistry();
moduleOptions.value = withLegacyModuleOption(
enabledModuleOptions(),
module,
moduleName
);
} catch (e: any) {
message.error(e.message || '模块清单加载失败');
} finally {
moduleLoading.value = false;
}
};
const loadOrganizations = async () => {
organizationLoading.value = true;
try {
@@ -273,9 +293,11 @@
}
if (props.data) {
assignObject(form, props.data);
await loadModuleOptions(props.data.module, props.data.moduleName);
isUpdate.value = true;
} else {
resetFields();
await loadModuleOptions();
isUpdate.value = false;
}
} else {
@@ -5,6 +5,8 @@
* 中展示的附件字段,未列出的字段会兜底追加到「其他信息」分组,避免数据丢失。
*/
import { detailKeyOf } from '@/utils/module-registry';
export interface ModuleSection {
// 分组标题
title: string;
@@ -896,9 +898,19 @@ export const TABLE_FIELD_LABELS: Record<string, string> = {
year: '年份'
};
/**
* 取模块的详情页配置。
*
* module → detailKey 的映射只走 detailKeyOf 一处(注册表 detail_key);
* 注册表未命中时回退 module 本身,停用模块的历史记录仍能打开详情。
*/
export const getModuleDetailConfig = (module?: string) => {
if (!module) {
return undefined;
}
return MODULE_DETAIL_CONFIGS[module];
const detailKey = detailKeyOf(module);
return (
(detailKey && MODULE_DETAIL_CONFIGS[detailKey]) ||
MODULE_DETAIL_CONFIGS[module]
);
};
+18 -12
View File
@@ -20,8 +20,14 @@
</template>
<script lang="ts" setup>
import { reactive, watch } from 'vue';
import { onMounted, reactive, ref, watch } from 'vue';
import { message } from 'ant-design-vue';
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
import {
enabledModuleOptions,
loadModuleRegistry
} from '@/utils/module-registry';
import type { ModuleOption } from '@/utils/module-registry-core';
const props = withDefaults(
defineProps<{
@@ -42,17 +48,17 @@
keywords: undefined
});
const moduleOptions = [
{ label: '十佳青年岗位能手', value: 'gxmu_sjqn' },
{ label: '十佳团支部书记', value: 'gxmu_sjtbzbsj' },
{ label: '五四红旗团委', value: 'gxmu_wshqtw' },
{ label: '五四红旗团支部', value: 'gxmu_wshqtzb' },
{ label: '优秀共青团干部', value: 'gxmu_yxgqtdgb' },
{ label: '优秀共青团员', value: 'gxmu_yxgqty' },
{ label: '挑战杯', value: 'gxmu_tzbcy_form' },
{ label: '青马工程', value: 'gxmu_qmgc_form' },
{ label: '未来学术之星', value: 'gxmu_wxxzx_project' }
];
// 模块筛选项来自模块注册表(只含启用模块),不再维护前端硬编码清单
const moduleOptions = ref<ModuleOption[]>([]);
onMounted(async () => {
try {
await loadModuleRegistry();
moduleOptions.value = enabledModuleOptions();
} catch (e: any) {
message.error(e.message || '模块清单加载失败');
}
});
const search = () => {
emit('search', {