jiaxing.liao преди 1 месец
родител
ревизия
607abf2a2f

+ 241 - 65
src/renderer/src/views/designer/sidebar/Hierarchy.vue

@@ -1,107 +1,283 @@
 <template>
-  <el-tabs v-model="activeMenu" stretch class="w-full h-full">
-    <el-tab-pane label="屏幕" name="screen" class="flex-1">
-      <!-- 屏幕层 -->
-      <el-tree
-        ref="screenTreeRef"
-        style="max-width: 600px"
-        default-expand-all
-        node-key="id"
-        :data="projectStore.project?.screens"
-        :props="{ label: 'name', children: 'pages' }"
-        @node-click="handlePageNodeClick"
-      >
-        <template #default="{ node, data }">
-          <ScreenTreeItem :node="node" :data="data" />
-        </template>
-      </el-tree>
-    </el-tab-pane>
-    <el-tab-pane label="页面" name="page" class="w-full h-full">
-      <!-- 页面层 -->
-      <div class="h-full overflow-hidden" ref="pageBoxRef">
+  <div class="w-full h-full flex flex-col gap-8px">
+    <div class="p-8px">
+      <el-input v-model="search" clearable :prefix-icon="Search" placeholder="搜索" />
+    </div>
+
+    <div class="flex-1 overflow-hidden" ref="treeBoxRef">
+      <el-scrollbar class="h-full">
         <el-tree
-          ref="pageTreeRef"
+          ref="hierarchyTreeRef"
           style="max-width: 600px"
           default-expand-all
           node-key="id"
           draggable
           :allow-drag="allowDrag"
           :allow-drop="allowDrop"
-          :height="height || 100"
+          :height="treeHeight"
           :highlight-current="false"
-          :default-expanded-keys="projectStore.activePageId ? [projectStore.activePageId] : []"
-          :data="projectStore.activePage ? [projectStore.activePage] : []"
+          :data="hierarchyTreeData"
           :props="{ label: 'name', children: 'children' }"
-          @node-click="handleWidgetNodeClick"
+          :filter-node-method="filterNode"
+          @node-click="handleNodeClick"
+          @node-drop="handleNodeDrop"
         >
           <template #default="{ node, data }">
-            <PageTreeItem :node="node" :data="data" />
+            <ScreenTreeItem
+              v-if="isScreenOrPageNode(data)"
+              :node="node"
+              :data="getScreenOrPageRawNode(data)"
+            />
+            <PageTreeItem v-else :node="node" :data="getWidgetRawNode(data)" />
           </template>
         </el-tree>
-      </div>
-    </el-tab-pane>
-  </el-tabs>
+      </el-scrollbar>
+    </div>
+  </div>
 </template>
 
 <script setup lang="ts">
-import { ref } from 'vue'
+import { computed, ref, watch } from 'vue'
+import { Search } from '@element-plus/icons-vue'
+import { useElementSize } from '@vueuse/core'
 
 import ScreenTreeItem from './components/ScreenTreeItem.vue'
 import PageTreeItem from './components/PageTreeItem.vue'
 import { useProjectStore } from '@/store/modules/project'
-import { useElementSize } from '@vueuse/core'
 
-import type { AllowDragFunction, AllowDropFunction } from 'element-plus'
+import type { BaseWidget } from '@/types/baseWidget'
+import type { Page } from '@/types/page'
+import type { Screen } from '@/types/screen'
+import type {
+  AllowDragFunction,
+  AllowDropFunction,
+  FilterNodeMethodFunction,
+  NodeDropType,
+  TreeInstance,
+  TreeNodeData
+} from 'element-plus'
+
+type HierarchyNodeData = {
+  id: string
+  name: string
+  nodeType: 'screen' | 'page' | 'widget'
+  raw: Screen | Page | BaseWidget
+  screenId: string
+  pageId?: string
+  children: HierarchyNodeData[]
+}
 
 const projectStore = useProjectStore()
-const activeMenu = ref<string>('screen')
-const pageBoxRef = ref<HTMLDivElement>()
+const search = ref('')
+const hierarchyTreeRef = ref<TreeInstance | null>(null)
+const treeBoxRef = ref<HTMLDivElement | null>(null)
 
-const { height } = useElementSize(pageBoxRef, {
+const { height } = useElementSize(treeBoxRef, {
   height: 100,
   width: 100
 })
+const treeHeight = computed(() => Math.max(height.value || 0, 100))
+
+const getHierarchyNode = (data: TreeNodeData): HierarchyNodeData => data as HierarchyNodeData
+const getScreenOrPageRawNode = (data: TreeNodeData): Screen | Page =>
+  getHierarchyNode(data).raw as Screen | Page
+const getWidgetRawNode = (data: TreeNodeData): BaseWidget =>
+  getHierarchyNode(data).raw as BaseWidget
+const isScreenOrPageNode = (data: TreeNodeData) => {
+  const node = getHierarchyNode(data)
+  return node.nodeType === 'screen' || node.nodeType === 'page'
+}
+
+const buildWidgetNodes = (
+  widgets: BaseWidget[] = [],
+  screenId: string,
+  pageId: string
+): HierarchyNodeData[] => {
+  return widgets.map((widget) => ({
+    id: widget.id,
+    name: widget.name,
+    nodeType: 'widget',
+    raw: widget,
+    screenId,
+    pageId,
+    children: buildWidgetNodes(widget.children || [], screenId, pageId)
+  }))
+}
+
+const hierarchyTreeData = computed<HierarchyNodeData[]>(() => {
+  return (
+    projectStore.project?.screens.map((screen) => ({
+      id: screen.id,
+      name: screen.name,
+      nodeType: 'screen',
+      raw: screen,
+      screenId: screen.id,
+      children: screen.pages.map((page) => ({
+        id: page.id,
+        name: page.name,
+        nodeType: 'page',
+        raw: page,
+        screenId: screen.id,
+        pageId: page.id,
+        children: buildWidgetNodes(page.children || [], screen.id, page.id)
+      }))
+    })) || []
+  )
+})
+
+const setOpenedPage = (screenId?: string, pageId?: string) => {
+  if (!screenId || !pageId) return
 
-const handlePageNodeClick = (node: any) => {
-  if (node.type === 'page') {
-    projectStore.activePageId = node.id
+  const screenIndex =
+    projectStore.project?.screens.findIndex((screen) => screen.id === screenId) ?? -1
+  if (screenIndex !== -1) {
+    projectStore.openPageIds[screenIndex] = pageId
   }
-  // 当前屏幕打开的页面
-  projectStore.project?.screens.forEach((screen, index) => {
-    screen.pages.forEach((page) => {
-      if (page.id === node.id) {
-        projectStore.openPageIds[index] = page.id
-      }
-    })
-  })
 }
 
-const handleWidgetNodeClick = (nodeData, node, e) => {
-  if (nodeData.type !== 'page' && nodeData?.id) {
-    if (e.ctrlKey) {
-      projectStore.activeWidgets.push(nodeData)
-    } else {
-      projectStore.setSelectWidgets([nodeData])
-    }
+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)
+    projectStore.setSelectWidgets([])
+    return
   }
-  const parent = node.parent?.data
-  if (!nodeData?.id && parent) {
-    if (e.ctrlKey) {
-      projectStore.activeWidgets.push(parent)
-    } else {
-      projectStore.setSelectWidgets([parent])
-    }
+
+  if (nodeData.nodeType !== 'widget' || !e || !nodeData?.id) return
+
+  const pageChanged = !!nodeData.pageId && nodeData.pageId !== projectStore.activePageId
+  if (nodeData.pageId) {
+    projectStore.activePageId = nodeData.pageId
+    setOpenedPage(nodeData.screenId, nodeData.pageId)
+  }
+
+  if (e.ctrlKey && !pageChanged) {
+    projectStore.activeWidgets.push(nodeData.raw as BaseWidget)
+  } else {
+    projectStore.setSelectWidgets([nodeData.raw as BaseWidget])
   }
 }
 
+const matchesKeyword = (keyword: string, node: HierarchyNodeData) => {
+  const currentName = String(node.raw.name || '').toLowerCase()
+  if (currentName.includes(keyword)) return true
+
+  return node.children.some((child) => matchesKeyword(keyword, child))
+}
+
+const filterNode: FilterNodeMethodFunction = (value, data) => {
+  const keyword = String(value || '')
+    .trim()
+    .toLowerCase()
+  if (!keyword) return true
+
+  return matchesKeyword(keyword, getHierarchyNode(data))
+}
+
+watch(search, (value) => {
+  hierarchyTreeRef.value?.filter(value.trim())
+})
+
+const isDescendantNode = (node: HierarchyNodeData, childId: string) => {
+  return node.children.some((item) => item.id === childId || isDescendantNode(item, childId))
+}
+
 const allowDrag: AllowDragFunction = (node) => {
-  // 禁用拖拽的节点
-  const notallow = ['page']
+  const currentNode = getHierarchyNode(node.data)
+  return (
+    currentNode.nodeType === 'widget' &&
+    !!currentNode.pageId &&
+    currentNode.pageId === projectStore.activePageId
+  )
+}
+
+const allowDrop: AllowDropFunction = (dragNode, dropNode, type) => {
+  const dragData = getHierarchyNode(dragNode.data)
+  const dropData = getHierarchyNode(dropNode.data)
 
-  return !notallow.includes(node.data?.type)
+  if (dragData.nodeType !== 'widget' || dropData.nodeType === 'screen') return false
+  if (!dragData.pageId || !dropData.pageId || dragData.pageId !== dropData.pageId) return false
+  if (dragData.id === dropData.id || isDescendantNode(dragData, dropData.id)) return false
+
+  if (dropData.nodeType === 'page') {
+    return type === 'inner'
+  }
+
+  if (type === 'inner') {
+    return !!(dropData.raw as BaseWidget).children
+  }
+
+  return true
+}
+
+const findPageById = (pageId?: string) => {
+  if (!pageId) return
+
+  return projectStore.project?.screens
+    .flatMap((screen) => screen.pages)
+    .find((page) => page.id === pageId)
 }
 
-const allowDrop: AllowDropFunction = (_dragNode, dropNode) => {
-  return dropNode.data?.children
+const findWidgetListById = (widgets: BaseWidget[], widgetId: string): BaseWidget[] | undefined => {
+  for (const widget of widgets) {
+    if (widget.id === widgetId) {
+      return widgets
+    }
+
+    if (widget.children?.length) {
+      const found = findWidgetListById(widget.children, widgetId)
+      if (found) {
+        return found
+      }
+    }
+  }
+}
+
+const takeWidgetFromPage = (page: Page, widgetId: string) => {
+  const list = findWidgetListById(page.children || [], widgetId)
+  if (!list) return
+
+  const index = list.findIndex((widget) => widget.id === widgetId)
+  if (index === -1) return
+
+  return list.splice(index, 1)[0]
+}
+
+type TreeDragNode = {
+  data: TreeNodeData
+}
+
+const handleNodeDrop = (dragNode: TreeDragNode, dropNode: TreeDragNode, dropType: NodeDropType) => {
+  const dragData = getHierarchyNode(dragNode.data)
+  const targetData = getHierarchyNode(dropNode.data)
+
+  if (dropType === 'none') return
+
+  if (dragData.nodeType !== 'widget' || !dragData.pageId || dragData.pageId !== targetData.pageId) {
+    return
+  }
+
+  const page = findPageById(dragData.pageId)
+  if (!page) return
+
+  const widget = takeWidgetFromPage(page, dragData.id)
+  if (!widget) return
+
+  if (dropType === 'inner') {
+    const target = targetData.raw as Page | BaseWidget
+    target.children = target.children || []
+    target.children.push(widget)
+  } else {
+    const targetList = findWidgetListById(page.children || [], targetData.id)
+    if (!targetList) return
+
+    const index = targetList.findIndex((item) => item.id === targetData.id)
+    if (index === -1) return
+
+    targetList.splice(dropType === 'before' ? index : index + 1, 0, widget)
+  }
+
+  projectStore.setSelectWidgets([widget])
 }
 </script>

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

@@ -8,9 +8,15 @@
     <div class="flex-1 flex items-center gap-8px">
       <LuPanelsTopLeft size="14px" v-if="data.type === 'page'" />
       <LuBox size="14px" v-else />
-      <span>{{ node.label }}</span>
+      <span>{{ data.name }}</span>
     </div>
-    <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
+    <div
+      v-if="Object.hasOwn(data, 'locked')"
+      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>
+      </el-tooltip>
       <el-tooltip v-if="data.type !== 'page'" content="删除">
         <span @click="deleteWidget(data)"><LuTrash2 size="14px" /></span>
       </el-tooltip>
@@ -34,6 +40,7 @@
 import type { RenderContentContext } from 'element-plus'
 import type { BaseWidget } from '@/types/baseWidget'
 
+import { computed } from 'vue'
 import {
   LuTrash2,
   LuLock,
@@ -54,10 +61,20 @@ defineProps<{
 const projectStore = useProjectStore()
 const actionStore = useActionStore()
 
+const isBind = computed(() => {
+  // todo
+  return true
+})
+
 // 删除控件
 const deleteWidget = (data: BaseWidget) => {
   actionStore.onDeleteById(data.id)
 }
+
+// 解除绑定
+const unbind = () => {
+  // todo
+}
 </script>
 
 <style lang="less">

+ 37 - 33
src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue

@@ -10,33 +10,33 @@
       <div class="flex-1 flex items-center gap-8px">
         <LuMonitor size="14px" v-if="data.type !== 'page'" />
         <LuPanelsTopLeft size="14px" v-else />
-        <span>{{ node.label }}</span>
+        <span>{{ data.name }}</span>
       </div>
       <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
-        <el-tooltip v-if="data.type !== 'page'" content="添加页面">
+        <el-tooltip v-if="data.type !== 'page'" content="Add page">
           <span @click.capture.stop="addPage(data)"><LuPlus size="14px" /></span>
         </el-tooltip>
-        <el-tooltip content="编辑">
+        <el-tooltip content="Rename">
           <span><LuPencilLine size="14px" @click.capture.stop="edit = true" /></span>
         </el-tooltip>
-        <el-tooltip content="删除" v-if="data.type === 'page'">
+        <el-tooltip content="Delete" v-if="data.type === 'page'">
           <span>
-            <el-popconfirm class="box-item" title="确认删除?" @confirm="deletePage(data)">
+            <el-popconfirm class="box-item" title="Delete this page?" @confirm="deletePage(data)">
               <template #reference>
                 <span><LuTrash2 size="14px" /></span>
               </template>
             </el-popconfirm>
           </span>
         </el-tooltip>
-        <el-tooltip content="隐藏/显示" v-if="data.type === 'page'">
+        <el-tooltip content="Show / Hide" v-if="data.type === 'page'">
           <span @click.capture.stop="data.hidden = !data.hidden">
             <LuEye size="14px" v-if="!data.hidden" />
             <LuEyeOff size="14px" v-else />
           </span>
         </el-tooltip>
-        <el-tooltip content="锁定/解锁" v-if="data.type === 'page'">
-          <span @click.capture.stop="data.lock = !data.lock">
-            <LuLock size="14px" v-if="!data.lock" />
+        <el-tooltip content="Lock / Unlock" v-if="data.type === 'page'">
+          <span @click.capture.stop="data.locked = !data.locked">
+            <LuLock size="14px" v-if="!data.locked" />
             <LuUnlock size="14px" v-else />
           </span>
         </el-tooltip>
@@ -47,7 +47,7 @@
         v-model="data.name"
         size="small"
         style="width: 100%"
-        placeholder="请输入名称"
+        placeholder="Enter name"
         @blur="edit = false"
         @keyup.enter="edit = false"
         @click.stop.capture
@@ -57,8 +57,6 @@
 </template>
 
 <script setup lang="ts">
-import type { Screen } from '@/types/screen'
-import type { RenderContentContext } from 'element-plus'
 import { ref } from 'vue'
 import {
   LuTrash2,
@@ -71,53 +69,59 @@ import {
   LuPlus,
   LuPencilLine
 } from 'vue-icons-plus/lu'
-import { useProjectStore } from '@/store/modules/project'
+
 import { createPage } from '@/model'
-import { Page } from '@/types/page'
 import { useActionStore } from '@/store/modules/action'
+import { useProjectStore } from '@/store/modules/project'
+
+import type { Screen } from '@/types/screen'
+import type { Page } from '@/types/page'
+import type { RenderContentContext } from 'element-plus'
 
 const props = defineProps<{
   node: RenderContentContext['node']
-  data: any
+  data: Screen | Page
 }>()
 
 const edit = ref(false)
 const projectStore = useProjectStore()
 const actionStore = useActionStore()
 
-// 创建页面
 const addPage = (screen: Screen) => {
-  const newScreen = createPage()
+  const newPage = createPage()
   screen.pages.push({
-    ...newScreen,
-    name: `${newScreen.name}_${screen.pages.length + 1}`
+    ...newPage,
+    name: `${newPage.name}_${screen.pages.length + 1}`
   })
-  // 选择当前页面
-  projectStore.activePageId = newScreen.id
-  // 当前屏幕打开的页面
-  projectStore.project?.screens.forEach((screen, index) => {
-    screen.pages.forEach((page) => {
-      if (page.id === newScreen.id) {
-        projectStore.openPages[index] = page
-      }
-    })
+
+  projectStore.activePageId = newPage.id
+  projectStore.project?.screens.forEach((item, index) => {
+    if (item.id === screen.id) {
+      projectStore.openPageIds[index] = newPage.id
+    }
   })
 }
 
-// 删除页面
 const deletePage = (page: Page) => {
   actionStore.onDeletePage(page.id)
 }
 
-// 双击最大化当前屏幕
 const handleSetMaxScreen = () => {
   const { data, node } = props
+
   if (data.type === 'screen') {
     projectStore.currentMaxScreen = data.id
-  } else {
-    projectStore.currentMaxScreen = node?.parent?.data.id
-    projectStore.activePageId = data.id
+    return
   }
+
+  projectStore.currentMaxScreen = node?.parent?.data.id
+  projectStore.activePageId = data.id
+
+  projectStore.project?.screens.forEach((screen, index) => {
+    if (screen.id === node?.parent?.data.id) {
+      projectStore.openPageIds[index] = data.id
+    }
+  })
 }
 </script>