@gadmin2n/schematics 0.0.109 → 0.0.111

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.
@@ -30,18 +30,11 @@ export const Event: ModelConfig = {
30
30
  fields: [
31
31
  "*",
32
32
  "!created_at",
33
- "!businessRequester",
34
- "!number",
35
- "!additionalInfo",
36
- "!snSysId",
37
- "!oitOwner",
38
33
  "!createdAt",
39
34
  "!endDate",
40
35
  "!creator",
41
36
  "!updatedAt",
42
37
  "!startDate",
43
- "!eventType",
44
- "!country",
45
38
  ],
46
39
  rowSelection: {
47
40
  actions: [
@@ -86,11 +79,6 @@ export const Event: ModelConfig = {
86
79
  fields: [
87
80
  "*",
88
81
  "!created_at",
89
- "!businessRequester",
90
- "!number",
91
- "!additionalInfo",
92
- "!snSysId",
93
- "!oitOwner",
94
82
  "!createdAt",
95
83
  "!endDate",
96
84
  "!creator",
@@ -27,7 +27,7 @@ export const ITActivityDay: ModelConfig = {
27
27
  filter: [],
28
28
  sorter: ["id"],
29
29
  },
30
- fields: ["*", "!createdAt", "!updatedAt", "!dateKey"],
30
+ fields: ["*", "!createdAt", "!updatedAt"],
31
31
  rowSelection: {
32
32
  actions: [
33
33
  { action: "DELETE", desc: "Delete" },
@@ -10,12 +10,15 @@ import AddDataModal from './AddDataModal';
10
10
  import AddPageModal from './AddPageModal';
11
11
  import DeleteDataConfirm from './DeleteDataConfirm';
12
12
  import EditDataModal from './EditDataModal';
13
+ import OnboardingModal from './OnboardingModal';
13
14
  import { resolvePagePaths } from '../components/agentPanel/pagePathUtils';
14
15
  import SkillMenu from './SkillMenu';
15
16
  import { agentAllowedPromise } from '../config/agentAllowed';
16
17
  import './style.css';
17
18
  import UndoConfirm from './UndoConfirm';
18
19
 
20
+ const ONBOARDING_KEY = 'gadmin-onboarded';
21
+
19
22
  declare global {
20
23
  interface Window {
21
24
  ChatSDK: any;
@@ -82,6 +85,9 @@ function DevShellInner() {
82
85
  const [modal, setModal] = useState<ModalType>(null);
83
86
  const [isDataMngtPage, setIsDataMngtPage] = useState(false);
84
87
  const [pageType, setPageType] = useState<string>('');
88
+ const [showOnboarding, setShowOnboarding] = useState(
89
+ () => !localStorage.getItem(ONBOARDING_KEY),
90
+ );
85
91
  const pageContextRef = useRef<PageContext | null>(null);
86
92
  const inspectorSendDepthRef = useRef(0);
87
93
 
@@ -475,6 +481,19 @@ function DevShellInner() {
475
481
  />
476
482
 
477
483
  {/* Modals */}
484
+ {showOnboarding && (
485
+ <OnboardingModal
486
+ onAddData={() => {
487
+ localStorage.setItem(ONBOARDING_KEY, '1');
488
+ setShowOnboarding(false);
489
+ setModal('addData');
490
+ }}
491
+ onDismiss={() => {
492
+ localStorage.setItem(ONBOARDING_KEY, '1');
493
+ setShowOnboarding(false);
494
+ }}
495
+ />
496
+ )}
478
497
  {modal === 'undo' && (
479
498
  <UndoConfirm
480
499
  onConfirm={(prompt) => {
@@ -0,0 +1,101 @@
1
+ import React, { useState } from 'react';
2
+
3
+ interface Props {
4
+ onAddData: () => void;
5
+ onDismiss: () => void;
6
+ }
7
+
8
+ const CAPABILITIES = [
9
+ {
10
+ icon: '🗄️',
11
+ title: '管理数据表',
12
+ desc: '用自然语言描述业务数据,AI 自动生成数据库和完整的数据管理页面',
13
+ },
14
+ {
15
+ icon: '✏️',
16
+ title: '所见即所得定制',
17
+ desc: 'Inspector模式下,点击数据管理页面任意元素,告诉 AI 要改什么,立即生效',
18
+ },
19
+ {
20
+ icon: '📊',
21
+ title: '新建 Dashboard',
22
+ desc: '在 Canvas 画布上拖拽搭建数据可视化看板,完成后一键发布到导航菜单',
23
+ },
24
+ ];
25
+
26
+ export default function OnboardingModal({ onAddData, onDismiss }: Props) {
27
+ const [step, setStep] = useState<1 | 2>(1);
28
+
29
+ return (
30
+ <div id="onboarding-overlay">
31
+ <div id="onboarding-card">
32
+ {step === 1 ? (
33
+ <>
34
+ <div className="onboarding-header">
35
+ <div className="onboarding-welcome-icon">👋</div>
36
+ <h2 className="onboarding-title">欢迎使用 AI+ Ops Admin</h2>
37
+ <p className="onboarding-subtitle">
38
+ 这里是你的 AI 驱动开发环境,你可以做这些事:
39
+ </p>
40
+ </div>
41
+
42
+ <div className="onboarding-caps">
43
+ {CAPABILITIES.map((cap) => (
44
+ <div key={cap.title} className="onboarding-cap-card">
45
+ <div className="onboarding-cap-icon">{cap.icon}</div>
46
+ <div className="onboarding-cap-title">{cap.title}</div>
47
+ <div className="onboarding-cap-desc">{cap.desc}</div>
48
+ </div>
49
+ ))}
50
+ </div>
51
+
52
+ <div className="onboarding-footer">
53
+ <button className="add-data-btn-cancel" onClick={onDismiss}>
54
+ 我已了解,跳过
55
+ </button>
56
+ <button
57
+ className="add-data-btn-confirm"
58
+ onClick={() => setStep(2)}
59
+ >
60
+ 下一步:如何开始 →
61
+ </button>
62
+ </div>
63
+ </>
64
+ ) : (
65
+ <>
66
+ <div className="onboarding-step2-body">
67
+ <div className="onboarding-step2-icon">🚀</div>
68
+ <h3 className="onboarding-step2-title">开始旅程:添加数据表</h3>
69
+ <p className="onboarding-step2-desc">
70
+ 数据表是一切的基础。描述你要管理什么业务数据,AI
71
+ 会自动设计数据库结构,并同步生成完整的管理页面。
72
+ <br />
73
+ <br />
74
+ 有了数据表之后,你就可以用 Inspector 点击页面元素进行定制, 或在
75
+ Canvas 上搭建可视化看板。
76
+ </p>
77
+ <div className="onboarding-step2-hint">
78
+ 💡 之后随时可以通过右下角菜单「添加数据表」重新触发
79
+ </div>
80
+ </div>
81
+
82
+ <div className="onboarding-footer">
83
+ <button
84
+ className="add-data-btn-cancel"
85
+ onClick={() => setStep(1)}
86
+ >
87
+ ← 返回
88
+ </button>
89
+ <button className="add-data-btn-cancel" onClick={onDismiss}>
90
+ 先跳过
91
+ </button>
92
+ <button className="add-data-btn-confirm" onClick={onAddData}>
93
+ 立即添加数据表
94
+ </button>
95
+ </div>
96
+ </>
97
+ )}
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -833,3 +833,132 @@ div.chat-header {
833
833
  opacity: 1;
834
834
  transform: translateY(-50%) scale(1);
835
835
  }
836
+
837
+ /* ── Onboarding Modal ────────────────────────────────────────────────────── */
838
+ #onboarding-overlay {
839
+ position: fixed;
840
+ inset: 0;
841
+ z-index: 999999;
842
+ background: rgba(0, 0, 0, 0.5);
843
+ display: flex;
844
+ align-items: center;
845
+ justify-content: center;
846
+ }
847
+
848
+ #onboarding-card {
849
+ background: #fff;
850
+ border-radius: 16px;
851
+ width: 600px;
852
+ max-width: calc(100vw - 32px);
853
+ box-shadow: 0 12px 48px rgba(0, 0, 0, 0.22);
854
+ display: flex;
855
+ flex-direction: column;
856
+ overflow: hidden;
857
+ }
858
+
859
+ .onboarding-header {
860
+ padding: 32px 32px 20px;
861
+ text-align: center;
862
+ }
863
+
864
+ .onboarding-welcome-icon {
865
+ font-size: 36px;
866
+ line-height: 1;
867
+ margin-bottom: 12px;
868
+ }
869
+
870
+ .onboarding-title {
871
+ font-size: 18px;
872
+ font-weight: 700;
873
+ color: #1a1a1a;
874
+ margin: 0 0 8px;
875
+ }
876
+
877
+ .onboarding-subtitle {
878
+ font-size: 13px;
879
+ color: #8c8c8c;
880
+ margin: 0;
881
+ }
882
+
883
+ .onboarding-caps {
884
+ display: flex;
885
+ gap: 12px;
886
+ padding: 0 32px 24px;
887
+ }
888
+
889
+ .onboarding-cap-card {
890
+ flex: 1;
891
+ display: flex;
892
+ flex-direction: column;
893
+ align-items: center;
894
+ gap: 6px;
895
+ padding: 20px 12px;
896
+ border: 1.5px solid #f0f0f0;
897
+ border-radius: 12px;
898
+ background: #fafafa;
899
+ text-align: center;
900
+ }
901
+
902
+ .onboarding-cap-icon {
903
+ font-size: 28px;
904
+ line-height: 1;
905
+ }
906
+
907
+ .onboarding-cap-title {
908
+ font-size: 13px;
909
+ font-weight: 600;
910
+ color: #1a1a1a;
911
+ }
912
+
913
+ .onboarding-cap-desc {
914
+ font-size: 12px;
915
+ color: #8c8c8c;
916
+ line-height: 1.5;
917
+ }
918
+
919
+ .onboarding-footer {
920
+ display: flex;
921
+ gap: 8px;
922
+ justify-content: flex-end;
923
+ padding: 16px 32px 24px;
924
+ border-top: 1px solid #f0f0f0;
925
+ }
926
+
927
+ /* Step 2 */
928
+ .onboarding-step2-body {
929
+ padding: 32px 40px 24px;
930
+ text-align: center;
931
+ display: flex;
932
+ flex-direction: column;
933
+ align-items: center;
934
+ gap: 12px;
935
+ }
936
+
937
+ .onboarding-step2-icon {
938
+ font-size: 44px;
939
+ line-height: 1;
940
+ }
941
+
942
+ .onboarding-step2-title {
943
+ font-size: 17px;
944
+ font-weight: 700;
945
+ color: #1a1a1a;
946
+ margin: 0;
947
+ }
948
+
949
+ .onboarding-step2-desc {
950
+ font-size: 13px;
951
+ color: #595959;
952
+ line-height: 1.7;
953
+ margin: 0;
954
+ max-width: 440px;
955
+ }
956
+
957
+ .onboarding-step2-hint {
958
+ font-size: 12px;
959
+ color: #8c8c8c;
960
+ background: #f5f5f5;
961
+ border-radius: 8px;
962
+ padding: 10px 16px;
963
+ margin-top: 4px;
964
+ }
@@ -8,15 +8,9 @@ import React, {
8
8
  import { GridLayout, useContainerWidth } from 'react-grid-layout';
9
9
  import type { LayoutItem } from 'react-grid-layout';
10
10
  import { Dropdown, Modal, Checkbox, Input, Button, Space } from 'antd';
11
- import type { MenuProps } from 'antd';
12
11
  import {
13
12
  PlusOutlined,
14
- DeleteOutlined,
15
- EditOutlined,
16
- CopyOutlined,
17
- SettingOutlined,
18
13
  PlusCircleOutlined,
19
- DatabaseOutlined,
20
14
  MinusCircleOutlined,
21
15
  } from '@ant-design/icons';
22
16
  import CanvasCell from './CanvasCell';
@@ -26,9 +20,9 @@ import { CANVAS_COMPONENTS, CANVAS_DEFAULTS } from './canvasDefaults';
26
20
  import { CANVAS_CONFIG_REGISTRY } from './canvasConfigRegistry';
27
21
  import { useAgent } from '@/components/agentPanel/AgentContext';
28
22
  import { generatePrompt } from '@/components/agentPanel/promptGenerator';
29
- import { CANVAS_CONTEXT_MENU_REGISTRY } from './canvasContextMenuRegistry';
30
23
  import type { MenuActionContext } from './canvasContextMenuRegistry';
31
24
  import type { CanvasConfigModalProps } from './canvasConfigRegistry';
25
+ import { useCanvasContextMenu } from './hooks/useCanvasContextMenu';
32
26
  import { useIsAgentAllowed } from 'config/agentAllowed';
33
27
  import NumCardDataSourceModal from './components/NumCardDataSourceModal';
34
28
  import TableDataSourceModal from './components/TableDataSourceModal';
@@ -387,7 +381,7 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
387
381
  }) => {
388
382
  const { items, layout: currentLayout, onBothChange } = liveRef.current;
389
383
  onBothChange(
390
- [...items, item],
384
+ [...items, { ...item, code: syncCodeHeight(item.code, layout.h) }],
391
385
  [
392
386
  ...currentLayout,
393
387
  {
@@ -548,203 +542,26 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
548
542
 
549
543
  // ── 右键菜单 ──
550
544
 
551
- const [itemMenu, setItemMenu] = useState<{
552
- x: number;
553
- y: number;
554
- id: string;
555
- } | null>(null);
556
-
557
- const handleItemContextMenu = useCallback(
558
- (e: React.MouseEvent, id: string) => {
559
- setItemMenu({ x: e.clientX, y: e.clientY, id });
560
- },
561
- [],
562
- );
563
-
564
- const itemMenuItems: MenuProps['items'] = itemMenu
565
- ? (() => {
566
- const item = items.find((i) => i.id === itemMenu.id);
567
- const componentType = item?.componentType ?? '';
568
- const hasConfig = componentType in CANVAS_CONFIG_REGISTRY;
569
-
570
- // ── 第 1 层:组件特有操作 ──
571
- const specificActionsFactory =
572
- CANVAS_CONTEXT_MENU_REGISTRY[componentType];
573
- const specificActions = specificActionsFactory
574
- ? specificActionsFactory(
575
- item?.code ?? '',
576
- (newCode) => {
577
- updateCode(itemMenu.id, newCode);
578
- },
579
- () => setItemMenu(null),
580
- menuActionContext,
581
- t,
582
- )
583
- : [];
584
-
585
- // ── 第 2 层:通用操作(检查元素) ──
586
- const commonActions: MenuProps['items'] = [
587
- ...(hasConfig
588
- ? [
589
- {
590
- key: 'config',
591
- icon: <SettingOutlined />,
592
- label:
593
- componentType === 'MultiChart'
594
- ? (t('canvas.configCharts') ?? '配置显示图表与顺序')
595
- : componentType === 'Table'
596
- ? (t('canvas.menu.configColumns') ?? '配置列')
597
- : t('canvas.config'),
598
- onClick: () => {
599
- setConfigModal({ id: itemMenu.id, componentType });
600
- setItemMenu(null);
601
- },
602
- },
603
- ]
604
- : []),
605
- {
606
- key: 'data-source',
607
- icon: <DatabaseOutlined />,
608
- label: t('canvas.dataSource.label'),
609
- onClick: () => {
610
- if (
611
- componentType === 'NumCard' ||
612
- componentType === 'Table' ||
613
- componentType === 'BarChart' ||
614
- componentType === 'LineChart' ||
615
- componentType === 'RadarChart' ||
616
- componentType === 'MultiChart'
617
- ) {
618
- setDataSourceModal({
619
- id: itemMenu.id,
620
- componentType,
621
- code: item?.code ?? '',
622
- });
623
- } else {
624
- menuActionContext.showPrompt({
625
- label: t('canvas.dataSource.label'),
626
- placeholder: t('canvas.dataSource.placeholder'),
627
- defaultValue: '',
628
- onConfirm: (value) => {
629
- if (!value.trim()) return;
630
- const prompt = generatePrompt({
631
- skill: 'canvas-sql-query',
632
- pageInfo: agent?.pageInfo ?? {
633
- resourceName: '',
634
- pageType: 'unknown',
635
- path: '',
636
- sourceFilePath: null,
637
- },
638
- userPrompt: `为此 ${componentType} 组件用 SQL 查询真实数据并填充:${value.trim()}`,
639
- inspectedElement: {
640
- type: 'canvas-component' as any,
641
- resource: 'canvas',
642
- x: 0,
643
- y: 0,
644
- meta: {
645
- componentType,
646
- canvasItemId: itemMenu.id,
647
- code: item?.code ?? '',
648
- },
649
- },
650
- });
651
- agent?.sendPrompt(prompt);
652
- },
653
- });
654
- }
655
- setItemMenu(null);
656
- },
657
- },
658
- {
659
- key: 'custom-agent',
660
- icon: <EditOutlined />,
661
- label: t('canvas.custom'),
662
- onClick: () => {
663
- setItemMenu(null);
664
- menuActionContext.showPrompt({
665
- label: t('canvas.customAction'),
666
- placeholder: t('canvas.customPlaceholder'),
667
- defaultValue: '',
668
- onConfirm: (value) => {
669
- if (!value.trim()) return;
670
- const prompt = generatePrompt({
671
- skill: 'canvas-component-edit',
672
- pageInfo: agent?.pageInfo ?? {
673
- resourceName: '',
674
- pageType: 'unknown',
675
- path: '',
676
- sourceFilePath: null,
677
- },
678
- userPrompt: value.trim(),
679
- inspectedElement: {
680
- type: 'canvas-component' as any,
681
- resource: 'canvas',
682
- x: 0,
683
- y: 0,
684
- meta: {
685
- componentType,
686
- canvasItemId: itemMenu.id,
687
- code: item?.code ?? '',
688
- },
689
- },
690
- });
691
- agent?.sendPrompt(prompt);
692
- },
693
- });
694
- },
695
- },
696
- ];
697
-
698
- // ── 第 3 层:代码级操作 ──
699
- const codeActions: MenuProps['items'] = [
700
- {
701
- key: 'edit',
702
- icon: <EditOutlined />,
703
- label: t('canvas.editCode'),
704
- onClick: () => {
705
- setEditingId(itemMenu.id);
706
- setItemMenu(null);
707
- },
708
- },
709
- {
710
- key: 'copy',
711
- icon: <CopyOutlined />,
712
- label: t('canvas.duplicate'),
713
- onClick: () => {
714
- duplicateComponent(itemMenu.id);
715
- setItemMenu(null);
716
- },
717
- },
718
- {
719
- key: 'delete',
720
- icon: <DeleteOutlined />,
721
- label: t('canvas.delete'),
722
- danger: true,
723
- onClick: () => {
724
- removeComponent(itemMenu.id);
725
- setItemMenu(null);
726
- },
727
- },
728
- ];
729
-
730
- // ── 组装三层 ──
731
- const result: MenuProps['items'] = [];
732
- if (specificActions && specificActions.length > 0) {
733
- result.push(...specificActions);
734
- result.push({ type: 'divider' as const });
735
- }
736
- result.push(...commonActions);
737
- result.push({ type: 'divider' as const });
738
- result.push(...codeActions);
739
-
740
- return result;
741
- })()
742
- : [];
545
+ const { itemMenu, handleItemContextMenu, closeItemMenu, itemMenuItems } =
546
+ useCanvasContextMenu({
547
+ items,
548
+ updateCode,
549
+ duplicateComponent,
550
+ removeComponent,
551
+ menuActionContext,
552
+ t,
553
+ agent,
554
+ openConfigModal: (id, componentType) =>
555
+ setConfigModal({ id, componentType }),
556
+ openDataSourceModal: (id, componentType, code) =>
557
+ setDataSourceModal({ id, componentType, code }),
558
+ openEditCode: (id) => setEditingId(id),
559
+ });
743
560
 
744
561
  const handleCanvasClick = useCallback(() => {
745
562
  setSelectedId(null);
746
- setItemMenu(null);
747
- }, []);
563
+ closeItemMenu();
564
+ }, [closeItemMenu]);
748
565
 
749
566
  const editingItem = editingId ? items.find((i) => i.id === editingId) : null;
750
567
  const editingDef = editingItem
@@ -938,7 +755,7 @@ const CanvasPage: React.FC<CanvasPageProps> = ({
938
755
  open
939
756
  trigger={['click']}
940
757
  onOpenChange={(open) => {
941
- if (!open) setItemMenu(null);
758
+ if (!open) closeItemMenu();
942
759
  }}
943
760
  >
944
761
  <div
@@ -27,7 +27,16 @@ import {
27
27
  Section,
28
28
  } from '@gadmin2n/charts';
29
29
  import * as echarts from 'echarts';
30
- import { Form, Select, Input, DatePicker, Tag, Button, Space } from 'antd';
30
+ import {
31
+ Form,
32
+ Select,
33
+ Input,
34
+ DatePicker,
35
+ Tag,
36
+ Button,
37
+ Space,
38
+ Spin,
39
+ } from 'antd';
31
40
  import {
32
41
  CaretUpOutlined,
33
42
  CaretDownOutlined,
@@ -59,6 +68,7 @@ const SCOPE: Record<string, unknown> = {
59
68
  Tag,
60
69
  Button,
61
70
  Space,
71
+ Spin,
62
72
  CaretUpOutlined,
63
73
  CaretDownOutlined,
64
74
  UserOutlined,
@@ -16,6 +16,7 @@ import {
16
16
  writeMultiChartTableFlags,
17
17
  buildVirtualTableCode,
18
18
  } from './utils/tableCodeUtils';
19
+ import { parseJsLiteralProp } from './utils/jsLiteralParser';
19
20
 
20
21
  // ─── 组件特有右键菜单操作注册表 ────────────────────────────────────────────────
21
22
  //
@@ -1068,25 +1069,16 @@ function parseDefaultVariant(code: string): string {
1068
1069
 
1069
1070
  /** 从 code 中解析 charts 列表 */
1070
1071
  function parseChartsList(code: string): string[] {
1071
- const match = code.match(/charts=\{(\[.*?\])\}/s);
1072
- if (!match) return ['bar', 'line', 'pie'];
1073
- try {
1074
- return JSON.parse(match[1].replace(/'/g, '"'));
1075
- } catch {
1076
- return ['bar', 'line', 'pie'];
1077
- }
1072
+ const parsed = parseJsLiteralProp(code, 'charts');
1073
+ return Array.isArray(parsed) ? parsed : ['bar', 'line', 'pie'];
1078
1074
  }
1079
1075
 
1080
- /** 从 code 中解析 chartConfig JSON */
1076
+ /** 从 code 中解析 chartConfig */
1081
1077
  function parseChartConfig(code: string): Record<string, Record<string, any>> {
1082
- const match = code.match(/chartConfig=\{([\s\S]*?)\}\s*(?=\w+=|\/?>)/);
1083
- if (!match) return {};
1084
- try {
1085
- // chartConfig={{"bar":{"showGrid":false}}}
1086
- return JSON.parse(match[1]);
1087
- } catch {
1088
- return {};
1089
- }
1078
+ const parsed = parseJsLiteralProp(code, 'chartConfig');
1079
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1080
+ ? (parsed as Record<string, Record<string, any>>)
1081
+ : {};
1090
1082
  }
1091
1083
 
1092
1084
  /** 将 chartConfig 写回 code 字符串 */
@@ -1186,6 +1178,8 @@ function multiChartActions(
1186
1178
  virtualCode += ` ${k}={${v}}\n`;
1187
1179
  } else if (typeof v === 'string') {
1188
1180
  virtualCode += ` ${k}="${v}"\n`;
1181
+ } else if (typeof v === 'number') {
1182
+ virtualCode += ` ${k}={${v}}\n`;
1189
1183
  }
1190
1184
  }
1191
1185
  virtualCode += ` testId="virtual"\n/>`;
@@ -1205,6 +1199,11 @@ function multiChartActions(
1205
1199
  for (const m of strMatches) {
1206
1200
  if (m[1] !== 'testId') newConfig[m[1]] = m[2];
1207
1201
  }
1202
+ // 解析 number props: xxx={123}
1203
+ const numMatches = newVirtualCode.matchAll(/(\w+)\s*=\s*\{(\d+)\}/g);
1204
+ for (const m of numMatches) {
1205
+ if (m[1] !== 'testId') newConfig[m[1]] = Number(m[2]);
1206
+ }
1208
1207
 
1209
1208
  // 更新 chartConfig
1210
1209
  const updatedConfig = { ...chartConfig, [activeChart]: newConfig };
@@ -0,0 +1,246 @@
1
+ import { useCallback, useState } from 'react';
2
+ import type { MenuProps } from 'antd';
3
+ import type { TFunction } from 'i18next';
4
+ import {
5
+ EditOutlined,
6
+ CopyOutlined,
7
+ SettingOutlined,
8
+ DatabaseOutlined,
9
+ DeleteOutlined,
10
+ } from '@ant-design/icons';
11
+ import { CANVAS_CONFIG_REGISTRY } from '../canvasConfigRegistry';
12
+ import { CANVAS_CONTEXT_MENU_REGISTRY } from '../canvasContextMenuRegistry';
13
+ import type { MenuActionContext } from '../canvasContextMenuRegistry';
14
+ import { generatePrompt } from '@/components/agentPanel/promptGenerator';
15
+ import type { useAgent } from '@/components/agentPanel/AgentContext';
16
+ import type { CanvasItem as CanvasItemType } from '../types';
17
+
18
+ interface UseCanvasContextMenuParams {
19
+ items: CanvasItemType[];
20
+ updateCode: (id: string, code: string) => void;
21
+ duplicateComponent: (id: string) => void;
22
+ removeComponent: (id: string) => void;
23
+ menuActionContext: MenuActionContext;
24
+ t: TFunction;
25
+ agent: ReturnType<typeof useAgent>;
26
+ openConfigModal: (id: string, componentType: string) => void;
27
+ openDataSourceModal: (
28
+ id: string,
29
+ componentType: string,
30
+ code: string,
31
+ ) => void;
32
+ openEditCode: (id: string) => void;
33
+ }
34
+
35
+ interface ItemMenuState {
36
+ x: number;
37
+ y: number;
38
+ id: string;
39
+ }
40
+
41
+ /** 画布组件右键菜单:开关 state + 三层菜单项(组件特有 / 通用 / 代码级)的组装逻辑 */
42
+ export function useCanvasContextMenu({
43
+ items,
44
+ updateCode,
45
+ duplicateComponent,
46
+ removeComponent,
47
+ menuActionContext,
48
+ t,
49
+ agent,
50
+ openConfigModal,
51
+ openDataSourceModal,
52
+ openEditCode,
53
+ }: UseCanvasContextMenuParams) {
54
+ const [itemMenu, setItemMenu] = useState<ItemMenuState | null>(null);
55
+
56
+ const handleItemContextMenu = useCallback(
57
+ (e: React.MouseEvent, id: string) => {
58
+ setItemMenu({ x: e.clientX, y: e.clientY, id });
59
+ },
60
+ [],
61
+ );
62
+
63
+ const closeItemMenu = useCallback(() => setItemMenu(null), []);
64
+
65
+ const itemMenuItems: MenuProps['items'] = itemMenu
66
+ ? (() => {
67
+ const item = items.find((i) => i.id === itemMenu.id);
68
+ const componentType = item?.componentType ?? '';
69
+ const hasConfig = componentType in CANVAS_CONFIG_REGISTRY;
70
+
71
+ // ── 第 1 层:组件特有操作 ──
72
+ const specificActionsFactory =
73
+ CANVAS_CONTEXT_MENU_REGISTRY[componentType];
74
+ const specificActions = specificActionsFactory
75
+ ? specificActionsFactory(
76
+ item?.code ?? '',
77
+ (newCode) => {
78
+ updateCode(itemMenu.id, newCode);
79
+ },
80
+ () => setItemMenu(null),
81
+ menuActionContext,
82
+ t,
83
+ )
84
+ : [];
85
+
86
+ // ── 第 2 层:通用操作(检查元素) ──
87
+ const commonActions: MenuProps['items'] = [
88
+ ...(hasConfig
89
+ ? [
90
+ {
91
+ key: 'config',
92
+ icon: <SettingOutlined />,
93
+ label:
94
+ componentType === 'MultiChart'
95
+ ? (t('canvas.configCharts') ?? '配置显示图表与顺序')
96
+ : componentType === 'Table'
97
+ ? (t('canvas.menu.configColumns') ?? '配置列')
98
+ : t('canvas.config'),
99
+ onClick: () => {
100
+ openConfigModal(itemMenu.id, componentType);
101
+ setItemMenu(null);
102
+ },
103
+ },
104
+ ]
105
+ : []),
106
+ {
107
+ key: 'data-source',
108
+ icon: <DatabaseOutlined />,
109
+ label: t('canvas.dataSource.label'),
110
+ onClick: () => {
111
+ if (
112
+ componentType === 'NumCard' ||
113
+ componentType === 'Table' ||
114
+ componentType === 'BarChart' ||
115
+ componentType === 'LineChart' ||
116
+ componentType === 'RadarChart' ||
117
+ componentType === 'MultiChart'
118
+ ) {
119
+ openDataSourceModal(
120
+ itemMenu.id,
121
+ componentType,
122
+ item?.code ?? '',
123
+ );
124
+ } else {
125
+ menuActionContext.showPrompt({
126
+ label: t('canvas.dataSource.label'),
127
+ placeholder: t('canvas.dataSource.placeholder'),
128
+ defaultValue: '',
129
+ onConfirm: (value) => {
130
+ if (!value.trim()) return;
131
+ const prompt = generatePrompt({
132
+ skill: 'canvas-sql-query',
133
+ pageInfo: agent?.pageInfo ?? {
134
+ resourceName: '',
135
+ pageType: 'unknown',
136
+ path: '',
137
+ sourceFilePath: null,
138
+ },
139
+ userPrompt: `为此 ${componentType} 组件用 SQL 查询真实数据并填充:${value.trim()}`,
140
+ inspectedElement: {
141
+ type: 'canvas-component' as any,
142
+ resource: 'canvas',
143
+ x: 0,
144
+ y: 0,
145
+ meta: {
146
+ componentType,
147
+ canvasItemId: itemMenu.id,
148
+ code: item?.code ?? '',
149
+ },
150
+ },
151
+ });
152
+ agent?.sendPrompt(prompt);
153
+ },
154
+ });
155
+ }
156
+ setItemMenu(null);
157
+ },
158
+ },
159
+ {
160
+ key: 'custom-agent',
161
+ icon: <EditOutlined />,
162
+ label: t('canvas.custom'),
163
+ onClick: () => {
164
+ setItemMenu(null);
165
+ menuActionContext.showPrompt({
166
+ label: t('canvas.customAction'),
167
+ placeholder: t('canvas.customPlaceholder'),
168
+ defaultValue: '',
169
+ onConfirm: (value) => {
170
+ if (!value.trim()) return;
171
+ const prompt = generatePrompt({
172
+ skill: 'canvas-component-edit',
173
+ pageInfo: agent?.pageInfo ?? {
174
+ resourceName: '',
175
+ pageType: 'unknown',
176
+ path: '',
177
+ sourceFilePath: null,
178
+ },
179
+ userPrompt: value.trim(),
180
+ inspectedElement: {
181
+ type: 'canvas-component' as any,
182
+ resource: 'canvas',
183
+ x: 0,
184
+ y: 0,
185
+ meta: {
186
+ componentType,
187
+ canvasItemId: itemMenu.id,
188
+ code: item?.code ?? '',
189
+ },
190
+ },
191
+ });
192
+ agent?.sendPrompt(prompt);
193
+ },
194
+ });
195
+ },
196
+ },
197
+ ];
198
+
199
+ // ── 第 3 层:代码级操作 ──
200
+ const codeActions: MenuProps['items'] = [
201
+ {
202
+ key: 'edit',
203
+ icon: <EditOutlined />,
204
+ label: t('canvas.editCode'),
205
+ onClick: () => {
206
+ openEditCode(itemMenu.id);
207
+ setItemMenu(null);
208
+ },
209
+ },
210
+ {
211
+ key: 'copy',
212
+ icon: <CopyOutlined />,
213
+ label: t('canvas.duplicate'),
214
+ onClick: () => {
215
+ duplicateComponent(itemMenu.id);
216
+ setItemMenu(null);
217
+ },
218
+ },
219
+ {
220
+ key: 'delete',
221
+ icon: <DeleteOutlined />,
222
+ label: t('canvas.delete'),
223
+ danger: true,
224
+ onClick: () => {
225
+ removeComponent(itemMenu.id);
226
+ setItemMenu(null);
227
+ },
228
+ },
229
+ ];
230
+
231
+ // ── 组装三层 ──
232
+ const result: MenuProps['items'] = [];
233
+ if (specificActions && specificActions.length > 0) {
234
+ result.push(...specificActions);
235
+ result.push({ type: 'divider' as const });
236
+ }
237
+ result.push(...commonActions);
238
+ result.push({ type: 'divider' as const });
239
+ result.push(...codeActions);
240
+
241
+ return result;
242
+ })()
243
+ : [];
244
+
245
+ return { itemMenu, handleItemContextMenu, closeItemMenu, itemMenuItems };
246
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * 从 JSX 代码中提取某个 prop 的字面量值,按 JS 表达式语法求值
3
+ * (兼容裸 key、单引号字符串等非严格 JSON 写法,因为这本来就是 JS/JSX,不是 JSON)。
4
+ * 求值失败(语法错误、未找到该 prop 等)时返回 undefined。
5
+ */
6
+ export function parseJsLiteralProp(code: string, propName: string): unknown {
7
+ const openBraceMatch = code.match(new RegExp(`${propName}\\s*=\\s*\\{`));
8
+ if (!openBraceMatch || openBraceMatch.index === undefined) return undefined;
9
+
10
+ const openIndex = openBraceMatch.index + openBraceMatch[0].length - 1;
11
+ const closeIndex = findMatchingBrace(code, openIndex);
12
+ if (closeIndex === -1) return undefined;
13
+
14
+ const expr = code.slice(openIndex + 1, closeIndex);
15
+ try {
16
+ // eslint-disable-next-line @typescript-eslint/no-implied-eval
17
+ return new Function(`return (${expr})`)();
18
+ } catch {
19
+ return undefined;
20
+ }
21
+ }
22
+
23
+ /** 从 openIndex(必须指向 '{')开始,找到与之匹配的 '}' 的下标;跳过字符串内的花括号 */
24
+ function findMatchingBrace(code: string, openIndex: number): number {
25
+ let depth = 0;
26
+ let inString: '"' | "'" | null = null;
27
+
28
+ for (let i = openIndex; i < code.length; i++) {
29
+ const ch = code[i];
30
+
31
+ if (inString) {
32
+ if (ch === '\\') {
33
+ i++; // 跳过转义字符,避免把 \" 误判为字符串结束
34
+ } else if (ch === inString) {
35
+ inString = null;
36
+ }
37
+ continue;
38
+ }
39
+
40
+ if (ch === '"' || ch === "'") {
41
+ inString = ch;
42
+ } else if (ch === '{') {
43
+ depth++;
44
+ } else if (ch === '}') {
45
+ depth--;
46
+ if (depth === 0) return i;
47
+ }
48
+ }
49
+ return -1;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gadmin2n/schematics",
3
- "version": "0.0.109",
3
+ "version": "0.0.111",
4
4
  "description": "Gadmin - modern, fast, powerful node.js web framework (@schematics)",
5
5
  "main": "dist/index.js",
6
6
  "files": [