@gadmin2n/schematics 0.0.94 → 0.0.96
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/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/admin-code.ts +14 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/page-helpers.ts +17 -5
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/scripts/lib/resync-sequences.ts +89 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/agenda.seed.ts +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/audit.seed.ts +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/bootstrap.ts +23 -13
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/canvas.seed.ts +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/game.seed.ts +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/index.ts +4 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/seed/permission.seed.ts +4 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.controller.ts +32 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/server/src/modules/canvas/canvas.service.ts +28 -6
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/package.json +2 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/components/agentPanel/promptGenerator.ts +19 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/en/common.json +18 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json +18 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx +173 -1
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx +27 -2
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts +29 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasConfigRegistry.tsx +2 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasContextMenuRegistry.tsx +98 -3
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/components/TableConfigModal.tsx +192 -0
- package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/utils/tableCodeUtils.ts +338 -0
- package/package.json +1 -1
- /package/dist/lib/application/files/gadmin2-game-angle-demo/{server/GRACEFUL-DEPLOYMENT.md → server-graceful-depoyment.md} +0 -0
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasListPage.tsx
CHANGED
|
@@ -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
|
-
|
|
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')}
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/CanvasPage.tsx
CHANGED
|
@@ -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
|
-
:
|
|
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:
|
|
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}
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/routes/canvas/canvasApi.ts
CHANGED
|
@@ -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;
|