Bladeren bron

feat: 合并多选功能,解决冲突,升级版本到2.0.4

奔跑的面条 3 jaren geleden
bovenliggende
commit
fa34300401
55 gewijzigde bestanden met toevoegingen van 1741 en 636 verwijderingen
  1. 1 1
      README.md
  2. 1 1
      package.json
  3. 0 0
      readme/go-view-canvas.png
  4. BIN
      readme/go-view-color.png
  5. BIN
      readme/go-view-fetch.png
  6. BIN
      readme/go-view-filter.png
  7. 0 0
      readme/go-view-qq.png
  8. 6 0
      src/components/Pages/ChartItemSetting/SizeSetting.vue
  9. 17 20
      src/components/Pages/ChartItemSetting/StylesSetting.vue
  10. 27 3
      src/enums/editPageEnum.ts
  11. 23 18
      src/packages/index.d.ts
  12. 34 3
      src/packages/public/publicConfig.ts
  13. 14 2
      src/plugins/icon.ts
  14. 11 2
      src/settings/designSetting.ts
  15. 9 6
      src/store/modules/chartEditStore/chartEditStore.d.ts
  16. 309 120
      src/store/modules/chartEditStore/chartEditStore.ts
  17. 2 0
      src/store/modules/chartHistoryStore/chartHistoryDefine.ts
  18. 7 2
      src/store/modules/chartHistoryStore/chartHistoryStore.d.ts
  19. 37 71
      src/store/modules/chartHistoryStore/chartHistoryStore.ts
  20. 1 1
      src/views/chart/ContentCharts/components/ChartsItemBox/index.vue
  21. 7 2
      src/views/chart/ContentConfigurations/components/CanvasPage/components/ChartThemeColor/index.vue
  22. 1 1
      src/views/chart/ContentConfigurations/components/CanvasPage/index.vue
  23. 6 2
      src/views/chart/ContentConfigurations/components/ChartAnimation/index.vue
  24. 6 2
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataAjax/index.vue
  25. 5 2
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/components/RequestGlobalConfig/index.vue
  26. 2 3
      src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue
  27. 2 2
      src/views/chart/ContentConfigurations/components/ChartSetting/index.vue
  28. 2 2
      src/views/chart/ContentConfigurations/components/hooks/useTargetData.hook.ts
  29. 6 0
      src/views/chart/ContentConfigurations/index.d.ts
  30. 32 49
      src/views/chart/ContentConfigurations/index.vue
  31. 8 29
      src/views/chart/ContentEdit/components/EditAlignLine/index.vue
  32. 3 0
      src/views/chart/ContentEdit/components/EditGroup/index.ts
  33. 122 0
      src/views/chart/ContentEdit/components/EditGroup/index.vue
  34. 22 46
      src/views/chart/ContentEdit/components/EditHistory/index.vue
  35. 6 2
      src/views/chart/ContentEdit/components/EditRange/index.vue
  36. 7 2
      src/views/chart/ContentEdit/components/EditRule/index.vue
  37. 3 0
      src/views/chart/ContentEdit/components/EditSelect/index.ts
  38. 111 0
      src/views/chart/ContentEdit/components/EditSelect/index.vue
  39. 21 12
      src/views/chart/ContentEdit/components/EditShapeBox/index.vue
  40. 29 20
      src/views/chart/ContentEdit/components/EditShortcutKey/ShortcutKeyModal.vue
  41. 4 4
      src/views/chart/ContentEdit/components/EditTools/hooks/useFile.hooks.ts
  42. 196 49
      src/views/chart/ContentEdit/hooks/useDrag.hook.ts
  43. 67 29
      src/views/chart/ContentEdit/index.vue
  44. 3 0
      src/views/chart/ContentLayers/components/LayersGroupListItem/index.ts
  45. 224 0
      src/views/chart/ContentLayers/components/LayersGroupListItem/index.vue
  46. 7 3
      src/views/chart/ContentLayers/components/LayersListItem/index.vue
  47. 68 20
      src/views/chart/ContentLayers/index.vue
  48. 1 12
      src/views/chart/hooks/useContextMenu.hook.d.ts
  49. 77 41
      src/views/chart/hooks/useContextMenu.hook.ts
  50. 36 15
      src/views/chart/hooks/useKeyboard.hook.ts
  51. 71 26
      src/views/chart/hooks/useSync.hook.ts
  52. 3 0
      src/views/preview/components/PreviewRenderGroup/index.ts
  53. 53 0
      src/views/preview/components/PreviewRenderGroup/index.vue
  54. 16 4
      src/views/preview/components/PreviewRenderList/index.vue
  55. 15 7
      src/views/preview/hooks/useComInstall.hook.ts

+ 1 - 1
README.md

@@ -122,6 +122,6 @@ export const http = (type?: RequestHttpEnum) => {
 
 QQ 群:1030129384
 
-![QQ群](readme/goView-QQ.png)
+![QQ群](readme/go-view-qq.png)
 
 ![渲染海报](readme/logo-poster.png)

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "go-view",
-  "version": "2.0.3",
+  "version": "2.0.4",
   "scripts": {
     "dev": "vite --host",
     "build": "vue-tsc --noEmit && vite build",

+ 0 - 0
readme/goView-canvas.png → readme/go-view-canvas.png


BIN
readme/go-view-color.png


BIN
readme/go-view-fetch.png


BIN
readme/go-view-filter.png


+ 0 - 0
readme/goView-QQ.png → readme/go-view-qq.png


+ 6 - 0
src/components/Pages/ChartItemSetting/SizeSetting.vue

@@ -3,6 +3,7 @@
     <n-input-number
       v-model:value="chartAttr.w"
       :min="50"
+      :disabled="isGroup"
       size="small"
       placeholder="px"
     >
@@ -13,6 +14,7 @@
     <n-input-number
       v-model:value="chartAttr.h"
       :min="50"
+      :disabled="isGroup"
       size="small"
       placeholder="px"
     >
@@ -32,6 +34,10 @@ const props = defineProps({
   chartAttr: {
     type: Object as PropType<Omit<PickCreateComponentType<'attr'>, 'node' | 'conNode'>>,
     required: true
+  },
+  isGroup: {
+    type: Boolean,
+    required: false
   }
 })
 </script>

+ 17 - 20
src/components/Pages/ChartItemSetting/StylesSetting.vue

@@ -1,4 +1,9 @@
 <template>
+  <div v-show="isGroup">
+    <n-divider n-divider style="margin: 10px 0"></n-divider>
+    <n-tag type="warning"> 解散分组「  {{ isCanvas ? '滤镜' : '滤镜 / 变换' }} 」也将消失!</n-tag>
+  </div>
+
   <collapse-item :name="isCanvas ? '滤镜' : '滤镜 / 变换'">
     <setting-item-box name="色相" :alone="true">
       <setting-item :name="`值:${chartStyles.hueRotate}deg`">
@@ -13,9 +18,7 @@
       </setting-item>
     </setting-item-box>
     <setting-item-box name="饱和度" :alone="true">
-      <setting-item
-        :name="`值:${(parseFloat(String(chartStyles.saturate)) * 100).toFixed(0)}%`"
-      >
+      <setting-item :name="`值:${(parseFloat(String(chartStyles.saturate)) * 100).toFixed(0)}%`">
         <!-- 透明度 -->
         <n-slider
           v-model:value="chartStyles.saturate"
@@ -27,9 +30,7 @@
       </setting-item>
     </setting-item-box>
     <setting-item-box name="对比度" :alone="true">
-      <setting-item
-        :name="`值:${(parseFloat(String(chartStyles.contrast)) * 100).toFixed(0)}%`"
-      >
+      <setting-item :name="`值:${(parseFloat(String(chartStyles.contrast)) * 100).toFixed(0)}%`">
         <!-- 透明度 -->
         <n-slider
           v-model:value="chartStyles.contrast"
@@ -41,9 +42,7 @@
       </setting-item>
     </setting-item-box>
     <setting-item-box name="亮度" :alone="true">
-      <setting-item
-        :name="`值:${(parseFloat(String(chartStyles.brightness)) * 100).toFixed(0)}%`"
-      >
+      <setting-item :name="`值:${(parseFloat(String(chartStyles.brightness)) * 100).toFixed(0)}%`">
         <!-- 透明度 -->
         <n-slider
           v-model:value="chartStyles.brightness"
@@ -55,9 +54,7 @@
       </setting-item>
     </setting-item-box>
     <setting-item-box name="透明度" :alone="true">
-      <setting-item
-        :name="`值:${(parseFloat(String(chartStyles.opacity)) * 100).toFixed(0)}%`"
-      >
+      <setting-item :name="`值:${(parseFloat(String(chartStyles.opacity)) * 100).toFixed(0)}%`">
         <!-- 透明度 -->
         <n-slider
           v-model:value="chartStyles.opacity"
@@ -68,7 +65,7 @@
         ></n-slider>
       </setting-item>
     </setting-item-box>
-    
+
     <!-- 变换 -->
     <setting-item-box v-if="!isCanvas" name="旋转°">
       <setting-item name="Z轴(平面) - 旋转">
@@ -130,21 +127,21 @@
 <script setup lang="ts">
 import { PropType } from 'vue'
 import { PickCreateComponentType } from '@/packages/index.d'
-import {
-  SettingItemBox,
-  SettingItem,
-  CollapseItem,
-} from '@/components/Pages/ChartItemSetting'
+import { SettingItemBox, SettingItem, CollapseItem } from '@/components/Pages/ChartItemSetting'
 
 const props = defineProps({
+  isGroup: {
+    type: Boolean,
+    required: false
+  },
   isCanvas: {
     type: Boolean,
     default: false
   },
   chartStyles: {
     type: Object as PropType<Omit<PickCreateComponentType<'styles'>, 'animations'>>,
-    required: true,
-  },
+    required: true
+  }
 })
 
 // 百分比格式化persen

+ 27 - 3
src/enums/editPageEnum.ts

@@ -1,23 +1,44 @@
+// 鼠标点击左右键
+export enum MouseEventButton {
+  LEFT = 1,
+  RIGHT = 2,
+}
+
 // 页面拖拽键名
 export enum DragKeyEnum {
-  DROG_KEY = 'ChartData'
+  DRAG_KEY = 'ChartData'
 }
 
-// 右键枚举
+// 操作枚举
 export enum MenuEnum {
+  // 移动
   ARROW_UP = 'up',
   ARROW_RIGHT = 'right',
   ARROW_DOWN = 'down',
   ARROW_LEFT = 'left',
+  // 删除
   DELETE = 'delete',
+  // 复制
   COPY = 'copy',
+  // 剪切
   CUT = 'cut',
+  // 粘贴
   PARSE = 'parse',
+  // 置顶
   TOP = 'top',
+  // 置底
   BOTTOM = 'bottom',
+  // 上移
   UP = 'up',
+  // 下移
   DOWN = 'down',
+  // 清空剪贴板
   CLEAR = 'clear',
+  // 成组
+  GROUP = 'group',
+  // 解组
+  UN_GROUP = 'unGroup',
+  // 后退
   BACK = 'back',
   FORWORD = 'forward',
   SAVE = 'save'
@@ -28,6 +49,9 @@ export enum WinKeyboard {
   CTRL = 'ctrl',
   SHIFT = 'shift',
   ALT = ' alt',
+  CTRL_SOURCE_KEY = "control",
+  SHIFT_SOURCE_KEY = "shift",
+  ALT_SOURCE_KEY = "alt"
 }
 
 // Mac 键盘枚举
@@ -48,4 +72,4 @@ export enum SyncEnum {
   SUCCESS,
   // 失败
   FAILURE
-}
+}

+ 23 - 18
src/packages/index.d.ts

@@ -27,12 +27,12 @@ export type ConfigType = {
 
 // 数据请求
 interface requestConfig {
-  request: RequestConfigType,
+  request: RequestConfigType
 }
 
 // Echarts 数据类型
 interface EchartsDataType {
-  dimensions: string[],
+  dimensions: string[]
   source: any[]
 }
 
@@ -56,27 +56,27 @@ export enum FilterEnum {
 
   // 倾斜
   SKEW_X = 'skewX',
-  SKEW_Y = 'skewY',
+  SKEW_Y = 'skewY'
 }
 
 // 组件实例类
-export interface PublicConfigType extends requestConfig {
+export interface PublicConfigType {
   id: string
-  rename?: string
+  isGroup: boolean
   attr: { x: number; y: number; w: number; h: number; zIndex: number }
   styles: {
-    [FilterEnum.OPACITY]: number;
-    [FilterEnum.SATURATE]: number;
-    [FilterEnum.CONTRAST]: number;
-    [FilterEnum.HUE_ROTATE]: number;
-    [FilterEnum.BRIGHTNESS]: number;
-
-    [FilterEnum.ROTATE_Z]: number;
-    [FilterEnum.ROTATE_X]: number;
-    [FilterEnum.ROTATE_Y]: number;
-
-    [FilterEnum.SKEW_X]: number;
-    [FilterEnum.SKEW_Y]: number;
+    [FilterEnum.OPACITY]: number
+    [FilterEnum.SATURATE]: number
+    [FilterEnum.CONTRAST]: number
+    [FilterEnum.HUE_ROTATE]: number
+    [FilterEnum.BRIGHTNESS]: number
+
+    [FilterEnum.ROTATE_Z]: number
+    [FilterEnum.ROTATE_X]: number
+    [FilterEnum.ROTATE_Y]: number
+
+    [FilterEnum.SKEW_X]: number
+    [FilterEnum.SKEW_Y]: number
     // 动画
     animations: string[]
   }
@@ -84,12 +84,17 @@ export interface PublicConfigType extends requestConfig {
   setPosition: Function
 }
 
-export interface CreateComponentType extends PublicConfigType {
+export interface CreateComponentType extends PublicConfigType, requestConfig {
   key: string
   chartConfig: ConfigType
   option: GlobalThemeJsonType
 }
 
+// 组件成组实例类
+export interface CreateComponentGroupType extends CreateComponentType {
+  groupList: Array<CreateComponentType>
+}
+
 // 获取组件实例类中某个key对应value类型的方法
 export type PickCreateComponentType<T extends keyof CreateComponentType> = Pick<CreateComponentType, T>[T]
 

+ 34 - 3
src/packages/public/publicConfig.ts

@@ -1,6 +1,7 @@
 import { getUUID } from '@/utils'
-import { PublicConfigType } from '@/packages/index.d'
+import { ChartFrameEnum, PublicConfigType, CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { RequestConfigType } from '@/store/modules/chartEditStore/chartEditStore.d'
+import { groupTitle } from '@/settings/designSetting'
 import {
   RequestHttpEnum,
   RequestDataTypeEnum,
@@ -10,6 +11,7 @@ import {
 } from '@/enums/httpEnum'
 import { chartInitConfig } from '@/settings/designSetting'
 
+// 请求基础属性
 const requestConfig: RequestConfigType = {
   requestDataType: RequestDataTypeEnum.STATIC,
   requestHttpType: RequestHttpEnum.GET,
@@ -33,10 +35,10 @@ const requestConfig: RequestConfigType = {
   }
 }
 
+// 单实例类
 export class publicConfig implements PublicConfigType {
   public id = getUUID()
-  // 重命名
-  public rename = undefined
+  public isGroup = false
   // 基本信息
   public attr = { ...chartInitConfig, zIndex: -1 }
   // 基本样式
@@ -75,3 +77,32 @@ export class publicConfig implements PublicConfigType {
     this.attr.y = y
   }
 }
+
+// 成组类 (部分属性不需要, 不继承 publicConfig)
+export class PublicGroupConfigClass extends publicConfig implements CreateComponentGroupType {
+  // 成组
+  public isGroup = true
+  // 名称
+  public chartConfig = {
+    key: 'group',
+    chartKey: 'group',
+    conKey: 'group',
+    category: 'group',
+    categoryName: 'group',
+    package: 'group',
+    chartFrame: ChartFrameEnum.COMMON,
+    title: groupTitle,
+    image: ''
+  }
+  // 组成员列表
+  public groupList: Array<CreateComponentType> = []
+  // ---- 原有 ---
+  // key
+  public key = 'group'
+  // 配置
+  public option = {}
+  // 标识
+  public id = getUUID()
+  // 基本信息
+  public attr = { w: 0, h: 0, x: 0, y: 0, zIndex: -1 }
+}

+ 14 - 2
src/plugins/icon.ts

@@ -56,7 +56,9 @@ import {
   Reload as ReloadIcon,
   ChevronUpOutline as ChevronUpOutlineIcon,
   ChevronDownOutline as ChevronDownOutlineIcon,
-  Pulse as PulseIcon
+  Pulse as PulseIcon,
+  Folder as FolderIcon,
+  FolderOpen as FolderOpenIcon
 } from '@vicons/ionicons5'
 
 import {
@@ -86,6 +88,8 @@ import {
   FitToHeight as FitToHeightIcon,
   FitToWidth as FitToWidthIcon,
   Save as SaveIcon,
+  Carbon3DCursor as Carbon3DCursorIcon,
+  Carbon3DSoftware as Carbon3DSoftwareIcon,
   Filter as FilterIcon,
   FilterEdit as FilterEditIcon
 } from '@vicons/carbon'
@@ -207,7 +211,11 @@ const ionicons5 = {
   // 向下
   ChevronDownOutlineIcon,
   // 脉搏
-  PulseIcon
+  PulseIcon,
+  // 文件夹
+  FolderIcon,
+  // 文件夹打开
+  FolderOpenIcon
 }
 
 const carbon = {
@@ -254,6 +262,10 @@ const carbon = {
   FitToWidthIcon,
   // 保存
   SaveIcon,
+  // 成组
+  Carbon3DCursorIcon,
+  // 解组
+  Carbon3DSoftwareIcon,
   // 过滤器
   FilterIcon,
   FilterEditIcon

+ 11 - 2
src/settings/designSetting.ts

@@ -8,6 +8,9 @@ export const lang = LangEnum.ZH
 // 水印文字
 export const watermarkText = "GoView 低代码平台"
 
+// 分组名称
+export const groupTitle = "分组"
+
 // 主题配置
 export const theme = {
   // 默认是否开启深色主题
@@ -37,7 +40,7 @@ export const asideCollapsedWidth = 60
 // 弹窗是否可以通过点击遮罩关闭
 export const maskClosable = false
 
-// 修改边框圆角
+// 全局边框圆角
 export const borderRadius = '6px'
 
 // 轮播间隔
@@ -59,4 +62,10 @@ export const saveInterval = 30
 export const requestIntervalUnit = RequestHttpIntervalEnum.SECOND
 
 // 工作区域历史记录存储最大数量
-export const editHistoryMax = 100
+export const editHistoryMax = 100
+
+// 拖拽时蒙层的 z-index,需比所有图表高
+export const canvasModelIndex = 9999
+
+// 框选时蒙层的 z-index,需比所有图表高
+export const selectBoxIndex = canvasModelIndex + 10

+ 9 - 6
src/store/modules/chartEditStore/chartEditStore.d.ts

@@ -1,4 +1,4 @@
-import { CreateComponentType, FilterEnum } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType, FilterEnum } from '@/packages/index.d'
 import { HistoryActionTypeEnum } from '@/store/modules/chartHistoryStore/chartHistoryStore.d'
 import { SyncEnum } from '@/enums/editPageEnum'
 import {
@@ -41,9 +41,10 @@ export enum EditCanvasTypeEnum {
   SCALE = 'scale',
   USER_SCALE = 'userScale',
   LOCK_SCALE = 'lockScale',
+  SAVE_STATUS = 'saveStatus',
   IS_CREATE = 'isCreate',
   IS_DRAG = 'isDrag',
-  SAVE_STATUS = 'saveStatus'
+  IS_SELECT = 'isSelect'
 }
 
 // 编辑区域(临时)
@@ -65,6 +66,8 @@ export type EditCanvasType = {
   [EditCanvasTypeEnum.IS_DRAG]: boolean
   // 保存状态
   [EditCanvasTypeEnum.SAVE_STATUS]: SyncEnum
+  // 框选中
+  [EditCanvasTypeEnum.IS_SELECT]: boolean
 }
 
 // 画布数据/滤镜/背景色/宽高主题等
@@ -146,7 +149,7 @@ export type TargetChartType = {
 
 // 数据记录
 export type RecordChartType = {
-  charts: CreateComponentType | CreateComponentType[]
+  charts: CreateComponentType | CreateComponentGroupType | Array<CreateComponentType | CreateComponentGroupType>
   type: HistoryActionTypeEnum.CUT | HistoryActionTypeEnum.COPY
 }
 
@@ -211,12 +214,12 @@ export interface ChartEditStoreType {
   [ChartEditStoreEnum.TARGET_CHART]: TargetChartType
   [ChartEditStoreEnum.RECORD_CHART]?: RecordChartType
   [ChartEditStoreEnum.REQUEST_GLOBAL_CONFIG]: RequestGlobalConfigType
-  [ChartEditStoreEnum.COMPONENT_LIST]: CreateComponentType[]
+  [ChartEditStoreEnum.COMPONENT_LIST]: Array<CreateComponentType | CreateComponentGroupType>
 }
 
-// 需要存储的数据内容
+// 存储数据类型
 export interface ChartEditStorage {
   [ChartEditStoreEnum.EDIT_CANVAS_CONFIG]: EditCanvasConfigType
   [ChartEditStoreEnum.REQUEST_GLOBAL_CONFIG]: RequestGlobalConfigType
-  [ChartEditStoreEnum.COMPONENT_LIST]: CreateComponentType[]
+  [ChartEditStoreEnum.COMPONENT_LIST]: Array<CreateComponentType | CreateComponentGroupType>
 }

+ 309 - 120
src/store/modules/chartEditStore/chartEditStore.ts

@@ -1,10 +1,11 @@
+import { toRaw } from 'vue'
 import { defineStore } from 'pinia'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
+import { PublicGroupConfigClass } from '@/packages/public/publicConfig'
 import debounce from 'lodash/debounce'
 import cloneDeep from 'lodash/cloneDeep'
 import { defaultTheme, globalThemeJson } from '@/settings/chartThemes/index'
 import { requestInterval, previewScaleType, requestIntervalUnit } from '@/settings/designSetting'
-import { RequestBodyEnum } from '@/enums/httpEnum'
 // 记录记录
 import { useChartHistoryStore } from '@/store/modules/chartHistoryStore/chartHistoryStore'
 // 全局设置
@@ -67,6 +68,8 @@ export const useChartEditStore = defineStore({
       isCreate: false,
       // 拖拽中
       isDrag: false,
+      // 框选中
+      isSelect: false,
       // 同步中
       saveStatus: SyncEnum.PENDING
     },
@@ -128,8 +131,8 @@ export const useChartEditStore = defineStore({
       requestIntervalUnit: requestIntervalUnit,
       requestParams: {
         Body: {
-          "form-data": {},
-          "x-www-form-urlencoded": {},
+          'form-data': {},
+          'x-www-form-urlencoded': {},
           json: '',
           xml: ''
         },
@@ -156,7 +159,7 @@ export const useChartEditStore = defineStore({
     getEditCanvasConfig(): EditCanvasConfigType {
       return this.editCanvasConfig
     },
-    getTargetChart():TargetChartType {
+    getTargetChart(): TargetChartType {
       return this.targetChart
     },
     getRecordChart(): RecordChartType | undefined {
@@ -165,7 +168,7 @@ export const useChartEditStore = defineStore({
     getRequestGlobalConfig(): RequestGlobalConfigType {
       return this.requestGlobalConfig
     },
-    getComponentList(): CreateComponentType[] {
+    getComponentList(): Array<CreateComponentType | CreateComponentGroupType> {
       return this.componentList
     },
     // 获取需要存储的数据项
@@ -183,11 +186,11 @@ export const useChartEditStore = defineStore({
       this.projectInfo[key] = value
     },
     // * 设置 editCanvas 数据项
-    setEditCanvas<T extends keyof EditCanvasType,  K extends EditCanvasType[T]>(key: T, value: K) {
+    setEditCanvas<T extends keyof EditCanvasType, K extends EditCanvasType[T]>(key: T, value: K) {
       this.editCanvas[key] = value
     },
     // * 设置 editCanvasConfig(需保存后端) 数据项
-    setEditCanvasConfig<T extends keyof EditCanvasConfigType,  K extends EditCanvasConfigType[T]>(key: T, value: K) {
+    setEditCanvasConfig<T extends keyof EditCanvasConfigType, K extends EditCanvasConfigType[T]>(key: T, value: K) {
       this.editCanvasConfig[key] = value
     },
     // * 设置右键菜单
@@ -195,39 +198,39 @@ export const useChartEditStore = defineStore({
       this.rightMenuShow = value
     },
     // * 设置目标数据 hover
-    setTargetHoverChart(hoverId?:TargetChartType["hoverId"]) {
+    setTargetHoverChart(hoverId?: TargetChartType['hoverId']) {
       this.targetChart.hoverId = hoverId
     },
     // * 设置目标数据 select
     setTargetSelectChart(selectId?: string | string[], push: boolean = false) {
       // 重复选中
-      if(this.targetChart.selectId.find((e: string) => e === selectId)) return
+      if (this.targetChart.selectId.find((e: string) => e === selectId)) return
 
       // 无 id 清空
-      if(!selectId) {
+      if (!selectId) {
         this.targetChart.selectId = []
         return
       }
-      // 新增
-      if(push) {
+      // 多选
+      if (push) {
         // 字符串
-        if(isString(selectId)) {
+        if (isString(selectId)) {
           this.targetChart.selectId.push(selectId)
           return
         }
         // 数组
-        if(isArray(selectId)) {
+        if (isArray(selectId)) {
           this.targetChart.selectId.push(...selectId)
           return
         }
       } else {
         // 字符串
-        if(isString(selectId)) {
+        if (isString(selectId)) {
           this.targetChart.selectId = [selectId]
           return
         }
         // 数组
-        if(isArray(selectId)) {
+        if (isArray(selectId)) {
           this.targetChart.selectId = selectId
           return
         }
@@ -239,66 +242,108 @@ export const useChartEditStore = defineStore({
     },
     // * 设置鼠标位置
     setMousePosition(x?: number, y?: number, startX?: number, startY?: number): void {
-      if (startX) this.mousePosition.startX = startX
-      if (startY) this.mousePosition.startY = startY
       if (x) this.mousePosition.x = x
       if (y) this.mousePosition.y = y
+      if (startX) this.mousePosition.startX = startX
+      if (startY) this.mousePosition.startY = startY
     },
-    // * 找到目标 id 数据下标位置(无则返回-1)
+    // * 找到目标 id 数据下标位置,id可为父级或子集数组(无则返回-1)
     fetchTargetIndex(id?: string): number {
-      const targetId = id || this.getTargetChart.selectId.length && this.getTargetChart.selectId[0] || undefined
-      if(!targetId) {
+      const targetId = id || (this.getTargetChart.selectId.length && this.getTargetChart.selectId[0]) || undefined
+      if (!targetId) {
         loadingFinish()
         return -1
       }
-      const index = this.componentList.findIndex(e => e.id === targetId)
-      if (index === -1) {
-        loadingError()
+      const targetIndex = this.componentList.findIndex(e => e.id === targetId)
+
+      // 当前
+      if (targetIndex !== -1) {
+        return targetIndex
+      } else {
+        const length = this.getComponentList.length
+        for (let i = 0; i < length; i++) {
+          if (this.getComponentList[i].isGroup) {
+            for (const cItem of (this.getComponentList[i] as CreateComponentGroupType).groupList) {
+              if (cItem.id === targetId) {
+                return i
+              }
+            }
+          }
+        }
       }
-      return index
+      return -1
+    },
+    // * 统一格式化处理入参 id
+    idPreFormat(id?: string | string[]) {
+      const idArr = []
+      if (!id) {
+        idArr.push(...this.getTargetChart.selectId)
+        return idArr
+      }
+      if (isString(id)) idArr.push(id)
+      if (isArray(id)) idArr.push(...id)
+      return idArr
     },
     /**
      * * 新增组件列表
-     * @param chartConfig 新图表实例
+     * @param componentInstance 新图表实例
      * @param isHead 是否头部插入
      * @param isHistory 是否进行记录
      * @returns
      */
-    addComponentList(chartConfig: CreateComponentType, isHead = false, isHistory = false): void {
+    addComponentList(
+      componentInstance:
+        | CreateComponentType
+        | CreateComponentGroupType
+        | Array<CreateComponentType | CreateComponentGroupType>,
+      isHead = false,
+      isHistory = false
+    ): void {
+      if (componentInstance instanceof Array) {
+        componentInstance.forEach(item => {
+          this.addComponentList(item, isHead, isHistory)
+        })
+        return
+      }
       if (isHistory) {
-        chartHistoryStore.createAddHistory(chartConfig)
+        chartHistoryStore.createAddHistory([componentInstance])
       }
       if (isHead) {
-        this.componentList.unshift(chartConfig)
+        this.componentList.unshift(componentInstance)
         return
       }
-      this.componentList.push(chartConfig)
+      this.componentList.push(componentInstance)
     },
-    // * 删除组件列表
-    removeComponentList(isHistory = true): void {
+    // * 删除组件
+    removeComponentList(id?: string | string[], isHistory = true): void {
       try {
+        const idArr = this.idPreFormat(id)
+        const history: Array<CreateComponentType | CreateComponentGroupType> = []
+        // 遍历所有对象
+        if (!idArr.length) return
+
         loadingStart()
-        const index  = this.fetchTargetIndex()
-        if (index !== -1) {
-          isHistory ? chartHistoryStore.createDeleteHistory(this.getComponentList[index]) : undefined
-          this.componentList.splice(index, 1)
-          loadingFinish()
-          return
-        }
-      } catch(value) {
+        idArr.forEach(ids => {
+          const index = this.fetchTargetIndex(ids)
+          if (index !== -1) {
+            history.push(this.getComponentList[index])
+            this.componentList.splice(index, 1)
+          }
+        })
+        isHistory && chartHistoryStore.createDeleteHistory(history)
+        loadingFinish()
+        return
+      } catch (value) {
         loadingError()
       }
     },
     // * 更新组件列表某一项的值
-    updateComponentList(index: number, newData: CreateComponentType) {
+    updateComponentList(index: number, newData: CreateComponentType | CreateComponentGroupType) {
       if (index < 1 && index > this.getComponentList.length) return
       this.componentList[index] = newData
     },
     // * 设置页面样式属性
-    setPageStyle<T extends keyof CSSStyleDeclaration>(
-      key: T,
-      value: any
-    ): void {
+    setPageStyle<T extends keyof CSSStyleDeclaration>(key: T, value: any): void {
       const dom = this.getEditCanvas.editContentDom
       if (dom) {
         dom.style[key] = value
@@ -307,6 +352,9 @@ export const useChartEditStore = defineStore({
     // * 移动组件列表层级位置到两端
     setBothEnds(isEnd = false, isHistory = true): void {
       try {
+        // 暂不支持多选
+        if (this.getTargetChart.selectId.length > 1) return
+
         loadingStart()
         const length = this.getComponentList.length
         if (length < 2) {
@@ -314,18 +362,18 @@ export const useChartEditStore = defineStore({
           return
         }
 
-        const index  = this.fetchTargetIndex()
+        const index = this.fetchTargetIndex()
         const targetData = this.getComponentList[index]
         if (index !== -1) {
           // 置底排除最底层, 置顶排除最顶层
-          if ((isEnd && index === 0) || (!isEnd && index === length - 1 )) {
+          if ((isEnd && index === 0) || (!isEnd && index === length - 1)) {
             loadingFinish()
             return
           }
 
           // 记录原有位置
-          const setIndex = (t:CreateComponentType, i:number) => {
-            const temp = cloneDeep(t)
+          const setIndex = (componentInstance: CreateComponentType | CreateComponentGroupType, i: number) => {
+            const temp = cloneDeep(componentInstance)
             temp.attr.zIndex = i
             return temp
           }
@@ -333,18 +381,18 @@ export const useChartEditStore = defineStore({
           // 历史记录
           if (isHistory) {
             chartHistoryStore.createLayerHistory(
-              setIndex(targetData, index),
+              [setIndex(targetData, index)],
               isEnd ? HistoryActionTypeEnum.BOTTOM : HistoryActionTypeEnum.TOP
             )
           }
 
           // 插入两端
           this.addComponentList(targetData, isEnd)
-          this.getComponentList.splice(isEnd ? index + 1: index, 1)
+          this.getComponentList.splice(isEnd ? index + 1 : index, 1)
           loadingFinish()
           return
         }
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
@@ -359,6 +407,9 @@ export const useChartEditStore = defineStore({
     // * 上移/下移互换图表位置
     wrap(isDown = false, isHistory = true) {
       try {
+        // 暂不支持多选
+        if (this.getTargetChart.selectId.length > 1) return
+
         loadingStart()
         const length = this.getComponentList.length
         if (length < 2) {
@@ -366,7 +417,7 @@ export const useChartEditStore = defineStore({
           return
         }
 
-        const index:number  = this.fetchTargetIndex()
+        const index: number = this.fetchTargetIndex()
         if (index !== -1) {
           // 下移排除最底层, 上移排除最顶层
           if ((isDown && index === 0) || (!isDown && index === length - 1)) {
@@ -381,7 +432,7 @@ export const useChartEditStore = defineStore({
           // 历史记录
           if (isHistory) {
             chartHistoryStore.createLayerHistory(
-              targetItem,
+              [targetItem],
               isDown ? HistoryActionTypeEnum.DOWN : HistoryActionTypeEnum.UP
             )
           }
@@ -390,7 +441,7 @@ export const useChartEditStore = defineStore({
           loadingFinish()
           return
         }
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
@@ -405,18 +456,21 @@ export const useChartEditStore = defineStore({
     // * 复制
     setCopy(isCut = false) {
       try {
+        // 暂不支持多选
+        if (this.getTargetChart.selectId.length > 1) return
+
         loadingStart()
-        const index:number  = this.fetchTargetIndex()
+        const index: number = this.fetchTargetIndex()
         if (index !== -1) {
-          const copyData:RecordChartType = {
-           charts :this.getComponentList[index],
-           type: isCut ? HistoryActionTypeEnum.CUT : HistoryActionTypeEnum.COPY
+          const copyData: RecordChartType = {
+            charts: this.getComponentList[index],
+            type: isCut ? HistoryActionTypeEnum.CUT : HistoryActionTypeEnum.COPY
           }
           this.setRecordChart(copyData)
           window['$message'].success(isCut ? '剪切图表成功' : '复制图表成功!')
           loadingFinish()
         }
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
@@ -433,7 +487,7 @@ export const useChartEditStore = defineStore({
           loadingFinish()
           return
         }
-        const parseHandle = (e: CreateComponentType) => {
+        const parseHandle = (e: CreateComponentType | CreateComponentGroupType) => {
           e = cloneDeep(e)
           // 生成新 id
           e.id = getUUID()
@@ -449,7 +503,7 @@ export const useChartEditStore = defineStore({
             // 剪切需删除原数据
             if (isCut) {
               this.setTargetSelectChart(e.id)
-              this.removeComponentList(true)
+              this.removeComponentList(undefined, true)
             }
           })
           if (isCut) this.setRecordChart(undefined)
@@ -464,50 +518,59 @@ export const useChartEditStore = defineStore({
           this.setRecordChart(undefined)
         }
         loadingFinish()
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
     // * 撤回/前进 目标处理
-    setBackAndSetForwardHandle(item: HistoryItemType, isForward = false) {
+    setBackAndSetForwardHandle(HistoryItem: HistoryItemType, isForward = false) {
       // 处理画布
-      if (item.targetType === HistoryTargetTypeEnum.CANVAS) {
-        this.editCanvas = item.historyData as EditCanvasType
+      if (HistoryItem.targetType === HistoryTargetTypeEnum.CANVAS) {
+        this.editCanvas = HistoryItem.historyData[0] as EditCanvasType
         return
       }
 
-      const historyData = item.historyData as CreateComponentType
+      let historyData = HistoryItem.historyData as Array<CreateComponentType | CreateComponentGroupType>
+      if (isArray(historyData)) {
+        // 选中目标元素,支持多个
+        historyData.forEach((item: CreateComponentType | CreateComponentGroupType) => {
+          this.setTargetSelectChart(item.id, true)
+        })
+      }
 
       // 处理新增类型
-      const isAdd = item.actionType === HistoryActionTypeEnum.ADD
-      const isDel = item.actionType === HistoryActionTypeEnum.DELETE
-      this.setTargetSelectChart(historyData.id)
+      const isAdd = HistoryItem.actionType === HistoryActionTypeEnum.ADD
+      const isDel = HistoryItem.actionType === HistoryActionTypeEnum.DELETE
       if (isAdd || isDel) {
         if ((isAdd && isForward) || (isDel && !isForward)) {
-          this.addComponentList(historyData)
-          return  
+          historyData.forEach(item => {
+            this.addComponentList(item)
+          })
+          return
         }
-        this.removeComponentList(false)
+        historyData.forEach(item => {
+          this.removeComponentList(item.id, false)
+        })
         return
       }
 
       // 处理层级
-      const isTop = item.actionType === HistoryActionTypeEnum.TOP
-      const isBottom = item.actionType === HistoryActionTypeEnum.BOTTOM
+      const isTop = HistoryItem.actionType === HistoryActionTypeEnum.TOP
+      const isBottom = HistoryItem.actionType === HistoryActionTypeEnum.BOTTOM
       if (isTop || isBottom) {
         if (!isForward) {
           // 插入到原有位置
           if (isTop) this.getComponentList.pop()
           if (isBottom) this.getComponentList.shift()
-          this.getComponentList.splice(historyData.attr.zIndex, 0, historyData)
+          this.getComponentList.splice(historyData[0].attr.zIndex, 0, historyData[0])
           return
         }
         if (isTop) this.setTop(false)
         if (isBottom) this.setBottom(false)
       }
 
-      const isUp = item.actionType === HistoryActionTypeEnum.UP
-      const isDown = item.actionType === HistoryActionTypeEnum.DOWN
+      const isUp = HistoryItem.actionType === HistoryActionTypeEnum.UP
+      const isDown = HistoryItem.actionType === HistoryActionTypeEnum.DOWN
       if (isUp || isDown) {
         if ((isUp && isForward) || (isDown && !isForward)) {
           this.setUp(false)
@@ -517,8 +580,32 @@ export const useChartEditStore = defineStore({
         return
       }
 
-      // 处理内容修改
-      this.getComponentList[this.fetchTargetIndex()] = item.historyData as CreateComponentType
+      // 处理分组
+      const isGroup = HistoryItem.actionType === HistoryActionTypeEnum.GROUP
+      const isUnGroup = HistoryItem.actionType === HistoryActionTypeEnum.UN_GROUP
+      if (isGroup || isUnGroup) {
+        if ((isGroup && isForward) || (isUnGroup && !isForward)) {
+          const ids: string[] = []
+          if(historyData.length > 1) {
+            historyData.forEach(item => {
+              ids.push(item.id)
+            })
+          } else {
+            (historyData[0] as CreateComponentGroupType).groupList.forEach(item => {
+              ids.push(item.id)
+            })
+          }
+          this.setGroup(ids, false)
+          return
+        }
+        // 都需使用子组件的id去解组
+        if(historyData.length > 1) {
+          this.setUnGroup([(historyData[0] as CreateComponentType).id], undefined, false)
+        } else {
+          this.setUnGroup([(historyData[0] as CreateComponentGroupType).groupList[0].id], undefined, false)
+        }
+        return
+      }
     },
     // * 撤回
     setBack() {
@@ -529,17 +616,9 @@ export const useChartEditStore = defineStore({
           loadingFinish()
           return
         }
-        if (Array.isArray(targetData)) {
-          targetData.forEach((e: HistoryItemType) => {
-            this.setBackAndSetForwardHandle(e)
-          })
-          loadingFinish()
-          return
-        }
         this.setBackAndSetForwardHandle(targetData)
         loadingFinish()
-
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
@@ -552,39 +631,157 @@ export const useChartEditStore = defineStore({
           loadingFinish()
           return
         }
-        if (Array.isArray(targetData)) {
-          targetData.forEach((e: HistoryItemType) => {
-            this.setBackAndSetForwardHandle(e, true)
-          })
-          loadingFinish()
-          return
-        }
         this.setBackAndSetForwardHandle(targetData, true)
         loadingFinish()
-
-      } catch(value) {
+      } catch (value) {
         loadingError()
       }
     },
     // * 移动位置
     setMove(keyboardValue: MenuEnum) {
-      const index  = this.fetchTargetIndex()
-      if(index === -1) return
+      const index = this.fetchTargetIndex()
+      if (index === -1) return
       const attr = this.getComponentList[index].attr
       const distance = settingStore.getChartMoveDistance
       switch (keyboardValue) {
         case MenuEnum.ARROW_UP:
           attr.y -= distance
-          break;
+          break
         case MenuEnum.ARROW_RIGHT:
           attr.x += distance
-          break;
+          break
         case MenuEnum.ARROW_DOWN:
           attr.y += distance
-          break;
+          break
         case MenuEnum.ARROW_LEFT:
           attr.x -= distance
-          break;
+          break
+      }
+    },
+    // * 创建分组
+    setGroup(id?: string | string[], isHistory = true) {
+      try {
+        const selectIds = this.idPreFormat(id) || this.getTargetChart.selectId
+        if (selectIds.length < 2) return
+
+        loadingStart()
+        const groupClass = new PublicGroupConfigClass()
+        // 记录整体坐标
+        const groupAttr = {
+          l: this.getEditCanvasConfig.width,
+          t: this.getEditCanvasConfig.height,
+          r: 0,
+          b: 0
+        }
+        const targetList: CreateComponentType[] = []
+        const historyList: CreateComponentType[] = []
+
+        // 若目标中有数组则先解组
+        const newSelectIds: string[] = []
+        selectIds.forEach((id: string) => {
+          const targetIndex = this.fetchTargetIndex(id)
+          if (targetIndex !== -1 && this.getComponentList[targetIndex].isGroup) {
+            this.setUnGroup(
+              [id],
+              (e: CreateComponentType[]) => {
+                e.forEach(e => {
+                  this.addComponentList(e)
+                  newSelectIds.push(e.id)
+                })
+              },
+              false
+            )
+          } else if (targetIndex !== -1) {
+            newSelectIds.push(id)
+          }
+        })
+        newSelectIds.forEach((id: string) => {
+          // 获取目标数据并从 list 中移除 (成组后不可再次成组, 断言处理)
+          const item = this.componentList.splice(this.fetchTargetIndex(id), 1)[0] as CreateComponentType
+          const { x, y, w, h } = item.attr
+          const { l, t, r, b } = groupAttr
+          // 左
+          groupAttr.l = l > x ? x : l
+          // 上
+          groupAttr.t = t > y ? y : t
+          // 宽
+          groupAttr.r = r < x + w ? x + w : r
+          // 高
+          groupAttr.b = b < y + h ? y + h : b
+
+          targetList.push(item)
+          historyList.push(toRaw(item))
+        })
+
+        // 修改原数据之前,先记录
+        if (isHistory) chartHistoryStore.createGroupHistory(historyList)
+
+        // 设置子组件的位置
+        targetList.forEach((item: CreateComponentType) => {
+          item.attr.x = item.attr.x - groupAttr.l
+          item.attr.y = item.attr.y - groupAttr.t
+          groupClass.groupList.push(item)
+        })
+
+        // 设置 group 属性
+        groupClass.attr.x = groupAttr.l
+        groupClass.attr.y = groupAttr.t
+        groupClass.attr.w = groupAttr.r - groupAttr.l
+        groupClass.attr.h = groupAttr.b - groupAttr.t
+
+        this.addComponentList(groupClass)
+        this.setTargetSelectChart(groupClass.id)
+
+        loadingFinish()
+      } catch (error) {
+        console.log(error)
+        window['$message'].error('创建分组失败,请联系管理员!')
+        loadingFinish()
+      }
+    },
+    // * 解除分组
+    setUnGroup(ids?: string[], callBack?: (e: CreateComponentType[]) => void, isHistory = true) {
+      try {
+        const selectGroupIdArr = ids || this.getTargetChart.selectId
+        if (selectGroupIdArr.length !== 1) return
+        loadingStart()
+
+        // 解组
+        const unGroup = (targetIndex: number) => {
+          const targetGroup = this.getComponentList[targetIndex] as CreateComponentGroupType
+          if (!targetGroup.isGroup) return
+
+          // 记录数据
+          if (isHistory) chartHistoryStore.createUnGroupHistory(cloneDeep([targetGroup]))
+
+          // 分离组件并还原位置属性
+          targetGroup.groupList.forEach(item => {
+            item.attr.x = item.attr.x + targetGroup.attr.x
+            item.attr.y = item.attr.y + targetGroup.attr.y
+            if (!callBack) {
+              this.addComponentList(item)
+            }
+          })
+          this.setTargetSelectChart(targetGroup.id)
+          // 删除分组
+          this.removeComponentList(targetGroup.id, false)
+
+          if (callBack) {
+            callBack(targetGroup.groupList)
+          }
+        }
+
+        const targetIndex = this.fetchTargetIndex(selectGroupIdArr[0])
+        // 判断目标是否为分组父级
+        if (targetIndex !== -1) {
+          unGroup(targetIndex)
+        }
+
+        loadingFinish()
+      } catch (error) {
+        console.log(error)
+        window['$message'].error('解除分组失败,请联系管理员!')
+        loadingFinish()
       }
     },
     // * 页面缩放设置-----------------
@@ -597,32 +794,24 @@ export const useChartEditStore = defineStore({
     computedScale() {
       if (this.getEditCanvas.editLayoutDom) {
         // 现有展示区域
-        const width =
-          this.getEditCanvas.editLayoutDom.clientWidth - this.getEditCanvas.offset * 2 - 5
-        const height =
-          this.getEditCanvas.editLayoutDom.clientHeight - this.getEditCanvas.offset * 4
+        const width = this.getEditCanvas.editLayoutDom.clientWidth - this.getEditCanvas.offset * 2 - 5
+        const height = this.getEditCanvas.editLayoutDom.clientHeight - this.getEditCanvas.offset * 4
 
         // 用户设定大小
         const editCanvasWidth = this.editCanvasConfig.width
         const editCanvasHeight = this.editCanvasConfig.height
 
         // 需保持的比例
-        const baseProportion = parseFloat(
-          (editCanvasWidth / editCanvasHeight).toFixed(5)
-        )
+        const baseProportion = parseFloat((editCanvasWidth / editCanvasHeight).toFixed(5))
         const currentRate = parseFloat((width / height).toFixed(5))
 
         if (currentRate > baseProportion) {
           // 表示更宽
-          const scaleWidth = parseFloat(
-            ((height * baseProportion) / editCanvasWidth).toFixed(5)
-          )
-          this.setScale( scaleWidth > 1 ? 1 : scaleWidth)
+          const scaleWidth = parseFloat(((height * baseProportion) / editCanvasWidth).toFixed(5))
+          this.setScale(scaleWidth > 1 ? 1 : scaleWidth)
         } else {
           // 表示更高
-          const scaleHeight = parseFloat(
-            (width / baseProportion / editCanvasHeight).toFixed(5)
-          )
+          const scaleHeight = parseFloat((width / baseProportion / editCanvasHeight).toFixed(5))
           this.setScale(scaleHeight > 1 ? 1 : scaleHeight)
         }
       } else {
@@ -655,4 +844,4 @@ export const useChartEditStore = defineStore({
       }
     }
   }
-})
+})

+ 2 - 0
src/store/modules/chartHistoryStore/chartHistoryDefine.ts

@@ -15,6 +15,8 @@ export const historyActionTypeName = {
   [HistoryActionTypeEnum.BOTTOM]: '层级置底',
   [HistoryActionTypeEnum.UP]: '层级上移',
   [HistoryActionTypeEnum.DOWN]: '层级下移',
+  [HistoryActionTypeEnum.GROUP]: '创建分组',
+  [HistoryActionTypeEnum.UN_GROUP]: '解除分组',
   [HistoryActionTypeEnum.SELECT_HISTORY]: '选择记录',
   
   [HistoryTargetTypeEnum.CANVAS]: '画布初始化'

+ 7 - 2
src/store/modules/chartHistoryStore/chartHistoryStore.d.ts

@@ -1,4 +1,4 @@
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { EditCanvasType } from '@/store/modules/chartEditStore/chartEditStore.d'
 
 // 操作类型枚举
@@ -25,6 +25,10 @@ export enum HistoryActionTypeEnum {
   UP = 'up',
   // 下移
   DOWN = 'down',
+  // 成组
+  GROUP = 'group',
+  // 解组
+  UN_GROUP = 'unGroup',
   // 选择历史记录
   SELECT_HISTORY = 'selectHistory'
 }
@@ -51,10 +55,11 @@ export enum HistoryStackItemEnum {
 
 // 历史记录项类型
 export interface HistoryItemType {
+  // 会有同时操作多个组件场景
   [HistoryStackItemEnum.ID]: string
   [HistoryStackItemEnum.TARGET_TYPE]: HistoryTargetTypeEnum
   [HistoryStackItemEnum.ACTION_TYPE]: HistoryActionTypeEnum
-  [HistoryStackItemEnum.HISTORY_DATA]: CreateComponentType | EditCanvasType
+  [HistoryStackItemEnum.HISTORY_DATA]: CreateComponentType[] | CreateComponentGroupType[] | EditCanvasType[]
 }
 
 // 历史 Store 类型

+ 37 - 71
src/store/modules/chartHistoryStore/chartHistoryStore.ts

@@ -1,5 +1,5 @@
 import { defineStore } from 'pinia'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { EditCanvasType } from '@/store/modules/chartEditStore/chartEditStore.d'
 import { loadingStart, loadingFinish, loadingError } from '@/utils'
 import { editHistoryMax } from '@/settings/designSetting'
@@ -35,31 +35,26 @@ export const useChartHistoryStore = defineStore({
      * @param targetType 对象类型(默认图表)
      */
     createStackItem(
-      item: CreateComponentType | EditCanvasType,
+      item: CreateComponentType[] | CreateComponentGroupType[] | EditCanvasType[],
       actionType: HistoryActionTypeEnum,
       targetType: HistoryTargetTypeEnum = HistoryTargetTypeEnum.CHART
     ) {
-      // 优化性能转为freeze
-      this.pushBackStackItem(Object.freeze({
-        [HistoryStackItemEnum.ID]: new Date().getTime().toString(),
-        [HistoryStackItemEnum.HISTORY_DATA]: item,
-        [HistoryStackItemEnum.ACTION_TYPE]: actionType,
-        [HistoryStackItemEnum.TARGET_TYPE]: targetType
-      } as const))
+      // 优化性能转为 freeze
+      this.pushBackStackItem(
+        Object.freeze({
+          [HistoryStackItemEnum.ID]: new Date().getTime().toString(),
+          [HistoryStackItemEnum.HISTORY_DATA]: item,
+          [HistoryStackItemEnum.ACTION_TYPE]: actionType,
+          [HistoryStackItemEnum.TARGET_TYPE]: targetType
+        } as const)
+      )
     },
     // * 画布初始化
     canvasInit(canvas: EditCanvasType) {
-      this.createStackItem(
-        canvas,
-        HistoryActionTypeEnum.ADD,
-        HistoryTargetTypeEnum.CANVAS
-      )
+      this.createStackItem([canvas], HistoryActionTypeEnum.ADD, HistoryTargetTypeEnum.CANVAS)
     },
     // * 推入后退栈
-    pushBackStackItem(
-      item: HistoryItemType | Array<HistoryItemType>,
-      notClear = false
-    ): void {
+    pushBackStackItem(item: HistoryItemType | Array<HistoryItemType>, notClear = false): void {
       if (item instanceof Array) this.backStack = [...this.backStack, ...item]
       else this.backStack.push(item)
       this.backStack.splice(0, this.backStack.length - editHistoryMax)
@@ -69,30 +64,17 @@ export const useChartHistoryStore = defineStore({
     },
     // * 推入前进栈
     pushForwardStack(item: HistoryItemType | Array<HistoryItemType>): void {
-      if (item instanceof Array)
-        this.forwardStack = [...this.forwardStack, ...item]
+      if (item instanceof Array) this.forwardStack = [...this.forwardStack, ...item]
       else this.forwardStack.push(item)
     },
     // * 移出后退栈
-    popBackStackItem(
-      index?: number
-    ): HistoryItemType[] | HistoryItemType | undefined {
-      const length = this.backStack.length
-      if (index && length >= index) {
-        return this.backStack.splice(-index)
-      }
+    popBackStackItem(): HistoryItemType | undefined {
       if (this.backStack.length > 0) {
         return this.backStack.pop()
       }
     },
     // * 移出前进栈
-    popForwardStack(
-      index?: number
-    ): HistoryItemType[] | HistoryItemType | undefined {
-      const length = this.forwardStack.length
-      if (index && length >= index) {
-        return this.forwardStack.splice(-index)
-      }
+    popForwardStack(): HistoryItemType | undefined {
       if (this.forwardStack.length > 0) {
         return this.forwardStack.pop()
       }
@@ -104,7 +86,7 @@ export const useChartHistoryStore = defineStore({
     // * 清空后退栈(保留初始化)
     clearBackStack() {
       const canvasHistory = this.getBackStack[0]
-      this.backStack =  [canvasHistory]
+      this.backStack = [canvasHistory]
     },
     // * 撤回
     backAction() {
@@ -148,59 +130,43 @@ export const useChartHistoryStore = defineStore({
       }
     },
     // * 新增组件记录
-    createAddHistory(item: CreateComponentType) {
-      this.createStackItem(
-        item,
-        HistoryActionTypeEnum.ADD,
-        HistoryTargetTypeEnum.CHART
-      )
+    createAddHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.ADD, HistoryTargetTypeEnum.CHART)
     },
     // * 更新属性记录(大小、图表属性)
-    createUpdateHistory(item: CreateComponentType) {
-      this.createStackItem(
-        item,
-        HistoryActionTypeEnum.UPDATE,
-        HistoryTargetTypeEnum.CHART
-      )
+    createUpdateHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.UPDATE, HistoryTargetTypeEnum.CHART)
     },
     // * 删除组件记录
-    createDeleteHistory(item: CreateComponentType) {
-      this.createStackItem(
-        item,
-        HistoryActionTypeEnum.DELETE,
-        HistoryTargetTypeEnum.CHART
-      )
+    createDeleteHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.DELETE, HistoryTargetTypeEnum.CHART)
     },
     // * 移动组件记录
-    createMoveHistory(item: CreateComponentType) {
-      this.createStackItem(
-        item,
-        HistoryActionTypeEnum.MOVE,
-        HistoryTargetTypeEnum.CHART
-      )
+    createMoveHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.MOVE, HistoryTargetTypeEnum.CHART)
     },
     // * 改变层级组件记录
     createLayerHistory(
-      item: CreateComponentType,
+      item: Array<CreateComponentType | CreateComponentGroupType>,
       type:
         | HistoryActionTypeEnum.TOP
         | HistoryActionTypeEnum.DOWN
         | HistoryActionTypeEnum.UP
         | HistoryActionTypeEnum.BOTTOM
     ) {
-      this.createStackItem(
-        item,
-        type,
-        HistoryTargetTypeEnum.CHART
-      )
+      this.createStackItem(item, type, HistoryTargetTypeEnum.CHART)
     },
     // * 剪切组件记录
-    createPasteHistory(item: CreateComponentType) {
-      this.createStackItem(
-        item,
-        HistoryActionTypeEnum.CUT,
-        HistoryTargetTypeEnum.CHART
-      )
+    createPasteHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.CUT, HistoryTargetTypeEnum.CHART)
+    },
+    // * 创建分组
+    createGroupHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.GROUP, HistoryTargetTypeEnum.CHART)
+    },
+    // * 解除分组
+    createUnGroupHistory(item: Array<CreateComponentType | CreateComponentGroupType>) {
+      this.createStackItem(item, HistoryActionTypeEnum.UN_GROUP, HistoryTargetTypeEnum.CHART)
     }
   }
 })

+ 1 - 1
src/views/chart/ContentCharts/components/ChartsItemBox/index.vue

@@ -45,7 +45,7 @@ const dragStartHandle = (e: DragEvent, item: ConfigType) => {
   componentInstall(item.chartKey, fetchChartComponent(item))
   componentInstall(item.conKey, fetchConfigComponent(item))
   // 将配置项绑定到拖拽属性上
-  e!.dataTransfer!.setData(DragKeyEnum.DROG_KEY, JSON.stringify(omit(item, ['image'])))
+  e!.dataTransfer!.setData(DragKeyEnum.DRAG_KEY, JSON.stringify(omit(item, ['image'])))
   // 修改状态
   chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_CREATE, true)
 }

+ 7 - 2
src/views/chart/ContentConfigurations/components/CanvasPage/components/ChartThemeColor/index.vue

@@ -28,7 +28,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed } from 'vue'
+import { computed } from 'vue'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { EditCanvasConfigEnum } from '@/store/modules/chartEditStore/chartEditStore.d'
 import {
@@ -46,8 +46,13 @@ const chartEditStore = useChartEditStore()
 
 // 全局颜色
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
+// 选中名称
 const selectName = computed(() => {
   return chartEditStore.getEditCanvasConfig.chartThemeColor
 })

+ 1 - 1
src/views/chart/ContentConfigurations/components/CanvasPage/index.vue

@@ -96,7 +96,7 @@
     </n-space>
 
     <!-- 滤镜 -->
-    <styles-setting :is-canvas="true" :chartStyles="canvasConfig"></styles-setting>
+    <styles-setting :isCanvas="true" :chartStyles="canvasConfig"></styles-setting>
     <n-divider style="margin: 10px 0;"></n-divider>
 
     <!-- 主题选择和全局配置 -->

+ 6 - 2
src/views/chart/ContentConfigurations/components/ChartAnimation/index.vue

@@ -34,7 +34,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { ref, computed } from 'vue'
 import { animations } from '@/settings/animations/index'
 import { CollapseItem } from '@/components/Pages/ChartItemSetting'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
@@ -42,12 +42,16 @@ import { useTargetData } from '../hooks/useTargetData.hook'
 
 // 全局颜色
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
 const hoverPreviewAnimate = ref('')
 
 const { targetData } = useTargetData()
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 // * 选中的动画样式
 const activeIndex = (value: string) => {
   const selectValue = targetData.value.styles.animations

+ 6 - 2
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataAjax/index.vue

@@ -84,7 +84,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs, onBeforeUnmount, watchEffect, toRaw } from 'vue'
+import { ref, toRefs, computed, onBeforeUnmount, watchEffect, toRaw } from 'vue'
 import { icon } from '@/plugins'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
 import { SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
@@ -108,7 +108,6 @@ const {
   requestIntervalUnit: GlobalRequestIntervalUnit
 } = toRefs(chartEditStore.getRequestGlobalConfig)
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
 // 是否展示数据分析
 const loading = ref(false)
@@ -142,6 +141,11 @@ const sendHandle = async () => {
   }
 }
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 watchEffect(() => {
   const filter = targetData.value?.filter
   if (lastFilter !== filter && firstFocus) {

+ 5 - 2
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/components/RequestGlobalConfig/index.vue

@@ -65,7 +65,7 @@
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs } from 'vue'
+import { ref, toRefs, computed } from 'vue'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
 import { SettingItemBox, SettingItem } from '@/components/Pages/ChartItemSetting'
 import { useTargetData } from '@/views/chart/ContentConfigurations/components/hooks/useTargetData.hook'
@@ -79,9 +79,12 @@ const { requestOriginUrl, requestInterval, requestIntervalUnit } = toRefs(chartE
 const editDisabled = ref(true)
 
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
 const showTable = ref(false)
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
 </script>
 
 <style lang="scss" scoped>

+ 2 - 3
src/views/chart/ContentConfigurations/components/ChartData/components/ChartDataRequest/index.vue

@@ -15,7 +15,7 @@
       <template #action>
         <n-space justify="space-between">
           <div>
-            <n-text>「 {{ chartConfig.categoryName || rename }} 」</n-text>
+            <n-text>「 {{ chartConfig.categoryName }} 」</n-text>
             <n-text>—— </n-text>
             <n-tag type="primary" :bordered="false" style="border-radius: 5px"> {{ requestContentTypeObj[requestContentType] }} </n-tag>
           </div>
@@ -37,9 +37,8 @@ const emit = defineEmits(['update:modelShow'])
 
 const { targetData } = useTargetData()
 // 解构基础配置
-const { chartConfig, rename } = toRefs(targetData.value)
+const { chartConfig } = toRefs(targetData.value)
 const { requestContentType } = toRefs(targetData.value.request)
-
 const requestContentTypeObj = {
   [RequestContentTypeEnum.DEFAULT]: '普通请求',
   [RequestContentTypeEnum.SQL]: 'SQL 请求'

+ 2 - 2
src/views/chart/ContentConfigurations/components/ChartSetting/index.vue

@@ -3,11 +3,11 @@
     <!-- 名称 -->
     <name-setting :chartConfig="targetData.chartConfig"></name-setting>
     <!-- 尺寸 -->
-    <size-setting :chartAttr="targetData.attr"></size-setting>
+    <size-setting :isGroup="targetData.isGroup" :chartAttr="targetData.attr"></size-setting>
     <!-- 位置 -->
     <position-setting :chartAttr="targetData.attr" :canvasConfig="chartEditStore.getEditCanvasConfig"/>
     <!-- 滤镜 -->
-    <styles-setting :chartStyles="targetData.styles"></styles-setting>
+    <styles-setting :isGroup="targetData.isGroup" :chartStyles="targetData.styles"></styles-setting>
     <!-- 自定义配置项 -->
     <component :is="targetData.chartConfig.conKey" :optionData="targetData.option"></component>
   </div>

+ 2 - 2
src/views/chart/ContentConfigurations/components/hooks/useTargetData.hook.ts

@@ -1,11 +1,11 @@
 import { computed, Ref } from 'vue'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 
 // 获取当前对象数据
 export const useTargetData = () => {
   const chartEditStore = useChartEditStore()
-  const targetData: Ref<CreateComponentType> = computed(() => {
+  const targetData: Ref<CreateComponentType | CreateComponentGroupType> = computed(() => {
     const list = chartEditStore.getComponentList
     const targetIndex = chartEditStore.fetchTargetIndex()
     return list[targetIndex]

+ 6 - 0
src/views/chart/ContentConfigurations/index.d.ts

@@ -0,0 +1,6 @@
+export enum TabsEnum {
+  PAGE_SETTING = 'pageSetting',
+  CHART_SETTING = 'chartSetting',
+  CHART_ANIMATION = 'chartAnimation',
+  CHART_DATA = 'chartData',
+}

+ 32 - 49
src/views/chart/ContentConfigurations/index.vue

@@ -11,21 +11,12 @@
       :collapsed="collapsed"
       :native-scrollbar="false"
       show-trigger="bar"
-      @collapse="collapsedHindle"
-      @expand="expandHindle"
+      @collapse="collapsedHandle"
+      @expand="expandHandle"
     >
-      <content-box
-        class="go-content-layers go-boderbox"
-        :show-top="false"
-        :depth="2"
-      >
+      <content-box class="go-content-layers go-boderbox" :show-top="false" :depth="2">
         <!-- 页面配置 -->
-        <n-tabs
-          v-show="!selectTarget"
-          class="tabs-box"
-          size="small"
-          type="segment"
-        >
+        <n-tabs v-if="!selectTarget" class="tabs-box" size="small" type="segment">
           <n-tab-pane
             v-for="item in globalTabList"
             :key="item.key"
@@ -46,14 +37,9 @@
         </n-tabs>
 
         <!-- 编辑 -->
-        <n-tabs
-          v-show="selectTarget"
-          class="tabs-box"
-          size="small"
-          type="segment"
-        >
+        <n-tabs v-if="selectTarget" v-model:value="tabsSelect" class="tabs-box" size="small" type="segment">
           <n-tab-pane
-            v-for="(item) in canvasTabList"
+            v-for="item in selectTarget.isGroup ? chartsDefaultTabList : chartsTabList"
             :key="item.key"
             :name="item.key"
             size="small"
@@ -80,6 +66,7 @@ import { ref, toRefs, watch, computed } from 'vue'
 import { icon } from '@/plugins'
 import { loadAsyncComponent } from '@/utils'
 import { ContentBox } from '../ContentBox/index'
+import { TabsEnum } from './index.d'
 import { useChartLayoutStore } from '@/store/modules/chartLayoutStore/chartLayoutStore'
 import { ChartLayoutStoreEnum } from '@/store/modules/chartLayoutStore/chartLayoutStore.d'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
@@ -88,35 +75,23 @@ const { getDetails } = toRefs(useChartLayoutStore())
 const { setItem } = useChartLayoutStore()
 const chartEditStore = useChartEditStore()
 
-const {
-  ConstructIcon,
-  FlashIcon,
-  DesktopOutlineIcon,
-  LeafIcon
-} = icon.ionicons5
+const { ConstructIcon, FlashIcon, DesktopOutlineIcon, LeafIcon } = icon.ionicons5
 
 const ContentEdit = loadAsyncComponent(() => import('../ContentEdit/index.vue'))
-const CanvasPage = loadAsyncComponent(() =>
-  import('./components/CanvasPage/index.vue')
-)
-const ChartSetting = loadAsyncComponent(() =>
-  import('./components/ChartSetting/index.vue')
-)
-const ChartData = loadAsyncComponent(() =>
-  import('./components/ChartData/index.vue')
-)
-const ChartAnimation = loadAsyncComponent(() =>
-  import('./components/ChartAnimation/index.vue')
-)
+const CanvasPage = loadAsyncComponent(() => import('./components/CanvasPage/index.vue'))
+const ChartSetting = loadAsyncComponent(() => import('./components/ChartSetting/index.vue'))
+const ChartData = loadAsyncComponent(() => import('./components/ChartData/index.vue'))
+const ChartAnimation = loadAsyncComponent(() => import('./components/ChartAnimation/index.vue'))
 
 const collapsed = ref<boolean>(getDetails.value)
+const tabsSelect = ref<TabsEnum>(TabsEnum.CHART_SETTING)
 
-const collapsedHindle = () => {
+const collapsedHandle = () => {
   collapsed.value = true
   setItem(ChartLayoutStoreEnum.DETAILS, true)
 }
 
-const expandHindle = () => {
+const expandHandle = () => {
   collapsed.value = false
   setItem(ChartLayoutStoreEnum.DETAILS, false)
 }
@@ -125,42 +100,50 @@ const selectTarget = computed(() => {
   const selectId = chartEditStore.getTargetChart.selectId
   // 排除多个
   if (selectId.length !== 1) return undefined
-  return chartEditStore.componentList[chartEditStore.fetchTargetIndex()]
+  const target = chartEditStore.componentList[chartEditStore.fetchTargetIndex()]
+  if (target?.isGroup) {
+    tabsSelect.value = TabsEnum.CHART_SETTING
+  }
+  return target
 })
 
 watch(getDetails, newData => {
   if (newData) {
-    collapsedHindle()
+    collapsedHandle()
   } else {
-    expandHindle()
+    expandHandle()
   }
 })
 
 // 页面设置
 const globalTabList = [
   {
-    key: 'pageSetting',
+    key: TabsEnum.PAGE_SETTING,
     title: '页面配置',
     icon: DesktopOutlineIcon,
     render: CanvasPage
   }
 ]
 
-const canvasTabList = [
+const chartsDefaultTabList = [
   {
-    key: 'ChartSetting',
+    key: TabsEnum.CHART_SETTING,
     title: '定制',
     icon: ConstructIcon,
     render: ChartSetting
   },
   {
-    key: 'ChartAnimation',
+    key: TabsEnum.CHART_ANIMATION,
     title: '动画',
     icon: LeafIcon,
     render: ChartAnimation
-  },
+  }
+]
+
+const chartsTabList = [
+  ...chartsDefaultTabList,
   {
-    key: 'ChartData',
+    key: TabsEnum.CHART_DATA,
     title: '数据',
     icon: FlashIcon,
     render: ChartData

+ 8 - 29
src/views/chart/ContentEdit/components/EditAlignLine/index.vue

@@ -11,17 +11,16 @@
 </template>
 
 <script setup lang="ts">
-import { ref, reactive, computed, watch } from 'vue'
+import { reactive, computed, watch } from 'vue'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { EditCanvasTypeEnum } from '@/store/modules/chartEditStore/chartEditStore.d'
 import { useSettingStore } from '@/store/modules/settingStore/settingStore'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import throttle from 'lodash/throttle'
 import cloneDeep from 'lodash/cloneDeep'
 // 全局颜色
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
 const chartEditStore = useChartEditStore()
 const settingStore = useSettingStore()
@@ -49,6 +48,11 @@ const useComponentStyle = (attr?: Partial<{ x: number; y: number }>) => {
   return componentStyle
 }
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 // * 吸附距离
 const minDistance = computed(() => {
   return settingStore.getChartAlignRange
@@ -111,7 +115,7 @@ watch(
     line.select.clear()
     line.sorptioned.y = false
     // 循环查询所有组件数据
-    const componentList = chartEditStore.getComponentList.map((e: CreateComponentType) => {
+    const componentList = chartEditStore.getComponentList.map((e: CreateComponentType | CreateComponentGroupType) => {
       return {
         id: e.id,
         attr: e.attr
@@ -228,30 +232,6 @@ watch(
             selectTarget.value.setPosition(componentRightX - selectW, selectTopY)
           }
         }
-
-        /*
-          * 我也不知道为什么这个不行,还没时间调
-          if(lineItem.includes('row')) {
-            selectY.forEach(sY => {
-              componentY.forEach(cY => {
-                if (isSorption(sY, cY)) {
-                  line.select.set(lineItem, { y: cY })
-                }
-              })
-            })
-            return
-          }
-          if(lineItem.includes('col')) {
-            seletX.forEach(sX => {
-              componentX.forEach(cX => {
-                if (isSorption(sX, cX)) {
-                  line.select.set(lineItem, { x: sX })
-                }
-              })
-            })
-            return
-          }
-        */
       })
     })
   }, 200),
@@ -267,7 +247,6 @@ watch(
     if (!val) {
       line.select.clear()
       line.sorptioned.y = false
-      chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, true)
     }
   }
 )

+ 3 - 0
src/views/chart/ContentEdit/components/EditGroup/index.ts

@@ -0,0 +1,3 @@
+import EditGroup from './index.vue'
+
+export { EditGroup }

+ 122 - 0
src/views/chart/ContentEdit/components/EditGroup/index.vue

@@ -0,0 +1,122 @@
+<template>
+  <div class="go-edit-group-box">
+    <edit-shape-box
+      :key="groupData.id"
+      :data-id="groupData.id"
+      :index="groupIndex"
+      :item="groupData"
+      :hiddenPoint="true"
+      :class="animationsClass(groupData.styles.animations)"
+      :style="{
+        ...useComponentStyle(groupData.attr, groupIndex),
+        ...useSizeStyle(groupData.attr),
+        ...getFilterStyle(groupData.styles),
+        ...getTransformStyle(groupData.styles)
+      }"
+      @click="mouseClickHandle($event, groupData)"
+      @mousedown="mousedownHandle($event, groupData)"
+      @mouseenter="mouseenterHandle($event, groupData)"
+      @mouseleave="mouseleaveHandle($event, groupData)"
+      @contextmenu="handleContextMenu($event, groupData, optionsHandle)"
+    >
+      <!-- 组合组件 -->
+      <edit-shape-box
+        v-for="item in groupData.groupList"
+        :key="item.id"
+        :data-id="item.id"
+        :index="groupIndex"
+        :item="item"
+        :hiddenPoint="true"
+        :style="{
+          ...useComponentStyle(item.attr, groupIndex)
+        }"
+      >
+        <component
+          class="edit-content-chart"
+          :class="animationsClass(item.styles.animations)"
+          :is="item.chartConfig.chartKey"
+          :chartConfig="item"
+          :themeSetting="themeSetting"
+          :themeColor="themeColor"
+          :style="{
+            ...useSizeStyle(item.attr),
+            ...getFilterStyle(item.styles),
+            ...getTransformStyle(item.styles)
+          }"
+        ></component>
+      </edit-shape-box>
+    </edit-shape-box>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { computed, PropType } from 'vue'
+import { MenuEnum } from '@/enums/editPageEnum'
+import { chartColors } from '@/settings/chartThemes/index'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
+import { MenuOptionsItemType } from '@/views/chart/hooks/useContextMenu.hook.d'
+import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
+import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { useContextMenu, divider } from '@/views/chart/hooks/useContextMenu.hook'
+import { useMouseHandle } from '../../hooks/useDrag.hook'
+import { useComponentStyle, useSizeStyle } from '../../hooks/useStyle.hook'
+import { EditShapeBox } from '../../components/EditShapeBox'
+
+const props = defineProps({
+  groupData: {
+    type: Object as PropType<CreateComponentGroupType>,
+    required: true
+  },
+  groupIndex: {
+    type: Number,
+    required: true
+  }
+})
+
+const chartEditStore = useChartEditStore()
+const { handleContextMenu } = useContextMenu()
+
+// 点击事件
+const { mouseenterHandle, mouseleaveHandle, mousedownHandle, mouseClickHandle } = useMouseHandle()
+
+// 右键
+const optionsHandle = (
+  targetList: MenuOptionsItemType[],
+  allList: MenuOptionsItemType[],
+  targetInstance: CreateComponentType
+) => {
+  // 多选
+  const moreMenuEnums = [MenuEnum.GROUP, MenuEnum.DELETE]
+  // 单选
+  const singleMenuEnums = [MenuEnum.UN_GROUP]
+
+  const filter = (menulist: MenuEnum[]) => {
+    const list: MenuOptionsItemType[] = []
+    allList.forEach(item => {
+      if (menulist.includes(item.key as MenuEnum)) {
+        list.push(item)
+      }
+    })
+    return list
+  }
+
+  // 多选处理
+  if (chartEditStore.getTargetChart.selectId.length > 1) {
+    return filter(moreMenuEnums)
+  } else {
+    return [...filter(singleMenuEnums), divider(), ...targetList]
+  }
+}
+
+// 配置项
+const themeColor = computed(() => {
+  const chartThemeColor = chartEditStore.getEditCanvasConfig.chartThemeColor
+  return chartColors[chartThemeColor]
+})
+
+// 主题色
+const themeSetting = computed(() => {
+  const chartThemeSetting = chartEditStore.getEditCanvasConfig.chartThemeSetting
+  return chartThemeSetting
+})
+</script>

+ 22 - 46
src/views/chart/ContentEdit/components/EditHistory/index.vue

@@ -1,21 +1,8 @@
 <template>
   <div class="go-flex-items-center">
-    <n-popover
-      class="edit-history-popover"
-      :show="showDropdownRef"
-      :show-arrow="false"
-      size="small"
-      trigger="click"
-      placement="top-start"
-    >
+    <n-popover class="edit-history-popover" :show-arrow="false" size="small" trigger="click" placement="top-start">
       <template #trigger>
-        <n-button
-          class="go-mr-1"
-          secondary
-          size="small"
-          :disabled="options.length === 0"
-          @click="handleClick"
-        >
+        <n-button class="mr-10" secondary size="small" :disabled="options.length === 0">
           <span class="btn-text">历史记录</span>
         </n-button>
       </template>
@@ -24,16 +11,11 @@
         <n-scrollbar style="max-height: 500px">
           <div
             class="list-item go-flex-items-center go-ellipsis-1"
-            v-for="item in options"
-            :key="item.key"
+            v-for="(item, index) in options"
+            :key="index"
             :title="item.label"
           >
-            <n-icon
-              class="item-icon"
-              size="16"
-              :depth="2"
-              :component="item.icon"
-            />
+            <n-icon class="item-icon" size="16" :depth="2" :component="item.icon" />
             <n-text depth="2">{{ item.label }}</n-text>
           </div>
         </n-scrollbar>
@@ -55,7 +37,6 @@
 <script setup lang="ts">
 import { ref, computed } from 'vue'
 import { icon } from '@/plugins'
-import { renderIcon } from '@/utils'
 import { useChartHistoryStore } from '@/store/modules/chartHistoryStore/chartHistoryStore'
 import { historyActionTypeName } from '@/store/modules/chartHistoryStore/chartHistoryDefine'
 import { CreateComponentType } from '@/packages/index.d'
@@ -64,20 +45,12 @@ import reverse from 'lodash/reverse'
 import {
   HistoryItemType,
   HistoryTargetTypeEnum,
-  HistoryActionTypeEnum,
+  HistoryActionTypeEnum
 } from '@/store/modules/chartHistoryStore/chartHistoryStore.d'
 
-const {
-  DesktopOutlineIcon,
-  PencilIcon,
-  TrashIcon,
-  CopyIcon,
-  LayersIcon,
-  DuplicateIcon,
-  HelpOutlineIcon,
-} = icon.ionicons5
-const { StackedMoveIcon } = icon.carbon
-const showDropdownRef = ref(false)
+const { DesktopOutlineIcon, PencilIcon, TrashIcon, CopyIcon, LayersIcon, DuplicateIcon, HelpOutlineIcon } =
+  icon.ionicons5
+const { StackedMoveIcon, Carbon3DCursorIcon, Carbon3DSoftwareIcon } = icon.carbon
 
 const chartHistoryStoreStore = useChartHistoryStore()
 
@@ -106,6 +79,10 @@ const iconHandle = (e: HistoryItemType) => {
       return StackedMoveIcon
     case HistoryActionTypeEnum.ADD:
       return DuplicateIcon
+    case HistoryActionTypeEnum.GROUP:
+      return Carbon3DCursorIcon
+    case HistoryActionTypeEnum.UN_GROUP:
+      return Carbon3DSoftwareIcon
     default:
       return PencilIcon
   }
@@ -116,10 +93,11 @@ const labelHandle = (e: HistoryItemType) => {
   // 画布编辑
   if (e.targetType === HistoryTargetTypeEnum.CANVAS) {
     return historyActionTypeName[HistoryTargetTypeEnum.CANVAS]
+  } else if (e.actionType === HistoryActionTypeEnum.GROUP || e.actionType === HistoryActionTypeEnum.UN_GROUP) {
+    return `${historyActionTypeName[e.actionType]}`
+  } else if (e.historyData.length) {
+    return `${historyActionTypeName[e.actionType]} - ${(e.historyData[0] as CreateComponentType).chartConfig.title}`
   }
-  return `${historyActionTypeName[e.actionType]} - ${
-    (e.historyData as CreateComponentType).chartConfig.title
-  }`
 }
 
 const options = computed(() => {
@@ -127,16 +105,14 @@ const options = computed(() => {
   const options = backStack.map((e: HistoryItemType) => {
     return {
       label: labelHandle(e),
-      key: e.id,
-      icon: iconHandle(e),
+      icon: iconHandle(e)
     }
   })
-  return reverse(options)
-})
 
-const handleClick = () => {
-  showDropdownRef.value = !showDropdownRef.value
-}
+  return reverse(options.filter(item => {
+    return item.label
+  }))
+})
 </script>
 
 <style lang="scss" scoped>

+ 6 - 2
src/views/chart/ContentEdit/components/EditRange/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="go-edit-range go-transition" :style="rangeStyle" @mousedown="mousedownHandleUnStop($event, undefined)">
+  <div class="go-edit-range go-transition" :style="rangeStyle" @mousedown="mousedownBoxSelect($event, undefined)">
     <slot></slot>
     <!-- 水印 -->
     <edit-watermark></edit-watermark>
@@ -7,6 +7,8 @@
     <edit-rule></edit-rule>
     <!-- 拖拽时的辅助线 -->
     <edit-align-line></edit-align-line>
+    <!-- 框选时的样式框 -->
+    <edit-select></edit-select>
     <!-- 拖拽时的遮罩 -->
     <div class="go-edit-range-model" :style="rangeModelStyle"></div>
   </div>
@@ -15,11 +17,13 @@
 <script setup lang="ts">
 import { toRefs, computed } from 'vue'
 import { useSizeStyle } from '../../hooks/useStyle.hook'
-import { mousedownHandleUnStop } from '../../hooks/useDrag.hook'
+import { canvasModelIndex } from '@/settings/designSetting'
+import { mousedownBoxSelect } from '../../hooks/useDrag.hook'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { EditAlignLine } from '../EditAlignLine'
 import { EditWatermark } from '../EditWatermark'
 import { EditRule } from '../EditRule'
+import { EditSelect } from '../EditSelect'
 
 const chartEditStore = useChartEditStore()
 

+ 7 - 2
src/views/chart/ContentEdit/components/EditRule/index.vue

@@ -11,13 +11,12 @@
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs } from 'vue'
+import { toRefs, computed } from 'vue'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
 
 const chartEditStore = useChartEditStore()
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 
 const { width, height } = toRefs(chartEditStore.getEditCanvasConfig)
 
@@ -34,6 +33,12 @@ const lines = {
   h: [],
   v: []
 }
+
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 </script>
 
 <style>

+ 3 - 0
src/views/chart/ContentEdit/components/EditSelect/index.ts

@@ -0,0 +1,3 @@
+import EditSelect from './index.vue'
+
+export { EditSelect }

+ 111 - 0
src/views/chart/ContentEdit/components/EditSelect/index.vue

@@ -0,0 +1,111 @@
+<template>
+  <div class="go-edit-select" v-if="isSelect" :style="positionStyle">
+    <div class="select-background"></div>
+    <div class="select-border"></div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, toRefs, watch, computed } from 'vue'
+import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { useDesignStore } from '@/store/modules/designStore/designStore'
+import { useSizeStyle, useComponentStyle } from '../../hooks/useStyle.hook'
+import { selectBoxIndex } from '@/settings/designSetting'
+
+// 全局颜色
+const designStore = useDesignStore()
+const chartEditStore = useChartEditStore()
+const { isSelect, scale } = toRefs(chartEditStore.getEditCanvas)
+
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
+// 位置
+const positionStyle = ref()
+
+watch(
+  () => chartEditStore.getMousePosition,
+  positionInfo => {
+    if (!isSelect.value) return
+
+    // 这里的 x,y 是已经计算过的相对位移值
+    const { startX, startY, x, y } = positionInfo
+
+    const attr = {
+      zIndex: selectBoxIndex,
+      // left
+      x: 0,
+      // top
+      y: 0,
+      // 宽
+      w: 0,
+      // 高
+      h: 0
+    }
+
+    // 处理位置
+    if (x > startX && y > startY) {
+      // 右下方向
+      attr.x = startX
+      attr.y = startY
+      attr.w = Math.round((x - startX) / scale.value)
+      attr.h = Math.round((y - startY) / scale.value)
+    } else if (x > startX && y < startY) {
+      // 右上方向
+      attr.x = startX
+      attr.w = Math.round((x - startX) / scale.value)
+      attr.h = Math.round((startY - y) / scale.value)
+      attr.y = startY - attr.h
+    } else if (x < startX && y > startY) {
+      // 左下方向
+      attr.y = startY
+      attr.w = Math.round((startX - x) / scale.value)
+      attr.h = Math.round((y - startY) / scale.value)
+      attr.x = startX - attr.w
+    } else {
+      // 左上方向
+      attr.w = Math.round((startX - x) / scale.value)
+      attr.h = Math.round((startY - y) / scale.value)
+      attr.x = startX - attr.w
+      attr.y = startY - attr.h
+    }
+
+    positionStyle.value = {
+      ...useComponentStyle(attr, selectBoxIndex),
+      ...useSizeStyle(attr)
+    }
+  },
+  {
+    deep: true
+  }
+)
+</script>
+
+<style lang="scss" scoped>
+@include go('edit-select') {
+  position: absolute;
+  .select-border,
+  .select-background {
+    position: absolute;
+    width: 100%;
+    height: 100%;
+    border-radius: 10px;
+    overflow: hidden;
+  }
+  .select-border {
+    left: 0;
+    top: 0;
+    opacity: 0.5;
+    border-width: 2px;
+    border-style: solid;
+    border-color: v-bind('themeColor');
+  }
+  .select-background {
+    top: 2px;
+    left: 2px;
+    opacity: 0.03;
+    background-color: v-bind('themeColor');
+  }
+}
+</style>

+ 21 - 12
src/views/chart/ContentEdit/components/EditShapeBox/index.vue

@@ -2,13 +2,15 @@
   <div class="go-shape-box">
     <slot></slot>
     <!-- 锚点 -->
-    <div
-      :class="`shape-point ${point}`"
-      v-for="(point, index) in select ? pointList : []"
-      :key="index"
-      :style="usePointStyle(point, index, item.attr, cursorResize)"
-      @mousedown="useMousePointHandle($event, point, item.attr)"
-    ></div>
+    <template v-if="!hiddenPoint">
+      <div
+        :class="`shape-point ${point}`"
+        v-for="(point, index) in select ? pointList : []"
+        :key="index"
+        :style="usePointStyle(point, index, item.attr, cursorResize)"
+        @mousedown="useMousePointHandle($event, point, item.attr)"
+      ></div>
+    </template>
 
     <!-- 选中 -->
     <div class="shape-modal" :style="useSizeStyle(item.attr)">
@@ -19,23 +21,25 @@
 </template>
 
 <script setup lang="ts">
-import { ref, computed, PropType, toRefs } from 'vue'
+import { computed, PropType } from 'vue'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { useSizeStyle, usePointStyle } from '../../hooks/useStyle.hook'
 import { useMousePointHandle } from '../../hooks/useDrag.hook'
 
 const props = defineProps({
   item: {
-    type: Object as PropType<CreateComponentType>,
+    type: Object as PropType<CreateComponentType | CreateComponentGroupType>,
     required: true
+  },
+  hiddenPoint: {
+    type: Boolean,
+    required: false
   }
 })
 
-// 全局颜色
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 const chartEditStore = useChartEditStore()
 
 // 锚点
@@ -44,6 +48,11 @@ const pointList = ['t', 'r', 'b', 'l', 'lt', 'rt', 'lb', 'rb']
 // 光标朝向
 const cursorResize = ['n', 'e', 's', 'w', 'nw', 'ne', 'sw', 'se']
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 // 计算当前选中目标
 const hover = computed(() => {
   return props.item.id === chartEditStore.getTargetChart.hoverId

+ 29 - 20
src/views/chart/ContentEdit/components/EditShortcutKey/ShortcutKeyModal.vue

@@ -1,9 +1,5 @@
 <template>
-  <n-modal
-    v-model:show="modelShow"
-    :mask-closable="true"
-    @afterLeave="closeHandle"
-  >
+  <n-modal v-model:show="modelShow" :mask-closable="true" @afterLeave="closeHandle">
     <n-table class="model-content" :bordered="false" :single-line="false">
       <thead>
         <tr>
@@ -24,9 +20,7 @@
           <td>{{ item.label }}</td>
           <td>{{ item.win }}</td>
           <td>
-            <n-gradient-text :size="22">{{
-              item.mac.substr(0, 1)
-            }}</n-gradient-text>
+            <n-gradient-text :size="22">{{ item.mac.substr(0, 1) }}</n-gradient-text>
             + {{ item.mac.substr(3) }}
           </td>
         </tr>
@@ -44,7 +38,7 @@ const { CloseIcon } = icon.ionicons5
 const emit = defineEmits(['update:modelShow'])
 
 defineProps({
-  modelShow: Boolean,
+  modelShow: Boolean
 })
 
 // 快捷键
@@ -52,58 +46,73 @@ const shortcutKeyOptions = [
   {
     label: '向上移动',
     win: `${WinKeyboard.CTRL.toUpperCase()} + ↑ `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + ↑ `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + ↑ `
   },
   {
     label: '向右移动',
     win: `${WinKeyboard.CTRL.toUpperCase()} + → `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + → `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + → `
   },
   {
     label: '向下移动',
     win: `${WinKeyboard.CTRL.toUpperCase()} + ↓ `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + ↓ `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + ↓ `
   },
   {
     label: '向左移动',
     win: `${WinKeyboard.CTRL.toUpperCase()} + ← `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + ← `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + ← `
   },
   {
     label: '删除',
     win: 'Delete'.toUpperCase(),
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + Backspace `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + Backspace `
   },
   {
     label: '复制',
     win: `${WinKeyboard.CTRL.toUpperCase()} + C `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + C `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + C `
   },
   {
     label: '剪切',
     win: `${WinKeyboard.CTRL.toUpperCase()} + X `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + X `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + X `
   },
   {
     label: '粘贴',
     win: `${WinKeyboard.CTRL.toUpperCase()} + V `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + V `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + V `
   },
   {
     label: '后退',
     win: `${WinKeyboard.CTRL.toUpperCase()} + Z `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + Z `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + Z `
   },
   {
     label: '前进',
     win: `${WinKeyboard.CTRL.toUpperCase()} + ${WinKeyboard.SHIFT.toUpperCase()} + Z `,
-    mac: `${MacKeyboard.CTRL.toUpperCase()} + ${MacKeyboard.SHIFT.toUpperCase()} + Z `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + ${MacKeyboard.SHIFT.toUpperCase()} + Z `
   },
   {
     label: '保存',
     win: `${WinKeyboard.CTRL.toUpperCase()} + S `,
     mac: `${MacKeyboard.CTRL.toUpperCase()} + S `,
   },
+  {
+    label: '多选',
+    win: `${WinKeyboard.CTRL.toUpperCase()} + 🖱️ `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + 🖱️ `
+  },
+  {
+    label: '创建分组',
+    win: `${WinKeyboard.CTRL.toUpperCase()} + G / 🖱️ `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + G / 🖱️`
+  },
+  {
+    label: '解除分组',
+    win: `${WinKeyboard.CTRL.toUpperCase()} + ${WinKeyboard.SHIFT.toUpperCase()} + G `,
+    mac: `${MacKeyboard.CTRL.toUpperCase()} + ${WinKeyboard.SHIFT.toUpperCase()} + G `
+  }
 ]
 const closeHandle = () => {
   emit('update:modelShow', false)
@@ -120,4 +129,4 @@ const closeHandle = () => {
     padding: 5px 10px;
   }
 }
-</style>
+</style>

+ 4 - 4
src/views/chart/ContentEdit/components/EditTools/hooks/useFile.hooks.ts

@@ -31,15 +31,15 @@ export const useFile = () => {
             negativeText: '覆盖(不可撤回)',
             negativeButtonProps: { type: 'info', ghost: false },
             // 新增
-            onNegativeCallback: async () => {
+            onPositiveCallback: async () => {
               fileData = JSON.parse(fileData)
-              await updateComponent(fileData, true)
+              await updateComponent(fileData, false, true)
               window['$message'].success('导入成功!')
             },
             // 覆盖
-            onPositiveCallback: async () => {
+            onNegativeCallback: async () => {
               fileData = JSON.parse(fileData)
-              await updateComponent(fileData)
+              await updateComponent(fileData, true, true)
               window['$message'].success('导入成功!')
             }
           })

+ 196 - 49
src/views/chart/ContentEdit/hooks/useDrag.hook.ts

@@ -1,10 +1,8 @@
-import { DragKeyEnum } from '@/enums/editPageEnum'
+import { toRaw } from 'vue'
+import { DragKeyEnum, MouseEventButton, WinKeyboard, MacKeyboard } from '@/enums/editPageEnum'
 import { createComponent } from '@/packages'
 import { ConfigType } from '@/packages/index.d'
-import {
-  CreateComponentType,
-  PickCreateComponentType,
-} from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType, PickCreateComponentType } from '@/packages/index.d'
 import { useContextMenu } from '@/views/chart/hooks/useContextMenu.hook'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 import { EditCanvasTypeEnum } from '@/store/modules/chartEditStore/chartEditStore.d'
@@ -22,7 +20,7 @@ export const dragHandle = async (e: DragEvent) => {
     loadingStart()
 
     // 获取拖拽数据
-    const drayDataString = e!.dataTransfer!.getData(DragKeyEnum.DROG_KEY)
+    const drayDataString = e!.dataTransfer!.getData(DragKeyEnum.DRAG_KEY)
     if (!drayDataString) {
       loadingFinish()
       return
@@ -35,10 +33,7 @@ export const dragHandle = async (e: DragEvent) => {
     // 创建新图表组件
     let newComponent: CreateComponentType = await createComponent(dropData)
 
-    newComponent.setPosition(
-      e.offsetX - newComponent.attr.w / 2,
-      e.offsetY - newComponent.attr.h / 2
-    )
+    newComponent.setPosition(e.offsetX - newComponent.attr.w / 2, e.offsetY - newComponent.attr.h / 2)
     chartEditStore.addComponentList(newComponent, false, true)
     chartEditStore.setTargetSelectChart(newComponent.id)
     loadingFinish()
@@ -57,10 +52,7 @@ export const dragoverHandle = (e: DragEvent) => {
 }
 
 // * 不拦截默认行为点击
-export const mousedownHandleUnStop = (
-  e: MouseEvent,
-  item?: CreateComponentType
-) => {
+export const mousedownHandleUnStop = (e: MouseEvent, item?: CreateComponentType | CreateComponentGroupType) => {
   if (item) {
     chartEditStore.setTargetSelectChart(item.id)
     return
@@ -68,56 +60,213 @@ export const mousedownHandleUnStop = (
   chartEditStore.setTargetSelectChart(undefined)
 }
 
-// * 移动图表
+// * 框选
+export const mousedownBoxSelect = (e: MouseEvent, item?: CreateComponentType | CreateComponentGroupType) => {
+  mousedownHandleUnStop(e)
+
+  // 记录点击初始位置
+  const startOffsetX = e.offsetX
+  const startOffsetY = e.offsetY
+  const startScreenX = e.screenX
+  const startScreenY = e.screenY
+
+  // 记录缩放
+  const scale = chartEditStore.getEditCanvas.scale
+
+  chartEditStore.setMousePosition(undefined, undefined, startOffsetX, startOffsetY)
+
+  // 移动框选
+  const mousemove = throttle((moveEvent: MouseEvent) => {
+    // 取消当前选中
+    chartEditStore.setTargetSelectChart()
+    chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_SELECT, true)
+
+    // 这里先把相对值算好,不然组件无法获取 startScreenX 和 startScreenY 的值
+    const currX = startOffsetX + moveEvent.screenX - startScreenX
+    const currY = startOffsetY + moveEvent.screenY - startScreenY
+    chartEditStore.setMousePosition(currX, currY)
+
+    // 计算框选的左上角和右下角
+    const selectAttr = {
+      // 左上角
+      x1: 0,
+      y1: 0,
+      // 右下角
+      x2: 0,
+      y2: 0
+    }
+    if (currX > startOffsetX && currY > startOffsetY) {
+      // 右下方向
+      selectAttr.x1 = startOffsetX
+      selectAttr.y1 = startOffsetY
+      selectAttr.x2 = Math.round(startOffsetX + (moveEvent.screenX - startScreenX) / scale)
+      selectAttr.y2 = Math.round(startOffsetY + (moveEvent.screenY - startScreenY) / scale)
+    } else if (currX > startOffsetX && currY < startOffsetY) {
+      // 右上方向
+      selectAttr.x1 = startOffsetX
+      selectAttr.y1 = Math.round(startOffsetY - (startScreenY - moveEvent.screenY) / scale)
+      selectAttr.x2 = Math.round(startOffsetX + (moveEvent.screenX - startScreenX) / scale)
+      selectAttr.y2 = startOffsetY
+    } else if (currX < startOffsetX && currY > startOffsetY) {
+      selectAttr.x1 = Math.round(startOffsetX - (startScreenX - moveEvent.screenX) / scale)
+      selectAttr.y1 = startOffsetY
+      selectAttr.x2 = startOffsetX
+      selectAttr.y2 = Math.round(startOffsetY + (moveEvent.screenY - startScreenY ) / scale)
+      // 左下方向
+    } else {
+      // 左上方向
+      selectAttr.x1 = Math.round(startOffsetX - (startScreenX - moveEvent.screenX) / scale)
+      selectAttr.y1 = Math.round(startOffsetY - (startScreenY - moveEvent.screenY) / scale)
+      selectAttr.x2 = startOffsetX
+      selectAttr.y2 = startOffsetY
+    }
+
+    // 遍历组件
+    chartEditStore.getComponentList.forEach(item => {
+      if (!chartEditStore.getTargetChart.selectId.includes(item.id)) {
+        // 处理左上角
+        let isSelect = false
+        const { x, y, w, h } = item.attr
+        const targetAttr = {
+          // 左上角
+          x1: x,
+          y1: y,
+          // 右下角
+          x2: x + w,
+          y2: y + h
+        }
+        // 全包含则选中
+        if (
+          targetAttr.x1 - selectAttr.x1 >= 0 &&
+          targetAttr.y1 - selectAttr.y1 >= 0 &&
+          targetAttr.x2 - selectAttr.x2 <= 0 &&
+          targetAttr.y2 - selectAttr.y2 <= 0
+        ) {
+          isSelect = true
+          chartEditStore.setTargetSelectChart(item.id, true)
+        }
+      }
+    })
+  }, 20)
+
+  // 鼠标抬起
+  const mouseup = () => {
+    chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_SELECT, false)
+    chartEditStore.setMousePosition(0, 0, 0, 0)
+    document.removeEventListener('mousemove', mousemove)
+    document.removeEventListener('mouseup', mouseup)
+  }
+  document.addEventListener('mousemove', mousemove)
+  document.addEventListener('mouseup', mouseup)
+}
+
+// * 鼠标事件
 export const useMouseHandle = () => {
-  // 点击事件(包含移动事件)
-  const mousedownHandle = (e: MouseEvent, item: CreateComponentType) => {
+  // *  Click 事件, 松开鼠标触发
+  const mouseClickHandle = (e: MouseEvent, item: CreateComponentType | CreateComponentGroupType) => {
     e.preventDefault()
     e.stopPropagation()
+    // 若此时按下了 CTRL, 表示多选
+    if (
+      window.$KeyboardActive?.has(WinKeyboard.CTRL_SOURCE_KEY) ||
+      window.$KeyboardActive?.has(MacKeyboard.CTRL_SOURCE_KEY)
+    ) {
+      // 若已选中,则去除
+      if (chartEditStore.targetChart.selectId.includes(item.id)) {
+        const exList = chartEditStore.targetChart.selectId.filter(e => e !== item.id)
+        chartEditStore.setTargetSelectChart(exList)
+      } else {
+        chartEditStore.setTargetSelectChart(item.id, true)
+      }
+    }
+  }
 
+  // * 按下事件(包含移动事件)
+  const mousedownHandle = (e: MouseEvent, item: CreateComponentType | CreateComponentGroupType) => {
+    e.preventDefault()
+    e.stopPropagation()
     onClickOutSide()
+    // 按下左键 + CTRL
+    if (
+      e.buttons === MouseEventButton.LEFT &&
+      (window.$KeyboardActive?.has(WinKeyboard.CTRL_SOURCE_KEY) ||
+        window.$KeyboardActive?.has(MacKeyboard.CTRL_SOURCE_KEY))
+    )
+      return
+
+    // 按下右键 + 选中多个 + 目标元素是多选子元素
+    const selectId = chartEditStore.getTargetChart.selectId
+    if (e.buttons === MouseEventButton.RIGHT && selectId.length > 1 && selectId.includes(item.id)) return
+
+    // 选中当前目标组件
     chartEditStore.setTargetSelectChart(item.id)
+
+    // 按下右键
+    if (e.buttons === MouseEventButton.RIGHT) return
+
     const scale = chartEditStore.getEditCanvas.scale
-    const width = chartEditStore.getEditCanvasConfig.width
-    const height = chartEditStore.getEditCanvasConfig.height
+    const canvasWidth = chartEditStore.getEditCanvasConfig.width
+    const canvasHeight = chartEditStore.getEditCanvasConfig.height
 
     // 记录图表初始位置和大小
-    const itemAttrX = item.attr.x
-    const itemAttrY = item.attr.y
-    const itemAttrW = item.attr.w
-    const itemAttrH = item.attr.h
+    const targetMap = new Map()
+    chartEditStore.getTargetChart.selectId.forEach(id => {
+      const index = chartEditStore.fetchTargetIndex(id)
+      if (index !== -1) {
+        const { x, y, w, h } = toRaw(chartEditStore.getComponentList[index]).attr
+        targetMap.set(id, { x, y, w, h })
+      }
+    })
 
     // 记录点击初始位置
     const startX = e.screenX
     const startY = e.screenY
+
     // 记录初始位置
-    chartEditStore.setMousePosition(startX, startY)
+    chartEditStore.setMousePosition(undefined, undefined, startX, startY)
 
-    // 计算偏移量(处理 scale 比例问题)
+    // 移动-计算偏移量
     const mousemove = throttle((moveEvent: MouseEvent) => {
       chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, true)
       chartEditStore.setMousePosition(moveEvent.screenX, moveEvent.screenY)
 
-      let currX = Math.round(itemAttrX + (moveEvent.screenX - startX) / scale)
-      let currY = Math.round(itemAttrY + (moveEvent.screenY - startY) / scale)
+      // 当前偏移量,处理 scale 比例问题
+      let offsetX = (moveEvent.screenX - startX) / scale
+      let offsetY = (moveEvent.screenY - startY) / scale
+
+      chartEditStore.getTargetChart.selectId.forEach(id => {
+        if (!targetMap.has(id)) return
+
+        const index = chartEditStore.fetchTargetIndex(id)
+        // 拿到初始位置数据
+        const { x, y, w, h } = targetMap.get(id)
+        const componentInstance = chartEditStore.getComponentList[index]
 
-      // 要预留的距离
-      const distance = 50
-      // 基于左上角位置检测
-      currX = currX < -itemAttrW + distance ? -itemAttrW + distance : currX
-      currY = currY < -itemAttrH + distance ? -itemAttrH + distance : currY
+        let currX = Math.round(x + offsetX)
+        let currY = Math.round(y + offsetY)
 
-      // 基于右下角位置检测
-      currX = currX > width - distance ? width - distance : currX
-      currY = currY > height - distance ? height - distance : currY
+        // 要预留的距离
+        const distance = 50
 
-      item.attr.x = currX
-      item.attr.y = currY
-    }, 30)
+        // 基于左上角位置检测
+        currX = currX < -w + distance ? -w + distance : currX
+        currY = currY < -h + distance ? -h + distance : currY
+
+        // 基于右下角位置检测
+        currX = currX > canvasWidth - distance ? canvasWidth - distance : currX
+        currY = currY > canvasHeight - distance ? canvasHeight - distance : currY
+
+        componentInstance.attr = Object.assign(componentInstance.attr, {
+          x: currX,
+          y: currY
+        })
+      })
+      return
+    }, 20)
 
     const mouseup = () => {
+      chartEditStore.setMousePosition(0, 0, 0, 0)
       chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, false)
-      chartEditStore.setMousePosition(0, 0)
       document.removeEventListener('mousemove', mousemove)
       document.removeEventListener('mouseup', mouseup)
     }
@@ -127,29 +276,27 @@ export const useMouseHandle = () => {
   }
 
   // * 进入事件
-  const mouseenterHandle = (e: MouseEvent, item: CreateComponentType) => {
+  const mouseenterHandle = (e: MouseEvent, item: CreateComponentType | CreateComponentGroupType) => {
     e.preventDefault()
     e.stopPropagation()
-    chartEditStore.setTargetHoverChart(item.id)
+    if (!chartEditStore.getEditCanvas.isSelect) {
+      chartEditStore.setTargetHoverChart(item.id)
+    }
   }
 
   // * 移出事件
-  const mouseleaveHandle = (e: MouseEvent, item: CreateComponentType) => {
+  const mouseleaveHandle = (e: MouseEvent, item: CreateComponentType | CreateComponentGroupType) => {
     e.preventDefault()
     e.stopPropagation()
     chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, false)
     chartEditStore.setTargetHoverChart(undefined)
   }
 
-  return { mousedownHandle, mouseenterHandle, mouseleaveHandle }
+  return { mouseClickHandle, mousedownHandle, mouseenterHandle, mouseleaveHandle }
 }
 
 // * 移动锚点
-export const useMousePointHandle = (
-  e: MouseEvent,
-  point: string,
-  attr: PickCreateComponentType<'attr'>
-) => {
+export const useMousePointHandle = (e: MouseEvent, point: string, attr: PickCreateComponentType<'attr'>) => {
   e.stopPropagation()
   e.preventDefault()
 
@@ -191,7 +338,7 @@ export const useMousePointHandle = (
 
   const mouseup = () => {
     chartEditStore.setEditCanvas(EditCanvasTypeEnum.IS_DRAG, false)
-    chartEditStore.setMousePosition(0, 0)
+    chartEditStore.setMousePosition(0, 0, 0, 0)
     document.removeEventListener('mousemove', mousemove)
     document.removeEventListener('mouseup', mouseup)
   }

+ 67 - 29
src/views/chart/ContentEdit/index.vue

@@ -15,37 +15,45 @@
       <!-- 展示 -->
       <edit-range>
         <!-- 滤镜预览 -->
-        <div :style="{
+        <div
+          :style="{
             ...getFilterStyle(chartEditStore.getEditCanvasConfig),
             ...rangeStyle
-          }">
+          }"
+        >
           <!-- 图表 -->
-          <edit-shape-box
-            v-for="(item, index) in chartEditStore.getComponentList"
-            :key="item.id"
-            :data-id="item.id"
-            :index="index"
-            :style="useComponentStyle(item.attr, index)"
-            :item="item"
-            @mousedown="mousedownHandle($event, item)"
-            @mouseenter="mouseenterHandle($event, item)"
-            @mouseleave="mouseleaveHandle($event, item)"
-            @contextmenu="handleContextMenu($event, item)"
-          >
-            <component
-              class="edit-content-chart"
-              :class="animationsClass(item.styles.animations)"
-              :is="item.chartConfig.chartKey"
-              :chartConfig="item"
-              :themeSetting="themeSetting"
-              :themeColor="themeColor"
-              :style="{
-                ...useSizeStyle(item.attr),
-                ...getFilterStyle(item.styles),
-                ...getTransformStyle(item.styles),
-              }"
-            ></component>
-          </edit-shape-box>
+          <div v-for="(item, index) in chartEditStore.getComponentList" :key="item.id">
+            <!-- 分组 -->
+            <edit-group v-if="item.isGroup" :groupData="(item as CreateComponentGroupType)" :groupIndex="index"></edit-group>
+            
+            <!-- 单组件 -->
+            <edit-shape-box
+              v-else
+              :data-id="item.id"
+              :index="index"
+              :style="useComponentStyle(item.attr, index)"
+              :item="item"
+              @click="mouseClickHandle($event, item)"
+              @mousedown="mousedownHandle($event, item)"
+              @mouseenter="mouseenterHandle($event, item)"
+              @mouseleave="mouseleaveHandle($event, item)"
+              @contextmenu="handleContextMenu($event, item, optionsHandle)"
+            >
+              <component
+                class="edit-content-chart"
+                :class="animationsClass(item.styles.animations)"
+                :is="item.chartConfig.chartKey"
+                :chartConfig="item"
+                :themeSetting="themeSetting"
+                :themeColor="themeColor"
+                :style="{
+                  ...useSizeStyle(item.attr),
+                  ...getFilterStyle(item.styles),
+                  ...getTransformStyle(item.styles)
+                }"
+              ></component>
+            </edit-shape-box>
+          </div>
         </div>
       </edit-range>
     </div>
@@ -65,8 +73,11 @@
 <script lang="ts" setup>
 import { onMounted, computed } from 'vue'
 import { chartColors } from '@/settings/chartThemes/index'
+import { MenuEnum } from '@/enums/editPageEnum'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
 import { useContextMenu } from '@/views/chart/hooks/useContextMenu.hook'
+import { MenuOptionsItemType } from '@/views/chart/hooks/useContextMenu.hook.d'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 
 import { useLayout } from './hooks/useLayout.hook'
@@ -76,6 +87,7 @@ import { dragHandle, dragoverHandle, useMouseHandle } from './hooks/useDrag.hook
 import { useComponentStyle, useSizeStyle } from './hooks/useStyle.hook'
 
 import { ContentBox } from '../ContentBox/index'
+import { EditGroup } from './components/EditGroup'
 import { EditRange } from './components/EditRange'
 import { EditBottom } from './components/EditBottom'
 import { EditShapeBox } from './components/EditShapeBox'
@@ -89,7 +101,33 @@ const { dataSyncFetch, intervalDataSyncUpdate } = useSync()
 useLayout()
 
 // 点击事件
-const { mouseenterHandle, mouseleaveHandle, mousedownHandle } = useMouseHandle()
+const { mouseenterHandle, mouseleaveHandle, mousedownHandle, mouseClickHandle } = useMouseHandle()
+
+// 右键事件
+const optionsHandle = (
+  targetList: MenuOptionsItemType[],
+  allList: MenuOptionsItemType[],
+  targetInstance: CreateComponentType
+) => {
+  // 多选
+  const moreMenuEnums = [MenuEnum.GROUP, MenuEnum.DELETE]
+  // 单选
+  const singleMenuEnums = targetList
+
+  // 多选处理
+  if (chartEditStore.getTargetChart.selectId.length > 1) {
+    const list: MenuOptionsItemType[] = []
+    
+    allList.forEach(item => {
+      // 成组
+      if (moreMenuEnums.includes(item.key as MenuEnum)) {
+        list.push(item)
+      }
+    })
+    return list
+  }
+  return singleMenuEnums
+}
 
 // 主题色
 const themeSetting = computed(() => {

+ 3 - 0
src/views/chart/ContentLayers/components/LayersGroupListItem/index.ts

@@ -0,0 +1,3 @@
+import LayersGroupListItem from './index.vue'
+
+export { LayersGroupListItem }

+ 224 - 0
src/views/chart/ContentLayers/components/LayersGroupListItem/index.vue

@@ -0,0 +1,224 @@
+<template>
+  <div class="go-content-layers-group-list-item">
+    <div
+      class="root-item-content"
+      :class="{ hover: hover, select: select }"
+      @click="clickHandle($event)"
+      @mousedown="groupMousedownHandle($event)"
+      @mouseenter="mouseenterHandle(componentGroupData)"
+      @mouseleave="mouseleaveHandle(componentGroupData)"
+      @contextmenu="handleContextMenu($event, componentGroupData, optionsHandle)"
+    >
+      <div class="go-flex-items-center item-content">
+        <n-icon size="20" class="go-ml-1">
+          <template v-if="expend">
+            <folder-open-icon></folder-open-icon>
+          </template>
+          <template v-else>
+            <folder-icon></folder-icon>
+          </template>
+        </n-icon>
+        <n-ellipsis>
+          <n-text class="go-ml-2 list-text" :depth="2">
+            {{ componentGroupData.chartConfig.title }}
+          </n-text>
+        </n-ellipsis>
+      </div>
+      <div :class="{ 'select-modal': select }"></div>
+    </div>
+    <n-collapse-transition :show="expend">
+      <LayersListItem
+        v-for="element in componentGroupData.groupList"
+        :key="element.id"
+        :componentData="element"
+        @mousedown="mousedownHandle($event, element, componentGroupData.id)"
+        @mouseenter="mouseenterHandle(element)"
+        @mouseleave="mouseleaveHandle(element)"
+        @contextmenu="handleContextMenu($event, componentGroupData, optionsHandle)"
+      ></LayersListItem>
+    </n-collapse-transition>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, PropType } from 'vue'
+import { MouseEventButton, WinKeyboard, MacKeyboard } from '@/enums/editPageEnum'
+import { MenuEnum } from '@/enums/editPageEnum'
+import { useDesignStore } from '@/store/modules/designStore/designStore'
+import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
+import { useContextMenu, divider } from '@/views/chart/hooks/useContextMenu.hook'
+import { MenuOptionsItemType } from '@/views/chart/hooks/useContextMenu.hook.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
+import { LayersListItem } from '../LayersListItem'
+import throttle from 'lodash/throttle'
+import { icon } from '@/plugins'
+
+const props = defineProps({
+  componentGroupData: {
+    type: Object as PropType<CreateComponentGroupType>,
+    required: true
+  }
+})
+
+// 右键
+const pickOptionsList = [MenuEnum.UN_GROUP]
+
+// 全局颜色
+const designStore = useDesignStore()
+const { FolderIcon, FolderOpenIcon } = icon.ionicons5
+
+const chartEditStore = useChartEditStore()
+const { handleContextMenu, onClickOutSide } = useContextMenu()
+
+const expend = ref(false)
+
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
+// 右键
+const optionsHandle = (
+  targetList: MenuOptionsItemType[],
+  allList: MenuOptionsItemType[],
+  targetInstance: CreateComponentType
+) => {
+  const filter = (menulist: MenuEnum[]) => {
+    const list: MenuOptionsItemType[] = []
+    allList.forEach(item => {
+      if (menulist.includes(item.key as MenuEnum)) {
+        list.push(item)
+      }
+    })
+    return list
+  }
+
+  // 多选处理
+  if (chartEditStore.getTargetChart.selectId.length > 1) {
+    return filter([MenuEnum.GROUP])
+  } else {
+    return [...filter([MenuEnum.UN_GROUP]), divider(), ...targetList]
+  }
+}
+
+// 点击
+const clickHandle = (e: MouseEvent) => {
+  // 按下左键 + CTRL
+  if (
+    window.$KeyboardActive?.has(WinKeyboard.CTRL_SOURCE_KEY) ||
+    window.$KeyboardActive?.has(MacKeyboard.CTRL_SOURCE_KEY)
+  )
+    return
+  // 判断左右键
+  expend.value = !expend.value
+  mousedownHandle(e, props.componentGroupData)
+}
+
+// 计算当前选中目标
+const select = computed(() => {
+  const id = props.componentGroupData.id
+  return chartEditStore.getTargetChart.selectId.find((e: string) => e === id)
+})
+
+// 悬浮
+const hover = computed(() => {
+  return props.componentGroupData.id === chartEditStore.getTargetChart.hoverId
+})
+
+// 组点击事件
+const groupMousedownHandle = (e: MouseEvent) => {
+  onClickOutSide()
+  // 若此时按下了 CTRL, 表示多选
+  const id = props.componentGroupData.id
+  if (
+    e.buttons === MouseEventButton.LEFT &&
+    (window.$KeyboardActive?.has(WinKeyboard.CTRL_SOURCE_KEY) ||
+      window.$KeyboardActive?.has(MacKeyboard.CTRL_SOURCE_KEY))
+  ) {
+    // 若已选中,则去除
+    if (chartEditStore.targetChart.selectId.includes(id)) {
+      const exList = chartEditStore.targetChart.selectId.filter(e => e !== id)
+      chartEditStore.setTargetSelectChart(exList)
+    } else {
+      chartEditStore.setTargetSelectChart(id, true)
+    }
+    return
+  }
+  chartEditStore.setTargetSelectChart(id)
+}
+
+// 公共点击事件
+const mousedownHandle = (e: MouseEvent, componentInstance: CreateComponentType | CreateComponentGroupType, id?: string) => {
+  e.preventDefault()
+  e.stopPropagation()
+
+  onClickOutSide()
+  chartEditStore.setTargetSelectChart(id || componentInstance.id)
+}
+
+// 公共进入事件
+const mouseenterHandle = (componentInstance: CreateComponentType | CreateComponentGroupType) => {
+  chartEditStore.setTargetHoverChart(componentInstance.id)
+}
+
+// 公共移出事件
+const mouseleaveHandle = (componentInstance: CreateComponentType | CreateComponentGroupType) => {
+  chartEditStore.setTargetHoverChart(undefined)
+}
+</script>
+
+<style lang="scss" scoped>
+$centerHeight: 52px;
+$textSize: 10px;
+
+@include go(content-layers-group-list-item) {
+  position: relative;
+  width: 90%;
+  margin: 10px 5%;
+  margin-bottom: 5px;
+  @extend .go-transition-quick;
+
+  .root-item-content {
+    height: $centerHeight;
+    cursor: pointer;
+    border-radius: 5px;
+    border: 1px solid rgba(0, 0, 0, 0);
+    &.hover,
+    &:hover {
+      @include fetch-bg-color('background-color4');
+    }
+    /* 选中 */
+    &.select {
+      border: 1px solid v-bind('themeColor');
+      /* 需要设置最高级,覆盖 hover 的颜色 */
+      background-color: rgba(0, 0, 0, 0);
+      .list-img {
+        border: 1px solid v-bind('themeColor') !important;
+      }
+    }
+  }
+  .select-modal,
+  .item-content {
+    position: absolute;
+    top: 0;
+    left: 0;
+  }
+  .item-content {
+    z-index: 1;
+    padding: 6px 5px;
+    justify-content: start !important;
+    width: calc(100% - 10px);
+    height: calc(#{$centerHeight} - 10px);
+  }
+  .select-modal {
+    width: 100%;
+    height: calc(#{$centerHeight} + 2px);
+    opacity: 0.3;
+    background-color: v-bind('themeColor');
+  }
+  .list-text {
+    padding-left: 6px;
+    font-size: $textSize;
+  }
+}
+</style>

+ 7 - 3
src/views/chart/ContentLayers/components/LayersListItem/index.vue

@@ -22,16 +22,20 @@
 </template>
 
 <script setup lang="ts">
-import { ref, toRefs, computed } from 'vue'
+import { toRefs, computed } from 'vue'
 import { requireErrorImg } from '@/utils'
 import { useDesignStore } from '@/store/modules/designStore/designStore'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
 
 // 全局颜色
 const designStore = useDesignStore()
-const themeColor = ref(designStore.getAppTheme)
 const chartEditStore = useChartEditStore()
 
+// 颜色
+const themeColor = computed(() => {
+  return designStore.getAppTheme
+})
+
 const props = defineProps({
   componentData: {
     type: Object,
@@ -60,7 +64,7 @@ $textSize: 10px;
   position: relative;
   height: $centerHeight;
   width: 90%;
-  margin: 10px 5%;
+  margin: 5px 5%;
   margin-bottom: 5px;
   border-radius: 5px;
   cursor: pointer;

+ 68 - 20
src/views/chart/ContentLayers/index.vue

@@ -5,6 +5,7 @@
     title="图层"
     :depth="2"
     @back="backHandle"
+    @mousedown="boxMousedownHandle($event)"
   >
     <template #icon>
       <n-icon size="16" :depth="2">
@@ -16,20 +17,21 @@
       <n-text class="not-layer-text">暂无图层~</n-text>
     </n-space>
     <!-- https://github.com/SortableJS/vue.draggable.next -->
-    <draggable
-      item-key="id"
-      v-model="reverseList"
-      ghostClass="ghost"
-      @change="onMoveCallback"
-    >
+    <draggable item-key="id" v-model="reverseList" ghostClass="ghost" @change="onMoveCallback">
       <template #item="{ element }">
-        <layers-list-item
-          :componentData="element"
-          @mousedown="mousedownHandle(element)"
-          @mouseenter="mouseenterHandle(element)"
-          @mouseleave="mouseleaveHandle(element)"
-          @contextmenu="handleContextMenu($event, element)"
-        ></layers-list-item>
+        <div class="go-content-layer-box">
+          <!-- 组合 -->
+          <layers-group-list-item v-if="element.isGroup" :componentGroupData="element"></layers-group-list-item>
+          <!-- 单组件 -->
+          <layers-list-item
+            v-else
+            :componentData="element"
+            @mousedown="mousedownHandle($event, element)"
+            @mouseenter="mouseenterHandle(element)" 
+            @mouseleave="mouseleaveHandle(element)"
+            @contextmenu="handleContextMenu($event, element, optionsHandle)"
+          ></layers-list-item>
+        </div>
       </template>
     </draggable>
   </content-box>
@@ -39,27 +41,49 @@
 import { computed, toRaw } from 'vue'
 import Draggable from 'vuedraggable'
 import cloneDeep from 'lodash/cloneDeep'
-
 import { ContentBox } from '../ContentBox/index'
 import { useChartLayoutStore } from '@/store/modules/chartLayoutStore/chartLayoutStore'
 import { ChartLayoutStoreEnum } from '@/store/modules/chartLayoutStore/chartLayoutStore.d'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
+import { MenuOptionsItemType } from '@/views/chart/hooks/useContextMenu.hook.d'
 import { useContextMenu } from '@/views/chart/hooks/useContextMenu.hook'
-import { MenuEnum } from '@/enums/editPageEnum'
+import { MenuEnum, MouseEventButton, WinKeyboard, MacKeyboard } from '@/enums/editPageEnum'
 
 import { LayersListItem } from './components/LayersListItem/index'
+import { LayersGroupListItem } from './components/LayersGroupListItem/index'
+
 import { icon } from '@/plugins'
 
 const { LayersIcon } = icon.ionicons5
 const chartLayoutStore = useChartLayoutStore()
 const chartEditStore = useChartEditStore()
 
-const { handleContextMenu } = useContextMenu()
+const { handleContextMenu, onClickOutSide } = useContextMenu()
 
+// 右键事件
+const optionsHandle = (
+  targetList: MenuOptionsItemType[],
+  allList: MenuOptionsItemType[],
+  targetInstance: CreateComponentType
+) => {
+  // 多选处理
+  if (chartEditStore.getTargetChart.selectId.length > 1) {
+    const list: MenuOptionsItemType[] = []
+    targetList.forEach(item => {
+      // 成组
+      if (item.key === MenuEnum.GROUP) {
+        list.push(item)
+      }
+    })
+    return list
+  }
+  return targetList
+}
+ 
 // 逆序展示
 const reverseList = computed(() => {
-  const list: CreateComponentType[] = cloneDeep(chartEditStore.getComponentList)
+  const list: Array<CreateComponentType | CreateComponentGroupType> = cloneDeep(chartEditStore.getComponentList)
   return list.reverse()
 })
 
@@ -87,9 +111,33 @@ const onMoveCallback = (val: any) => {
   }
 }
 
+const boxMousedownHandle = (e: MouseEvent) => {
+  const box = document.querySelector('.go-content-layer-box')
+  if ((e.target as any).contains(box)) {
+    chartEditStore.setTargetSelectChart()
+  }
+}
+
 // 点击事件
-const mousedownHandle = (item: CreateComponentType) => {
-  chartEditStore.setTargetSelectChart(item.id)
+const mousedownHandle = (e: MouseEvent, item: CreateComponentType) => {
+  onClickOutSide()
+  // 若此时按下了 CTRL, 表示多选
+  const id = item.id
+  if (
+    e.buttons === MouseEventButton.LEFT &&
+    (window.$KeyboardActive?.has(WinKeyboard.CTRL_SOURCE_KEY) ||
+      window.$KeyboardActive?.has(MacKeyboard.CTRL_SOURCE_KEY))
+  ) {
+    // 若已选中,则去除
+    if (chartEditStore.targetChart.selectId.includes(id)) {
+      const exList = chartEditStore.targetChart.selectId.filter(e => e !== id)
+      chartEditStore.setTargetSelectChart(exList)
+    } else {
+      chartEditStore.setTargetSelectChart(id, true)
+    }
+    return
+  }
+  chartEditStore.setTargetSelectChart(id)
 }
 
 // 进入事件

+ 1 - 12
src/views/chart/hooks/useContextMenu.hook.d.ts

@@ -1,15 +1,4 @@
-// 右键枚举
-export enum MenuEnum {
-  DELETE = 'delete',
-  COPY = 'copy',
-  CUT = 'cut',
-  PARSE = 'parse',
-  TOP = 'top',
-  BOTTOM = 'bottom',
-  UP = 'up',
-  DOWN = 'down',
-  CLEAR = 'clear',
-}
+import { MenuEnum } from '@/enums/editPageEnum'
 
 export interface MenuOptionsItemType {
   type?: string

+ 77 - 41
src/views/chart/hooks/useContextMenu.hook.ts

@@ -1,87 +1,109 @@
-import { ref, nextTick } from 'vue'
+import { ref, nextTick, toRaw } from 'vue'
 import { useChartEditStore } from '@/store/modules/chartEditStore/chartEditStore'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { renderIcon, loadingError } from '@/utils'
 import { icon } from '@/plugins'
 import { MenuOptionsItemType } from './useContextMenu.hook.d'
 import { MenuEnum } from '@/enums/editPageEnum'
+import cloneDeep from 'lodash/cloneDeep'
 
-const {
-  CopyIcon,
-  CutIcon,
-  ClipboardOutlineIcon,
-  TrashIcon,
-  ChevronDownIcon,
-  ChevronUpIcon,
-} = icon.ionicons5
-const { UpToTopIcon, DownToBottomIcon, PaintBrushIcon } = icon.carbon
+const { CopyIcon, CutIcon, ClipboardOutlineIcon, TrashIcon, ChevronDownIcon, ChevronUpIcon } = icon.ionicons5
+const { UpToTopIcon, DownToBottomIcon, PaintBrushIcon, Carbon3DSoftwareIcon, Carbon3DCursorIcon } = icon.carbon
 
 const chartEditStore = useChartEditStore()
 
-// * 默认选项
-const defaultOptions: MenuOptionsItemType[] = [
+/**
+ * 分割线
+ * @param {number} n > 2
+ * @returns
+ */
+export const divider = (n:number = 3) => {
+  return {
+    type: 'divider',
+    key: `d${n}`
+  }
+}
+
+// * 默认单组件选项
+export const defaultOptions: MenuOptionsItemType[] = [
   {
     label: '复制',
     key: MenuEnum.COPY,
     icon: renderIcon(CopyIcon),
-    fnHandle: chartEditStore.setCopy,
+    fnHandle: chartEditStore.setCopy
   },
   {
     label: '剪切',
     key: MenuEnum.CUT,
     icon: renderIcon(CutIcon),
-    fnHandle: chartEditStore.setCut,
+    fnHandle: chartEditStore.setCut
   },
   {
     label: '粘贴',
     key: MenuEnum.PARSE,
     icon: renderIcon(ClipboardOutlineIcon),
-    fnHandle: chartEditStore.setParse,
+    fnHandle: chartEditStore.setParse
   },
   {
     type: 'divider',
-    key: 'd1',
+    key: 'd1'
   },
   {
     label: '置顶',
     key: MenuEnum.TOP,
     icon: renderIcon(UpToTopIcon),
-    fnHandle: chartEditStore.setTop,
+    fnHandle: chartEditStore.setTop
   },
   {
     label: '置底',
     key: MenuEnum.BOTTOM,
     icon: renderIcon(DownToBottomIcon),
-    fnHandle: chartEditStore.setBottom,
+    fnHandle: chartEditStore.setBottom
   },
   {
     label: '上移一层',
     key: MenuEnum.UP,
     icon: renderIcon(ChevronUpIcon),
-    fnHandle: chartEditStore.setUp,
+    fnHandle: chartEditStore.setUp
   },
   {
     label: '下移一层',
     key: MenuEnum.DOWN,
     icon: renderIcon(ChevronDownIcon),
-    fnHandle: chartEditStore.setDown,
+    fnHandle: chartEditStore.setDown
   },
   {
     type: 'divider',
-    key: 'd2',
+    key: 'd2'
   },
   {
     label: '清空剪贴板',
     key: MenuEnum.CLEAR,
     icon: renderIcon(PaintBrushIcon),
-    fnHandle: chartEditStore.setRecordChart,
+    fnHandle: chartEditStore.setRecordChart
   },
   {
     label: '删除',
     key: MenuEnum.DELETE,
     icon: renderIcon(TrashIcon),
-    fnHandle: chartEditStore.removeComponentList,
+    fnHandle: chartEditStore.removeComponentList
+  }
+]
+
+// * 默认多选组件选项
+export const defaultMultiSelectOptions: MenuOptionsItemType[] = [
+  {
+    label: '创建分组',
+    key: MenuEnum.GROUP,
+    icon: renderIcon(Carbon3DSoftwareIcon),
+    fnHandle: chartEditStore.setGroup
   },
+  {
+    label: '解除分组',
+    key: MenuEnum.UN_GROUP,
+    icon: renderIcon(Carbon3DCursorIcon),
+    fnHandle: chartEditStore.setUnGroup
+  }
 ]
 
 // * 无数据传递拥有的选项
@@ -95,9 +117,11 @@ const defaultNoItemKeys = [MenuEnum.PARSE, MenuEnum.CLEAR]
  */
 const pickOption = (options: MenuOptionsItemType[], pickList?: MenuEnum[]) => {
   if (!pickList) return options
-  return options.filter((op: MenuOptionsItemType) => {
-    return pickList.findIndex((e: MenuEnum) => e === op.key) !== -1
+  const list: MenuOptionsItemType[] = []
+  pickList.forEach(e => {
+    list.push(...options.filter(op => op.key === e))
   })
+  return list
 }
 
 /**
@@ -120,36 +144,49 @@ const menuOptions = ref<MenuOptionsItemType[]>([])
 const handleContextMenu = (
   e: MouseEvent,
   // 右键对象
-  item?: CreateComponentType,
+  targetInstance?: CreateComponentType | CreateComponentGroupType,
   // 判断函数
   optionsHandle?: Function,
   // 隐藏选项列表
   hideOptionsList?: MenuEnum[],
   // 挑选选项列表
-  pickOptionsList?: MenuEnum[],
+  pickOptionsList?: MenuEnum[]
 ) => {
   e.stopPropagation()
   e.preventDefault()
+
   let target = e.target
   while (target instanceof SVGElement) {
     target = target.parentNode
   }
+
+  // 展示列表
   chartEditStore.setRightMenuShow(false)
 
-  // * 设置默认选项
-  menuOptions.value = defaultOptions
+  // * 多选默认选项
+  if (chartEditStore.getTargetChart.selectId.length > 1) {
+    menuOptions.value = defaultMultiSelectOptions
+  } else {
+    // * 单选默认选项
+    menuOptions.value = defaultOptions
+  }
 
-  if (!item) {
-    menuOptions.value = pickOption(menuOptions.value, defaultNoItemKeys)
+  if (!targetInstance) {
+    menuOptions.value = pickOption(toRaw(menuOptions.value), defaultNoItemKeys)
   }
   if (hideOptionsList) {
-    menuOptions.value = hideOption(menuOptions.value, hideOptionsList)
+    menuOptions.value = hideOption([...defaultMultiSelectOptions, divider(), ...defaultOptions], hideOptionsList)
   }
   if (pickOptionsList) {
-    menuOptions.value = hideOption(menuOptions.value, pickOptionsList)
+    menuOptions.value = pickOption([...defaultMultiSelectOptions, divider(), ...defaultOptions], pickOptionsList)
   }
   if (optionsHandle) {
-    menuOptions.value = optionsHandle(menuOptions.value)
+    // 自定义函数能够拿到当前选项和所有选项
+    menuOptions.value = optionsHandle(
+      cloneDeep(toRaw(menuOptions.value)),
+      [...defaultMultiSelectOptions, ...defaultOptions],
+      targetInstance
+    )
   }
   nextTick().then(() => {
     chartEditStore.setMousePosition(e.clientX, e.clientY)
@@ -163,7 +200,6 @@ const handleContextMenu = (
  * @returns
  */
 export const useContextMenu = () => {
-
   // 设置默认项
   menuOptions.value = defaultOptions
 
@@ -175,9 +211,7 @@ export const useContextMenu = () => {
   // * 事件处理
   const handleMenuSelect = (key: string) => {
     chartEditStore.setRightMenuShow(false)
-    const targetItem: MenuOptionsItemType[] = menuOptions.value.filter(
-      (e: MenuOptionsItemType) => e.key === key
-    )
+    const targetItem: MenuOptionsItemType[] = menuOptions.value.filter((e: MenuOptionsItemType) => e.key === key)
 
     menuOptions.value.forEach((e: MenuOptionsItemType) => {
       if (e.key === key) {
@@ -189,12 +223,14 @@ export const useContextMenu = () => {
       }
     })
   }
-  
+
   return {
     menuOptions,
+    defaultOptions,
+    defaultMultiSelectOptions,
     handleContextMenu,
     onClickOutSide,
     handleMenuSelect,
-    mousePosition: chartEditStore.getMousePosition,
+    mousePosition: chartEditStore.getMousePosition
   }
 }

+ 36 - 15
src/views/chart/hooks/useKeyboard.hook.ts

@@ -24,6 +24,8 @@ export const winKeyboardValue = {
   [MenuEnum.BACK]: winCtrlMerge('z'),
   [MenuEnum.FORWORD]: winCtrlMerge(winShiftMerge('z')),
   [MenuEnum.SAVE]: winCtrlMerge('s'),
+  [MenuEnum.GROUP]: winCtrlMerge('g'),
+  [MenuEnum.UN_GROUP]: winCtrlMerge(winShiftMerge('g')),
 }
 
 // 这个 Ctrl 后面还是换成了 ⌘
@@ -44,6 +46,8 @@ export const macKeyboardValue = {
   [MenuEnum.BACK]: macCtrlMerge('z'),
   [MenuEnum.FORWORD]: macCtrlMerge(macShiftMerge('z')),
   [MenuEnum.SAVE]: macCtrlMerge('s'),
+  [MenuEnum.GROUP]: macCtrlMerge('g'),
+  [MenuEnum.UN_GROUP]: macCtrlMerge(macShiftMerge('g')),
 }
 
 // Win 快捷键列表
@@ -62,6 +66,8 @@ const winKeyList: Array<string> = [
   winKeyboardValue.forward,
 
   winKeyboardValue.save,
+  winKeyboardValue.group,
+  winKeyboardValue.unGroup,
 ]
 
 // Mac 快捷键列表
@@ -80,65 +86,80 @@ const macKeyList: Array<string> = [
   macKeyboardValue.forward,
 
   macKeyboardValue.save,
+  macKeyboardValue.group,
+  macKeyboardValue.unGroup,
 ]
 
 // 处理键盘记录
 const keyRecordHandle = () => {
-  document.onkeydown = throttle((e: KeyboardEvent) => {
+  // 初始化清空
+  if(window.$KeyboardActive) window.$KeyboardActive = new Set([])
+
+  document.onkeydown = (e: KeyboardEvent) => {
     if(window.$KeyboardActive) window.$KeyboardActive.add(e.key.toLocaleLowerCase())
-    else window.$KeyboardActive = new Set([e.key])
-  }, 200)
+    else window.$KeyboardActive = new Set([e.key.toLocaleLowerCase()])
+  }
 
-  document.onkeyup = throttle((e: KeyboardEvent) => {
+  document.onkeyup = (e: KeyboardEvent) => {
     if(window.$KeyboardActive) window.$KeyboardActive.delete(e.key.toLocaleLowerCase())
-  }, 200)
+  }
 }
 
 // 初始化监听事件
 export const useAddKeyboard = () => {
+  const throttleTime = 50
   const switchHandle = (keyboardValue: typeof winKeyboardValue, e: string) => {
     switch (e) {
       // ct+↑
       case keyboardValue.up:
-        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_UP); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_UP); return false }, throttleTime))
         break;
       // ct+→
       case keyboardValue.right:
-        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_RIGHT); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_RIGHT); return false }, throttleTime))
         break;
       // ct+↓
       case keyboardValue.down:
-        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_DOWN); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_DOWN); return false }, throttleTime))
         break;
       // ct+←
       case keyboardValue.left:
-        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_LEFT); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setMove(MenuEnum.ARROW_LEFT); return false }, throttleTime))
         break;
 
       // 删除 delete
       case keyboardValue.delete:
-        keymaster(e, debounce(() => { chartEditStore.removeComponentList(); return false }, 200))
+        keymaster(e, debounce(() => { chartEditStore.removeComponentList(); return false }, throttleTime))
         break;
       // 复制 ct+v
       case keyboardValue.copy:
-        keymaster(e, debounce(() => { chartEditStore.setCopy(); return false }, 200))
+        keymaster(e, debounce(() => { chartEditStore.setCopy(); return false }, throttleTime))
         break;
       // 剪切 ct+x
       case keyboardValue.cut:
-        keymaster(e, debounce(() => { chartEditStore.setCut(); return false }, 200))
+        keymaster(e, debounce(() => { chartEditStore.setCut(); return false }, throttleTime))
         break;
       // 粘贴 ct+v
       case keyboardValue.parse:
-        keymaster(e, throttle(() => { chartEditStore.setParse(); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setParse(); return false }, throttleTime))
         break;
 
       // 撤回 ct+z
       case keyboardValue.back:
-        keymaster(e, throttle(() => { chartEditStore.setBack(); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setBack(); return false }, throttleTime))
         break;
       // 前进 ct+sh+z
       case keyboardValue.forward:
-        keymaster(e, throttle(() => { chartEditStore.setForward(); return false }, 200))
+        keymaster(e, throttle(() => { chartEditStore.setForward(); return false }, throttleTime))
+        break;
+      
+      // 创建分组 ct+g
+      case keyboardValue.group:
+        keymaster(e, throttle(() => { chartEditStore.setGroup(); return false }, throttleTime))
+        break;
+      // 解除分组 ct+sh+g
+      case keyboardValue.unGroup:
+        keymaster(e, throttle(() => { chartEditStore.setUnGroup(); return false }, throttleTime))
         break;
 
       // 保存 ct+s

+ 71 - 26
src/views/chart/hooks/useSync.hook.ts

@@ -6,7 +6,6 @@ import { EditCanvasTypeEnum, ChartEditStoreEnum, ProjectInfoEnum, ChartEditStora
 import { useChartHistoryStore } from '@/store/modules/chartHistoryStore/chartHistoryStore'
 import { useSystemStore } from '@/store/modules/systemStore/systemStore'
 import { fetchChartComponent, fetchConfigComponent, createComponent } from '@/packages/index'
-import { CreateComponentType } from '@/packages/index.d'
 import { saveInterval } from '@/settings/designSetting'
 import throttle from 'lodash/throttle'
 // 接口状态
@@ -15,6 +14,8 @@ import { ResultEnum } from '@/enums/httpEnum'
 import { saveProjectApi, fetchProjectApi, uploadFile, updateProjectApi } from '@/api/path'
 // 画布枚举
 import { SyncEnum } from '@/enums/editPageEnum'
+import { CreateComponentType, CreateComponentGroupType, ConfigType } from '@/packages/index.d'
+import { PublicGroupConfigClass } from '@/packages/public/publicConfig'
 
 // 请求处理
 export const useSync = () => {
@@ -25,11 +26,11 @@ export const useSync = () => {
   /**
    * * 组件动态注册
    * @param projectData 项目数据
-   * @param isSplace 是否替换数据
-   * @returns 
+   * @param isReplace 是否替换数据
+   * @returns
    */
-  const updateComponent = async (projectData: ChartEditStorage, isSplace = false) => {
-    if (isSplace) {
+  const updateComponent = async (projectData: ChartEditStorage, isReplace = false, changeId = false) => {
+    if (isReplace) {
       // 清除列表
       chartEditStore.componentList = []
       // 清除历史记录
@@ -37,16 +38,20 @@ export const useSync = () => {
       chartHistoryStore.clearForwardStack()
     }
     // 列表组件注册
-    projectData.componentList.forEach(async (e: CreateComponentType) => {
-      if (!window['$vue'].component(e.chartConfig.chartKey)) {
-        window['$vue'].component(
-          e.chartConfig.chartKey,
-          fetchChartComponent(e.chartConfig)
-        )
-        window['$vue'].component(
-          e.chartConfig.conKey,
-          fetchConfigComponent(e.chartConfig)
-        )
+    projectData.componentList.forEach(async (e: CreateComponentType | CreateComponentGroupType) => {
+      const intComponent = (target: CreateComponentType) => {
+        if (!window['$vue'].component(target.chartConfig.chartKey)) {
+          window['$vue'].component(target.chartConfig.chartKey, fetchChartComponent(target.chartConfig))
+          window['$vue'].component(target.chartConfig.conKey, fetchConfigComponent(target.chartConfig))
+        }
+      }
+
+      if (e.isGroup) {
+        ;(e as CreateComponentGroupType).groupList.forEach(groupItem => {
+          intComponent(groupItem)
+        })
+      } else {
+        intComponent(e as CreateComponentType)
       }
     })
     // 数据赋值
@@ -54,20 +59,60 @@ export const useSync = () => {
       // 组件
       if (key === ChartEditStoreEnum.COMPONENT_LIST) {
         for (const comItem of projectData[key]) {
-          // 补充 class 上的方法
-          let newComponent: CreateComponentType = await createComponent(
-            comItem.chartConfig
-          )
-          chartEditStore.addComponentList(
-            Object.assign(newComponent, {...comItem, id: getUUID()}),
-            false,
-            true
-          )
+          // 重新创建是为了处理类种方法消失的问题
+          const create = async (
+            _componentInstance: CreateComponentType,
+            callBack?: (componentInstance: CreateComponentType) => void
+          ) => {
+            // 补充 class 上的方法
+            let newComponent: CreateComponentType = await createComponent(_componentInstance.chartConfig)
+            if (callBack) {
+              if (changeId) {
+                callBack(Object.assign(newComponent, { ..._componentInstance, id: getUUID() }))
+              } else {
+                callBack(Object.assign(newComponent))
+              }
+            } else {
+              if (changeId) {
+                chartEditStore.addComponentList(
+                  Object.assign(newComponent, { ..._componentInstance, id: getUUID() }),
+                  false,
+                  true
+                )
+              } else {
+                chartEditStore.addComponentList(Object.assign(newComponent), false, true)
+              }
+            }
+          }
+
+          if (comItem.isGroup) {
+            // 创建分组
+            let groupClass = new PublicGroupConfigClass()
+            if (changeId) {
+              groupClass = Object.assign(groupClass, { ...comItem, id: getUUID() })
+            } else {
+              groupClass = Object.assign(groupClass, { ...comItem })
+            }
+
+            // 注册子应用
+            const targetList: CreateComponentType[] = []
+            ;(comItem as CreateComponentGroupType).groupList.forEach(groupItem => {
+              create(groupItem, e => {
+                targetList.push(e)
+              })
+            })
+            groupClass.groupList = targetList
+
+            // 分组插入到列表
+            chartEditStore.addComponentList(groupClass, false, true)
+          } else {
+            create(comItem as CreateComponentType)
+          }
         }
       } else {
         // 非组件(顺便排除脏数据)
         if (key !== 'editCanvasConfig' && key !== 'requestGlobalConfig') return
-        Object.assign((chartEditStore as any)[key], projectData[key])
+        Object.assign(chartEditStore[key], projectData[key])
       }
     }
   }
@@ -187,4 +232,4 @@ export const useSync = () => {
     dataSyncUpdate,
     intervalDataSyncUpdate
   }
-}
+}

+ 3 - 0
src/views/preview/components/PreviewRenderGroup/index.ts

@@ -0,0 +1,3 @@
+import PreviewRenderGroup from './index.vue'
+
+export { PreviewRenderGroup }

+ 53 - 0
src/views/preview/components/PreviewRenderGroup/index.vue

@@ -0,0 +1,53 @@
+<template>
+  <div
+    class="chart-item"
+    v-for="item in groupData.groupList"
+    :class="animationsClass(item.styles.animations)"
+    :key="item.id"
+    :style="{ 
+      ...getComponentAttrStyle(item.attr, groupIndex),
+      ...getFilterStyle(item.styles),
+      ...getTransformStyle(item.styles)
+    }"
+  >
+    <component
+      :is="item.chartConfig.chartKey"
+      :chartConfig="item"
+      :themeSetting="themeSetting"
+      :themeColor="themeColor"
+      :style="{...getSizeStyle(item.attr)}"
+    ></component>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { PropType } from 'vue'
+import { CreateComponentGroupType } from '@/packages/index.d'
+import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
+import { getSizeStyle, getComponentAttrStyle } from '../../utils'
+
+const props = defineProps({
+  groupData: {
+    type: Object as PropType<CreateComponentGroupType>,
+    required: true
+  },
+  themeSetting: {
+    type: Object,
+    required: true
+  },
+  themeColor: {
+    type: Object,
+    required: true
+  },
+  groupIndex: {
+    type: Number,
+    required: true
+  }
+})
+</script>
+
+<style lang="scss" scoped>
+.chart-item {
+  position: absolute;
+}
+</style>

+ 16 - 4
src/views/preview/components/PreviewRenderList/index.vue

@@ -1,21 +1,32 @@
 <template>
   <div
     class="chart-item"
-    :class="animationsClass(item.styles.animations)"
     v-for="(item, index) in localStorageInfo.componentList"
+    :class="animationsClass(item.styles.animations)"
     :key="item.id"
-    :style="{ 
+    :style="{
       ...getComponentAttrStyle(item.attr, index),
       ...getFilterStyle(item.styles),
       ...getTransformStyle(item.styles)
     }"
   >
+    <!-- 分组 -->
+    <preview-render-group
+      v-if="item.isGroup"
+      :groupData="(item as CreateComponentGroupType)"
+      :groupIndex="index"
+      :themeSetting="themeSetting"
+      :themeColor="themeColor"
+    ></preview-render-group>
+
+    <!-- 单组件 -->
     <component
+      v-else
       :is="item.chartConfig.chartKey"
       :chartConfig="item"
       :themeSetting="themeSetting"
       :themeColor="themeColor"
-      :style="{...getSizeStyle(item.attr)}"
+      :style="{ ...getSizeStyle(item.attr) }"
     ></component>
   </div>
 </template>
@@ -23,6 +34,8 @@
 <script setup lang="ts">
 import { PropType, computed } from 'vue'
 import { ChartEditStorageType } from '../../index.d'
+import { PreviewRenderGroup } from '../PreviewRenderGroup/index'
+import { CreateComponentGroupType } from '@/packages/index.d'
 import { chartColors } from '@/settings/chartThemes/index'
 import { animationsClass, getFilterStyle, getTransformStyle } from '@/utils'
 import { getSizeStyle, getComponentAttrStyle } from '../../utils'
@@ -45,7 +58,6 @@ const themeColor = computed(() => {
   const chartThemeColor = props.localStorageInfo.editCanvasConfig.chartThemeColor
   return chartColors[chartThemeColor]
 })
-
 </script>
 
 <style lang="scss" scoped>

+ 15 - 7
src/views/preview/hooks/useComInstall.hook.ts

@@ -1,6 +1,6 @@
 import { ref } from 'vue'
 import { ChartEditStorageType } from '../index.d'
-import { CreateComponentType } from '@/packages/index.d'
+import { CreateComponentType, CreateComponentGroupType } from '@/packages/index.d'
 import { fetchChartComponent } from '@/packages/index'
 
 export const useComInstall = (localStorageInfo: ChartEditStorageType) => {
@@ -10,12 +10,20 @@ export const useComInstall = (localStorageInfo: ChartEditStorageType) => {
   const intervalTiming = setInterval(() => {
     if (window['$vue']?.component) {
       clearInterval(intervalTiming)
-      localStorageInfo.componentList.forEach(async (e: CreateComponentType) => {
-        if (!window['$vue'].component(e.chartConfig.chartKey)) {
-          window['$vue'].component(
-            e.chartConfig.chartKey,
-            fetchChartComponent(e.chartConfig)
-          )
+
+      const intComponent = (target: CreateComponentType) => {
+        if (!window['$vue'].component(target.chartConfig.chartKey)) {
+          window['$vue'].component(target.chartConfig.chartKey, fetchChartComponent(target.chartConfig))
+        }
+      }
+
+      localStorageInfo.componentList.forEach(async (e: CreateComponentType | CreateComponentGroupType) => {
+        if (e.isGroup) {
+          (e as CreateComponentGroupType).groupList.forEach(groupItem => {
+            intComponent(groupItem)
+          })
+        } else {
+          intComponent(e as CreateComponentType)
         }
       })
       show.value = true