@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
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// B4:roles 出自 sys_user_role 权威源(users.roles JSON 列已被 0003 移除,D10)——
|
|
3
3
|
// 体验层角色走本资料接口,不进 JWT claims;avatar 读 avatar_base64,email 读 0007 迁移列。
|
|
4
4
|
// 响应一律走 json.ok/json.fail 信封(oj 红线)。
|
|
5
|
-
|
|
5
|
+
const handler = {
|
|
6
6
|
async get() {
|
|
7
7
|
const uid = http.user?.id;
|
|
8
8
|
if (!uid) {
|
|
@@ -35,3 +35,23 @@ export default {
|
|
|
35
35
|
}
|
|
36
36
|
},
|
|
37
37
|
};
|
|
38
|
+
|
|
39
|
+
// 出参:登录链用户资料(roles 出自 sys_user_role,permissions 预留恒空)
|
|
40
|
+
handler.get.response = {
|
|
41
|
+
type: "object",
|
|
42
|
+
required: ["id", "avatar", "username", "displayName", "email", "phoneNumber", "description", "roles", "mustChange", "permissions"],
|
|
43
|
+
properties: {
|
|
44
|
+
id: { type: "string" },
|
|
45
|
+
avatar: { type: "string" },
|
|
46
|
+
username: { type: "string" },
|
|
47
|
+
displayName: { type: "string" },
|
|
48
|
+
email: { type: "string" },
|
|
49
|
+
phoneNumber: { type: "string" },
|
|
50
|
+
description: { type: "string" },
|
|
51
|
+
roles: { type: "array", items: { type: "string" } },
|
|
52
|
+
mustChange: { type: "boolean" },
|
|
53
|
+
permissions: { type: "array", items: { type: "string" } },
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export default handler;
|
|
@@ -4,5 +4,33 @@
|
|
|
4
4
|
import { z } from "@oj-module/runtime";
|
|
5
5
|
|
|
6
6
|
export const schemas = {
|
|
7
|
+
"webGetAsyncRoutesGet": {
|
|
8
|
+
data: z.array(z.object({
|
|
9
|
+
"path": z.string().optional(),
|
|
10
|
+
"component": z.string().optional(),
|
|
11
|
+
"children": z.array(z.object({
|
|
7
12
|
|
|
13
|
+
})).optional()
|
|
14
|
+
})),
|
|
15
|
+
},
|
|
16
|
+
"webHelloGet": {
|
|
17
|
+
data: z.object({
|
|
18
|
+
"message": z.string(),
|
|
19
|
+
"user": z.number().nullable()
|
|
20
|
+
}),
|
|
21
|
+
},
|
|
22
|
+
"webUserInfoGet": {
|
|
23
|
+
data: z.object({
|
|
24
|
+
"id": z.string(),
|
|
25
|
+
"avatar": z.string(),
|
|
26
|
+
"username": z.string(),
|
|
27
|
+
"displayName": z.string(),
|
|
28
|
+
"email": z.string(),
|
|
29
|
+
"phoneNumber": z.string(),
|
|
30
|
+
"description": z.string(),
|
|
31
|
+
"roles": z.array(z.string()),
|
|
32
|
+
"mustChange": z.boolean(),
|
|
33
|
+
"permissions": z.array(z.string())
|
|
34
|
+
}),
|
|
35
|
+
},
|
|
8
36
|
};
|
|
@@ -64,14 +64,22 @@ async function toApiError(e: unknown): Promise<unknown> {
|
|
|
64
64
|
return e;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
-
export
|
|
67
|
+
export type WebGetAsyncRoutesGetData = z.infer<(typeof schemas)["webGetAsyncRoutesGet"]["data"]>;
|
|
68
|
+
|
|
69
|
+
export async function webGetAsyncRoutesGet(): Promise<WebGetAsyncRoutesGetData> {
|
|
68
70
|
const client = ensureReq();
|
|
69
71
|
try {
|
|
70
|
-
const env = await client.get(`web/get-async-routes`).json<OjEnvelope<
|
|
72
|
+
const env = await client.get(`web/get-async-routes`).json<OjEnvelope<WebGetAsyncRoutesGetData>>();
|
|
71
73
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
72
74
|
if (typeof env.code === "number" && env.code !== 0)
|
|
73
75
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
74
|
-
const data = env.data as
|
|
76
|
+
const data = env.data as WebGetAsyncRoutesGetData;
|
|
77
|
+
if (import.meta.env.DEV) {
|
|
78
|
+
const { schemas } = await import("./api.schemas");
|
|
79
|
+
const r = schemas["webGetAsyncRoutesGet"].data.safeParse(data);
|
|
80
|
+
if (!r.success)
|
|
81
|
+
throw new ContractApiError(-1, `[契约违例] webGetAsyncRoutesGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
82
|
+
}
|
|
75
83
|
return data;
|
|
76
84
|
}
|
|
77
85
|
catch (e) {
|
|
@@ -79,14 +87,22 @@ export async function webGetAsyncRoutesGet(): Promise<unknown> {
|
|
|
79
87
|
}
|
|
80
88
|
}
|
|
81
89
|
|
|
82
|
-
export
|
|
90
|
+
export type WebHelloGetData = z.infer<(typeof schemas)["webHelloGet"]["data"]>;
|
|
91
|
+
|
|
92
|
+
export async function webHelloGet(): Promise<WebHelloGetData> {
|
|
83
93
|
const client = ensureReq();
|
|
84
94
|
try {
|
|
85
|
-
const env = await client.get(`web/hello`).json<OjEnvelope<
|
|
95
|
+
const env = await client.get(`web/hello`).json<OjEnvelope<WebHelloGetData>>();
|
|
86
96
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
87
97
|
if (typeof env.code === "number" && env.code !== 0)
|
|
88
98
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
89
|
-
const data = env.data as
|
|
99
|
+
const data = env.data as WebHelloGetData;
|
|
100
|
+
if (import.meta.env.DEV) {
|
|
101
|
+
const { schemas } = await import("./api.schemas");
|
|
102
|
+
const r = schemas["webHelloGet"].data.safeParse(data);
|
|
103
|
+
if (!r.success)
|
|
104
|
+
throw new ContractApiError(-1, `[契约违例] webHelloGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
105
|
+
}
|
|
90
106
|
return data;
|
|
91
107
|
}
|
|
92
108
|
catch (e) {
|
|
@@ -94,14 +110,22 @@ export async function webHelloGet(): Promise<unknown> {
|
|
|
94
110
|
}
|
|
95
111
|
}
|
|
96
112
|
|
|
97
|
-
export
|
|
113
|
+
export type WebUserInfoGetData = z.infer<(typeof schemas)["webUserInfoGet"]["data"]>;
|
|
114
|
+
|
|
115
|
+
export async function webUserInfoGet(): Promise<WebUserInfoGetData> {
|
|
98
116
|
const client = ensureReq();
|
|
99
117
|
try {
|
|
100
|
-
const env = await client.get(`web/user-info`).json<OjEnvelope<
|
|
118
|
+
const env = await client.get(`web/user-info`).json<OjEnvelope<WebUserInfoGetData>>();
|
|
101
119
|
// 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
|
|
102
120
|
if (typeof env.code === "number" && env.code !== 0)
|
|
103
121
|
throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
|
|
104
|
-
const data = env.data as
|
|
122
|
+
const data = env.data as WebUserInfoGetData;
|
|
123
|
+
if (import.meta.env.DEV) {
|
|
124
|
+
const { schemas } = await import("./api.schemas");
|
|
125
|
+
const r = schemas["webUserInfoGet"].data.safeParse(data);
|
|
126
|
+
if (!r.success)
|
|
127
|
+
throw new ContractApiError(-1, `[契约违例] webUserInfoGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
|
|
128
|
+
}
|
|
105
129
|
return data;
|
|
106
130
|
}
|
|
107
131
|
catch (e) {
|
package/templates/web.config.ts
CHANGED
|
@@ -10,24 +10,17 @@
|
|
|
10
10
|
* 登记了不存在的前端入口才报错。
|
|
11
11
|
*
|
|
12
12
|
* 注意:这里的登记顺序只影响模块加载顺序,**不决定侧边栏导航顺序**——
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* `home` 必须第一个登记(shell 的 "/" 重定向到它)。
|
|
13
|
+
* 菜单顺序由各根路由的 handle.order 决定(≥0 主菜单区升序;<0 底部固定区,
|
|
14
|
+
* pin 在侧边栏底部;同 order 按根 path 字母序)。
|
|
16
15
|
*/
|
|
17
16
|
const allModules = [
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
//
|
|
21
|
-
{ name: "
|
|
22
|
-
// 个人中心:runtime 用户菜单固定导航 /personal-center/my-profile,缺则点击落空
|
|
23
|
-
{ name: "personal-center", entry: "src/personal-center/web/entry.ts" },
|
|
24
|
-
// 通知:onInit 注册 notificationsApi provider(四方法全必填),
|
|
25
|
-
// 通知铃的互动(单条已读/全部已读/清空)由此生效;未登记则永远回落内置只读兜底
|
|
26
|
-
{ name: "notification", entry: "src/notification/web/entry.ts" },
|
|
27
|
-
// 系统管理:用户/成员资格/角色权限/操作日志/站点设置/登录方式(oj 后端 /api/system/*)
|
|
28
|
-
{ name: "system", entry: "src/system/web/entry.ts" },
|
|
17
|
+
// 框架内置页面:首页/登录/个人中心/通知/系统管理聚合成**一个多根模块**。
|
|
18
|
+
// 各根按 handle.order 排序(/home 1、/personal-center 110、/system -100 底部),
|
|
19
|
+
// 与拆分时的菜单逐项一致;/login、/change-password、/notification-center 走 hideInMenu。
|
|
20
|
+
{ name: "app", entry: "src/app/web/entry.ts" },
|
|
29
21
|
// 解决方案市场:第一方能力(消费 _platform 编排面 /api/_platform/market/*,
|
|
30
|
-
// R29
|
|
22
|
+
// R29:第三方方案不得仿照此调用面),默认随脚手架下发。保持独立模块——
|
|
23
|
+
// 可选能力,删目录即可下线。
|
|
31
24
|
{ name: "oj-market", entry: "src/oj-market/web/entry.ts" },
|
|
32
25
|
// #region demo-only
|
|
33
26
|
// 以下为模板内置的演示模块(about/access/exception/outside/route-nest),
|
|
@@ -49,4 +42,17 @@ export default {
|
|
|
49
42
|
/** 产物 URL 前缀,留空表示同源相对路径 */
|
|
50
43
|
baseUrl: "",
|
|
51
44
|
modules: allModules,
|
|
45
|
+
/**
|
|
46
|
+
* 生成 client 的归属模块:`api 前缀 → 承载其 __client 的 web 模块`。
|
|
47
|
+
*
|
|
48
|
+
* 多根聚合模块把多个业务域的页面/client 收进 `src/app/web`,故五个 api 前缀的
|
|
49
|
+
* 生成物都落在 `src/app/web/<api 前缀>/__client`(与页面同级,页面相对导入不变)。
|
|
50
|
+
* 不配则回落同名模块(`src/<api 前缀>/web/__client`,历史行为)。
|
|
51
|
+
*/
|
|
52
|
+
clientOwners: {
|
|
53
|
+
"home": "app",
|
|
54
|
+
"personal-center": "app",
|
|
55
|
+
"notification": "app",
|
|
56
|
+
"system": "app",
|
|
57
|
+
},
|
|
52
58
|
};
|
|
@@ -8,9 +8,9 @@
|
|
|
8
8
|
"react-router": "7.18.2",
|
|
9
9
|
"react-router/dom": "7.18.2",
|
|
10
10
|
"@tanstack/react-query": "5.102.8",
|
|
11
|
-
"@oj-module/runtime": "0.2.
|
|
12
|
-
"@oj-module/runtime/contract": "0.2.
|
|
13
|
-
"@oj-module/runtime/contract/errors": "0.2.
|
|
11
|
+
"@oj-module/runtime": "0.2.54",
|
|
12
|
+
"@oj-module/runtime/contract": "0.2.54",
|
|
13
|
+
"@oj-module/runtime/contract/errors": "0.2.54",
|
|
14
14
|
"@plane/kit": "0.1.20",
|
|
15
15
|
"@plane/editor": "0.1.20",
|
|
16
16
|
"antd": "6.6.1",
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { HomeOutlined } from "@ant-design/icons";
|
|
2
|
-
import { defineModule } from "@oj-module/runtime";
|
|
3
|
-
import { createElement, lazy } from "react";
|
|
4
|
-
|
|
5
|
-
const Home = lazy(() => import("./pages/index"));
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* 首页模块:提供 `/home` 路由。
|
|
9
|
-
*
|
|
10
|
-
* 为什么必须有:shell 预构建产物把 `VITE_BASE_HOME_PATH` 定为 `/home`
|
|
11
|
-
* (登录成功回跳、logo 点击、tabbar 首页、403/404 回首页都指向它)。
|
|
12
|
-
* 工程缺 `/home` 路由时这些跳转会落到错误边界。
|
|
13
|
-
*
|
|
14
|
-
* 结构对齐 apps/playground-oj/web/src/home;页面用统计卡片 + 折线/柱/饼图
|
|
15
|
-
* 演示宿主共享依赖矩阵(echarts / echarts-for-react / react-countup / dayjs /
|
|
16
|
-
* antd / @ant-design/icons / react-i18next,均由宿主 importmap 提供单例)。
|
|
17
|
-
* 图表数据为静态演示值——真实业务走模块契约 + `ojm api` 生成的 client。
|
|
18
|
-
*/
|
|
19
|
-
export default defineModule({
|
|
20
|
-
name: "home",
|
|
21
|
-
description: "首页模块",
|
|
22
|
-
version: "0.1.0",
|
|
23
|
-
peerRuntime: ">=0.0.0",
|
|
24
|
-
routes: [
|
|
25
|
-
{
|
|
26
|
-
path: "/home",
|
|
27
|
-
handle: {
|
|
28
|
-
layout: "container",
|
|
29
|
-
order: 1,
|
|
30
|
-
title: "home:menu.home",
|
|
31
|
-
icon: createElement(HomeOutlined),
|
|
32
|
-
},
|
|
33
|
-
children: [
|
|
34
|
-
{
|
|
35
|
-
index: true,
|
|
36
|
-
Component: Home,
|
|
37
|
-
handle: {
|
|
38
|
-
title: "home:menu.home",
|
|
39
|
-
icon: createElement(HomeOutlined),
|
|
40
|
-
},
|
|
41
|
-
},
|
|
42
|
-
],
|
|
43
|
-
},
|
|
44
|
-
],
|
|
45
|
-
i18n: {
|
|
46
|
-
"zh-CN": () => import("./locales/zh-CN.json"),
|
|
47
|
-
"en-US": () => import("./locales/en-US.json"),
|
|
48
|
-
},
|
|
49
|
-
});
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
import { defineModule, useAuthStore, useTenantStore } from "@oj-module/runtime";
|
|
2
|
-
import { lazy } from "react";
|
|
3
|
-
import { authProvider } from "./auth-provider";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 登录模块:提供 `/login` 路由与内容区。
|
|
7
|
-
*
|
|
8
|
-
* shell 宿主只消费模块路由(`getRoutes()`),**不挂 runtime 的内置 baseRoutes**,
|
|
9
|
-
* 因此缺少本模块时 `/login` 无路由可跳(登出/回跳登录会落空)。本模块补上该路由:
|
|
10
|
-
* 登录链路仍回落到 runtime 默认链(`fetchLogin` → 后端内置 `auth/login` +
|
|
11
|
-
* 守卫 `getUserInfo` → `web/user-info`),模块只负责「内容区」。
|
|
12
|
-
*
|
|
13
|
-
* `handle.login: true` 让框架识别为登录页(`resolveLoginRoute` 校验路径必须是
|
|
14
|
-
* `/login`);`handle.layout: "fullscreen"` 由框架注入全屏外壳。
|
|
15
|
-
*/
|
|
16
|
-
export default defineModule({
|
|
17
|
-
name: "login",
|
|
18
|
-
description: "登录模块(内容区由模块提供,外壳由框架提供)",
|
|
19
|
-
version: "0.1.0",
|
|
20
|
-
peerRuntime: ">=0.0.0",
|
|
21
|
-
routes: [
|
|
22
|
-
{
|
|
23
|
-
path: "/login",
|
|
24
|
-
handle: {
|
|
25
|
-
layout: "fullscreen",
|
|
26
|
-
login: true,
|
|
27
|
-
hideInMenu: true,
|
|
28
|
-
title: "login:page.title",
|
|
29
|
-
},
|
|
30
|
-
children: [
|
|
31
|
-
{
|
|
32
|
-
index: true,
|
|
33
|
-
Component: lazy(() => import("./pages/login")),
|
|
34
|
-
handle: { title: "login:page.title" },
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
{
|
|
39
|
-
// 首登/重置后强制改密(must_change 悬挂期唯一可用写端);
|
|
40
|
-
// 管理面 403 msg=password change required 时也引导至此
|
|
41
|
-
path: "/change-password",
|
|
42
|
-
handle: {
|
|
43
|
-
layout: "fullscreen",
|
|
44
|
-
hideInMenu: true,
|
|
45
|
-
title: "login:page.changePwd.title",
|
|
46
|
-
},
|
|
47
|
-
children: [
|
|
48
|
-
{
|
|
49
|
-
index: true,
|
|
50
|
-
Component: lazy(() => import("./pages/change-password")),
|
|
51
|
-
handle: { title: "login:page.changePwd.title", hideInMenu: true },
|
|
52
|
-
},
|
|
53
|
-
],
|
|
54
|
-
},
|
|
55
|
-
],
|
|
56
|
-
i18n: {
|
|
57
|
-
"zh-CN": () => import("./locales/zh-CN.json"),
|
|
58
|
-
"en-US": () => import("./locales/en-US.json"),
|
|
59
|
-
},
|
|
60
|
-
lifecycle: {
|
|
61
|
-
/**
|
|
62
|
-
* 租户上下文(L2):请求头由**运行时**注入(`runtime/store/tenant.ts`),模块不再注册
|
|
63
|
-
* headerProvider——宿主 shell 的 hydrateUser / solution-routes 都在任何模块 onInit 之前
|
|
64
|
-
* 就发请求,模块注册必然晚一步(2026-10-30 冷启动 e2e 实测两个 400 missing tenant header)。
|
|
65
|
-
* 模块这里只保留「登出清租户」这一业务语义。
|
|
66
|
-
*/
|
|
67
|
-
async onInit(ctx) {
|
|
68
|
-
// 认证 provider:仅接管「企微票据换会话」,其余方式原样委托内置链(见 auth-provider.ts)。
|
|
69
|
-
// 注册时机必须在任何登录动作之前——onInit 先于路由渲染。
|
|
70
|
-
ctx.register.authProvider(authProvider);
|
|
71
|
-
// 登出必须清租户:否则下一个用户会带着上一个用户的租户头(跨租户请求 fail-closed 403,
|
|
72
|
-
// 但单租户部署会静默落到别人的 default 视图)。token 由非空转空即视为登出。
|
|
73
|
-
useAuthStore.subscribe((state, prev) => {
|
|
74
|
-
if (prev.token && !state.token)
|
|
75
|
-
useTenantStore.getState().clear();
|
|
76
|
-
});
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
});
|
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
import type { AppRouteRecordRaw, NotificationsApiProvider } from "@oj-module/runtime";
|
|
2
|
-
|
|
3
|
-
import { BellOutlined } from "@ant-design/icons";
|
|
4
|
-
import { defineModule } from "@oj-module/runtime";
|
|
5
|
-
|
|
6
|
-
import { createElement, lazy } from "react";
|
|
7
|
-
|
|
8
|
-
import { createNotificationClient } from "./__client/api";
|
|
9
|
-
|
|
10
|
-
const NotificationCenter = lazy(() => import("./pages/center"));
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* 通知中心页(N3):全量消息 + 类型 Tab,铃铛弹层「查看全部」跳转至此。
|
|
14
|
-
* hideInMenu——入口在铃铛,不占侧边导航。
|
|
15
|
-
* 布局挂父路由 handle(resolveRouteLayouts 只给「无 Component 有 children」的
|
|
16
|
-
* 父路由注入 layout,页面组件写父级上布局会被忽略、裸渲染无 chrome)。
|
|
17
|
-
*/
|
|
18
|
-
const routes: AppRouteRecordRaw[] = [
|
|
19
|
-
{
|
|
20
|
-
path: "/notification-center",
|
|
21
|
-
handle: {
|
|
22
|
-
layout: "container",
|
|
23
|
-
title: "notification:menu.notificationCenter",
|
|
24
|
-
icon: createElement(BellOutlined),
|
|
25
|
-
hideInMenu: true,
|
|
26
|
-
},
|
|
27
|
-
children: [
|
|
28
|
-
{
|
|
29
|
-
index: true,
|
|
30
|
-
Component: NotificationCenter,
|
|
31
|
-
handle: {
|
|
32
|
-
title: "notification:menu.notificationCenter",
|
|
33
|
-
icon: createElement(BellOutlined),
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
},
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* notification 模块(G5 模板形态):通知聚合的唯一前端消费面。
|
|
42
|
-
*
|
|
43
|
-
* onInit 全量接管通知 API(D9 + G4):把通知读/写收敛到本模块 `/notification`
|
|
44
|
-
* 前缀下的 oj 后端,通知铃的互动(单条已读/全部已读/清空)由此生效;发射端
|
|
45
|
-
* 三方法(push/pushBroadcast/del)使本模块同时作为跨模块通知原语的接收端
|
|
46
|
-
* (前端 facade 委托至此)。
|
|
47
|
-
*/
|
|
48
|
-
export default defineModule({
|
|
49
|
-
name: "notification",
|
|
50
|
-
description: "通知模块(D9/G4 注入通知读写 + 通知中心页)",
|
|
51
|
-
version: "0.1.0",
|
|
52
|
-
peerRuntime: ">=0.0.0",
|
|
53
|
-
routes,
|
|
54
|
-
i18n: {
|
|
55
|
-
"zh-CN": () => import("./locales/zh-CN.json"),
|
|
56
|
-
"en-US": () => import("./locales/en-US.json"),
|
|
57
|
-
},
|
|
58
|
-
lifecycle: {
|
|
59
|
-
async onInit(ctx) {
|
|
60
|
-
// 构造即接线:createNotificationClient 登记契约前缀 /notification
|
|
61
|
-
// (唯一真源在 __client/api.ts 的 API_PREFIX)+ 绑定圈好前缀的 scoped
|
|
62
|
-
// request(越界/穿越发请求前即拒绝,前缀收敛 D11),返回的 client
|
|
63
|
-
// 方法都自动带 /api/notification/ 前缀。
|
|
64
|
-
const notificationClient = createNotificationClient(ctx);
|
|
65
|
-
const provider: NotificationsApiProvider = {
|
|
66
|
-
// 端点函数名由 oj operationId 派生(camelCase 子路径 + 方法),
|
|
67
|
-
// 见 __client/api.ts;provider 方法名(fetchNotifications/markRead…)不变。
|
|
68
|
-
fetchNotifications: () => notificationClient.notificationNotificationsGet(),
|
|
69
|
-
// id 为命名空间串("u:<id>" / "b:<id>"),原样透传
|
|
70
|
-
markRead: id => notificationClient.notificationNotificationsReadPost({ id: String(id) }).then(() => {}),
|
|
71
|
-
markAllRead: () => notificationClient.notificationNotificationsReadAllPost().then(() => {}),
|
|
72
|
-
clearAll: () => notificationClient.notificationNotificationsClearPost().then(() => {}),
|
|
73
|
-
push: input => notificationClient
|
|
74
|
-
.notificationPushPost({ userId: input.userId, title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
75
|
-
.then(r => String((r as { id: string }).id)),
|
|
76
|
-
pushBroadcast: input => notificationClient
|
|
77
|
-
.notificationPushBroadcastPost({ title: input.title, message: input.message, link: input.link, avatar: input.avatar, category: input.category })
|
|
78
|
-
.then(r => String((r as { id: string }).id)),
|
|
79
|
-
del: (id, userId) => notificationClient.notificationDelPost({ id: String(id), userId }).then(() => {}),
|
|
80
|
-
};
|
|
81
|
-
ctx.register.notificationsApi(provider);
|
|
82
|
-
},
|
|
83
|
-
},
|
|
84
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import type { AppRouteRecordRaw, ModuleDefinition, 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
|
-
const routes: AppRouteRecordRaw[] = [
|
|
11
|
-
{
|
|
12
|
-
path: "/personal-center",
|
|
13
|
-
handle: {
|
|
14
|
-
layout: "container",
|
|
15
|
-
order: 110,
|
|
16
|
-
title: "personal-center:menu.personalCenter",
|
|
17
|
-
icon: createElement(RiAccountCircleLine),
|
|
18
|
-
},
|
|
19
|
-
children: [
|
|
20
|
-
{
|
|
21
|
-
path: "/personal-center/my-profile",
|
|
22
|
-
Component: MyProfile,
|
|
23
|
-
handle: {
|
|
24
|
-
title: "personal-center:menu.profile",
|
|
25
|
-
icon: createElement(ProfileCardIcon),
|
|
26
|
-
},
|
|
27
|
-
},
|
|
28
|
-
],
|
|
29
|
-
},
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const mod: ModuleDefinition = {
|
|
33
|
-
name: "personal-center",
|
|
34
|
-
description: "个人中心模块",
|
|
35
|
-
version: "0.1.0",
|
|
36
|
-
routes,
|
|
37
|
-
i18n: {
|
|
38
|
-
"zh-CN": () => import("./locales/zh-CN.json"),
|
|
39
|
-
"en-US": () => import("./locales/en-US.json"),
|
|
40
|
-
},
|
|
41
|
-
lifecycle: {
|
|
42
|
-
// P4-3:接管头像上传端点(D9)。antd Upload 直连 action,headers 在上传瞬间
|
|
43
|
-
// 取当前 token 注入 Bearer(token 存 runtime auth store,跨渲染期读 getState)。
|
|
44
|
-
async onInit(ctx) {
|
|
45
|
-
// 构造即接线:登记契约前缀 /personal-center + 绑定 scoped request,
|
|
46
|
-
// 页面直接 import 的 personalCenterProfilePost 才有可用的请求通道
|
|
47
|
-
createPersonalCenterClient(ctx);
|
|
48
|
-
const provider: UploadApiProvider = {
|
|
49
|
-
action: "/api/personal-center/upload",
|
|
50
|
-
headers: (): Record<string, string> => {
|
|
51
|
-
const token = useAuthStore.getState().token;
|
|
52
|
-
return token ? { Authorization: `Bearer ${token}` } : {};
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
ctx.register.uploadApi(provider);
|
|
56
|
-
},
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export default mod;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
/package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|