@oj-module/cli 0.2.50 → 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.
Files changed (130) hide show
  1. package/devkit/SKILL.md +2 -2
  2. package/devkit/manual.md +32 -25
  3. package/devkit/wecom-internal-app.md +111 -4
  4. package/package.json +2 -2
  5. package/shell-dist/assets/{index-FYt9CjFd.js → index-e_Mp2mb5.js} +5 -1
  6. package/shell-dist/assets/runtime.js +29 -5
  7. package/shell-dist/index.html +9 -12
  8. package/shell-dist/versions.json +3 -3
  9. package/src/__tests__/templates-tenant-scope.test.ts +3 -2
  10. package/src/build.ts +19 -2
  11. package/src/config.ts +9 -0
  12. package/src/contract/__tests__/client-path.test.ts +30 -0
  13. package/src/contract/__tests__/emit-client.test.ts +18 -0
  14. package/src/contract/__tests__/handler-scan.test.ts +23 -0
  15. package/src/contract/check.ts +5 -5
  16. package/src/contract/client-path.ts +30 -0
  17. package/src/contract/emit-client.ts +18 -4
  18. package/src/contract/handler-scan.ts +40 -3
  19. package/src/contract/run.ts +5 -3
  20. package/src/init.ts +19 -1
  21. package/src/shared-deps.ts +27 -9
  22. package/src/site-base.ts +1 -1
  23. package/templates/README.md +40 -44
  24. package/templates/init.include.yaml +15 -14
  25. package/templates/openapi.json +3422 -1955
  26. package/templates/src/_platform/api/_shared/service/domain/users.ts +40 -4
  27. package/templates/src/_platform/api/contacts/api.ts +29 -1
  28. package/templates/src/_platform/api/contacts/detail/api.ts +25 -1
  29. package/templates/src/_platform/api/solutions/api.ts +60 -1
  30. package/templates/src/_platform/web/__client/api.schemas.ts +99 -35
  31. package/templates/src/_platform/web/__client/api.ts +103 -59
  32. package/templates/src/app/web/entry.ts +77 -0
  33. package/templates/src/{home/web → app/web/home}/__client/api.ts +5 -2
  34. package/templates/src/app/web/home/routes.ts +41 -0
  35. package/templates/src/{login/web → app/web/login}/pages/login.tsx +54 -24
  36. package/templates/src/app/web/login/routes.ts +71 -0
  37. package/templates/src/{notification/web → app/web/notification}/__client/api.schemas.ts +14 -0
  38. package/templates/src/{notification/web → app/web/notification}/__client/api.ts +67 -20
  39. package/templates/src/app/web/notification/routes.ts +70 -0
  40. package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.ts +5 -2
  41. package/templates/src/app/web/personal-center/routes.ts +49 -0
  42. package/templates/src/{system/web → app/web/system}/__client/api.schemas.ts +28 -22
  43. package/templates/src/{system/web → app/web/system}/__client/api.ts +103 -58
  44. package/templates/src/{system/web → app/web/system}/locales/en-US.json +14 -0
  45. package/templates/src/{system/web → app/web/system}/locales/zh-CN.json +14 -0
  46. package/templates/src/{system/web → app/web/system}/pages/users/index.tsx +5 -2
  47. package/templates/src/{system/web → app/web/system}/pages/users/roles.tsx +14 -3
  48. package/templates/src/{system/web → app/web/system}/pages/wecom/index.tsx +139 -1
  49. package/templates/src/app/web/system/routes.ts +97 -0
  50. package/templates/src/auth/api/_shared/wecom/admin.ts +4 -0
  51. package/templates/src/auth/api/_shared/wecom/client.ts +9 -2
  52. package/templates/src/auth/api/_shared/wecom/events.ts +3 -0
  53. package/templates/src/auth/api/_shared/wecom/tenant-admin.ts +194 -0
  54. package/templates/src/auth/api/api.ts +59 -0
  55. package/templates/src/auth/api/ldap-login/api.ts +37 -1
  56. package/templates/src/auth/api/wecom/admin/tenant-admin/api.ts +41 -0
  57. package/templates/src/auth/api/wecom/admin/tenant-admin/password/api.ts +66 -0
  58. package/templates/src/auth/api/wecom/api.ts +23 -0
  59. package/templates/src/auth/web/__client/api.schemas.ts +133 -13
  60. package/templates/src/auth/web/__client/api.ts +129 -31
  61. package/templates/src/notification/api/api.ts +17 -0
  62. package/templates/src/notification/api/del/api.ts +2 -0
  63. package/templates/src/system/api/roles/api.ts +3 -0
  64. package/templates/src/system/api/roles/bindings/api.ts +3 -0
  65. package/templates/src/system/api/settings/api.ts +6 -3
  66. package/templates/src/system/api/users/forbidden/api.ts +3 -0
  67. package/templates/src/system/api/users/password/api.ts +3 -0
  68. package/templates/src/system/api/users/roles/api.ts +3 -0
  69. package/templates/src/web/api/get-async-routes/api.ts +16 -1
  70. package/templates/src/web/api/hello/api.ts +13 -1
  71. package/templates/src/web/api/user-info/api.ts +21 -1
  72. package/templates/src/web/web/__client/api.schemas.ts +28 -0
  73. package/templates/src/web/web/__client/api.ts +33 -9
  74. package/templates/tsconfig.paths.json +0 -1
  75. package/templates/web.config.ts +22 -17
  76. package/vendor/host-versions.json +3 -3
  77. package/shell-dist/assets/echarts-charts.js +0 -58571
  78. package/shell-dist/assets/echarts-features.js +0 -24057
  79. package/shell-dist/assets/echarts-for-react.js +0 -704
  80. package/shell-dist/assets/echarts.js +0 -82916
  81. package/shell-dist/assets/editor.js +0 -161457
  82. package/shell-dist/assets/vditor-index-css.js +0 -7
  83. package/shell-dist/assets/vditor.js +0 -15223
  84. package/templates/src/demo/api/manifest.yaml +0 -5
  85. package/templates/src/demo/api/migrations/0001__create_todos.sql +0 -9
  86. package/templates/src/demo/api/migrations/0002__tenant.mysql.sql +0 -2
  87. package/templates/src/demo/api/migrations/0002__tenant.sql +0 -2
  88. package/templates/src/demo/api/schema.yaml +0 -12
  89. package/templates/src/demo/api/seed.sql +0 -5
  90. package/templates/src/demo/api/todos/api.ts +0 -59
  91. package/templates/src/demo/web/__client/api.schemas.ts +0 -20
  92. package/templates/src/demo/web/__client/api.ts +0 -89
  93. package/templates/src/demo/web/entry.ts +0 -42
  94. package/templates/src/demo/web/locales/en-US.json +0 -10
  95. package/templates/src/demo/web/locales/zh-CN.json +0 -10
  96. package/templates/src/demo/web/pages/index.tsx +0 -45
  97. package/templates/src/home/web/entry.ts +0 -49
  98. package/templates/src/login/web/entry.ts +0 -79
  99. package/templates/src/notification/web/entry.ts +0 -84
  100. package/templates/src/personal-center/web/entry.ts +0 -60
  101. package/templates/src/system/web/entry.ts +0 -108
  102. /package/templates/src/{home/web → app/web/home}/__client/api.schemas.ts +0 -0
  103. /package/templates/src/{home/web → app/web/home}/locales/en-US.json +0 -0
  104. /package/templates/src/{home/web → app/web/home}/locales/zh-CN.json +0 -0
  105. /package/templates/src/{home/web → app/web/home}/pages/components/bar-chart.tsx +0 -0
  106. /package/templates/src/{home/web → app/web/home}/pages/components/card-list.tsx +0 -0
  107. /package/templates/src/{home/web → app/web/home}/pages/components/line-chart.tsx +0 -0
  108. /package/templates/src/{home/web → app/web/home}/pages/components/pie-chart.tsx +0 -0
  109. /package/templates/src/{home/web → app/web/home}/pages/index.tsx +0 -0
  110. /package/templates/src/{login/web → app/web/login}/auth-provider.ts +0 -0
  111. /package/templates/src/{login/web → app/web/login}/locales/en-US.json +0 -0
  112. /package/templates/src/{login/web → app/web/login}/locales/zh-CN.json +0 -0
  113. /package/templates/src/{login/web → app/web/login}/pages/change-password.tsx +0 -0
  114. /package/templates/src/{login/web → app/web/login}/wecom.ts +0 -0
  115. /package/templates/src/{notification/web → app/web/notification}/locales/en-US.json +0 -0
  116. /package/templates/src/{notification/web → app/web/notification}/locales/zh-CN.json +0 -0
  117. /package/templates/src/{notification/web → app/web/notification}/pages/center/index.tsx +0 -0
  118. /package/templates/src/{personal-center/web → app/web/personal-center}/__client/api.schemas.ts +0 -0
  119. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/en-US.json +0 -0
  120. /package/templates/src/{personal-center/web → app/web/personal-center}/locales/zh-CN.json +0 -0
  121. /package/templates/src/{personal-center/web → app/web/personal-center}/pages/my-profile/index.tsx +0 -0
  122. /package/templates/src/{system/web → app/web/system}/pages/departments/index.tsx +0 -0
  123. /package/templates/src/{system/web → app/web/system}/pages/departments/sync-bar.tsx +0 -0
  124. /package/templates/src/{system/web → app/web/system}/pages/departments/tree.ts +0 -0
  125. /package/templates/src/{system/web → app/web/system}/pages/errors.ts +0 -0
  126. /package/templates/src/{system/web → app/web/system}/pages/forbidden.tsx +0 -0
  127. /package/templates/src/{system/web → app/web/system}/pages/login-methods/index.tsx +0 -0
  128. /package/templates/src/{system/web → app/web/system}/pages/oplog/index.tsx +0 -0
  129. /package/templates/src/{system/web → app/web/system}/pages/roles/index.tsx +0 -0
  130. /package/templates/src/{system/web → app/web/system}/pages/settings/index.tsx +0 -0
@@ -1,5 +0,0 @@
1
- name: demo
2
- desc: 演示模块(todos 等,P3 接数据层)
3
- version: 0.1.0
4
- tables:
5
- - todos
@@ -1,9 +0,0 @@
1
- -- demo 模块建表(P3-2)。todos 待办表,S007 要求序号连续且唯一,单文件 0001。
2
- -- done 用 INTEGER(0/1) 承载布尔,handler 映射为 boolean。
3
- CREATE TABLE IF NOT EXISTS todos (
4
- id INTEGER PRIMARY KEY AUTOINCREMENT,
5
- title TEXT NOT NULL DEFAULT '',
6
- done INTEGER NOT NULL DEFAULT 0,
7
- create_time INTEGER NOT NULL DEFAULT 0,
8
- update_time INTEGER NOT NULL DEFAULT 0
9
- );
@@ -1,2 +0,0 @@
1
- -- 0002__tenant(mysql 方言):demo.todos 租户化。TEXT 不能带字面默认值 → VARCHAR(255)。
2
- ALTER TABLE todos ADD COLUMN tenant_id VARCHAR(255) NOT NULL DEFAULT 'default';
@@ -1,2 +0,0 @@
1
- -- 0002__tenant:demo.todos 租户化(企微代开发 v3 Phase 0 的 A 组)。
2
- ALTER TABLE todos ADD COLUMN tenant_id TEXT NOT NULL DEFAULT 'default';
@@ -1,12 +0,0 @@
1
- # 声明式表结构(§4.2 / D1-C):demo 模块自有表 todos。
2
- # 加表/加可空列由 reconcile 自动收敛;类型变更/改名走 migrations/。
3
- tables:
4
- todos:
5
- pk: id
6
- columns:
7
- id: {type: integer, autoincrement: true}
8
- tenant_id: {type: text, null: false}
9
- title: {type: text, null: false}
10
- done: {type: integer, null: false}
11
- create_time: {type: integer, null: false}
12
- update_time: {type: integer, null: false}
@@ -1,5 +0,0 @@
1
- -- demo 模块种子(P3-2):示例待办,幂等 INSERT OR IGNORE,按分号朴素切分(S006)。
2
- INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (1, '学习 uni-dev 契约机制', 1, 0, 0);
3
- INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (2, '完成 playground-oj system 模块', 0, 0, 0);
4
- INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (3, '编写 P3 教学文档', 0, 0, 0);
5
- INSERT OR IGNORE INTO todos (id, title, done, create_time, update_time) VALUES (4, '联调 demo 真实数据层', 0, 0, 0);
@@ -1,59 +0,0 @@
1
- // GET /api/demo/todos —— 演示待办列表(真实化 P3-2,AC-D9 apiPrefix=/demo)。
2
- // Bearer 守卫保护;keyword 模糊过滤 title;snake_case → camelCase,done 映射布尔。
3
-
4
- // 契约(v0.1.71「最新方式」):动词挂 `.schema` 四通道(params/query/body/response),
5
- // oj 运行期据此校验入参、并供 `oj openapi` 生成 openapi.json → 前端 `ojm api` 生成 __client。
6
- const todos = {
7
- async get() {
8
- const kw = http.query.keyword;
9
- const hasKw = typeof kw === "string" && kw.length > 0;
10
- // 构造器:模糊过滤走语义 op(contains 自动转义 %/_/!),count 走聚合,方言无关。
11
- const build = () => db.table("todos").whereIf(hasKw, { field: "title", op: "contains", value: kw });
12
- try {
13
- const total = Number((await build().select([{ fn: "count", as: "c" }]).all())[0]?.c ?? 0);
14
- const rows = await build()
15
- .select(["id", "title", "done", "create_time", "update_time"])
16
- .orderBy([{ field: "id", dir: "asc" }])
17
- .all();
18
- const list = rows.map(r => ({
19
- id: Number(r.id),
20
- title: String(r.title),
21
- done: Number(r.done) === 1,
22
- }));
23
- json.ok({ list, total });
24
- }
25
- catch (e) {
26
- json.fail(500, String(e));
27
- }
28
- },
29
- };
30
-
31
- todos.get.schema = {
32
- query: {
33
- type: "object",
34
- properties: { keyword: { type: "string" } },
35
- },
36
- };
37
-
38
- // oj v0.1.72 起出参走平级 .response(.schema 只放入参)。
39
- todos.get.response = {
40
- type: "object",
41
- required: ["list", "total"],
42
- properties: {
43
- list: {
44
- type: "array",
45
- items: {
46
- type: "object",
47
- required: ["id", "title", "done"],
48
- properties: {
49
- id: { type: "number" },
50
- title: { type: "string" },
51
- done: { type: "boolean" },
52
- },
53
- },
54
- },
55
- total: { type: "number" },
56
- },
57
- };
58
-
59
- export default todos;
@@ -1,20 +0,0 @@
1
- /* eslint-disable */
2
- // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
- // AC-D15:仅供 DEV 校验动态 import,生产构建不进产物
4
- import { z } from "@oj-module/runtime";
5
-
6
- export const schemas = {
7
- "demoTodosGet": {
8
- query: z.object({
9
- "keyword": z.string().optional()
10
- }),
11
- data: z.object({
12
- "list": z.array(z.object({
13
- "id": z.number(),
14
- "title": z.string(),
15
- "done": z.boolean()
16
- })),
17
- "total": z.number()
18
- }),
19
- },
20
- };
@@ -1,89 +0,0 @@
1
- /* eslint-disable */
2
- // 生成物:ojm api 从契约生成,勿手改(改动请改契约文件后重跑 ojm api)
3
- import { ContractApiError } from "@oj-module/runtime/contract/errors";
4
- import type { ScopedRequestLike } from "@oj-module/runtime/contract/errors";
5
- import type { ModuleContext } from "@oj-module/runtime";
6
- import type { z } from "@oj-module/runtime";
7
- import type { schemas } from "./api.schemas";
8
-
9
- /** oj 信封(AC-D16):code=0 成功;非 0 时 HTTP status=code,由 toApiError 归一为 ContractApiError */
10
- interface OjEnvelope<T> { code: number, msg?: string, data?: T }
11
-
12
- let req: ScopedRequestLike | undefined;
13
-
14
- /** 线上 API 前缀(ojm api 从契约抽取,唯一真源,勿手改):请求基址 + 前缀登记用 */
15
- export const API_PREFIX = "/demo";
16
-
17
- /** 认领身份前缀(契约 apiPrefix,字面等于模块目录名):跨模块认领护栏对比基准 */
18
- export const MODULE_PREFIX = "/demo";
19
-
20
- /**
21
- * 模块 entry 的 onInit 里一行创建(构造即 install):完成 apiPrefix 登记 +
22
- * scoped request 绑定,返回绑好前缀的端点函数集合。一模块一 client;
23
- * 重复构造幂等安全(前缀重复登记同值、request 重绑同值)。
24
- */
25
- export function createDemoClient(ctx: ModuleContext) {
26
- // 跨模块认领护栏(DEV):工厂知道自己该属于谁(MODULE_PREFIX = 契约 apiPrefix),
27
- // ctx 又带 module.name——名实不符说明别的模块在替本模块创建 client,会共享 request
28
- // 单槽、卸载互相耦合;跨模块需求应由本模块创建后经 provider 暴露。
29
- // 注意对比基准是 MODULE_PREFIX(目录名)而非 API_PREFIX——D-M8 映射时线上前缀
30
- // 属于别的命名空间,认领身份仍按目录名。
31
- if (import.meta.env.DEV && ctx.module.name !== MODULE_PREFIX.slice(1)) {
32
- console.warn(`[ojm-api] 模块 "${ctx.module.name}" 正在认领 ${MODULE_PREFIX} 的 client——跨模块认领会共享 request 单槽、卸载互相耦合;应由 "${MODULE_PREFIX.slice(1)}" 模块在自身 onInit 创建,跨模块需求经 provider 暴露。`);
33
- }
34
- ctx.register.apiPrefix(API_PREFIX);
35
- bindRequest(ctx.utils.request);
36
- return { demoTodosGet };
37
- }
38
-
39
- /**
40
- * @deprecated 请改用 createDemoClient(ctx)——工厂构造即完成
41
- * 前缀登记 + request 绑定,不会出现「登记了前缀没绑 request」的半接线状态。
42
- */
43
- export function bindRequest(r: ScopedRequestLike): void {
44
- req = r;
45
- }
46
-
47
- function ensureReq(): ScopedRequestLike {
48
- if (!req)
49
- throw new ContractApiError(-1, "[ojm-api] 请求未绑定——请在模块 entry.ts 的 onInit 里调用 createDemoClient(ctx)(或 bindRequest(ctx.utils.request))。");
50
- return req;
51
- }
52
-
53
- /** ky HTTPError → ContractApiError(错误体为信封时取 code/msg);契约违例与原错误原样透传 */
54
- async function toApiError(e: unknown): Promise<unknown> {
55
- if (e instanceof ContractApiError)
56
- return e;
57
- // ky 2.x 构造 HTTPError 时已用 getReader() 消费掉原 response 体(res.clone().json() 按规范抛 TypeError,
58
- // 体方法一律失效),解析好的错误体在公开字段 error.data——故读 error.data,勿改回 clone().json()。
59
- const err = e as { data?: { code?: unknown, msg?: string }, response?: Response } | null;
60
- const res = err?.response;
61
- const d = err?.data;
62
- if (res instanceof Response && d && typeof d.code === "number")
63
- return new ContractApiError(d.code, d.msg ?? res.statusText);
64
- return e;
65
- }
66
-
67
- export type DemoTodosGetQuery = z.input<(typeof schemas)["demoTodosGet"]["query"]>;
68
- export type DemoTodosGetData = z.infer<(typeof schemas)["demoTodosGet"]["data"]>;
69
-
70
- export async function demoTodosGet(query: DemoTodosGetQuery): Promise<DemoTodosGetData> {
71
- const client = ensureReq();
72
- try {
73
- const env = await client.get(`demo/todos`, { searchParams: query as Record<string, string | number | boolean> }).json<OjEnvelope<DemoTodosGetData>>();
74
- // 2xx + code!==0 也是业务错误(§6.2 通道 a):oj 不会这么发,但契约机制的价值恰是防漂移
75
- if (typeof env.code === "number" && env.code !== 0)
76
- throw new ContractApiError(env.code, env.msg ?? "业务错误(信封 code 非 0)");
77
- const data = env.data as DemoTodosGetData;
78
- if (import.meta.env.DEV) {
79
- const { schemas } = await import("./api.schemas");
80
- const r = schemas["demoTodosGet"].data.safeParse(data);
81
- if (!r.success)
82
- throw new ContractApiError(-1, `[契约违例] demoTodosGet 响应与契约不符:${r.error.issues.map(i => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`);
83
- }
84
- return data;
85
- }
86
- catch (e) {
87
- throw await toApiError(e);
88
- }
89
- }
@@ -1,42 +0,0 @@
1
- import { HomeOutlined } from "@ant-design/icons";
2
- import { defineModule } from "@oj-module/runtime";
3
- import { createElement } from "react";
4
-
5
- import DemoPage from "./pages/index";
6
-
7
- /**
8
- * demo 前端模块:本文件是模块元数据唯一来源(name/version/routes/i18n)。
9
- * 只允许 import 三类东西:runtime、共享依赖(宿主 importmap 提供)、自身相对路径。
10
- */
11
- export default defineModule({
12
- name: "demo",
13
- description: "uni-dev 演示模块",
14
- version: "0.1.0",
15
- peerRuntime: ">=0.0.0",
16
- routes: [
17
- {
18
- path: "/demo",
19
- handle: {
20
- layout: "container",
21
- order: 100,
22
- title: "demo:menu.demo",
23
- icon: createElement(HomeOutlined),
24
- },
25
- children: [
26
- {
27
- index: true,
28
- Component: DemoPage,
29
- handle: {
30
- title: "demo:menu.demo",
31
- icon: createElement(HomeOutlined),
32
- keepAlive: true,
33
- },
34
- },
35
- ],
36
- },
37
- ],
38
- i18n: {
39
- "zh-CN": () => import("./locales/zh-CN.json"),
40
- "en-US": () => import("./locales/en-US.json"),
41
- },
42
- });
@@ -1,10 +0,0 @@
1
- {
2
- "menu": {
3
- "demo": "Demo"
4
- },
5
- "page": {
6
- "title": "uni-dev Demo",
7
- "loaded": "Module loaded",
8
- "hello": "Backend says"
9
- }
10
- }
@@ -1,10 +0,0 @@
1
- {
2
- "menu": {
3
- "demo": "演示模块"
4
- },
5
- "page": {
6
- "title": "uni-dev 演示",
7
- "loaded": "模块加载成功",
8
- "hello": "后端响应"
9
- }
10
- }
@@ -1,45 +0,0 @@
1
- import { BasicContent, useAuthStore } from "@oj-module/runtime";
2
- import { Card, Tag, Typography } from "antd";
3
- import { useEffect, useState } from "react";
4
- import { useTranslation } from "react-i18next";
5
-
6
- /**
7
- * 演示页:调后端 /api/web/hello(oj dev/ts 热更,改 api/src 保存即生效)。
8
- * 业务侧直接消费 oj 原生信封 {code:0,data};登录链信封由 runtime 适配(D10)。
9
- *
10
- * 注意(集中审阅 F6 评估结论):这里用裸 fetch 是有意的 demo 简化——
11
- * runtime 不向页面组件暴露请求客户端(scoped client `ctx.utils.request`
12
- * 只在模块生命周期钩子可用),真实业务模块请把请求收敛到模块自己的
13
- * api/ 层再封装,不要在页面里散落 fetch。
14
- */
15
- export default function DemoPage() {
16
- const { t } = useTranslation();
17
- const token = useAuthStore(state => state.token);
18
- const [hello, setHello] = useState<string>("");
19
-
20
- useEffect(() => {
21
- const controller = new AbortController();
22
- fetch("/api/web/hello", { headers: { Authorization: `Bearer ${token}` }, signal: controller.signal })
23
- .then(res => res.json())
24
- .then((body: { data?: { message?: string } }) => setHello(body?.data?.message ?? ""))
25
- .catch(() => setHello(""));
26
- return () => controller.abort();
27
- }, [token]);
28
-
29
- return (
30
- <BasicContent>
31
- <Card title={t("demo:page.title")}>
32
- <Tag color="success">{t("demo:page.loaded")}</Tag>
33
- {hello
34
- ? (
35
- <Typography.Paragraph>
36
- {t("demo:page.hello")}
37
- {": "}
38
- <Typography.Text code>{hello}</Typography.Text>
39
- </Typography.Paragraph>
40
- )
41
- : null}
42
- </Card>
43
- </BasicContent>
44
- );
45
- }
@@ -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;