Forráskód Böngészése

style: 分屏组图层组件支持边缘横向和纵向拉伸拖拽

lihao16 1 hónapja
szülő
commit
821aa74fb9

+ 11 - 7
smsb-plus-ui/src/views/smsb/itemProgram/EditProgram.vue

@@ -44,7 +44,7 @@
         </template>
       </div>
 
-      <div class="sidebar-recycle-bin" @dragover.prevent="onRecycleDrop($event)" title="拖动组件到此处删除">
+      <div class="sidebar-recycle-bin" @dragover.prevent @drop="onRecycleDrop" title="拖动组件到此处删除">
         <el-icon><Delete /></el-icon>
         <span>拖拽到此移除</span>
       </div>
@@ -315,11 +315,15 @@ function alignComponent(type: string) {
 const sidebarDrag = ref<{ item: any; idx: number } | null>(null);
 
 function onRecycleDrop(e: DragEvent) {
+  e.preventDefault();
   const depth = e.dataTransfer?.getData('component-depth');
   if (!depth) return;
   const idx = editorContent.value.elements.findIndex((el) => String(el.depth) === depth && el.type !== 'canvas');
   if (idx > -1) {
     editorContent.value.elements.splice(idx, 1);
+    if (selectedComponent.value && String(selectedComponent.value.depth) === depth) {
+      selectedComponent.value = null;
+    }
     ElMessage.success('组件已移除');
   }
 }
@@ -490,12 +494,12 @@ function onCanvasDrop(e: DragEvent) {
   const y = e.clientY - (rect?.top || 0);
 
   const componentDefaults = {
-    text: { type: 'text', text: '新文本', color: '#222', fontSize: 24, fontWeight: 'normal', align: 'center', width: 200, height: 40, borderRadius: 0 },
-    scrollingText: { type: 'scrollingText', text: '新滚动文本', color: '#222', fontSize: 24, fontWeight: 'normal', align: 'center', speed: 50, width: 300, height: 40, borderRadius: 0 },
-    mediaAsset: { type: 'mediaAsset', mediaId: '', width: 120, height: 120, borderRadius: 0 },
-    live: { type: 'live', liveUrl: '', playAudio: true, width: 200, height: 120, borderRadius: 0 },
-    webPage: { type: 'webPage', url: 'https://example.com', width: 300, height: 200, borderRadius: 0 },
-    clock: { type: 'clock', format: '24h', width: 200, height: 60 },
+    text: { type: 'text', text: '新文本', color: '#222', fontSize: 24, fontWeight: 'normal', align: 'center', width: 400, height: 100, borderRadius: 0 },
+    scrollingText: { type: 'scrollingText', text: '新滚动文本', color: '#222', fontSize: 24, fontWeight: 'normal', align: 'center', speed: 50, width: 400, height: 80, borderRadius: 0 },
+    mediaAsset: { type: 'mediaAsset', mediaId: '', width: 400, height: 400, borderRadius: 0 },
+    live: { type: 'live', liveUrl: '', playAudio: true, width: 400, height: 400, borderRadius: 0 },
+    webPage: { type: 'webPage', url: 'https://example.com', width: 400, height: 400, borderRadius: 0 },
+    clock: { type: 'clock', format: 'date', width: 400, height: 60 },
   };
 
   const newComponent = {

+ 67 - 7
smsb-plus-ui/src/views/smsb/itemProgram/component/LiveBoard.vue

@@ -24,8 +24,12 @@
       </div>
     </div>
     <template v-if="selected">
-      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
-        @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 角落控制点 -->
+      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="'corner-' + dir" class="resize-handle" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 边缘控制点 -->
+      <div v-for="dir in ['n', 'e', 's', 'w']" :key="'edge-' + dir" class="resize-handle edge" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
     </template>
   </div>
 </template>
@@ -64,17 +68,45 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   startY = e.clientY;
   startW = props.width;
   startH = props.height;
+  const startXPos = parseFloat(e.target?.parentElement?.style.left || '0');
+  const startYPos = parseFloat(e.target?.parentElement?.style.top || '0');
+
   function onMouseMove(ev: MouseEvent) {
     let dx = ev.clientX - startX;
     let dy = ev.clientY - startY;
     let newW = startW;
     let newH = startH;
-    if (dir.includes('e')) newW = Math.max(40, startW + dx);
-    if (dir.includes('s')) newH = Math.max(40, startH + dy);
-    if (dir.includes('w')) newW = Math.max(40, startW - dx);
-    if (dir.includes('n')) newH = Math.max(40, startH - dy);
-    emit('resize', { width: newW, height: newH });
+    let newX = startXPos;
+    let newY = startYPos;
+
+    // 处理边缘控制点
+    if (dir === 'e') {
+      newW = Math.max(40, startW + dx);
+    } else if (dir === 'w') {
+      newW = Math.max(40, startW - dx);
+      newX += dx;
+    } else if (dir === 's') {
+      newH = Math.max(40, startH + dy);
+    } else if (dir === 'n') {
+      newH = Math.max(40, startH - dy);
+      newY += dy;
+    }
+    // 处理角落控制点
+    else {
+      if (dir.includes('e')) newW = Math.max(40, startW + dx);
+      if (dir.includes('s')) newH = Math.max(40, startH + dy);
+      if (dir.includes('w')) newW = Math.max(40, startW - dx);
+      if (dir.includes('n')) newH = Math.max(40, startH - dy);
+    }
+
+    emit('resize', {
+      width: newW,
+      height: newH,
+      x: newX,
+      y: newY
+    });
   }
+
   function onMouseUp() {
     window.removeEventListener('mousemove', onMouseMove);
     window.removeEventListener('mouseup', onMouseUp);
@@ -266,4 +298,32 @@ function select() {
   right: -4px;
   cursor: se-resize;
 }
+/* 边缘控制点 */
+.resize-handle.edge.resize-n {
+  top: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: n-resize;
+}
+
+.resize-handle.edge.resize-e {
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+  cursor: e-resize;
+}
+
+.resize-handle.edge.resize-s {
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: s-resize;
+}
+
+.resize-handle.edge.resize-w {
+  top: 50%;
+  left: -4px;
+  transform: translateY(-50%);
+  cursor: w-resize;
+}
 </style>

+ 65 - 8
smsb-plus-ui/src/views/smsb/itemProgram/component/MediaAssetBoard.vue

@@ -44,8 +44,12 @@
 
     <!-- Resize -->
     <template v-if="selected && !props.readonly">
-      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
-        @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 角落控制点 -->
+      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="'corner-' + dir" class="resize-handle" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 边缘控制点 -->
+      <div v-for="dir in ['n', 'e', 's', 'w']" :key="'edge-' + dir" class="resize-handle edge" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
     </template>
   </div>
 </template>
@@ -242,24 +246,49 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   startY = e.clientY;
   startW = props.width;
   startH = props.height;
+  const startXPos = parseFloat(e.target?.parentElement?.style.left || '0');
+  const startYPos = parseFloat(e.target?.parentElement?.style.top || '0');
 
   function onMouseMove(ev: MouseEvent) {
     let dx = ev.clientX - startX;
     let dy = ev.clientY - startY;
     let newW = startW;
     let newH = startH;
-    if (dir.includes('e')) newW = Math.max(40, startW + dx);
-    if (dir.includes('s')) newH = Math.max(40, startH + dy);
-    if (dir.includes('w')) newW = Math.max(40, startW - dx);
-    if (dir.includes('n')) newH = Math.max(40, startH - dy);
-    emit('resize', { width: newW, height: newH });
+    let newX = startXPos;
+    let newY = startYPos;
+
+    // 处理边缘控制点
+    if (dir === 'e') {
+      newW = Math.max(40, startW + dx);
+    } else if (dir === 'w') {
+      newW = Math.max(40, startW - dx);
+      newX += dx;
+    } else if (dir === 's') {
+      newH = Math.max(40, startH + dy);
+    } else if (dir === 'n') {
+      newH = Math.max(40, startH - dy);
+      newY += dy;
+    }
+    // 处理角落控制点
+    else {
+      if (dir.includes('e')) newW = Math.max(40, startW + dx);
+      if (dir.includes('s')) newH = Math.max(40, startH + dy);
+      if (dir.includes('w')) newW = Math.max(40, startW - dx);
+      if (dir.includes('n')) newH = Math.max(40, startH - dy);
+    }
+
+    emit('resize', {
+      width: newW,
+      height: newH,
+      x: newX,
+      y: newY
+    });
   }
 
   function onMouseUp() {
     window.removeEventListener('mousemove', onMouseMove);
     window.removeEventListener('mouseup', onMouseUp);
   }
-
   window.addEventListener('mousemove', onMouseMove);
   window.addEventListener('mouseup', onMouseUp);
 }
@@ -377,4 +406,32 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   bottom: -6px;
   cursor: nwse-resize;
 }
+/* 边缘控制点 */
+.resize-handle.edge.resize-n {
+  top: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: n-resize;
+}
+
+.resize-handle.edge.resize-e {
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+  cursor: e-resize;
+}
+
+.resize-handle.edge.resize-s {
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: s-resize;
+}
+
+.resize-handle.edge.resize-w {
+  top: 50%;
+  left: -4px;
+  transform: translateY(-50%);
+  cursor: w-resize;
+}
 </style>

+ 81 - 9
smsb-plus-ui/src/views/smsb/itemProgram/component/ScrollingTextBoard.vue

@@ -1,12 +1,16 @@
 <template>
   <div class="scrolling-text-board-wrapper" :class="{ selected }"
-    :style="{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }">
+       :style="{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }">
     <div class="scrolling-text-board" :style="textStyle">
       <div class="scrolling-text-content" :style="scrollStyle" ref="scrollRef">{{ text }}</div>
     </div>
     <template v-if="selected">
-      <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="dir" class="resize-handle" :class="dir"
-        @mousedown.stop="onResizeMouseDown($event, dir)"></div>
+      <!-- 角落控制点 -->
+      <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="'corner-' + dir" class="resize-handle" :class="dir"
+           @mousedown.stop="onResizeMouseDown($event, dir)"></div>
+      <!-- 边缘控制点 -->
+      <div v-for="dir in ['t', 'r', 'b', 'l']" :key="'edge-' + dir" class="resize-handle edge" :class="dir"
+           @mousedown.stop="onResizeMouseDown($event, dir)"></div>
     </template>
   </div>
 </template>
@@ -99,19 +103,48 @@ function onResizeMouseDown(e: MouseEvent, dir: string) {
     startY = e.clientY;
   const startWidth = Number(props.width) || 200;
   const startHeight = Number(props.height) || 40;
+  const startXPos = parseFloat(e.target?.parentElement?.parentElement?.style.left || '0');
+  const startYPos = parseFloat(e.target?.parentElement?.parentElement?.style.top || '0');
+
   function onMove(ev: MouseEvent) {
     let newWidth = startWidth,
-      newHeight = startHeight;
-    if (dir.includes('r')) newWidth += ev.clientX - startX;
-    if (dir.includes('l')) newWidth -= ev.clientX - startX;
-    if (dir.includes('b')) newHeight += ev.clientY - startY;
-    if (dir.includes('t')) newHeight -= ev.clientY - startY;
-    emit('resize', { width: Math.max(20, newWidth), height: Math.max(20, newHeight) });
+      newHeight = startHeight,
+      newX = startXPos,
+      newY = startYPos;
+
+    // 处理边缘控制点
+    if (dir === 'r') {
+      newWidth += ev.clientX - startX;
+    } else if (dir === 'l') {
+      newWidth -= ev.clientX - startX;
+      newX += ev.clientX - startX;
+    } else if (dir === 'b') {
+      newHeight += ev.clientY - startY;
+    } else if (dir === 't') {
+      newHeight -= ev.clientY - startY;
+      newY += ev.clientY - startY;
+    }
+    // 处理角落控制点
+    else {
+      if (dir.includes('r')) newWidth += ev.clientX - startX;
+      if (dir.includes('l')) newWidth -= ev.clientX - startX;
+      if (dir.includes('b')) newHeight += ev.clientY - startY;
+      if (dir.includes('t')) newHeight -= ev.clientY - startY;
+    }
+
+    emit('resize', {
+      width: Math.max(20, newWidth),
+      height: Math.max(20, newHeight),
+      x: newX,
+      y: newY
+    });
   }
+
   function onUp() {
     document.removeEventListener('mousemove', onMove);
     document.removeEventListener('mouseup', onUp);
   }
+
   document.addEventListener('mousemove', onMove);
   document.addEventListener('mouseup', onUp);
 }
@@ -151,6 +184,7 @@ function onResizeMouseDown(e: MouseEvent, dir: string) {
   z-index: 2;
 }
 
+/* 角落控制点 */
 .resize-handle.tr {
   top: -4px;
   right: -4px;
@@ -174,4 +208,42 @@ function onResizeMouseDown(e: MouseEvent, dir: string) {
   left: -4px;
   cursor: sw-resize;
 }
+
+/* 边缘控制点 */
+.resize-handle.edge.t {
+  top: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: n-resize;
+}
+
+.resize-handle.edge.r {
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+  cursor: e-resize;
+}
+
+.resize-handle.edge.b {
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: s-resize;
+}
+
+.resize-handle.edge.l {
+  top: 50%;
+  left: -4px;
+  transform: translateY(-50%);
+  cursor: w-resize;
+}
+
+.scrolling-text-content {
+  position: absolute;
+  left: 0;
+  transform: translateY(-50%);
+  white-space: nowrap;
+  will-change: left;
+  user-select: none;
+}
 </style>

+ 107 - 11
smsb-plus-ui/src/views/smsb/itemProgram/component/TextBoard.vue

@@ -1,16 +1,22 @@
 <template>
   <div class="text-board-wrapper" :class="{ selected }"
-    :style="{ width: '100%', height: '100%', position: 'relative' }">
+       :style="{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }"
+       @mouseenter="isHovered = true"
+       @mouseleave="isHovered = false">
     <div class="text-board" :style="textStyle">{{ text }}</div>
     <template v-if="selected">
-      <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="dir" class="resize-handle" :class="dir"
-        @mousedown.stop="onResizeMouseDown($event, dir)"></div>
+      <!-- 角落控制点 -->
+      <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="'corner-' + dir" class="resize-handle" :class="dir"
+           @mousedown.stop="onResizeMouseDown($event, dir)"></div>
+      <!-- 边缘控制点 -->
+      <div v-for="dir in ['t', 'r', 'b', 'l']" :key="'edge-' + dir" class="resize-handle edge" :class="dir"
+           @mousedown.stop="onResizeMouseDown($event, dir)"></div>
     </template>
   </div>
 </template>
 
 <script setup lang="ts">
-import { computed } from 'vue';
+import { computed, ref } from 'vue';
 const emit = defineEmits(['resize']);
 interface Props {
   text?: string;
@@ -25,6 +31,8 @@ interface Props {
 }
 const props = defineProps<Props>();
 const selected = computed(() => !!props.selected);
+const isHovered = ref(false);
+
 const textStyle = computed(() => ({
   color: props.color || '#222',
   fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
@@ -38,6 +46,7 @@ const textStyle = computed(() => ({
   userSelect: 'none',
   borderRadius: props.borderRadius ? `${props.borderRadius}px` : '0',
   overflow: 'hidden',
+  cursor: isHovered.value ? (selected.value ? 'move' : 'pointer') : 'default'
 }));
 
 function onResizeMouseDown(e: MouseEvent, dir: string) {
@@ -46,23 +55,72 @@ function onResizeMouseDown(e: MouseEvent, dir: string) {
     startY = e.clientY;
   const startWidth = Number(props.width) || 200;
   const startHeight = Number(props.height) || 40;
+  const startXPos = parseFloat(e.target?.parentElement?.style.left || '0');
+  const startYPos = parseFloat(e.target?.parentElement?.style.top || '0');
+
+  // 设置拖拽时的光标样式
+  document.body.style.cursor = getResizeCursor(dir);
+
   function onMove(ev: MouseEvent) {
     let newWidth = startWidth,
-      newHeight = startHeight;
-    if (dir.includes('r')) newWidth += ev.clientX - startX;
-    if (dir.includes('l')) newWidth -= ev.clientX - startX;
-    if (dir.includes('b')) newHeight += ev.clientY - startY;
-    if (dir.includes('t')) newHeight -= ev.clientY - startY;
-    emit('resize', { width: Math.max(20, newWidth), height: Math.max(20, newHeight) });
+      newHeight = startHeight,
+      newX = startXPos,
+      newY = startYPos;
+
+    // 处理边缘控制点
+    if (dir === 'r') {
+      newWidth += ev.clientX - startX;
+    } else if (dir === 'l') {
+      newWidth -= ev.clientX - startX;
+      newX += ev.clientX - startX;
+    } else if (dir === 'b') {
+      newHeight += ev.clientY - startY;
+    } else if (dir === 't') {
+      newHeight -= ev.clientY - startY;
+      newY += ev.clientY - startY;
+    }
+    // 处理角落控制点
+    else {
+      if (dir.includes('r')) newWidth += ev.clientX - startX;
+      if (dir.includes('l')) newWidth -= ev.clientX - startX;
+      if (dir.includes('b')) newHeight += ev.clientY - startY;
+      if (dir.includes('t')) newHeight -= ev.clientY - startY;
+    }
+
+    emit('resize', {
+      width: Math.max(20, newWidth),
+      height: Math.max(20, newHeight),
+      x: newX,
+      y: newY
+    });
   }
+
   function onUp() {
     document.removeEventListener('mousemove', onMove);
     document.removeEventListener('mouseup', onUp);
+    // 恢复光标样式
+    document.body.style.cursor = '';
   }
   document.addEventListener('mousemove', onMove);
   document.addEventListener('mouseup', onUp);
 }
-const text = computed(() => props.text || '双击编辑文本');
+
+// 根据拖拽方向获取对应的光标样式
+function getResizeCursor(dir: string) {
+  const cursors: Record<string, string> = {
+    't': 'n-resize',
+    'r': 'e-resize',
+    'b': 's-resize',
+    'l': 'w-resize',
+    'tr': 'ne-resize',
+    'tl': 'nw-resize',
+    'br': 'se-resize',
+    'bl': 'sw-resize'
+  };
+  return cursors[dir] || 'move';
+}
+
+const text = computed(() => props.text || '文本内容');
 </script>
 
 <style scoped>
@@ -71,6 +129,14 @@ const text = computed(() => props.text || '双击编辑文本');
   outline-offset: 0;
 }
 
+.text-board-wrapper {
+  cursor: pointer;
+}
+
+.text-board-wrapper.selected {
+  cursor: move;
+}
+
 .resize-handle {
   width: 8px;
   height: 8px;
@@ -81,6 +147,7 @@ const text = computed(() => props.text || '双击编辑文本');
   z-index: 2;
 }
 
+/* 角落控制点 */
 .resize-handle.tr {
   top: -4px;
   right: -4px;
@@ -105,6 +172,35 @@ const text = computed(() => props.text || '双击编辑文本');
   cursor: sw-resize;
 }
 
+/* 边缘控制点 */
+.resize-handle.edge.t {
+  top: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: n-resize;
+}
+
+.resize-handle.edge.r {
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+  cursor: e-resize;
+}
+
+.resize-handle.edge.b {
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: s-resize;
+}
+
+.resize-handle.edge.l {
+  top: 50%;
+  left: -4px;
+  transform: translateY(-50%);
+  cursor: w-resize;
+}
+
 .text-board {
   width: 100%;
   height: 100%;

+ 67 - 7
smsb-plus-ui/src/views/smsb/itemProgram/component/WebPageBoard.vue

@@ -17,8 +17,12 @@
     </div>
 
     <template v-if="selected">
-      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="dir" class="resize-handle" :class="'resize-' + dir"
-        @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 角落控制点 -->
+      <div v-for="dir in ['nw', 'ne', 'sw', 'se']" :key="'corner-' + dir" class="resize-handle" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
+      <!-- 边缘控制点 -->
+      <div v-for="dir in ['n', 'e', 's', 'w']" :key="'edge-' + dir" class="resize-handle edge" :class="'resize-' + dir"
+           @mousedown.stop="onResizeMouseDown(dir, $event)" />
     </template>
   </div>
 </template>
@@ -51,17 +55,45 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   startY = e.clientY;
   startW = props.width;
   startH = props.height;
+  const startXPos = parseFloat(e.target?.parentElement?.style.left || '0');
+  const startYPos = parseFloat(e.target?.parentElement?.style.top || '0');
+
   function onMouseMove(ev: MouseEvent) {
     let dx = ev.clientX - startX;
     let dy = ev.clientY - startY;
     let newW = startW;
     let newH = startH;
-    if (dir.includes('e')) newW = Math.max(40, startW + dx);
-    if (dir.includes('s')) newH = Math.max(40, startH + dy);
-    if (dir.includes('w')) newW = Math.max(40, startW - dx);
-    if (dir.includes('n')) newH = Math.max(40, startH - dy);
-    emit('resize', { width: newW, height: newH });
+    let newX = startXPos;
+    let newY = startYPos;
+
+    // 处理边缘控制点
+    if (dir === 'e') {
+      newW = Math.max(40, startW + dx);
+    } else if (dir === 'w') {
+      newW = Math.max(40, startW - dx);
+      newX += dx;
+    } else if (dir === 's') {
+      newH = Math.max(40, startH + dy);
+    } else if (dir === 'n') {
+      newH = Math.max(40, startH - dy);
+      newY += dy;
+    }
+    // 处理角落控制点
+    else {
+      if (dir.includes('e')) newW = Math.max(40, startW + dx);
+      if (dir.includes('s')) newH = Math.max(40, startH + dy);
+      if (dir.includes('w')) newW = Math.max(40, startW - dx);
+      if (dir.includes('n')) newH = Math.max(40, startH - dy);
+    }
+
+    emit('resize', {
+      width: newW,
+      height: newH,
+      x: newX,
+      y: newY
+    });
   }
+
   function onMouseUp() {
     window.removeEventListener('mousemove', onMouseMove);
     window.removeEventListener('mouseup', onMouseUp);
@@ -150,4 +182,32 @@ function onResizeMouseDown(dir: string, e: MouseEvent) {
   bottom: -6px;
   cursor: nwse-resize;
 }
+/* 边缘控制点 */
+.resize-handle.edge.resize-n {
+  top: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: n-resize;
+}
+
+.resize-handle.edge.resize-e {
+  top: 50%;
+  right: -4px;
+  transform: translateY(-50%);
+  cursor: e-resize;
+}
+
+.resize-handle.edge.resize-s {
+  bottom: -4px;
+  left: 50%;
+  transform: translateX(-50%);
+  cursor: s-resize;
+}
+
+.resize-handle.edge.resize-w {
+  top: 50%;
+  left: -4px;
+  transform: translateY(-50%);
+  cursor: w-resize;
+}
 </style>