@gadmin2n/schematics 0.0.95 → 0.0.97

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 (43) hide show
  1. package/dist/lib/application/files/gadmin2-game-angle-demo/DESIGN.md +348 -0
  2. package/dist/lib/application/files/gadmin2-game-angle-demo/PRODUCT.md +75 -0
  3. package/dist/lib/application/files/gadmin2-game-angle-demo/config/prisma/workflow.prisma +5 -0
  4. package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/workflow-node-types.ts +24 -1
  5. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +32 -3
  6. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.service.ts +28 -6
  7. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.spec.ts +220 -0
  8. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-dsl-validate.ts +129 -0
  9. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.dto.ts +1 -0
  10. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow-export.service.ts +4 -0
  11. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.spec.ts +46 -0
  12. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflow/workflow.service.ts +27 -0
  13. package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/workflowNodeType/workflowNodeType.service.spec.ts +6 -0
  14. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/node-types.ts +43 -4
  15. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/dsl/validate.ts +109 -0
  16. package/dist/lib/application/files/gadmin2-game-angle-demo/temporal/worker/src/tests/validate.test.ts +205 -0
  17. package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +1 -1
  18. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +19 -3
  19. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/header.tsx +55 -56
  20. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/layout.tsx +7 -3
  21. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/logo.tsx +7 -1
  22. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/sider.tsx +179 -160
  23. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/layout/title.tsx +34 -31
  24. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/constants/layout.ts +24 -0
  25. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +24 -2
  26. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +24 -2
  27. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +173 -1
  28. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +27 -2
  29. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +29 -0
  30. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasConfigRegistry.tsx +2 -0
  31. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +98 -3
  32. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/TableConfigModal.tsx +192 -0
  33. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/utils/tableCodeUtils.ts +338 -0
  34. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/CustomNode.tsx +66 -51
  35. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/EnhancedFlowRenderer.tsx +7 -1
  36. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/ExecutionStatusNode.tsx +66 -26
  37. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/components/FlowRenderer.tsx +7 -1
  38. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/editor.tsx +9 -2
  39. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/hooks/useWorkflowAgent.ts +30 -0
  40. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/show.tsx +9 -2
  41. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/types.ts +1 -0
  42. package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/workflow/utils/resolveOutputs.ts +27 -0
  43. package/package.json +1 -1
@@ -16,10 +16,14 @@ import {
16
16
  createCanvas,
17
17
  deleteCanvas,
18
18
  updateCanvas,
19
+ fetchAllCanvases,
20
+ type SavedCanvasWithOwner,
19
21
  } from './canvasApi';
20
22
  import type { SavedCanvas } from './types';
21
23
  import IsolatedLivePreview from './IsolatedLivePreview';
22
24
  import { useTranslation } from 'react-i18next';
25
+ import { useUserPageAccess } from 'hooks/useUserPageAccess';
26
+ import { customRequest } from 'helpers/http';
23
27
 
24
28
  const COLS = 48;
25
29
  const ROW_HEIGHT = 10;
@@ -67,7 +71,33 @@ const CanvasListPage: React.FC = () => {
67
71
  const [loading, setLoading] = useState(true);
68
72
  const { t } = useTranslation();
69
73
 
70
- console.log('[CanvasListPage] rendered, pathname:', window.location.pathname);
74
+ const { roleNames } = useUserPageAccess();
75
+ const isSystemAdmin = roleNames.includes('SYSTEM_ADMIN');
76
+
77
+ const [othersCanvases, setOthersCanvases] = useState<SavedCanvasWithOwner[]>(
78
+ [],
79
+ );
80
+ const [othersLoading, setOthersLoading] = useState(false);
81
+
82
+ const [currentUserId, setCurrentUserId] = useState<string>('');
83
+ useEffect(() => {
84
+ customRequest<{ userid: string }>('userinfo', 'GET')
85
+ .then((res) => {
86
+ setCurrentUserId(res.userid ?? '');
87
+ })
88
+ .catch(() => {});
89
+ }, []);
90
+
91
+ useEffect(() => {
92
+ if (!isSystemAdmin || !currentUserId) return;
93
+ setOthersLoading(true);
94
+ fetchAllCanvases()
95
+ .then((all) => {
96
+ setOthersCanvases(all.filter((c) => c.userId !== currentUserId));
97
+ })
98
+ .catch(() => message.error('加载其他人的 Canvas 失败'))
99
+ .finally(() => setOthersLoading(false));
100
+ }, [isSystemAdmin, currentUserId]);
71
101
 
72
102
  useEffect(() => {
73
103
  fetchCanvases()
@@ -209,6 +239,9 @@ const CanvasListPage: React.FC = () => {
209
239
 
210
240
  {/* Grid */}
211
241
  <Card>
242
+ <Typography.Title level={5} style={{ margin: '0 0 16px 0' }}>
243
+ {t('canvas.list.myCanvases')}
244
+ </Typography.Title>
212
245
  {canvases.length > 0 ? (
213
246
  <div
214
247
  style={{
@@ -359,6 +392,145 @@ const CanvasListPage: React.FC = () => {
359
392
  )}
360
393
  </Card>
361
394
 
395
+ {/* 其他人的 Canvas(仅 system_admin 可见) */}
396
+ {isSystemAdmin && (
397
+ <Card style={{ marginTop: 16 }}>
398
+ <Typography.Title level={5} style={{ margin: '0 0 16px 0' }}>
399
+ {t('canvas.list.othersCanvases')}
400
+ </Typography.Title>
401
+ {othersLoading ? (
402
+ <div
403
+ style={{
404
+ display: 'flex',
405
+ justifyContent: 'center',
406
+ padding: '40px 0',
407
+ }}
408
+ >
409
+ <Spin size="large" />
410
+ </div>
411
+ ) : othersCanvases.length > 0 ? (
412
+ <div
413
+ style={{
414
+ display: 'grid',
415
+ gridTemplateColumns: 'repeat(3, 1fr)',
416
+ gap: 20,
417
+ }}
418
+ >
419
+ {othersCanvases.map((canvas) => (
420
+ <div
421
+ key={canvas.id}
422
+ style={{
423
+ background: '#fff',
424
+ borderRadius: 8,
425
+ overflow: 'hidden',
426
+ border: '1px solid #e5e5e5',
427
+ cursor: 'pointer',
428
+ transition: 'box-shadow 0.2s',
429
+ }}
430
+ onMouseEnter={(e) => {
431
+ (e.currentTarget as HTMLDivElement).style.boxShadow =
432
+ '0 4px 12px rgba(0,0,0,0.1)';
433
+ }}
434
+ onMouseLeave={(e) => {
435
+ (e.currentTarget as HTMLDivElement).style.boxShadow =
436
+ 'none';
437
+ }}
438
+ onClick={() => navigate(`/admin/canvas/edit/${canvas.id}`)}
439
+ >
440
+ {/* Thumbnail */}
441
+ <div
442
+ style={{
443
+ height: 180,
444
+ overflow: 'hidden',
445
+ background: '#f0f0f0',
446
+ position: 'relative',
447
+ pointerEvents: 'none',
448
+ }}
449
+ >
450
+ {canvas.items.length > 0 ? (
451
+ <div
452
+ style={{
453
+ transform: 'scale(0.28)',
454
+ transformOrigin: 'top left',
455
+ width: 1200,
456
+ height: 640,
457
+ }}
458
+ >
459
+ <IsolatedLivePreview code={buildPreviewCode(canvas)} />
460
+ </div>
461
+ ) : (
462
+ <div
463
+ style={{
464
+ display: 'flex',
465
+ alignItems: 'center',
466
+ justifyContent: 'center',
467
+ height: '100%',
468
+ color: '#bbb',
469
+ fontSize: 14,
470
+ }}
471
+ >
472
+ 空画布
473
+ </div>
474
+ )}
475
+ </div>
476
+ {/* Info */}
477
+ <div
478
+ style={{
479
+ padding: '12px 16px',
480
+ borderTop: '1px solid #f0f0f0',
481
+ }}
482
+ >
483
+ <div
484
+ style={{
485
+ display: 'flex',
486
+ justifyContent: 'space-between',
487
+ alignItems: 'center',
488
+ marginBottom: 4,
489
+ }}
490
+ >
491
+ <span
492
+ style={{
493
+ fontWeight: 500,
494
+ fontSize: 14,
495
+ overflow: 'hidden',
496
+ textOverflow: 'ellipsis',
497
+ whiteSpace: 'nowrap',
498
+ maxWidth: 180,
499
+ }}
500
+ >
501
+ {canvas.name}
502
+ </span>
503
+ <Tag
504
+ color={canvas.publishedPageCode ? 'green' : 'default'}
505
+ >
506
+ {canvas.publishedPageCode
507
+ ? t('canvas.list.published')
508
+ : t('canvas.list.unpublished')}
509
+ </Tag>
510
+ </div>
511
+ <div
512
+ style={{ fontSize: 12, color: '#999', marginBottom: 4 }}
513
+ >
514
+ 创建者: {canvas.ownerName}
515
+ </div>
516
+ <div style={{ fontSize: 12, color: '#999' }}>
517
+ 修改于{' '}
518
+ {new Date(canvas.updatedAt).toLocaleString('zh-CN')}
519
+ </div>
520
+ </div>
521
+ </div>
522
+ ))}
523
+ </div>
524
+ ) : (
525
+ <div
526
+ style={{ textAlign: 'center', color: '#999', padding: '40px 0' }}
527
+ >
528
+ 没有其他用户的 Canvas
529
+ </div>
530
+ )}
531
+ </Card>
532
+ )}
533
+
362
534
  {/* 重命名 Modal */}
363
535
  <Modal
364
536
  title={t('canvas.list.renameTitle')}
@@ -35,6 +35,7 @@ import BarChartDataSourceModal from './components/BarChartDataSourceModal';
35
35
  import LineChartDataSourceModal from './components/LineChartDataSourceModal';
36
36
  import RadarChartDataSourceModal from './components/RadarChartDataSourceModal';
37
37
  import MultiChartDataSourceModal from './components/MultiChartDataSourceModal';
38
+ import TableConfigModal from './components/TableConfigModal';
38
39
  import PromptModal from './components/PromptModal';
39
40
  import { createSectionCompactor } from './sectionCompactor';
40
41
  import type { CanvasItem as CanvasItemType } from './types';
@@ -166,9 +167,19 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
166
167
  onConfirm: (value: string) => void;
167
168
  } | null>(null);
168
169
 
170
+ const [virtualTableConfigModal, setVirtualTableConfigModal] = useState<{
171
+ virtualCode: string;
172
+ onConfirm: (newVirtualCode: string) => void;
173
+ } | null>(null);
174
+
169
175
  const menuActionContext: MenuActionContext = useMemo(
170
176
  () => ({
171
177
  showPrompt: (opts) => setPromptModal(opts),
178
+ openTableColumnConfig: (opts) => {
179
+ // 与独立 Table 走 registry 的 configModal 互斥,避免两个 modal 同时挂载
180
+ setConfigModal(null);
181
+ setVirtualTableConfigModal(opts);
182
+ },
172
183
  }),
173
184
  [],
174
185
  );
@@ -492,7 +503,9 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
492
503
  label:
493
504
  componentType === 'MultiChart'
494
505
  ? (t('canvas.configCharts') ?? '配置显示图表与顺序')
495
- : t('canvas.config'),
506
+ : componentType === 'Table'
507
+ ? (t('canvas.menu.configColumns') ?? '配置列')
508
+ : t('canvas.config'),
496
509
  onClick: () => {
497
510
  setConfigModal({ id: itemMenu.id, componentType });
498
511
  setItemMenu(null);
@@ -566,7 +579,7 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
566
579
  onConfirm: (value) => {
567
580
  if (!value.trim()) return;
568
581
  const prompt = generatePrompt({
569
- skill: null,
582
+ skill: 'canvas-component-edit',
570
583
  pageInfo: agent?.pageInfo ?? {
571
584
  resourceName: '',
572
585
  pageType: 'unknown',
@@ -1471,6 +1484,18 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
1471
1484
  onCancel={() => setDataSourceModal(null)}
1472
1485
  />
1473
1486
 
1487
+ {/* MultiChart 切到 table 时的列配置 Modal(virtualCode 桥接) */}
1488
+ {virtualTableConfigModal && (
1489
+ <TableConfigModal
1490
+ code={virtualTableConfigModal.virtualCode}
1491
+ onConfirm={(newVirtualCode) => {
1492
+ virtualTableConfigModal.onConfirm(newVirtualCode);
1493
+ setVirtualTableConfigModal(null);
1494
+ }}
1495
+ onCancel={() => setVirtualTableConfigModal(null)}
1496
+ />
1497
+ )}
1498
+
1474
1499
  {/* ── 通用文本输入弹窗(如修改标题) ── */}
1475
1500
  <PromptModal
1476
1501
  open={!!promptModal}
@@ -81,3 +81,32 @@ export async function publishCanvas(
81
81
  export async function unpublishCanvas(id: string): Promise<void> {
82
82
  await customRequest(`canvas/${id}/unpublish`, 'DELETE');
83
83
  }
84
+
85
+ // ── Admin: 查看所有人的 canvas ──────────────────────────────────────────────
86
+
87
+ interface CanvasRecordWithOwner extends CanvasRecord {
88
+ ownerName: string;
89
+ }
90
+
91
+ export interface SavedCanvasWithOwner extends SavedCanvas {
92
+ ownerName: string;
93
+ userId: string; // 用于前端过滤掉自己的 canvas
94
+ }
95
+
96
+ function toSavedCanvasWithOwner(
97
+ r: CanvasRecordWithOwner,
98
+ ): SavedCanvasWithOwner {
99
+ return {
100
+ ...toSavedCanvas(r),
101
+ userId: r.userId,
102
+ ownerName: r.ownerName,
103
+ };
104
+ }
105
+
106
+ export async function fetchAllCanvases(): Promise<SavedCanvasWithOwner[]> {
107
+ const records = await customRequest<CanvasRecordWithOwner[]>(
108
+ 'canvas/all',
109
+ 'GET',
110
+ );
111
+ return records.map(toSavedCanvasWithOwner);
112
+ }
@@ -1,5 +1,6 @@
1
1
  import type React from 'react';
2
2
  import MultiChartConfigModal from './components/MultiChartConfigModal';
3
+ import TableConfigModal from './components/TableConfigModal';
3
4
 
4
5
  // ─── 通用配置 Modal 接口 ──────────────────────────────────────────────────────
5
6
  //
@@ -22,4 +23,5 @@ export type ConfigModalComponent = React.FC<CanvasConfigModalProps>;
22
23
 
23
24
  export const CANVAS_CONFIG_REGISTRY: Record<string, ConfigModalComponent> = {
24
25
  MultiChart: MultiChartConfigModal,
26
+ Table: TableConfigModal,
25
27
  };
@@ -6,7 +6,16 @@ import {
6
6
  EditOutlined,
7
7
  SwapOutlined,
8
8
  DownloadOutlined,
9
+ SettingOutlined,
9
10
  } from '@ant-design/icons';
11
+ import {
12
+ readTableFlag,
13
+ writeTableFlag,
14
+ parseMultiChartTableColumns,
15
+ readMultiChartTableFlags,
16
+ writeMultiChartTableFlags,
17
+ buildVirtualTableCode,
18
+ } from './utils/tableCodeUtils';
10
19
 
11
20
  // ─── 组件特有右键菜单操作注册表 ────────────────────────────────────────────────
12
21
  //
@@ -34,6 +43,16 @@ export interface MenuActionContext {
34
43
  defaultValue?: string;
35
44
  onConfirm: (value: string) => void;
36
45
  }) => void;
46
+
47
+ /**
48
+ * MultiChart 切到 table 时打开列配置 modal。
49
+ * - virtualCode:合成的独立 Table JSX,喂给 TableConfigModal
50
+ * - onConfirm:用户确认时回调,参数是 modal 改写后的 newVirtualCode
51
+ */
52
+ openTableColumnConfig?: (opts: {
53
+ virtualCode: string;
54
+ onConfirm: (newVirtualCode: string) => void;
55
+ }) => void;
37
56
  }
38
57
 
39
58
  // ─── NumCard 特有操作 ─────────────────────────────────────────────────────────
@@ -228,15 +247,52 @@ function tableActions(
228
247
  ctx?: MenuActionContext,
229
248
  t?: TFunction,
230
249
  ): MenuProps['items'] {
250
+ const sortableFlag = readTableFlag(code, 'sortable');
251
+ const filterableFlag = readTableFlag(code, 'filterable');
252
+ // Only 'fully on' (sortable={true} or omitted) shows "禁用排序";
253
+ // disabled and partial-array forms show "启用排序" (clicking restores default true).
254
+ const sortingActive = sortableFlag === true;
255
+ const filteringActive = filterableFlag === true;
231
256
  const hasPagination = /pagination\s*=\s*\{\{/.test(code);
232
257
 
258
+ const sortFilterItems: MenuProps['items'] = [
259
+ {
260
+ key: 'toggle-sortable',
261
+ icon: sortingActive ? <MinusOutlined /> : <PlusOutlined />,
262
+ label: sortingActive
263
+ ? (t?.('canvas.menu.disableSorting') ?? '禁用排序')
264
+ : (t?.('canvas.menu.enableSorting') ?? '启用排序'),
265
+ danger: sortingActive ? true : undefined,
266
+ onClick: () => {
267
+ updateCode(
268
+ writeTableFlag(code, 'sortable', sortingActive ? false : true),
269
+ );
270
+ closeMenu();
271
+ },
272
+ },
273
+ {
274
+ key: 'toggle-filterable',
275
+ icon: filteringActive ? <MinusOutlined /> : <PlusOutlined />,
276
+ label: filteringActive
277
+ ? (t?.('canvas.menu.disableFiltering') ?? '禁用过滤')
278
+ : (t?.('canvas.menu.enableFiltering') ?? '启用过滤'),
279
+ danger: filteringActive ? true : undefined,
280
+ onClick: () => {
281
+ updateCode(
282
+ writeTableFlag(code, 'filterable', filteringActive ? false : true),
283
+ );
284
+ closeMenu();
285
+ },
286
+ },
287
+ { type: 'divider' as const, key: 'div-sort-filter' },
288
+ ];
289
+
233
290
  const downloadItems = chartDownloadActions(code, updateCode, closeMenu, t)!;
234
291
 
235
292
  if (hasPagination) {
236
- // 已有分页:显示"修改分页数量"子菜单 + "移除分页"
237
293
  const currentPageSize = code.match(/pageSize\s*:\s*(\d+)/)?.[1] ?? '10';
238
-
239
294
  return [
295
+ ...sortFilterItems,
240
296
  {
241
297
  key: 'change-page-size',
242
298
  icon: <PlusOutlined />,
@@ -271,8 +327,8 @@ function tableActions(
271
327
  ];
272
328
  }
273
329
 
274
- // 无分页:显示"添加分页"子菜单,选择 pageSize
275
330
  return [
331
+ ...sortFilterItems,
276
332
  {
277
333
  key: 'add-pagination',
278
334
  icon: <PlusOutlined />,
@@ -1176,6 +1232,45 @@ function multiChartActions(
1176
1232
  label: t?.('canvas.menu.switchTo') ?? '切换到',
1177
1233
  children: switchItems,
1178
1234
  },
1235
+
1236
+ // ── MultiChart 切到 table 时的「配置列」入口 ──
1237
+ ...(activeChart === 'table' && ctx?.openTableColumnConfig
1238
+ ? [
1239
+ { type: 'divider' as const, key: 'div-multi-table-cols' },
1240
+ {
1241
+ key: 'multi-table-column-config',
1242
+ icon: <SettingOutlined />,
1243
+ label: t?.('canvas.menu.configColumns') ?? '配置列',
1244
+ onClick: () => {
1245
+ const columns = parseMultiChartTableColumns(code);
1246
+ const flags = readMultiChartTableFlags(code);
1247
+ // 解析失败也打开 modal —— modal 内部会显示 Alert + 禁用确认
1248
+ const virtualCode =
1249
+ columns && columns.length > 0
1250
+ ? buildVirtualTableCode(columns, flags)
1251
+ : `<Table dataSource={[]} columns={[]} testId="virtual-multichart-table" />`;
1252
+ closeMenu();
1253
+ ctx?.openTableColumnConfig?.({
1254
+ virtualCode,
1255
+ onConfirm: (newVirtualCode) => {
1256
+ const sortable = readTableFlag(newVirtualCode, 'sortable');
1257
+ const filterable = readTableFlag(
1258
+ newVirtualCode,
1259
+ 'filterable',
1260
+ );
1261
+ const newCode = writeMultiChartTableFlags(
1262
+ code,
1263
+ sortable,
1264
+ filterable,
1265
+ );
1266
+ updateCode(newCode);
1267
+ },
1268
+ });
1269
+ },
1270
+ },
1271
+ ]
1272
+ : []),
1273
+
1179
1274
  ...(chartSpecificItems && chartSpecificItems.length > 0
1180
1275
  ? [
1181
1276
  { type: 'divider' as const, key: 'div-chart-config' },
@@ -0,0 +1,192 @@
1
+ import React, { useMemo, useState } from 'react';
2
+ import { Modal, Table as AntTable, Checkbox, Alert } from 'antd';
3
+ import type { ColumnsType } from 'antd/es/table';
4
+ import { useTranslation } from 'react-i18next';
5
+ import type { CanvasConfigModalProps } from '../canvasConfigRegistry';
6
+ import {
7
+ CANVAS_MODAL_PROPS,
8
+ CANVAS_MODAL_TITLE_STYLE,
9
+ } from './canvasModalProps';
10
+ import {
11
+ parseTableColumnsMeta,
12
+ readTableFlag,
13
+ writeTableFlag,
14
+ } from '../utils/tableCodeUtils';
15
+ import type { ColumnMeta } from '../utils/tableCodeUtils';
16
+
17
+ interface Row extends ColumnMeta {
18
+ key: string; // = dataIndex
19
+ }
20
+
21
+ const TableConfigModal: React.FC<CanvasConfigModalProps> = ({
22
+ code,
23
+ onConfirm,
24
+ onCancel,
25
+ }) => {
26
+ const { t } = useTranslation();
27
+
28
+ // ── 解析 columns ─────────────────────────────────────────────
29
+ const columnsMeta = useMemo(() => parseTableColumnsMeta(code), [code]);
30
+ const parseFailed = columnsMeta == null || columnsMeta.length === 0;
31
+
32
+ const rows: Row[] = useMemo(
33
+ () => (columnsMeta ?? []).map((c) => ({ ...c, key: c.dataIndex })),
34
+ [columnsMeta],
35
+ );
36
+
37
+ // ── 解析当前 sortable / filterable 状态 ────────────────────
38
+ const initialSortSet = useMemo<Set<string>>(() => {
39
+ if (parseFailed) return new Set();
40
+ const flag = readTableFlag(code, 'sortable');
41
+ if (flag === false) return new Set();
42
+ if (flag === true) return new Set(rows.map((r) => r.dataIndex));
43
+ return new Set(flag);
44
+ }, [code, rows, parseFailed]);
45
+
46
+ const initialFilterSet = useMemo<Set<string>>(() => {
47
+ if (parseFailed) return new Set();
48
+ const flag = readTableFlag(code, 'filterable');
49
+ if (flag === false) return new Set();
50
+ if (flag === true) return new Set(rows.map((r) => r.dataIndex));
51
+ return new Set(flag);
52
+ }, [code, rows, parseFailed]);
53
+
54
+ const [sortSet, setSortSet] = useState<Set<string>>(initialSortSet);
55
+ const [filterSet, setFilterSet] = useState<Set<string>>(initialFilterSet);
56
+
57
+ // ── 全选 / 半选状态 ──────────────────────────────────────────
58
+ const totalCount = rows.length;
59
+ const sortAllChecked = totalCount > 0 && sortSet.size === totalCount;
60
+ const sortIndeterminate = sortSet.size > 0 && sortSet.size < totalCount;
61
+ const filterAllChecked = totalCount > 0 && filterSet.size === totalCount;
62
+ const filterIndeterminate = filterSet.size > 0 && filterSet.size < totalCount;
63
+
64
+ // ── 切换单行 ─────────────────────────────────────────────────
65
+ const toggle = (
66
+ set: Set<string>,
67
+ setter: React.Dispatch<React.SetStateAction<Set<string>>>,
68
+ dataIndex: string,
69
+ checked: boolean,
70
+ ) => {
71
+ const next = new Set(set);
72
+ if (checked) next.add(dataIndex);
73
+ else next.delete(dataIndex);
74
+ setter(next);
75
+ };
76
+
77
+ const toggleAll = (
78
+ setter: React.Dispatch<React.SetStateAction<Set<string>>>,
79
+ checked: boolean,
80
+ ) => {
81
+ setter(checked ? new Set(rows.map((r) => r.dataIndex)) : new Set());
82
+ };
83
+
84
+ // ── 写回 code ────────────────────────────────────────────────
85
+ const setToFlag = (set: Set<string>) => {
86
+ if (set.size === totalCount) return true; // 全开
87
+ if (set.size === 0) return false; // 全关
88
+ // 中间状态:保持 rows 顺序输出数组
89
+ return rows.map((r) => r.dataIndex).filter((d) => set.has(d));
90
+ };
91
+
92
+ const handleOk = () => {
93
+ let next = code;
94
+ next = writeTableFlag(next, 'sortable', setToFlag(sortSet));
95
+ next = writeTableFlag(next, 'filterable', setToFlag(filterSet));
96
+ onConfirm(next);
97
+ };
98
+
99
+ // ── 表格列定义 ───────────────────────────────────────────────
100
+ const tableColumns: ColumnsType<Row> = [
101
+ {
102
+ title: t('canvas.modal.columnName', { defaultValue: '列名' }),
103
+ dataIndex: 'title',
104
+ key: 'title',
105
+ },
106
+ {
107
+ title: (
108
+ <Checkbox
109
+ checked={sortAllChecked}
110
+ indeterminate={sortIndeterminate}
111
+ onChange={(e) => toggleAll(setSortSet, e.target.checked)}
112
+ >
113
+ {t('canvas.modal.sortable', { defaultValue: '排序' })}
114
+ </Checkbox>
115
+ ),
116
+ key: 'sortable',
117
+ width: 100,
118
+ align: 'center',
119
+ render: (_v, row) => (
120
+ <Checkbox
121
+ checked={sortSet.has(row.dataIndex)}
122
+ onChange={(e) =>
123
+ toggle(sortSet, setSortSet, row.dataIndex, e.target.checked)
124
+ }
125
+ />
126
+ ),
127
+ },
128
+ {
129
+ title: (
130
+ <Checkbox
131
+ checked={filterAllChecked}
132
+ indeterminate={filterIndeterminate}
133
+ onChange={(e) => toggleAll(setFilterSet, e.target.checked)}
134
+ >
135
+ {t('canvas.modal.filterable', { defaultValue: '过滤' })}
136
+ </Checkbox>
137
+ ),
138
+ key: 'filterable',
139
+ width: 100,
140
+ align: 'center',
141
+ render: (_v, row) => (
142
+ <Checkbox
143
+ checked={filterSet.has(row.dataIndex)}
144
+ onChange={(e) =>
145
+ toggle(filterSet, setFilterSet, row.dataIndex, e.target.checked)
146
+ }
147
+ />
148
+ ),
149
+ },
150
+ ];
151
+
152
+ return (
153
+ <Modal
154
+ {...CANVAS_MODAL_PROPS}
155
+ width={520}
156
+ open
157
+ title={
158
+ <span style={CANVAS_MODAL_TITLE_STYLE}>
159
+ {t('canvas.modal.columnConfigTitle', { defaultValue: '列配置' })}
160
+ </span>
161
+ }
162
+ okText={t('canvas.modal.ok', { defaultValue: '确认' })}
163
+ cancelText={t('canvas.modal.cancel', { defaultValue: '取消' })}
164
+ onOk={handleOk}
165
+ onCancel={onCancel}
166
+ okButtonProps={{ disabled: parseFailed }}
167
+ >
168
+ {parseFailed && (
169
+ <Alert
170
+ type="warning"
171
+ showIcon
172
+ style={{ marginBottom: 12 }}
173
+ message={t('canvas.modal.parseFailed', {
174
+ defaultValue: '解析列失败,请使用右键菜单的整表开关。',
175
+ })}
176
+ />
177
+ )}
178
+ {!parseFailed && (
179
+ <div data-testid="canvas-table-config-modal-table">
180
+ <AntTable<Row>
181
+ dataSource={rows}
182
+ columns={tableColumns}
183
+ pagination={false}
184
+ size="small"
185
+ />
186
+ </div>
187
+ )}
188
+ </Modal>
189
+ );
190
+ };
191
+
192
+ export default TableConfigModal;