@oj-module/cli 0.1.17 → 0.1.21

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.
Files changed (69) hide show
  1. package/devkit/README.md +11 -10
  2. package/devkit/SKILL.md +32 -18
  3. package/devkit/manual.md +332 -137
  4. package/package.json +2 -2
  5. package/shell-dist/assets/contract.js +0 -55
  6. package/shell-dist/assets/runtime.js +229 -213
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +5 -5
  9. package/src/build.ts +3 -0
  10. package/src/contract/__tests__/check-api.test.ts +74 -0
  11. package/src/contract/__tests__/emit-client.test.ts +132 -0
  12. package/src/contract/__tests__/emit-jsonschema-zod.test.ts +93 -0
  13. package/src/contract/__tests__/mock.test.ts +56 -0
  14. package/src/contract/__tests__/openapi-ir.test.ts +137 -0
  15. package/src/contract/__tests__/run-api.test.ts +67 -0
  16. package/src/contract/check.ts +67 -294
  17. package/src/contract/emit-client.ts +23 -15
  18. package/src/contract/emit-jsonschema-zod.ts +209 -0
  19. package/src/contract/emit-oj-schema.ts +108 -161
  20. package/src/contract/emit-stub.ts +17 -57
  21. package/src/contract/handler-scan.ts +263 -0
  22. package/src/contract/ir.ts +11 -179
  23. package/src/contract/json-schema-sample.ts +72 -0
  24. package/src/contract/mock.ts +28 -100
  25. package/src/contract/openapi-ir.ts +145 -0
  26. package/src/contract/run.ts +71 -120
  27. package/src/dev.ts +20 -18
  28. package/src/init.ts +18 -3
  29. package/src/oj.ts +6 -2
  30. package/src/vendor.ts +8 -4
  31. package/templates/api/config.yaml +1 -0
  32. package/templates/api/src/auth/api.ts +66 -0
  33. package/templates/api/src/notification/_shared/notify.ts +199 -0
  34. package/templates/api/src/notification/api.ts +231 -0
  35. package/templates/api/src/notification/del/api.ts +47 -0
  36. package/templates/api/src/notification/manifest.yaml +3 -1
  37. package/templates/api/src/notification/migrations/0001__create_notifications.sql +1 -1
  38. package/templates/api/src/notification/migrations/0002__notifications_user.sql +9 -0
  39. package/templates/api/src/notification/migrations/0003__notifications_broadcast.sql +22 -0
  40. package/templates/api/src/notification/schema.yaml +23 -1
  41. package/templates/api/src/notification/seed.sql +6 -4
  42. package/templates/api/src/personal-center/upload/api.ts +12 -1
  43. package/templates/env.d.ts +1 -1
  44. package/templates/openapi.json +471 -0
  45. package/templates/web/src/auth/__client/api.schemas.ts +8 -0
  46. package/templates/web/src/auth/__client/api.ts +110 -0
  47. package/templates/web/src/notification/__client/api.schemas.ts +50 -0
  48. package/templates/web/src/notification/{client → __client}/api.ts +29 -29
  49. package/templates/web/src/notification/entry.ts +11 -9
  50. package/templates/web/src/notifications/__client/api.schemas.ts +8 -0
  51. package/templates/web/src/notifications/__client/api.ts +80 -0
  52. package/templates/web/src/personal-center/__client/api.schemas.ts +10 -0
  53. package/templates/web/src/personal-center/__client/api.ts +88 -0
  54. package/templates/web/src/web/__client/api.schemas.ts +8 -0
  55. package/templates/web/src/web/__client/api.ts +110 -0
  56. package/vendor/host-versions.json +5 -5
  57. package/src/contract/emit-meta.ts +0 -175
  58. package/src/contract/emit-schema.ts +0 -133
  59. package/src/contract/evaluate.ts +0 -88
  60. package/templates/api/src/auth/login/api.ts +0 -27
  61. package/templates/api/src/auth/logout/api.ts +0 -10
  62. package/templates/api/src/auth/refresh/api.ts +0 -26
  63. package/templates/api/src/notification/contract.ts +0 -98
  64. package/templates/api/src/notification/notifications/api.ts +0 -24
  65. package/templates/api/src/notification/notifications/clear/api.ts +0 -13
  66. package/templates/api/src/notification/notifications/read/api.ts +0 -19
  67. package/templates/api/src/notification/notifications/read-all/api.ts +0 -13
  68. package/templates/api/src/personal-center/contract.ts +0 -16
  69. package/templates/web/src/notification/client/api.schemas.ts +0 -50
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@oj-module/cli",
3
3
  "type": "module",
4
- "version": "0.1.17",
4
+ "version": "0.1.21",
5
5
  "description": "React Antd Admin 模块工程命令行工具(含预构建宿主 shell-dist)",
6
6
  "license": "MIT",
7
7
  "exports": {
@@ -49,7 +49,7 @@
49
49
  ],
50
50
  "dependencies": {
51
51
  "@faker-js/faker": "^10.4.0",
52
- "@oj-module/runtime": "0.1.17",
52
+ "@oj-module/runtime": "0.1.21",
53
53
  "@redocly/cli": "^2.51.0",
54
54
  "@tailwindcss/vite": "^4.3.0",
55
55
  "esbuild": "^0.28.2",
@@ -9,63 +9,10 @@ var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "sy
9
9
  // ../runtime/dist/contract/index.js
10
10
  var contract_exports = {};
11
11
  __export(contract_exports, {
12
- API_DEF: () => API_DEF,
13
12
  ContractApiError: () => ContractApiError,
14
- defineApi: () => defineApi,
15
13
  z: () => external_exports
16
14
  });
17
15
 
18
- // ../runtime/dist/contract/define-api.js
19
- var PARAM_SEGMENT = /^\{\*?[a-z_]\w*\}$/i;
20
- function fail(route, reason) {
21
- throw new Error(`[\u5951\u7EA6] \u7AEF\u70B9\u5B9A\u4E49\u975E\u6CD5\uFF08route: ${route}\uFF09\uFF1A${reason}`);
22
- }
23
- function validateDefinition(def) {
24
- const { apiPrefix, route } = def;
25
- if (!apiPrefix?.startsWith("/")) {
26
- throw new Error(`[\u5951\u7EA6] apiPrefix \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF08\u6536\u5230: ${apiPrefix}\uFF09\u2014\u2014\u5982 "/order"\uFF1Buni-dev \u5F62\u6001\u8BF7\u4E0E oj \u6A21\u5757\u6BB5\u540D\u4FDD\u6301\u4E00\u81F4\uFF08AC-D9\uFF09\u3002`);
27
- }
28
- if (def.urlPrefix !== void 0 && !def.urlPrefix.startsWith("/"))
29
- fail(route, `urlPrefix \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF08\u6536\u5230: ${def.urlPrefix}\uFF09\u3002`);
30
- if (!route?.startsWith("/"))
31
- fail(route, `route \u5FC5\u987B\u4EE5 "/" \u5F00\u5934\uFF08\u6536\u5230: ${route}\uFF09\uFF0C\u4E14\u4E00\u5F8B\u76F8\u5BF9 apiPrefix\u2014\u2014\u4E0D\u652F\u6301 oj \u7684\u6839\u7EDD\u5BF9\u5199\u6CD5\uFF08\u6A21\u5757\u624B\u518C D11 \u524D\u7F00\u6536\u655B\uFF09\u3002`);
32
- if (route.split("/").some((seg) => seg === ".." || seg === "." || seg === "\\"))
33
- fail(route, "route \u542B\u8DEF\u5F84\u7A7F\u8D8A\u6BB5\uFF08.././\\\uFF09\uFF0C\u8BF7\u6539\u4E3A\u6B63\u5E38\u9759\u6001\u6BB5\u3002");
34
- for (const seg of route.split("/")) {
35
- if (seg.includes("{") && !PARAM_SEGMENT.test(seg))
36
- fail(route, `\u53C2\u6570\u6BB5 "${seg}" \u6DF7\u5165\u5B57\u9762\u91CF\u2014\u2014matchit \u7EA6\u675F\uFF1A\u53C2\u6570\u6BB5\u5FC5\u987B\u6574\u6BB5\u4E3A {name} \u6216 {*name}\uFF0C\u9700\u8981\u524D\u7F00/\u540E\u7F00\u5B57\u9762\u65F6\u8BF7\u62C6\u6210\u9759\u6001\u591A\u6BB5\u3002`);
37
- }
38
- if (def.data && def.response === "raw")
39
- fail(route, 'data schema \u4E0E response:"raw" \u4E92\u65A5\u2014\u2014raw \u7AEF\u70B9\u4E0D\u89E3\u5305\u4FE1\u5C01\uFF0C\u4E0D\u9700\u8981 data schema\u3002');
40
- if (def.form && def.body)
41
- fail(route, "form \u4E0E body \u4E92\u65A5\u2014\u2014\u4E00\u4E2A\u7AEF\u70B9\u53EA\u6709\u4E00\u79CD\u8BF7\u6C42\u4F53\u5F62\u6001\uFF1Amultipart/form-data\uFF08form\uFF09\u4E0E JSON\uFF08body\uFF09\u58F0\u660E\u5176\u4E00\uFF0C\u6587\u672C\u5B57\u6BB5\u8BF7\u5199\u8FDB form.fields\u3002");
42
- if (def.form) {
43
- const files = def.form.files ?? [];
44
- if (!def.form.fields && files.length === 0)
45
- fail(route, "form \u81F3\u5C11\u9700\u8981 fields \u6216\u4E00\u9879 files\u2014\u2014\u7A7A multipart \u8BF7\u6C42\u4F53\u65E0\u610F\u4E49\uFF1B\u65E0\u8BF7\u6C42\u4F53\u8BF7\u5220\u6389 form\uFF08\u6539\u7528 query/params\uFF09\u3002");
46
- const seen = /* @__PURE__ */ new Set();
47
- for (const file2 of files) {
48
- if (!file2?.name)
49
- fail(route, "form.files \u6BCF\u9879\u5FC5\u987B\u6709 name\u2014\u2014\u5B83\u65E2\u662F FormData \u7684 append key\uFF0C\u4E5F\u662F oj \u4FA7 http.files[].field \u7684\u5B57\u6BB5\u540D\u3002");
50
- if (seen.has(file2.name))
51
- fail(route, `form.files \u5B57\u6BB5\u540D "${file2.name}" \u91CD\u590D\u2014\u2014\u540C\u540D\u591A\u6587\u4EF6\u8BF7\u7528 multiple: true\u3002`);
52
- seen.add(file2.name);
53
- }
54
- }
55
- if (def.response !== void 0 && def.response !== "raw")
56
- fail(route, `response \u4EC5\u652F\u6301 "raw"\uFF08\u6536\u5230: ${String(def.response)}\uFF09\u3002`);
57
- if (def.method === "OPTIONS")
58
- fail(route, "OPTIONS \u4E0D\u5728\u652F\u6301\u8303\u56F4\uFF08ky/oj \u5747\u65E0\u5BF9\u5E94\u65B9\u6CD5\uFF09\u2014\u2014\u9884\u68C0\u8BF7\u6C42\u7531 fetch/CORS \u5C42\u5904\u7406\uFF0C\u5951\u7EA6\u4E0D\u8868\u8FBE\u3002");
59
- if (def.method === "HEAD" && def.data)
60
- fail(route, "HEAD \u7AEF\u70B9\u65E0\u54CD\u5E94\u4F53\uFF0C\u4E0D\u80FD\u58F0\u660E data schema\u2014\u2014\u9700\u8981\u54CD\u5E94\u4F53\u8BF7\u6539\u7528 GET\u3002");
61
- }
62
- var API_DEF = /* @__PURE__ */ Symbol.for("ojm.api.def");
63
- function defineApi(def) {
64
- validateDefinition(def);
65
- Object.defineProperty(def, API_DEF, { value: true, enumerable: false });
66
- return def;
67
- }
68
-
69
16
  // ../runtime/dist/contract/errors.js
70
17
  var ContractApiError = class extends Error {
71
18
  constructor(code, msg) {
@@ -18947,9 +18894,7 @@ function date4(params) {
18947
18894
  // shell/.ojm-shim-contract.mjs
18948
18895
  var ojm_shim_contract_default = void 0 ?? contract_exports;
18949
18896
  export {
18950
- API_DEF,
18951
18897
  ContractApiError,
18952
18898
  ojm_shim_contract_default as default,
18953
- defineApi,
18954
18899
  external_exports as z
18955
18900
  };
@@ -255,10 +255,10 @@ function getAppInfo() {
255
255
  return {
256
256
  "pkg": {
257
257
  "name": "@oj-module/runtime",
258
- "version": "0.1.17",
258
+ "version": "0.1.21",
259
259
  "license": "MIT"
260
260
  },
261
- "lastBuildTime": "2026-10-13 22:01:51"
261
+ "lastBuildTime": "2026-10-15 18:45:29"
262
262
  };
263
263
  }
264
264
  var init_get_app_info = __esmMin((() => {}));
@@ -3580,7 +3580,7 @@ var init_use_layout_style = __esmMin((() => {
3580
3580
  }));
3581
3581
  //#endregion
3582
3582
  //#region src/store/tabs.ts
3583
- var initialState$3, useTabsStore;
3583
+ var initialState$3, closingKeys, useTabsStore;
3584
3584
  var init_tabs = __esmMin((() => {
3585
3585
  init_preferences$3();
3586
3586
  init_get_app_namespace();
@@ -3606,224 +3606,218 @@ var init_tabs = __esmMin((() => {
3606
3606
  */
3607
3607
  isMaximize: false
3608
3608
  };
3609
- useTabsStore = create()(persist((set) => ({
3610
- ...initialState$3,
3609
+ closingKeys = /* @__PURE__ */ new Set();
3610
+ useTabsStore = create()(persist((set, get) => {
3611
3611
  /**
3612
- * @zh 设置标签页是否处于刷新状态
3613
- * @en Set whether the tab is in a refresh state.
3612
+ * 关闭前守卫:待关闭集合里任一标签页声明了 beforeClose 且返回 false / 抛错时,整体中止。
3613
+ * fail-safe:守卫抛错按中止处理,防误关丢内容;console.error 留排查线索。
3614
3614
  */
3615
- setIsRefresh: (state) => {
3616
- set({ isRefresh: state });
3617
- },
3618
- /**
3619
- * @zh 设置标签页
3620
- * @en Set the tab.
3621
- */
3622
- setActiveKey: (routePath) => {
3623
- set({ activeKey: routePath });
3624
- },
3625
- /**
3626
- * @zh 在最前面插入标签页
3627
- * @en Insert a tab at the front.
3628
- */
3629
- insertBeforeTab: (routePath, tabProps) => {
3630
- set((state) => {
3631
- if (routePath?.length) {
3632
- const newMap = /* @__PURE__ */ new Map([[routePath, tabProps]]);
3633
- for (const [key, value] of state.openTabs) newMap.set(key, value);
3634
- return { openTabs: newMap };
3635
- }
3636
- return state;
3637
- });
3638
- },
3639
- /**
3640
- * @zh 添加标签页
3641
- * @en Add a tab.
3642
- */
3643
- addTab: (routePath, tabProps) => {
3644
- set((state) => {
3645
- if (routePath?.length) {
3646
- const newTabs = new Map(state.openTabs);
3647
- /**
3648
- * 1. 如果 tab 已经存在,则更新 historyState 属性,所以不去重,且 ...newTabs.get(routePath) 是为了保证首页的 closable 属性不被覆盖
3649
- * 2. 如果 tab 不存在,则添加到 Map 中
3650
- */
3651
- newTabs.set(routePath, {
3652
- ...newTabs.get(routePath),
3653
- ...tabProps
3654
- });
3655
- return { openTabs: newTabs };
3615
+ const guardClose = async (keys) => {
3616
+ for (const key of keys) {
3617
+ const fn = get().openTabs.get(key)?.beforeClose;
3618
+ if (fn == null) continue;
3619
+ const fns = Array.isArray(fn) ? fn : [fn];
3620
+ for (const f of fns) try {
3621
+ if (!await f()) return false;
3622
+ } catch (e) {
3623
+ console.error("[tabs] beforeClose 守卫抛错,按中止关闭处理", e);
3624
+ return false;
3656
3625
  }
3657
- return state;
3658
- });
3659
- },
3626
+ }
3627
+ return true;
3628
+ };
3660
3629
  /**
3661
- * @zh 移除标签页
3662
- * @en Remove a tab.
3630
+ * 共用关闭路径:in-flight 去重 → 守卫 → 精确删除获批集合(TOCTOU 防护:
3631
+ * 不对 live state 重算,await 期间新开的 tab 不受影响)→ activeKey 修正。
3663
3632
  */
3664
- removeTab: (routePath) => {
3665
- set((state) => {
3633
+ const guardAndClose = async (removed, fallbackActive) => {
3634
+ if (removed.length === 0) return;
3635
+ for (const k of removed) closingKeys.add(k);
3636
+ try {
3637
+ if (!await guardClose(removed)) return;
3638
+ set((state) => {
3639
+ const toClose = new Set(removed);
3640
+ const newTabs = new Map([...state.openTabs].filter(([k]) => !toClose.has(k)));
3641
+ if (newTabs.size === state.openTabs.size) return state;
3642
+ const remaining = [...newTabs.keys()];
3643
+ return {
3644
+ openTabs: newTabs,
3645
+ activeKey: toClose.has(state.activeKey) || !newTabs.has(state.activeKey) ? fallbackActive(remaining) : state.activeKey
3646
+ };
3647
+ });
3648
+ } finally {
3649
+ for (const k of removed) closingKeys.delete(k);
3650
+ }
3651
+ };
3652
+ return {
3653
+ ...initialState$3,
3654
+ /**
3655
+ * @zh 设置标签页是否处于刷新状态
3656
+ * @en Set whether the tab is in a refresh state.
3657
+ */
3658
+ setIsRefresh: (state) => {
3659
+ set({ isRefresh: state });
3660
+ },
3661
+ /**
3662
+ * @zh 设置标签页
3663
+ * @en Set the tab.
3664
+ */
3665
+ setActiveKey: (routePath) => {
3666
+ set({ activeKey: routePath });
3667
+ },
3668
+ /**
3669
+ * @zh 在最前面插入标签页
3670
+ * @en Insert a tab at the front.
3671
+ */
3672
+ insertBeforeTab: (routePath, tabProps) => {
3673
+ set((state) => {
3674
+ if (routePath?.length) {
3675
+ const newMap = /* @__PURE__ */ new Map([[routePath, tabProps]]);
3676
+ for (const [key, value] of state.openTabs) newMap.set(key, value);
3677
+ return { openTabs: newMap };
3678
+ }
3679
+ return state;
3680
+ });
3681
+ },
3682
+ /**
3683
+ * @zh 添加标签页
3684
+ * @en Add a tab.
3685
+ */
3686
+ addTab: (routePath, tabProps) => {
3687
+ set((state) => {
3688
+ if (routePath?.length) {
3689
+ const newTabs = new Map(state.openTabs);
3690
+ /**
3691
+ * 1. 如果 tab 已经存在,则更新 historyState 属性,所以不去重,且 ...newTabs.get(routePath) 是为了保证首页的 closable 属性不被覆盖
3692
+ * 2. 如果 tab 不存在,则添加到 Map 中
3693
+ * 3. beforeClose 是注册语义而非覆盖语义:数组 concat + 按引用去重
3694
+ * (keep-alive 重挂载重复注册不叠加);null 显式清除全部守卫
3695
+ */
3696
+ const prev = newTabs.get(routePath);
3697
+ const merged = {
3698
+ ...prev,
3699
+ ...tabProps
3700
+ };
3701
+ if (tabProps.beforeClose === null) merged.beforeClose = void 0;
3702
+ else if (prev?.beforeClose != null && tabProps.beforeClose !== void 0) merged.beforeClose = [...Array.isArray(prev.beforeClose) ? prev.beforeClose : [prev.beforeClose], ...Array.isArray(tabProps.beforeClose) ? tabProps.beforeClose : [tabProps.beforeClose]].filter((f, i, a) => a.indexOf(f) === i);
3703
+ newTabs.set(routePath, merged);
3704
+ return { openTabs: newTabs };
3705
+ }
3706
+ return state;
3707
+ });
3708
+ },
3709
+ /**
3710
+ * @zh 移除标签页
3711
+ * @en Remove a tab.
3712
+ */
3713
+ removeTab: async (routePath) => {
3666
3714
  const homePath = "/home";
3667
- if (routePath === homePath) return state;
3668
- const newTabs = new Map(state.openTabs);
3669
- newTabs.delete(routePath);
3670
- let newActiveKey = state.activeKey;
3671
- if (routePath === state.activeKey) newActiveKey = Array.from(newTabs.keys()).at(-1) || homePath;
3672
- if (newTabs.size === 0) {
3673
- newTabs.set(homePath, state.openTabs.get(homePath));
3674
- newActiveKey = homePath;
3675
- }
3676
- return {
3677
- openTabs: newTabs,
3678
- activeKey: newActiveKey
3679
- };
3680
- });
3681
- },
3682
- /**
3683
- * @zh 关闭右侧标签页
3684
- * @en Close tabs on the right.
3685
- */
3686
- closeRightTabs: (routePath) => {
3687
- set((state) => {
3688
- const newTabs = /* @__PURE__ */ new Map();
3689
- let found = false;
3690
- let activeKeyFound = false;
3691
- let newActiveKey = state.activeKey;
3692
- for (const [key, value] of state.openTabs) {
3693
- if (found) break;
3694
- newTabs.set(key, value);
3695
- if (key === routePath) found = true;
3696
- if (key === state.activeKey) activeKeyFound = true;
3697
- }
3698
- if (!activeKeyFound) newActiveKey = routePath;
3699
- return {
3700
- openTabs: newTabs,
3701
- activeKey: newActiveKey
3702
- };
3703
- });
3704
- },
3705
- /**
3706
- * @zh 关闭左侧标签页
3707
- * @en Close tabs on the left.
3708
- */
3709
- closeLeftTabs: (routePath) => {
3710
- set((state) => {
3711
- const newTabs = /* @__PURE__ */ new Map();
3715
+ if (!routePath?.length || routePath === homePath || closingKeys.has(routePath)) return;
3716
+ await guardAndClose([routePath], (keys) => keys.at(-1) ?? homePath);
3717
+ },
3718
+ /**
3719
+ * @zh 关闭右侧标签页
3720
+ * @en Close tabs on the right.
3721
+ */
3722
+ closeRightTabs: async (routePath) => {
3723
+ if (closingKeys.has(routePath)) return;
3724
+ const keys = [...get().openTabs.keys()];
3725
+ const idx = keys.indexOf(routePath);
3726
+ if (idx < 0) return;
3727
+ await guardAndClose(keys.slice(idx + 1), () => routePath);
3728
+ },
3729
+ /**
3730
+ * @zh 关闭左侧标签页
3731
+ * @en Close tabs on the left.
3732
+ */
3733
+ closeLeftTabs: async (routePath) => {
3734
+ if (closingKeys.has(routePath)) return;
3712
3735
  const homePath = "/home";
3713
- let found = false;
3714
- let newActiveKey = state.activeKey;
3715
- let activeKeyOnRight = false;
3716
- newTabs.set(homePath, state.openTabs.get(homePath));
3717
- for (const [key, value] of state.openTabs) {
3718
- if (key === homePath) continue;
3719
- if (found || key === routePath) {
3720
- newTabs.set(key, value);
3721
- found = true;
3722
- }
3723
- if (key === state.activeKey && found) activeKeyOnRight = true;
3724
- }
3725
- if (!activeKeyOnRight) newActiveKey = routePath;
3726
- return {
3727
- openTabs: newTabs,
3728
- activeKey: newActiveKey
3729
- };
3730
- });
3731
- },
3732
- /**
3733
- * @zh 关闭其他标签页
3734
- * @en Close other tabs.
3735
- */
3736
- closeOtherTabs: (routePath) => {
3737
- set((state) => {
3738
- const newTabs = /* @__PURE__ */ new Map();
3736
+ const keys = [...get().openTabs.keys()];
3737
+ const idx = keys.indexOf(routePath);
3738
+ if (idx < 0) return;
3739
+ await guardAndClose(keys.slice(0, idx).filter((k) => k !== homePath), () => routePath);
3740
+ },
3741
+ /**
3742
+ * @zh 关闭其他标签页
3743
+ * @en Close other tabs.
3744
+ */
3745
+ closeOtherTabs: async (routePath) => {
3746
+ if (closingKeys.has(routePath)) return;
3739
3747
  const homePath = "/home";
3740
- newTabs.set(homePath, state.openTabs.get(homePath));
3741
- if (routePath !== homePath && state.openTabs.has(routePath)) newTabs.set(routePath, state.openTabs.get(routePath));
3742
- let newActiveKey = state.activeKey;
3743
- if (!newTabs.has(state.activeKey)) newActiveKey = routePath;
3744
- return {
3745
- openTabs: newTabs,
3746
- activeKey: newActiveKey
3747
- };
3748
- });
3749
- },
3750
- /**
3751
- * @zh 关闭所有标签页
3752
- * @en Close all tabs.
3753
- */
3754
- closeAllTabs: () => {
3755
- set((state) => {
3756
- const newTabs = /* @__PURE__ */ new Map();
3748
+ await guardAndClose([...get().openTabs.keys()].filter((k) => k !== homePath && k !== routePath), () => routePath);
3749
+ },
3750
+ /**
3751
+ * @zh 关闭所有标签页
3752
+ * @en Close all tabs.
3753
+ */
3754
+ closeAllTabs: async () => {
3757
3755
  const homePath = "/home";
3758
- newTabs.set(homePath, state.openTabs.get(homePath));
3759
- return {
3760
- openTabs: newTabs,
3761
- activeKey: homePath
3762
- };
3763
- });
3764
- },
3765
- /**
3766
- * @zh 更改标签页顺序
3767
- * @en Change tab order.
3768
- */
3769
- changeTabOrder: (from, to) => {
3770
- set((state) => {
3771
- const newTabs = Array.from(state.openTabs.entries());
3772
- const [movedTab] = newTabs.splice(from, 1);
3773
- newTabs.splice(to, 0, movedTab);
3774
- return { openTabs: new Map(newTabs) };
3775
- });
3776
- },
3777
- /**
3778
- * @zh 切换标签页最大化状态
3779
- * @en Toggle tab maximization status
3780
- * @param {boolean} state - 最大化状态
3781
- */
3782
- toggleMaximize: (state) => {
3783
- set({ isMaximize: state });
3784
- },
3785
- /**
3786
- * @zh 设置标签页标题
3787
- * @en Set the tab title
3788
- */
3789
- setTableTitle: (routePath, title) => {
3790
- set((state) => {
3791
- const newTabs = new Map(state.openTabs);
3792
- const targetTab = newTabs.get(routePath);
3793
- if (targetTab) {
3794
- targetTab.newTabTitle = title;
3795
- newTabs.set(routePath, targetTab);
3796
- return { openTabs: newTabs };
3797
- }
3798
- return state;
3799
- });
3800
- },
3801
- /**
3802
- * @zh 重置标签页标题(删除自定义的标题)
3803
- * @en Reset the tab title (delete custom titles)
3804
- */
3805
- resetTableTitle: (routePath) => {
3806
- set((state) => {
3807
- const newTabs = new Map(state.openTabs);
3808
- const targetTab = newTabs.get(routePath);
3809
- if (targetTab) {
3810
- delete targetTab.newTabTitle;
3811
- newTabs.set(routePath, targetTab);
3812
- return { openTabs: newTabs };
3813
- }
3814
- return state;
3815
- });
3816
- },
3817
- /**
3818
- * @zh 重置所有标签页的状态
3819
- * @en Reset all tab states
3820
- */
3821
- resetTabs: () => {
3822
- set(() => {
3823
- return { ...initialState$3 };
3824
- });
3825
- }
3826
- }), {
3756
+ await guardAndClose([...get().openTabs.keys()].filter((k) => k !== homePath), () => homePath);
3757
+ },
3758
+ /**
3759
+ * @zh 更改标签页顺序
3760
+ * @en Change tab order.
3761
+ */
3762
+ changeTabOrder: (from, to) => {
3763
+ set((state) => {
3764
+ const newTabs = Array.from(state.openTabs.entries());
3765
+ const [movedTab] = newTabs.splice(from, 1);
3766
+ newTabs.splice(to, 0, movedTab);
3767
+ return { openTabs: new Map(newTabs) };
3768
+ });
3769
+ },
3770
+ /**
3771
+ * @zh 切换标签页最大化状态
3772
+ * @en Toggle tab maximization status
3773
+ * @param {boolean} state - 最大化状态
3774
+ */
3775
+ toggleMaximize: (state) => {
3776
+ set({ isMaximize: state });
3777
+ },
3778
+ /**
3779
+ * @zh 设置标签页标题
3780
+ * @en Set the tab title
3781
+ */
3782
+ setTableTitle: (routePath, title) => {
3783
+ set((state) => {
3784
+ const newTabs = new Map(state.openTabs);
3785
+ const targetTab = newTabs.get(routePath);
3786
+ if (targetTab) {
3787
+ targetTab.newTabTitle = title;
3788
+ newTabs.set(routePath, targetTab);
3789
+ return { openTabs: newTabs };
3790
+ }
3791
+ return state;
3792
+ });
3793
+ },
3794
+ /**
3795
+ * @zh 重置标签页标题(删除自定义的标题)
3796
+ * @en Reset the tab title (delete custom titles)
3797
+ */
3798
+ resetTableTitle: (routePath) => {
3799
+ set((state) => {
3800
+ const newTabs = new Map(state.openTabs);
3801
+ const targetTab = newTabs.get(routePath);
3802
+ if (targetTab) {
3803
+ delete targetTab.newTabTitle;
3804
+ newTabs.set(routePath, targetTab);
3805
+ return { openTabs: newTabs };
3806
+ }
3807
+ return state;
3808
+ });
3809
+ },
3810
+ /**
3811
+ * @zh 重置所有标签页的状态
3812
+ * @en Reset all tab states
3813
+ */
3814
+ resetTabs: () => {
3815
+ set(() => {
3816
+ return { ...initialState$3 };
3817
+ });
3818
+ }
3819
+ };
3820
+ }, {
3827
3821
  name: getAppNamespace("tabbar"),
3828
3822
  /**
3829
3823
  * activeKey 不需要持久化存储
@@ -9378,6 +9372,28 @@ function getBooleanOptions(t) {
9378
9372
  }];
9379
9373
  }
9380
9374
  //#endregion
9375
+ //#region src/hooks/use-tab-key/index.ts
9376
+ init_remove_trailing_slash();
9377
+ /**
9378
+ * 本页自己的页签 key(`removeTrailingSlash(pathname)`),供 `setTableTitle` /
9379
+ * `resetTableTitle` / `addTab` 定位页签。
9380
+ *
9381
+ * keepAlive 陷阱:keepalive-for-react 把页面缓存在隐藏 portal 里,而 LocationContext
9382
+ * 在 KeepAlive **之上**——切 tab 时缓存树随**激活页**位置重渲染,缓存页里的
9383
+ * `useLocation()` 返回的是别家页签的路径。若拿它当页签 key,`setTableTitle` 会把
9384
+ * 本页标题写到当前激活页签上(最后打开的文档页把之后切换到的每个页签都改成自己的标题)。
9385
+ *
9386
+ * 页签存活期内 key 不变(宿主 cacheKey = pathname+search,一页一路径),故首渲染
9387
+ * (此时本页必是激活页)捕获一次即可;缓存树后续多少次重渲染都不会漂移。
9388
+ * 需要跟随导航的位置信息请直接用 `useLocation`,不要用本 hook。
9389
+ */
9390
+ function useTabKey() {
9391
+ const { pathname } = useLocation();
9392
+ const keyRef = useRef(void 0);
9393
+ if (keyRef.current === void 0) keyRef.current = removeTrailingSlash(pathname);
9394
+ return keyRef.current;
9395
+ }
9396
+ //#endregion
9381
9397
  //#region src/module-loader/define-module.ts
9382
9398
  /**
9383
9399
  * 声明一个模块。
@@ -27447,4 +27463,4 @@ init_ri();
27447
27463
  init_menu_icons();
27448
27464
  init_tree();
27449
27465
  //#endregion
27450
- export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLogin, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useTabsStore, useUserStore, external_exports as z };
27466
+ export { AccessControl, AccessControlRoles, AntdApp, BasicButton, BasicContent, BasicTable, EmbeddedIcon, ErrorBoundary, ExternalIcon, FormAvatarItem, FormTreeItem, Iframe, LayoutCenterIcon, LayoutEffects, LayoutLeftIcon, LayoutRightIcon, MixedNavigationIcon, OutsidePageIcon, PageError, ProfileCardIcon, RiAccountCircleLine, RiContrastFill, RiFullscreenExitLine, RiFullscreenLine, RiMailCheckLine, RiMoonIcon, RiReactjsLine, RiSunIcon, RiUserSettingsLine, ServerErrorIcon, SideNavigationIcon, TopNavigationIcon, TwoColumnNavigationIcon, accessControlCodes, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLogin, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useTabKey, useTabsStore, useUserStore, external_exports as z };
@@ -5,9 +5,9 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <link rel="icon" href="/favicon.ico" />
7
7
  <title>React Antd Admin · Shell</title>
8
- <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-cEEui3MttOAqCTHlBg5Xog=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
8
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-tZIZWgEHDhCICXVOR+DUzQ=='; script-src-attr 'none'; connect-src 'self' data: https://modules.cdn.example.com; style-src 'self' https://modules.cdn.example.com 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-src https://ant.design https://react.dev https://condorheroblog.github.io; form-action 'none'; base-uri 'none'; object-src 'none'; upgrade-insecure-requests" />
9
9
 
10
- <script type="importmap" nonce="cEEui3MttOAqCTHlBg5Xog==">{
10
+ <script type="importmap" nonce="tZIZWgEHDhCICXVOR+DUzQ==">{
11
11
  "imports": {
12
12
  "react": "/assets/react.js",
13
13
  "react/jsx-runtime": "/assets/jsx-runtime.js",
@@ -7,11 +7,11 @@
7
7
  "react-router": "7.18.2",
8
8
  "react-router/dom": "7.18.2",
9
9
  "@tanstack/react-query": "5.102.8",
10
- "@oj-module/runtime": "0.1.17",
11
- "@oj-module/runtime/contract": "0.1.17",
12
- "@oj-module/runtime/contract/errors": "0.1.17",
13
- "@plane/kit": "0.1.17",
14
- "@plane/editor": "0.1.17",
10
+ "@oj-module/runtime": "0.1.21",
11
+ "@oj-module/runtime/contract": "0.1.21",
12
+ "@oj-module/runtime/contract/errors": "0.1.21",
13
+ "@plane/kit": "0.1.20",
14
+ "@plane/editor": "0.1.20",
15
15
  "antd": "6.6.1",
16
16
  "antd/locale/en_US": "6.6.1",
17
17
  "antd/locale/zh_CN": "6.6.1",
package/src/build.ts CHANGED
@@ -42,6 +42,7 @@ export const AntdApp = _fn;
42
42
  export const ErrorBoundary = _fn;
43
43
  export const PageError = _fn;
44
44
  // api
45
+ export const notify = _obj;
45
46
  export const fetchPie = _fn;
46
47
  export const fetchLine = _fn;
47
48
  export const fetchLogin = _fn;
@@ -73,6 +74,8 @@ export const permissionPrefix = "permission:button";
73
74
  export const useAuthStore = _fn;
74
75
  export const useUserStore = _fn;
75
76
  export const useTabsStore = _fn;
77
+ // 页签 key 唯一正确取口(keepAlive 下 useLocation 是激活页位置,0.1.21)
78
+ export const useTabKey = _fn;
76
79
  // 图标
77
80
  export const menuIcons = _obj;
78
81
  export const EmbeddedIcon = _fn;