@oj-module/cli 0.2.2 → 0.2.5

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 (231) hide show
  1. package/devkit/README.md +14 -3
  2. package/devkit/SKILL.md +16 -4
  3. package/devkit/framework-dev.md +8 -6
  4. package/devkit/manual.md +243 -23
  5. package/package.json +2 -2
  6. package/shell-dist/assets/runtime.js +143 -8
  7. package/shell-dist/index.html +2 -2
  8. package/shell-dist/versions.json +3 -3
  9. package/src/args.ts +67 -0
  10. package/src/build.ts +11 -3
  11. package/src/bundle.ts +197 -0
  12. package/src/contract/__tests__/handler-scan.test.ts +82 -0
  13. package/src/contract/check.ts +2 -1
  14. package/src/contract/handler-scan.ts +32 -0
  15. package/src/deterministic-tar.ts +120 -0
  16. package/src/dev.ts +4 -2
  17. package/src/enable.ts +28 -0
  18. package/src/index.ts +63 -2
  19. package/src/info.ts +3 -2
  20. package/src/init.ts +147 -14
  21. package/src/install.ts +268 -0
  22. package/src/keygen.ts +88 -0
  23. package/src/layout.ts +31 -0
  24. package/src/lifecycle/__tests__/authoring.test.ts +109 -0
  25. package/src/lifecycle/__tests__/bundle.test.ts +150 -0
  26. package/src/lifecycle/__tests__/deterministic-tar.test.ts +145 -0
  27. package/src/lifecycle/__tests__/fixtures/README.md +59 -0
  28. package/src/lifecycle/__tests__/fixtures/fixture-invalid/EXPECTED-VIOLATIONS.md +27 -0
  29. package/src/lifecycle/__tests__/fixtures/fixture-invalid/api/src/Demo/manifest.yaml +6 -0
  30. package/src/lifecycle/__tests__/fixtures/fixture-invalid/manifest.yaml +40 -0
  31. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/entry.ts +14 -0
  32. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/en-US.json +12 -0
  33. package/src/lifecycle/__tests__/fixtures/fixture-invalid/web/src/Demo/locales/zh-CN.json +12 -0
  34. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/manifest.yaml +9 -0
  35. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/migrations/0001__init.sql +6 -0
  36. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/ping/api.ts +9 -0
  37. package/src/lifecycle/__tests__/fixtures/fixture-valid/api/src/demo-solution/schema.yaml +9 -0
  38. package/src/lifecycle/__tests__/fixtures/fixture-valid/manifest.yaml +96 -0
  39. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/entry.ts +23 -0
  40. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/en-US.json +29 -0
  41. package/src/lifecycle/__tests__/fixtures/fixture-valid/web/src/demo-solution/locales/zh-CN.json +29 -0
  42. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz +0 -0
  43. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.bad.sig +7 -0
  44. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.sig +7 -0
  45. package/src/lifecycle/__tests__/fixtures/sig/demo-solution-0.2.0.tgz.unknown.sig +7 -0
  46. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.d.mts +21 -0
  47. package/src/lifecycle/__tests__/fixtures/sig/gen-signed-fixtures.mjs +196 -0
  48. package/src/lifecycle/__tests__/helpers.ts +78 -0
  49. package/src/lifecycle/__tests__/install.test.ts +514 -0
  50. package/src/lifecycle/__tests__/keygen.test.ts +61 -0
  51. package/src/lifecycle/__tests__/manifest-contract.test.ts +228 -0
  52. package/src/lifecycle/__tests__/publish.test.ts +80 -0
  53. package/src/lifecycle/__tests__/sig-fixtures.test.ts +100 -0
  54. package/src/lifecycle/__tests__/sign.test.ts +131 -0
  55. package/src/lifecycle/__tests__/smoke.test.ts +61 -0
  56. package/src/lifecycle/__tests__/trust.test.ts +177 -0
  57. package/src/lifecycle/__tests__/uninstall.test.ts +87 -0
  58. package/src/lifecycle/keygen.ts +40 -0
  59. package/src/lifecycle/sign.ts +74 -0
  60. package/src/manifest-contract.ts +696 -0
  61. package/src/oj-cert.ts +19 -0
  62. package/src/oj-cmd.ts +3 -3
  63. package/src/preview.ts +2 -2
  64. package/src/publish.ts +150 -0
  65. package/src/sign.ts +128 -0
  66. package/src/site.ts +146 -0
  67. package/src/trust.ts +116 -0
  68. package/src/uninstall.ts +29 -0
  69. package/src/usage.ts +35 -1
  70. package/templates/README.md +1 -1
  71. package/templates/api/config.yaml +47 -0
  72. package/templates/api/seed.mysql.sql +1 -1
  73. package/templates/api/seed.pg.sql +1 -1
  74. package/templates/api/src/_platform/_lifecycle/atomic.ts +98 -0
  75. package/templates/api/src/_platform/_lifecycle/history.ts +31 -0
  76. package/templates/api/src/_platform/_lifecycle/installer.ts +1080 -0
  77. package/templates/api/src/_platform/_lifecycle/lock.ts +148 -0
  78. package/templates/api/src/_platform/_lifecycle/semver.ts +61 -0
  79. package/templates/api/src/_platform/_lifecycle/sha384.ts +187 -0
  80. package/templates/api/src/_platform/_lifecycle/state.ts +256 -0
  81. package/templates/api/src/_platform/_lifecycle/steps/deploy-web.ts +182 -0
  82. package/templates/api/src/_platform/_lifecycle/steps/download.ts +90 -0
  83. package/templates/api/src/_platform/_lifecycle/steps/extract.ts +239 -0
  84. package/templates/api/src/_platform/_lifecycle/steps/migrate.ts +363 -0
  85. package/templates/api/src/_platform/_lifecycle/steps/preflight.ts +590 -0
  86. package/templates/api/src/_platform/_lifecycle/steps/register.ts +211 -0
  87. package/templates/api/src/_platform/_lifecycle/steps/reload.ts +81 -0
  88. package/templates/api/src/_platform/_lifecycle/steps/smoke.ts +216 -0
  89. package/templates/api/src/_platform/_lifecycle/steps/verify.ts +207 -0
  90. package/templates/api/src/_platform/_lifecycle/tasks.ts +119 -0
  91. package/templates/api/src/_platform/_lifecycle/text-edit.ts +110 -0
  92. package/templates/api/src/_platform/_lifecycle/tgz-guard.ts +41 -0
  93. package/templates/api/src/_platform/_lifecycle/trust.ts +229 -0
  94. package/templates/api/src/_platform/_lifecycle/types.ts +94 -0
  95. package/templates/api/src/_platform/_lifecycle/uninstall.ts +283 -0
  96. package/templates/api/src/_platform/_lifecycle/verify-checks.ts +228 -0
  97. package/templates/api/src/_platform/_lifecycle/verify-report.ts +92 -0
  98. package/templates/api/src/_platform/_shared/service/access.ts +2 -2
  99. package/templates/api/src/_platform/_shared/service/settings.ts +10 -1
  100. package/templates/api/src/_platform/_shared/service/solution-perms.ts +14 -7
  101. package/templates/api/src/_platform/_shared/site-facade.ts +44 -0
  102. package/templates/api/src/_platform/manifest.yaml +1 -0
  103. package/templates/api/src/_platform/market/api.ts +883 -0
  104. package/templates/api/src/_platform/market/bootstrap.ts +39 -0
  105. package/templates/api/src/_platform/migrations/0010__sys_solution_route.mysql.sql +17 -0
  106. package/templates/api/src/_platform/migrations/0010__sys_solution_route.pg.sql +16 -0
  107. package/templates/api/src/_platform/migrations/0010__sys_solution_route.sql +18 -0
  108. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.mysql.sql +2 -0
  109. package/templates/api/src/_platform/migrations/0011__sys_setting_group_label.sql +4 -0
  110. package/templates/api/src/_platform/migrations/0012__market_history.mysql.sql +13 -0
  111. package/templates/api/src/_platform/migrations/0012__market_history.pg.sql +12 -0
  112. package/templates/api/src/_platform/migrations/0012__market_history.sql +14 -0
  113. package/templates/api/src/_platform/schema.yaml +21 -4
  114. package/templates/api/src/_platform/seed.sql +2 -2
  115. package/templates/api/src/_platform/solutions/api.ts +212 -0
  116. package/templates/api/src/demo/manifest.yaml +5 -0
  117. package/templates/api/src/demo/migrations/0001__create_todos.sql +9 -0
  118. package/templates/api/src/demo/schema.yaml +11 -0
  119. package/templates/api/src/demo/seed.sql +5 -0
  120. package/templates/api/src/demo/todos/api.ts +59 -0
  121. package/templates/api/src/home/line/api.ts +57 -0
  122. package/templates/api/src/home/manifest.yaml +6 -0
  123. package/templates/api/src/home/migrations/0001__create_home_tables.sql +13 -0
  124. package/templates/api/src/home/pie/api.ts +40 -0
  125. package/templates/api/src/home/schema.yaml +14 -0
  126. package/templates/api/src/home/seed.sql +70 -0
  127. package/templates/api/src/system/oplog/api.ts +44 -2
  128. package/templates/api/tests/fixture.ts +16 -0
  129. package/templates/api/tests/lifecycle/atomic-state.spec.ts +183 -0
  130. package/templates/api/tests/lifecycle/bootstrap.spec.ts +116 -0
  131. package/templates/api/tests/lifecycle/deploy-smoke.spec.ts +492 -0
  132. package/templates/api/tests/lifecycle/installer.spec.ts +1218 -0
  133. package/templates/api/tests/lifecycle/market-api.spec.ts +971 -0
  134. package/templates/api/tests/lifecycle/migrate.spec.ts +575 -0
  135. package/templates/api/tests/lifecycle/preflight.spec.ts +255 -0
  136. package/templates/api/tests/lifecycle/register.spec.ts +194 -0
  137. package/templates/api/tests/lifecycle/semver.spec.ts +43 -0
  138. package/templates/api/tests/lifecycle/sha384.spec.ts +48 -0
  139. package/templates/api/tests/lifecycle/sig-fixtures.spec.ts +65 -0
  140. package/templates/api/tests/lifecycle/solutions-api.spec.ts +98 -0
  141. package/templates/api/tests/lifecycle/steps-fs.spec.ts +623 -0
  142. package/templates/api/tests/lifecycle/text-edit.spec.ts +177 -0
  143. package/templates/api/tests/lifecycle/tgz-guard.spec.ts +103 -0
  144. package/templates/api/tests/lifecycle/trust.spec.ts +310 -0
  145. package/templates/api/tests/lifecycle/uninstall.spec.ts +386 -0
  146. package/templates/api/tests/lifecycle/verify-checks.spec.ts +385 -0
  147. package/templates/api/tests/lifecycle/verify-report.spec.ts +290 -0
  148. package/templates/api/tests/rbac-settings.spec.ts +8 -0
  149. package/templates/init.include.yaml +56 -0
  150. package/templates/openapi.json +527 -0
  151. package/templates/package.json +52 -0
  152. package/templates/web/src/_platform/__client/api.schemas.ts +8 -0
  153. package/templates/web/src/_platform/__client/api.ts +335 -0
  154. package/templates/web/src/about/entry.ts +42 -0
  155. package/templates/web/src/about/locales/en-US.json +16 -0
  156. package/templates/web/src/about/locales/zh-CN.json +16 -0
  157. package/templates/web/src/about/pages/constants.ts +20 -0
  158. package/templates/web/src/about/pages/index.tsx +135 -0
  159. package/templates/web/src/access/entry.ts +74 -0
  160. package/templates/web/src/access/locales/en-US.json +46 -0
  161. package/templates/web/src/access/locales/zh-CN.json +46 -0
  162. package/templates/web/src/access/pages/access-mode/index.tsx +16 -0
  163. package/templates/web/src/access/pages/admin-visible/index.tsx +16 -0
  164. package/templates/web/src/access/pages/button-control/index.tsx +181 -0
  165. package/templates/web/src/access/pages/common-visible/index.tsx +16 -0
  166. package/templates/web/src/access/pages/page-control/index.tsx +142 -0
  167. package/templates/web/src/demo/__client/api.schemas.ts +20 -0
  168. package/templates/web/src/demo/__client/api.ts +89 -0
  169. package/templates/web/src/exception/entry.ts +80 -0
  170. package/templates/web/src/exception/locales/en-US.json +13 -0
  171. package/templates/web/src/exception/locales/zh-CN.json +13 -0
  172. package/templates/web/src/exception/pages/403/index.tsx +30 -0
  173. package/templates/web/src/exception/pages/404/index.tsx +30 -0
  174. package/templates/web/src/exception/pages/500/index.tsx +30 -0
  175. package/templates/web/src/exception/pages/unknown-component/index.tsx +38 -0
  176. package/templates/web/src/home/__client/api.schemas.ts +22 -0
  177. package/templates/web/src/home/__client/api.ts +113 -0
  178. package/templates/web/src/login/locales/en-US.json +6 -0
  179. package/templates/web/src/login/locales/zh-CN.json +6 -0
  180. package/templates/web/src/login/pages/login.tsx +67 -18
  181. package/templates/web/src/oj-market/__tests__/CardGrid.test.tsx +93 -0
  182. package/templates/web/src/oj-market/__tests__/HistoryDrawer.test.tsx +28 -0
  183. package/templates/web/src/oj-market/__tests__/InstallDialog.test.tsx +69 -0
  184. package/templates/web/src/oj-market/__tests__/RollbackDialog.test.tsx +43 -0
  185. package/templates/web/src/oj-market/__tests__/UninstallConfirm.test.tsx +30 -0
  186. package/templates/web/src/oj-market/__tests__/marketClient.fake.ts +70 -0
  187. package/templates/web/src/oj-market/__tests__/poll.test.ts +24 -0
  188. package/templates/web/src/oj-market/__tests__/render.tsx +14 -0
  189. package/templates/web/src/oj-market/__tests__/runtime-stub.ts +14 -0
  190. package/templates/web/src/oj-market/__tests__/semver.test.ts +26 -0
  191. package/templates/web/src/oj-market/api/MarketClientProvider.tsx +12 -0
  192. package/templates/web/src/oj-market/api/client.ts +135 -0
  193. package/templates/web/src/oj-market/api/context.ts +11 -0
  194. package/templates/web/src/oj-market/api/defaultClient.ts +7 -0
  195. package/templates/web/src/oj-market/components/CardGrid.tsx +76 -0
  196. package/templates/web/src/oj-market/components/CardItem.tsx +102 -0
  197. package/templates/web/src/oj-market/components/DetailDrawer.tsx +53 -0
  198. package/templates/web/src/oj-market/components/HistoryDrawer.tsx +75 -0
  199. package/templates/web/src/oj-market/components/InstallDialog.tsx +112 -0
  200. package/templates/web/src/oj-market/components/RollbackDialog.tsx +79 -0
  201. package/templates/web/src/oj-market/components/UninstallConfirm.tsx +74 -0
  202. package/templates/web/src/oj-market/entry.ts +27 -0
  203. package/templates/web/src/oj-market/hooks/useMarket.ts +91 -0
  204. package/templates/web/src/oj-market/locales/en-US.json +100 -0
  205. package/templates/web/src/oj-market/locales/zh-CN.json +100 -0
  206. package/templates/web/src/oj-market/pages/MarketApp.tsx +136 -0
  207. package/templates/web/src/oj-market/types.ts +84 -0
  208. package/templates/web/src/oj-market/utils/poll.ts +36 -0
  209. package/templates/web/src/oj-market/utils/semver.ts +14 -0
  210. package/templates/web/src/outside/entry.ts +80 -0
  211. package/templates/web/src/outside/locales/en-US.json +10 -0
  212. package/templates/web/src/outside/locales/zh-CN.json +10 -0
  213. package/templates/web/src/personal-center/entry.ts +1 -10
  214. package/templates/web/src/personal-center/locales/en-US.json +1 -5
  215. package/templates/web/src/personal-center/locales/zh-CN.json +1 -5
  216. package/templates/web/src/route-nest/entry.ts +73 -0
  217. package/templates/web/src/route-nest/locales/en-US.json +9 -0
  218. package/templates/web/src/route-nest/locales/zh-CN.json +9 -0
  219. package/templates/web/src/route-nest/pages/menu1/menu1-1/index.tsx +5 -0
  220. package/templates/web/src/route-nest/pages/menu1/menu1-2/index.tsx +5 -0
  221. package/templates/web/src/route-nest/pages/menu2/index.tsx +5 -0
  222. package/templates/web/src/system/__client/api.schemas.ts +12 -0
  223. package/templates/web/src/system/__client/api.ts +25 -1
  224. package/templates/web/src/system/pages/oplog/index.tsx +1 -1
  225. package/templates/web/src/system/pages/roles/index.tsx +1 -0
  226. package/templates/web/src/system/pages/settings/index.tsx +1 -1
  227. package/templates/web/src/system/pages/users/index.tsx +1 -1
  228. package/templates/web.config.ts +32 -40
  229. package/vendor/host-versions.json +3 -3
  230. package/templates/pnpm-workspace.yaml +0 -14
  231. package/templates/web/src/personal-center/pages/settings/index.tsx +0 -5
@@ -0,0 +1,112 @@
1
+ import type { IndexRow, TaskRecord } from "../types";
2
+ import { Alert, Button, Modal, Progress, Space, Typography } from "antd";
3
+ import React, { useEffect, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useMarketClient } from "../api/context";
6
+ import { useInstallTask } from "../hooks/useMarket";
7
+
8
+ export interface InstallDialogProps {
9
+ row: IndexRow | null
10
+ open: boolean
11
+ onClose: () => void
12
+ onDone?: (record: TaskRecord) => void
13
+ /** 显式目标版本(回滚用);缺省取 row.latest(安装/升级) */
14
+ version?: string | null
15
+ /** 动作模式:安装/升级/回滚(仅影响标题文案;回滚 = 对旧版降级安装) */
16
+ mode?: "install" | "upgrade" | "rollback"
17
+ }
18
+
19
+ const isTerminal = (r: TaskRecord | null) => r != null && r.status !== "running";
20
+
21
+ /**
22
+ * 安装/升级/回滚对话框:触发 POST install → 800ms 轮询 TaskRecord 终态渲染(reason+retryable+hintKey 双语)。
23
+ * 回滚 = 对已装旧版降级安装(显式 version);降版迁移差异校验(R12)的可读失败由终态 error 渲染。
24
+ */
25
+ export function InstallDialog({ row, open, onClose, onDone, version, mode }: InstallDialogProps) {
26
+ const { t } = useTranslation();
27
+ const client = useMarketClient();
28
+ const [taskId, setTaskId] = useState<string | null>(null);
29
+ const [started, setStarted] = useState(false);
30
+ const { record, done } = useInstallTask(taskId);
31
+
32
+ const targetVersion = version ?? row?.latest ?? null;
33
+ const title
34
+ = mode === "rollback"
35
+ ? t("oj-market:rollback:running")
36
+ : row?.installed
37
+ ? t("oj-market:card:upgrade")
38
+ : t("oj-market:task:installing");
39
+
40
+ // 打开即触发安装(version 缺省取 latest;已装视为升级;显式 version = 回滚降级安装)
41
+ useEffect(() => {
42
+ if (open && row && !started) {
43
+ setStarted(true);
44
+ client
45
+ .install({ name: row.name, version: targetVersion, source: "cdn" })
46
+ .then(res => setTaskId(res.task_id))
47
+ .catch(e => setTaskId(`__error__${e instanceof Error ? e.message : String(e)}`));
48
+ }
49
+ if (!open) {
50
+ setTaskId(null);
51
+ setStarted(false);
52
+ }
53
+ }, [open, row, started, client, targetVersion]);
54
+
55
+ useEffect(() => {
56
+ if (isTerminal(record) && done)
57
+ onDone?.(record);
58
+ }, [record, done, onDone]);
59
+
60
+ const terminal = isTerminal(record);
61
+ const failed = record?.status === "failed" || record?.status === "partial";
62
+ const errorTask = typeof taskId === "string" && taskId.startsWith("__error__");
63
+
64
+ return (
65
+ <Modal
66
+ title={title}
67
+ open={open}
68
+ onCancel={onClose}
69
+ footer={terminal ? <Button onClick={onClose}>{t("oj-market:task:close")}</Button> : null}
70
+ data-testid="install-dialog"
71
+ >
72
+ {errorTask && <Alert type="error" message={taskId!.slice("__error__".length)} />}
73
+ {!errorTask && !record && (
74
+ <Typography.Text>
75
+ {t("oj-market:task:installing")}
76
+ …
77
+ </Typography.Text>
78
+ )}
79
+ {record && (
80
+ <>
81
+ <Progress percent={record.progress} status={terminal ? (failed ? "exception" : "success") : "active"} />
82
+ <div data-testid="install-stage">{record.stage ? t(`oj-market:task:stage.${record.stage}`, record.stage) : record.stage}</div>
83
+ <div data-testid="install-message">{record.message}</div>
84
+ {failed && record.error && (
85
+ <Alert
86
+ type="error"
87
+ showIcon
88
+ message={record.error.reason}
89
+ description={(
90
+ <Space direction="vertical">
91
+ <span>{record.error.retryable ? t("oj-market:task:retryable") : t("oj-market:task:notRetryable")}</span>
92
+ {record.error.hintKey && <span data-testid="install-hint">{t(record.error.hintKey, record.error.hintKey)}</span>}
93
+ </Space>
94
+ )}
95
+ />
96
+ )}
97
+ {failed && (
98
+ <Button
99
+ style={{ marginTop: 12 }}
100
+ onClick={() => {
101
+ setTaskId(null);
102
+ setStarted(false);
103
+ }}
104
+ >
105
+ {t("oj-market:card:retry")}
106
+ </Button>
107
+ )}
108
+ </>
109
+ )}
110
+ </Modal>
111
+ );
112
+ }
@@ -0,0 +1,79 @@
1
+ import type { IndexRow } from "../types";
2
+ import { Alert, Modal, Radio, Space, Spin, Typography } from "antd";
3
+ import React, { useEffect, useMemo, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useMarketHistory } from "../hooks/useMarket";
6
+ import { compareVersions } from "../utils/semver";
7
+
8
+ export interface RollbackDialogProps {
9
+ row: IndexRow | null
10
+ open: boolean
11
+ onClose: () => void
12
+ onRollback: (row: IndexRow, version: string) => void
13
+ }
14
+
15
+ /**
16
+ * 回滚版本选择(P1-15b):候选 = 该方案 market_history 出现过且版本 < 已装版本的历史版本
17
+ * (compareVersions;宿主 CDN 索引版本列表未进 IndexRow 契约,故以历史已装版本为回滚真源)。
18
+ * 回滚 = 对所选旧版重新安装(降级);迁移差异校验(R12)的可读失败由 InstallDialog 终态 error 渲染。
19
+ */
20
+ export function RollbackDialog({ row, open, onClose, onRollback }: RollbackDialogProps) {
21
+ const { t } = useTranslation();
22
+ const { status, data } = useMarketHistory();
23
+ const [selected, setSelected] = useState<string | null>(null);
24
+
25
+ const candidates = useMemo(() => {
26
+ const installed = row?.installed;
27
+ if (!row || !installed)
28
+ return [] as string[];
29
+ const versions = new Set<string>();
30
+ for (const h of status === "ok" ? data : []) {
31
+ if (h.solution === row.name && compareVersions(h.version, installed) < 0)
32
+ versions.add(h.version);
33
+ }
34
+ return [...versions].sort((a, b) => compareVersions(b, a)); // 较新的旧版在前
35
+ }, [row, status, data]);
36
+
37
+ useEffect(() => {
38
+ setSelected(candidates.length ? candidates[0] : null);
39
+ }, [candidates]);
40
+
41
+ const loading = status === "loading";
42
+
43
+ return (
44
+ <Modal
45
+ title={t("oj-market:rollback:title")}
46
+ open={open}
47
+ onCancel={onClose}
48
+ okText={t("oj-market:rollback:confirm")}
49
+ cancelText={t("oj-market:confirm:cancel")}
50
+ okButtonProps={{ danger: true, disabled: !selected }}
51
+ onOk={() => row && selected && onRollback(row, selected)}
52
+ data-testid="rollback-dialog"
53
+ >
54
+ <Typography.Paragraph>
55
+ {t("oj-market:rollback:body", { name: row?.name ?? "", installed: row?.installed ?? "-" })}
56
+ </Typography.Paragraph>
57
+ <Alert type="warning" showIcon message={t("oj-market:rollback:warning")} style={{ marginBottom: 12 }} />
58
+ {loading && <Spin />}
59
+ {!loading && candidates.length === 0 && (
60
+ <div data-testid="rollback-empty">{t("oj-market:rollback:noCandidates")}</div>
61
+ )}
62
+ {!loading && candidates.length > 0 && (
63
+ <Radio.Group
64
+ value={selected ?? undefined}
65
+ onChange={e => setSelected(e.target.value)}
66
+ data-testid="rollback-versions"
67
+ >
68
+ <Space direction="vertical">
69
+ {candidates.map(v => (
70
+ <Radio key={v} value={v}>
71
+ {v}
72
+ </Radio>
73
+ ))}
74
+ </Space>
75
+ </Radio.Group>
76
+ )}
77
+ </Modal>
78
+ );
79
+ }
@@ -0,0 +1,74 @@
1
+ import type { IndexRow, UninstallImpact } from "../types";
2
+ import { Alert, Modal, Spin, Typography } from "antd";
3
+ import React, { useEffect, useState } from "react";
4
+ import { useTranslation } from "react-i18next";
5
+ import { useMarketClient } from "../api/context";
6
+
7
+ export interface UninstallConfirmProps {
8
+ row: IndexRow | null
9
+ open: boolean
10
+ onClose: () => void
11
+ onConfirmed: (row: IndexRow) => void
12
+ }
13
+
14
+ /**
15
+ * 卸载二次确认(P3 U1 语义前移):打开即拉取影响面预检清单(受影响角色/用户计数),
16
+ * 供 superadmin 二次确认。空锁/级联由服务端(P1-15a)统一把关,UI 仅做可读呈现。
17
+ */
18
+ export function UninstallConfirm({ row, open, onClose, onConfirmed }: UninstallConfirmProps) {
19
+ const { t } = useTranslation();
20
+ const client = useMarketClient();
21
+ const [impact, setImpact] = useState<UninstallImpact | null>(null);
22
+ const [loading, setLoading] = useState(false);
23
+ const [error, setError] = useState<string | null>(null);
24
+
25
+ useEffect(() => {
26
+ if (!open || !row)
27
+ return;
28
+ setLoading(true);
29
+ setError(null);
30
+ setImpact(null);
31
+ client
32
+ .uninstallImpact(row.name)
33
+ .then(setImpact)
34
+ .catch(e => setError(e instanceof Error ? e.message : String(e)))
35
+ .finally(() => setLoading(false));
36
+ }, [open, row, client]);
37
+
38
+ return (
39
+ <Modal
40
+ title={t("oj-market:confirm:uninstallTitle")}
41
+ open={open}
42
+ onCancel={onClose}
43
+ okText={t("oj-market:confirm:ok")}
44
+ cancelText={t("oj-market:confirm:cancel")}
45
+ okButtonProps={{ danger: true }}
46
+ onOk={() => row && onConfirmed(row)}
47
+ data-testid="uninstall-confirm"
48
+ >
49
+ <Typography.Paragraph>
50
+ {t("oj-market:confirm:uninstallBody", { name: row?.name ?? "" })}
51
+ </Typography.Paragraph>
52
+ {loading && <Spin tip={t("oj-market:confirm:impactTitle")} />}
53
+ {error && <Alert type="warning" message={error} />}
54
+ {impact && (
55
+ <div data-testid="uninstall-impact">
56
+ <Typography.Text strong>{t("oj-market:confirm:impactTitle")}</Typography.Text>
57
+ <div>
58
+ {t("oj-market:history:colAction.uninstall")}
59
+ :
60
+ {impact.affectedRoles}
61
+ {" "}
62
+ ·
63
+ {t("oj-market:history:colOperator")}
64
+ :
65
+ {impact.affectedUsers}
66
+ </div>
67
+ {impact.affectedRoles === 0 && impact.affectedUsers === 0 && (
68
+ <Typography.Text type="secondary">{t("oj-market:confirm:impactEmpty")}</Typography.Text>
69
+ )}
70
+ </div>
71
+ )}
72
+ </Modal>
73
+ );
74
+ }
@@ -0,0 +1,27 @@
1
+ import { defineModule } from "@oj-module/runtime";
2
+ import MarketApp from "./pages/MarketApp";
3
+
4
+ // 方案市场前端模块(第一方专用,R29:消费 /api/_platform/market/* 编排面,第三方方案不得仿照此调用面)。
5
+ // 菜单标题走 i18next 命名空间语法 "<moduleName>:menu.<key>"(i18n 资源在 locales/,安装时随包注册)。
6
+ export default defineModule({
7
+ name: "oj-market",
8
+ description: "oj 方案市场应用——卡片展示/安装/卸载/历史(第一方专用,R29)",
9
+ version: "0.1.0",
10
+ routes: [
11
+ {
12
+ path: "/oj-market",
13
+ handle: { layout: "container", order: 200, title: "oj-market:menu.main" },
14
+ children: [
15
+ {
16
+ index: true,
17
+ Component: MarketApp,
18
+ handle: { title: "oj-market:menu.main" },
19
+ },
20
+ ],
21
+ },
22
+ ],
23
+ i18n: {
24
+ "zh-CN": () => import("./locales/zh-CN.json"),
25
+ "en-US": () => import("./locales/en-US.json"),
26
+ },
27
+ });
@@ -0,0 +1,91 @@
1
+ import type { IndexRow, MarketHistory, TaskRecord } from "../types";
2
+ import { useCallback, useEffect, useState } from "react";
3
+ import { useMarketClient } from "../api/context";
4
+ import { poll } from "../utils/poll";
5
+
6
+ /** 加载态三态(空态/加载/错误)统一建模,便于组件 DRY 渲染 */
7
+ export type LoadState<T> = { status: "loading" } | { status: "ok", data: T } | { status: "error", error: string };
8
+
9
+ /** 卡片索引加载(含重试);消费 GET /_platform/market/index */
10
+ export function useMarketIndex(): LoadState<IndexRow[]> & { retry: () => void } {
11
+ const client = useMarketClient();
12
+ const [state, setState] = useState<LoadState<IndexRow[]>>({ status: "loading" });
13
+
14
+ const load = useCallback(() => {
15
+ let active = true;
16
+ setState({ status: "loading" });
17
+ client
18
+ .index()
19
+ .then(data => active && setState({ status: "ok", data }))
20
+ .catch(e => active && setState({ status: "error", error: e instanceof Error ? e.message : String(e) }));
21
+ return () => {
22
+ active = false;
23
+ };
24
+ }, [client]);
25
+
26
+ useEffect(() => load(), [load]);
27
+
28
+ return { ...state, retry: load };
29
+ }
30
+
31
+ /** 任务轮询:给定 task_id,800ms 轮询至终态(done/failed/partial);超时不抛,返回当前值 */
32
+ export function useInstallTask(taskId: string | null): {
33
+ record: TaskRecord | null
34
+ done: boolean
35
+ } {
36
+ const client = useMarketClient();
37
+ const [record, setRecord] = useState<TaskRecord | null>(null);
38
+ const [done, setDone] = useState(false);
39
+
40
+ useEffect(() => {
41
+ if (!taskId)
42
+ return;
43
+ let active = true;
44
+ setRecord(null);
45
+ setDone(false);
46
+ poll(
47
+ () => client.task(taskId),
48
+ r => r.status !== "running",
49
+ { intervalMs: 800, timeoutMs: 30 * 60 * 1000 },
50
+ )
51
+ .then(rec => active && setRecord(rec))
52
+ .catch(e => active && setRecord({
53
+ id: taskId,
54
+ name: "",
55
+ version: "",
56
+ status: "failed",
57
+ stage: null,
58
+ progress: 0,
59
+ message: e instanceof Error ? e.message : String(e),
60
+ stages: [],
61
+ }))
62
+ .finally(() => active && setDone(true));
63
+ return () => {
64
+ active = false;
65
+ };
66
+ }, [client, taskId]);
67
+
68
+ return { record, done };
69
+ }
70
+
71
+ /** 市场操作历史加载(只读消费本仓端点) */
72
+ export function useMarketHistory(): LoadState<MarketHistory[]> & { retry: () => void } {
73
+ const client = useMarketClient();
74
+ const [state, setState] = useState<LoadState<MarketHistory[]>>({ status: "loading" });
75
+
76
+ const load = useCallback(() => {
77
+ let active = true;
78
+ setState({ status: "loading" });
79
+ client
80
+ .history()
81
+ .then(data => active && setState({ status: "ok", data }))
82
+ .catch(e => active && setState({ status: "error", error: e instanceof Error ? e.message : String(e) }));
83
+ return () => {
84
+ active = false;
85
+ };
86
+ }, [client]);
87
+
88
+ useEffect(() => load(), [load]);
89
+
90
+ return { ...state, retry: load };
91
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "menu": {
3
+ "main": "Market"
4
+ },
5
+ "perm": {
6
+ "view": "View solution market"
7
+ },
8
+ "role": {
9
+ "viewer": "Solution market viewer"
10
+ },
11
+ "settings": {
12
+ "group": {
13
+ "display": "Display preferences"
14
+ },
15
+ "display": {
16
+ "greeting": "Greeting"
17
+ }
18
+ },
19
+ "card": {
20
+ "title": "Solution Market",
21
+ "subtitle": "Browse, install and manage oj solutions",
22
+ "loading": "Loading cards…",
23
+ "empty": "No solutions available (market address not configured or index empty)",
24
+ "error": "Failed to load cards",
25
+ "retry": "Retry",
26
+ "version": "Version",
27
+ "latest": "Latest",
28
+ "installed": "Installed",
29
+ "notInstalled": "Not installed",
30
+ "enabled": "Enabled",
31
+ "disabled": "Disabled",
32
+ "upgradable": "Upgradable",
33
+ "details": "Details",
34
+ "history": "History",
35
+ "install": "Install",
36
+ "uninstall": "Uninstall",
37
+ "enable": "Enable",
38
+ "disable": "Disable",
39
+ "upgrade": "Upgrade",
40
+ "rollback": "Rollback"
41
+ },
42
+ "task": {
43
+ "installing": "Installing",
44
+ "uninstalling": "Uninstalling",
45
+ "progress": "Progress",
46
+ "stage": "Stage",
47
+ "status": {
48
+ "running": "Running",
49
+ "done": "Done",
50
+ "failed": "Failed",
51
+ "partial": "Partial"
52
+ },
53
+ "reason": "Reason",
54
+ "retryable": "Retryable",
55
+ "notRetryable": "Not retryable",
56
+ "hint": "Hint",
57
+ "close": "Close",
58
+ "viewLog": "View log"
59
+ },
60
+ "confirm": {
61
+ "uninstallTitle": "Confirm uninstall",
62
+ "uninstallBody": "Uninstalling \"{{name}}\" affects the roles and users below, and the site must keep at least one locked module. This cannot be undone. Continue?",
63
+ "impactTitle": "Impact pre-check",
64
+ "impactEmpty": "No affected roles or users (orphan solution)",
65
+ "cancel": "Cancel",
66
+ "ok": "Confirm uninstall"
67
+ },
68
+ "rollback": {
69
+ "title": "Rollback version",
70
+ "running": "Rolling back",
71
+ "body": "Pick a prior version to roll \"{{name}}\" back to (currently installed {{installed}}). Rollback reinstalls the chosen older version; if the migration diff check (R12) fails you will get a readable error.",
72
+ "selectLabel": "Target version",
73
+ "noCandidates": "No earlier version in history to roll back to",
74
+ "confirm": "Confirm rollback",
75
+ "warning": "Rollback reinstalls the chosen older version; it is rejected if the older migration is irreversible."
76
+ },
77
+ "history": {
78
+ "title": "Operation history",
79
+ "empty": "No records yet",
80
+ "colSolution": "Solution",
81
+ "colVersion": "Version",
82
+ "colAction": "Action",
83
+ "colStatus": "Status",
84
+ "colOperator": "Operator",
85
+ "colTime": "Time",
86
+ "action": {
87
+ "install": "Install",
88
+ "uninstall": "Uninstall",
89
+ "enable": "Enable",
90
+ "disable": "Disable",
91
+ "upgrade": "Upgrade",
92
+ "rollback": "Rollback"
93
+ },
94
+ "status": {
95
+ "done": "Done",
96
+ "failed": "Failed",
97
+ "partial": "Partial"
98
+ }
99
+ }
100
+ }
@@ -0,0 +1,100 @@
1
+ {
2
+ "menu": {
3
+ "main": "方案市场"
4
+ },
5
+ "perm": {
6
+ "view": "查看方案市场"
7
+ },
8
+ "role": {
9
+ "viewer": "方案市场查看者"
10
+ },
11
+ "settings": {
12
+ "group": {
13
+ "display": "显示偏好"
14
+ },
15
+ "display": {
16
+ "greeting": "问候语"
17
+ }
18
+ },
19
+ "card": {
20
+ "title": "方案市场",
21
+ "subtitle": "浏览、安装与管理 oj 方案",
22
+ "loading": "卡片加载中…",
23
+ "empty": "市场暂无可用方案(未配置市场地址或索引为空)",
24
+ "error": "卡片加载失败",
25
+ "retry": "重试",
26
+ "version": "版本",
27
+ "latest": "最新",
28
+ "installed": "已装",
29
+ "notInstalled": "未安装",
30
+ "enabled": "已启用",
31
+ "disabled": "已停用",
32
+ "upgradable": "可升级",
33
+ "details": "详情",
34
+ "history": "历史",
35
+ "install": "安装",
36
+ "uninstall": "卸载",
37
+ "enable": "启用",
38
+ "disable": "停用",
39
+ "upgrade": "升级",
40
+ "rollback": "回滚"
41
+ },
42
+ "task": {
43
+ "installing": "安装中",
44
+ "uninstalling": "卸载中",
45
+ "progress": "进度",
46
+ "stage": "阶段",
47
+ "status": {
48
+ "running": "进行中",
49
+ "done": "完成",
50
+ "failed": "失败",
51
+ "partial": "部分完成"
52
+ },
53
+ "reason": "原因",
54
+ "retryable": "可重试",
55
+ "notRetryable": "不可重试",
56
+ "hint": "提示",
57
+ "close": "关闭",
58
+ "viewLog": "查看日志"
59
+ },
60
+ "confirm": {
61
+ "uninstallTitle": "确认卸载",
62
+ "uninstallBody": "卸载「{{name}}」将影响以下角色与用户,且站点至少保留一个已锁模块。此操作不可撤销,确定继续?",
63
+ "impactTitle": "影响面预检",
64
+ "impactEmpty": "无受影响角色或用户(孤立方案)",
65
+ "cancel": "取消",
66
+ "ok": "确认卸载"
67
+ },
68
+ "rollback": {
69
+ "title": "回滚版本",
70
+ "running": "回滚中",
71
+ "body": "选择「{{name}}」要回滚到的历史版本(当前已装 {{installed}})。回滚即按所选旧版重新安装,迁移差异校验(R12)不通过时会给出可读失败。",
72
+ "selectLabel": "目标版本",
73
+ "noCandidates": "无更早的历史版本可回滚",
74
+ "confirm": "确认回滚",
75
+ "warning": "回滚会按所选旧版重新安装;若旧版迁移不可逆将被拒绝。"
76
+ },
77
+ "history": {
78
+ "title": "操作历史",
79
+ "empty": "暂无操作记录",
80
+ "colSolution": "方案",
81
+ "colVersion": "版本",
82
+ "colAction": "操作",
83
+ "colStatus": "状态",
84
+ "colOperator": "操作人",
85
+ "colTime": "时间",
86
+ "action": {
87
+ "install": "安装",
88
+ "uninstall": "卸载",
89
+ "enable": "启用",
90
+ "disable": "停用",
91
+ "upgrade": "升级",
92
+ "rollback": "回滚"
93
+ },
94
+ "status": {
95
+ "done": "成功",
96
+ "failed": "失败",
97
+ "partial": "部分"
98
+ }
99
+ }
100
+ }