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>