|
@@ -1,16 +1,22 @@
|
|
|
<template>
|
|
<template>
|
|
|
<div class="text-board-wrapper" :class="{ selected }"
|
|
<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>
|
|
<div class="text-board" :style="textStyle">{{ text }}</div>
|
|
|
<template v-if="selected">
|
|
<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>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
|
<script setup lang="ts">
|
|
|
-import { computed } from 'vue';
|
|
|
|
|
|
|
+import { computed, ref } from 'vue';
|
|
|
const emit = defineEmits(['resize']);
|
|
const emit = defineEmits(['resize']);
|
|
|
interface Props {
|
|
interface Props {
|
|
|
text?: string;
|
|
text?: string;
|
|
@@ -25,6 +31,8 @@ interface Props {
|
|
|
}
|
|
}
|
|
|
const props = defineProps<Props>();
|
|
const props = defineProps<Props>();
|
|
|
const selected = computed(() => !!props.selected);
|
|
const selected = computed(() => !!props.selected);
|
|
|
|
|
+const isHovered = ref(false);
|
|
|
|
|
+
|
|
|
const textStyle = computed(() => ({
|
|
const textStyle = computed(() => ({
|
|
|
color: props.color || '#222',
|
|
color: props.color || '#222',
|
|
|
fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
|
|
fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
|
|
@@ -38,6 +46,7 @@ const textStyle = computed(() => ({
|
|
|
userSelect: 'none',
|
|
userSelect: 'none',
|
|
|
borderRadius: props.borderRadius ? `${props.borderRadius}px` : '0',
|
|
borderRadius: props.borderRadius ? `${props.borderRadius}px` : '0',
|
|
|
overflow: 'hidden',
|
|
overflow: 'hidden',
|
|
|
|
|
+ cursor: isHovered.value ? (selected.value ? 'move' : 'pointer') : 'default'
|
|
|
}));
|
|
}));
|
|
|
|
|
|
|
|
function onResizeMouseDown(e: MouseEvent, dir: string) {
|
|
function onResizeMouseDown(e: MouseEvent, dir: string) {
|
|
@@ -46,23 +55,72 @@ function onResizeMouseDown(e: MouseEvent, dir: string) {
|
|
|
startY = e.clientY;
|
|
startY = e.clientY;
|
|
|
const startWidth = Number(props.width) || 200;
|
|
const startWidth = Number(props.width) || 200;
|
|
|
const startHeight = Number(props.height) || 40;
|
|
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) {
|
|
function onMove(ev: MouseEvent) {
|
|
|
let newWidth = startWidth,
|
|
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() {
|
|
function onUp() {
|
|
|
document.removeEventListener('mousemove', onMove);
|
|
document.removeEventListener('mousemove', onMove);
|
|
|
document.removeEventListener('mouseup', onUp);
|
|
document.removeEventListener('mouseup', onUp);
|
|
|
|
|
+ // 恢复光标样式
|
|
|
|
|
+ document.body.style.cursor = '';
|
|
|
}
|
|
}
|
|
|
document.addEventListener('mousemove', onMove);
|
|
document.addEventListener('mousemove', onMove);
|
|
|
document.addEventListener('mouseup', onUp);
|
|
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>
|
|
</script>
|
|
|
|
|
|
|
|
<style scoped>
|
|
<style scoped>
|
|
@@ -71,6 +129,14 @@ const text = computed(() => props.text || '双击编辑文本');
|
|
|
outline-offset: 0;
|
|
outline-offset: 0;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+.text-board-wrapper {
|
|
|
|
|
+ cursor: pointer;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+.text-board-wrapper.selected {
|
|
|
|
|
+ cursor: move;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
.resize-handle {
|
|
.resize-handle {
|
|
|
width: 8px;
|
|
width: 8px;
|
|
|
height: 8px;
|
|
height: 8px;
|
|
@@ -81,6 +147,7 @@ const text = computed(() => props.text || '双击编辑文本');
|
|
|
z-index: 2;
|
|
z-index: 2;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+/* 角落控制点 */
|
|
|
.resize-handle.tr {
|
|
.resize-handle.tr {
|
|
|
top: -4px;
|
|
top: -4px;
|
|
|
right: -4px;
|
|
right: -4px;
|
|
@@ -105,6 +172,35 @@ const text = computed(() => props.text || '双击编辑文本');
|
|
|
cursor: sw-resize;
|
|
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 {
|
|
.text-board {
|
|
|
width: 100%;
|
|
width: 100%;
|
|
|
height: 100%;
|
|
height: 100%;
|