| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212 |
- <template>
- <div class="text-board-wrapper" :class="{ selected }"
- :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="'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, ref } from 'vue';
- const emit = defineEmits(['resize']);
- interface Props {
- text?: string;
- color?: string;
- fontSize?: string | number;
- fontWeight?: string | number;
- align?: 'left' | 'center' | 'right';
- width?: number;
- height?: number;
- selected?: boolean;
- borderRadius?: number;
- }
- 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',
- fontWeight: props.fontWeight || 'normal',
- textAlign: props.align || 'center',
- width: '100%',
- height: '100%',
- display: 'flex',
- alignItems: 'center',
- justifyContent: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center',
- 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) {
- e.stopPropagation();
- const startX = e.clientX,
- 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,
- 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);
- }
- // 根据拖拽方向获取对应的光标样式
- 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>
- .text-board-wrapper.selected {
- outline: 2px dashed #409eff;
- outline-offset: 0;
- }
- .text-board-wrapper {
- cursor: pointer;
- }
- .text-board-wrapper.selected {
- cursor: move;
- }
- .resize-handle {
- width: 8px;
- height: 8px;
- background: #fff;
- border: 1.5px solid #409eff;
- border-radius: 50%;
- position: absolute;
- z-index: 2;
- }
- /* 角落控制点 */
- .resize-handle.tr {
- top: -4px;
- right: -4px;
- cursor: ne-resize;
- }
- .resize-handle.tl {
- top: -4px;
- left: -4px;
- cursor: nw-resize;
- }
- .resize-handle.br {
- bottom: -4px;
- right: -4px;
- cursor: se-resize;
- }
- .resize-handle.bl {
- bottom: -4px;
- 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;
- }
- .text-board {
- width: 100%;
- height: 100%;
- background: transparent;
- outline: none;
- cursor: text;
- user-select: text;
- }
- </style>
|