فهرست منبع

feat: 添加解绑功能

jiaxing.liao 1 ماه پیش
والد
کامیت
562cc5ab16

+ 116 - 1
src/renderer/src/store/modules/action.ts

@@ -12,6 +12,7 @@ import { useAppStore } from './app'
 import { debounce } from 'lodash-es'
 
 import type { BaseWidget } from '@/types/baseWidget'
+import type { Page } from '@/types/page'
 
 type AlignType =
   | 'left'
@@ -29,6 +30,77 @@ export const useActionStore = defineStore('action', () => {
   const appStore = useAppStore()
   const clipboard = ref<BaseWidget[]>()
 
+  const findWidgetLocation = (
+    parent: Page | BaseWidget,
+    widgetId: string,
+    ancestors: Array<Page | BaseWidget> = [parent]
+  ):
+    | {
+        widget: BaseWidget
+        parent: Page | BaseWidget
+        list: BaseWidget[]
+        index: number
+        ancestors: Array<Page | BaseWidget>
+      }
+    | undefined => {
+    const children = parent.children || []
+    for (let index = 0; index < children.length; index++) {
+      const widget = children[index]
+      if (widget.id === widgetId) {
+        return {
+          widget,
+          parent,
+          list: children,
+          index,
+          ancestors
+        }
+      }
+
+      if (widget.children?.length) {
+        const found = findWidgetLocation(widget, widgetId, [...ancestors, widget])
+        if (found) {
+          return found
+        }
+      }
+    }
+  }
+
+  const findWidgetLocationInProject = (widgetId: string) => {
+    for (const screen of projectStore.project?.screens || []) {
+      for (const page of screen.pages) {
+        const location = findWidgetLocation(page, widgetId)
+        if (location) {
+          return {
+            ...location,
+            page,
+            screen
+          }
+        }
+      }
+    }
+  }
+
+  const getUnbindOffset = (ancestors: Array<Page | BaseWidget>) => {
+    return ancestors.reduce(
+      (acc, item) => {
+        if ('props' in item) {
+          acc.x += Number(item.props?.x || 0)
+          acc.y += Number(item.props?.y || 0)
+        }
+        return acc
+      },
+      {
+        x: 0,
+        y: 0
+      }
+    )
+  }
+
+  const isWidgetBoundById = (widgetId: string) => {
+    const location = findWidgetLocationInProject(widgetId)
+    return !!location && location.parent.type !== 'page'
+  }
+
   /**
    * 对齐组件
    * @param type 对齐类型
@@ -375,6 +447,47 @@ export const useActionStore = defineStore('action', () => {
     projectStore.setSelectWidgets(projectStore.activePage?.children || [])
   }
 
+  /**
+   * 解绑控件,将其从容器内部释放到页面根节点末尾
+   */
+  const onUnbind = (widgetIds = projectStore.activeWidgets.map((widget) => widget.id)) => {
+    if (!widgetIds.length) return
+
+    const movedWidgets: BaseWidget[] = []
+    let targetPageId = ''
+    let targetScreenId = ''
+
+    widgetIds.forEach((widgetId) => {
+      const location = findWidgetLocationInProject(widgetId)
+      if (!location || location.parent.type === 'page') return
+
+      const { widget, list, index, ancestors, page, screen } = location
+      const offset = getUnbindOffset(ancestors.slice(1))
+
+      list.splice(index, 1)
+      widget.props.x = Number(widget.props?.x || 0) + offset.x
+      widget.props.y = Number(widget.props?.y || 0) + offset.y
+      page.children.push(widget)
+      movedWidgets.push(widget)
+      targetPageId = page.id
+      targetScreenId = screen.id
+    })
+
+    if (movedWidgets.length) {
+      if (targetPageId) {
+        projectStore.activePageId = targetPageId
+      }
+      if (targetScreenId && targetPageId) {
+        const screenIndex =
+          projectStore.project?.screens.findIndex((screen) => screen.id === targetScreenId) ?? -1
+        if (screenIndex !== -1) {
+          projectStore.openPageIds[screenIndex] = targetPageId
+        }
+      }
+      projectStore.setSelectWidgets(movedWidgets)
+    }
+  }
+
   /**
    * 显示事件
    */
@@ -569,8 +682,10 @@ export const useActionStore = defineStore('action', () => {
     onLock,
     onPaste,
     onCut,
+    onUnbind,
     onHidden,
     onShowEvent,
-    onSelectAll
+    onSelectAll,
+    isWidgetBoundById
   }
 })

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

@@ -136,7 +136,6 @@ const setOpenedPage = (screenId?: string, pageId?: string) => {
 }
 
 const handleNodeClick = (nodeData: HierarchyNodeData, _node: unknown, e?: MouseEvent) => {
-  console.log(1111, nodeData)
   if (nodeData.nodeType === 'page') {
     projectStore.activePageId = nodeData.id
     setOpenedPage(nodeData.screenId, nodeData.id)

+ 7 - 7
src/renderer/src/views/designer/sidebar/components/PageTreeItem.vue

@@ -15,10 +15,10 @@
       class="flex items-center gap-4px pr-12px invisible group-hover/item:visible"
     >
       <el-tooltip v-if="isBind" content="解除绑定">
-        <span @click="unbind"><LuTrash2 size="14px" /></span>
+        <span @click.capture.stop="unbind"><LuUnlink size="14px" /></span>
       </el-tooltip>
       <el-tooltip v-if="data.type !== 'page'" content="删除">
-        <span @click="deleteWidget(data)"><LuTrash2 size="14px" /></span>
+        <span @click.capture.stop="deleteWidget(data)"><LuTrash2 size="14px" /></span>
       </el-tooltip>
       <el-tooltip content="隐藏/显示">
         <span @click.capture.stop="data.hidden = !data.hidden">
@@ -48,12 +48,13 @@ import {
   LuEye,
   LuEyeOff,
   LuPanelsTopLeft,
-  LuBox
+  LuBox,
+  LuUnlink
 } from 'vue-icons-plus/lu'
 import { useProjectStore } from '@/store/modules/project'
 import { useActionStore } from '@/store/modules/action'
 
-defineProps<{
+const props = defineProps<{
   node: RenderContentContext['node']
   data: BaseWidget
 }>()
@@ -62,8 +63,7 @@ const projectStore = useProjectStore()
 const actionStore = useActionStore()
 
 const isBind = computed(() => {
-  // todo
-  return true
+  return actionStore.isWidgetBoundById(props.data.id)
 })
 
 // 删除控件
@@ -73,7 +73,7 @@ const deleteWidget = (data: BaseWidget) => {
 
 // 解除绑定
 const unbind = () => {
-  // todo
+  actionStore.onUnbind([props.data.id])
 }
 </script>
 

+ 147 - 118
src/renderer/src/views/designer/workspace/stage/ContextMenu.vue

@@ -15,7 +15,8 @@
     <template #dropdown>
       <el-dropdown-menu>
         <el-dropdown-item
-          v-for="item in widgetType === 'page' ? pageMenus : widgetMenus"
+          v-for="item in currentMenus"
+          :key="item.value"
           :icon="item.icon"
           :divided="item.divider"
           @click="item.onclick"
@@ -33,27 +34,37 @@
 <script setup lang="ts">
 import type { DropdownInstance } from 'element-plus'
 
-import { onBeforeUnmount, ref } from 'vue'
+import { computed, onBeforeUnmount, ref } from 'vue'
+import { useEventBus } from '@vueuse/core'
 import {
+  LuArrowDown,
+  LuArrowDownToLine,
+  LuArrowUp,
+  LuArrowUpToLine,
+  LuClipboard,
   LuCopy,
   LuCopyPlus,
-  LuScissors,
-  LuClipboard,
-  LuTrash2,
   LuEyeOff,
   LuLock,
+  LuScissors,
+  LuStar,
+  LuTrash2,
   LuUnlock,
-  LuArrowUpToLine,
-  LuArrowDownToLine,
-  LuArrowUp,
-  LuArrowDown,
   LuZap,
-  LuStar
+  LuUnlink
 } from 'vue-icons-plus/lu'
 
-import { useEventBus } from '@vueuse/core'
 import { useActionStore } from '@/store/modules/action'
 
+type MenuItem = {
+  label: string
+  value: string
+  fastKey: string
+  icon: any
+  divider?: boolean
+  onclick: () => void
+}
+
 const props = defineProps<{
   id: string
   virtualRef: any
@@ -75,119 +86,133 @@ onBeforeUnmount(() => {
   unsubscribe()
 })
 
-/**
- * 创建自定义组件
- */
 const createCustomComponent = () => {
   createCustomCompBus.emit('create', props.id)
 }
 
-const widgetMenus = [
-  {
-    label: '复制',
-    value: 'copy',
-    fastKey: 'Ctrl+C',
-    icon: LuCopy,
-    onclick: () => actionStore.onCopy()
-  },
-  {
-    label: '复用',
-    value: 'duplicate',
-    fastKey: 'Ctrl+D',
-    icon: LuCopyPlus,
-    onclick: () => actionStore.onCopyFrom()
-  },
-  {
-    label: '剪切',
-    value: 'cut',
-    fastKey: 'Ctrl+X',
-    icon: LuScissors,
-    onclick: () => actionStore.onCut()
-  },
-  {
-    label: '粘贴',
-    value: 'paste',
-    fastKey: 'Ctrl+V',
-    icon: LuClipboard,
-    onclick: () => actionStore.onPaste()
-  },
-  {
-    label: '删除',
-    value: 'delete',
-    fastKey: 'Delete/Backspace',
-    icon: LuTrash2,
-    divider: true,
-    onclick: () => actionStore.onDelete()
-  },
-  {
-    label: '隐藏',
-    value: 'hidden',
-    fastKey: 'Ctrl+H',
-    icon: LuEyeOff,
-    onclick: () => actionStore.onHidden(true)
-  },
-  {
-    label: '锁定',
-    value: 'locked',
-    fastKey: 'Ctrl+L',
-    icon: LuLock,
-    onclick: () => actionStore.onLock(true)
-  },
-  {
-    label: '解锁',
-    value: 'unlock',
-    fastKey: 'Ctrl+Shift+L',
-    icon: LuUnlock,
-    onclick: () => actionStore.onLock(false)
-  },
-  {
-    label: '上移一层',
-    value: 'up',
-    fastKey: 'Ctrl+Up',
-    icon: LuArrowUp,
-    divider: true,
-    onclick: () => actionStore.onLevel('up')
-  },
-  {
-    label: '下移一层',
-    value: 'down',
-    fastKey: 'Ctrl+Down',
-    icon: LuArrowDown,
-    onclick: () => actionStore.onLevel('down')
-  },
-  {
-    label: '置顶',
-    value: 'top',
-    fastKey: 'Ctrl+Shift+Up',
-    icon: LuArrowUpToLine,
-    onclick: () => actionStore.onLevel('top')
-  },
-  {
-    label: '置底',
-    value: 'bottom',
-    fastKey: 'Ctrl+Shift+Down',
-    icon: LuArrowDownToLine,
-    onclick: () => actionStore.onLevel('bottom')
-  },
-  {
-    label: '添加事件',
-    value: 'event',
-    fastKey: '',
-    icon: LuZap,
-    divider: true,
-    onclick: () => actionStore.onShowEvent()
-  },
-  {
-    label: '创建自定义组件',
-    value: 'custom-component',
-    fastKey: '',
-    icon: LuStar,
-    divider: true,
-    onclick: createCustomComponent
+const widgetMenus = computed<MenuItem[]>(() => {
+  const menus: MenuItem[] = [
+    {
+      label: '复制',
+      value: 'copy',
+      fastKey: 'Ctrl+C',
+      icon: LuCopy,
+      onclick: () => actionStore.onCopy()
+    },
+    {
+      label: '复用',
+      value: 'duplicate',
+      fastKey: 'Ctrl+D',
+      icon: LuCopyPlus,
+      onclick: () => actionStore.onCopyFrom()
+    },
+    {
+      label: '剪切',
+      value: 'cut',
+      fastKey: 'Ctrl+X',
+      icon: LuScissors,
+      onclick: () => actionStore.onCut()
+    },
+    {
+      label: '粘贴',
+      value: 'paste',
+      fastKey: 'Ctrl+V',
+      icon: LuClipboard,
+      onclick: () => actionStore.onPaste()
+    },
+    {
+      label: '删除',
+      value: 'delete',
+      fastKey: 'Delete/Backspace',
+      icon: LuTrash2,
+      divider: true,
+      onclick: () => actionStore.onDelete()
+    }
+  ]
+
+  if (actionStore.isWidgetBoundById(props.id)) {
+    menus.push({
+      label: '解绑',
+      value: 'unbind',
+      fastKey: '',
+      icon: LuUnlink,
+      onclick: () => actionStore.onUnbind([props.id])
+    })
   }
-]
 
-const pageMenus = [
+  menus.push(
+    {
+      label: '隐藏',
+      value: 'hidden',
+      fastKey: 'Ctrl+H',
+      icon: LuEyeOff,
+      onclick: () => actionStore.onHidden(true)
+    },
+    {
+      label: '锁定',
+      value: 'locked',
+      fastKey: 'Ctrl+L',
+      icon: LuLock,
+      onclick: () => actionStore.onLock(true)
+    },
+    {
+      label: '解锁',
+      value: 'unlock',
+      fastKey: 'Ctrl+Shift+L',
+      icon: LuUnlock,
+      onclick: () => actionStore.onLock(false)
+    },
+    {
+      label: '上移一层',
+      value: 'up',
+      fastKey: 'Ctrl+Up',
+      icon: LuArrowUp,
+      divider: true,
+      onclick: () => actionStore.onLevel('up')
+    },
+    {
+      label: '下移一层',
+      value: 'down',
+      fastKey: 'Ctrl+Down',
+      icon: LuArrowDown,
+      onclick: () => actionStore.onLevel('down')
+    },
+    {
+      label: '置顶',
+      value: 'top',
+      fastKey: 'Ctrl+Shift+Up',
+      icon: LuArrowUpToLine,
+      onclick: () => actionStore.onLevel('top')
+    },
+    {
+      label: '置底',
+      value: 'bottom',
+      fastKey: 'Ctrl+Shift+Down',
+      icon: LuArrowDownToLine,
+      onclick: () => actionStore.onLevel('bottom')
+    },
+    {
+      label: '添加事件',
+      value: 'event',
+      fastKey: '',
+      icon: LuZap,
+      divider: true,
+      onclick: () => actionStore.onShowEvent()
+    },
+    {
+      label: '创建自定义组件',
+      value: 'custom-component',
+      fastKey: '',
+      icon: LuStar,
+      divider: true,
+      onclick: createCustomComponent
+    }
+  )
+
+  return menus
+})
+
+const pageMenus: MenuItem[] = [
   {
     label: '粘贴',
     value: 'paste',
@@ -226,6 +251,10 @@ const pageMenus = [
   }
 ]
 
+const currentMenus = computed(() => {
+  return props.widgetType === 'page' ? pageMenus : widgetMenus.value
+})
+
 defineExpose({
   handleClose: () => {
     dropdownRef.value?.handleClose()