Просмотр исходного кода

pref: 优化拖拽节点性能问题

jiaxing.liao 2 месяцев назад
Родитель
Сommit
60d1479648

+ 1 - 1
src/renderer/src/lvgl-widgets/image-button/index.ts

@@ -122,7 +122,7 @@ export default {
       },
       },
       {
       {
         label: '删除标识',
         label: '删除标识',
-        field: 'addFlags',
+        field: 'removeFlags',
         valueType: 'select',
         valueType: 'select',
         componentProps: {
         componentProps: {
           options: flagOptions,
           options: flagOptions,

+ 10 - 1
src/renderer/src/store/modules/project.ts

@@ -70,13 +70,21 @@ export const useProjectStore = defineStore('project', () => {
   const openPages = ref<Page[]>([])
   const openPages = ref<Page[]>([])
 
 
   // 当前选中元素列表
   // 当前选中元素列表
-  const activeWidgets = ref<any[]>([])
+  const activeWidgets = ref<BaseWidget[]>([])
 
 
   // 当前激活的元素
   // 当前激活的元素
   const activeWidget = computed(() => {
   const activeWidget = computed(() => {
     return activeWidgets.value?.at(-1) ?? activePage.value
     return activeWidgets.value?.at(-1) ?? activePage.value
   })
   })
 
 
+  // 当前激活元素map
+  const activeWidgetMap = computed(() => {
+    return activeWidgets.value.reduce((acc, cur) => {
+      acc[cur.id] = cur
+      return acc
+    }, {})
+  })
+
   /**
   /**
    * 创建应用
    * 创建应用
    * @param meta 应用元信息
    * @param meta 应用元信息
@@ -269,6 +277,7 @@ export const useProjectStore = defineStore('project', () => {
     activeWidgets,
     activeWidgets,
     openPages,
     openPages,
     setSelectWidgets,
     setSelectWidgets,
+    activeWidgetMap,
 
 
     // 历史记录
     // 历史记录
     history,
     history,

+ 0 - 1
src/renderer/src/views/designer/config/PropertyConfig.vue

@@ -658,7 +658,6 @@ watch(
       moduleValue.value = LvglWidgets[data.value.type]?.parts[0].name || 'main'
       moduleValue.value = LvglWidgets[data.value.type]?.parts[0].name || 'main'
       stateValue.value = LvglWidgets[data.value.type]?.parts[0].stateList[0] || 'default'
       stateValue.value = LvglWidgets[data.value.type]?.parts[0].stateList[0] || 'default'
       data.value = value || ({} as Page['props'])
       data.value = value || ({} as Page['props'])
-      console.log(value)
     }
     }
   }
   }
 )
 )

+ 1 - 0
src/renderer/src/views/designer/sidebar/Libary.vue

@@ -95,6 +95,7 @@ function handleAdd(item: IComponentModelConfig) {
   }
   }
 
 
   projectStore.activePage?.children?.unshift(newWidget)
   projectStore.activePage?.children?.unshift(newWidget)
+  projectStore.setSelectWidgets([newWidget])
 }
 }
 </script>
 </script>
 
 

+ 2 - 0
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -26,6 +26,7 @@
             position: 'static'
             position: 'static'
           }"
           }"
         />
         />
+        <Moveable :root-container="canvasRef!" />
       </div>
       </div>
     </div>
     </div>
   </div>
   </div>
@@ -41,6 +42,7 @@ import { useScroll, useElementSize, useResizeObserver } from '@vueuse/core'
 import { useProjectStore } from '@/store/modules/project'
 import { useProjectStore } from '@/store/modules/project'
 
 
 import Nodes from './Node.vue'
 import Nodes from './Node.vue'
+import Moveable from './Moveable.vue'
 
 
 const props = defineProps<{
 const props = defineProps<{
   state: StageState
   state: StageState

+ 219 - 0
src/renderer/src/views/designer/workspace/stage/Moveable.vue

@@ -0,0 +1,219 @@
+<template>
+  <Moveable
+    :target="elements"
+    :draggable="true"
+    :resizable="true"
+    :rotatable="false"
+    :padding="2"
+    :container="rootContainer"
+    :useMutationObserver="true"
+    :useResizeObserver="true"
+    :throttleDrag="1"
+    :throttleResize="1"
+    :throttleRotate="1"
+    :snappable="true"
+    :snapDirections="{
+      top: true,
+      left: true,
+      bottom: true,
+      right: true,
+      center: true,
+      middle: true
+    }"
+    :elementSnapDirections="{
+      top: true,
+      left: true,
+      bottom: true,
+      right: true,
+      center: true,
+      middle: true
+    }"
+    :maxSnapElementGuidelineDistance="100"
+    :elementGuidelines="getElementGridelines"
+    :verticalGuidelines="verticalGuidelines"
+    :horizontalGuidelines="horizontalGuidelines"
+    :controlPadding="4"
+    @render="onRender"
+    @drag="onDrag"
+    @resize="onResize"
+    @resizeEnd="onResizeEnd"
+    @dragStart="onDragStart"
+    @dragEnd="onDragEnd"
+    @dragGroup="onDragGroup"
+    @dragGroupEnd="onDragGroupEnd"
+    @resizeGroup="onResizeGroup"
+    @resizeGroupEnd="onResizeGroupEnd"
+  />
+</template>
+
+<script setup lang="ts">
+import { computed, ref, inject, watch } from 'vue'
+import Moveable from 'vue3-moveable'
+import { useProjectStore } from '@/store/modules/project'
+import { useMutationObserver } from '@vueuse/core'
+
+import type { StageState } from './type'
+
+const props = defineProps<{
+  // 父级容器 拖拽缩放设置
+  rootContainer: HTMLElement
+}>()
+
+const pageState = inject<StageState>('state')
+const pageEl = inject<HTMLElement>('pageEl')
+const projectStore = useProjectStore()
+const elements = ref<HTMLElement[]>([])
+
+// 吸附附近元素
+const elementGridelines = ref<Element[]>([])
+// 获取吸附附近元素
+const getElementGridelines = computed(() => {
+  const ids = projectStore.activeWidgets.map((item) => item.id)
+  return elementGridelines.value.filter((el) => {
+    const id = el.attributes.getNamedItem('widget-id')?.value
+    return id && !ids.includes(id)
+  })
+})
+// 垂直辅助线
+const verticalGuidelines = computed(() => {
+  return (
+    (pageState?.showReferenceLine &&
+      projectStore.activePage?.referenceLine
+        .filter((item) => item.type === 'horizontal')
+        .map((item) => item.value)) ||
+    []
+  )
+})
+// 水平辅助线
+const horizontalGuidelines = computed(() => {
+  return (
+    (pageState?.showReferenceLine &&
+      projectStore.activePage?.referenceLine
+        .filter((item) => item.type === 'vertical')
+        .map((item) => item.value)) ||
+    []
+  )
+})
+
+// 监听选中元素
+watch(
+  () => projectStore.activeWidgets,
+  () => {
+    elements.value = []
+    const ids = projectStore.activeWidgets.map((item) => item.id)
+    document.querySelectorAll('[widget-id]').forEach((el) => {
+      const id = el.attributes.getNamedItem('widget-id')?.value
+      if (id && ids.includes(id)) {
+        elements.value.push(el as HTMLElement)
+      }
+    })
+  },
+  {
+    deep: true
+  }
+)
+
+// 监听页面节点变化 设置元素辅助线
+useMutationObserver(
+  pageEl,
+  (mutations) => {
+    mutations.forEach(() => {
+      const els = document.querySelectorAll('.widget-node')
+      elementGridelines.value = []
+      els.forEach((el) => {
+        elementGridelines.value.push(el)
+      })
+    })
+  },
+  {
+    childList: true,
+    subtree: true
+  }
+)
+// 临时层级
+const zIndex = ref()
+
+// 拖拽开始
+const onDragStart = (e) => {
+  zIndex.value = e.target.style.zIndex
+  e.target.style.zIndex = 999
+}
+// 渲染节点拖拽
+const onDrag = (e) => {
+  // 当前选中节点整体移动
+  e.target.style.transform = e.transform
+}
+// 拖拽结束
+const onDragEnd = (e) => {
+  e.target.style.zIndex = zIndex.value
+  const id = e.target.attributes['widget-id']?.value
+  if (id && projectStore.activeWidgetMap[id] && e?.lastEvent?.translate) {
+    projectStore.activeWidgetMap[id].props.x = Math.round(e.lastEvent.translate[0])
+    projectStore.activeWidgetMap[id].props.y = Math.round(e.lastEvent.translate[1])
+  }
+}
+// 渲染节点缩放
+const onResize = (e) => {
+  e.target.style.width = `${e.width}px`
+  e.target.style.height = `${e.height}px`
+  e.target.style.transform = e.drag.transform
+}
+// 节点缩放完成
+const onResizeEnd = (e) => {
+  const id = e.target.attributes['widget-id']?.value
+  if (e.lastEvent && id && projectStore.activeWidgetMap[id]) {
+    projectStore.activeWidgetMap[id].props.width = Math.round(e.lastEvent.width)
+    projectStore.activeWidgetMap[id].props.height = Math.round(e.lastEvent.height)
+    // 设置位置
+    if (e.lastEvent.drag?.translate) {
+      projectStore.activeWidgetMap[id].props.x = Math.round(e.lastEvent.drag.translate[0])
+      projectStore.activeWidgetMap[id].props.y = Math.round(e.lastEvent.drag.translate[1])
+    }
+  }
+}
+// 渲染节点事件
+const onRender = (e) => {
+  e.target.style.cssText += e.cssText
+}
+// 节点组拖拽
+const onDragGroup = ({ events }) => {
+  events.forEach((ev) => {
+    ev.target.style.transform = ev.transform
+  })
+}
+// 节点组缩放
+const onResizeGroup = ({ events }) => {
+  events.forEach((ev) => {
+    ev.target.style.width = `${ev.width}px`
+    ev.target.style.height = `${ev.height}px`
+    ev.target.style.transform = ev.drag.transform
+  })
+}
+// 节点组拖拽结束
+const onDragGroupEnd = ({ events }) => {
+  events.forEach((ev) => {
+    const id = ev.target.attributes['widget-id']?.value
+    if (id && projectStore.activeWidgetMap[id] && ev?.lastEvent?.translate) {
+      projectStore.activeWidgetMap[id].props.x = Math.round(ev.lastEvent.translate[0])
+      projectStore.activeWidgetMap[id].props.y = Math.round(ev.lastEvent.translate[1])
+    }
+  })
+}
+// 节点组缩放结束
+const onResizeGroupEnd = ({ events }) => {
+  events.forEach((ev) => {
+    const id = ev.target.attributes['widget-id']?.value
+    if (ev.lastEvent && id && projectStore.activeWidgetMap[id]) {
+      projectStore.activeWidgetMap[id].props.width = Math.round(ev.lastEvent.width)
+      projectStore.activeWidgetMap[id].props.height = Math.round(ev.lastEvent.height)
+      // 设置位置
+      if (ev.lastEvent.drag?.translate) {
+        projectStore.activeWidgetMap[id].props.x = Math.round(ev.lastEvent.drag.translate[0])
+        projectStore.activeWidgetMap[id].props.y = Math.round(ev.lastEvent.drag.translate[1])
+      }
+    }
+  })
+}
+</script>
+
+<style scoped></style>

+ 15 - 131
src/renderer/src/views/designer/workspace/stage/Node.vue

@@ -4,12 +4,17 @@
     :style="getStyle"
     :style="getStyle"
     :class="schema.type === 'page' ? '' : 'ignore-click widget-node'"
     :class="schema.type === 'page' ? '' : 'ignore-click widget-node'"
     :widget-id="schema.id"
     :widget-id="schema.id"
-    @click.stop="handleSelect"
+    @click="handleSelect"
     @contextmenu.stop="handleContextmenu"
     @contextmenu.stop="handleContextmenu"
     v-if="!schema.hidden"
     v-if="!schema.hidden"
   >
   >
     <!-- 控件 -->
     <!-- 控件 -->
-    <component :is="widget" v-bind="schema.props" :styles="schema.style" />
+    <component
+      :is="widget"
+      v-bind="schema.props"
+      :styles="schema.style"
+      style="width: 100%; height: 100%"
+    />
     <!-- 子节点 -->
     <!-- 子节点 -->
     <NodeItem
     <NodeItem
       v-if="schema.children"
       v-if="schema.children"
@@ -20,47 +25,6 @@
       :rootContainer="nodeRef!"
       :rootContainer="nodeRef!"
     />
     />
   </div>
   </div>
-  <!-- 拖拽、缩放、吸附 -->
-  <div v-show="schema.type !== 'page' && selected">
-    <Moveable
-      :target="nodeRef"
-      :draggable="selected && !schema.locked"
-      :resizable="selected && !schema.locked"
-      :padding="2"
-      :container="rootContainer"
-      :snappable="true"
-      :useMutationObserver="true"
-      :useResizeObserver="true"
-      :throttleDrag="1"
-      :throttleResize="1"
-      :snapDirections="{
-        top: true,
-        left: true,
-        bottom: true,
-        right: true,
-        center: true,
-        middle: true
-      }"
-      :elementSnapDirections="{
-        top: true,
-        left: true,
-        bottom: true,
-        right: true,
-        center: true,
-        middle: true
-      }"
-      :maxSnapElementGuidelineDistance="100"
-      :elementGuidelines="elementGridelines"
-      :verticalGuidelines="verticalGuidelines"
-      :horizontalGuidelines="horizontalGuidelines"
-      :controlPadding="4"
-      @render="onRender"
-      @drag="onDrag"
-      @resize="onResize"
-      @dragStart="onDragStart"
-      @dragEnd="onDragEnd"
-    />
-  </div>
   <!-- 右键菜单 -->
   <!-- 右键菜单 -->
   <ContentMenu
   <ContentMenu
     ref="contentMenuRef"
     ref="contentMenuRef"
@@ -80,9 +44,7 @@ import { useDrop } from 'vue-hooks-plus'
 import { createWidget } from '@/model'
 import { createWidget } from '@/model'
 import LvglWidgets from '@/lvgl-widgets'
 import LvglWidgets from '@/lvgl-widgets'
 import { useProjectStore } from '@/store/modules/project'
 import { useProjectStore } from '@/store/modules/project'
-import { useMutationObserver } from '@vueuse/core'
 
 
-import Moveable from 'vue3-moveable'
 import ContentMenu from './ContentMenu.vue'
 import ContentMenu from './ContentMenu.vue'
 import { getAddWidgetIndex } from '@/utils'
 import { getAddWidgetIndex } from '@/utils'
 
 
@@ -92,7 +54,6 @@ defineOptions({
 
 
 const page = inject<Page>('page')
 const page = inject<Page>('page')
 const pageState = inject<StageState>('state')
 const pageState = inject<StageState>('state')
-const pageEl = inject<HTMLElement>('pageEl')
 
 
 const props = defineProps<{
 const props = defineProps<{
   // 父级容器 拖拽缩放设置
   // 父级容器 拖拽缩放设置
@@ -112,10 +73,7 @@ const widget = computed(() => LvglWidgets[props.schema.type]?.component)
 const nodeRef = ref<HTMLDivElement>()
 const nodeRef = ref<HTMLDivElement>()
 // 当前层级
 // 当前层级
 const zIndex = ref(props.zIndex)
 const zIndex = ref(props.zIndex)
-// 判断当前节点是否选中
-const selected = computed(() =>
-  projectStore.activeWidgets.map((item) => item.id).includes(props.schema.id)
-)
+
 // 放置效果样式
 // 放置效果样式
 const dropStyle = ref<CSSProperties>({})
 const dropStyle = ref<CSSProperties>({})
 
 
@@ -124,7 +82,8 @@ const getStyle = computed((): CSSProperties => {
   const { style = {}, schema } = props
   const { style = {}, schema } = props
 
 
   const other: CSSProperties = {}
   const other: CSSProperties = {}
-  if (pageState?.showBorder) {
+  // 显示所有控件边框
+  if (pageState?.showBorder && props.schema.type !== 'page') {
     other.border = '1px dashed #000'
     other.border = '1px dashed #000'
     other.transform = `translate(${schema.props.x - 1}px, ${schema.props.y - 1}px)`
     other.transform = `translate(${schema.props.x - 1}px, ${schema.props.y - 1}px)`
   }
   }
@@ -143,50 +102,6 @@ const getStyle = computed((): CSSProperties => {
   }
   }
 })
 })
 
 
-// 吸附辅助线
-const elementGridelines = ref<Element[]>([])
-// 垂直辅助线
-const verticalGuidelines = computed(() => {
-  return (
-    (pageState?.showReferenceLine &&
-      projectStore.activePage?.referenceLine
-        .filter((item) => item.type === 'horizontal')
-        .map((item) => item.value)) ||
-    []
-  )
-})
-// 水平辅助线
-const horizontalGuidelines = computed(() => {
-  return (
-    (pageState?.showReferenceLine &&
-      projectStore.activePage?.referenceLine
-        .filter((item) => item.type === 'vertical')
-        .map((item) => item.value)) ||
-    []
-  )
-})
-
-// 监听页面节点变化 设置元素辅助线
-useMutationObserver(
-  pageEl,
-  (mutations) => {
-    mutations.forEach(() => {
-      const els = document.querySelectorAll('.widget-node')
-      elementGridelines.value = []
-      els.forEach((el) => {
-        // 排除自己
-        if (el.attributes['widget-id']?.value !== props.schema.id) {
-          elementGridelines.value.push(el)
-        }
-      })
-    })
-  },
-  {
-    childList: true,
-    subtree: true
-  }
-)
-
 // 拖拽放置事件处理
 // 拖拽放置事件处理
 useDrop(nodeRef, {
 useDrop(nodeRef, {
   // 元素放置
   // 元素放置
@@ -217,54 +132,22 @@ useDrop(nodeRef, {
 })
 })
 
 
 // 选择节点
 // 选择节点
-const handleSelect = (e) => {
+const handleSelect = (e: MouseEvent) => {
   if (props.schema.type !== 'page') {
   if (props.schema.type !== 'page') {
+    e.stopPropagation()
     // 判断当前是否按住ctrl
     // 判断当前是否按住ctrl
     if (e.ctrlKey) {
     if (e.ctrlKey) {
       projectStore.activeWidgets.push(props.schema as BaseWidget)
       projectStore.activeWidgets.push(props.schema as BaseWidget)
     } else {
     } else {
       projectStore.setSelectWidgets([props.schema as BaseWidget])
       projectStore.setSelectWidgets([props.schema as BaseWidget])
     }
     }
+  } else {
+    projectStore.setSelectWidgets([])
   }
   }
   // 关闭右键菜单
   // 关闭右键菜单
   contentMenuRef.value?.handleClose()
   contentMenuRef.value?.handleClose()
 }
 }
 
 
-// 拖拽开始
-const onDragStart = () => {
-  zIndex.value = 999
-}
-
-// 拖拽结束
-const onDragEnd = () => {
-  zIndex.value = props.zIndex
-}
-
-// 渲染节点拖拽
-const onDrag = (e) => {
-  // 当前选中节点整体移动
-  projectStore.activeWidgets.forEach((item) => {
-    item.props.x += e.beforeDelta[0]
-    item.props.y += e.beforeDelta[1]
-  })
-}
-
-// 渲染节点缩放
-const onResize = (e) => {
-  props.schema.props.width = e.width
-  props.schema.props.height = e.height
-  if (e.drag?.beforeTranslate) {
-    props.schema.props.x = e.drag.beforeTranslate[0]
-    props.schema.props.y = e.drag.beforeTranslate[1]
-  }
-  e.target.style.cssText += e.cssText
-}
-
-// 渲染节点事件
-const onRender = (e) => {
-  e.target.style.cssText += e.cssText
-}
-
 /******************************右键菜单*********************************/
 /******************************右键菜单*********************************/
 const position = ref({
 const position = ref({
   top: 0,
   top: 0,
@@ -279,6 +162,7 @@ const triggerRef = ref({
   getBoundingClientRect: () => position.value
   getBoundingClientRect: () => position.value
 })
 })
 
 
+// 处理右键菜单
 const handleContextmenu = (event: MouseEvent) => {
 const handleContextmenu = (event: MouseEvent) => {
   const { clientX, clientY } = event
   const { clientX, clientY } = event
   position.value = DOMRect.fromRect({
   position.value = DOMRect.fromRect({

+ 4 - 2
src/renderer/src/views/designer/workspace/stage/index.vue

@@ -175,7 +175,9 @@ const handleMouseDown = (e: MouseEvent) => {
     target?.closest('.refer-line-img') ||
     target?.closest('.refer-line-img') ||
     target?.closest('.workspace-bottom') ||
     target?.closest('.workspace-bottom') ||
     target?.closest('.refer-line') ||
     target?.closest('.refer-line') ||
-    target?.closest('.ignore-click')
+    target?.closest('.ignore-click') ||
+    target?.closest('.moveable-area') ||
+    target?.closest('.moveable-control')
   ) {
   ) {
     return
     return
   }
   }
@@ -208,7 +210,7 @@ const handleMouseMove = throttle((e: MouseEvent) => {
 }, 50)
 }, 50)
 /* 鼠标抬起 */
 /* 鼠标抬起 */
 const handleMouseUp = (e: MouseEvent) => {
 const handleMouseUp = (e: MouseEvent) => {
-  projectStore.setSelectWidgets([])
+  // projectStore.setSelectWidgets([])
   if (!isMouseDown || (startX === 0 && startY === 0)) return
   if (!isMouseDown || (startX === 0 && startY === 0)) return
 
 
   isMouseDown.value = false
   isMouseDown.value = false