@oj-module/cli 0.2.53 → 0.2.54
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/SKILL.md +2 -2
- package/devkit/manual.md +32 -25
- package/package.json +2 -2
- package/shell-dist/assets/runtime.js +7 -4
- package/shell-dist/index.html +3 -3
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/templates-tenant-scope.test.ts +3 -2
- package/src/config.ts +9 -0
- package/src/contract/__tests__/client-path.test.ts +30 -0
- package/src/contract/__tests__/emit-client.test.ts +18 -0
- package/src/contract/__tests__/handler-scan.test.ts +23 -0
- package/src/contract/check.ts +5 -5
- package/src/contract/client-path.ts +30 -0
- package/src/contract/emit-client.ts +18 -4
- package/src/contract/handler-scan.ts +40 -3
- package/src/contract/run.ts +5 -3
- package/src/init.ts +19 -1
- package/templates/README.md +40 -44
- package/templates/init.include.yaml +12 -8
- package/templates/openapi.json +1393 -34
- package/templates/src/_platform/api/contacts/api.ts +29 -1
- package/templates/src/_platform/api/contacts/detail/api.ts +25 -1
- package/templates/src/_platform/api/solutions/api.ts +60 -1
- package/templates/src/_platform/web/__client/api.schemas.ts +99 -35
- package/templates/src/_platform/web/__client/api.ts +103 -59
- package/templates/src/app/web/entry.ts +77 -0
- package/templates/src/{home/web → app/web/home}/__client/api.ts +5 -2
- package/templates/src/app/web/home/routes.ts +41 -0
- package/templates/src/app/web/login/routes.ts +71 -0
- package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
- package/templates/src/{notification/web → app/web/notification}/__client/api.ts +67 -20
- package/templates/src/app/web/notification/routes.ts +70 -0
- package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.ts +5 -2
- package/templates/src/app/web/personal-center/routes.ts +49 -0
- package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +28 -22
- package/templates/src/{system/web → app/web/system}/__client/api.ts +103 -58
- package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +3 -3
- package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +12 -3
- package/templates/src/{system/web/entry.ts → app/web/system/routes.ts} +24 -35
- package/templates/src/auth/api/api.ts +59 -0
- package/templates/src/auth/api/ldap-login/api.ts +37 -1
- package/templates/src/auth/api/wecom/api.ts +23 -0
- package/templates/src/auth/web/__client/api.schemas.ts +133 -13
- package/templates/src/auth/web/__client/api.ts +129 -31
- package/templates/src/notification/api/api.ts +17 -0
- package/templates/src/notification/api/del/api.ts +2 -0
- package/templates/src/system/api/roles/api.ts +3 -0
- package/templates/src/system/api/roles/bindings/api.ts +3 -0
- package/templates/src/system/api/settings/api.ts +3 -0
- package/templates/src/system/api/users/forbidden/api.ts +3 -0
- package/templates/src/system/api/users/password/api.ts +3 -0
- package/templates/src/system/api/users/roles/api.ts +3 -0
- package/templates/src/web/api/get-async-routes/api.ts +16 -1
- package/templates/src/web/api/hello/api.ts +13 -1
- package/templates/src/web/api/user-info/api.ts +21 -1
- package/templates/src/web/web/__client/api.schemas.ts +28 -0
- package/templates/src/web/web/__client/api.ts +33 -9
- package/templates/web.config.ts +21 -15
- package/vendor/host-versions.json +3 -3
- package/templates/src/home/web/entry.ts +0 -49
- package/templates/src/login/web/entry.ts +0 -79
- package/templates/src/notification/web/entry.ts +0 -84
- package/templates/src/personal-center/web/entry.ts +0 -60
- /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
- /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
- /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
- /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
- /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
- /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
- /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/pages/login.tsx +0 -0
- /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
- /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
- /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
- /package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
- /package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/locales/en-US.json +0 -0
- /package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +0 -0
- /package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +0 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ModuleContext, ModuleI18n } from "@oj-module/runtime";
|
|
2
|
+
import { defineModule } from "@oj-module/runtime";
|
|
3
|
+
|
|
4
|
+
import { routes as homeRoutes } from "./home/routes";
|
|
5
|
+
import { onInit as loginOnInit, routes as loginRoutes } from "./login/routes";
|
|
6
|
+
import { onInit as notificationOnInit, routes as notificationRoutes } from "./notification/routes";
|
|
7
|
+
import { onInit as personalCenterOnInit, routes as personalCenterRoutes } from "./personal-center/routes";
|
|
8
|
+
import { onInit as systemOnInit, routes as systemRoutes } from "./system/routes";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 应用模块(多根聚合):一个 entry 承载六个业务根,各根独立成一级菜单。
|
|
12
|
+
*
|
|
13
|
+
* 为什么聚合:这六个根(首页/登录/个人中心/通知/系统管理)是脚手架的框架内置页面,
|
|
14
|
+
* 同版本发布、同生命周期、缺一不可,拆成六个模块只会把工程目录、web.config 登记、
|
|
15
|
+
* modules.json 与首屏模块入口请求都放大六倍。聚合后仍是「一目录一模块」,但模块内
|
|
16
|
+
* 按根分目录(`<root>/routes.ts` + `pages/` + `__client/`),页面相对导入不变。
|
|
17
|
+
*
|
|
18
|
+
* 结构约定(多根模块):
|
|
19
|
+
* - 顶层路由首段去重 > 1 → runtime 判为多根(`isMultiRoot`),各根按 entry 里的
|
|
20
|
+
* **绝对路径**原样注册,不被 `routePrefix` 改写——URL 与菜单 key 都不变;
|
|
21
|
+
* - 菜单顺序仍由各根 `handle.order` 决定(≥0 主区升序、<0 底部固定区),与聚合前
|
|
22
|
+
* 逐项一致;改顺序只改 order,不要依赖 routes 数组下标(缺省 order 会按数组下标兜底);
|
|
23
|
+
* - 门禁挂根路由:`/system` 的 `handle.roles` 等价于原先的模块级 requiredRoles。
|
|
24
|
+
*
|
|
25
|
+
* i18n 用 `i18nNamespaces` 沿用各根**原命名空间**(home / login / personal-center /
|
|
26
|
+
* notification / system),因此 `t("system:menu.users")`、`useTranslation("system")`
|
|
27
|
+
* 与菜单标题 `<ns>:menu.<key>` 全部零改动。
|
|
28
|
+
*/
|
|
29
|
+
const i18nNamespaces: Record<string, ModuleI18n> = {
|
|
30
|
+
"home": {
|
|
31
|
+
"zh-CN": () => import("./home/locales/zh-CN.json"),
|
|
32
|
+
"en-US": () => import("./home/locales/en-US.json"),
|
|
33
|
+
},
|
|
34
|
+
"login": {
|
|
35
|
+
"zh-CN": () => import("./login/locales/zh-CN.json"),
|
|
36
|
+
"en-US": () => import("./login/locales/en-US.json"),
|
|
37
|
+
},
|
|
38
|
+
"personal-center": {
|
|
39
|
+
"zh-CN": () => import("./personal-center/locales/zh-CN.json"),
|
|
40
|
+
"en-US": () => import("./personal-center/locales/en-US.json"),
|
|
41
|
+
},
|
|
42
|
+
"notification": {
|
|
43
|
+
"zh-CN": () => import("./notification/locales/zh-CN.json"),
|
|
44
|
+
"en-US": () => import("./notification/locales/en-US.json"),
|
|
45
|
+
},
|
|
46
|
+
"system": {
|
|
47
|
+
"zh-CN": () => import("./system/locales/zh-CN.json"),
|
|
48
|
+
"en-US": () => import("./system/locales/en-US.json"),
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export default defineModule({
|
|
53
|
+
name: "app",
|
|
54
|
+
description: "应用模块:框架内置页面(首页/登录/个人中心/通知/系统管理),多根聚合",
|
|
55
|
+
version: "0.1.0",
|
|
56
|
+
peerRuntime: ">=0.0.0",
|
|
57
|
+
routes: [
|
|
58
|
+
...homeRoutes,
|
|
59
|
+
...loginRoutes,
|
|
60
|
+
...personalCenterRoutes,
|
|
61
|
+
...notificationRoutes,
|
|
62
|
+
...systemRoutes,
|
|
63
|
+
],
|
|
64
|
+
i18nNamespaces,
|
|
65
|
+
lifecycle: {
|
|
66
|
+
/**
|
|
67
|
+
* 各根的 onInit 按原模块加载顺序串行——login 必须最先(认证 provider 要在任何
|
|
68
|
+
* 登录动作前就位),其余是前缀登记/API provider 注入,顺序无相互依赖。
|
|
69
|
+
*/
|
|
70
|
+
async onInit(ctx: ModuleContext) {
|
|
71
|
+
await loginOnInit(ctx);
|
|
72
|
+
await personalCenterOnInit(ctx);
|
|
73
|
+
await notificationOnInit(ctx);
|
|
74
|
+
await systemOnInit(ctx);
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
});
|
|
@@ -17,6 +17,9 @@ export const API_PREFIX = "/home";
|
|
|
17
17
|
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
18
|
export const MODULE_PREFIX = "/home";
|
|
19
19
|
|
|
20
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
21
|
+
export const MODULE_OWNER = "/app";
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
25
|
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
@@ -28,8 +31,8 @@ export function createHomeClient(ctx: ModuleContext) {
|
|
|
28
31
|
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
32
|
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
33
|
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !==
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${
|
|
34
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
|
|
35
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
36
|
}
|
|
34
37
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
38
|
bindRequest(ctx.utils.request);
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AppRouteRecordRaw } from "@oj-module/runtime";
|
|
2
|
+
|
|
3
|
+
import { HomeOutlined } from "@ant-design/icons";
|
|
4
|
+
|
|
5
|
+
import { createElement, lazy } from "react";
|
|
6
|
+
|
|
7
|
+
const Home = lazy(() => import("./pages/index"));
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 首页根 `/home`。
|
|
11
|
+
*
|
|
12
|
+
* 为什么必须有:shell 预构建产物把 `VITE_BASE_HOME_PATH` 定为 `/home`
|
|
13
|
+
* (登录成功回跳、logo 点击、tabbar 首页、403/404 回首页都指向它)。
|
|
14
|
+
* 工程缺 `/home` 路由时这些跳转会落到错误边界。
|
|
15
|
+
*
|
|
16
|
+
* 结构对齐 playground-oj/web/src/home;页面用统计卡片 + 折线/柱/饼图
|
|
17
|
+
* 演示宿主共享依赖矩阵(echarts / echarts-for-react / react-countup / dayjs /
|
|
18
|
+
* antd / @ant-design/icons / react-i18next,均由宿主 importmap 提供单例)。
|
|
19
|
+
* 图表数据为静态演示值——真实业务走模块契约 + `ojm api` 生成的 client。
|
|
20
|
+
*/
|
|
21
|
+
export const routes: AppRouteRecordRaw[] = [
|
|
22
|
+
{
|
|
23
|
+
path: "/home",
|
|
24
|
+
handle: {
|
|
25
|
+
layout: "container",
|
|
26
|
+
order: 1,
|
|
27
|
+
title: "home:menu.home",
|
|
28
|
+
icon: createElement(HomeOutlined),
|
|
29
|
+
},
|
|
30
|
+
children: [
|
|
31
|
+
{
|
|
32
|
+
index: true,
|
|
33
|
+
Component: Home,
|
|
34
|
+
handle: {
|
|
35
|
+
title: "home:menu.home",
|
|
36
|
+
icon: createElement(HomeOutlined),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
];
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { AppRouteRecordRaw, ModuleContext } from "@oj-module/runtime";
|
|
2
|
+
|
|
3
|
+
import { useAuthStore, useTenantStore } from "@oj-module/runtime";
|
|
4
|
+
import { lazy } from "react";
|
|
5
|
+
|
|
6
|
+
import { authProvider } from "./auth-provider";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 登录根 `/login`(+ 强制改密 `/change-password`)。
|
|
10
|
+
*
|
|
11
|
+
* shell 宿主只消费模块路由(`getRoutes()`),**不挂 runtime 的内置 baseRoutes**,
|
|
12
|
+
* 因此缺少该根时 `/login` 无路由可跳(登出/回跳登录会落空)。本根补上路由:
|
|
13
|
+
* 登录链路仍回落到 runtime 默认链(`fetchLogin` → 后端内置 `auth/login` +
|
|
14
|
+
* 守卫 `getUserInfo` → `web/user-info`),本根只负责「内容区」。
|
|
15
|
+
*
|
|
16
|
+
* `handle.login: true` 让框架识别为登录页(`resolveLoginRoute` 校验路径必须是
|
|
17
|
+
* `/login`);`handle.layout: "fullscreen"` 由框架注入全屏外壳。
|
|
18
|
+
*/
|
|
19
|
+
export const routes: AppRouteRecordRaw[] = [
|
|
20
|
+
{
|
|
21
|
+
path: "/login",
|
|
22
|
+
handle: {
|
|
23
|
+
layout: "fullscreen",
|
|
24
|
+
login: true,
|
|
25
|
+
hideInMenu: true,
|
|
26
|
+
title: "login:page.title",
|
|
27
|
+
},
|
|
28
|
+
children: [
|
|
29
|
+
{
|
|
30
|
+
index: true,
|
|
31
|
+
Component: lazy(() => import("./pages/login")),
|
|
32
|
+
handle: { title: "login:page.title" },
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
// 首登/重置后强制改密(must_change 悬挂期唯一可用写端);
|
|
38
|
+
// 管理面 403 msg=password change required 时也引导至此
|
|
39
|
+
path: "/change-password",
|
|
40
|
+
handle: {
|
|
41
|
+
layout: "fullscreen",
|
|
42
|
+
hideInMenu: true,
|
|
43
|
+
title: "login:page.changePwd.title",
|
|
44
|
+
},
|
|
45
|
+
children: [
|
|
46
|
+
{
|
|
47
|
+
index: true,
|
|
48
|
+
Component: lazy(() => import("./pages/change-password")),
|
|
49
|
+
handle: { title: "login:page.changePwd.title", hideInMenu: true },
|
|
50
|
+
},
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* 认证 provider:仅接管「企微票据换会话」,其余方式原样委托内置链(见 auth-provider.ts)。
|
|
57
|
+
* 注册时机必须在任何登录动作之前——onInit 先于路由渲染。
|
|
58
|
+
*/
|
|
59
|
+
export async function onInit(ctx: ModuleContext) {
|
|
60
|
+
ctx.register.authProvider(authProvider);
|
|
61
|
+
// 登出必须清租户:否则下一个用户会带着上一个用户的租户头(跨租户请求 fail-closed 403,
|
|
62
|
+
// 但单租户部署会静默落到别人的 default 视图)。token 由非空转空即视为登出。
|
|
63
|
+
//
|
|
64
|
+
// 租户上下文的请求头由**运行时**注入(`runtime/store/tenant.ts`),本根不注册
|
|
65
|
+
// headerProvider——宿主 shell 的 hydrateUser / solution-routes 都在任何模块 onInit
|
|
66
|
+
// 之前就发请求,模块注册必然晚一步(2026-10-30 冷启动 e2e 实测两个 400)。
|
|
67
|
+
useAuthStore.subscribe((state, prev) => {
|
|
68
|
+
if (prev.token && !state.token)
|
|
69
|
+
useTenantStore.getState().clear();
|
|
70
|
+
});
|
|
71
|
+
}
|
|
@@ -21,6 +21,13 @@ export const schemas = {
|
|
|
21
21
|
body: z.object({
|
|
22
22
|
"id": z.string()
|
|
23
23
|
}),
|
|
24
|
+
data: z.null(),
|
|
25
|
+
},
|
|
26
|
+
"notificationNotificationsReadAllPost": {
|
|
27
|
+
data: z.null(),
|
|
28
|
+
},
|
|
29
|
+
"notificationNotificationsClearPost": {
|
|
30
|
+
data: z.null(),
|
|
24
31
|
},
|
|
25
32
|
"notificationPushPost": {
|
|
26
33
|
body: z.object({
|
|
@@ -30,6 +37,9 @@ export const schemas = {
|
|
|
30
37
|
"link": z.string().optional(),
|
|
31
38
|
"avatar": z.string().optional(),
|
|
32
39
|
"category": z.enum(["notice", "pm", "mention"]).optional()
|
|
40
|
+
}),
|
|
41
|
+
data: z.object({
|
|
42
|
+
"id": z.string()
|
|
33
43
|
}),
|
|
34
44
|
},
|
|
35
45
|
"notificationPushBroadcastPost": {
|
|
@@ -39,6 +49,9 @@ export const schemas = {
|
|
|
39
49
|
"link": z.string().optional(),
|
|
40
50
|
"avatar": z.string().optional(),
|
|
41
51
|
"category": z.enum(["notice", "pm", "mention"]).optional()
|
|
52
|
+
}),
|
|
53
|
+
data: z.object({
|
|
54
|
+
"id": z.string()
|
|
42
55
|
}),
|
|
43
56
|
},
|
|
44
57
|
"notificationDelPost": {
|
|
@@ -46,5 +59,6 @@ export const schemas = {
|
|
|
46
59
|
"id": z.string(),
|
|
47
60
|
"userId": z.union([z.string(), z.number()]).optional()
|
|
48
61
|
}),
|
|
62
|
+
data: z.null(),
|
|
49
63
|
},
|
|
50
64
|
};
|
|
@@ -17,6 +17,9 @@ export const API_PREFIX = "/notification";
|
|
|
17
17
|
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
18
|
export const MODULE_PREFIX = "/notification";
|
|
19
19
|
|
|
20
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
21
|
+
export const MODULE_OWNER = "/app";
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
25
|
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
@@ -28,8 +31,8 @@ export function createNotificationClient(ctx: ModuleContext) {
|
|
|
28
31
|
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
32
|
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
33
|
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !==
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${
|
|
34
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
|
|
35
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
36
|
}
|
|
34
37
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
38
|
bindRequest(ctx.utils.request);
|
|
@@ -88,15 +91,22 @@ export async function notificationNotificationsGet(): Promise<NotificationNotifi
|
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
export type NotificationNotificationsReadPostBody = z.input<(typeof schemas)["notificationNotificationsReadPost"]["body"]>;
|
|
94
|
+
export type NotificationNotificationsReadPostData = z.infer<(typeof schemas)["notificationNotificationsReadPost"]["data"]>;
|
|
91
95
|
|
|
92
|
-
export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<
|
|
96
|
+
export async function notificationNotificationsReadPost(body: NotificationNotificationsReadPostBody): Promise<NotificationNotificationsReadPostData> {
|
|
93
97
|
const client = ensureReq();
|
|
94
98
|
try {
|
|
95
|
-
const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<
|
|
99
|
+
const env = await client.post(`notification/notifications/read`, { json: body }).json<OjEnvelope<NotificationNotificationsReadPostData>>();
|
|
96
100
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
97
101
|
if (typeof env.code === "number" && env.code !== 0)
|
|
98
102
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
99
|
-
const data = env.data as
|
|
103
|
+
const data = env.data as NotificationNotificationsReadPostData;
|
|
104
|
+
if (import.meta.env.DEV) {
|
|
105
|
+
const { schemas } = await import("./api.schemas");
|
|
106
|
+
const r = schemas["notificationNotificationsReadPost"].data.safeParse(data);
|
|
107
|
+
if (!r.success)
|
|
108
|
+
throw new ContractApiError(-1, `[契约违例] notificationNotificationsReadPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
109
|
+
}
|
|
100
110
|
return data;
|
|
101
111
|
}
|
|
102
112
|
catch (e) {
|
|
@@ -104,14 +114,22 @@ export async function notificationNotificationsReadPost(body: NotificationNotifi
|
|
|
104
114
|
}
|
|
105
115
|
}
|
|
106
116
|
|
|
107
|
-
export
|
|
117
|
+
export type NotificationNotificationsReadAllPostData = z.infer<(typeof schemas)["notificationNotificationsReadAllPost"]["data"]>;
|
|
118
|
+
|
|
119
|
+
export async function notificationNotificationsReadAllPost(): Promise<NotificationNotificationsReadAllPostData> {
|
|
108
120
|
const client = ensureReq();
|
|
109
121
|
try {
|
|
110
|
-
const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<
|
|
122
|
+
const env = await client.post(`notification/notifications/read-all`).json<OjEnvelope<NotificationNotificationsReadAllPostData>>();
|
|
111
123
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
112
124
|
if (typeof env.code === "number" && env.code !== 0)
|
|
113
125
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
114
|
-
const data = env.data as
|
|
126
|
+
const data = env.data as NotificationNotificationsReadAllPostData;
|
|
127
|
+
if (import.meta.env.DEV) {
|
|
128
|
+
const { schemas } = await import("./api.schemas");
|
|
129
|
+
const r = schemas["notificationNotificationsReadAllPost"].data.safeParse(data);
|
|
130
|
+
if (!r.success)
|
|
131
|
+
throw new ContractApiError(-1, `[契约违例] notificationNotificationsReadAllPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
132
|
+
}
|
|
115
133
|
return data;
|
|
116
134
|
}
|
|
117
135
|
catch (e) {
|
|
@@ -119,14 +137,22 @@ export async function notificationNotificationsReadAllPost(): Promise<unknown> {
|
|
|
119
137
|
}
|
|
120
138
|
}
|
|
121
139
|
|
|
122
|
-
export
|
|
140
|
+
export type NotificationNotificationsClearPostData = z.infer<(typeof schemas)["notificationNotificationsClearPost"]["data"]>;
|
|
141
|
+
|
|
142
|
+
export async function notificationNotificationsClearPost(): Promise<NotificationNotificationsClearPostData> {
|
|
123
143
|
const client = ensureReq();
|
|
124
144
|
try {
|
|
125
|
-
const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<
|
|
145
|
+
const env = await client.post(`notification/notifications/clear`).json<OjEnvelope<NotificationNotificationsClearPostData>>();
|
|
126
146
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
127
147
|
if (typeof env.code === "number" && env.code !== 0)
|
|
128
148
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
129
|
-
const data = env.data as
|
|
149
|
+
const data = env.data as NotificationNotificationsClearPostData;
|
|
150
|
+
if (import.meta.env.DEV) {
|
|
151
|
+
const { schemas } = await import("./api.schemas");
|
|
152
|
+
const r = schemas["notificationNotificationsClearPost"].data.safeParse(data);
|
|
153
|
+
if (!r.success)
|
|
154
|
+
throw new ContractApiError(-1, `[契约违例] notificationNotificationsClearPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
155
|
+
}
|
|
130
156
|
return data;
|
|
131
157
|
}
|
|
132
158
|
catch (e) {
|
|
@@ -135,15 +161,22 @@ export async function notificationNotificationsClearPost(): Promise<unknown> {
|
|
|
135
161
|
}
|
|
136
162
|
|
|
137
163
|
export type NotificationPushPostBody = z.input<(typeof schemas)["notificationPushPost"]["body"]>;
|
|
164
|
+
export type NotificationPushPostData = z.infer<(typeof schemas)["notificationPushPost"]["data"]>;
|
|
138
165
|
|
|
139
|
-
export async function notificationPushPost(body: NotificationPushPostBody): Promise<
|
|
166
|
+
export async function notificationPushPost(body: NotificationPushPostBody): Promise<NotificationPushPostData> {
|
|
140
167
|
const client = ensureReq();
|
|
141
168
|
try {
|
|
142
|
-
const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<
|
|
169
|
+
const env = await client.post(`notification/push`, { json: body }).json<OjEnvelope<NotificationPushPostData>>();
|
|
143
170
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
144
171
|
if (typeof env.code === "number" && env.code !== 0)
|
|
145
172
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
146
|
-
const data = env.data as
|
|
173
|
+
const data = env.data as NotificationPushPostData;
|
|
174
|
+
if (import.meta.env.DEV) {
|
|
175
|
+
const { schemas } = await import("./api.schemas");
|
|
176
|
+
const r = schemas["notificationPushPost"].data.safeParse(data);
|
|
177
|
+
if (!r.success)
|
|
178
|
+
throw new ContractApiError(-1, `[契约违例] notificationPushPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
179
|
+
}
|
|
147
180
|
return data;
|
|
148
181
|
}
|
|
149
182
|
catch (e) {
|
|
@@ -152,15 +185,22 @@ export async function notificationPushPost(body: NotificationPushPostBody): Prom
|
|
|
152
185
|
}
|
|
153
186
|
|
|
154
187
|
export type NotificationPushBroadcastPostBody = z.input<(typeof schemas)["notificationPushBroadcastPost"]["body"]>;
|
|
188
|
+
export type NotificationPushBroadcastPostData = z.infer<(typeof schemas)["notificationPushBroadcastPost"]["data"]>;
|
|
155
189
|
|
|
156
|
-
export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<
|
|
190
|
+
export async function notificationPushBroadcastPost(body: NotificationPushBroadcastPostBody): Promise<NotificationPushBroadcastPostData> {
|
|
157
191
|
const client = ensureReq();
|
|
158
192
|
try {
|
|
159
|
-
const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<
|
|
193
|
+
const env = await client.post(`notification/push-broadcast`, { json: body }).json<OjEnvelope<NotificationPushBroadcastPostData>>();
|
|
160
194
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
161
195
|
if (typeof env.code === "number" && env.code !== 0)
|
|
162
196
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
163
|
-
const data = env.data as
|
|
197
|
+
const data = env.data as NotificationPushBroadcastPostData;
|
|
198
|
+
if (import.meta.env.DEV) {
|
|
199
|
+
const { schemas } = await import("./api.schemas");
|
|
200
|
+
const r = schemas["notificationPushBroadcastPost"].data.safeParse(data);
|
|
201
|
+
if (!r.success)
|
|
202
|
+
throw new ContractApiError(-1, `[契约违例] notificationPushBroadcastPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
203
|
+
}
|
|
164
204
|
return data;
|
|
165
205
|
}
|
|
166
206
|
catch (e) {
|
|
@@ -169,15 +209,22 @@ export async function notificationPushBroadcastPost(body: NotificationPushBroadc
|
|
|
169
209
|
}
|
|
170
210
|
|
|
171
211
|
export type NotificationDelPostBody = z.input<(typeof schemas)["notificationDelPost"]["body"]>;
|
|
212
|
+
export type NotificationDelPostData = z.infer<(typeof schemas)["notificationDelPost"]["data"]>;
|
|
172
213
|
|
|
173
|
-
export async function notificationDelPost(body: NotificationDelPostBody): Promise<
|
|
214
|
+
export async function notificationDelPost(body: NotificationDelPostBody): Promise<NotificationDelPostData> {
|
|
174
215
|
const client = ensureReq();
|
|
175
216
|
try {
|
|
176
|
-
const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<
|
|
217
|
+
const env = await client.post(`notification/del`, { json: body }).json<OjEnvelope<NotificationDelPostData>>();
|
|
177
218
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
178
219
|
if (typeof env.code === "number" && env.code !== 0)
|
|
179
220
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
180
|
-
const data = env.data as
|
|
221
|
+
const data = env.data as NotificationDelPostData;
|
|
222
|
+
if (import.meta.env.DEV) {
|
|
223
|
+
const { schemas } = await import("./api.schemas");
|
|
224
|
+
const r = schemas["notificationDelPost"].data.safeParse(data);
|
|
225
|
+
if (!r.success)
|
|
226
|
+
throw new ContractApiError(-1, `[契约违例] notificationDelPost 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
227
|
+
}
|
|
181
228
|
return data;
|
|
182
229
|
}
|
|
183
230
|
catch (e) {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { AppRouteRecordRaw, ModuleContext, NotificationsApiProvider } from "@oj-module/runtime";
|
|
2
|
+
|
|
3
|
+
import { BellOutlined } from "@ant-design/icons";
|
|
4
|
+
|
|
5
|
+
import { createElement, lazy } from "react";
|
|
6
|
+
|
|
7
|
+
import { createNotificationClient } from "./__client/api";
|
|
8
|
+
|
|
9
|
+
const NotificationCenter = lazy(() => import("./pages/center"));
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* 通知中心页(N3):全量消息 + 类型 Tab,铃铛弹层「查看全部」跳转至此。
|
|
13
|
+
* hideInMenu——入口在铃铛,不占侧边导航。
|
|
14
|
+
* 布局挂父路由 handle(resolveRouteLayouts 只给「无 Component 有 children」的
|
|
15
|
+
* 父路由注入 layout,页面组件写父级上布局会被忽略、裸渲染无 chrome)。
|
|
16
|
+
*/
|
|
17
|
+
export const routes: AppRouteRecordRaw[] = [
|
|
18
|
+
{
|
|
19
|
+
path: "/notification-center",
|
|
20
|
+
handle: {
|
|
21
|
+
layout: "container",
|
|
22
|
+
title: "notification:menu.notificationCenter",
|
|
23
|
+
icon: createElement(BellOutlined),
|
|
24
|
+
hideInMenu: true,
|
|
25
|
+
},
|
|
26
|
+
children: [
|
|
27
|
+
{
|
|
28
|
+
index: true,
|
|
29
|
+
Component: NotificationCenter,
|
|
30
|
+
handle: {
|
|
31
|
+
title: "notification:menu.notificationCenter",
|
|
32
|
+
icon: createElement(BellOutlined),
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 通知聚合的唯一前端消费面(G5 模板形态)。
|
|
41
|
+
*
|
|
42
|
+
* onInit 全量接管通知 API(D9 + G4):把通知读/写收敛到本模块 `/notification`
|
|
43
|
+
* 前缀下的 oj 后端,通知铃的互动(单条已读/全部已读/清空)由此生效;发射端
|
|
44
|
+
* 三方法(push/pushBroadcast/del)使本模块同时作为跨模块通知原语的接收端
|
|
45
|
+
* (前端 facade 委托至此)。
|
|
46
|
+
*/
|
|
47
|
+
export async function onInit(ctx: ModuleContext) {
|
|
48
|
+
// 构造即接线:createNotificationClient 登记契约前缀 /notification
|
|
49
|
+
// (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
|
|
50
|
+
// request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
|
|
51
|
+
// 方法都自动带 /api/notification/ 前缀。
|
|
52
|
+
const notificationClient = createNotificationClient(ctx);
|
|
53
|
+
const provider: NotificationsApiProvider = {
|
|
54
|
+
// 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
|
|
55
|
+
// 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
|
|
56
|
+
fetchNotifications: () => notificationClient.notificationNotificationsGet(),
|
|
57
|
+
// id 为命名空间串("u:<id>" / "b:<id>"),原样透传
|
|
58
|
+
markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
|
|
59
|
+
markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
|
|
60
|
+
clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
|
|
61
|
+
push: input => notificationClient
|
|
62
|
+
.notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
63
|
+
.then(r => String((r as { id: string }).id)),
|
|
64
|
+
pushBroadcast: input => notificationClient
|
|
65
|
+
.notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
66
|
+
.then(r => String((r as { id: string }).id)),
|
|
67
|
+
del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
|
|
68
|
+
};
|
|
69
|
+
ctx.register.notificationsApi(provider);
|
|
70
|
+
}
|
|
@@ -17,6 +17,9 @@ export const API_PREFIX = "/personal-center";
|
|
|
17
17
|
/** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
|
|
18
18
|
export const MODULE_PREFIX = "/personal-center";
|
|
19
19
|
|
|
20
|
+
/** 认领方模块(web.config clientOwners 指定):跨模块认领护栏对比基准 */
|
|
21
|
+
export const MODULE_OWNER = "/app";
|
|
22
|
+
|
|
20
23
|
/**
|
|
21
24
|
* 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
|
|
22
25
|
* scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
|
|
@@ -28,8 +31,8 @@ export function createPersonalCenterClient(ctx: ModuleContext) {
|
|
|
28
31
|
// 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
|
|
29
32
|
// 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
|
|
30
33
|
// 属于别的命名空间,认领身份仍按目录名。
|
|
31
|
-
if (import.meta.env.DEV && ctx.module.name !==
|
|
32
|
-
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${
|
|
34
|
+
if (import.meta.env.DEV && ctx.module.name !== MODULE_OWNER.slice(1)) {
|
|
35
|
+
console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_OWNER.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
|
|
33
36
|
}
|
|
34
37
|
ctx.register.apiPrefix(API_PREFIX);
|
|
35
38
|
bindRequest(ctx.utils.request);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { AppRouteRecordRaw, ModuleContext, UploadApiProvider } from "@oj-module/runtime";
|
|
2
|
+
import { ProfileCardIcon, RiAccountCircleLine, useAuthStore } from "@oj-module/runtime";
|
|
3
|
+
|
|
4
|
+
import { createElement, lazy } from "react";
|
|
5
|
+
|
|
6
|
+
import { createPersonalCenterClient } from "./__client/api";
|
|
7
|
+
|
|
8
|
+
const MyProfile = lazy(() => import("./pages/my-profile"));
|
|
9
|
+
|
|
10
|
+
/** 个人中心根 `/personal-center`:runtime 用户菜单固定导航 `/personal-center/my-profile`,缺则点击落空。 */
|
|
11
|
+
export const routes: AppRouteRecordRaw[] = [
|
|
12
|
+
{
|
|
13
|
+
path: "/personal-center",
|
|
14
|
+
handle: {
|
|
15
|
+
layout: "container",
|
|
16
|
+
order: 110,
|
|
17
|
+
title: "personal-center:menu.personalCenter",
|
|
18
|
+
icon: createElement(RiAccountCircleLine),
|
|
19
|
+
},
|
|
20
|
+
children: [
|
|
21
|
+
{
|
|
22
|
+
path: "/personal-center/my-profile",
|
|
23
|
+
Component: MyProfile,
|
|
24
|
+
handle: {
|
|
25
|
+
title: "personal-center:menu.profile",
|
|
26
|
+
icon: createElement(ProfileCardIcon),
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* P4-3:接管头像上传端点(D9)。antd Upload 直连 action,headers 在上传瞬间
|
|
35
|
+
* 取当前 token 注入 Bearer(token 存 runtime auth store,跨渲染期读 getState)。
|
|
36
|
+
*/
|
|
37
|
+
export async function onInit(ctx: ModuleContext) {
|
|
38
|
+
// 构造即接线:登记契约前缀 /personal-center + 绑定 scoped request,
|
|
39
|
+
// 页面直接 import 的 personalCenterProfilePost 才有可用的请求通道
|
|
40
|
+
createPersonalCenterClient(ctx);
|
|
41
|
+
const provider: UploadApiProvider = {
|
|
42
|
+
action: "/api/personal-center/upload",
|
|
43
|
+
headers: (): Record<string, string> => {
|
|
44
|
+
const token = useAuthStore.getState().token;
|
|
45
|
+
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
ctx.register.uploadApi(provider);
|
|
49
|
+
}
|