Переглянути джерело

fix: 修改画布展示问题

jiaxing.liao 4 тижнів тому
батько
коміт
b362294d7a

+ 1 - 0
package.json

@@ -29,6 +29,7 @@
     "monaco-editor": "^0.54.0",
     "normalize.css": "^8.0.1",
     "pinia": "^3.0.3",
+    "reka-ui": "^2.6.0",
     "vue-hooks-plus": "^2.4.1",
     "vue-i18n": "^11.1.12",
     "vue-icons-plus": "^0.1.8",

+ 112 - 0
pnpm-lock.yaml

@@ -32,6 +32,9 @@ importers:
       pinia:
         specifier: ^3.0.3
         version: 3.0.3(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
+      reka-ui:
+        specifier: ^2.6.0
+        version: 2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3))
       vue-hooks-plus:
         specifier: ^2.4.1
         version: 2.4.1(vue@3.5.22(typescript@5.9.3))
@@ -482,6 +485,9 @@ packages:
   '@floating-ui/utils@0.2.10':
     resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
 
+  '@floating-ui/vue@1.1.9':
+    resolution: {integrity: sha512-BfNqNW6KA83Nexspgb9DZuz578R7HT8MZw1CfK9I6Ah4QReNWEJsXWHN+SdmOVLNGmTPDi+fDT535Df5PzMLbQ==}
+
   '@gar/promisify@1.1.3':
     resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
 
@@ -507,6 +513,12 @@ packages:
   '@iconify/utils@3.0.2':
     resolution: {integrity: sha512-EfJS0rLfVuRuJRn4psJHtK2A9TqVnkxPpHY6lYHiB9+8eSuudsxbwMiavocG45ujOo6FJ+CIRlRnlOGinzkaGQ==}
 
+  '@internationalized/date@3.10.0':
+    resolution: {integrity: sha512-oxDR/NTEJ1k+UFVQElaNIk65E/Z83HK1z1WI3lQyhTtnNg4R5oVXaPzK3jcpKG8UHKDVuDQHzn+wsxSz8RP3aw==}
+
+  '@internationalized/number@3.6.5':
+    resolution: {integrity: sha512-6hY4Kl4HPBvtfS62asS/R22JzNNy8vi/Ssev7x6EobfCp+9QIB2hKvI2EtbdJ0VSQacxVNtqhE/NmF/NZ0gm6g==}
+
   '@intlify/core-base@11.1.12':
     resolution: {integrity: sha512-whh0trqRsSqVLNEUCwU59pyJZYpU8AmSWl8M3Jz2Mv5ESPP6kFh4juas2NpZ1iCvy7GlNRffUD1xr84gceimjg==}
     engines: {node: '>= 16'}
@@ -718,6 +730,9 @@ packages:
     resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
     engines: {node: '>=10'}
 
+  '@swc/helpers@0.5.17':
+    resolution: {integrity: sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A==}
+
   '@sxzz/popperjs-es@2.11.7':
     resolution: {integrity: sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==}
 
@@ -725,6 +740,14 @@ packages:
     resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
     engines: {node: '>=10'}
 
+  '@tanstack/virtual-core@3.13.12':
+    resolution: {integrity: sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==}
+
+  '@tanstack/vue-virtual@3.13.12':
+    resolution: {integrity: sha512-vhF7kEU9EXWXh+HdAwKJ2m3xaOnTTmgcdXcF2pim8g4GvI7eRrk2YRuV5nUlZnd/NbCIX4/Ja2OZu5EjJL06Ww==}
+    peerDependencies:
+      vue: ^2.7.0 || ^3.0.0
+
   '@tootallnate/once@2.0.0':
     resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
     engines: {node: '>= 10'}
@@ -1001,6 +1024,9 @@ packages:
     peerDependencies:
       vue: ^3.5.0
 
+  '@vueuse/core@12.8.2':
+    resolution: {integrity: sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ==}
+
   '@vueuse/core@14.0.0':
     resolution: {integrity: sha512-d6tKRWkZE8IQElX2aHBxXOMD478fHIYV+Dzm2y9Ag122ICBpNKtGICiXKOhWU3L1kKdttDD9dCMS4bGP3jhCTQ==}
     peerDependencies:
@@ -1009,12 +1035,18 @@ packages:
   '@vueuse/core@9.13.0':
     resolution: {integrity: sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==}
 
+  '@vueuse/metadata@12.8.2':
+    resolution: {integrity: sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A==}
+
   '@vueuse/metadata@14.0.0':
     resolution: {integrity: sha512-6yoGqbJcMldVCevkFiHDBTB1V5Hq+G/haPlGIuaFZHpXC0HADB0EN1ryQAAceiW+ryS3niUwvdFbGiqHqBrfVA==}
 
   '@vueuse/metadata@9.13.0':
     resolution: {integrity: sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==}
 
+  '@vueuse/shared@12.8.2':
+    resolution: {integrity: sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w==}
+
   '@vueuse/shared@14.0.0':
     resolution: {integrity: sha512-mTCA0uczBgurRlwVaQHfG0Ja7UdGe4g9mwffiJmvLiTtp1G4AQyIjej6si/k8c8pUwTfVpNufck+23gXptPAkw==}
     peerDependencies:
@@ -1120,6 +1152,10 @@ packages:
   argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  aria-hidden@1.2.6:
+    resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==}
+    engines: {node: '>=10'}
+
   assert-plus@1.0.0:
     resolution: {integrity: sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==}
     engines: {node: '>=0.8'}
@@ -2382,6 +2418,9 @@ packages:
   ofetch@1.5.1:
     resolution: {integrity: sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==}
 
+  ohash@2.0.11:
+    resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
+
   once@1.4.0:
     resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
 
@@ -2575,6 +2614,11 @@ packages:
     resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==}
     engines: {node: '>= 14.18.0'}
 
+  reka-ui@2.6.0:
+    resolution: {integrity: sha512-NrGMKrABD97l890mFS3TNUzB0BLUfbL3hh0NjcJRIUSUljb288bx3Mzo31nOyUcdiiW0HqFGXJwyCBh9cWgb0w==}
+    peerDependencies:
+      vue: '>= 3.2.0'
+
   require-directory@2.1.1:
     resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
     engines: {node: '>=0.10.0'}
@@ -3491,6 +3535,15 @@ snapshots:
 
   '@floating-ui/utils@0.2.10': {}
 
+  '@floating-ui/vue@1.1.9(vue@3.5.22(typescript@5.9.3))':
+    dependencies:
+      '@floating-ui/dom': 1.7.4
+      '@floating-ui/utils': 0.2.10
+      vue-demi: 0.14.10(vue@3.5.22(typescript@5.9.3))
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - vue
+
   '@gar/promisify@1.1.3': {}
 
   '@humanfs/core@0.19.1': {}
@@ -3519,6 +3572,14 @@ snapshots:
     transitivePeerDependencies:
       - supports-color
 
+  '@internationalized/date@3.10.0':
+    dependencies:
+      '@swc/helpers': 0.5.17
+
+  '@internationalized/number@3.6.5':
+    dependencies:
+      '@swc/helpers': 0.5.17
+
   '@intlify/core-base@11.1.12':
     dependencies:
       '@intlify/message-compiler': 11.1.12
@@ -3681,12 +3742,23 @@ snapshots:
 
   '@sindresorhus/is@4.6.0': {}
 
+  '@swc/helpers@0.5.17':
+    dependencies:
+      tslib: 2.8.1
+
   '@sxzz/popperjs-es@2.11.7': {}
 
   '@szmarczak/http-timer@4.0.6':
     dependencies:
       defer-to-connect: 2.0.1
 
+  '@tanstack/virtual-core@3.13.12': {}
+
+  '@tanstack/vue-virtual@3.13.12(vue@3.5.22(typescript@5.9.3))':
+    dependencies:
+      '@tanstack/virtual-core': 3.13.12
+      vue: 3.5.22(typescript@5.9.3)
+
   '@tootallnate/once@2.0.0': {}
 
   '@types/cacheable-request@6.0.3':
@@ -4106,6 +4178,15 @@ snapshots:
       '@vueuse/shared': 14.0.0(vue@3.5.22(typescript@5.9.3))
       vue: 3.5.22(typescript@5.9.3)
 
+  '@vueuse/core@12.8.2(typescript@5.9.3)':
+    dependencies:
+      '@types/web-bluetooth': 0.0.21
+      '@vueuse/metadata': 12.8.2
+      '@vueuse/shared': 12.8.2(typescript@5.9.3)
+      vue: 3.5.22(typescript@5.9.3)
+    transitivePeerDependencies:
+      - typescript
+
   '@vueuse/core@14.0.0(vue@3.5.22(typescript@5.9.3))':
     dependencies:
       '@types/web-bluetooth': 0.0.21
@@ -4123,10 +4204,18 @@ snapshots:
       - '@vue/composition-api'
       - vue
 
+  '@vueuse/metadata@12.8.2': {}
+
   '@vueuse/metadata@14.0.0': {}
 
   '@vueuse/metadata@9.13.0': {}
 
+  '@vueuse/shared@12.8.2(typescript@5.9.3)':
+    dependencies:
+      vue: 3.5.22(typescript@5.9.3)
+    transitivePeerDependencies:
+      - typescript
+
   '@vueuse/shared@14.0.0(vue@3.5.22(typescript@5.9.3))':
     dependencies:
       vue: 3.5.22(typescript@5.9.3)
@@ -4280,6 +4369,10 @@ snapshots:
 
   argparse@2.0.1: {}
 
+  aria-hidden@1.2.6:
+    dependencies:
+      tslib: 2.8.1
+
   assert-plus@1.0.0:
     optional: true
 
@@ -5658,6 +5751,8 @@ snapshots:
       node-fetch-native: 1.6.7
       ufo: 1.6.1
 
+  ohash@2.0.11: {}
+
   once@1.4.0:
     dependencies:
       wrappy: 1.0.2
@@ -5844,6 +5939,23 @@ snapshots:
 
   readdirp@4.1.2: {}
 
+  reka-ui@2.6.0(typescript@5.9.3)(vue@3.5.22(typescript@5.9.3)):
+    dependencies:
+      '@floating-ui/dom': 1.7.4
+      '@floating-ui/vue': 1.1.9(vue@3.5.22(typescript@5.9.3))
+      '@internationalized/date': 3.10.0
+      '@internationalized/number': 3.6.5
+      '@tanstack/vue-virtual': 3.13.12(vue@3.5.22(typescript@5.9.3))
+      '@vueuse/core': 12.8.2(typescript@5.9.3)
+      '@vueuse/shared': 12.8.2(typescript@5.9.3)
+      aria-hidden: 1.2.6
+      defu: 6.1.4
+      ohash: 2.0.11
+      vue: 3.5.22(typescript@5.9.3)
+    transitivePeerDependencies:
+      - '@vue/composition-api'
+      - typescript
+
   require-directory@2.1.1: {}
 
   resedit@1.7.2:

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

@@ -22,6 +22,7 @@ declare module 'vue' {
     ElFlex: typeof import('element-plus/es')['ElFlex']
     ElHeader: typeof import('element-plus/es')['ElHeader']
     ElMain: typeof import('element-plus/es')['ElMain']
+    ElSlider: typeof import('element-plus/es')['ElSlider']
     ElSplitter: typeof import('element-plus/es')['ElSplitter']
     ElSplitterPanel: typeof import('element-plus/es')['ElSplitterPanel']
     ElTabPane: typeof import('element-plus/es')['ElTabPane']

+ 1 - 1
src/renderer/index.html

@@ -2,7 +2,7 @@
 <html class="dark">
   <head>
     <meta charset="UTF-8" />
-    <title>lvgl-designer</title>
+    <title>sunmicro lvgl designer</title>
     <!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
     <meta
       http-equiv="Content-Security-Policy"

+ 3 - 1
src/renderer/src/components/MonacoEditor/index.vue

@@ -4,6 +4,7 @@ import type { editor } from 'monaco-editor'
 import { nextTick, onMounted, ref, watch } from 'vue'
 import { useAppStore } from '@/store/modules/app'
 import * as monaco from 'monaco-editor'
+import { LuFullscreen, LuShrink } from 'vue-icons-plus/lu'
 
 const props = withDefaults(
   defineProps<{
@@ -166,7 +167,8 @@ defineExpose({
       @click="isFullScreen = !isFullScreen"
       v-if="props.allowFullscreen"
     >
-      全屏
+      <LuFullscreen v-if="!isFullScreen" :size="16" />
+      <LuShrink v-else :size="16" />
     </div>
   </div>
 </template>

+ 11 - 8
src/renderer/src/views/designer/index.vue

@@ -5,17 +5,19 @@
         <Tools />
       </div>
       <div class="flex-1 bg-bg-primary p-0!">
-        <el-splitter>
-          <el-splitter-panel :min="260" :size="300">
+        <SplitterGroup direction="horizontal">
+          <SplitterPanel :minSize="14" :default-size="20">
             <Sidebar />
-          </el-splitter-panel>
-          <el-splitter-panel :min="200">
+          </SplitterPanel>
+          <SplitterResizeHandle class="w-2px bg-border" />
+          <SplitterPanel :minSize="20">
             <Workspace />
-          </el-splitter-panel>
-          <el-splitter-panel :min="260" :size="300">
+          </SplitterPanel>
+          <SplitterResizeHandle class="w-2px bg-border" />
+          <SplitterPanel :minSize="14" :default-size="20">
             <Config />
-          </el-splitter-panel>
-        </el-splitter>
+          </SplitterPanel>
+        </SplitterGroup>
       </div>
       <div class="h-32px">
         <Info></Info>
@@ -25,6 +27,7 @@
 </template>
 
 <script setup lang="ts">
+import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 import Tools from './tools/index.vue'
 import Sidebar from './sidebar/index.vue'
 import Workspace from './workspace/index.vue'

+ 8 - 6
src/renderer/src/views/designer/workspace/index.vue

@@ -1,19 +1,21 @@
 <template>
   <div class="w-full h-full">
-    <el-splitter layout="vertical">
-      <el-splitter-panel>
+    <SplitterGroup direction="vertical">
+      <SplitterPanel>
         <Stage key="1" />
-      </el-splitter-panel>
-      <el-splitter-panel>
+      </SplitterPanel>
+      <SplitterResizeHandle class="h-2px bg-border" />
+      <SplitterPanel>
         <Stage key="2" />
-      </el-splitter-panel>
-    </el-splitter>
+      </SplitterPanel>
+    </SplitterGroup>
   </div>
 </template>
 
 <script setup lang="ts">
 import { ref, onMounted } from 'vue'
 import Stage from './stage/index.vue'
+import { SplitterGroup, SplitterPanel, SplitterResizeHandle } from 'reka-ui'
 
 const content = ref('')
 onMounted(() => {

+ 65 - 59
src/renderer/src/views/designer/workspace/stage/DesignerCanvas.vue

@@ -2,9 +2,10 @@
   <div class="stage-wrapper" ref="stageWrapperRef">
     <div class="stage" ref="stageRef" :style="getStyles.stageStyle">
       <div ref="tipRef" class="tip-txt" :style="getStyles.tipStyle">页面名称</div>
+      <div class="absolute transparent-bg" :style="getStyles.transpartBg"></div>
       <div
         ref="canvasRef"
-        id="canvasContainer"
+        class="absolute"
         ondragover="return false"
         :style="getStyles.canvasStyle"
         @drop="handleDrop"
@@ -31,13 +32,12 @@ import {
   onBeforeUnmount,
   computed,
   nextTick,
-  watch,
   defineExpose,
   defineProps,
   defineEmits
 } from 'vue'
 
-import { useScroll } from '@vueuse/core'
+import { useScroll, useElementSize, set } from '@vueuse/core'
 import ComponentWrapper from './ComponentWrapper.vue'
 
 const props = defineProps<{
@@ -49,49 +49,45 @@ const emit = defineEmits<{
 const stageWrapperRef: Ref<HTMLElement | null> = ref(null)
 const stageRef: Ref<HTMLElement | null> = ref(null)
 const canvasRef: Ref<HTMLElement | null> = ref(null)
+const { width: clientWidth, height: clientHeight } = useElementSize(stageWrapperRef)
 
 const STAGE_SCALE = 3
 const getStyles = computed(() => {
   const { width = 1280, height = 720, scale } = props.state
-  // 视窗宽高
-  const clientWidth = stageWrapperRef.value!?.clientWidth || 0
-  const clientHeight = stageWrapperRef.value?.clientHeight || 0
+
   // 可滚动距离 至少2倍窗口大小
-  const scrollWidth = width * STAGE_SCALE > clientWidth * 2 ? width * STAGE_SCALE : clientWidth * 2
+  const scrollWidth =
+    width * STAGE_SCALE > clientWidth.value * 2 ? width * STAGE_SCALE : clientWidth.value * 2
   const scrollHeight =
-    height * STAGE_SCALE > clientHeight * 2 ? height * STAGE_SCALE : clientHeight * 2
+    height * STAGE_SCALE > clientHeight.value * 2 ? height * STAGE_SCALE : clientHeight.value * 2
 
   // 计算居中偏移量
-  const canvasOffsetX = -(width - width * scale) / 2 + (clientWidth - width * scale) / 2
-  const canvasOffsetY = -(height - height * scale) / 2 + (clientHeight - height * scale) / 2
+  const canvasOffsetX = -(width - width * scale) / 2 + (clientWidth.value - width * scale) / 2
+  const canvasOffsetY = -(height - height * scale) / 2 + (clientHeight.value - height * scale) / 2
   // 距离左边 = (可滚动距离 - 视窗宽度) / 2 + 缩放实际偏移量
-  const canvasLeft = (scrollWidth - clientWidth) / 2 + canvasOffsetX
+  const canvasLeft = (scrollWidth - clientWidth.value) / 2 + canvasOffsetX
   // 距离顶边 = (可滚动距离 - 视窗高度) / 2 + 缩放实际偏移量
-  const canvasTop = (scrollHeight - clientHeight) / 2 + canvasOffsetY
+  const canvasTop = (scrollHeight - clientHeight.value) / 2 + canvasOffsetY
 
-  const tipOffsetX = (clientWidth - width * scale) / 2
-  const tipOffsetY = (clientHeight - height * scale) / 2 - 20
-  const tipLeft = (scrollWidth - clientWidth) / 2 + tipOffsetX
-  const tipTop = (scrollHeight - clientHeight) / 2 + tipOffsetY
+  const tipOffsetX = (clientWidth.value - width * scale) / 2
+  const tipOffsetY = (clientHeight.value - height * scale) / 2 - 20
+  const tipLeft = (scrollWidth - clientWidth.value) / 2 + tipOffsetX
+  const tipTop = (scrollHeight - clientHeight.value) / 2 + tipOffsetY
 
   emit('changeState', {
-    width,
-    height,
     originX: tipLeft,
     originY: tipTop + 20,
-    viewportWidth: clientWidth,
-    viewportHeight: clientHeight,
     wrapperWidth: scrollWidth,
-    wrapperHeight: scrollHeight
+    wrapperHeight: scrollHeight,
+    viewportWidth: clientWidth.value,
+    viewportHeight: clientHeight.value
   })
-  // const { background } = projectStore.currentPage || {};
-  // const canvasBackground: Record<string, string | undefined> = {};
-  // if(background?.type === 'color') {
-  //   canvasBackground['background-color'] = background.color;
-  // } else if(background?.type === 'image') {
-  //   canvasBackground['background-image'] = `url(${background.image})`;
-  //   // todo 背景填充方式
-  // }
+
+  const startY = tipTop + 20
+  const endX = tipLeft + width * scale
+  const endY = startY + height * scale
+
+  const clipPath = `rect(${startY}px ${endX}px ${endY}px ${tipLeft}px)`
 
   return {
     // 舞台样式
@@ -106,20 +102,23 @@ const getStyles = computed(() => {
       'transform-origin': '50% 50%',
       transform: `scale(${scale})`,
       left: `${canvasLeft}px`,
-      top: `${canvasTop}px`,
-      border: '1px solid #ddd',
-      background: '#fff'
+      top: `${canvasTop}px`
+      // background: '#fff'
     },
     // 提示样式
     tipStyle: {
       left: `${tipLeft}px`,
       top: `${tipTop}px`
+    },
+    // 画布背景
+    transpartBg: {
+      'clip-path': clipPath
     }
   }
 })
 
 useScroll(stageWrapperRef, {
-  throttle: 10,
+  throttle: 0,
   onScroll: () => {
     const scrollTop = stageWrapperRef.value!.scrollTop
     const scrollLeft = stageWrapperRef.value!.scrollLeft
@@ -131,11 +130,11 @@ useScroll(stageWrapperRef, {
 })
 
 /* 设置缩放倍数 */
-const initScale = () => {
+const initScale = async () => {
+  if (!clientWidth.value || !clientHeight.value) return
   // 4为滚动条宽度 40为左右上下间隔20px
-  const windowWidth = stageWrapperRef.value!.clientWidth - 4 - 40
-  const windowHeight = stageWrapperRef.value!.clientHeight - 4 - 40
-
+  const windowWidth = clientWidth.value - 4 - 40
+  const windowHeight = clientHeight.value - 4 - 40
   const { width = 1280, height = 720 } = props.state
   let scale
   let maxScale
@@ -148,8 +147,9 @@ const initScale = () => {
   }
 
   const result = scale > maxScale ? maxScale : scale
+
   emit('changeState', {
-    scale: result > 0.1 ? result : 0.1
+    scale: result > 0.1 ? Number(result) : 0.1
   })
 }
 
@@ -158,16 +158,17 @@ const initStagePosition = async () => {
   await nextTick()
   const scrollWidth = stageWrapperRef.value!.scrollWidth
   const scrollHeight = stageWrapperRef.value!.scrollHeight
-  const clientWidth = stageWrapperRef.value!.clientWidth
-  const clientHeight = stageWrapperRef.value!.clientHeight
 
-  const centerX = (scrollWidth - clientWidth) / 2
-  const centerY = (scrollHeight - clientHeight) / 2
+  const centerX = (scrollWidth - clientWidth.value) / 2
+  const centerY = (scrollHeight - clientHeight.value) / 2
 
   emit('changeState', {
     centerX,
-    centerY
+    centerY,
+    scrollX: centerX,
+    scrollY: centerY
   })
+
   stageWrapperRef.value!.scrollTo(centerX, centerY)
 }
 // 拖拽组件结束 添加组件到指定位置
@@ -179,28 +180,19 @@ const handleDrop = (e: DragEvent) => {
   // TODO 添加组件到画布
 }
 
-/* 适应大小设置 */
-watch(
-  () => [props.state.width, props.state.height, props.state.scale],
-  () => {
-    initScale()
-    initStagePosition()
-  }
-)
-
 defineExpose({
   getPosition: () => canvasRef.value?.getBoundingClientRect()
 })
 
 onMounted(() => {
-  initStagePosition()
-  initScale()
+  setTimeout(() => {
+    initScale()
+    initStagePosition()
+  }, 100)
   window.addEventListener('resize', initScale)
-  window.addEventListener('resize', initStagePosition)
 })
 onBeforeUnmount(() => {
   window.removeEventListener('resize', initScale)
-  window.removeEventListener('resize', initStagePosition)
 })
 </script>
 
@@ -216,13 +208,27 @@ onBeforeUnmount(() => {
 .stage {
   position: relative;
 }
-#canvasContainer {
-  position: absolute;
-}
 .tip-txt {
   position: absolute;
   font-size: 12px;
   line-height: 20px;
   color: #999;
 }
+.transparent-bg {
+  transform: scale(1) !important;
+  width: 100%;
+  height: 100%;
+  background-color: #fff;
+  background-image:
+    linear-gradient(45deg, #c8c8c8 25%, transparent 25%),
+    linear-gradient(-45deg, #c8c8c8 25%, transparent 25%),
+    linear-gradient(45deg, transparent 75%, #c8c8c8 75%),
+    linear-gradient(-45deg, transparent 75%, #c8c8c8 75%);
+  background-size: 10px 10px;
+  background-position:
+    0 0,
+    0 -5px,
+    -5px 5px,
+    5px 0px;
+}
 </style>

+ 32 - 40
src/renderer/src/views/designer/workspace/stage/Scaleplate.vue

@@ -1,5 +1,5 @@
 <template>
-  <div class="scaleplate" id="scaleplate">
+  <div class="scaleplate">
     <!-- 显示/隐藏参考线 -->
     <div class="refer-line-img">
       <LuEye :size="16" />
@@ -15,7 +15,7 @@
     >
       <canvas
         ref="scaleplateHorizontalRef"
-        :style="{ width: windowSize.width + 'px', height: '20px' }"
+        :style="{ width: horizontalWidth + 'px', height: '20px' }"
       />
     </div>
     <div
@@ -28,7 +28,7 @@
     >
       <canvas
         ref="scaleplateVerticalRef"
-        :style="{ width: '20px', height: windowSize.height + 'px' }"
+        :style="{ width: '20px', height: verticalHeight + 'px' }"
       />
     </div>
 
@@ -71,26 +71,27 @@ import { onMounted, ref, onBeforeUnmount, nextTick, watch, defineProps } from 'v
 import { LuEye, LuEyeOff } from 'vue-icons-plus/lu'
 import { UseDraggable } from '@vueuse/components'
 import { drawScaleplate } from './utils'
+import { useElementSize, useResizeObserver } from '@vueuse/core'
 
 const props = defineProps<{
   state: StageState
 }>()
+
 const horizontalRef = ref<HTMLElement | null>(null)
 const verticalRef = ref<HTMLElement | null>(null)
 const scaleplateHorizontalRef = ref<HTMLCanvasElement | null>(null)
 const scaleplateVerticalRef = ref<HTMLCanvasElement | null>(null)
-const windowSize: Ref<{ width: number; height: number }> = ref({
-  width: 0,
-  height: 0
-})
+
+const { width: horizontalWidth, height: horizontalHeight } = useElementSize(horizontalRef)
+const { width: verticalWidth, height: verticalHeight } = useElementSize(verticalRef)
 
 /* 绘制标尺刻度 */
-const handleDrawScaleplate = () => {
+const handleDrawHScaleplate = async () => {
   // 水平轴
   const { scale, scrollX, scrollY, originX, originY } = props.state
   drawScaleplate({
     canvas: scaleplateHorizontalRef.value!,
-    canvasStyleWidth: windowSize.value.width,
+    canvasStyleWidth: horizontalWidth.value,
     canvasStyleHeight: 20,
     direcotion: 'horizontal',
     scale,
@@ -99,11 +100,16 @@ const handleDrawScaleplate = () => {
     originX,
     originY
   })
+}
+
+const handleDrawVScaleplate = async () => {
+  // 水平轴
+  const { scale, scrollX, scrollY, originX, originY } = props.state
   // 垂直轴
   drawScaleplate({
     canvas: scaleplateVerticalRef.value!,
     canvasStyleWidth: 20,
-    canvasStyleHeight: windowSize.value.height,
+    canvasStyleHeight: verticalHeight.value,
     direcotion: 'vertical',
     scale,
     scrollX,
@@ -169,52 +175,38 @@ const handleDragReferLine = ({ x, y }: { x: number; y: number }, _: PointerEvent
 }
 /* ===============================参考线结束================================= */
 
-/* 设置刻度宽高 */
-const setWindowSize = async () => {
-  windowSize.value = {
-    width: horizontalRef.value!.clientWidth,
-    height: verticalRef.value!.clientHeight
-  }
-  await nextTick()
-  handleDrawScaleplate()
-}
-
 watch(
   () => [
     props.state.scrollX,
     props.state.scrollY,
     props.state.scale,
-    props.state.viewportHeight,
-    props.state.viewportWidth
+    props.state.viewportWidth,
+    props.state.originX
   ],
   () => {
-    handleDrawScaleplate()
+    handleDrawHScaleplate()
   },
   { immediate: false }
 )
 
-let observer: ResizeObserver
-onMounted(() => {
-  setWindowSize()
+watch(
+  () => [props.state.scrollY, props.state.scale, props.state.viewportHeight, props.state.originY],
+  () => {
+    handleDrawVScaleplate()
+  },
+  { immediate: false }
+)
 
-  observer = new ResizeObserver(setWindowSize)
-  /* 监听画布尺寸变化 */
-  const element = document.getElementsByClassName('workspace-wrapper')?.[0]
-  if (element) {
-    observer.observe(element)
-  }
-})
-onBeforeUnmount(() => {
-  observer.disconnect()
-})
+useResizeObserver(horizontalRef, handleDrawHScaleplate)
+useResizeObserver(verticalRef, handleDrawVScaleplate)
 </script>
 
 <style lang="less" scoped>
 .refer-line-img {
   width: 20px;
   height: 20px;
-  border-bottom: solid 1px #eee;
-  border-right: solid 1px #eee;
+  border-bottom: solid 1px var(--border-color);
+  border-right: solid 1px var(--border-color);
   cursor: pointer;
   display: flex;
   align-items: center;
@@ -226,7 +218,7 @@ onBeforeUnmount(() => {
   top: 0;
   width: calc(100% - 20px);
   height: 20px;
-  border-bottom: solid 1px #eee;
+  border-bottom: solid 1px var(--border-color);
 }
 .scaleplate-vertical {
   position: absolute;
@@ -234,7 +226,7 @@ onBeforeUnmount(() => {
   left: 0;
   height: calc(100% - 20px);
   width: 20px;
-  border-right: solid 1px #eee;
+  border-right: solid 1px var(--border-color);
 }
 
 .refer-line {

+ 18 - 41
src/renderer/src/views/designer/workspace/stage/index.vue

@@ -1,7 +1,7 @@
 <template>
   <div
     ref="boxRef"
-    style="width: 100%; height: 100%; position: relative"
+    class="w-full h-full relative"
     @mousedown="handleMouseDown"
     @mousemove="handleMouseMove"
     @mouseup="handleMouseUp"
@@ -16,29 +16,31 @@
           <span style="margin-right: 12px">画布尺寸:</span>
           <span>画布自适应:</span>
         </div>
-        <div class="bottom-right">
+        <div class="bottom-right flex items-center">
           <el-button
             size="small"
             type="text"
             :disabled="state.scale <= 0.1"
             @click="handleSizeChange(Number(state.scale) - 0.1)"
           >
-            <LuMinusCircle />
+            <LuMinusCircle :size="16" />
           </el-button>
-          <el-autocomplete
+          <el-slider
+            v-model="state.scale"
+            :min="0.1"
+            :max="4"
+            :step="0.1"
+            :show-tooltip="false"
             size="small"
-            style="width: 120px"
-            :options="sizeOptions"
-            :value="(state.scale * 100).toFixed(0) + '%'"
-            @change="handleSizeChange"
-          />
+            style="width: 140px"
+          ></el-slider>
           <el-button
             size="small"
             type="text"
             :disabled="state.scale >= 4"
             @click="handleSizeChange(Number(state.scale) + 0.1)"
           >
-            <LuPlusCircle />
+            <LuPlusCircle :size="16" />
           </el-button>
         </div>
       </div>
@@ -74,20 +76,6 @@ const state = reactive<StageState>({
   wrapperHeight: 0
 })
 
-const sizeOptions = [
-  { value: 0.1, label: '10%' },
-  { value: 0.25, label: '25%' },
-  { value: 0.5, label: '50%' },
-  { value: 0.75, label: '75%' },
-  { value: 1, label: '100%' },
-  { value: 1.25, label: '125%' },
-  { value: 1.5, label: '150%' },
-  { value: 2, label: '200%' },
-  { value: 3, label: '300%' },
-  { value: 4, label: '400%' },
-  { value: 0, label: '适应大小' }
-]
-
 // 修改状态
 const handleSetState = (newState: Partial<StageState>) => {
   Object.entries(newState).forEach(([key, value]) => {
@@ -95,22 +83,11 @@ const handleSetState = (newState: Partial<StageState>) => {
   })
 }
 
-const handleSizeChange = (val: any) => {
-  if (Number.isFinite(val)) {
-    // 为0时为自动适应大小
-    if (val === 0) {
-      state.scale = 0
-      return
-    }
-    state.scale = (val as number) < 0.1 ? 0.1 : val
-  }
-  if (typeof val === 'string') {
-    const n = +(val + '').replace('%', '')
-    if (Number.isNaN(n) && n >= 10 && n <= 400) {
-      state.scale = (n / 100).toFixed(2) as unknown as number
-    } else {
-      state.scale = 0.1
-    }
+const handleSizeChange = (n: number) => {
+  if (Number.isNaN(n) && n >= 10 && n <= 400) {
+    state.scale = Number((n / 100).toFixed(2))
+  } else {
+    state.scale = 0.1
   }
 }
 
@@ -203,7 +180,7 @@ const handleSelectComponent = (startX: number, startY: number, endX: number, end
   }
   &-bottom {
     padding: 0 20px;
-    height: 40px;
+    height: 24px;
     background: var(--background-primary);
     border-top: solid 1px var(--border-color);
     font-size: 12px;