@gadmin2n/schematics 0.0.136 → 0.0.137

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 (94) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/Dockerfile.server +2 -2
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/compose-ctl.sh +1 -1
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/canvas.prisma +1 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +14 -3
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/config/ui/User.ts +4 -4
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.md +2 -2
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/docker-compose.yml +13 -92
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/readme.md +105 -7
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/ensure-database.js +1 -1
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/package.json +5 -5
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/prisma.config.ts +9 -1
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/notify-dev-servers.js +9 -8
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/wait-for-nest-ready.js +5 -5
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +43 -2
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow.seed.ts +9 -1
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflows.ts +24 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.dto.ts +27 -0
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.spec.ts +430 -0
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas-export.service.ts +203 -0
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +36 -3
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.module.ts +2 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.spec.ts +54 -0
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.controller.ts +245 -0
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.module.ts +10 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.spec.ts +366 -0
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/temporalTenantConfig/temporalTenantConfig.service.ts +110 -0
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-execution.dto.ts +19 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.controller.ts +15 -0
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +79 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +41 -19
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/server/start-prod.sh +5 -5
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/README.md +37 -573
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/ecosystem.dev.config.cjs +12 -1
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -0
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +3 -2
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/config/routeRegistry.tsx +39 -26
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/i18n.ts +3 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +3 -0
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +3 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasEditPage.tsx +23 -5
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +66 -10
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +16 -7
  43. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasToolbar.tsx +21 -0
  44. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +30 -0
  45. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ExportModal.tsx +84 -0
  46. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/ImportModal.tsx +166 -0
  47. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/hooks/useCanvasHistory.ts +52 -13
  48. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/DslView.tsx +6 -0
  49. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/NodePropertyPanel.tsx +6 -1
  50. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +17 -0
  51. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/instance-detail.tsx +18 -6
  52. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/temporal-tenant/index.tsx +225 -0
  53. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/utilities/lazy-with-reload.ts +31 -0
  54. package/package.json +1 -1
  55. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +0 -220
  56. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +0 -132
  57. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/TODO.md +0 -152
  58. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/config/development-sql.yaml +0 -5
  59. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/docker-compose.yml +0 -25
  60. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/package.json +0 -13
  61. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.dockerignore +0 -12
  62. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/.env +0 -7
  63. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/Dockerfile +0 -79
  64. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/GRACEFUL-DEPLOYMENT.md +0 -270
  65. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/SANDBOX.md +0 -122
  66. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package-lock.json +0 -4285
  67. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/package.json +0 -28
  68. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/code-execute.ts +0 -51
  69. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-execute.ts +0 -85
  70. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/db-query.ts +0 -35
  71. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/http-request.ts +0 -54
  72. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/index.ts +0 -6
  73. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/reporting.ts +0 -85
  74. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/activities/send-notification.ts +0 -47
  75. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/config.ts +0 -13
  76. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/condition.ts +0 -101
  77. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/context.ts +0 -58
  78. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/graph.ts +0 -184
  79. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/helpers.ts +0 -133
  80. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +0 -96
  81. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/types.ts +0 -77
  82. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +0 -109
  83. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/index.ts +0 -101
  84. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/outbox-poller.ts +0 -382
  85. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/code-execute.test.ts +0 -44
  86. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/activities/http-request.test.ts +0 -87
  87. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/cron-trigger-workflow.test.ts +0 -20
  88. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/helpers.test.ts +0 -225
  89. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/node-type-consistency.test.ts +0 -101
  90. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +0 -205
  91. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/workflows/dsl-workflow.ts +0 -499
  92. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/tsconfig.json +0 -19
  93. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/vitest.config.ts +0 -8
  94. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/yarn.lock +0 -1905
@@ -1,6 +1,11 @@
1
1
  // pm2 dev 场景专用配置:直接跑 vite CLI,不套 npm 壳。
2
2
  // 由 branch.service.ts 通过 pm2 CLI 传入 PM2_NAME / VITE_PORT / VITE_BRANCH_OWNER 等环境变量。
3
- // vite 自己会读 .env / .env.local,无需 dotenv-cli 层。
3
+ const path = require('path');
4
+ const dotenv = require('dotenv');
5
+
6
+ const baseEnv = dotenv.config({ path: path.join(__dirname, '.env') }).parsed || {};
7
+ const localEnv = dotenv.config({ path: path.join(__dirname, '.env.local') }).parsed || {};
8
+
4
9
  module.exports = {
5
10
  apps: [
6
11
  {
@@ -13,9 +18,15 @@ module.exports = {
13
18
  // NODE_OPTIONS 保留原 package.json 里的 4GB 上限设置。
14
19
  node_args: '--max-old-space-size=4096',
15
20
  env: {
21
+ ...baseEnv,
22
+ ...localEnv,
23
+ // branch.service.ts 注入的变量优先,覆盖 .env* 里的默认值
16
24
  ...(process.env.VITE_BRANCH_OWNER
17
25
  ? { VITE_BRANCH_OWNER: process.env.VITE_BRANCH_OWNER }
18
26
  : {}),
27
+ ...(process.env.VITE_PORT
28
+ ? { VITE_PORT: process.env.VITE_PORT }
29
+ : {}),
19
30
  },
20
31
  },
21
32
  ],
@@ -65,6 +65,7 @@
65
65
  "@typescript-eslint/parser": "^8.59.0",
66
66
  "@vitejs/plugin-react": "^4.3.4",
67
67
  "autoprefixer": "^10.4.24",
68
+ "dotenv": "^17.4.2",
68
69
  "eslint": "^8",
69
70
  "eslint-plugin-react": "^7.37.5",
70
71
  "eslint-plugin-react-hooks": "^4",
@@ -14,6 +14,7 @@ import { useTranslation } from 'react-i18next';
14
14
  import type { MenuProps } from 'antd';
15
15
  import { agentAttrs } from 'components/agentPanel/agentAttributes';
16
16
  import { HEADER_HEIGHT, Z_INDEX } from 'constants/layout';
17
+ import { SUPPORTED_LANGUAGES } from '../../i18n';
17
18
 
18
19
  type IUser = {
19
20
  id: number;
@@ -37,7 +38,7 @@ export const Header: React.FC = () => {
37
38
  const langMenu = useMemo<MenuProps>(
38
39
  () => ({
39
40
  selectedKeys: currentLocale ? [currentLocale] : [],
40
- items: [...(i18n.languages || [])].sort().map((lang: string) => ({
41
+ items: [...SUPPORTED_LANGUAGES].sort().map((lang: string) => ({
41
42
  key: lang,
42
43
  icon: (
43
44
  <span style={{ marginRight: 8, display: 'inline-flex' }}>
@@ -48,7 +49,7 @@ export const Header: React.FC = () => {
48
49
  })),
49
50
  onClick: ({ key: lang }) => changeLanguage(lang),
50
51
  }),
51
- [i18n.languages, currentLocale, changeLanguage],
52
+ [currentLocale, changeLanguage],
52
53
  );
53
54
 
54
55
  const currentLanguageLabel = currentLocale === 'en' ? 'English' : '中文';
@@ -1,4 +1,5 @@
1
- import React, { lazy, Suspense } from 'react';
1
+ import React, { Suspense } from 'react';
2
+ import { lazyWithReload } from 'utilities/lazy-with-reload';
2
3
  import { Outlet, Route } from 'react-router-dom';
3
4
  import { Spin } from 'antd';
4
5
  import generatedResources from 'generated/resources';
@@ -184,7 +185,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
184
185
  // 菜单指引 — sortOrder 1020, code: admin.permission.readme
185
186
  permission_readme: {
186
187
  path: 'readme',
187
- component: lazy(() =>
188
+ component: lazyWithReload(() =>
188
189
  import('routes/permissionReadme').then((m) => ({
189
190
  default: m.PermissionReadmePage,
190
191
  })),
@@ -193,14 +194,14 @@ export const routeRegistry: Record<string, RouteEntry> = {
193
194
  // 用户管理 — sortOrder 1030, code: admin.permission.user
194
195
  user: {
195
196
  path: 'user',
196
- component: lazy(() =>
197
+ component: lazyWithReload(() =>
197
198
  import('routes/user').then((m) => ({ default: m.UserListPage })),
198
199
  ),
199
200
  },
200
201
  // 角色管理 — sortOrder 1040, code: admin.permission.role
201
202
  role: {
202
203
  path: 'role-list',
203
- component: lazy(() =>
204
+ component: lazyWithReload(() =>
204
205
  import('routes/role').then((m) => ({
205
206
  default: m.RoleManagementListPage,
206
207
  })),
@@ -209,7 +210,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
209
210
  // 页面管理 — sortOrder 1050, code: admin.permission.page
210
211
  page: {
211
212
  path: 'page-list',
212
- component: lazy(() =>
213
+ component: lazyWithReload(() =>
213
214
  import('routes/page/list').then((m) => ({
214
215
  default: m.PageManagementListPage,
215
216
  })),
@@ -218,7 +219,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
218
219
  // 资源管理 — sortOrder 1060, code: admin.permission.resource
219
220
  resource: {
220
221
  path: 'resource-list',
221
- component: lazy(() =>
222
+ component: lazyWithReload(() =>
222
223
  import('routes/resource').then((m) => ({
223
224
  default: m.ResourceManagementListPage,
224
225
  })),
@@ -237,7 +238,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
237
238
  list: {
238
239
  path: '',
239
240
  index: true,
240
- component: lazy(() =>
241
+ component: lazyWithReload(() =>
241
242
  import('routes/canvas').then((m) => ({
242
243
  default: m.CanvasListPage,
243
244
  })),
@@ -245,7 +246,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
245
246
  },
246
247
  edit: {
247
248
  path: 'edit/:id',
248
- component: lazy(() =>
249
+ component: lazyWithReload(() =>
249
250
  import('routes/canvas').then((m) => ({
250
251
  default: m.CanvasEditPage,
251
252
  })),
@@ -256,7 +257,7 @@ export const routeRegistry: Record<string, RouteEntry> = {
256
257
  // Canvas published pages (dynamic, DB-driven)
257
258
  'canvas-page': {
258
259
  path: 'p/:slug',
259
- component: lazy(() => import('routes/canvas-page')),
260
+ component: lazyWithReload(() => import('routes/canvas-page')),
260
261
  },
261
262
  /*game: {
262
263
  path: 'game-list',
@@ -264,25 +265,25 @@ export const routeRegistry: Record<string, RouteEntry> = {
264
265
  list: {
265
266
  path: '',
266
267
  index: true,
267
- component: lazy(() =>
268
+ component: lazyWithReload(() =>
268
269
  import('routes/game').then((m) => ({ default: m.GameList })),
269
270
  ),
270
271
  },
271
272
  show: {
272
273
  path: 'show/:id',
273
- component: lazy(() =>
274
+ component: lazyWithReload(() =>
274
275
  import('routes/game').then((m) => ({ default: m.GameShow })),
275
276
  ),
276
277
  },
277
278
  edit: {
278
279
  path: 'edit/:id',
279
- component: lazy(() =>
280
+ component: lazyWithReload(() =>
280
281
  import('routes/game').then((m) => ({ default: m.GameEdit })),
281
282
  ),
282
283
  },
283
284
  create: {
284
285
  path: 'create',
285
- component: lazy(() =>
286
+ component: lazyWithReload(() =>
286
287
  import('routes/game').then((m) => ({ default: m.GameCreate })),
287
288
  ),
288
289
  },
@@ -295,11 +296,11 @@ export const routeRegistry: Record<string, RouteEntry> = {
295
296
  list: {
296
297
  path: '',
297
298
  index: true,
298
- component: lazy(() => import('routes/agenda')),
299
+ component: lazyWithReload(() => import('routes/agenda')),
299
300
  },
300
301
  show: {
301
302
  path: 'show/:jobName',
302
- component: lazy(() => import('routes/agenda/show')),
303
+ component: lazyWithReload(() => import('routes/agenda/show')),
303
304
  },
304
305
  },
305
306
  },
@@ -310,56 +311,68 @@ export const routeRegistry: Record<string, RouteEntry> = {
310
311
  workflow_list: {
311
312
  path: '',
312
313
  index: true,
313
- component: lazy(() => import('routes/workflow')),
314
+ component: lazyWithReload(() => import('routes/workflow')),
314
315
  },
315
316
  editor_new: {
316
317
  path: 'create',
317
- component: lazy(() => import('routes/workflow/editor')),
318
+ component: lazyWithReload(() => import('routes/workflow/editor')),
318
319
  },
319
320
  instances: {
320
321
  path: 'instances/show/:workflowId',
321
- component: lazy(() => import('routes/workflow/instances')),
322
+ component: lazyWithReload(
323
+ () => import('routes/workflow/instances'),
324
+ ),
322
325
  },
323
326
  instanceDetail: {
324
327
  path: 'instance/show/:instanceId',
325
- component: lazy(() => import('routes/workflow/instance-detail')),
328
+ component: lazyWithReload(
329
+ () => import('routes/workflow/instance-detail'),
330
+ ),
326
331
  },
327
332
  editor: {
328
333
  path: 'edit/:id',
329
- component: lazy(() => import('routes/workflow/editor')),
334
+ component: lazyWithReload(() => import('routes/workflow/editor')),
330
335
  },
331
336
  show: {
332
337
  path: 'show/:id',
333
- component: lazy(() => import('routes/workflow/show')),
338
+ component: lazyWithReload(() => import('routes/workflow/show')),
334
339
  },
335
340
  workflow_node_instance: {
336
341
  path: 'node-instances',
337
- component: lazy(() => import('routes/workflow/node-instances')),
342
+ component: lazyWithReload(
343
+ () => import('routes/workflow/node-instances'),
344
+ ),
338
345
  },
339
346
  nodeInstanceCreate: {
340
347
  path: 'node-instances/create',
341
- component: lazy(
348
+ component: lazyWithReload(
342
349
  () => import('routes/workflow/node-instances/create'),
343
350
  ),
344
351
  },
345
352
  nodeInstanceEdit: {
346
353
  path: 'node-instances/edit/:id',
347
- component: lazy(
354
+ component: lazyWithReload(
348
355
  () => import('routes/workflow/node-instances/edit'),
349
356
  ),
350
357
  },
351
358
  nodeInstanceShow: {
352
359
  path: 'node-instances/show/:id',
353
- component: lazy(
360
+ component: lazyWithReload(
354
361
  () => import('routes/workflow/node-instances/show'),
355
362
  ),
356
363
  },
364
+ workflow_temporal_tenant: {
365
+ path: 'temporal-tenant',
366
+ component: lazyWithReload(
367
+ () => import('routes/workflow/temporal-tenant'),
368
+ ),
369
+ },
357
370
  },
358
371
  },
359
372
  // 操作日志 — sortOrder 1070, code: admin.audit
360
373
  audits: {
361
374
  path: 'audit-log-list',
362
- component: lazy(() =>
375
+ component: lazyWithReload(() =>
363
376
  import('routes/audit').then((m) => ({ default: m.AuditLogPage })),
364
377
  ),
365
378
  },
@@ -1,4 +1,6 @@
1
1
  import i18n from 'i18next';
2
+
3
+ export const SUPPORTED_LANGUAGES = ['en', 'zh_CN'] as const;
2
4
  import { initReactI18next } from 'react-i18next';
3
5
  import detector from 'i18next-browser-languagedetector';
4
6
  import en from './locales/en/common.json';
@@ -8,7 +10,7 @@ i18n
8
10
  .use(detector)
9
11
  .use(initReactI18next)
10
12
  .init({
11
- supportedLngs: ['en', 'zh_CN'],
13
+ supportedLngs: [...SUPPORTED_LANGUAGES],
12
14
  resources: {
13
15
  en: { common: en },
14
16
  zh_CN: { common: zh_CN },
@@ -147,6 +147,7 @@
147
147
  "componentLib": "Components",
148
148
  "backToList": "Back to list",
149
149
  "undo": "Undo",
150
+ "redo": "Redo",
150
151
  "save": "Save",
151
152
  "saveDirty": "Save •",
152
153
  "preview": "Preview",
@@ -295,6 +296,8 @@
295
296
  "myCanvases": "My Canvas",
296
297
  "othersCanvases": "Others' Canvas",
297
298
  "create": "New Canvas",
299
+ "import": "Import",
300
+ "export": "Export",
298
301
  "published": "Published",
299
302
  "unpublished": "Unpublished",
300
303
  "rename": "Rename",
@@ -149,6 +149,7 @@
149
149
  "componentLib": "组件库",
150
150
  "backToList": "返回列表",
151
151
  "undo": "撤销",
152
+ "redo": "重做",
152
153
  "save": "保存",
153
154
  "saveDirty": "保存 •",
154
155
  "preview": "预览",
@@ -297,6 +298,8 @@
297
298
  "myCanvases": "我的 Canvas",
298
299
  "othersCanvases": "其他人的 Canvas",
299
300
  "create": "新建 Canvas",
301
+ "import": "导入",
302
+ "export": "导出",
300
303
  "published": "已发布",
301
304
  "unpublished": "未发布",
302
305
  "rename": "重命名",
@@ -114,12 +114,28 @@ const CanvasEditPage: React.FC = () => {
114
114
  [history],
115
115
  );
116
116
 
117
- // ── Undo: pop a snapshot and write it back directly, WITHOUT going
118
- // through handleItemsChange/handleLayoutChange/handleBothChange —
119
- // those push to history, which would re-add the state we're leaving
120
- // and make consecutive undos toggle instead of walking further back. ──
117
+ // ── Undo/Redo: move a snapshot between the two stacks and write it
118
+ // back directly, WITHOUT going through handleItemsChange/
119
+ // handleLayoutChange/handleBothChange — those push to history, which
120
+ // would corrupt the undo/redo chain (consecutive undos would toggle
121
+ // instead of walking further back, and redo would get cleared by its
122
+ // own undo). ──
121
123
  const handleUndo = useCallback(() => {
122
- const snapshot = history.pop();
124
+ const snapshot = history.undo({
125
+ items: liveRef.current.canvasItems,
126
+ layout: liveRef.current.canvasLayout,
127
+ });
128
+ if (!snapshot) return;
129
+ setCanvasItems(snapshot.items);
130
+ setCanvasLayout(snapshot.layout);
131
+ setIsDirty(true);
132
+ }, [history]);
133
+
134
+ const handleRedo = useCallback(() => {
135
+ const snapshot = history.redo({
136
+ items: liveRef.current.canvasItems,
137
+ layout: liveRef.current.canvasLayout,
138
+ });
123
139
  if (!snapshot) return;
124
140
  setCanvasItems(snapshot.items);
125
141
  setCanvasLayout(snapshot.layout);
@@ -208,6 +224,8 @@ const CanvasEditPage: React.FC = () => {
208
224
  isPublished={!!canvas.publishedPageCode}
209
225
  canUndo={history.canUndo}
210
226
  onUndo={handleUndo}
227
+ canRedo={history.canRedo}
228
+ onRedo={handleRedo}
211
229
  onItemsChange={handleItemsChange}
212
230
  onLayoutChange={handleLayoutChange}
213
231
  onBothChange={handleBothChange}
@@ -9,8 +9,15 @@ import {
9
9
  Modal,
10
10
  Input,
11
11
  Typography,
12
+ Space,
12
13
  } from 'antd';
13
- import { PlusOutlined, DeleteOutlined, EditOutlined } from '@ant-design/icons';
14
+ import {
15
+ PlusOutlined,
16
+ DeleteOutlined,
17
+ EditOutlined,
18
+ ExportOutlined,
19
+ ImportOutlined,
20
+ } from '@ant-design/icons';
14
21
  import {
15
22
  fetchCanvases,
16
23
  createCanvas,
@@ -21,6 +28,8 @@ import {
21
28
  } from './canvasApi';
22
29
  import type { SavedCanvas } from './types';
23
30
  import IsolatedLivePreview from './IsolatedLivePreview';
31
+ import { ExportModal } from './components/ExportModal';
32
+ import { ImportModal } from './components/ImportModal';
24
33
  import { useTranslation } from 'react-i18next';
25
34
  import { useUserPageAccess } from 'hooks/useUserPageAccess';
26
35
  import { customRequest } from 'helpers/http';
@@ -196,6 +205,11 @@ const CanvasListPage: React.FC = () => {
196
205
  }
197
206
  }, [renameModal, renameValue]);
198
207
 
208
+ // ── 导出/导入 ──
209
+ const [exportModalOpen, setExportModalOpen] = useState(false);
210
+ const [exportCanvasId, setExportCanvasId] = useState<string | null>(null);
211
+ const [importModalOpen, setImportModalOpen] = useState(false);
212
+
199
213
  // ── 删除确认 ──
200
214
  const [deleteConfirm, setDeleteConfirm] = useState<SavedCanvas | null>(null);
201
215
 
@@ -233,15 +247,25 @@ const CanvasListPage: React.FC = () => {
233
247
  <Typography.Title level={4} style={{ margin: 0 }}>
234
248
  {t('canvas.list.title')}
235
249
  </Typography.Title>
236
- {isAgentAllowed && (
237
- <Button
238
- type="primary"
239
- icon={<PlusOutlined />}
240
- onClick={handleCreate}
241
- >
242
- {t('canvas.list.create')}
243
- </Button>
244
- )}
250
+ <Space>
251
+ {isAgentAllowed && (
252
+ <Button
253
+ icon={<ImportOutlined />}
254
+ onClick={() => setImportModalOpen(true)}
255
+ >
256
+ {t('canvas.list.import')}
257
+ </Button>
258
+ )}
259
+ {isAgentAllowed && (
260
+ <Button
261
+ type="primary"
262
+ icon={<PlusOutlined />}
263
+ onClick={handleCreate}
264
+ >
265
+ {t('canvas.list.create')}
266
+ </Button>
267
+ )}
268
+ </Space>
245
269
  </div>
246
270
  </Card>
247
271
 
@@ -362,6 +386,19 @@ const CanvasListPage: React.FC = () => {
362
386
  <div style={{ display: 'flex', gap: 4 }}>
363
387
  {isAgentAllowed && (
364
388
  <>
389
+ <Button
390
+ type="text"
391
+ size="small"
392
+ icon={<ExportOutlined />}
393
+ onClick={(e) => {
394
+ e.stopPropagation();
395
+ setExportCanvasId(canvas.id);
396
+ setExportModalOpen(true);
397
+ }}
398
+ style={{ fontSize: 12, color: '#666' }}
399
+ >
400
+ {t('canvas.list.export')}
401
+ </Button>
365
402
  <Button
366
403
  type="text"
367
404
  size="small"
@@ -602,6 +639,25 @@ const CanvasListPage: React.FC = () => {
602
639
  : t('canvas.list.deleteConfirm')}
603
640
  </p>
604
641
  </Modal>
642
+
643
+ <ExportModal
644
+ open={exportModalOpen}
645
+ canvasId={exportCanvasId}
646
+ onClose={() => {
647
+ setExportModalOpen(false);
648
+ setExportCanvasId(null);
649
+ }}
650
+ />
651
+
652
+ <ImportModal
653
+ open={importModalOpen}
654
+ onClose={() => setImportModalOpen(false)}
655
+ onSuccess={() => {
656
+ fetchCanvases()
657
+ .then(setCanvases)
658
+ .catch(() => message.error(t('canvas.list.loadFailed')));
659
+ }}
660
+ />
605
661
  </div>
606
662
  );
607
663
  };
@@ -117,6 +117,8 @@ interface CanvasPageProps {
117
117
  isPublished: boolean;
118
118
  canUndo: boolean;
119
119
  onUndo: () => void;
120
+ canRedo: boolean;
121
+ onRedo: () => void;
120
122
  onItemsChange: (
121
123
  items: CanvasItemType[],
122
124
  opts?: { coalesce?: boolean },
@@ -143,6 +145,8 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
143
145
  isPublished,
144
146
  canUndo,
145
147
  onUndo,
148
+ canRedo,
149
+ onRedo,
146
150
  onItemsChange,
147
151
  onLayoutChange,
148
152
  onBothChange,
@@ -428,13 +432,12 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
428
432
  };
429
433
  }, []);
430
434
 
431
- // ── 全局 Ctrl/Cmd+Z 撤销快捷键 — 焦点在代码编辑器或普通输入框内时,
432
- // 交给它们自己处理本地撤销,不拦截 ──
435
+ // ── 全局 Ctrl/Cmd+Z 撤销 / Ctrl/Cmd+Shift+Z 重做快捷键 — 焦点在代码编辑器
436
+ // 或普通输入框内时,交给它们自己处理本地撤销/重做,不拦截 ──
433
437
  useEffect(() => {
434
438
  const handler = (e: KeyboardEvent) => {
435
- const isUndoCombo =
436
- (e.ctrlKey || e.metaKey) && !e.shiftKey && e.key.toLowerCase() === 'z';
437
- if (!isUndoCombo) return;
439
+ const isZCombo = (e.ctrlKey || e.metaKey) && e.key.toLowerCase() === 'z';
440
+ if (!isZCombo) return;
438
441
  const active = document.activeElement as HTMLElement | null;
439
442
  if (active) {
440
443
  if (active.closest('.monaco-editor')) return;
@@ -447,11 +450,15 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
447
450
  }
448
451
  }
449
452
  e.preventDefault();
450
- onUndo();
453
+ if (e.shiftKey) {
454
+ onRedo();
455
+ } else {
456
+ onUndo();
457
+ }
451
458
  };
452
459
  window.addEventListener('keydown', handler);
453
460
  return () => window.removeEventListener('keydown', handler);
454
- }, [onUndo]);
461
+ }, [onUndo, onRedo]);
455
462
 
456
463
  // ── MultiChart 工具栏切换图表类型时,同步更新代码中的 defaultVariant ──
457
464
  useEffect(() => {
@@ -658,6 +665,8 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
658
665
  isPublished={isPublished}
659
666
  canUndo={canUndo}
660
667
  onUndo={onUndo}
668
+ canRedo={canRedo}
669
+ onRedo={onRedo}
661
670
  onSave={onSave}
662
671
  onBack={onBack}
663
672
  onPublish={onPublish}
@@ -8,6 +8,7 @@ import {
8
8
  EyeOutlined,
9
9
  RobotOutlined,
10
10
  UndoOutlined,
11
+ RedoOutlined,
11
12
  } from '@ant-design/icons';
12
13
  import { useTranslation } from 'react-i18next';
13
14
 
@@ -17,6 +18,8 @@ interface CanvasToolbarProps {
17
18
  isPublished: boolean;
18
19
  canUndo: boolean;
19
20
  onUndo: () => void;
21
+ canRedo: boolean;
22
+ onRedo: () => void;
20
23
  onSave: () => void;
21
24
  onBack: () => void;
22
25
  onPublish: () => void;
@@ -31,6 +34,8 @@ const CanvasToolbar: React.FC<CanvasToolbarProps> = ({
31
34
  isPublished,
32
35
  canUndo,
33
36
  onUndo,
37
+ canRedo,
38
+ onRedo,
34
39
  onSave,
35
40
  onBack,
36
41
  onPublish,
@@ -117,6 +122,22 @@ const CanvasToolbar: React.FC<CanvasToolbarProps> = ({
117
122
  }}
118
123
  />
119
124
  </Tooltip>
125
+ <Tooltip title={t('canvas.redo')}>
126
+ <Button
127
+ size="small"
128
+ icon={<RedoOutlined />}
129
+ onClick={onRedo}
130
+ disabled={!canRedo}
131
+ style={{
132
+ borderRadius: 8,
133
+ border: '1px solid #e0e0e0',
134
+ background: 'transparent',
135
+ color: canRedo ? '#555' : '#ccc',
136
+ fontWeight: 500,
137
+ transition: 'all 200ms ease',
138
+ }}
139
+ />
140
+ </Tooltip>
120
141
  {isAgentAllowed && (
121
142
  <Button
122
143
  size="small"
@@ -110,3 +110,33 @@ export async function fetchAllCanvases(): Promise<SavedCanvasWithOwner[]> {
110
110
  );
111
111
  return records.map(toSavedCanvasWithOwner);
112
112
  }
113
+
114
+ // ── 导出/导入 ────────────────────────────────────────────────────────────
115
+
116
+ export async function generateExportToken(
117
+ id: string,
118
+ ): Promise<{ url: string }> {
119
+ return customRequest<{ url: string }>(`canvas/${id}/export-token`, 'POST');
120
+ }
121
+
122
+ export async function importPreview(
123
+ payload: any,
124
+ ): Promise<{ current: any; incoming: any }> {
125
+ return customRequest<{ current: any; incoming: any }>(
126
+ 'canvas/import-preview',
127
+ 'POST',
128
+ { payload },
129
+ );
130
+ }
131
+
132
+ export async function importConfirm(payload: any): Promise<{
133
+ success: boolean;
134
+ canvasId: string;
135
+ canvasName: string;
136
+ }> {
137
+ return customRequest<{
138
+ success: boolean;
139
+ canvasId: string;
140
+ canvasName: string;
141
+ }>('canvas/import-confirm', 'POST', { payload });
142
+ }