@oj-module/cli 0.2.15 → 0.2.17
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.
- package/devkit/framework-dev.md +5 -0
- package/devkit/manual.md +99 -40
- package/devkit/menu-rules.md +375 -0
- package/package.json +2 -2
- package/shell-dist/assets/{index-CLdhl3Qd.js → index-B3g19Uet.js} +76 -3
- package/shell-dist/assets/runtime.js +224 -126
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/manifest-routes-derive.test.ts +102 -0
- package/src/__tests__/usage.test.ts +69 -0
- package/src/build.ts +20 -2
- package/src/bundle.ts +27 -1
- package/src/config.ts +16 -2
- package/src/index.ts +18 -1
- package/src/manifest-routes-derive.ts +130 -0
- package/src/preview-config.ts +12 -5
- package/src/preview.ts +31 -10
- package/src/site-base.ts +103 -0
- package/src/usage.ts +119 -0
- package/vendor/host-versions.json +3 -3
|
@@ -38,8 +38,71 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
38
38
|
}
|
|
39
39
|
})();
|
|
40
40
|
//#endregion
|
|
41
|
+
//#region shell/src/site-urls.ts
|
|
42
|
+
/**
|
|
43
|
+
* 宿主 shell 侧的站点 base 解析(子路径部署,2026-10-21)。
|
|
44
|
+
*
|
|
45
|
+
* shell 产物(`shell-dist/index.html` + `assets/*`)里的 URL 一律是**站点根绝对路径**
|
|
46
|
+
* (`/assets/react.js`)。`ojm build` 合并全站时按 `web.config.ts` 的 `siteBase`
|
|
47
|
+
* 重写 index.html 并注入 `window.__OJ_BASE__`,运行期这里读它把 modules.json
|
|
48
|
+
* 里的入口 / CSS / 预载 URL 补齐——否则站点挂在 `/oj` 时资源全部 404(首页白屏)。
|
|
49
|
+
*
|
|
50
|
+
* 为什么 shell 不直接 import runtime 的同名工具:本文件被 vitest 直接加载做纯函数
|
|
51
|
+
* 断言,而 runtime 主入口会拉进 antd/react 整棵树。规则与
|
|
52
|
+
* `packages/runtime/src/utils/site-base.ts` 逐条一致,由
|
|
53
|
+
* `tests/cli/site-base.test.ts` 用同一张用例表对撞防漂移。
|
|
54
|
+
*/
|
|
55
|
+
var ROOT_BASE = "/";
|
|
56
|
+
/** 归一化为「以 `/` 开头且以 `/` 结尾」;空值与 `"/"` 归为根路径 */
|
|
57
|
+
function normalizeSiteBase(raw) {
|
|
58
|
+
if (!raw) return ROOT_BASE;
|
|
59
|
+
const trimmed = raw.trim();
|
|
60
|
+
if (!trimmed || trimmed === ROOT_BASE) return ROOT_BASE;
|
|
61
|
+
const withLeading = trimmed.startsWith(ROOT_BASE) ? trimmed : `${ROOT_BASE}${trimmed}`;
|
|
62
|
+
return withLeading.endsWith(ROOT_BASE) ? withLeading : `${withLeading}${ROOT_BASE}`;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* 当前站点部署 base(恒以 `/` 结尾)。
|
|
66
|
+
*
|
|
67
|
+
* 优先级:`window.__OJ_BASE__`(`ojm build` 按 siteBase 注入)> `import.meta.env.BASE_URL`
|
|
68
|
+
* (shell 构建期固定为 `"/"`,仅在未注入时兜底,即根路径部署与 `ojm dev`)。
|
|
69
|
+
*/
|
|
70
|
+
function resolveSiteBase() {
|
|
71
|
+
const injected = globalThis.__OJ_BASE__;
|
|
72
|
+
if (typeof injected === "string") return normalizeSiteBase(injected);
|
|
73
|
+
return normalizeSiteBase("/");
|
|
74
|
+
}
|
|
75
|
+
/** 站内绝对路径(`/modules/x/entry.js`)→ 带部署 base;已带 base 的幂等,绝对域原样 */
|
|
76
|
+
function withSiteBase(url, base = resolveSiteBase()) {
|
|
77
|
+
if (!url.startsWith(ROOT_BASE) || url.startsWith("//")) return url;
|
|
78
|
+
const normalized = normalizeSiteBase(base);
|
|
79
|
+
if (normalized === ROOT_BASE || url.startsWith(normalized)) return url;
|
|
80
|
+
return `${normalized.slice(0, -1)}${url}`;
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
41
83
|
//#region shell/src/manifest.ts
|
|
42
84
|
/**
|
|
85
|
+
* 把 modules.json 里的站内绝对 URL 补上部署 base(子路径部署,2026-10-21)。
|
|
86
|
+
*
|
|
87
|
+
* modules.json 是**构建期**产物(`ojm build`),URL 与站点挂载前缀无关
|
|
88
|
+
* (恒为 `/modules/...`);真实前缀只有运行期才知道(`window.__OJ_BASE__`)。
|
|
89
|
+
* 因此在宿主拿到清单后立刻归一,后续所有消费者(信任根校验、CSS 注入、
|
|
90
|
+
* modulepreload、loader entry)看到的都是最终 URL——只此一处,漏改即 404 白屏。
|
|
91
|
+
*
|
|
92
|
+
* 绝对域(CDN 模块)原样保留,见 `web.config.ts` 的 `baseUrl`。
|
|
93
|
+
*/
|
|
94
|
+
function resolveModuleUrls(list, base) {
|
|
95
|
+
return list.map((m) => ({
|
|
96
|
+
...m,
|
|
97
|
+
entry: m.entry === void 0 ? m.entry : withSiteBase(m.entry, base),
|
|
98
|
+
css: m.css?.map((href) => withSiteBase(href, base)),
|
|
99
|
+
chunks: m.chunks?.map((chunk) => ({
|
|
100
|
+
...chunk,
|
|
101
|
+
url: withSiteBase(chunk.url, base)
|
|
102
|
+
}))
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
43
106
|
* P7.7:modules.json(cli BuiltModule[])→ loader Manifest 的字段透传。
|
|
44
107
|
*
|
|
45
108
|
* 评审 F1/F2:此前裁剪成 {name, entry},导致 enabled 过滤(US-9 下线)
|
|
@@ -206,10 +269,10 @@ function Boot() {
|
|
|
206
269
|
(async () => {
|
|
207
270
|
try {
|
|
208
271
|
ensureI18n();
|
|
209
|
-
const base =
|
|
272
|
+
const base = resolveSiteBase();
|
|
210
273
|
const [res, versionsRes] = await Promise.all([fetch(`${base}modules.json`), fetch(`${base}versions.json`).catch(() => null)]);
|
|
211
274
|
if (!res.ok) throw new Error(`modules.json 加载失败:HTTP ${res.status}`);
|
|
212
|
-
const list = await res.json();
|
|
275
|
+
const list = resolveModuleUrls(await res.json(), base);
|
|
213
276
|
const runtimeVersion = versionsRes?.ok ? extractRuntimeVersion(await versionsRes.json()) : void 0;
|
|
214
277
|
if (!runtimeVersion) console.warn("[shell] 未获取到宿主 runtime 版本(versions.json 缺失),peerRuntime 校验跳过");
|
|
215
278
|
assertTrustedModules(list);
|
|
@@ -257,7 +320,17 @@ function Boot() {
|
|
|
257
320
|
element: /* @__PURE__ */ jsx(CatchAll, {})
|
|
258
321
|
}
|
|
259
322
|
]
|
|
260
|
-
}]
|
|
323
|
+
}], {
|
|
324
|
+
/**
|
|
325
|
+
* 子路径部署(oj `mounts: [{ prefix: /oj, web: dist, spa: true }]`):
|
|
326
|
+
* 不设 basename 时 react-router 看到的是含前缀的 `/oj/wiki/...`,
|
|
327
|
+
* 匹配不到模块路由(`routes[].path` 恒为 `/wiki/...`,无模块根以外的
|
|
328
|
+
* 前缀),深链刷新直接落 catch-all 404。取值同 modules.json 的
|
|
329
|
+
* base(`resolveSiteBase()`),保证资源与路由用同一个前缀。
|
|
330
|
+
*
|
|
331
|
+
* 浏览器 URL 不受影响:react-router 只是在匹配/生成时剥/补前缀。
|
|
332
|
+
*/
|
|
333
|
+
basename: base }));
|
|
261
334
|
} catch (e) {
|
|
262
335
|
if (!cancelled) setError(e instanceof Error ? e.message : String(e));
|
|
263
336
|
}
|
|
@@ -152,21 +152,68 @@ var notify = {
|
|
|
152
152
|
}
|
|
153
153
|
};
|
|
154
154
|
//#endregion
|
|
155
|
+
//#region src/utils/site-base.ts
|
|
156
|
+
/**
|
|
157
|
+
* 归一化为「以 `/` 开头且以 `/` 结尾」的形式。
|
|
158
|
+
*
|
|
159
|
+
* 接受 `oj` / `/oj` / `/oj/` / `oj/` 四种写法(配置手写极易带或不带斜杠),
|
|
160
|
+
* 空值与 `"/"` 一律归为根路径。
|
|
161
|
+
*/
|
|
162
|
+
function normalizeSiteBase(raw) {
|
|
163
|
+
if (!raw) return ROOT_BASE;
|
|
164
|
+
const trimmed = raw.trim();
|
|
165
|
+
if (!trimmed || trimmed === ROOT_BASE) return ROOT_BASE;
|
|
166
|
+
const withLeading = trimmed.startsWith(ROOT_BASE) ? trimmed : `${ROOT_BASE}${trimmed}`;
|
|
167
|
+
return withLeading.endsWith(ROOT_BASE) ? withLeading : `${withLeading}${ROOT_BASE}`;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* 当前站点的部署 base(恒以 `/` 结尾)。
|
|
171
|
+
*
|
|
172
|
+
* 优先级:`window.__OJ_BASE__`(部署期注入)> `import.meta.env.BASE_URL`(构建期值)。
|
|
173
|
+
*
|
|
174
|
+
* 每次调用都重新读取——不做模块级缓存。原因:测试需要在不重启模块的情况下模拟
|
|
175
|
+
* 不同部署前缀(`vi.stubGlobal`),而字符串归一化的成本可忽略。
|
|
176
|
+
*/
|
|
177
|
+
function resolveSiteBase() {
|
|
178
|
+
const injected = globalThis.__OJ_BASE__;
|
|
179
|
+
if (typeof injected === "string") return normalizeSiteBase(injected);
|
|
180
|
+
return normalizeSiteBase("/");
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* 把站内绝对路径(`/assets/x.js`)拼到站点 base 上(`/oj/assets/x.js`)。
|
|
184
|
+
*
|
|
185
|
+
* 已带 base 的路径幂等;`http(s)://`、`//cdn`、`data:` 等一切非站内绝对路径原样返回
|
|
186
|
+
* (模块 entry 允许是 CDN 绝对域,见 `web.config.ts` 的 `baseUrl`)。
|
|
187
|
+
*/
|
|
188
|
+
function withSiteBase(url, base = resolveSiteBase()) {
|
|
189
|
+
if (!url.startsWith(ROOT_BASE) || url.startsWith("//")) return url;
|
|
190
|
+
const normalized = normalizeSiteBase(base);
|
|
191
|
+
if (normalized === ROOT_BASE || url.startsWith(normalized)) return url;
|
|
192
|
+
return `${normalized.slice(0, -1)}${url}`;
|
|
193
|
+
}
|
|
194
|
+
var ROOT_BASE;
|
|
195
|
+
var init_site_base = __esmMin((() => {
|
|
196
|
+
ROOT_BASE = "/";
|
|
197
|
+
}));
|
|
198
|
+
//#endregion
|
|
155
199
|
//#region src/router/extra-info/route-path.ts
|
|
156
200
|
/**
|
|
157
201
|
* 判断 pathname 是否为登录页(basename 感知,P0)。
|
|
158
202
|
*
|
|
159
203
|
* 供 ky hooks 等 React 之外的场景使用:原始 `location.pathname` 含 BASE_URL,
|
|
160
|
-
* 子路径部署(/
|
|
204
|
+
* 子路径部署(/oj/wiki/...)下直接与 loginPath 比较恒为 false,401 时会重复 goLogin。
|
|
161
205
|
* React 组件内请用 `useLocation()`(react-router 已剥离 basename),无需本函数。
|
|
162
206
|
*
|
|
163
|
-
* @param base 部署
|
|
207
|
+
* @param base 部署 base,恒以 `/` 结尾;缺省取 `resolveSiteBase()`(部署期注入优先,
|
|
208
|
+
* 见 utils/site-base.ts —— runtime 产物里的 `import.meta.env.BASE_URL` 是构建期
|
|
209
|
+
* 内联的 `/`,子路径部署下直接用它恒错)
|
|
164
210
|
*/
|
|
165
|
-
function isLoginPathname(pathname, base =
|
|
211
|
+
function isLoginPathname(pathname, base = resolveSiteBase()) {
|
|
166
212
|
return pathname === `${base}${loginPath.slice(1)}`;
|
|
167
213
|
}
|
|
168
214
|
var loginPath, exceptionPath, exception403Path, exception404Path, exception500Path;
|
|
169
215
|
var init_route_path = __esmMin((() => {
|
|
216
|
+
init_site_base();
|
|
170
217
|
loginPath = "/login";
|
|
171
218
|
exceptionPath = "/exception";
|
|
172
219
|
exception403Path = `${exceptionPath}/403`;
|
|
@@ -256,10 +303,10 @@ function getAppInfo() {
|
|
|
256
303
|
return {
|
|
257
304
|
"pkg": {
|
|
258
305
|
"name": "@oj-module/runtime",
|
|
259
|
-
"version": "0.2.
|
|
306
|
+
"version": "0.2.17",
|
|
260
307
|
"license": "MIT"
|
|
261
308
|
},
|
|
262
|
-
"lastBuildTime": "2026-10-21
|
|
309
|
+
"lastBuildTime": "2026-10-21 21:54:09"
|
|
263
310
|
};
|
|
264
311
|
}
|
|
265
312
|
var init_get_app_info = __esmMin((() => {}));
|
|
@@ -3300,7 +3347,7 @@ function openMustChangeModal() {
|
|
|
3300
3347
|
maskClosable: false,
|
|
3301
3348
|
cancelButtonProps: { style: { display: "none" } },
|
|
3302
3349
|
onOk: () => {
|
|
3303
|
-
window.location.href =
|
|
3350
|
+
window.location.href = `${resolveSiteBase()}change-password`;
|
|
3304
3351
|
},
|
|
3305
3352
|
afterClose: () => {
|
|
3306
3353
|
mustChangeModalOpen = false;
|
|
@@ -3310,6 +3357,7 @@ function openMustChangeModal() {
|
|
|
3310
3357
|
var mustChangeModalOpen;
|
|
3311
3358
|
var init_must_change = __esmMin((() => {
|
|
3312
3359
|
init_is();
|
|
3360
|
+
init_site_base();
|
|
3313
3361
|
init_static_antd();
|
|
3314
3362
|
mustChangeModalOpen = false;
|
|
3315
3363
|
}));
|
|
@@ -7478,6 +7526,24 @@ function getModules() {
|
|
|
7478
7526
|
function getModule(name) {
|
|
7479
7527
|
return modules.get(name);
|
|
7480
7528
|
}
|
|
7529
|
+
/**
|
|
7530
|
+
* 给模块路由打 module 标(R1,0.2.16):递归 children,非突变返回新对象。
|
|
7531
|
+
* module 来自 `definition.name`,作者无需手写——归组据此甄别方案路由、
|
|
7532
|
+
* 生成 `solgroup:<category>:<module>` 父键,不再依赖宿主 DB 行。
|
|
7533
|
+
*/
|
|
7534
|
+
function tagModuleRoutes(routes, name) {
|
|
7535
|
+
return routes.map((route) => {
|
|
7536
|
+
const tagged = {
|
|
7537
|
+
...route,
|
|
7538
|
+
handle: {
|
|
7539
|
+
...route.handle,
|
|
7540
|
+
module: name
|
|
7541
|
+
}
|
|
7542
|
+
};
|
|
7543
|
+
if (Array.isArray(route.children) && route.children.length > 0) tagged.children = tagModuleRoutes(route.children, name);
|
|
7544
|
+
return tagged;
|
|
7545
|
+
});
|
|
7546
|
+
}
|
|
7481
7547
|
function getRoutes() {
|
|
7482
7548
|
const { roles, permissions = [] } = useUserStore.getState();
|
|
7483
7549
|
const routes = [];
|
|
@@ -7487,7 +7553,7 @@ function getRoutes() {
|
|
|
7487
7553
|
if (requiredRoles?.length && !requiredRoles.some((role) => roles.includes(role))) continue;
|
|
7488
7554
|
const requiredPermissions = instance.definition.config?.requiredPermissions;
|
|
7489
7555
|
if (requiredPermissions?.length && !requiredPermissions.every((perm) => permissions.includes(perm))) continue;
|
|
7490
|
-
if (instance.definition.routes.length > 0) routes.push(...resolveRouteLayouts(instance.definition.routes));
|
|
7556
|
+
if (instance.definition.routes.length > 0) routes.push(...resolveRouteLayouts(tagModuleRoutes(instance.definition.routes, instance.definition.name)));
|
|
7491
7557
|
}
|
|
7492
7558
|
return addRouteIdByPath(routes);
|
|
7493
7559
|
}
|
|
@@ -8294,13 +8360,23 @@ var init_constants = __esmMin((() => {
|
|
|
8294
8360
|
//#endregion
|
|
8295
8361
|
//#region src/router/index.ts
|
|
8296
8362
|
function createRouter() {
|
|
8297
|
-
return createBrowserRouter(rootRoute, {
|
|
8363
|
+
return createBrowserRouter(rootRoute, {
|
|
8364
|
+
/**
|
|
8365
|
+
* @zh 子路径部署(oj `mounts: [{ prefix: /oj }]`)下 basename 必须是部署前缀,
|
|
8366
|
+
* 否则 react-router 不剥前缀,深链 `/oj/wiki/...` 匹配不到任何路由(落到 404 兜底)。
|
|
8367
|
+
* 取值走 `resolveSiteBase()`:优先宿主注入的 `window.__OJ_BASE__`,回退构建期
|
|
8368
|
+
* `import.meta.env.BASE_URL`——后者在预构建产物里被内联成 `/`,单独用不可靠。
|
|
8369
|
+
* @en Under sub-path deployment the basename must be the deploy prefix; read it
|
|
8370
|
+
* through `resolveSiteBase()` instead of the build-time `import.meta.env.BASE_URL`.
|
|
8371
|
+
*/
|
|
8372
|
+
basename: resolveSiteBase() });
|
|
8298
8373
|
}
|
|
8299
8374
|
var loadedPaths, rootRoute, router;
|
|
8300
8375
|
var init_router = __esmMin((() => {
|
|
8301
8376
|
init_layout_root();
|
|
8302
8377
|
init_preferences$3();
|
|
8303
8378
|
init_progress();
|
|
8379
|
+
init_site_base();
|
|
8304
8380
|
init_constants();
|
|
8305
8381
|
init_routes();
|
|
8306
8382
|
loadedPaths = /* @__PURE__ */ new Set();
|
|
@@ -8361,9 +8437,10 @@ function resolveMenuKey(path, parentPath) {
|
|
|
8361
8437
|
*/
|
|
8362
8438
|
function generateMenuItemsFromRoutes(routeList, parentPath = "/") {
|
|
8363
8439
|
return routeList.reduce((acc, item) => {
|
|
8364
|
-
const
|
|
8365
|
-
const
|
|
8366
|
-
const
|
|
8440
|
+
const handle = item.handle;
|
|
8441
|
+
const label = handle?.title;
|
|
8442
|
+
const externalLink = handle?.externalLink;
|
|
8443
|
+
const icon = handle?.icon;
|
|
8367
8444
|
const absolutePath = resolveMenuKey(item.path, parentPath);
|
|
8368
8445
|
const menuItem = {
|
|
8369
8446
|
key: absolutePath,
|
|
@@ -8377,11 +8454,14 @@ function generateMenuItemsFromRoutes(routeList, parentPath = "/") {
|
|
|
8377
8454
|
}, label) : label
|
|
8378
8455
|
};
|
|
8379
8456
|
if (icon) menuItem.icon = icon;
|
|
8457
|
+
if (handle?.module) menuItem.module = handle.module;
|
|
8458
|
+
if (handle?.category) menuItem.category = handle.category;
|
|
8459
|
+
if (handle?.order !== void 0) menuItem.order = handle.order;
|
|
8380
8460
|
if (Array.isArray(item.children) && item.children.length > 0) {
|
|
8381
|
-
const noIndexRoute = item.children.filter((route) => !route.index && !route?.handle?.hideInMenu);
|
|
8461
|
+
const noIndexRoute = item.children.filter((route) => !route.index && !route?.handle?.hideInMenu && route?.handle?.category !== "hidden");
|
|
8382
8462
|
if (noIndexRoute.length > 0) menuItem.children = generateMenuItemsFromRoutes(noIndexRoute, absolutePath);
|
|
8383
8463
|
}
|
|
8384
|
-
if (
|
|
8464
|
+
if (handle?.hideInMenu || handle?.category === "hidden") return acc;
|
|
8385
8465
|
return [...acc, menuItem];
|
|
8386
8466
|
}, []);
|
|
8387
8467
|
}
|
|
@@ -8393,158 +8473,132 @@ function defaultWarn(msg) {
|
|
|
8393
8473
|
warnedOnce.add(msg);
|
|
8394
8474
|
console.warn(msg);
|
|
8395
8475
|
}
|
|
8476
|
+
/** 取某 item 的有效归组元数据(handle 优先,行回退) */
|
|
8477
|
+
function effective(item, row) {
|
|
8478
|
+
return {
|
|
8479
|
+
module: item.module ?? row?.module,
|
|
8480
|
+
category: item.category ?? row?.category
|
|
8481
|
+
};
|
|
8482
|
+
}
|
|
8396
8483
|
/**
|
|
8397
|
-
*
|
|
8398
|
-
*
|
|
8399
|
-
*
|
|
8400
|
-
*
|
|
8401
|
-
*
|
|
8484
|
+
* 递归摘除 category==="admin" 的菜单项(handle 优先、行回退兜底),按 module 分桶收集;
|
|
8485
|
+
* 收集时压平 admin 项自身的层级——直接取 admin 项的 children,使"管理"中间级消失、
|
|
8486
|
+
* 子项直挂 `<module>:menu.main` 子组下,无 children 时退回 admin 项本身。
|
|
8487
|
+
* 其余项原样保留(children 递归同样处理),非突变;无变动的子树复用原引用。
|
|
8488
|
+
*
|
|
8489
|
+
* 返回新数组(有项被摘除/被重写父级时)或原数组引用(整段无 admin 时),便于调用方
|
|
8490
|
+
* 判断是否需重建 /system children(避免无 admin 项时给 /system 强加空 children)。
|
|
8402
8491
|
*/
|
|
8403
|
-
function
|
|
8492
|
+
function stripAdmin(list, byPath, buckets) {
|
|
8493
|
+
let changed = false;
|
|
8404
8494
|
const out = [];
|
|
8405
|
-
const
|
|
8406
|
-
|
|
8407
|
-
|
|
8408
|
-
|
|
8409
|
-
|
|
8410
|
-
|
|
8411
|
-
|
|
8412
|
-
|
|
8413
|
-
|
|
8414
|
-
|
|
8415
|
-
|
|
8416
|
-
|
|
8417
|
-
|
|
8418
|
-
|
|
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;
|
|
8495
|
+
for (const it of list) {
|
|
8496
|
+
const { module, category } = effective(it, byPath.get(it.key));
|
|
8497
|
+
if (category === "admin" && module) {
|
|
8498
|
+
const subItems = it.children && it.children.length > 0 ? it.children : [it];
|
|
8499
|
+
const bucket = buckets.get(module) ?? {
|
|
8500
|
+
items: [],
|
|
8501
|
+
module,
|
|
8502
|
+
minOrder: Infinity
|
|
8503
|
+
};
|
|
8504
|
+
for (const s of subItems) bucket.items.push(s);
|
|
8505
|
+
bucket.minOrder = Math.min(bucket.minOrder, it.order ?? 0);
|
|
8506
|
+
buckets.set(module, bucket);
|
|
8507
|
+
changed = true;
|
|
8508
|
+
continue;
|
|
8435
8509
|
}
|
|
8436
|
-
|
|
8437
|
-
|
|
8438
|
-
|
|
8439
|
-
|
|
8440
|
-
|
|
8441
|
-
|
|
8442
|
-
|
|
8443
|
-
|
|
8444
|
-
|
|
8445
|
-
|
|
8446
|
-
}
|
|
8510
|
+
if (it.children && it.children.length > 0) {
|
|
8511
|
+
const newChildren = stripAdmin(it.children, byPath, buckets);
|
|
8512
|
+
if (newChildren !== it.children) {
|
|
8513
|
+
changed = true;
|
|
8514
|
+
out.push({
|
|
8515
|
+
...it,
|
|
8516
|
+
children: newChildren
|
|
8517
|
+
});
|
|
8518
|
+
} else out.push(it);
|
|
8519
|
+
} else out.push(it);
|
|
8520
|
+
}
|
|
8521
|
+
return changed ? out : list;
|
|
8447
8522
|
}
|
|
8448
|
-
function
|
|
8449
|
-
const paired = items.map((item
|
|
8523
|
+
function makeAdminGroup(module, items) {
|
|
8524
|
+
const paired = items.map((item) => ({
|
|
8450
8525
|
item,
|
|
8451
|
-
order:
|
|
8526
|
+
order: item.order ?? 0
|
|
8452
8527
|
})).sort((a, b) => a.order - b.order);
|
|
8453
8528
|
return {
|
|
8454
|
-
key: `${
|
|
8455
|
-
label: `${module}:menu.
|
|
8529
|
+
key: `${ADMIN_PREFIX}${module}`,
|
|
8530
|
+
label: `${module}:menu.main`,
|
|
8456
8531
|
children: paired.map((p) => p.item)
|
|
8457
8532
|
};
|
|
8458
8533
|
}
|
|
8459
8534
|
/**
|
|
8460
8535
|
* 归组入口。
|
|
8461
8536
|
*
|
|
8462
|
-
*
|
|
8463
|
-
*
|
|
8537
|
+
* @param top 主区菜单项(order >= 0)
|
|
8538
|
+
* @param bottom 底部固定区菜单项(order < 0,含 /system)
|
|
8539
|
+
* @param rows 宿主方案路由行(可选):缺省或空时归组完全依赖 handle 元数据(dev 形态);
|
|
8540
|
+
* 提供时仅作 category 的向后兼容回退(0.2.15 已装站点),不参与新 bundle 的归组真源。
|
|
8541
|
+
* @param opts 选项(systemKey / onWarn)
|
|
8464
8542
|
*/
|
|
8465
8543
|
function groupSolutionMenus(top, bottom, rows, opts = {}) {
|
|
8466
8544
|
const systemKey = opts.systemKey ?? "/system";
|
|
8467
8545
|
const onWarn = opts.onWarn ?? defaultWarn;
|
|
8468
|
-
if (rows.length === 0) return {
|
|
8469
|
-
top: [...top],
|
|
8470
|
-
bottom: [...bottom]
|
|
8471
|
-
};
|
|
8472
8546
|
const byPath = /* @__PURE__ */ new Map();
|
|
8473
|
-
for (const r of rows) if (
|
|
8474
|
-
|
|
8475
|
-
|
|
8476
|
-
|
|
8477
|
-
|
|
8478
|
-
|
|
8479
|
-
|
|
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))
|
|
8547
|
+
for (const r of rows ?? []) if (r.category === "admin" && r.enabled !== false) byPath.set(r.path, r);
|
|
8548
|
+
const buckets = /* @__PURE__ */ new Map();
|
|
8549
|
+
const newTop = stripAdmin(top, byPath, buckets);
|
|
8550
|
+
const newBottom = stripAdmin(bottom, byPath, buckets);
|
|
8551
|
+
const adminGroups = [...buckets.entries()].map(([module, b]) => ({
|
|
8552
|
+
minOrder: b.minOrder,
|
|
8553
|
+
group: makeAdminGroup(module, b.items)
|
|
8496
8554
|
})).sort((a, b) => a.minOrder - b.minOrder);
|
|
8497
|
-
let
|
|
8555
|
+
let resultBottom = newBottom;
|
|
8498
8556
|
const sysIdx = newBottom.findIndex((it) => it.key === systemKey);
|
|
8499
|
-
if (
|
|
8500
|
-
|
|
8501
|
-
|
|
8502
|
-
|
|
8503
|
-
|
|
8504
|
-
|
|
8505
|
-
|
|
8506
|
-
|
|
8557
|
+
if (adminGroups.length > 0) {
|
|
8558
|
+
if (sysIdx !== -1) {
|
|
8559
|
+
const system = newBottom[sysIdx];
|
|
8560
|
+
resultBottom = newBottom.slice();
|
|
8561
|
+
resultBottom[sysIdx] = {
|
|
8562
|
+
...system,
|
|
8563
|
+
children: [...system.children ?? [], ...adminGroups.map((g) => g.group)]
|
|
8564
|
+
};
|
|
8565
|
+
} else onWarn(`[solution-menu-grouping] system menu "${systemKey}" not found; admin items dropped from menu.`);
|
|
8566
|
+
}
|
|
8507
8567
|
return {
|
|
8508
|
-
top:
|
|
8509
|
-
bottom:
|
|
8568
|
+
top: newTop,
|
|
8569
|
+
bottom: resultBottom
|
|
8510
8570
|
};
|
|
8511
8571
|
}
|
|
8512
|
-
var
|
|
8572
|
+
var ADMIN_PREFIX, warnedOnce;
|
|
8513
8573
|
var init_group_solution_menus = __esmMin((() => {
|
|
8514
|
-
MAIN_PREFIX = "solgroup:main:";
|
|
8515
8574
|
ADMIN_PREFIX = "solgroup:admin:";
|
|
8516
8575
|
warnedOnce = /* @__PURE__ */ new Set();
|
|
8517
8576
|
}));
|
|
8518
8577
|
//#endregion
|
|
8519
8578
|
//#region src/router/utils/merge-solution-categories.ts
|
|
8520
8579
|
/**
|
|
8521
|
-
* 方案路由归并(纯函数,R21
|
|
8580
|
+
* 方案路由归并(纯函数,R21 / 0.2.16 收窄):DB 行仅作为「管理覆写层」。
|
|
8522
8581
|
*
|
|
8523
|
-
*
|
|
8524
|
-
*
|
|
8525
|
-
* (禁用的路由摘除归装载层三处同写,D12);
|
|
8526
|
-
* - order 覆写为声明序(2026-10-20 双段序轴):框架常规项 ≥0 升序、管理面负数入
|
|
8527
|
-
* 底部固定区(pin 侧边栏底部,ascending 双段排序)、方案项从 200 起编,共用一条序轴;
|
|
8528
|
-
* 同 order 按根 path 字母序;
|
|
8529
|
-
* - 未匹配路由(框架/后端)原样保留(同一对象引用),入参不突变;
|
|
8530
|
-
* - solRoutes 为空(端点失败/未登录降级空表)时原样返回——降级不阻断,路由照常。
|
|
8582
|
+
* 0.2.16 起 `category`/`order`/`module` 已是路由 handle 真源(作者声明 + module-loader 注入,
|
|
8583
|
+
* 见 R1/R2),不再由本函数从行注写。本函数只保留一项管理语义:
|
|
8531
8584
|
*
|
|
8532
|
-
*
|
|
8533
|
-
*
|
|
8585
|
+
* - **禁用覆写**:行 `enabled === false`(或 deprecated)时追加 `hideInMenu: true`,
|
|
8586
|
+
* 使该菜单项不可见(路由仍可达,D12 口径——禁用在装载层三处同写);
|
|
8587
|
+
* - 其余字段(category/order/module/title)一律不覆写,保持 handle 原值;
|
|
8588
|
+
* - 入参不突变(未命中行或无需覆写时返回同一对象引用);
|
|
8589
|
+
* - solRoutes 为空(端点失败/未登录/dev 零行降级)时原样返回——降级不阻断,路由照常。
|
|
8534
8590
|
*/
|
|
8535
8591
|
function mergeSolutionCategories(routes, solRoutes) {
|
|
8536
8592
|
if (solRoutes.length === 0) return routes;
|
|
8537
8593
|
const byPath = new Map(solRoutes.map((r) => [r.path, r]));
|
|
8538
8594
|
return routes.map((route) => {
|
|
8539
8595
|
const row = route.path ? byPath.get(route.path) : void 0;
|
|
8540
|
-
if (!row) return route;
|
|
8596
|
+
if (!row || row.enabled !== false) return route;
|
|
8541
8597
|
return {
|
|
8542
8598
|
...route,
|
|
8543
8599
|
handle: {
|
|
8544
8600
|
...route.handle,
|
|
8545
|
-
|
|
8546
|
-
order: row.order,
|
|
8547
|
-
hideInMenu: row.category === "hidden" || !row.enabled || (route.handle.hideInMenu ?? false)
|
|
8601
|
+
hideInMenu: true
|
|
8548
8602
|
}
|
|
8549
8603
|
};
|
|
8550
8604
|
});
|
|
@@ -8601,6 +8655,42 @@ var init_resolve_login_route = __esmMin((() => {
|
|
|
8601
8655
|
isInternalLoginRoute = (r) => r?.handle?.login === true && r?.handle?.internal === true;
|
|
8602
8656
|
}));
|
|
8603
8657
|
//#endregion
|
|
8658
|
+
//#region src/router/utils/rewrite-admin-paths.ts
|
|
8659
|
+
/** 解析路由绝对路径(与 generate-menu-items-from-routes 同语义) */
|
|
8660
|
+
function resolveAbs(path, parentAbs) {
|
|
8661
|
+
if (!path) return parentAbs;
|
|
8662
|
+
return path.startsWith("/") ? path : `${parentAbs.replace(/\/+$/, "")}/${path}`;
|
|
8663
|
+
}
|
|
8664
|
+
function rewriteAdminPaths(routes, systemKey = SYSTEM_KEY) {
|
|
8665
|
+
const hoisted = [];
|
|
8666
|
+
return [...routes.map((r) => processRoute(r, "/", systemKey, hoisted, void 0)).filter((r) => r !== null), ...hoisted];
|
|
8667
|
+
}
|
|
8668
|
+
function processRoute(route, parentAbs, systemKey, hoisted, moduleRootAbs) {
|
|
8669
|
+
const abs = resolveAbs(route.path, parentAbs);
|
|
8670
|
+
const rootAbs = moduleRootAbs ?? abs;
|
|
8671
|
+
if (route.handle?.category === "admin") {
|
|
8672
|
+
const newAbs = `${systemKey}${rootAbs}${abs.slice(rootAbs.length) || ""}`;
|
|
8673
|
+
hoisted.push({
|
|
8674
|
+
...route,
|
|
8675
|
+
path: newAbs
|
|
8676
|
+
});
|
|
8677
|
+
return null;
|
|
8678
|
+
}
|
|
8679
|
+
if (!route.children || route.children.length === 0) return { ...route };
|
|
8680
|
+
const newChildren = [];
|
|
8681
|
+
for (const c of route.children) {
|
|
8682
|
+
const processed = processRoute(c, abs, systemKey, hoisted, rootAbs);
|
|
8683
|
+
if (processed !== null) newChildren.push(processed);
|
|
8684
|
+
}
|
|
8685
|
+
const result = { ...route };
|
|
8686
|
+
result.children = newChildren;
|
|
8687
|
+
return result;
|
|
8688
|
+
}
|
|
8689
|
+
var SYSTEM_KEY;
|
|
8690
|
+
var init_rewrite_admin_paths = __esmMin((() => {
|
|
8691
|
+
SYSTEM_KEY = "/system";
|
|
8692
|
+
}));
|
|
8693
|
+
//#endregion
|
|
8604
8694
|
//#region src/store/access.ts
|
|
8605
8695
|
/**
|
|
8606
8696
|
* 切分主区/底部固定区路由(ascending 已双段排序:负 order 段整体在后)。
|
|
@@ -8628,6 +8718,7 @@ var init_access = __esmMin((() => {
|
|
|
8628
8718
|
init_group_solution_menus();
|
|
8629
8719
|
init_merge_solution_categories();
|
|
8630
8720
|
init_resolve_login_route();
|
|
8721
|
+
init_rewrite_admin_paths();
|
|
8631
8722
|
baseMenuRoutes = splitMenuRoutes(baseRoutes);
|
|
8632
8723
|
initialState$1 = {
|
|
8633
8724
|
wholeMenus: generateMenuItemsFromRoutes(baseMenuRoutes.top),
|
|
@@ -8641,8 +8732,9 @@ var init_access = __esmMin((() => {
|
|
|
8641
8732
|
useAccessStore = create((set, get) => ({
|
|
8642
8733
|
...initialState$1,
|
|
8643
8734
|
setAccessStore: (routes) => {
|
|
8644
|
-
const
|
|
8645
|
-
router
|
|
8735
|
+
const rewrittenRoutes = rewriteAdminPaths(routes);
|
|
8736
|
+
const newRoutes = ascending([...resolveLoginRoute(baseRoutes, rewrittenRoutes, router, rootRoute), ...mergeSolutionCategories(rewrittenRoutes, get().solutionRoutes)]);
|
|
8737
|
+
router.patchRoutes(ROOT_ROUTE_ID, rewrittenRoutes);
|
|
8646
8738
|
const flatRouteList = flattenRoutes(newRoutes);
|
|
8647
8739
|
const { top, bottom } = splitMenuRoutes(newRoutes);
|
|
8648
8740
|
const grouped = groupSolutionMenus(generateMenuItemsFromRoutes(top), generateMenuItemsFromRoutes(bottom), get().solutionRoutes);
|
|
@@ -8823,15 +8915,20 @@ var init_remember_route = __esmMin((() => {
|
|
|
8823
8915
|
/**
|
|
8824
8916
|
* 跳转到登录页面
|
|
8825
8917
|
*
|
|
8918
|
+
* 必须带上部署 base(`resolveSiteBase()`):子路径部署下 `location.href = "/login"`
|
|
8919
|
+
* 会跳到宿主机根,而不是 `/oj/login`。不能用 `import.meta.env.BASE_URL`——预构建
|
|
8920
|
+
* 产物里它是构建期内联的 `/`。
|
|
8921
|
+
*
|
|
8826
8922
|
* @returns 无返回值
|
|
8827
8923
|
*/
|
|
8828
8924
|
function goLogin() {
|
|
8829
8925
|
useAuthStore.getState().reset();
|
|
8830
|
-
window.location.href =
|
|
8926
|
+
window.location.href = `${resolveSiteBase()}login${rememberRoute()}`;
|
|
8831
8927
|
}
|
|
8832
8928
|
var init_go_login = __esmMin((() => {
|
|
8833
8929
|
init_auth();
|
|
8834
8930
|
init_remember_route();
|
|
8931
|
+
init_site_base();
|
|
8835
8932
|
}));
|
|
8836
8933
|
//#endregion
|
|
8837
8934
|
//#region src/utils/request/header-safe.ts
|
|
@@ -27946,10 +28043,11 @@ init_user();
|
|
|
27946
28043
|
init_get_app_info();
|
|
27947
28044
|
init_get_redirect_path();
|
|
27948
28045
|
init_request();
|
|
28046
|
+
init_site_base();
|
|
27949
28047
|
init_user$1();
|
|
27950
28048
|
init_local_icons();
|
|
27951
28049
|
init_ri();
|
|
27952
28050
|
init_menu_icons();
|
|
27953
28051
|
init_tree();
|
|
27954
28052
|
//#endregion
|
|
27955
|
-
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, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacOplogPost, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchSolutionRoutes, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, groupSettingsByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, request, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useSolutionRoutes, useTabKey, useTabsStore, useUserStore, external_exports as z };
|
|
28053
|
+
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, bindingDiff, canAssignRole, canDeleteRole, canEditRoleBindings, defineModule, fetchAddMenuItem, fetchAddRoleItem, fetchAsyncRoutes, fetchChangePassword, fetchDeleteMenuItem, fetchDeleteRoleItem, fetchLdapLogin, fetchLogin, fetchLoginMethods, fetchLogout, fetchMenuByRoleId, fetchMenuList, fetchRbacAdminResetPassword, fetchRbacContactDetail, fetchRbacContacts, fetchRbacCreateRole, fetchRbacCreateUser, fetchRbacDeleteRole, fetchRbacOplog, fetchRbacOplogPost, fetchRbacPermissionList, fetchRbacRoleList, fetchRbacSetForbidden, fetchRbacSettings, fetchRbacUpdateBindings, fetchRbacUpdateSetting, fetchRbacUpdateUserRoles, fetchRbacUserList, fetchRefreshToken, fetchRoleList, fetchRoleMenu, fetchSolutionRoutes, fetchUpdateMenuItem, fetchUpdateRoleItem, fetchUserInfo, filterTree, getAllExpandedKeys, getAppInfo, getBooleanOptions, getModule, getModules, getRedirectPath, getRegisteredApiPrefix, getRegisteredStore, getRoutes, getYesNoOptions, groupByModule, groupSettingsByModule, handleTree, loadAll, mapTree, menuIcons, notify, permissionPrefix, request, resolveSiteBase, roleLabel, setupI18n, traverseTreeValues, unloadModule, useAccess, useAuthStore, usePreferences, useSlotNodes, useSolutionRoutes, useTabKey, useTabsStore, useUserStore, withSiteBase, external_exports as z };
|
package/shell-dist/index.html
CHANGED
|
@@ -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
|
|
8
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'self' https://modules.cdn.example.com 'wasm-unsafe-eval' 'nonce-NKdPzzWoLWlLgH/F6SaXrg=='; 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="
|
|
10
|
+
<script type="importmap" nonce="NKdPzzWoLWlLgH/F6SaXrg==">{
|
|
11
11
|
"imports": {
|
|
12
12
|
"react": "/assets/react.js",
|
|
13
13
|
"react/jsx-runtime": "/assets/jsx-runtime.js",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"dayjs/locale/zh-cn": "/assets/ojm-dayjs-locale-zh-cn.js"
|
|
134
134
|
}
|
|
135
135
|
}</script>
|
|
136
|
-
<script type="module" crossorigin src="/assets/index-
|
|
136
|
+
<script type="module" crossorigin src="/assets/index-B3g19Uet.js"></script>
|
|
137
137
|
</head>
|
|
138
138
|
<body>
|
|
139
139
|
<div id="root"></div>
|