Procházet zdrojové kódy

feat: 添加页面管理功能

jiaxing.liao před 3 týdny
rodič
revize
b81b23d9bf

+ 57 - 21
project.json5

@@ -135,7 +135,7 @@
       }
     ]
   },
-  // 复用控件
+  // 复用控件
   "widgets": [
     {
       "id": "copy_obj_1",
@@ -143,6 +143,12 @@
       "name": "copy_obj_1",
       // 控件类型
       "widgetType": "lv_object",
+      // 是否为可复用组件 widgets里面的为true
+      "isCopy": true,
+      // 复用来源组件id
+      "copyFrom": "",
+      // 元素类型
+      "type": "widget",
       // 属性 根据每个控件生成
       "props": {
         // 坐标x
@@ -234,21 +240,24 @@
         {
           "id": "label_1",
           "name": "lv_label",
-          "parentId": "lv_obj_01",
-          "type": "object",
+          "widgetType": "lv_label",
+          "isCopy": false,
+          "copyFrom": "",
+          "type": "widget",
           "hidden": false,
           "locked": false,
-          "props": [
-            {
-              "id": "prop_1",
-              "name": "text",
-              "type": "string",
-              "text": {
-                "valueType": "LANGUAGE",
-                "value": "hello" 
-              }
+          "props": {
+            "id": "prop_1",
+            "name": "text",
+            "type": "string",
+            "text": {
+              "valueType": "LANGUAGE",
+              "value": "hello" 
             }
-          ]
+          },
+          "style": {},
+          "events": [],
+          "children": []
         }
       ]
     },
@@ -382,24 +391,25 @@
   "screens": [
     {
       "id": "screen_1",
+      // 名称
       "name": "主屏",
-      // 类型
+      // 元素类型
       "type": "screen",
       // 屏幕宽 未设置取通用配置
       "width": 1920,
       // 屏幕高 未设置取通用配置
       "height": 1080,
-      // 隐藏
-      "hidden": false,
-      // 锁定
-      "locked": false,
+      // 颜色深度
+      "colorDepth": "16bit",
+      // 颜色格式
+      "colorFormat": "BGR",
       // 页面
       "pages": [
         {
           "id": "page_1",
           // 页面名称
           "name": "启动页",
-          // 类型
+          // 元素类型
           "type": "page",
           // 隐藏
           "hidden": false,
@@ -436,10 +446,14 @@
           "children": [
             {
               "id": "lv_obj_01",
-              // 复用控件ID
-              "widgetId": "obj_1",
+              // 控件类型
+              "widgetType": "lv_obj",
               // 控件名称
               "name": "容器",
+              // 是否为可复用组件
+              "isCopy": false,
+              // 复用来源组件id
+              "copyFrom": "copy_obj_1",
               // 类型
               "type": "widget",
               // 隐藏
@@ -453,6 +467,28 @@
                 // 坐标y
                 "y": 100
               }
+            },
+            {
+              "id": "label_1",
+              "name": "lv_label",
+              "widgetType": "lv_label",
+              "isCopy": false,
+              "copyFrom": "",
+              "type": "widget",
+              "hidden": false,
+              "locked": false,
+              "props": {
+                "id": "prop_1",
+                "name": "text",
+                "type": "string",
+                "text": {
+                  "valueType": "LANGUAGE",
+                  "value": "hello" 
+                }
+              },
+              "style": {},
+              "events": [],
+              "children": []
             }
           ]
         }

+ 1 - 0
src/renderer/components.d.ts

@@ -31,6 +31,7 @@ declare module 'vue' {
     ElInput: typeof import('element-plus/es')['ElInput']
     ElMain: typeof import('element-plus/es')['ElMain']
     ElOption: typeof import('element-plus/es')['ElOption']
+    ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
     ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
     ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
     ElRow: typeof import('element-plus/es')['ElRow']

+ 8 - 6
src/renderer/src/components/SplitterCollapse/SplitterCollapseItem.vue

@@ -1,5 +1,11 @@
 <template>
-  <SplitterPanel collapsible :collapsed-size="0" ref="splitterPanelRef" style="min-height: 32px">
+  <SplitterPanel
+    collapsible
+    :collapsed-size="0"
+    ref="splitterPanelRef"
+    style="min-height: 32px"
+    v-bind="$attrs"
+  >
     <template #default="{ isExpanded, collapse, expand }">
       <div class="w-full h-full flex flex-col">
         <!-- header -->
@@ -36,12 +42,8 @@ import { SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 withDefaults(
   defineProps<{
     title: string
-    min?: number
-    expand?: boolean
   }>(),
-  {
-    min: 0
-  }
+  {}
 )
 
 const emit = defineEmits(['change'])

+ 1 - 0
src/renderer/src/main.ts

@@ -3,6 +3,7 @@ import App from './App.vue'
 import 'virtual:uno.css'
 import 'element-plus/theme-chalk/dark/css-vars.css'
 import './theme/vars.css'
+import './style.less'
 
 import 'normalize.css'
 import router from './router'

+ 46 - 32
src/renderer/src/store/modules/app.ts

@@ -1,41 +1,55 @@
+import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
 import zhCn from 'element-plus/es/locale/lang/zh-cn'
 import en from 'element-plus/es/locale/lang/en'
+import { useI18n } from 'vue-i18n'
+import { useLocalStorage } from '@vueuse/core'
 
 /**
  * @description: 应用状态管理
  */
-export const useAppStore = defineStore('app', {
-  state() {
-    return {
-      // 加载中
-      loading: false,
-      // 语言
-      lang: localStorage.getItem('lang') || 'zh_CN',
-      // 主题
-      theme: localStorage.getItem('theme') || 'dark',
-      // 编辑器
-      editor: {
-        fontSize: localStorage.getItem('editorFontSize') || 16
-      },
-      // 显示连接设备
-      showLink: true,
-      // 显示下载到设备
-      showDownload: true
-    }
-  },
-  getters: {
-    getLocale(state) {
-      return state.lang === 'zh_CN' ? zhCn : en
-    }
-  },
-  actions: {
-    setAppLoading(loading: boolean) {
-      this.loading = loading
-    },
-    setLang(lang: string) {
-      this.lang = lang
-      localStorage.setItem('lang', lang)
-    }
+export const useAppStore = defineStore('app', () => {
+  // 屏幕布局
+  const screenLayout = ref<'vertical' | 'horizontal'>('vertical')
+  // 语言
+  const lang = useLocalStorage<'zh_CN' | 'en_US'>('lang', 'zh_CN')
+  // 主题
+  const theme = useLocalStorage('theme', 'dark')
+  // 编辑器
+  const editor = useLocalStorage('editor', {
+    fontSize: 14
+  })
+  // 显示链接
+  const showLink = ref(true)
+  // 显示下载
+  const showDownload = ref(true)
+  // 加载中
+  const loading = ref(false)
+
+  const { locale } = useI18n()
+
+  const getLocale = computed(() => (lang.value === 'zh_CN' ? zhCn : en))
+
+  // 设置语言
+  function setLang(val: 'zh_CN' | 'en_US') {
+    lang.value = val
+    locale.value = val
+  }
+  // 切换布局
+  function toggleLayout() {
+    screenLayout.value = screenLayout.value === 'vertical' ? 'horizontal' : 'vertical'
+  }
+
+  return {
+    screenLayout,
+    lang,
+    theme,
+    editor,
+    showLink,
+    showDownload,
+    loading,
+    getLocale,
+    setLang,
+    toggleLayout
   }
 })

+ 36 - 3
src/renderer/src/store/modules/project.ts

@@ -9,12 +9,13 @@ import type { Method } from '@/types/method'
 import type { BaseWidget } from '@/types/baseWidget'
 import type { Screen } from '@/types/screen'
 
-import { ref } from 'vue'
+import { computed, ref } from 'vue'
 import { defineStore } from 'pinia'
 import { klona } from 'klona'
 import { createBin, createScreen } from '@/model'
 
 export const useProjectStore = defineStore('project', () => {
+  // 项目信息
   const project = ref<{
     version: string
     meta: AppMeta
@@ -33,6 +34,15 @@ export const useProjectStore = defineStore('project', () => {
     screens: Screen[]
   }>()
 
+  // 活动页面key
+  const activePageId = ref<string>()
+
+  // 活动页面
+  const activePage = computed(() => {
+    const pages = project.value?.screens.map((screen) => screen.pages)
+    return pages?.flat().find((page) => page.id === activePageId.value)
+  })
+
   /**
    * 创建应用
    * @param meta 应用元信息
@@ -57,9 +67,12 @@ export const useProjectStore = defineStore('project', () => {
       screens: []
     }
     // 2、构建屏幕信息
+    activePageId.value = ''
     meta.screens.forEach((screen) => {
-      project.value?.screens.push(createScreen(screen))
+      const newScreen = createScreen(screen)
+      project.value?.screens.push(newScreen)
     })
+    activePageId.value = project.value.screens[0].pages[0].id
 
     // 3、创建BIN
     if (meta.resourcePackaging === 'c_bin' && meta.binNum > 0) {
@@ -69,8 +82,28 @@ export const useProjectStore = defineStore('project', () => {
     }
   }
 
+  // 删除页面
+  const deletePage = (pageId: string) => {
+    project.value?.screens.forEach((screen) => {
+      screen.pages = screen.pages.filter((page) => page.id !== pageId)
+    })
+  }
+
+  // 删除控件
+  const deleteWidget = (widgetId: string) => {
+    project.value?.screens.forEach((screen) => {
+      screen.pages.forEach((page) => {
+        page.children = page.children.filter((widget) => widget.id !== widgetId)
+      })
+    })
+  }
+
   return {
     createApp,
-    project
+    project,
+    activePageId,
+    activePage,
+    deletePage,
+    deleteWidget
   }
 })

+ 3 - 0
src/renderer/src/style.less

@@ -0,0 +1,3 @@
+.el-dialog {
+  border: solid 1px #666666;
+}

+ 6 - 0
src/renderer/src/types/baseWidget.d.ts

@@ -5,8 +5,14 @@ export type BaseWidget = {
   id: string
   // 控件名称
   name: string
+  // 元素类型
+  type: 'widget'
   // 控件类型
   widgetType: string
+  // 是否为可复用控件
+  isCopy: boolean
+  // 复用来源控件ID
+  copyFrom: string
   // 控件属性
   props: Record<string, any>
   // 样式

+ 2 - 2
src/renderer/src/views/designer/modals/projectModal/index.vue

@@ -379,8 +379,8 @@ const formData = reactive<
 >({
   version: '1.0.0',
   description: '',
-  name: '',
-  path: '',
+  name: 'ProjectName',
+  path: 'D:\\',
   type: 'chip',
   chip: {
     model: '',

+ 26 - 187
src/renderer/src/views/designer/sidebar/Hierarchy.vue

@@ -1,217 +1,56 @@
 <template>
   <SplitterCollapse>
     <SplitterCollapseItem title="屏幕页面">
+      <!-- 屏幕层 -->
       <el-tree
         ref="treeRef"
         style="max-width: 600px"
-        :data="screenData"
         default-expand-all
         node-key="id"
         highlight-current
+        check-on-click-node
+        :default-checked-keys="projectStore.activePageId ? [projectStore.activePageId] : []"
+        :data="projectStore.project?.screens"
         :props="{ label: 'name', children: 'pages' }"
-        :render-content="renderScreen"
-      />
+        @node-click="handleNodeClick"
+      >
+        <template #default="{ node, data }">
+          <ScreenTreeItem :node="node" :data="data" />
+        </template>
+      </el-tree>
     </SplitterCollapseItem>
     <SplitterCollapseItem title="图层">
+      <!-- 页面层 -->
       <el-tree
         ref="treeRef"
         style="max-width: 600px"
-        :data="widgetData"
+        :data="projectStore.activePage?.children"
         default-expand-all
         node-key="id"
         highlight-current
+        check-on-click-node
         :props="{ label: 'name' }"
-        :render-content="renderPage"
-      />
+      >
+        <template #default="{ node, data }">
+          <PageTreeItem :node="node" :data="data" />
+        </template>
+      </el-tree>
     </SplitterCollapseItem>
   </SplitterCollapse>
 </template>
 
 <script setup lang="ts">
 import { SplitterCollapse, SplitterCollapseItem } from '@/components/SplitterCollapse'
-import {
-  LuTrash2,
-  LuLock,
-  LuUnlock,
-  LuEye,
-  LuEyeOff,
-  LuMonitor,
-  LuPanelsTopLeft,
-  LuBox
-} from 'vue-icons-plus/lu'
-import type { RenderContentFunction } from 'element-plus'
-// 屏幕页面数据
-const screenData = [
-  {
-    id: 'screen_1',
-    name: '主屏',
-    type: 'screen',
-    backgroundColor: '#ffffff',
-    width: 0,
-    height: 0,
-    hidden: false,
-    locked: false,
-    pages: [
-      {
-        id: 'page_1',
-        name: '启动页',
-        type: 'page',
-        hidden: false,
-        locked: false,
-        referenceLine: [
-          {
-            id: 'r_1',
-            layout: 'horizontal',
-            position: 0,
-            visible: true
-          }
-        ],
-        props: {},
-        style: {},
-        events: [],
-        variable: [],
-        method: [],
-        children: []
-      },
-      {
-        id: 'page_2',
-        name: '详情页',
-        type: 'page',
-        hidden: false,
-        locked: false,
-        referenceLine: [
-          {
-            id: 'r_1',
-            layout: 'horizontal',
-            position: 0,
-            visible: true
-          }
-        ],
-        props: {},
-        style: {},
-        events: [],
-        variable: [],
-        method: [],
-        children: []
-      }
-    ]
-  }
-]
+import ScreenTreeItem from './components/ScreenTreeItem.vue'
+import PageTreeItem from './components/PageTreeItem.vue'
+import { useProjectStore } from '@/store/modules/project'
 
-// 图层数据
-const widgetData = [
-  {
-    id: 'widget_1',
-    name: '按钮',
-    type: 'widget',
-    hidden: false,
-    locked: false,
-    props: {},
-    style: {},
-    events: [],
-    variable: [],
-    method: []
-  }
-]
+const projectStore = useProjectStore()
 
-const renderScreen: RenderContentFunction = (h, { node, data }) => {
-  return h(
-    'div',
-    {
-      style: {
-        width: '100%',
-        display: 'flex',
-        'justify-content': 'space-between',
-        'align-items': 'center'
-      }
-    },
-    [
-      h(
-        'div',
-        {
-          style: {
-            display: 'flex',
-            'align-items': 'center',
-            gap: '8px',
-            paddingRight: '8px'
-          }
-        },
-        [
-          h(data.type === 'screen' ? LuMonitor : LuPanelsTopLeft, {
-            style: { color: 'var(--text-secondary)' },
-            size: 14
-          }),
-          h('span', null, node.label)
-        ]
-      ),
-      h(
-        'div',
-        {
-          style: {
-            display: 'flex',
-            'align-items': 'center',
-            gap: '8px',
-            paddingRight: '8px'
-          }
-        },
-        [
-          h(LuTrash2, { style: { color: 'var(--text-secondary)' }, size: 14 }),
-          h(LuEye, { style: { color: 'var(--text-secondary)' }, size: 14 }),
-          h(LuLock, { style: { color: 'var(--text-secondary)' }, size: 14 })
-        ]
-      )
-    ]
-  )
-}
-
-const renderPage: RenderContentFunction = (h, { node, data }) => {
-  return h(
-    'div',
-    {
-      style: {
-        width: '100%',
-        display: 'flex',
-        'justify-content': 'space-between',
-        'align-items': 'center'
-      }
-    },
-    [
-      h(
-        'div',
-        {
-          style: {
-            display: 'flex',
-            'align-items': 'center',
-            gap: '8px',
-            paddingRight: '8px'
-          }
-        },
-        [
-          h(data.type === 'page' ? LuPanelsTopLeft : LuBox, {
-            style: { color: 'var(--text-secondary)' },
-            size: 14
-          }),
-          h('span', null, node.label)
-        ]
-      ),
-      h(
-        'div',
-        {
-          style: {
-            display: 'flex',
-            'align-items': 'center',
-            gap: '8px',
-            paddingRight: '8px'
-          }
-        },
-        [
-          h(LuTrash2, { style: { color: 'var(--text-secondary)' }, size: 14 }),
-          h(LuEye, { style: { color: 'var(--text-secondary)' }, size: 14 }),
-          h(LuLock, { style: { color: 'var(--text-secondary)' }, size: 14 })
-        ]
-      )
-    ]
-  )
+const handleNodeClick = (node: any) => {
+  if (node.type === 'page') {
+    projectStore.activePageId = node.id
+  }
 }
 </script>
 

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

@@ -0,0 +1,63 @@
+<template>
+  <div class="w-full flex items-center justify-between group/item">
+    <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>
+    </div>
+    <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
+      <el-tooltip v-if="data.type === 'page'" content="删除">
+        <el-popconfirm
+          class="box-item"
+          title="确认删除?"
+          placement="top-start"
+          @confirm="deleteWidget(data)"
+        >
+          <template #reference>
+            <span><LuTrash2 size="14px" /></span>
+          </template>
+        </el-popconfirm>
+      </el-tooltip>
+      <el-tooltip content="隐藏/显示">
+        <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="锁定/解锁">
+        <span @click.capture.stop="data.lock = !data.lock">
+          <LuLock size="14px" v-if="!data.lock" />
+          <LuUnlock size="14px" v-else />
+        </span>
+      </el-tooltip>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { RenderContentContext } from 'element-plus'
+import { defineProps } from 'vue'
+import {
+  LuTrash2,
+  LuLock,
+  LuUnlock,
+  LuEye,
+  LuEyeOff,
+  LuPanelsTopLeft,
+  LuBox
+} from 'vue-icons-plus/lu'
+import { useProjectStore } from '@/store/modules/project'
+
+defineProps<{
+  node: RenderContentContext['node']
+  data: any
+}>()
+
+// 删除控件
+const deleteWidget = (data: any) => {
+  const projectStore = useProjectStore()
+  projectStore.deleteWidget(data.id)
+}
+</script>
+
+<style scoped></style>

+ 97 - 0
src/renderer/src/views/designer/sidebar/components/ScreenTreeItem.vue

@@ -0,0 +1,97 @@
+<template>
+  <div class="w-full flex items-center justify-between group/item">
+    <template v-if="!edit">
+      <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>
+      </div>
+      <div class="flex items-center gap-4px pr-12px invisible group-hover/item:visible">
+        <el-tooltip v-if="data.type !== 'page'" content="添加页面">
+          <span @click.capture.stop="addPage(data)"><LuPlus size="14px" /></span>
+        </el-tooltip>
+        <el-tooltip content="编辑">
+          <span><LuPencilLine size="14px" @click.capture.stop="edit = true" /></span>
+        </el-tooltip>
+        <el-tooltip content="删除" v-if="data.type === 'page'">
+          <el-popconfirm
+            class="box-item"
+            title="确认删除?"
+            placement="top-start"
+            @confirm="deletePage(data)"
+          >
+            <template #reference>
+              <span><LuTrash2 size="14px" /></span>
+            </template>
+          </el-popconfirm>
+        </el-tooltip>
+        <el-tooltip content="隐藏/显示" 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" />
+            <LuUnlock size="14px" v-else />
+          </span>
+        </el-tooltip>
+      </div>
+    </template>
+    <template v-else>
+      <el-input
+        v-model="data.name"
+        size="small"
+        style="width: 100%"
+        placeholder="请输入名称"
+        @blur="edit = false"
+        @keyup.enter="edit = false"
+        @click.stop.capture
+      />
+    </template>
+  </div>
+</template>
+
+<script setup lang="ts">
+import type { Screen } from '@/types/screen'
+import type { RenderContentContext } from 'element-plus'
+import { defineProps, ref } from 'vue'
+import {
+  LuTrash2,
+  LuLock,
+  LuUnlock,
+  LuEye,
+  LuEyeOff,
+  LuMonitor,
+  LuPanelsTopLeft,
+  LuPlus,
+  LuPencilLine
+} from 'vue-icons-plus/lu'
+import { useProjectStore } from '@/store/modules/project'
+import { createPage } from '@/model'
+import { Page } from '@/types/page'
+
+defineProps<{
+  node: RenderContentContext['node']
+  data: any
+}>()
+
+const edit = ref(false)
+const projectStore = useProjectStore()
+
+// 创建页面
+const addPage = (screen: Screen) => {
+  const newScreen = createPage()
+  screen.pages.push(newScreen)
+  // 选择当前页面
+  projectStore.activePageId = newScreen.id
+}
+
+// 删除页面
+const deletePage = (page: Page) => {
+  projectStore.deletePage(page.id)
+}
+</script>
+
+<style scoped></style>

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

@@ -1,11 +1,12 @@
 <template>
   <div class="w-full h-full">
-    <SplitterGroup direction="vertical">
+    <SplitterGroup :direction="appStore.screenLayout">
       <SplitterPanel>
         <Stage key="1" :data="projectStore.project?.screens[0]" />
       </SplitterPanel>
       <SplitterResizeHandle
-        class="h-2px bg-border"
+        class="bg-border"
+        :class="appStore.screenLayout === 'vertical' ? 'h-2px' : 'w-2px'"
         v-if="projectStore.project?.meta.screenType === 'dual'"
       />
       <SplitterPanel v-if="projectStore.project?.meta.screenType === 'dual'">
@@ -20,8 +21,10 @@ import { ref, onMounted } from 'vue'
 import Stage from './stage/index.vue'
 import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 import { useProjectStore } from '@/store/modules/project'
+import { useAppStore } from '@/store/modules/app'
 
 const projectStore = useProjectStore()
+const appStore = useAppStore()
 
 const content = ref('')
 onMounted(() => {

+ 6 - 1
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -38,7 +38,7 @@ import {
   defineEmits
 } from 'vue'
 
-import { useScroll, useElementSize } from '@vueuse/core'
+import { useScroll, useElementSize, useResizeObserver } from '@vueuse/core'
 import ComponentWrapper from './ComponentWrapper.vue'
 
 const props = defineProps<{
@@ -140,6 +140,11 @@ useScroll(stageWrapperRef, {
   }
 })
 
+useResizeObserver(stageWrapperRef, () => {
+  initScale()
+  initStagePosition()
+})
+
 /* 设置缩放倍数 */
 const initScale = async () => {
   if (!clientWidth.value || !clientHeight.value) return

+ 31 - 14
src/renderer/src/views/designer/workspace/stage/index.vue

@@ -20,11 +20,25 @@
       <div
         class="workspace-bottom flex justify-between items-center overflow-hidden bg-bg-secondary"
       >
-        <div class="bottom-left">
+        <div class="bottom-left flex items-center">
           <span style="margin-right: 12px">画布尺寸:</span>
           <span>{{ state.width }} * {{ state.height }}</span>
         </div>
         <div class="bottom-right flex items-center gap-8px">
+          <div
+            v-if="projectStore.project?.meta.screenType === 'dual'"
+            class="w-20px h-20px flex items-center justify-center cursor-pointer"
+            @click="appStore.toggleLayout"
+          >
+            <LuRows2 :size="16" v-if="appStore.screenLayout === 'vertical'" />
+            <LuColumns2 :size="16" v-else />
+          </div>
+          <div
+            class="w-20px h-20px flex items-center justify-center cursor-pointer"
+            @click="handleCenter"
+          >
+            <LuCircleDot :size="16" />
+          </div>
           <div
             class="w-20px h-20px flex items-center justify-center cursor-pointer border-1px border-solid border-transparent"
             :class="state.showRuler ? 'border-blue! border-1px border-solid bg-bg-primary' : ''"
@@ -67,20 +81,27 @@
 import type { StageState } from './type'
 import type { Screen } from '@/types/screen'
 
-import { ref, reactive, defineProps, watch } from 'vue'
+import { ref, reactive, defineProps, watch, nextTick } from 'vue'
 import Scaleplate from './Scaleplate.vue'
 import DesignerCanvas from './DesignerCanvas.vue'
 import { throttle } from 'lodash'
-import { LuGrid3X3, LuRuler, LuBoxSelect } from 'vue-icons-plus/lu'
+import {
+  LuGrid3X3,
+  LuRuler,
+  LuBoxSelect,
+  LuRows2,
+  LuColumns2,
+  LuCircleDot
+} from 'vue-icons-plus/lu'
 import { useProjectStore } from '@/store/modules/project'
 import { useAppStore } from '@/store/modules/app'
 
 const props = defineProps<{
-  data: Screen
+  data?: Screen
 }>()
 const projectStore = useProjectStore()
 const appStore = useAppStore()
-const canvasRef = ref<{ getPosition: HTMLElement['getBoundingClientRect'] } | null>()
+const canvasRef = ref<InstanceType<typeof DesignerCanvas>>()
 const state = reactive<StageState>({
   scale: 1,
   width: 1280,
@@ -102,7 +123,7 @@ const state = reactive<StageState>({
 
 watch(
   () => props.data,
-  (val) => {
+  async (val) => {
     if (val) {
       state.width = val.width
       state.height = val.height
@@ -117,14 +138,10 @@ const handleSetState = (newState: Partial<StageState>) => {
   })
 }
 
-const handleSizeChange = (n: number) => {
-  if (n < 0.1) {
-    state.scale = 0.1
-  } else if (n > 4) {
-    state.scale = 4
-  } else {
-    state.scale = n
-  }
+// 居中
+const handleCenter = () => {
+  canvasRef.value?.initScale()
+  canvasRef.value?.initStagePosition()
 }
 
 /* ====================处理框选多个组件======================== */