@oj-module/runtime 0.2.14 → 0.2.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,28 @@
1
+ import type { SolutionRouteRow } from "../../api/rbac/types";
2
+ import type { MenuItemType } from "../../layout/layout-menu/types";
3
+ /**
4
+ * 方案路由注册菜单归组(2026-10-21,docs/plans/2026-10-21-1028-solution-menu-grouping.md)。
5
+ *
6
+ * 纯函数、单一职责(SRP):只把已生成的菜单做"视觉层归组",不动路由树、不触碰
7
+ * i18n/翻译(组标题存裸 i18n key,交由渲染层 translateMenus 翻译)。跨列表操作
8
+ * (admin 项在 wholeMenus、`/system` 在 bottomMenus),故一次调用吃两段菜单。
9
+ *
10
+ * 依赖倒置(DIP):告警通道 onWarn 由调用方注入,缺省为模块级 warnOnce,便于单测
11
+ * 断言与生产环境接管。无副作用、不 mutate 入参(未变动项复用原引用,返回新数组)。
12
+ */
13
+ export interface GroupSolutionMenusOptions {
14
+ /** 宿主系统设置菜单 key,默认 "/system" */
15
+ systemKey?: string;
16
+ /** 告警回调;缺省走模块级 warnOnce(同消息只打一次) */
17
+ onWarn?: (msg: string) => void;
18
+ }
19
+ /**
20
+ * 归组入口。
21
+ *
22
+ * - 空表(未安装/端点失败)或无命中行 → 返回新数组(内容相等),开发形态不变;
23
+ * - 不 mutate 入参对象,未变动项复用原 menuItem 引用(仅重组数组与合成父项)。
24
+ */
25
+ export declare function groupSolutionMenus(top: MenuItemType[], bottom: MenuItemType[], rows: SolutionRouteRow[], opts?: GroupSolutionMenusOptions): {
26
+ top: MenuItemType[];
27
+ bottom: MenuItemType[];
28
+ };
package/dist/runtime.js CHANGED
@@ -256,10 +256,10 @@ function getAppInfo() {
256
256
  return {
257
257
  "pkg": {
258
258
  "name": "@oj-module/runtime",
259
- "version": "0.2.14",
259
+ "version": "0.2.15",
260
260
  "license": "MIT"
261
261
  },
262
- "lastBuildTime": "2026-10-21 09:05:39"
262
+ "lastBuildTime": "2026-10-21 12:22:16"
263
263
  };
264
264
  }
265
265
  var init_get_app_info = __esmMin((() => {}));
@@ -6077,6 +6077,7 @@ function useMenu() {
6077
6077
  * 菜单点击事件处理
6078
6078
  */
6079
6079
  const handleMenuSelect = (key, mode) => {
6080
+ if (key.startsWith("solgroup:")) return;
6080
6081
  if (key === removeTrailingSlash(pathname)) return;
6081
6082
  if (!shouldSplitMenuItems || mode !== "horizontal") {
6082
6083
  if (/http(s)?:/.test(key)) window.open(key);
@@ -8386,6 +8387,135 @@ function generateMenuItemsFromRoutes(routeList, parentPath = "/") {
8386
8387
  }
8387
8388
  var init_generate_menu_items_from_routes = __esmMin((() => {}));
8388
8389
  //#endregion
8390
+ //#region src/router/utils/group-solution-menus.ts
8391
+ function defaultWarn(msg) {
8392
+ if (warnedOnce.has(msg)) return;
8393
+ warnedOnce.add(msg);
8394
+ console.warn(msg);
8395
+ }
8396
+ /**
8397
+ * 处理单段菜单(顶层扫描,不递归 children):
8398
+ * - main 路由:模块可见项 ≥2 则延迟建组(组插到该模块首个被摘项原序位);否则原样直出;
8399
+ * - admin 路由:可挂载 `/system` 时摘除(交由调用方挂入 bottom 的 /system children),
8400
+ * 否则原样直出(回落顶层平铺,保持原序);
8401
+ * - 无对应行项:原样直出。
8402
+ */
8403
+ function processList(list, byPath, groupedModules, mountPossible) {
8404
+ const out = [];
8405
+ const groupsByModule = /* @__PURE__ */ new Map();
8406
+ const adminItems = [];
8407
+ list.forEach((it, idx) => {
8408
+ const row = byPath.get(it.key);
8409
+ if (!row) {
8410
+ out.push(it);
8411
+ return;
8412
+ }
8413
+ if (row.category === "admin") {
8414
+ adminItems.push({
8415
+ item: it,
8416
+ row
8417
+ });
8418
+ if (mountPossible) return;
8419
+ out.push(it);
8420
+ return;
8421
+ }
8422
+ if (groupedModules.has(row.module)) {
8423
+ let g = groupsByModule.get(row.module);
8424
+ if (!g) {
8425
+ g = {
8426
+ items: [],
8427
+ rows: [],
8428
+ firstIdx: idx
8429
+ };
8430
+ groupsByModule.set(row.module, g);
8431
+ }
8432
+ g.items.push(it);
8433
+ g.rows.push(row);
8434
+ return;
8435
+ }
8436
+ out.push(it);
8437
+ });
8438
+ const inserts = [...groupsByModule.values()].map((g) => ({
8439
+ index: g.firstIdx,
8440
+ group: makeGroup(MAIN_PREFIX, g.rows[0].module, g.items, g.rows)
8441
+ })).sort((a, b) => b.index - a.index);
8442
+ for (const ins of inserts) out.splice(ins.index, 0, ins.group);
8443
+ return {
8444
+ out,
8445
+ adminItems
8446
+ };
8447
+ }
8448
+ function makeGroup(prefix, module, items, rows) {
8449
+ const paired = items.map((item, i) => ({
8450
+ item,
8451
+ order: rows[i].order
8452
+ })).sort((a, b) => a.order - b.order);
8453
+ return {
8454
+ key: `${prefix}${module}`,
8455
+ label: `${module}:menu.group`,
8456
+ children: paired.map((p) => p.item)
8457
+ };
8458
+ }
8459
+ /**
8460
+ * 归组入口。
8461
+ *
8462
+ * - 空表(未安装/端点失败)或无命中行 → 返回新数组(内容相等),开发形态不变;
8463
+ * - 不 mutate 入参对象,未变动项复用原 menuItem 引用(仅重组数组与合成父项)。
8464
+ */
8465
+ function groupSolutionMenus(top, bottom, rows, opts = {}) {
8466
+ const systemKey = opts.systemKey ?? "/system";
8467
+ const onWarn = opts.onWarn ?? defaultWarn;
8468
+ if (rows.length === 0) return {
8469
+ top: [...top],
8470
+ bottom: [...bottom]
8471
+ };
8472
+ const byPath = /* @__PURE__ */ new Map();
8473
+ for (const r of rows) if ((r.category === "main" || r.category === "admin") && r.enabled !== false) byPath.set(r.path, r);
8474
+ if (byPath.size === 0) return {
8475
+ top: [...top],
8476
+ bottom: [...bottom]
8477
+ };
8478
+ const mainCount = /* @__PURE__ */ new Map();
8479
+ for (const list of [top, bottom]) for (const it of list) {
8480
+ const row = byPath.get(it.key);
8481
+ if (row?.category === "main") mainCount.set(row.module, (mainCount.get(row.module) ?? 0) + 1);
8482
+ }
8483
+ const groupedModules = new Set([...mainCount].filter(([, n]) => n >= 2).map(([module]) => module));
8484
+ const mountPossible = bottom.some((it) => it.key === systemKey);
8485
+ const topRes = processList(top, byPath, groupedModules, mountPossible);
8486
+ const bottomRes = processList(bottom, byPath, groupedModules, mountPossible);
8487
+ const adminGroupsMap = /* @__PURE__ */ new Map();
8488
+ for (const a of [...topRes.adminItems, ...bottomRes.adminItems]) {
8489
+ const arr = adminGroupsMap.get(a.row.module) ?? [];
8490
+ arr.push(a);
8491
+ adminGroupsMap.set(a.row.module, arr);
8492
+ }
8493
+ const adminGroups = [...adminGroupsMap.entries()].map(([module, items]) => ({
8494
+ minOrder: Math.min(...items.map((i) => i.row.order)),
8495
+ group: makeGroup(ADMIN_PREFIX, module, items.map((i) => i.item), items.map((i) => i.row))
8496
+ })).sort((a, b) => a.minOrder - b.minOrder);
8497
+ let newBottom = bottomRes.out;
8498
+ const sysIdx = newBottom.findIndex((it) => it.key === systemKey);
8499
+ if (mountPossible && sysIdx !== -1) {
8500
+ const system = newBottom[sysIdx];
8501
+ newBottom = newBottom.slice();
8502
+ newBottom[sysIdx] = {
8503
+ ...system,
8504
+ children: [...system.children ?? [], ...adminGroups.map((g) => g.group)]
8505
+ };
8506
+ } else if (adminGroups.length > 0) onWarn(`[solution-menu-grouping] system menu "${systemKey}" not found; admin items keep top-level (grouping degraded).`);
8507
+ return {
8508
+ top: topRes.out,
8509
+ bottom: newBottom
8510
+ };
8511
+ }
8512
+ var MAIN_PREFIX, ADMIN_PREFIX, warnedOnce;
8513
+ var init_group_solution_menus = __esmMin((() => {
8514
+ MAIN_PREFIX = "solgroup:main:";
8515
+ ADMIN_PREFIX = "solgroup:admin:";
8516
+ warnedOnce = /* @__PURE__ */ new Set();
8517
+ }));
8518
+ //#endregion
8389
8519
  //#region src/router/utils/merge-solution-categories.ts
8390
8520
  /**
8391
8521
  * 方案路由归并(纯函数,R21):按 path 把 sys_solution_route 行注记到已注册路由上。
@@ -8495,6 +8625,7 @@ var init_access = __esmMin((() => {
8495
8625
  init_ascending();
8496
8626
  init_flatten_routes();
8497
8627
  init_generate_menu_items_from_routes();
8628
+ init_group_solution_menus();
8498
8629
  init_merge_solution_categories();
8499
8630
  init_resolve_login_route();
8500
8631
  baseMenuRoutes = splitMenuRoutes(baseRoutes);
@@ -8514,9 +8645,10 @@ var init_access = __esmMin((() => {
8514
8645
  router.patchRoutes(ROOT_ROUTE_ID, routes);
8515
8646
  const flatRouteList = flattenRoutes(newRoutes);
8516
8647
  const { top, bottom } = splitMenuRoutes(newRoutes);
8648
+ const grouped = groupSolutionMenus(generateMenuItemsFromRoutes(top), generateMenuItemsFromRoutes(bottom), get().solutionRoutes);
8517
8649
  const newState = {
8518
- wholeMenus: generateMenuItemsFromRoutes(top),
8519
- bottomMenus: generateMenuItemsFromRoutes(bottom),
8650
+ wholeMenus: grouped.top,
8651
+ bottomMenus: grouped.bottom,
8520
8652
  routeList: newRoutes,
8521
8653
  flatRouteList,
8522
8654
  isAccessChecked: true
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/runtime",
3
3
  "type": "module",
4
- "version": "0.2.14",
4
+ "version": "0.2.15",
5
5
  "description": "React Antd Admin 框架运行时,供模块工程消费",
6
6
  "license": "MIT",
7
7
  "imports": {