@gadmin2n/schematics 0.0.95 → 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/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 +1 -1
- 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
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
Body,
|
|
3
3
|
Controller,
|
|
4
4
|
Delete,
|
|
5
|
+
ForbiddenException,
|
|
5
6
|
Get,
|
|
6
7
|
Param,
|
|
7
8
|
ParseIntPipe,
|
|
@@ -11,6 +12,7 @@ import {
|
|
|
11
12
|
UseGuards,
|
|
12
13
|
} from '@nestjs/common';
|
|
13
14
|
import { ApiTags } from '@nestjs/swagger';
|
|
15
|
+
import { PrismaService } from 'nestjs-prisma';
|
|
14
16
|
import AuthGuard, { AllowUnauthorizedRequest } from '../../lib/auth.guard';
|
|
15
17
|
import {
|
|
16
18
|
CreateCanvasDto,
|
|
@@ -24,13 +26,29 @@ import { CanvasService } from './canvas.service';
|
|
|
24
26
|
@UseGuards(AuthGuard)
|
|
25
27
|
@Controller('canvas')
|
|
26
28
|
export class CanvasController {
|
|
27
|
-
constructor(
|
|
29
|
+
constructor(
|
|
30
|
+
private readonly canvasService: CanvasService,
|
|
31
|
+
private readonly prisma: PrismaService,
|
|
32
|
+
) {}
|
|
28
33
|
|
|
29
34
|
@Get()
|
|
30
35
|
findAll(@Req() req) {
|
|
31
36
|
return this.canvasService.findAllByUser(req.user.userid);
|
|
32
37
|
}
|
|
33
38
|
|
|
39
|
+
@Get('all')
|
|
40
|
+
async findAllForAdmin(@Req() req) {
|
|
41
|
+
const record = await this.prisma.user.findFirst({
|
|
42
|
+
where: { userid: req.user.userid },
|
|
43
|
+
select: { roles: true },
|
|
44
|
+
});
|
|
45
|
+
const roles: string[] = record ? JSON.parse(record.roles) : [];
|
|
46
|
+
if (!roles.includes('SYSTEM_ADMIN')) {
|
|
47
|
+
throw new ForbiddenException('仅 SYSTEM_ADMIN 可访问');
|
|
48
|
+
}
|
|
49
|
+
return this.canvasService.findAllCanvases();
|
|
50
|
+
}
|
|
51
|
+
|
|
34
52
|
@Post()
|
|
35
53
|
create(@Req() req, @Body() dto: CreateCanvasDto) {
|
|
36
54
|
return this.canvasService.create(req.user.userid, dto);
|
|
@@ -56,12 +74,23 @@ export class CanvasController {
|
|
|
56
74
|
|
|
57
75
|
@Get(':id')
|
|
58
76
|
findOne(@Req() req, @Param('id') id: string) {
|
|
59
|
-
|
|
77
|
+
const roles: string[] = req.user?.configPerms?.roles ?? [];
|
|
78
|
+
const isAdmin = roles.includes('SYSTEM_ADMIN');
|
|
79
|
+
return this.canvasService.findOne(
|
|
80
|
+
id,
|
|
81
|
+
isAdmin ? undefined : req.user.userid,
|
|
82
|
+
);
|
|
60
83
|
}
|
|
61
84
|
|
|
62
85
|
@Put(':id')
|
|
63
86
|
update(@Req() req, @Param('id') id: string, @Body() dto: UpdateCanvasDto) {
|
|
64
|
-
|
|
87
|
+
const roles: string[] = req.user?.configPerms?.roles ?? [];
|
|
88
|
+
const isAdmin = roles.includes('SYSTEM_ADMIN');
|
|
89
|
+
return this.canvasService.update(
|
|
90
|
+
id,
|
|
91
|
+
isAdmin ? undefined : req.user.userid,
|
|
92
|
+
dto,
|
|
93
|
+
);
|
|
65
94
|
}
|
|
66
95
|
|
|
67
96
|
@Delete(':id')
|
|
@@ -25,9 +25,9 @@ export class CanvasService {
|
|
|
25
25
|
});
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
findOne(id: string, userId
|
|
28
|
+
findOne(id: string, userId?: string) {
|
|
29
29
|
return this.prisma.canvas.findFirst({
|
|
30
|
-
where: { id, userId },
|
|
30
|
+
where: userId ? { id, userId } : { id },
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -58,10 +58,12 @@ export class CanvasService {
|
|
|
58
58
|
});
|
|
59
59
|
}
|
|
60
60
|
|
|
61
|
-
async update(id: string, userId: string, dto: UpdateCanvasDto) {
|
|
61
|
+
async update(id: string, userId: string | undefined, dto: UpdateCanvasDto) {
|
|
62
62
|
if (dto.name !== undefined) {
|
|
63
63
|
const existing = await this.prisma.canvas.findFirst({
|
|
64
|
-
where:
|
|
64
|
+
where: userId
|
|
65
|
+
? { userId, name: dto.name, NOT: { id } }
|
|
66
|
+
: { name: dto.name, NOT: { id } },
|
|
65
67
|
});
|
|
66
68
|
if (existing) {
|
|
67
69
|
throw new ConflictException('Canvas 名称已存在');
|
|
@@ -71,7 +73,7 @@ export class CanvasService {
|
|
|
71
73
|
// 如果改名且已发布,同步更新 page 表的所有名称字段 + code + path
|
|
72
74
|
if (dto.name !== undefined) {
|
|
73
75
|
const canvas = await this.prisma.canvas.findFirst({
|
|
74
|
-
where: { id, userId },
|
|
76
|
+
where: userId ? { id, userId } : { id },
|
|
75
77
|
});
|
|
76
78
|
if (canvas?.publishedPageCode) {
|
|
77
79
|
const newSlug =
|
|
@@ -100,7 +102,7 @@ export class CanvasService {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
return this.prisma.canvas.updateMany({
|
|
103
|
-
where: { id, userId },
|
|
105
|
+
where: userId ? { id, userId } : { id },
|
|
104
106
|
data: {
|
|
105
107
|
...(dto.name !== undefined && { name: dto.name }),
|
|
106
108
|
...(dto.items !== undefined && { items: dto.items }),
|
|
@@ -228,6 +230,26 @@ export class CanvasService {
|
|
|
228
230
|
return { success: true };
|
|
229
231
|
}
|
|
230
232
|
|
|
233
|
+
async findAllCanvases() {
|
|
234
|
+
const canvases = await this.prisma.canvas.findMany({
|
|
235
|
+
orderBy: { createdAt: 'asc' },
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const userIds = [...new Set(canvases.map((c) => c.userId))];
|
|
239
|
+
const users = await this.prisma.user.findMany({
|
|
240
|
+
where: { userid: { in: userIds } },
|
|
241
|
+
select: { userid: true, username: true },
|
|
242
|
+
});
|
|
243
|
+
const userMap = new Map(
|
|
244
|
+
users.map((u) => [u.userid, u.username ?? u.userid]),
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
return canvases.map((c) => ({
|
|
248
|
+
...c,
|
|
249
|
+
ownerName: userMap.get(c.userId) ?? c.userId,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
|
|
231
253
|
async savedQueryCreate(dto: CreateSavedQueryDto) {
|
|
232
254
|
const sql = dto.sql.trim();
|
|
233
255
|
if (!sql.toUpperCase().startsWith('SELECT')) {
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"@dnd-kit/modifiers": "^9.0.0",
|
|
11
11
|
"@dnd-kit/sortable": "^7.0.2",
|
|
12
12
|
"@dnd-kit/utilities": "^3.2.2",
|
|
13
|
-
"@gadmin2n/charts": "^0.0.
|
|
13
|
+
"@gadmin2n/charts": "^0.0.8",
|
|
14
14
|
"@gadmin2n/react-common": "^0.0.70",
|
|
15
15
|
"@monaco-editor/react": "^4.7.0",
|
|
16
16
|
"@refinedev/antd": "^5.47.0",
|
|
@@ -27,12 +27,28 @@ export function generatePrompt(options: PromptOptions): string {
|
|
|
27
27
|
parts.push('');
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
parts.push(`严格按照以下 skill 完成代码修改:`);
|
|
31
|
+
parts.push(
|
|
32
|
+
`1. **canvas-component-edit skill**(位于 .claude/skills/canvas-component-edit/SKILL.md):负责修改流程和推送`,
|
|
33
|
+
);
|
|
34
|
+
parts.push(
|
|
35
|
+
`2. **canvas-design-standard skill**(位于 .claude/skills/canvas-design-standard/SKILL.md):所有手写 UI 代码必须遵循此规范`,
|
|
36
|
+
);
|
|
37
|
+
if (skill && skill !== 'canvas-component-edit') {
|
|
31
38
|
parts.push(
|
|
32
|
-
|
|
39
|
+
`3. **${skill} skill**(位于 .claude/skills/${skill}/SKILL.md):本次任务的主要执行规范`,
|
|
33
40
|
);
|
|
34
|
-
parts.push('');
|
|
35
41
|
}
|
|
42
|
+
parts.push(``);
|
|
43
|
+
parts.push(`**关于第 2 条的强制要求:**`);
|
|
44
|
+
parts.push(
|
|
45
|
+
`- 最终代码中禁止出现原生 HTML 表单元素(\`<input>\`、\`<button>\`、\`<select>\` 等)`,
|
|
46
|
+
);
|
|
47
|
+
parts.push(
|
|
48
|
+
`- 所有 UI 元素必须使用等价的 Ant Design 组件(如 \`Input\`、\`Button\`、\`Space\` 等)`,
|
|
49
|
+
);
|
|
50
|
+
parts.push(`- 间距使用规范值(8px / 12px / 16px),不得随意硬编码`);
|
|
51
|
+
parts.push('');
|
|
36
52
|
|
|
37
53
|
const canvasItemId = inspectedElement.meta?.canvasItemId ?? '';
|
|
38
54
|
parts.push('### 更多辅助信息:');
|
|
@@ -266,10 +266,26 @@
|
|
|
266
266
|
"enableSmooth": "Enable Smooth",
|
|
267
267
|
"switchTo": "Switch to",
|
|
268
268
|
"showDownload": "Show Download",
|
|
269
|
-
"hideDownload": "Hide Download"
|
|
269
|
+
"hideDownload": "Hide Download",
|
|
270
|
+
"enableSorting": "Enable Sorting",
|
|
271
|
+
"disableSorting": "Disable Sorting",
|
|
272
|
+
"enableFiltering": "Enable Filtering",
|
|
273
|
+
"disableFiltering": "Disable Filtering",
|
|
274
|
+
"configColumns": "Configure Columns"
|
|
275
|
+
},
|
|
276
|
+
"modal": {
|
|
277
|
+
"columnConfigTitle": "Column Settings",
|
|
278
|
+
"columnName": "Column",
|
|
279
|
+
"sortable": "Sort",
|
|
280
|
+
"filterable": "Filter",
|
|
281
|
+
"ok": "OK",
|
|
282
|
+
"cancel": "Cancel",
|
|
283
|
+
"parseFailed": "Failed to parse columns. Please use the table-level toggle from the right-click menu."
|
|
270
284
|
},
|
|
271
285
|
"list": {
|
|
272
|
-
"title": "
|
|
286
|
+
"title": "Canvas",
|
|
287
|
+
"myCanvases": "My Canvas",
|
|
288
|
+
"othersCanvases": "Others' Canvas",
|
|
273
289
|
"create": "New Canvas",
|
|
274
290
|
"published": "Published",
|
|
275
291
|
"unpublished": "Unpublished",
|
package/dist/lib/application/files/gadmin2-game-angle-demo/web/src/locales/zh_CN/common.json
CHANGED
|
@@ -268,10 +268,26 @@
|
|
|
268
268
|
"enableSmooth": "开启拟合曲线",
|
|
269
269
|
"switchTo": "切换到",
|
|
270
270
|
"showDownload": "显示下载按钮",
|
|
271
|
-
"hideDownload": "隐藏下载按钮"
|
|
271
|
+
"hideDownload": "隐藏下载按钮",
|
|
272
|
+
"enableSorting": "启用排序",
|
|
273
|
+
"disableSorting": "禁用排序",
|
|
274
|
+
"enableFiltering": "启用过滤",
|
|
275
|
+
"disableFiltering": "禁用过滤",
|
|
276
|
+
"configColumns": "配置列"
|
|
277
|
+
},
|
|
278
|
+
"modal": {
|
|
279
|
+
"columnConfigTitle": "列配置",
|
|
280
|
+
"columnName": "列名",
|
|
281
|
+
"sortable": "排序",
|
|
282
|
+
"filterable": "过滤",
|
|
283
|
+
"ok": "确认",
|
|
284
|
+
"cancel": "取消",
|
|
285
|
+
"parseFailed": "解析列失败,请使用右键菜单的整表开关。"
|
|
272
286
|
},
|
|
273
287
|
"list": {
|
|
274
|
-
"title": "
|
|
288
|
+
"title": "Canvas",
|
|
289
|
+
"myCanvases": "我的 Canvas",
|
|
290
|
+
"othersCanvases": "其他人的 Canvas",
|
|
275
291
|
"create": "新建 Canvas",
|
|
276
292
|
"published": "已发布",
|
|
277
293
|
"unpublished": "未发布",
|
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;
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canvas Table 代码字符串解析 / 改写工具。
|
|
3
|
+
*
|
|
4
|
+
* 这些函数全部以 JSX 字符串为输入输出,不操作 React 组件实例。
|
|
5
|
+
* 由 TableConfigModal 与 canvasContextMenuRegistry 中的 tableActions 共用。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export interface ColumnMeta {
|
|
9
|
+
title: string;
|
|
10
|
+
dataIndex: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type SortFilterFlag = true | false | string[];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* 在 JSX 对象字面量片段中读取一个字符串属性的值,支持双引号 / 单引号字面量
|
|
17
|
+
* 与 \" \\ \n 等转义序列。无法解析时返回 null。
|
|
18
|
+
*/
|
|
19
|
+
function readStringProp(obj: string, propName: string): string | null {
|
|
20
|
+
// 双引号字面量:(?:[^"\\]|\\.)* 表示除 " 和 \ 外的字符 或 反斜杠+任意单字符
|
|
21
|
+
const dq = obj.match(
|
|
22
|
+
new RegExp(`${propName}\\s*:\\s*("(?:[^"\\\\]|\\\\.)*")`),
|
|
23
|
+
);
|
|
24
|
+
if (dq) {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(dq[1]);
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const sq = obj.match(
|
|
32
|
+
new RegExp(`${propName}\\s*:\\s*'((?:[^'\\\\]|\\\\.)*)'`),
|
|
33
|
+
);
|
|
34
|
+
if (sq) {
|
|
35
|
+
// 把单引号字面量转成 JSON 双引号字面量(替换未转义的双引号、保留已转义序列)
|
|
36
|
+
const inner = sq[1].replace(/(^|[^\\])"/g, '$1\\"');
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(`"${inner}"`);
|
|
39
|
+
} catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 从 Table 组件的 JSX 字符串中解析 columns 数组里每列的 title/dataIndex。
|
|
48
|
+
*
|
|
49
|
+
* 解析失败(columns 含复杂 render 函数、JSON 不规范等)时返回 null。
|
|
50
|
+
* 调用方据此显示 Alert + 禁用确认。
|
|
51
|
+
*/
|
|
52
|
+
export function parseTableColumnsMeta(code: string): ColumnMeta[] | null {
|
|
53
|
+
const match = code.match(/columns\s*=\s*\{(\[[\s\S]*?\])\}/);
|
|
54
|
+
if (!match) return null;
|
|
55
|
+
const arrayLiteral = match[1];
|
|
56
|
+
|
|
57
|
+
// 用 regex 提取每个 { ... } 对象内的 title 和 dataIndex 字符串字面量。
|
|
58
|
+
// 这种方法对含 render: (v) => ... 的列也容错(直接忽略 render)。
|
|
59
|
+
const objects = arrayLiteral.match(/\{[^{}]*\}/g);
|
|
60
|
+
if (!objects || objects.length === 0) return null;
|
|
61
|
+
|
|
62
|
+
const result: ColumnMeta[] = [];
|
|
63
|
+
for (const obj of objects) {
|
|
64
|
+
const title = readStringProp(obj, 'title');
|
|
65
|
+
const dataIndex = readStringProp(obj, 'dataIndex');
|
|
66
|
+
if (title != null && dataIndex != null) {
|
|
67
|
+
result.push({ title, dataIndex });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return result.length > 0 ? result : null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 从 code 字符串中读取 sortable / filterable prop 当前状态。
|
|
75
|
+
*
|
|
76
|
+
* 返回:
|
|
77
|
+
* - true:未设置或显式 {true}(默认全开)
|
|
78
|
+
* - false:显式 {false}
|
|
79
|
+
* - string[]:显式数组形式 {[...]}
|
|
80
|
+
*/
|
|
81
|
+
export function readTableFlag(
|
|
82
|
+
code: string,
|
|
83
|
+
propName: 'sortable' | 'filterable',
|
|
84
|
+
): SortFilterFlag {
|
|
85
|
+
if (new RegExp(`\\b${propName}\\s*=\\s*\\{false\\}`).test(code)) return false;
|
|
86
|
+
const arrayMatch = code.match(
|
|
87
|
+
new RegExp(`\\b${propName}\\s*=\\s*\\{(\\[[^\\]]*\\])\\}`),
|
|
88
|
+
);
|
|
89
|
+
if (arrayMatch) {
|
|
90
|
+
try {
|
|
91
|
+
const parsed = JSON.parse(arrayMatch[1].replace(/'/g, '"'));
|
|
92
|
+
if (Array.isArray(parsed)) return parsed as string[];
|
|
93
|
+
} catch {
|
|
94
|
+
// 解析失败回退为 true
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* 把 sortable / filterable prop 写回 code 字符串。
|
|
102
|
+
*
|
|
103
|
+
* - flag === true:删除 prop(恢复默认)
|
|
104
|
+
* - flag === false:替换/插入 `propName={false}`
|
|
105
|
+
* - flag === string[]:替换/插入 `propName={["a","b"]}`
|
|
106
|
+
*
|
|
107
|
+
* 注入位置:testId= 那行之前;找不到 testId 则回退到 /> 之前。
|
|
108
|
+
*/
|
|
109
|
+
export function writeTableFlag(
|
|
110
|
+
code: string,
|
|
111
|
+
propName: 'sortable' | 'filterable',
|
|
112
|
+
flag: SortFilterFlag,
|
|
113
|
+
): string {
|
|
114
|
+
// 先删除现有 prop(无论什么形式)
|
|
115
|
+
let next = code.replace(
|
|
116
|
+
new RegExp(`\\s*\\b${propName}\\s*=\\s*\\{(true|false|\\[[^\\]]*\\])\\}`),
|
|
117
|
+
'',
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
if (flag === true) return next;
|
|
121
|
+
|
|
122
|
+
const value = flag === false ? '{false}' : `{${JSON.stringify(flag)}}`;
|
|
123
|
+
const inject = `${propName}=${value}`;
|
|
124
|
+
|
|
125
|
+
if (/testId\s*=/.test(next)) {
|
|
126
|
+
next = next.replace(/([ \t]*)(testId\s*=)/, `$1${inject}\n$1$2`);
|
|
127
|
+
} else {
|
|
128
|
+
next = next.replace(/\/>/, ` ${inject}\n/>`);
|
|
129
|
+
}
|
|
130
|
+
return next;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── MultiChart-as-table 列配置 helpers ────────────────────────────────
|
|
134
|
+
|
|
135
|
+
/** 在 s 中从 start 位置(指向 open)开始,找出与之匹配的 close 字符的位置;不平衡返回 -1。 */
|
|
136
|
+
function findBalancedClose(
|
|
137
|
+
s: string,
|
|
138
|
+
start: number,
|
|
139
|
+
open: string,
|
|
140
|
+
close: string,
|
|
141
|
+
): number {
|
|
142
|
+
let depth = 0;
|
|
143
|
+
for (let i = start; i < s.length; i++) {
|
|
144
|
+
if (s[i] === open) depth++;
|
|
145
|
+
else if (s[i] === close) {
|
|
146
|
+
depth--;
|
|
147
|
+
if (depth === 0) return i;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return -1;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 定位 chartConfig={...} 的 span。
|
|
155
|
+
* - start:'chartConfig' 文字开始位置
|
|
156
|
+
* - end:JSX 闭合 '}' 之后的位置(半开区间)
|
|
157
|
+
* - jsonStr:JSX 大括号里面的内容(trim 后),通常是一个 JSON 对象字面量
|
|
158
|
+
*
|
|
159
|
+
* 找不到返回 null。
|
|
160
|
+
*/
|
|
161
|
+
function findChartConfigSpan(
|
|
162
|
+
code: string,
|
|
163
|
+
): { start: number; end: number; jsonStr: string } | null {
|
|
164
|
+
const m = code.match(/chartConfig\s*=\s*\{/);
|
|
165
|
+
if (!m || m.index == null) return null;
|
|
166
|
+
const propStart = m.index;
|
|
167
|
+
const jsxOpen = propStart + m[0].length - 1;
|
|
168
|
+
const jsxClose = findBalancedClose(code, jsxOpen, '{', '}');
|
|
169
|
+
if (jsxClose === -1) return null;
|
|
170
|
+
const jsonStr = code.slice(jsxOpen + 1, jsxClose).trim();
|
|
171
|
+
return { start: propStart, end: jsxClose + 1, jsonStr };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* 从 MultiChart code 字符串中解析 data prop,推导出 antd Table columns 元数据。
|
|
176
|
+
* 与 MultiChart 内部 toTableData(data) 保持一致:
|
|
177
|
+
* - 第一列 "分类" / dataIndex "__x"
|
|
178
|
+
* - 后续列对应 data.series 各项的 name
|
|
179
|
+
*
|
|
180
|
+
* 解析失败返回 null(调用方据此显示 Alert + 禁用确认)。
|
|
181
|
+
*
|
|
182
|
+
* 内部用平衡大括号匹配定位 data={{...}},避免 lazy 正则因 series 中嵌套 [...] 而截断。
|
|
183
|
+
*/
|
|
184
|
+
export function parseMultiChartTableColumns(code: string): ColumnMeta[] | null {
|
|
185
|
+
const m = code.match(/data\s*=\s*\{/);
|
|
186
|
+
if (!m || m.index == null) return null;
|
|
187
|
+
const jsxOpen = m.index + m[0].length - 1;
|
|
188
|
+
const jsxClose = findBalancedClose(code, jsxOpen, '{', '}');
|
|
189
|
+
if (jsxClose === -1) return null;
|
|
190
|
+
const expr = code.slice(jsxOpen + 1, jsxClose);
|
|
191
|
+
|
|
192
|
+
// 仅在 series: [...] 范围内抽 name 字段,避免 tooltip.name / itemStyle.name 等污染
|
|
193
|
+
const seriesIdx = expr.search(/series\s*:\s*\[/);
|
|
194
|
+
if (seriesIdx === -1) return null;
|
|
195
|
+
const arrOpen = expr.indexOf('[', seriesIdx);
|
|
196
|
+
if (arrOpen === -1) return null;
|
|
197
|
+
const arrClose = findBalancedClose(expr, arrOpen, '[', ']');
|
|
198
|
+
if (arrClose === -1) return null;
|
|
199
|
+
const seriesBody = expr.slice(arrOpen + 1, arrClose);
|
|
200
|
+
|
|
201
|
+
const nameMatches = Array.from(
|
|
202
|
+
seriesBody.matchAll(/name\s*:\s*['"]([^'"]+)['"]/g),
|
|
203
|
+
);
|
|
204
|
+
const seriesNames = nameMatches.map((mm) => mm[1]);
|
|
205
|
+
if (seriesNames.length === 0) return null;
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
{ title: '分类', dataIndex: '__x' },
|
|
209
|
+
...seriesNames.map((name) => ({ title: name, dataIndex: name })),
|
|
210
|
+
];
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* 从 MultiChart code 中读取 chartConfig.table.sortable / .filterable。
|
|
215
|
+
* 三态语义同 readTableFlag。缺省返回 { sortable: true, filterable: true }。
|
|
216
|
+
*/
|
|
217
|
+
export function readMultiChartTableFlags(code: string): {
|
|
218
|
+
sortable: SortFilterFlag;
|
|
219
|
+
filterable: SortFilterFlag;
|
|
220
|
+
} {
|
|
221
|
+
const span = findChartConfigSpan(code);
|
|
222
|
+
if (!span) return { sortable: true, filterable: true };
|
|
223
|
+
|
|
224
|
+
let cfg: Record<string, Record<string, unknown>>;
|
|
225
|
+
try {
|
|
226
|
+
cfg = JSON.parse(span.jsonStr);
|
|
227
|
+
} catch {
|
|
228
|
+
return { sortable: true, filterable: true };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const tableCfg = cfg.table ?? {};
|
|
232
|
+
const readFlag = (v: unknown): SortFilterFlag => {
|
|
233
|
+
if (v === false) return false;
|
|
234
|
+
if (Array.isArray(v)) return v as string[];
|
|
235
|
+
return true;
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
sortable: readFlag(tableCfg.sortable),
|
|
240
|
+
filterable: readFlag(tableCfg.filterable),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* 把 sortable / filterable 写回 MultiChart code 的 chartConfig.table。
|
|
246
|
+
* - true → 删除该字段(恢复默认)
|
|
247
|
+
* - false → 写 false
|
|
248
|
+
* - string[] → 写数组
|
|
249
|
+
*
|
|
250
|
+
* 清理空对象:tableCfg 为空 → 删 chartConfig.table;
|
|
251
|
+
* chartConfig 整体为空 → 删 prop(同时吃掉前置的换行与缩进,避免空行)。
|
|
252
|
+
* 保留其它图表的配置(chartConfig.bar / .line 等)不变。
|
|
253
|
+
*/
|
|
254
|
+
export function writeMultiChartTableFlags(
|
|
255
|
+
code: string,
|
|
256
|
+
sortable: SortFilterFlag,
|
|
257
|
+
filterable: SortFilterFlag,
|
|
258
|
+
): string {
|
|
259
|
+
const span = findChartConfigSpan(code);
|
|
260
|
+
let cfg: Record<string, Record<string, unknown>> = {};
|
|
261
|
+
if (span) {
|
|
262
|
+
try {
|
|
263
|
+
cfg = JSON.parse(span.jsonStr);
|
|
264
|
+
} catch {
|
|
265
|
+
cfg = {};
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const tableCfg = { ...(cfg.table ?? {}) };
|
|
270
|
+
|
|
271
|
+
if (sortable === true) delete tableCfg.sortable;
|
|
272
|
+
else tableCfg.sortable = sortable;
|
|
273
|
+
|
|
274
|
+
if (filterable === true) delete tableCfg.filterable;
|
|
275
|
+
else tableCfg.filterable = filterable;
|
|
276
|
+
|
|
277
|
+
const newCfg: Record<string, Record<string, unknown>> = { ...cfg };
|
|
278
|
+
if (Object.keys(tableCfg).length === 0) {
|
|
279
|
+
delete newCfg.table;
|
|
280
|
+
} else {
|
|
281
|
+
newCfg.table = tableCfg;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// chartConfig 整体为空 → 删除整段 prop
|
|
285
|
+
if (Object.keys(newCfg).length === 0) {
|
|
286
|
+
if (!span) return code;
|
|
287
|
+
// 向前吃 ' ' / '\t',以及紧贴的一个 '\n',避免留下空行
|
|
288
|
+
let lead = span.start;
|
|
289
|
+
while (lead > 0 && (code[lead - 1] === ' ' || code[lead - 1] === '\t'))
|
|
290
|
+
lead--;
|
|
291
|
+
if (lead > 0 && code[lead - 1] === '\n') lead--;
|
|
292
|
+
return code.slice(0, lead) + code.slice(span.end);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const newCfgStr = `chartConfig={${JSON.stringify(newCfg)}}`;
|
|
296
|
+
if (span) {
|
|
297
|
+
return code.slice(0, span.start) + newCfgStr + code.slice(span.end);
|
|
298
|
+
}
|
|
299
|
+
// 没有 chartConfig prop,注入到 testId 之前
|
|
300
|
+
if (/testId\s*=/.test(code)) {
|
|
301
|
+
return code.replace(/([ \t]*)(testId\s*=)/, `$1${newCfgStr}\n$1$2`);
|
|
302
|
+
}
|
|
303
|
+
return code.replace(/\/>/, ` ${newCfgStr}\n/>`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 构造一段虚拟独立 Table JSX,用于喂给 TableConfigModal。
|
|
308
|
+
* Modal 内部走原有 parseTableColumnsMeta / readTableFlag 路径,
|
|
309
|
+
* 完全感知不到这段 code 是合成的。
|
|
310
|
+
*
|
|
311
|
+
* 注意:columns 必须输出双引号字符串字面量,否则 parseTableColumnsMeta
|
|
312
|
+
* 的正则匹配不到 title / dataIndex。
|
|
313
|
+
*/
|
|
314
|
+
export function buildVirtualTableCode(
|
|
315
|
+
columns: ColumnMeta[],
|
|
316
|
+
flags: { sortable: SortFilterFlag; filterable: SortFilterFlag },
|
|
317
|
+
): string {
|
|
318
|
+
const colsStr = columns
|
|
319
|
+
.map(
|
|
320
|
+
(c) =>
|
|
321
|
+
` { title: ${JSON.stringify(c.title)}, dataIndex: ${JSON.stringify(c.dataIndex)}, key: ${JSON.stringify(c.dataIndex)} }`,
|
|
322
|
+
)
|
|
323
|
+
.join(',\n');
|
|
324
|
+
|
|
325
|
+
const flagToProp = (name: string, flag: SortFilterFlag): string => {
|
|
326
|
+
if (flag === true) return '';
|
|
327
|
+
if (flag === false) return ` ${name}={false}\n`;
|
|
328
|
+
return ` ${name}={${JSON.stringify(flag)}}\n`;
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
return `<Table
|
|
332
|
+
dataSource={[]}
|
|
333
|
+
columns={[
|
|
334
|
+
${colsStr}
|
|
335
|
+
]}
|
|
336
|
+
${flagToProp('sortable', flags.sortable)}${flagToProp('filterable', flags.filterable)} testId="virtual-multichart-table"
|
|
337
|
+
/>`;
|
|
338
|
+
}
|