@oj-module/cli 0.2.69 → 0.2.71
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/README.md +10 -8
- package/devkit/framework-dev.md +1 -1
- package/devkit/manual.md +45 -19
- package/devkit/module-dev-and-cdn-playground.md +20 -14
- package/devkit/publish-walkthrough.md +132 -43
- package/devkit/wecom-customer-reauth-guide.md +110 -0
- package/package.json +2 -2
- package/shell-dist/assets/{runtime.zrv2zhj9j0.js → runtime.7x99hmm020.js} +2 -2
- package/shell-dist/bundles/oj-market-0.2.0.tgz +0 -0
- package/shell-dist/bundles/oj-market-0.2.0.tgz.sig +8 -0
- package/shell-dist/index.html +4 -4
- package/shell-dist/versions.json +3 -3
- package/src/__tests__/list.test.ts +7 -7
- package/src/__tests__/oj-health-host.test.ts +19 -0
- package/src/__tests__/site-tenant-header.test.ts +2 -2
- package/src/__tests__/templates-builtin-list.test.ts +47 -0
- package/src/__tests__/templates-schema-columns.test.ts +11 -2
- package/src/__tests__/templates-tenant-scope.test.ts +2 -3
- package/src/__tests__/users-roles-display.test.ts +84 -0
- package/src/bundle.ts +1 -1
- package/src/contract/__tests__/market-client.integration.test.ts +23 -22
- package/src/enable.ts +2 -2
- package/src/init.ts +45 -0
- package/src/install.ts +5 -5
- package/src/lifecycle/__tests__/install.test.ts +29 -29
- package/src/lifecycle/__tests__/market-bundle.test.ts +87 -0
- package/src/lifecycle/__tests__/publish.test.ts +8 -10
- package/src/lifecycle/__tests__/trust.test.ts +14 -14
- package/src/lifecycle/__tests__/uninstall.test.ts +4 -4
- package/src/list.ts +3 -3
- package/src/market/__tests__/bootstrap.test.ts +24 -7
- package/src/market/__tests__/failure-playbook.test.ts +0 -1
- package/src/market/__tests__/remote-push.test.ts +228 -0
- package/src/market/__tests__/verify.test.ts +45 -207
- package/src/market/bootstrap.ts +12 -7
- package/src/market/command.ts +33 -35
- package/src/market/db-seed.ts +129 -0
- package/src/market/failure-playbook.ts +3 -9
- package/src/market/fixture.ts +2 -6
- package/src/market/publish.ts +145 -89
- package/src/market/remote-push.ts +236 -0
- package/src/market/trial.ts +2 -2
- package/src/market/verify.ts +17 -270
- package/src/market/walkthrough.ts +38 -63
- package/src/oj.ts +13 -2
- package/src/preview.ts +5 -3
- package/src/publish.ts +3 -2
- package/src/scaffold-guard.ts +4 -0
- package/src/site.ts +1 -1
- package/src/trust.ts +8 -8
- package/src/uninstall.ts +2 -2
- package/src/usage.ts +23 -21
- package/templates/config.yaml +22 -3
- package/templates/init.include.yaml +16 -11
- package/templates/openapi.json +3948 -3655
- package/templates/src/.ojm-builtin.json +13 -0
- package/templates/src/README.md +5 -2
- package/templates/src/app/api/_platform/README.md +3 -2
- package/templates/src/app/api/_platform/_lifecycle/atomic.ts +7 -2
- package/templates/src/app/api/_platform/_lifecycle/bundle-converge.ts +158 -0
- package/templates/src/app/api/_platform/_lifecycle/history.ts +3 -2
- package/templates/src/app/api/_platform/_lifecycle/installer.ts +25 -5
- package/templates/src/app/api/_platform/_lifecycle/local-archive.ts +61 -0
- package/templates/src/app/api/_platform/_lifecycle/manifest-of-upload.ts +62 -0
- package/templates/src/app/api/_platform/_lifecycle/steps/preflight.ts +12 -2
- package/templates/src/app/api/_platform/_lifecycle/steps/reload.ts +4 -1
- package/templates/src/app/api/_platform/_lifecycle/steps/smoke.ts +8 -2
- package/templates/src/app/api/_platform/_lifecycle/types.ts +1 -1
- package/templates/src/app/api/_platform/_lifecycle/uninstall.ts +14 -5
- package/templates/src/app/api/_platform/_shared/site-facade.ts +31 -0
- package/templates/src/app/api/_platform/manifest.yaml +1 -2
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0003__market_catalog.sql +18 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.mysql.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.pg.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0004__market_drop_source_seq.sql +6 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.mysql.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.pg.sql +8 -0
- package/templates/src/app/api/_platform/migrations/0005__market_sig_registry.sql +8 -0
- package/templates/src/app/api/_platform/schema.yaml +8 -11
- package/templates/src/app/api/_platform/seed.mysql.sql +1 -1
- package/templates/src/app/api/_platform/seed.pg.sql +1 -1
- package/templates/src/app/api/_platform/seed.sql +1 -1
- package/templates/src/app/api/auth/_shared/wecom/sync.ts +6 -0
- package/templates/src/app/web/_platform/__client/api.schemas.ts +0 -114
- package/templates/src/app/web/_platform/__client/index.ts +1 -333
- package/templates/src/app/web/system/locales/en-US.json +15 -1
- package/templates/src/app/web/system/locales/zh-CN.json +15 -1
- package/templates/src/app/web/system/pages/departments/sync-bar.tsx +9 -3
- package/templates/src/app/web/system/pages/roles/index.tsx +4 -8
- package/templates/src/app/web/system/pages/roles/role-label.ts +26 -0
- package/templates/src/app/web/system/pages/solutions/index.tsx +110 -0
- package/templates/src/app/web/system/pages/solutions/market-entry.ts +46 -0
- package/templates/src/app/web/system/pages/users/index.tsx +12 -2
- package/templates/src/app/web/system/pages/users/match-keyword.ts +19 -0
- package/templates/src/app/web/system/pages/users/roles.tsx +43 -14
- package/templates/src/app/web/system/routes.ts +18 -1
- package/templates/src/{app/api/_platform/market → oj-market/api}/api.ts +475 -592
- package/templates/src/oj-market/api/backfill.ts +221 -0
- package/templates/src/{app/api/_platform/market → oj-market/api}/bootstrap.ts +2 -2
- package/templates/src/oj-market/api/cdn/api.ts +312 -0
- package/templates/src/oj-market/api/cdn/shared.ts +104 -0
- package/templates/src/oj-market/api/cdn/signed-auth.ts +75 -0
- package/templates/src/oj-market/api/manifest.yaml +8 -0
- package/templates/src/oj-market/solution.yaml +21 -0
- package/templates/src/oj-market/web/__client/api.schemas.ts +167 -0
- package/templates/src/oj-market/web/__client/index.ts +509 -0
- package/templates/src/oj-market/web/__tests__/marketClient.fake.ts +6 -6
- package/templates/src/oj-market/web/api/client.ts +6 -6
- package/templates/src/oj-market/web/api/defaultClient.ts +31 -25
- package/templates/src/oj-market/web/entry.ts +5 -3
- package/templates/src/oj-market/web/hooks/useMarket.ts +6 -6
- package/templates/src/oj-market/web/pages/MarketApp.tsx +20 -3
- package/templates/src/oj-market/web/publish/locales/en-US.json +1 -1
- package/templates/src/oj-market/web/publish/locales/zh-CN.json +1 -1
- package/templates/src/oj-market/web/publish/pages/history.tsx +2 -11
- package/templates/src/oj-market/web/routes.ts +12 -0
- package/templates/src/oj-market/web/types.ts +14 -8
- package/templates/tests/lifecycle/anonymous-wiring.spec.ts +65 -0
- package/templates/tests/lifecycle/backfill.spec.ts +392 -0
- package/templates/tests/lifecycle/bootstrap.spec.ts +1 -1
- package/templates/tests/lifecycle/bundle-converge.spec.ts +264 -0
- package/templates/tests/lifecycle/cdn-upload.spec.ts +661 -0
- package/templates/tests/lifecycle/deploy-smoke.spec.ts +25 -0
- package/templates/tests/lifecycle/exec-grant.spec.ts +38 -0
- package/templates/tests/lifecycle/installer.spec.ts +36 -0
- package/templates/tests/lifecycle/market-api.spec.ts +448 -803
- package/templates/tests/lifecycle/preflight.spec.ts +18 -0
- package/templates/tests/lifecycle/trust.spec.ts +2 -2
- package/templates/tests/lifecycle/uninstall.spec.ts +36 -2
- package/templates/tests/lifecycle/verify-checks.spec.ts +1 -1
- package/templates/tests/solutions-entry.spec.ts +55 -0
- package/templates/tsconfig.paths.json +1 -0
- package/templates/web.config.ts +6 -6
- package/vendor/host-versions.json +3 -3
- package/bin/oj +0 -4
- package/src/market/__tests__/build-index.test.ts +0 -641
- package/src/market/__tests__/sharding.test.ts +0 -90
- package/src/market/__tests__/yank-ledger.test.ts +0 -60
- package/src/market/build-index.ts +0 -526
- package/src/market/index-builder.ts +0 -149
- package/src/market/sharding.ts +0 -94
- package/src/market/yank-ledger.ts +0 -91
- package/templates/src/app/api/_platform/_lifecycle/source-seq.ts +0 -88
- package/templates/src/app/api/_platform/market/index-source.ts +0 -112
- package/templates/tests/lifecycle/source-seq.spec.ts +0 -55
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
"oplog": "操作日志",
|
|
8
8
|
"settings": "站点设置",
|
|
9
9
|
"wecom": "企微集成",
|
|
10
|
-
"departments": "组织架构"
|
|
10
|
+
"departments": "组织架构",
|
|
11
|
+
"solutions": "方案安装"
|
|
11
12
|
},
|
|
12
13
|
"common": {
|
|
13
14
|
"confirm": "确认",
|
|
@@ -40,6 +41,7 @@
|
|
|
40
41
|
},
|
|
41
42
|
"mainDept": "主部门",
|
|
42
43
|
"filterDept": "按部门筛选(含子部门)",
|
|
44
|
+
"searchPlaceholder": "按用户名/显示名搜索",
|
|
43
45
|
"password": "口令",
|
|
44
46
|
"usernameHint": "ldap:/ sso:/ wecom: 为外部身份源保留"
|
|
45
47
|
},
|
|
@@ -129,6 +131,18 @@
|
|
|
129
131
|
"secretHint": "敏感项不可在线修改",
|
|
130
132
|
"saved": "设置已保存"
|
|
131
133
|
},
|
|
134
|
+
"solutionsPage": {
|
|
135
|
+
"title": "方案市场(oj-market)",
|
|
136
|
+
"absentTitle": "站点目录中暂无方案市场安装包",
|
|
137
|
+
"absentHint": "出厂未装态:请重新执行 ojm init --yes 补充随站分发的安装包,或由超管触发 POST /api/oj-market/backfill 回填本地目录后刷新本页。",
|
|
138
|
+
"latest": "目录最新版 {{version}}",
|
|
139
|
+
"installed": "已装 {{version}}",
|
|
140
|
+
"install": "安装",
|
|
141
|
+
"doneTitle": "安装完成",
|
|
142
|
+
"goMarket": "前往方案市场",
|
|
143
|
+
"upgradableHint": "目录有更新版本——经方案市场页内升级。",
|
|
144
|
+
"needsTrust": "发布方密钥待信任确认——首方包已预置平台信任,出现本态多为目录被第三方包占用,请核对后重试。"
|
|
145
|
+
},
|
|
132
146
|
"wecom": {
|
|
133
147
|
"title": "开通企业",
|
|
134
148
|
"hint": "点「生成开通链接」把链接或二维码发给企业管理员,由其扫码/打开授权;授权成功后本系统经 create_auth 回调自动登记该企业(落库 wecom_app_profile)。链接由企微 get_customized_auth_url 生成,有效期以企微返回为准(可达 10 天),过期后二维码置灰,可重新生成。",
|
|
@@ -37,6 +37,12 @@ const KEY = ["system", "wecom-sync"] as const;
|
|
|
37
37
|
|
|
38
38
|
/** 单片推完到下一次 start 的最小间隔:后端限速 10 次/分,串行驱动别自己撞 429 */
|
|
39
39
|
const MIN_GAP_MS = 6000;
|
|
40
|
+
/**
|
|
41
|
+
* 同步请求超时:单条 `sync/start` 处理一片(预算 12s + 单次上游软超时 5s,最坏 ~17s),
|
|
42
|
+
* 远长于运行时 `request` 的默认 10s。不单独放宽,前端必然在「片还没推完」时先抛
|
|
43
|
+
* `Request timed out`。30s 与服务器默认路由超时对齐,留足余量。
|
|
44
|
+
*/
|
|
45
|
+
const SYNC_TIMEOUT_MS = 30_000;
|
|
40
46
|
/** 一轮点击最多推多少片(12 秒/片 ≈ 12 分钟):兜底防呆,正常企业两三次就到底 */
|
|
41
47
|
const MAX_SLICES = 60;
|
|
42
48
|
|
|
@@ -61,21 +67,21 @@ function remember(view: SyncView): SyncView {
|
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
async function fetchStatus(): Promise<SyncView> {
|
|
64
|
-
const env = await request.get(withAgent("auth/wecom/sync/status")).json<{ code: number, data?: SyncView }>();
|
|
70
|
+
const env = await request.get(withAgent("auth/wecom/sync/status"), { timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
|
|
65
71
|
if (!env.data)
|
|
66
72
|
throw new Error("企业微信同步状态读取失败");
|
|
67
73
|
return remember(env.data);
|
|
68
74
|
}
|
|
69
75
|
|
|
70
76
|
async function pushSlice(): Promise<SyncView> {
|
|
71
|
-
const env = await request.post("auth/wecom/sync/start", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
|
|
77
|
+
const env = await request.post("auth/wecom/sync/start", { json: agentBody(), timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
|
|
72
78
|
if (!env.data)
|
|
73
79
|
throw new Error("企业微信同步未能启动");
|
|
74
80
|
return remember(env.data);
|
|
75
81
|
}
|
|
76
82
|
|
|
77
83
|
async function cancelSync(): Promise<SyncView> {
|
|
78
|
-
const env = await request.post("auth/wecom/sync/cancel", { json: agentBody() }).json<{ code: number, data?: SyncView }>();
|
|
84
|
+
const env = await request.post("auth/wecom/sync/cancel", { json: agentBody(), timeout: SYNC_TIMEOUT_MS }).json<{ code: number, data?: SyncView }>();
|
|
79
85
|
if (!env.data)
|
|
80
86
|
throw new Error("取消同步失败");
|
|
81
87
|
return remember(env.data);
|
|
@@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
|
|
|
7
7
|
import { systemPermissionsGet, systemRolesGet } from "../../__client";
|
|
8
8
|
import { isForbidden } from "../errors";
|
|
9
9
|
import { ForbiddenPanel } from "../forbidden";
|
|
10
|
+
import { roleLabel } from "./role-label";
|
|
10
11
|
import { visibleRoles } from "./visible-roles";
|
|
11
12
|
|
|
12
13
|
type RoleRow = SystemRolesGetData[number];
|
|
@@ -41,16 +42,11 @@ export default function RolesPage() {
|
|
|
41
42
|
{
|
|
42
43
|
// R2:solution 角色带 title_key → 按当前语言实时翻译,title(安装期解析文本)兜底;
|
|
43
44
|
// 内置角色(builtin)的 title 是种子里的站点默认语言文本,按角色名再兜一层 locale;
|
|
44
|
-
// custom 角色无 key 原样显示 title(对齐下方权限点 title_key
|
|
45
|
+
// custom 角色无 key 原样显示 title(对齐下方权限点 title_key 的渲染先例)。
|
|
46
|
+
// 渲染口径与成员资格页共用 roleLabel(pages/roles/role-label.ts),改一处两处生效。
|
|
45
47
|
title: t("system:rolesPage.columns.title"),
|
|
46
48
|
dataIndex: "title",
|
|
47
|
-
render: (_: string, row: RoleRow) =>
|
|
48
|
-
if (row.title_key)
|
|
49
|
-
return t(row.title_key, { defaultValue: row.title ?? "" });
|
|
50
|
-
return row.builtin
|
|
51
|
-
? t(`system:rolesPage.roleTitle.${row.name}`, { defaultValue: row.title ?? row.name })
|
|
52
|
-
: row.title;
|
|
53
|
-
},
|
|
49
|
+
render: (_: string, row: RoleRow) => roleLabel(row, t),
|
|
54
50
|
},
|
|
55
51
|
{ title: t("system:rolesPage.holders"), dataIndex: "holders" },
|
|
56
52
|
{
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 角色语言名解析(纯函数):角色页(roles/index.tsx R2 先例)与成员资格页
|
|
3
|
+
* (users/roles.tsx)共用的显示名口径,BDD 用例见 cli __tests__/users-roles-display.test.ts。
|
|
4
|
+
*
|
|
5
|
+
* 回退链(与角色页表格列渲染逐字一致):
|
|
6
|
+
* title_key(solution 角色,跨模块 namespace)→ t 译文,缺译文回退 title;
|
|
7
|
+
* 内置角色(builtin)→ `system:rolesPage.roleTitle.<name>` locale 键,缺键回退 title → name;
|
|
8
|
+
* custom 角色 → 原样 title,title 空回退 name。
|
|
9
|
+
*/
|
|
10
|
+
export interface RoleLabelRow {
|
|
11
|
+
name: string
|
|
12
|
+
title: string | null
|
|
13
|
+
title_key: string | null
|
|
14
|
+
builtin: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** i18next t 的最小消费面(TFunction 兼容此签名) */
|
|
18
|
+
type Translate = (key: string, options?: { defaultValue?: string }) => string;
|
|
19
|
+
|
|
20
|
+
export function roleLabel(row: RoleLabelRow, t: Translate): string {
|
|
21
|
+
if (row.title_key)
|
|
22
|
+
return t(row.title_key, { defaultValue: row.title ?? "" });
|
|
23
|
+
return row.builtin
|
|
24
|
+
? t(`system:rolesPage.roleTitle.${row.name}`, { defaultValue: row.title ?? row.name })
|
|
25
|
+
: (row.title ?? row.name);
|
|
26
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { SolutionsRow, TaskLike } from "./market-entry";
|
|
2
|
+
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { App, Button, Card, Progress, Result, Space, Tag } from "antd";
|
|
4
|
+
import { useEffect, useState } from "react";
|
|
5
|
+
import { useTranslation } from "react-i18next";
|
|
6
|
+
|
|
7
|
+
import { Link } from "react-router";
|
|
8
|
+
import { ojMarketIndexGet, ojMarketInstallPost, ojMarketTaskGet } from "../../../../../oj-market/web/__client";
|
|
9
|
+
import { explainError, isForbidden } from "../errors";
|
|
10
|
+
import { ForbiddenPanel } from "../forbidden";
|
|
11
|
+
import { marketEntryOf, taskOutcomeOf } from "./market-entry";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 方案安装入口(Phase B T3,superadmin):市场 UI 出厂未装(纯 web 方案包),
|
|
15
|
+
* 首装经此页走既有平台编排面 POST /api/oj-market/install(api 随脚手架内置,R29)。
|
|
16
|
+
* 状态推导与任务终态映射见 ./market-entry(纯函数,tests/solutions-entry.spec 钉)。
|
|
17
|
+
*/
|
|
18
|
+
export default function SolutionsPage() {
|
|
19
|
+
const { t } = useTranslation();
|
|
20
|
+
const { message } = App.useApp();
|
|
21
|
+
const queryClient = useQueryClient();
|
|
22
|
+
const [taskId, setTaskId] = useState<string | null>(null);
|
|
23
|
+
|
|
24
|
+
const query = useQuery({
|
|
25
|
+
queryKey: ["system", "solutions"],
|
|
26
|
+
// size 上限 200(生成契约 max);首方目录规模小,翻页边角不在本页处理
|
|
27
|
+
queryFn: () => ojMarketIndexGet({ page: 1, size: 200 }),
|
|
28
|
+
});
|
|
29
|
+
const entry = marketEntryOf((query.data?.list ?? []) as unknown as SolutionsRow[]);
|
|
30
|
+
|
|
31
|
+
const taskQuery = useQuery({
|
|
32
|
+
queryKey: ["system", "solutions-task", taskId],
|
|
33
|
+
enabled: taskId !== null,
|
|
34
|
+
refetchInterval: q => (q.state.data && taskOutcomeOf(q.state.data as unknown as TaskLike).kind !== "running" ? false : 800),
|
|
35
|
+
queryFn: () => ojMarketTaskGet({ id: taskId! }),
|
|
36
|
+
});
|
|
37
|
+
const outcome = taskQuery.data ? taskOutcomeOf(taskQuery.data as unknown as TaskLike) : null;
|
|
38
|
+
|
|
39
|
+
// 终态后刷新目录行:安装完成 → 卡片翻「已装」态(含取消/失败也刷,状态以服务端为准)
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (outcome !== null && outcome.kind !== "running")
|
|
42
|
+
void queryClient.invalidateQueries({ queryKey: ["system", "solutions"] });
|
|
43
|
+
}, [outcome, queryClient]);
|
|
44
|
+
|
|
45
|
+
const installMut = useMutation({
|
|
46
|
+
mutationFn: () => ojMarketInstallPost({ source: "cdn", name: "oj-market" }),
|
|
47
|
+
onSuccess: r => setTaskId(r.task_id ?? null),
|
|
48
|
+
onError: e => message.error(explainError(e, t)),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
if (query.error && isForbidden(query.error))
|
|
52
|
+
return <ForbiddenPanel t={t} error={query.error} />;
|
|
53
|
+
|
|
54
|
+
if (entry.kind === "absent") {
|
|
55
|
+
return (
|
|
56
|
+
<Result
|
|
57
|
+
status="warning"
|
|
58
|
+
title={t("system:solutionsPage.absentTitle")}
|
|
59
|
+
subTitle={t("system:solutionsPage.absentHint")}
|
|
60
|
+
style={{ paddingTop: 48 }}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<Space direction="vertical" style={{ width: "100%", paddingTop: 16 }}>
|
|
67
|
+
<Card title={t("system:solutionsPage.title")}>
|
|
68
|
+
<Space direction="vertical" style={{ width: "100%" }}>
|
|
69
|
+
<Space wrap>
|
|
70
|
+
<Tag color="blue">oj-market</Tag>
|
|
71
|
+
{entry.kind === "installable" && <Tag>{t("system:solutionsPage.latest", { version: entry.latest })}</Tag>}
|
|
72
|
+
{entry.kind === "installed" && <Tag color="green">{t("system:solutionsPage.installed", { version: entry.installed })}</Tag>}
|
|
73
|
+
{entry.kind === "installed" && entry.latest !== null && entry.latest !== entry.installed && (
|
|
74
|
+
<Tag color="orange">{t("system:solutionsPage.latest", { version: entry.latest })}</Tag>
|
|
75
|
+
)}
|
|
76
|
+
</Space>
|
|
77
|
+
{entry.kind === "installable" && outcome?.kind !== "done" && (
|
|
78
|
+
<>
|
|
79
|
+
<Button type="primary" loading={installMut.isPending || outcome?.kind === "running"} onClick={() => installMut.mutate()}>
|
|
80
|
+
{t("system:solutionsPage.install")}
|
|
81
|
+
</Button>
|
|
82
|
+
{outcome?.kind === "running" && (
|
|
83
|
+
<>
|
|
84
|
+
<Progress percent={outcome.progress} size="small" style={{ maxWidth: 480 }} />
|
|
85
|
+
<span>{outcome.message}</span>
|
|
86
|
+
</>
|
|
87
|
+
)}
|
|
88
|
+
{outcome?.kind === "failed" && <span style={{ color: "#cf1322" }}>{outcome.message}</span>}
|
|
89
|
+
{outcome?.kind === "needsTrust" && <span style={{ color: "#d48806" }}>{t("system:solutionsPage.needsTrust")}</span>}
|
|
90
|
+
</>
|
|
91
|
+
)}
|
|
92
|
+
{outcome?.kind === "done" && (
|
|
93
|
+
<Result
|
|
94
|
+
status="success"
|
|
95
|
+
title={t("system:solutionsPage.doneTitle")}
|
|
96
|
+
extra={<Link to="/oj-market"><Button type="primary">{t("system:solutionsPage.goMarket")}</Button></Link>}
|
|
97
|
+
style={{ padding: "16px 0" }}
|
|
98
|
+
/>
|
|
99
|
+
)}
|
|
100
|
+
{entry.kind === "installed" && (
|
|
101
|
+
<Space>
|
|
102
|
+
<Link to="/oj-market"><Button type="primary">{t("system:solutionsPage.goMarket")}</Button></Link>
|
|
103
|
+
{entry.upgradable && <span>{t("system:solutionsPage.upgradableHint")}</span>}
|
|
104
|
+
</Space>
|
|
105
|
+
)}
|
|
106
|
+
</Space>
|
|
107
|
+
</Card>
|
|
108
|
+
</Space>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// 方案安装入口的纯推导(Phase B T3):页面只做 query + 渲染,状态口径收这里,
|
|
2
|
+
// 与 roles/visible-roles 同范式(展示层逻辑可单测,不入 React 组件)。
|
|
3
|
+
// 数据源:GET /api/oj-market/index 的行(superadmin;R29 第一方编排面,平台内置)。
|
|
4
|
+
|
|
5
|
+
/** index 行中本入口消费的最小形状(完整行见 oj-market api IndexRow,catchall 契约) */
|
|
6
|
+
export interface SolutionsRow {
|
|
7
|
+
name: string
|
|
8
|
+
latest: string | null
|
|
9
|
+
installed: string | null
|
|
10
|
+
upgradable: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type MarketEntry = { kind: "absent" } | { kind: "installable", latest: string } | { kind: "installed", installed: string, latest: string | null, upgradable: boolean };
|
|
14
|
+
|
|
15
|
+
/** 市场包入口状态:目录无行(出厂未装且未回填)/ 可安装 / 已装(含可升级位) */
|
|
16
|
+
export function marketEntryOf(list: SolutionsRow[]): MarketEntry {
|
|
17
|
+
const row = list.find(r => r.name === "oj-market");
|
|
18
|
+
if (row === undefined)
|
|
19
|
+
return { kind: "absent" };
|
|
20
|
+
if (row.installed === null)
|
|
21
|
+
return row.latest === null ? { kind: "absent" } : { kind: "installable", latest: row.latest };
|
|
22
|
+
return { kind: "installed", installed: row.installed, latest: row.latest, upgradable: row.upgradable };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/** 任务记录的最小消费形状(TaskRecord 真源:api/_platform/_lifecycle/types.ts) */
|
|
26
|
+
export interface TaskLike {
|
|
27
|
+
status: string
|
|
28
|
+
progress: number
|
|
29
|
+
message: string
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type TaskOutcome = { kind: "running", progress: number, message: string } | { kind: "done" } | { kind: "needsTrust" } | { kind: "failed", message: string };
|
|
33
|
+
|
|
34
|
+
/** 任务终态 → 入口渲染口径:needs_trust 是非成功态(TOFU 确认前不得绿) */
|
|
35
|
+
export function taskOutcomeOf(rec: TaskLike): TaskOutcome {
|
|
36
|
+
switch (rec.status) {
|
|
37
|
+
case "running":
|
|
38
|
+
return { kind: "running", progress: rec.progress, message: rec.message };
|
|
39
|
+
case "done":
|
|
40
|
+
return { kind: "done" };
|
|
41
|
+
case "needs_trust":
|
|
42
|
+
return { kind: "needsTrust" };
|
|
43
|
+
default:
|
|
44
|
+
return { kind: "failed", message: rec.message };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -14,6 +14,7 @@ import { buildTreeData, subtreeIds } from "../departments/tree";
|
|
|
14
14
|
import { explainError, isForbidden } from "../errors";
|
|
15
15
|
import { ForbiddenPanel } from "../forbidden";
|
|
16
16
|
import { canManageUser } from "./can-manage";
|
|
17
|
+
import { matchUserKeyword } from "./match-keyword";
|
|
17
18
|
|
|
18
19
|
type UserRow = SystemUsersGetData[number];
|
|
19
20
|
|
|
@@ -42,6 +43,7 @@ export default function UsersPage() {
|
|
|
42
43
|
const [pwdTarget, setPwdTarget] = useState<UserRow | null>(null);
|
|
43
44
|
const [pwdForm] = Form.useForm<{ password: string }>();
|
|
44
45
|
const [deptId, setDeptId] = useState<number | null>(null);
|
|
46
|
+
const [keyword, setKeyword] = useState("");
|
|
45
47
|
|
|
46
48
|
const usersQuery = useQuery({ queryKey: ["system", "users"], queryFn: systemUsersGet });
|
|
47
49
|
// 与部门页共用同一份缓存键:那边改完部门,这边的筛选树也跟着新
|
|
@@ -83,8 +85,9 @@ export default function UsersPage() {
|
|
|
83
85
|
const scope = useMemo(() => (deptId == null ? null : subtreeIds(deptRows, [deptId])), [deptRows, deptId]);
|
|
84
86
|
const visibleUsers = useMemo(() => {
|
|
85
87
|
const all = usersQuery.data ?? [];
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
const scoped = scope ? all.filter(u => u.departments.some(d => scope.has(d.id))) : all;
|
|
89
|
+
return scoped.filter(u => matchUserKeyword(u, keyword));
|
|
90
|
+
}, [usersQuery.data, scope, keyword]);
|
|
88
91
|
|
|
89
92
|
if (usersQuery.error && isForbidden(usersQuery.error))
|
|
90
93
|
return <ForbiddenPanel t={t} error={usersQuery.error} />;
|
|
@@ -158,6 +161,13 @@ export default function UsersPage() {
|
|
|
158
161
|
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
|
159
162
|
{t("system:users.create")}
|
|
160
163
|
</Button>
|
|
164
|
+
<Input
|
|
165
|
+
allowClear
|
|
166
|
+
style={{ width: 220 }}
|
|
167
|
+
placeholder={t("system:users.searchPlaceholder")}
|
|
168
|
+
value={keyword}
|
|
169
|
+
onChange={e => setKeyword(e.target.value)}
|
|
170
|
+
/>
|
|
161
171
|
<TreeSelect
|
|
162
172
|
allowClear
|
|
163
173
|
showSearch
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 用户列表关键词搜索(纯函数,BDD 用例见 cli __tests__/users-roles-display.test.ts)。
|
|
3
|
+
*
|
|
4
|
+
* 口径(20261106-1811 计划文档):users 表无 nickname 列,「昵称与 display」按
|
|
5
|
+
* username(账号)+ display_name(显示名/昵称)匹配,大小写不敏感;空白关键字 = 全可见。
|
|
6
|
+
* 名单一页全量,过滤在前端做(与部门筛选同口径)。
|
|
7
|
+
*/
|
|
8
|
+
export interface UserKeywordRow {
|
|
9
|
+
username: string
|
|
10
|
+
display_name: string | null
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function matchUserKeyword(row: UserKeywordRow, keyword: string): boolean {
|
|
14
|
+
const kw = keyword.trim().toLowerCase();
|
|
15
|
+
if (!kw)
|
|
16
|
+
return true;
|
|
17
|
+
const tail = row.display_name ?? "";
|
|
18
|
+
return row.username.toLowerCase().includes(kw) || tail.toLowerCase().includes(kw);
|
|
19
|
+
}
|
|
@@ -1,14 +1,16 @@
|
|
|
1
|
+
import type { SystemRolesGetData } from "../../__client";
|
|
1
2
|
import { useUserStore } from "@oj-module/runtime";
|
|
2
3
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
3
|
-
import { App, Button, Card, Checkbox, Space } from "antd";
|
|
4
|
-
import { useState } from "react";
|
|
4
|
+
import { App, Button, Card, Checkbox, Divider, Space, Typography } from "antd";
|
|
5
|
+
import { useMemo, useState } from "react";
|
|
6
|
+
|
|
5
7
|
import { useTranslation } from "react-i18next";
|
|
6
8
|
|
|
7
9
|
import { useNavigate, useParams } from "react-router";
|
|
8
|
-
|
|
9
10
|
import { systemRolesGet, systemUsersGet, systemUsersRolesPut } from "../../__client";
|
|
10
11
|
import { explainError, isForbidden } from "../errors";
|
|
11
12
|
import { ForbiddenPanel } from "../forbidden";
|
|
13
|
+
import { roleLabel } from "../roles/role-label";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* 成员资格(AC1-7):整集替换某用户角色。后端无 GET 单用户端点,
|
|
@@ -18,6 +20,9 @@ import { ForbiddenPanel } from "../forbidden";
|
|
|
18
20
|
* 授予 admin 已下放到租户内,否则企业租户永远配不出管理员(平台 superadmin 因租户
|
|
19
21
|
* 隔离查不到企业租户的用户)。但 **superadmin 仍只许 superadmin 授**——它握着跨租户
|
|
20
22
|
* 权限,故这里对非 superadmin 直接把该选项置灰,比让他提交后吃 403 清楚。
|
|
23
|
+
*
|
|
24
|
+
* 展示(20261106-1811):角色名按语言名称(key)解析(与角色页共用 roleLabel),
|
|
25
|
+
* 并按模块分组——角色行自带 module 字段,组标题显示模块名原文。
|
|
21
26
|
*/
|
|
22
27
|
export default function UserRolesPage() {
|
|
23
28
|
const { t } = useTranslation();
|
|
@@ -35,6 +40,18 @@ export default function UserRolesPage() {
|
|
|
35
40
|
const usersQuery = useQuery({ queryKey: ["system", "users"], queryFn: systemUsersGet });
|
|
36
41
|
const rolesQuery = useQuery({ queryKey: ["system", "roles"], queryFn: systemRolesGet });
|
|
37
42
|
|
|
43
|
+
// 按模块分组(组内保持 listRoles 的 name 排序);模块名原文作组标题。
|
|
44
|
+
// 位置在提前 return 之前——hooks 不得落在条件分支里
|
|
45
|
+
const groups = useMemo(() => {
|
|
46
|
+
const byModule = new Map<string, SystemRolesGetData>();
|
|
47
|
+
for (const r of rolesQuery.data ?? []) {
|
|
48
|
+
const list = byModule.get(r.module) ?? [];
|
|
49
|
+
list.push(r);
|
|
50
|
+
byModule.set(r.module, list);
|
|
51
|
+
}
|
|
52
|
+
return [...byModule.entries()].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
53
|
+
}, [rolesQuery.data]);
|
|
54
|
+
|
|
38
55
|
const saveMut = useMutation({
|
|
39
56
|
mutationFn: (roles: string[]) => systemUsersRolesPut({ query: { uid: Number(uid) }, body: { roles } }),
|
|
40
57
|
onSuccess: () => {
|
|
@@ -55,18 +72,30 @@ export default function UserRolesPage() {
|
|
|
55
72
|
|
|
56
73
|
return (
|
|
57
74
|
<Card title={`${t("system:userRoles.title")}: ${user?.username ?? `#${uid}`}`}>
|
|
58
|
-
<Space direction="vertical">
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
75
|
+
<Space direction="vertical" style={{ width: "100%" }}>
|
|
76
|
+
{groups.map(([mod, rows]) => {
|
|
77
|
+
const names = new Set(rows.map(r => r.name));
|
|
78
|
+
return (
|
|
79
|
+
<div key={mod}>
|
|
80
|
+
<Typography.Text strong>{mod}</Typography.Text>
|
|
81
|
+
<div style={{ paddingLeft: 12, paddingTop: 4 }}>
|
|
82
|
+
<Checkbox.Group
|
|
83
|
+
options={rows.map(r => ({
|
|
84
|
+
label: `${roleLabel(r, t)}${r.deprecated ? t("system:userRoles.deprecated") : ""}`,
|
|
85
|
+
value: r.name,
|
|
86
|
+
// 非 superadmin 授不了平台级角色:置灰,提交后 403 太晚也太含糊
|
|
87
|
+
disabled: r.name === "superadmin" && !isSuper,
|
|
88
|
+
}))}
|
|
89
|
+
// 整集替换语义不变:每组只回显/改动本组名下的选中项
|
|
90
|
+
value={value.filter(v => names.has(v))}
|
|
91
|
+
onChange={vals => setSelected([...value.filter(v => !names.has(v)), ...vals])}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
})}
|
|
69
97
|
{value.length === 0 ? <span>{t("system:userRoles.none")}</span> : null}
|
|
98
|
+
<Divider style={{ margin: "8px 0" }} />
|
|
70
99
|
<Button
|
|
71
100
|
type="primary"
|
|
72
101
|
disabled={isSelf || selected === null}
|
|
@@ -4,6 +4,10 @@ import { SettingOutlined } from "@ant-design/icons";
|
|
|
4
4
|
|
|
5
5
|
import { createElement, lazy } from "react";
|
|
6
6
|
|
|
7
|
+
// 方案安装入口(Phase B T3):平台内置编排面 /api/oj-market/*(api 随脚手架内置,R29
|
|
8
|
+
// 第一方专用)由 system 域代管。用生成物明示的 bindRequest 逃生门而非
|
|
9
|
+
// createOjMarketClient 工厂——后者 DEV 有跨模块认领告警(该 client 名义属 oj-market)。
|
|
10
|
+
import { bindRequest as bindMarketRequest } from "../../../oj-market/web/__client";
|
|
7
11
|
import { createSystemClient } from "./__client";
|
|
8
12
|
|
|
9
13
|
const Users = lazy(() => import("./pages/users"));
|
|
@@ -13,6 +17,7 @@ const Roles = lazy(() => import("./pages/roles"));
|
|
|
13
17
|
const Oplog = lazy(() => import("./pages/oplog"));
|
|
14
18
|
const Settings = lazy(() => import("./pages/settings"));
|
|
15
19
|
const WecomAdmin = lazy(() => import("./pages/wecom"));
|
|
20
|
+
const Solutions = lazy(() => import("./pages/solutions"));
|
|
16
21
|
|
|
17
22
|
/**
|
|
18
23
|
* `/system` 根的角色门禁(对齐后端 assertSystemAdmin,access.ts:87)——
|
|
@@ -75,6 +80,12 @@ export const routes: AppRouteRecordRaw[] = [
|
|
|
75
80
|
Component: Settings,
|
|
76
81
|
handle: { title: "system:menu.settings", roles: ["superadmin"] },
|
|
77
82
|
},
|
|
83
|
+
{
|
|
84
|
+
// 方案安装(Phase B T3,出厂未装态的首装入口):仅超管可见可访问
|
|
85
|
+
path: "/system/solutions",
|
|
86
|
+
Component: Solutions,
|
|
87
|
+
handle: { title: "system:menu.solutions", roles: ["superadmin"] },
|
|
88
|
+
},
|
|
78
89
|
{
|
|
79
90
|
// 企微代开发后台(服务商运营面):仅超管可见(路由级 roles + 页面内二次判定)
|
|
80
91
|
path: "/system/wecom",
|
|
@@ -85,7 +96,13 @@ export const routes: AppRouteRecordRaw[] = [
|
|
|
85
96
|
},
|
|
86
97
|
];
|
|
87
98
|
|
|
88
|
-
/**
|
|
99
|
+
/**
|
|
100
|
+
* 构造即接线:登记 /system 前缀 + 绑定 scoped request(D11 前缀收敛);
|
|
101
|
+
* 另登记 /oj-market(平台内置市场编排面,R29 第一方)并绑定其生成 client——
|
|
102
|
+
* 方案安装页(/system/solutions)出厂未装态即经此调用安装端点。
|
|
103
|
+
*/
|
|
89
104
|
export async function onInit(ctx: ModuleContext) {
|
|
90
105
|
createSystemClient(ctx);
|
|
106
|
+
ctx.register.apiPrefix("/oj-market");
|
|
107
|
+
bindMarketRequest(ctx.utils.request);
|
|
91
108
|
}
|