fix(gxmu): 修复 spec #58 双轴 review 的 admin 侧问题
1. 新建审批链丢审批人规则:新建/编辑共用 toReviewFlowPayload,reviewerRule 不再漏带; 自检新增「审批链保存体」一组断言。 2. 审核列表模块筛选取注册表全量清单(?all=true),停用模块标题带「(已停用)」; 只有审批链配置下拉仍用启用集。 3. 审批人规则 placeholder 换成真实角色码 YouthLeagueCommitteeOfSecondaryColleges。 4. getModuleDetailConfig 改为显式传入注册表快照(纯函数),消除加载前后配置不一致; 快照在 module-registry 里冻结,并落 localStorage 供失败兜底。 5. 自检脚本:fixture 与种子对齐(10 行、5 个启用,含 cms_manuscript), 「5 组行为」改为按实际组数统计;新增 npm run selfcheck:module-registry。 6. 注册表取不到时:保留上次成功清单 + 顶部 a-alert 提示 + 重试按钮(不静默留空下拉)。 门禁:npm run build 通过;npm run selfcheck:module-registry 6 组行为全绿。
This commit is contained in:
@@ -6,6 +6,7 @@
|
|||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"serve": "vite build && vite preview",
|
"serve": "vite build && vite preview",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
"selfcheck:module-registry": "node scripts/selfcheck-module-registry.mjs",
|
||||||
"lint:eslint": "eslint --cache --max-warnings 0 \"src/**/*.{vue,ts}\" --fix",
|
"lint:eslint": "eslint --cache --max-warnings 0 \"src/**/*.{vue,ts}\" --fix",
|
||||||
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite/",
|
"clean:cache": "rimraf node_modules/.cache/ && rimraf node_modules/.vite/",
|
||||||
"clean:lib": "rimraf node_modules"
|
"clean:lib": "rimraf node_modules"
|
||||||
|
|||||||
@@ -2,13 +2,16 @@
|
|||||||
/**
|
/**
|
||||||
* 模块注册表前端逻辑的最小自检(仓库无单测基建,用 Node + typescript.transpileModule 跑纯函数)。
|
* 模块注册表前端逻辑的最小自检(仓库无单测基建,用 Node + typescript.transpileModule 跑纯函数)。
|
||||||
*
|
*
|
||||||
* 覆盖 ticket #65 的可见行为:
|
* 覆盖 ticket #65 与 spec #58 修复后的可见行为:
|
||||||
* 1. 「可新建审批链」下拉只含 enabled=1 的模块,按 sortNumber 升序,标题取注册表 title;
|
* 1. 「可新建审批链」下拉只含 enabled=1 的模块,按 sortNumber 升序,标题取注册表 title;
|
||||||
* 2. module → detailKey 的映射收在 resolveDetailKey 一处,注册表里没有该模块时回退 module 本身,
|
* 2. 审核列表的模块筛选用全量清单(含停用模块),停用项标题带「(已停用)」,
|
||||||
* 保证停用模块的历史审批链仍能回显标题、仍能打开详情;
|
* 且只有审批链配置下拉用启用清单;
|
||||||
* 3. 编辑历史配置时把停用的模块补进下拉(带「已停用」标记),不重复添加。
|
* 3. module → detailKey 的映射收在 resolveDetailKey 一处,注册表里没有该模块时回退 module 本身;
|
||||||
|
* 4. 标题解析:注册表命中取 title,未命中回退 module 本身;
|
||||||
|
* 5. 编辑历史配置时把停用的模块补进下拉(带「已停用」标记),不重复添加;
|
||||||
|
* 6. 审批链保存体:新建与编辑两条分支都必须带上每级的 reviewerRule(spec #58 A5)。
|
||||||
*
|
*
|
||||||
* 运行:node scripts/selfcheck-module-registry.mjs
|
* 运行:npm run selfcheck:module-registry
|
||||||
*/
|
*/
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { createRequire } from 'node:module';
|
import { createRequire } from 'node:module';
|
||||||
@@ -38,7 +41,10 @@ const loadTs = (relative) => {
|
|||||||
return mod.exports;
|
return mod.exports;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const readSource = (relative) => readFileSync(resolve(root, relative), 'utf8');
|
||||||
|
|
||||||
let failed = 0;
|
let failed = 0;
|
||||||
|
let groupCount = 0;
|
||||||
const assert = (name, condition, detail) => {
|
const assert = (name, condition, detail) => {
|
||||||
if (condition) {
|
if (condition) {
|
||||||
console.log(' ok ' + name);
|
console.log(' ok ' + name);
|
||||||
@@ -47,37 +53,71 @@ const assert = (name, condition, detail) => {
|
|||||||
failed += 1;
|
failed += 1;
|
||||||
console.log(' FAIL ' + name + (detail ? ' -> ' + detail : ''));
|
console.log(' FAIL ' + name + (detail ? ' -> ' + detail : ''));
|
||||||
};
|
};
|
||||||
|
/** 开始一组行为断言,结尾按实际组数统计,避免硬编码组数 */
|
||||||
|
const group = (name) => {
|
||||||
|
groupCount += 1;
|
||||||
|
console.log(name);
|
||||||
|
};
|
||||||
|
|
||||||
const core = loadTs('src/utils/module-registry-core.ts');
|
const core = loadTs('src/utils/module-registry-core.ts');
|
||||||
|
const payloadBuilder = loadTs(
|
||||||
|
'src/views/gxmu/reviewFlow/components/reviewFlowPayload.ts'
|
||||||
|
);
|
||||||
|
|
||||||
// 与后端种子数据一致:10 个模块里只有 4 个 enabled=1
|
// 与后端种子一致:sql/20261110_spec58_approval_chain.sql 里 10 个模块,
|
||||||
|
// 其中 5 个 enabled=1(含稿件 cms_manuscript,它有在跑的审核链)。
|
||||||
const REGISTRY = [
|
const REGISTRY = [
|
||||||
{ module: 'gxmu_sjqn', detailKey: 'gxmu_sjqn', title: '十佳青年岗位能手', enabled: 1, sortNumber: 1 },
|
{ module: 'gxmu_sjqn', detailKey: 'gxmu_sjqn', title: '十佳青年岗位能手', enabled: 1, sortNumber: 1 },
|
||||||
{ module: 'gxmu_sjtbzbsj', detailKey: 'gxmu_sjtbzbsj', title: '十佳团支部书记', enabled: 0, sortNumber: 2 },
|
{ module: 'gxmu_sjtbzbsj', detailKey: 'gxmu_sjtbzbsj', title: '十佳团支部书记', enabled: 0, sortNumber: 2 },
|
||||||
|
{ module: 'gxmu_wshqtw', detailKey: 'gxmu_wshqtw', title: '五四红旗团委', enabled: 0, sortNumber: 3 },
|
||||||
|
{ module: 'gxmu_wshqtzb', detailKey: 'gxmu_wshqtzb', title: '五四红旗团支部', enabled: 0, sortNumber: 4 },
|
||||||
|
{ module: 'gxmu_yxgqtdgb', detailKey: 'gxmu_yxgqtdgb', title: '优秀共青团干部', enabled: 0, sortNumber: 5 },
|
||||||
|
{ module: 'gxmu_yxgqty', detailKey: 'gxmu_yxgqty', title: '优秀共青团员', enabled: 0, sortNumber: 6 },
|
||||||
{ module: 'gxmu_tzbcy_form', detailKey: 'gxmu_tzbcy_form', title: '挑战杯', enabled: 1, sortNumber: 7 },
|
{ module: 'gxmu_tzbcy_form', detailKey: 'gxmu_tzbcy_form', title: '挑战杯', enabled: 1, sortNumber: 7 },
|
||||||
{ module: 'gxmu_qmgc_form', detailKey: 'gxmu_qmgc_form', title: '青马工程', enabled: 1, sortNumber: 8 },
|
{ module: 'gxmu_qmgc_form', detailKey: 'gxmu_qmgc_form', title: '青马工程', enabled: 1, sortNumber: 8 },
|
||||||
{ module: 'gxmu_wxxzx_project', detailKey: 'gxmu_wxxzx_project', title: '未来学术之星', enabled: 1, sortNumber: 9 },
|
{ module: 'gxmu_wxxzx_project', detailKey: 'gxmu_wxxzx_project', title: '未来学术之星', enabled: 1, sortNumber: 9 },
|
||||||
{ module: 'cms_manuscript', detailKey: 'cms_manuscript', title: '稿件', enabled: 0, sortNumber: 10 }
|
{ module: 'cms_manuscript', detailKey: 'cms_manuscript', title: '稿件', enabled: 1, sortNumber: 10 }
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log('新建审批链的模块下拉(enabled=1,按 sortNumber)');
|
group('新建审批链的模块下拉(enabled=1,按 sortNumber)');
|
||||||
const options = core.toModuleOptions(REGISTRY);
|
const options = core.toModuleOptions(REGISTRY);
|
||||||
assert(
|
assert(
|
||||||
'只含已启用模块',
|
'只含已启用模块',
|
||||||
options.map((o) => o.value).join(',') ===
|
options.map((o) => o.value).join(',') ===
|
||||||
'gxmu_sjqn,gxmu_tzbcy_form,gxmu_qmgc_form,gxmu_wxxzx_project',
|
'gxmu_sjqn,gxmu_tzbcy_form,gxmu_qmgc_form,gxmu_wxxzx_project,cms_manuscript',
|
||||||
JSON.stringify(options)
|
JSON.stringify(options)
|
||||||
);
|
);
|
||||||
assert('label 取注册表标题', options[0].label === '十佳青年岗位能手');
|
assert('label 取注册表标题', options[0].label === '十佳青年岗位能手');
|
||||||
assert('停用模块不出现', !options.some((o) => o.value === 'gxmu_sjtbzbsj'));
|
assert('停用模块不出现', !options.some((o) => o.value === 'gxmu_sjtbzbsj'));
|
||||||
|
|
||||||
console.log('审核列表的模块筛选项(同理,按注册表顺序)');
|
group('审核列表的模块筛选(全量清单,停用项带标记)');
|
||||||
|
const filterOptions = core.toAllModuleOptions(REGISTRY);
|
||||||
|
assert('含停用模块', filterOptions.length === REGISTRY.length, String(filterOptions.length));
|
||||||
assert(
|
assert(
|
||||||
'与新建下拉同源',
|
'按 sortNumber 升序',
|
||||||
core.toModuleOptions(REGISTRY).length === options.length
|
filterOptions.map((o) => o.value).join(',') === REGISTRY.map((r) => r.module).join(','),
|
||||||
|
JSON.stringify(filterOptions.map((o) => o.value))
|
||||||
|
);
|
||||||
|
const disabledOption = filterOptions.find((o) => o.value === 'gxmu_sjtbzbsj');
|
||||||
|
assert('停用项标题带「(已停用)」', disabledOption?.label === '十佳团支部书记(已停用)', JSON.stringify(disabledOption));
|
||||||
|
assert('启用项标题不加标记', filterOptions.find((o) => o.value === 'gxmu_sjqn')?.label === '十佳青年岗位能手');
|
||||||
|
assert(
|
||||||
|
'停用模块仍能被筛出来(ADR 0006 历史数据仍可查)',
|
||||||
|
['gxmu_sjtbzbsj', 'gxmu_wshqtw', 'gxmu_wshqtzb', 'gxmu_yxgqtdgb', 'gxmu_yxgqty'].every((m) =>
|
||||||
|
filterOptions.some((o) => o.value === m)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const searchSource = readSource('src/views/gxmu/reviewList/components/search.vue');
|
||||||
|
assert('审核列表筛选入口用 allModuleOptions', /allModuleOptions\(/.test(searchSource));
|
||||||
|
const configEditSource = readSource(
|
||||||
|
'src/views/gxmu/reviewFlowConfig/components/reviewFlowConfigEdit.vue'
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
'审批链配置下拉仍只用启用清单',
|
||||||
|
/enabledModuleOptions\(/.test(configEditSource) && !/allModuleOptions\(/.test(configEditSource)
|
||||||
);
|
);
|
||||||
|
|
||||||
console.log('module → detailKey 映射(收在一处)');
|
group('module → detailKey 映射(收在一处)');
|
||||||
assert('走注册表 detailKey', core.resolveDetailKey(REGISTRY, 'gxmu_sjqn') === 'gxmu_sjqn');
|
assert('走注册表 detailKey', core.resolveDetailKey(REGISTRY, 'gxmu_sjqn') === 'gxmu_sjqn');
|
||||||
assert(
|
assert(
|
||||||
'注册表缺该模块时回退 module(停用模块的详情仍可解析)',
|
'注册表缺该模块时回退 module(停用模块的详情仍可解析)',
|
||||||
@@ -85,11 +125,11 @@ assert(
|
|||||||
);
|
);
|
||||||
assert('空模块返回空', core.resolveDetailKey(REGISTRY, undefined) === undefined);
|
assert('空模块返回空', core.resolveDetailKey(REGISTRY, undefined) === undefined);
|
||||||
|
|
||||||
console.log('标题解析(历史配置回显)');
|
group('标题解析(历史配置回显)');
|
||||||
assert('注册表命中', core.resolveModuleTitle(REGISTRY, 'gxmu_qmgc_form') === '青马工程');
|
assert('注册表命中', core.resolveModuleTitle(REGISTRY, 'gxmu_qmgc_form') === '青马工程');
|
||||||
assert('注册表未命中回退 module 本身', core.resolveModuleTitle(REGISTRY, 'legacy_module') === 'legacy_module');
|
assert('注册表未命中回退 module 本身', core.resolveModuleTitle(REGISTRY, 'legacy_module') === 'legacy_module');
|
||||||
|
|
||||||
console.log('编辑历史配置:把停用模块补进下拉');
|
group('编辑历史配置:把停用模块补进下拉');
|
||||||
const merged = core.ensureModuleOption(options, 'gxmu_sjtbzbsj', '十佳团支部书记');
|
const merged = core.ensureModuleOption(options, 'gxmu_sjtbzbsj', '十佳团支部书记');
|
||||||
assert('补进来的项标记已停用', merged[0]?.label === '十佳团支部书记(已停用)', JSON.stringify(merged[0]));
|
assert('补进来的项标记已停用', merged[0]?.label === '十佳团支部书记(已停用)', JSON.stringify(merged[0]));
|
||||||
assert('补进来的项在最前面且可回显', merged[0]?.value === 'gxmu_sjtbzbsj');
|
assert('补进来的项在最前面且可回显', merged[0]?.value === 'gxmu_sjtbzbsj');
|
||||||
@@ -101,8 +141,48 @@ assert(
|
|||||||
core.ensureModuleOption(options, 'legacy_module')[0]?.label === 'legacy_module(已停用)'
|
core.ensureModuleOption(options, 'legacy_module')[0]?.label === 'legacy_module(已停用)'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
group('审批链保存体:新建与编辑都带 reviewerRule(spec #58 A5)');
|
||||||
|
const newChainForm = {
|
||||||
|
title: '新链',
|
||||||
|
organizationIds: [1, 2],
|
||||||
|
flowItems: [{ organizationId: 1 }, { organizationId: 2 }],
|
||||||
|
id: undefined
|
||||||
|
};
|
||||||
|
const created = payloadBuilder.toReviewFlowPayload(
|
||||||
|
newChainForm,
|
||||||
|
{ organizationId: 1, reviewerRule: ' COLLEGE_ROLE:YouthLeagueCommitteeOfSecondaryColleges ' },
|
||||||
|
1
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
'新建分支带 reviewerRule(已 trim)',
|
||||||
|
created.reviewerRule === 'COLLEGE_ROLE:YouthLeagueCommitteeOfSecondaryColleges',
|
||||||
|
JSON.stringify(created.reviewerRule)
|
||||||
|
);
|
||||||
|
assert('新建分支带 level', created.level === 1);
|
||||||
|
assert(
|
||||||
|
'提交体不带前端专用字段',
|
||||||
|
!('organizationIds' in created) && !('flowItems' in created) && !('id' in created)
|
||||||
|
);
|
||||||
|
assert(
|
||||||
|
'未填规则时写 undefined 而不是空串',
|
||||||
|
payloadBuilder.toReviewFlowPayload(newChainForm, { organizationId: 2 }, 2).reviewerRule === undefined
|
||||||
|
);
|
||||||
|
const edited = payloadBuilder.toReviewFlowPayload(
|
||||||
|
newChainForm,
|
||||||
|
{ organizationId: 2, reviewUserId: 7, reviewerRule: 'USER:12' },
|
||||||
|
2
|
||||||
|
);
|
||||||
|
assert('编辑分支同样带 reviewerRule', edited.reviewerRule === 'USER:12');
|
||||||
|
assert('编辑分支保留指定人', edited.reviewUserId === 7);
|
||||||
|
const flowEditSource = readSource('src/views/gxmu/reviewFlow/components/reviewFlowEdit.vue');
|
||||||
|
assert(
|
||||||
|
'新建/编辑两条分支共用同一份构造逻辑(不再逐字段手拼)',
|
||||||
|
(flowEditSource.match(/toReviewFlowPayload\(/g) || []).length === 2 &&
|
||||||
|
!/const formData: ReviewFlow = \{/.test(flowEditSource)
|
||||||
|
);
|
||||||
|
|
||||||
if (failed) {
|
if (failed) {
|
||||||
console.error('\n自检失败:' + failed + ' 项');
|
console.error('\n自检失败:' + failed + ' 项');
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log('\n自检通过:' + 5 + ' 组行为');
|
console.log('\n自检通过:' + groupCount + ' 组行为');
|
||||||
|
|||||||
@@ -4,11 +4,16 @@ import type { ModuleRegistry } from './model';
|
|||||||
import { MODULES_API_URL } from '@/config/setting';
|
import { MODULES_API_URL } from '@/config/setting';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 启用模块清单(注册表唯一来源,前端不再维护模块清单)
|
* 模块清单(注册表唯一来源,前端不再维护模块清单)。
|
||||||
|
*
|
||||||
|
* all=true 取全量(含停用):筛选与标题回显要用它,否则停用模块的历史记录
|
||||||
|
* 在界面上就筛不出来了;只有「可新建审批链」的下拉需要按 enabled 再过滤。
|
||||||
|
* 后端 /list 默认只返回启用模块。
|
||||||
*/
|
*/
|
||||||
export async function listModuleRegistry() {
|
export async function listModuleRegistry(all = true) {
|
||||||
const res = await request.get<ApiResult<ModuleRegistry[]>>(
|
const res = await request.get<ApiResult<ModuleRegistry[]>>(
|
||||||
MODULES_API_URL + '/gxmu/module-registry/list'
|
MODULES_API_URL + '/gxmu/module-registry/list',
|
||||||
|
{ params: { all: all ? 'true' : 'false' } }
|
||||||
);
|
);
|
||||||
if (res.data.code === 0) {
|
if (res.data.code === 0) {
|
||||||
return res.data.data || [];
|
return res.data.data || [];
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/**
|
/**
|
||||||
* 模块注册表的纯逻辑:选项构造、module → detailKey 映射、标题回退。
|
* 模块注册表的纯逻辑:选项构造、module → detailKey 映射、标题回退。
|
||||||
*
|
*
|
||||||
* 这里刻意不依赖 request / Vue,便于 scripts/selfcheck-module-registry.mjs
|
* 这里刻意不依赖 request / Vue / localStorage,便于 scripts/selfcheck-module-registry.mjs
|
||||||
* 在无单测基建的前端仓库里直接验证行为。副作用(拉取与缓存)在
|
* 在无单测基建的前端仓库里直接验证行为。副作用(拉取与缓存)在
|
||||||
* src/utils/module-registry.ts。
|
* src/utils/module-registry.ts。
|
||||||
*/
|
*/
|
||||||
@@ -21,23 +21,52 @@ export interface ModuleOption {
|
|||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按 sortNumber 升序(缺省排最后,同序号保持原顺序) */
|
||||||
|
const bySortNumber = (a: RegistryModule, b: RegistryModule) =>
|
||||||
|
(a.sortNumber ?? Number.MAX_SAFE_INTEGER) -
|
||||||
|
(b.sortNumber ?? Number.MAX_SAFE_INTEGER);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构造模块下拉选项:只含启用模块,按 sortNumber 升序,标题取注册表 title。
|
* 构造「可新建审批链」的模块下拉选项:只含启用模块,按 sortNumber 升序,
|
||||||
|
* 标题取注册表 title。
|
||||||
*
|
*
|
||||||
* 停用模块(enabled=0)不出现在「可新建审批链」的下拉里,历史数据仍可查,
|
* 停用模块(enabled=0)不出现在这里,历史数据仍可查,见
|
||||||
* 见 docs/adr/0006-approval-chain-convergence.md。
|
* docs/adr/0006-approval-chain-convergence.md。审核列表等「查存量」入口请用
|
||||||
|
* toAllModuleOptions。
|
||||||
*/
|
*/
|
||||||
export const toModuleOptions = (list?: RegistryModule[]): ModuleOption[] => {
|
export const toModuleOptions = (
|
||||||
|
list?: readonly RegistryModule[]
|
||||||
|
): ModuleOption[] => {
|
||||||
return (list || [])
|
return (list || [])
|
||||||
.filter((item) => !!item.module && item.enabled === 1)
|
.filter((item) => !!item.module && item.enabled === 1)
|
||||||
.slice()
|
.slice()
|
||||||
.sort((a, b) => (a.sortNumber ?? 0) - (b.sortNumber ?? 0))
|
.sort(bySortNumber)
|
||||||
.map((item) => ({
|
.map((item) => ({
|
||||||
label: item.title || item.module || '',
|
label: item.title || item.module || '',
|
||||||
value: item.module || ''
|
value: item.module || ''
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构造「查存量」入口(审核列表模块筛选等)的模块下拉选项:含停用模块,
|
||||||
|
* 停用项标题后加「(已停用)」,与 ADR 0006「历史数据仍可查」一致。
|
||||||
|
*/
|
||||||
|
export const toAllModuleOptions = (
|
||||||
|
list?: readonly RegistryModule[]
|
||||||
|
): ModuleOption[] => {
|
||||||
|
return (list || [])
|
||||||
|
.filter((item) => !!item.module)
|
||||||
|
.slice()
|
||||||
|
.sort(bySortNumber)
|
||||||
|
.map((item) => {
|
||||||
|
const title = item.title || item.module || '';
|
||||||
|
return {
|
||||||
|
label: item.enabled === 1 ? title : `${title}(已停用)`,
|
||||||
|
value: item.module || ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* module → 详情页配置键(moduleDetailConfig.ts 的键)的唯一映射。
|
* module → 详情页配置键(moduleDetailConfig.ts 的键)的唯一映射。
|
||||||
*
|
*
|
||||||
@@ -45,7 +74,7 @@ export const toModuleOptions = (list?: RegistryModule[]): ModuleOption[] => {
|
|||||||
* 而注册表的 detail_key 与 module 不一致时以注册表为准。
|
* 而注册表的 detail_key 与 module 不一致时以注册表为准。
|
||||||
*/
|
*/
|
||||||
export const resolveDetailKey = (
|
export const resolveDetailKey = (
|
||||||
list: RegistryModule[] | undefined,
|
list: readonly RegistryModule[] | undefined,
|
||||||
module?: string
|
module?: string
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
if (!module) {
|
if (!module) {
|
||||||
@@ -57,7 +86,7 @@ export const resolveDetailKey = (
|
|||||||
|
|
||||||
/** 模块标题,注册表未命中时回退 module 本身,避免回显成空白 */
|
/** 模块标题,注册表未命中时回退 module 本身,避免回显成空白 */
|
||||||
export const resolveModuleTitle = (
|
export const resolveModuleTitle = (
|
||||||
list: RegistryModule[] | undefined,
|
list: readonly RegistryModule[] | undefined,
|
||||||
module?: string
|
module?: string
|
||||||
): string | undefined => {
|
): string | undefined => {
|
||||||
if (!module) {
|
if (!module) {
|
||||||
@@ -81,5 +110,8 @@ export const ensureModuleOption = (
|
|||||||
if (!module || options.some((item) => item.value === module)) {
|
if (!module || options.some((item) => item.value === module)) {
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
return [{ label: `${moduleName || module}(已停用)`, value: module }, ...options];
|
return [
|
||||||
|
{ label: `${moduleName || module}(已停用)`, value: module },
|
||||||
|
...options
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,42 +2,110 @@ import { listModuleRegistry } from '@/api/gxmu/moduleRegistry';
|
|||||||
import type { ModuleRegistry } from '@/api/gxmu/moduleRegistry/model';
|
import type { ModuleRegistry } from '@/api/gxmu/moduleRegistry/model';
|
||||||
import {
|
import {
|
||||||
ensureModuleOption,
|
ensureModuleOption,
|
||||||
resolveDetailKey,
|
toAllModuleOptions,
|
||||||
resolveModuleTitle,
|
|
||||||
toModuleOptions
|
toModuleOptions
|
||||||
} from './module-registry-core';
|
} from './module-registry-core';
|
||||||
import type { ModuleOption } from './module-registry-core';
|
import type { ModuleOption } from './module-registry-core';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 模块注册表缓存:首次使用时拉一次,进程内缓存;并发调用共用同一个请求。
|
* 模块注册表缓存。
|
||||||
|
*
|
||||||
|
* 口径(与后端、移动端统一):
|
||||||
|
* - 拉取成功后写入内存快照并冻结,同时落 localStorage,供下次拉取失败时兜底;
|
||||||
|
* - 拉取失败时保留上次成功的清单(内存 → localStorage),并让调用方做
|
||||||
|
* 「顶部提示 + 重试」,不允许静默留空下拉。
|
||||||
*/
|
*/
|
||||||
let cache: ModuleRegistry[] = [];
|
const STORAGE_KEY = 'gxmu.module-registry.snapshot.v1';
|
||||||
let pending: Promise<ModuleRegistry[]> | null = null;
|
|
||||||
|
|
||||||
/** 当前缓存(未加载时为空数组) */
|
/** 只读快照:换新快照时整体替换引用,已拿到的引用不会被原地修改 */
|
||||||
export const getModuleRegistryCache = (): ModuleRegistry[] => cache;
|
let cache: ReadonlyArray<ModuleRegistry> = Object.freeze([]);
|
||||||
|
let pending: Promise<ReadonlyArray<ModuleRegistry>> | null = null;
|
||||||
|
/** 当前快照是否来自「上次成功」的兜底(最近一次拉取失败) */
|
||||||
|
let stale = false;
|
||||||
|
|
||||||
|
const freezeList = (list?: ModuleRegistry[]): ReadonlyArray<ModuleRegistry> =>
|
||||||
|
Object.freeze((list || []).slice());
|
||||||
|
|
||||||
|
/** 当前快照是否是「上次成功」的兜底数据,供顶部提示判断 */
|
||||||
|
export const isModuleRegistryStale = (): boolean => stale;
|
||||||
|
|
||||||
|
const readStoredSnapshot = (): ModuleRegistry[] => {
|
||||||
|
if (typeof localStorage === 'undefined') {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(STORAGE_KEY);
|
||||||
|
if (!raw) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
return Array.isArray(parsed?.list) ? parsed.list : [];
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const writeStoredSnapshot = (list: ReadonlyArray<ModuleRegistry>) => {
|
||||||
|
if (typeof localStorage === 'undefined') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEY,
|
||||||
|
JSON.stringify({ savedAt: Date.now(), list })
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// 隐私模式 / 配额满:兜底缓存失败不影响主流程
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 用上次成功的清单填充内存快照(仅在内存为空时),返回是否恢复了数据 */
|
||||||
|
const restoreStoredSnapshot = (): boolean => {
|
||||||
|
if (cache.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const stored = readStoredSnapshot();
|
||||||
|
if (!stored.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
cache = freezeList(stored);
|
||||||
|
stale = true;
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
/** 清空缓存(注册表变更后手动失效) */
|
/** 清空缓存(注册表变更后手动失效) */
|
||||||
export const invalidateModuleRegistry = (): void => {
|
export const invalidateModuleRegistry = (): void => {
|
||||||
cache = [];
|
cache = Object.freeze([]);
|
||||||
pending = null;
|
pending = null;
|
||||||
|
stale = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载模块注册表。默认命中缓存直接返回;force 为 true 时重新拉取。
|
* 加载模块注册表(含停用模块,筛选与标题回显都从这里取)。
|
||||||
|
*
|
||||||
|
* 默认命中快照直接返回;force 为 true 时重新拉取。拉取失败时保留上次成功的
|
||||||
|
* 清单并抛出错误,由调用方展示「顶部提示 + 重试」;若本次直接命中兜底快照,
|
||||||
|
* 则通过 isModuleRegistryStale() 告知调用方。
|
||||||
*/
|
*/
|
||||||
export const loadModuleRegistry = async (
|
export const loadModuleRegistry = async (
|
||||||
force = false
|
force = false
|
||||||
): Promise<ModuleRegistry[]> => {
|
): Promise<ReadonlyArray<ModuleRegistry>> => {
|
||||||
if (!force && cache.length) {
|
if (!force && cache.length) {
|
||||||
return cache;
|
return cache;
|
||||||
}
|
}
|
||||||
if (!pending) {
|
if (!pending) {
|
||||||
pending = listModuleRegistry()
|
pending = listModuleRegistry()
|
||||||
.then((list) => {
|
.then((list) => {
|
||||||
cache = list || [];
|
cache = freezeList(list);
|
||||||
|
stale = false;
|
||||||
|
writeStoredSnapshot(cache);
|
||||||
return cache;
|
return cache;
|
||||||
})
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
stale = true;
|
||||||
|
restoreStoredSnapshot();
|
||||||
|
throw e;
|
||||||
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
pending = null;
|
pending = null;
|
||||||
});
|
});
|
||||||
@@ -45,17 +113,19 @@ export const loadModuleRegistry = async (
|
|||||||
return pending;
|
return pending;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** 「可新建审批链」的模块下拉:只含启用模块 */
|
/** 「可新建审批链」的模块下拉:只含启用模块(仅 reviewFlowConfigEdit 使用) */
|
||||||
export const enabledModuleOptions = (): ModuleOption[] =>
|
export const enabledModuleOptions = (
|
||||||
toModuleOptions(cache);
|
list: ReadonlyArray<ModuleRegistry> = cache
|
||||||
|
): ModuleOption[] => toModuleOptions(list);
|
||||||
|
|
||||||
/** module → 详情页配置键,映射收在这一处 */
|
/**
|
||||||
export const detailKeyOf = (module?: string): string | undefined =>
|
* 「查存量」入口的模块下拉:含停用模块,停用项标题带「(已停用)」。
|
||||||
resolveDetailKey(cache, module);
|
*
|
||||||
|
* 审核列表的模块筛选等入口用它,保证停用模块的历史记录仍能被筛出来。
|
||||||
/** 模块标题,注册表未命中时回退 module 本身 */
|
*/
|
||||||
export const moduleTitleOf = (module?: string): string | undefined =>
|
export const allModuleOptions = (
|
||||||
resolveModuleTitle(cache, module);
|
list: ReadonlyArray<ModuleRegistry> = cache
|
||||||
|
): ModuleOption[] => toAllModuleOptions(list);
|
||||||
|
|
||||||
/** 编辑存量配置时把停用模块补进下拉,保证标题回显 */
|
/** 编辑存量配置时把停用模块补进下拉,保证标题回显 */
|
||||||
export const withLegacyModuleOption = (
|
export const withLegacyModuleOption = (
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<!-- 模块注册表加载失败提示:保留上次清单 + 重试入口,避免静默留空下拉 -->
|
||||||
|
<template>
|
||||||
|
<a-alert
|
||||||
|
v-if="error"
|
||||||
|
type="warning"
|
||||||
|
show-icon
|
||||||
|
:message="error"
|
||||||
|
style="margin-bottom: 12px"
|
||||||
|
>
|
||||||
|
<template #action>
|
||||||
|
<a-button size="small" type="link" @click="emit('retry')">重试</a-button>
|
||||||
|
</template>
|
||||||
|
</a-alert>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
defineProps<{
|
||||||
|
// 加载失败的提示文案,为空表示不显示
|
||||||
|
error?: string;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'retry'): void;
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
@@ -111,6 +111,7 @@
|
|||||||
import type { Organization } from '@/api/system/organization/model';
|
import type { Organization } from '@/api/system/organization/model';
|
||||||
import { listUsers } from '@/api/system/user';
|
import { listUsers } from '@/api/system/user';
|
||||||
import type { User } from '@/api/system/user/model';
|
import type { User } from '@/api/system/user/model';
|
||||||
|
import { toReviewFlowPayload } from './reviewFlowPayload';
|
||||||
|
|
||||||
interface ReviewFlowItem {
|
interface ReviewFlowItem {
|
||||||
organizationId?: number;
|
organizationId?: number;
|
||||||
@@ -158,7 +159,7 @@
|
|||||||
const reviewUserNameMap = ref<Record<number, string>>({});
|
const reviewUserNameMap = ref<Record<number, string>>({});
|
||||||
// 审批人规则语法见 docs/adr/0006-approval-chain-convergence.md
|
// 审批人规则语法见 docs/adr/0006-approval-chain-convergence.md
|
||||||
const reviewerRulePlaceholder =
|
const reviewerRulePlaceholder =
|
||||||
'审批人规则:USER:12 或 COLLEGE_ROLE:COLLEGE_LEAGUE_SECRETARY,可用 ; 拼装多段,留空沿用「指定人」';
|
'审批人规则:USER:12 或 COLLEGE_ROLE:YouthLeagueCommitteeOfSecondaryColleges,可用 ; 拼装多段,留空沿用「指定人」';
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const form = reactive<ReviewFlowForm>({
|
const form = reactive<ReviewFlowForm>({
|
||||||
@@ -415,34 +416,23 @@
|
|||||||
}
|
}
|
||||||
for (let index = 0; index < form.flowItems.length; index++) {
|
for (let index = 0; index < form.flowItems.length; index++) {
|
||||||
const item = form.flowItems[index];
|
const item = form.flowItems[index];
|
||||||
const formData: ReviewFlow = {
|
// 新建与编辑共用同一份构造逻辑,reviewerRule 不会漏带
|
||||||
...form,
|
const formData: ReviewFlow = toReviewFlowPayload(
|
||||||
title: form.title,
|
form,
|
||||||
organizationId: item.organizationId,
|
item,
|
||||||
reviewUserId: item.reviewUserId,
|
index + 1
|
||||||
reviewerRule: item.reviewerRule?.trim() || undefined,
|
);
|
||||||
level: index + 1
|
|
||||||
};
|
|
||||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
|
||||||
.organizationIds;
|
|
||||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
|
||||||
.flowItems;
|
|
||||||
delete formData.id;
|
|
||||||
successMessage = await addReviewFlow(formData);
|
successMessage = await addReviewFlow(formData);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for (let index = 0; index < form.flowItems.length; index++) {
|
for (let index = 0; index < form.flowItems.length; index++) {
|
||||||
const item = form.flowItems[index];
|
const item = form.flowItems[index];
|
||||||
const formData: ReviewFlow = {
|
// 新建与编辑共用同一份构造逻辑,reviewerRule 不会漏带
|
||||||
...form,
|
const formData: ReviewFlow = toReviewFlowPayload(
|
||||||
organizationId: item.organizationId,
|
form,
|
||||||
reviewUserId: item.reviewUserId,
|
item,
|
||||||
level: index + 1
|
index + 1
|
||||||
};
|
);
|
||||||
delete (formData as ReviewFlow & { organizationIds?: number[] })
|
|
||||||
.organizationIds;
|
|
||||||
delete (formData as ReviewFlow & { flowItems?: ReviewFlowItem[] })
|
|
||||||
.flowItems;
|
|
||||||
successMessage = await addReviewFlow(formData);
|
successMessage = await addReviewFlow(formData);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/**
|
||||||
|
* 审批链保存时逐级构造提交体。
|
||||||
|
*
|
||||||
|
* 新建与编辑两条分支必须共用这一份构造逻辑:审批人规则(reviewerRule)是
|
||||||
|
* 每一级的字段而不是表单级字段,逐字段手拼时漏带会让后台录入的规则静默丢失
|
||||||
|
* (spec #58 A5)。这里顺带剥掉前端专用的 organizationIds / flowItems / id。
|
||||||
|
*/
|
||||||
|
export interface ReviewFlowItemLike {
|
||||||
|
organizationId?: number;
|
||||||
|
reviewUserId?: number;
|
||||||
|
// 审批人规则:';' 分隔可拼装多段,USER:<userId> / COLLEGE_ROLE:<roleCode>
|
||||||
|
reviewerRule?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 单级提交体:form 上的公共字段 + 当前级的机构/审批人/规则/层级 */
|
||||||
|
export const toReviewFlowPayload = (
|
||||||
|
form: Record<string, any>,
|
||||||
|
item: ReviewFlowItemLike,
|
||||||
|
level: number
|
||||||
|
): Record<string, any> => {
|
||||||
|
const payload: Record<string, any> = {
|
||||||
|
...form,
|
||||||
|
organizationId: item.organizationId,
|
||||||
|
reviewUserId: item.reviewUserId,
|
||||||
|
reviewerRule: item.reviewerRule?.trim() || undefined,
|
||||||
|
level
|
||||||
|
};
|
||||||
|
delete payload.organizationIds;
|
||||||
|
delete payload.flowItems;
|
||||||
|
delete payload.id;
|
||||||
|
return payload;
|
||||||
|
};
|
||||||
@@ -11,6 +11,10 @@
|
|||||||
@update:visible="updateVisible"
|
@update:visible="updateVisible"
|
||||||
@ok="save"
|
@ok="save"
|
||||||
>
|
>
|
||||||
|
<module-registry-notice
|
||||||
|
:error="moduleLoadError"
|
||||||
|
@retry="loadModuleOptions(form.module, form.moduleName, true)"
|
||||||
|
/>
|
||||||
<a-form
|
<a-form
|
||||||
ref="formRef"
|
ref="formRef"
|
||||||
:model="form"
|
:model="form"
|
||||||
@@ -84,10 +88,12 @@
|
|||||||
import type { Organization } from '@/api/system/organization/model';
|
import type { Organization } from '@/api/system/organization/model';
|
||||||
import {
|
import {
|
||||||
enabledModuleOptions,
|
enabledModuleOptions,
|
||||||
|
isModuleRegistryStale,
|
||||||
loadModuleRegistry,
|
loadModuleRegistry,
|
||||||
withLegacyModuleOption
|
withLegacyModuleOption
|
||||||
} from '@/utils/module-registry';
|
} from '@/utils/module-registry';
|
||||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||||
|
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
||||||
|
|
||||||
// 是否是修改
|
// 是否是修改
|
||||||
const isUpdate = ref(false);
|
const isUpdate = ref(false);
|
||||||
@@ -121,6 +127,7 @@
|
|||||||
// 模块下拉来自模块注册表(只含启用模块),不再维护前端硬编码清单
|
// 模块下拉来自模块注册表(只含启用模块),不再维护前端硬编码清单
|
||||||
const moduleLoading = ref(false);
|
const moduleLoading = ref(false);
|
||||||
const moduleOptions = ref<ModuleOption[]>([]);
|
const moduleOptions = ref<ModuleOption[]>([]);
|
||||||
|
const moduleLoadError = ref<string>();
|
||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
const form = reactive<ReviewFlowConfig>({
|
const form = reactive<ReviewFlowConfig>({
|
||||||
@@ -203,22 +210,40 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载模块下拉。
|
* 加载模块下拉(审批链配置的下拉只含启用模块)。
|
||||||
*
|
*
|
||||||
* 注册表接口只返回启用模块,编辑停用模块的历史审批链时把当前值补成
|
* 注册表快照显式传入,不再读模块级可变缓存;编辑停用模块的历史审批链时把
|
||||||
* 「标题(已停用)」,保证回显不空白。
|
* 当前值补成「标题(已停用)」,保证回显不空白。拉取失败时保留上次成功的
|
||||||
|
* 清单并在顶部提示 + 重试,不静默留空下拉。
|
||||||
*/
|
*/
|
||||||
const loadModuleOptions = async (module?: string, moduleName?: string) => {
|
const loadModuleOptions = async (
|
||||||
|
module?: string,
|
||||||
|
moduleName?: string,
|
||||||
|
force = false
|
||||||
|
) => {
|
||||||
moduleLoading.value = true;
|
moduleLoading.value = true;
|
||||||
|
moduleLoadError.value = undefined;
|
||||||
try {
|
try {
|
||||||
await loadModuleRegistry();
|
const list = await loadModuleRegistry(force);
|
||||||
|
moduleOptions.value = withLegacyModuleOption(
|
||||||
|
enabledModuleOptions(list),
|
||||||
|
module,
|
||||||
|
moduleName
|
||||||
|
);
|
||||||
|
// 命中「上次成功」的兜底快照:只给顶部提示 + 重试,不重复弹 message
|
||||||
|
if (isModuleRegistryStale()) {
|
||||||
|
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
moduleOptions.value = withLegacyModuleOption(
|
moduleOptions.value = withLegacyModuleOption(
|
||||||
enabledModuleOptions(),
|
enabledModuleOptions(),
|
||||||
module,
|
module,
|
||||||
moduleName
|
moduleName
|
||||||
);
|
);
|
||||||
} catch (e: any) {
|
moduleLoadError.value = moduleOptions.value.length
|
||||||
message.error(e.message || '模块清单加载失败');
|
? '模块清单加载失败,已保留上次成功加载的清单'
|
||||||
|
: '模块清单加载失败,请重试';
|
||||||
|
message.warning(moduleLoadError.value);
|
||||||
} finally {
|
} finally {
|
||||||
moduleLoading.value = false;
|
moduleLoading.value = false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -210,7 +210,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed, ref, shallowRef, watch } from 'vue';
|
||||||
import { Input, Modal, message } from 'ant-design-vue';
|
import { Input, Modal, message } from 'ant-design-vue';
|
||||||
import { h } from 'vue';
|
import { h } from 'vue';
|
||||||
import {
|
import {
|
||||||
@@ -245,6 +245,8 @@
|
|||||||
parseAttachmentImages,
|
parseAttachmentImages,
|
||||||
type AttachmentFile
|
type AttachmentFile
|
||||||
} from '@/views/gxmu/utils/attachment';
|
} from '@/views/gxmu/utils/attachment';
|
||||||
|
import { loadModuleRegistry } from '@/utils/module-registry';
|
||||||
|
import type { RegistryModule } from '@/utils/module-registry-core';
|
||||||
|
|
||||||
type SheetFieldType =
|
type SheetFieldType =
|
||||||
| 'text'
|
| 'text'
|
||||||
@@ -485,8 +487,11 @@
|
|||||||
const tzbcyMaterials = ref<TzbcyMaterial[]>([]);
|
const tzbcyMaterials = ref<TzbcyMaterial[]>([]);
|
||||||
const materialLoading = ref(false);
|
const materialLoading = ref(false);
|
||||||
|
|
||||||
|
// 注册表快照:详情配置由它显式解析,不再依赖模块级可变缓存
|
||||||
|
const moduleRegistry = shallowRef<ReadonlyArray<RegistryModule>>([]);
|
||||||
|
|
||||||
const currentConfig = computed<ModuleDetailConfig | undefined>(() =>
|
const currentConfig = computed<ModuleDetailConfig | undefined>(() =>
|
||||||
getModuleDetailConfig(props.module)
|
getModuleDetailConfig(props.module, moduleRegistry.value)
|
||||||
);
|
);
|
||||||
|
|
||||||
const modalTitle = computed(() => {
|
const modalTitle = computed(() => {
|
||||||
@@ -931,6 +936,14 @@
|
|||||||
activeTab.value = 'sheet';
|
activeTab.value = 'sheet';
|
||||||
tzbcyMaterials.value = [];
|
tzbcyMaterials.value = [];
|
||||||
|
|
||||||
|
// 先把注册表快照取到再解析配置,避免「加载前后返回不同配置」;
|
||||||
|
// 取不到时回退 module 本身解析,停用模块的历史数据仍能打开详情。
|
||||||
|
try {
|
||||||
|
moduleRegistry.value = await loadModuleRegistry();
|
||||||
|
} catch (e) {
|
||||||
|
moduleRegistry.value = [];
|
||||||
|
}
|
||||||
|
|
||||||
if (!props.pk || !currentConfig.value) {
|
if (!props.pk || !currentConfig.value) {
|
||||||
if (props.module && !currentConfig.value) {
|
if (props.module && !currentConfig.value) {
|
||||||
loadFailed.value = true;
|
loadFailed.value = true;
|
||||||
@@ -1086,4 +1099,4 @@
|
|||||||
color: #8c8c8c;
|
color: #8c8c8c;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -5,7 +5,8 @@
|
|||||||
* 中展示的附件字段,未列出的字段会兜底追加到「其他信息」分组,避免数据丢失。
|
* 中展示的附件字段,未列出的字段会兜底追加到「其他信息」分组,避免数据丢失。
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { detailKeyOf } from '@/utils/module-registry';
|
import { resolveDetailKey } from '@/utils/module-registry-core';
|
||||||
|
import type { RegistryModule } from '@/utils/module-registry-core';
|
||||||
|
|
||||||
export interface ModuleSection {
|
export interface ModuleSection {
|
||||||
// 分组标题
|
// 分组标题
|
||||||
@@ -899,16 +900,22 @@ export const TABLE_FIELD_LABELS: Record<string, string> = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 取模块的详情页配置。
|
* 取模块的详情页配置(纯函数:同样的入参一定得到同样的配置)。
|
||||||
*
|
*
|
||||||
* module → detailKey 的映射只走 detailKeyOf 一处(注册表 detail_key);
|
* module → detailKey 的映射只走 resolveDetailKey 一处(注册表 detail_key),
|
||||||
* 注册表未命中时回退 module 本身,停用模块的历史记录仍能打开详情。
|
* 注册表快照由调用方显式传入:不再读模块级可变缓存,避免同一 module 在注册表
|
||||||
|
* 加载前后解析出不同配置(spec #58 B10)。注册表未命中或未就绪时回退 module
|
||||||
|
* 本身,停用模块的历史记录仍能打开详情。
|
||||||
*/
|
*/
|
||||||
export const getModuleDetailConfig = (module?: string) => {
|
export const getModuleDetailConfig = (
|
||||||
|
module?: string,
|
||||||
|
registry?: readonly RegistryModule[]
|
||||||
|
) => {
|
||||||
if (!module) {
|
if (!module) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
const detailKey = detailKeyOf(module);
|
const detailKey =
|
||||||
|
registry && registry.length ? resolveDetailKey(registry, module) : module;
|
||||||
return (
|
return (
|
||||||
(detailKey && MODULE_DETAIL_CONFIGS[detailKey]) ||
|
(detailKey && MODULE_DETAIL_CONFIGS[detailKey]) ||
|
||||||
MODULE_DETAIL_CONFIGS[module]
|
MODULE_DETAIL_CONFIGS[module]
|
||||||
|
|||||||
@@ -1,22 +1,28 @@
|
|||||||
<template>
|
<template>
|
||||||
<a-space :size="10" style="flex-wrap: wrap">
|
<div>
|
||||||
<a-select
|
<module-registry-notice
|
||||||
v-model:value="where.module"
|
:error="moduleLoadError"
|
||||||
allow-clear
|
@retry="loadModuleOptions(true)"
|
||||||
placeholder="请选择模块"
|
|
||||||
style="width: 220px"
|
|
||||||
:options="moduleOptions"
|
|
||||||
@change="search"
|
|
||||||
/>
|
/>
|
||||||
<a-input-search
|
<a-space :size="10" style="flex-wrap: wrap">
|
||||||
v-model:value="where.keywords"
|
<a-select
|
||||||
allow-clear
|
v-model:value="where.module"
|
||||||
placeholder="请输入名称/审核意见"
|
allow-clear
|
||||||
style="width: 260px"
|
placeholder="请选择模块"
|
||||||
@search="search"
|
style="width: 220px"
|
||||||
@pressEnter="search"
|
:options="moduleOptions"
|
||||||
/>
|
@change="search"
|
||||||
</a-space>
|
/>
|
||||||
|
<a-input-search
|
||||||
|
v-model:value="where.keywords"
|
||||||
|
allow-clear
|
||||||
|
placeholder="请输入名称/审核意见"
|
||||||
|
style="width: 260px"
|
||||||
|
@search="search"
|
||||||
|
@pressEnter="search"
|
||||||
|
/>
|
||||||
|
</a-space>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
@@ -24,10 +30,12 @@
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
import type { ReviewListParam } from '@/api/gxmu/reviewList/model';
|
||||||
import {
|
import {
|
||||||
enabledModuleOptions,
|
allModuleOptions,
|
||||||
|
isModuleRegistryStale,
|
||||||
loadModuleRegistry
|
loadModuleRegistry
|
||||||
} from '@/utils/module-registry';
|
} from '@/utils/module-registry';
|
||||||
import type { ModuleOption } from '@/utils/module-registry-core';
|
import type { ModuleOption } from '@/utils/module-registry-core';
|
||||||
|
import ModuleRegistryNotice from '@/views/gxmu/components/moduleRegistryNotice.vue';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
@@ -48,17 +56,31 @@
|
|||||||
keywords: undefined
|
keywords: undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
// 模块筛选项来自模块注册表(只含启用模块),不再维护前端硬编码清单
|
// 模块筛选项来自模块注册表的全量清单(含停用模块,标题带「(已停用)」):
|
||||||
|
// 审核列表是「查存量」的入口,停用模块的历史记录仍要能按模块筛出来
|
||||||
const moduleOptions = ref<ModuleOption[]>([]);
|
const moduleOptions = ref<ModuleOption[]>([]);
|
||||||
|
const moduleLoadError = ref<string>();
|
||||||
|
|
||||||
onMounted(async () => {
|
const loadModuleOptions = async (force = false) => {
|
||||||
|
moduleLoadError.value = undefined;
|
||||||
try {
|
try {
|
||||||
await loadModuleRegistry();
|
const list = await loadModuleRegistry(force);
|
||||||
moduleOptions.value = enabledModuleOptions();
|
moduleOptions.value = allModuleOptions(list);
|
||||||
|
// 命中「上次成功」的兜底快照:只给顶部提示 + 重试,不重复弹 message
|
||||||
|
if (isModuleRegistryStale()) {
|
||||||
|
moduleLoadError.value = '模块清单加载失败,已保留上次成功加载的清单';
|
||||||
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e.message || '模块清单加载失败');
|
// 拉取失败:保留上次成功的清单(内存 / localStorage),顶部提示 + 重试
|
||||||
|
moduleOptions.value = allModuleOptions();
|
||||||
|
moduleLoadError.value = moduleOptions.value.length
|
||||||
|
? '模块清单加载失败,已保留上次成功加载的清单'
|
||||||
|
: '模块清单加载失败,请重试';
|
||||||
|
message.warning(moduleLoadError.value);
|
||||||
}
|
}
|
||||||
});
|
};
|
||||||
|
|
||||||
|
onMounted(() => loadModuleOptions());
|
||||||
|
|
||||||
const search = () => {
|
const search = () => {
|
||||||
emit('search', {
|
emit('search', {
|
||||||
|
|||||||
Reference in New Issue
Block a user