TextBoard.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. <template>
  2. <div class="text-board-wrapper" :class="{ selected }"
  3. :style="{ width: '100%', height: '100%', position: 'relative', overflow: 'hidden' }"
  4. @mouseenter="isHovered = true"
  5. @mouseleave="isHovered = false">
  6. <div class="text-board" :style="textStyle">{{ text }}</div>
  7. <template v-if="selected">
  8. <!-- 角落控制点 -->
  9. <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="'corner-' + dir" class="resize-handle" :class="dir"
  10. @mousedown.stop="onResizeMouseDown($event, dir)"></div>
  11. <!-- 边缘控制点 -->
  12. <div v-for="dir in ['t', 'r', 'b', 'l']" :key="'edge-' + dir" class="resize-handle edge" :class="dir"
  13. @mousedown.stop="onResizeMouseDown($event, dir)"></div>
  14. </template>
  15. </div>
  16. </template>
  17. <script setup lang="ts">
  18. import { computed, ref } from 'vue';
  19. const emit = defineEmits(['resize']);
  20. interface Props {
  21. text?: string;
  22. color?: string;
  23. fontSize?: string | number;
  24. fontWeight?: string | number;
  25. align?: 'left' | 'center' | 'right';
  26. width?: number;
  27. height?: number;
  28. selected?: boolean;
  29. borderRadius?: number;
  30. }
  31. const props = defineProps<Props>();
  32. const selected = computed(() => !!props.selected);
  33. const isHovered = ref(false);
  34. const textStyle = computed(() => ({
  35. color: props.color || '#222',
  36. fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
  37. fontWeight: props.fontWeight || 'normal',
  38. textAlign: props.align || 'center',
  39. width: '100%',
  40. height: '100%',
  41. display: 'flex',
  42. alignItems: 'center',
  43. justifyContent: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center',
  44. userSelect: 'none',
  45. borderRadius: props.borderRadius ? `${props.borderRadius}px` : '0',
  46. overflow: 'hidden',
  47. cursor: isHovered.value ? (selected.value ? 'move' : 'pointer') : 'default'
  48. }));
  49. function onResizeMouseDown(e: MouseEvent, dir: string) {
  50. e.stopPropagation();
  51. const startX = e.clientX,
  52. startY = e.clientY;
  53. const startWidth = Number(props.width) || 200;
  54. const startHeight = Number(props.height) || 40;
  55. const startXPos = parseFloat(e.target?.parentElement?.style.left || '0');
  56. const startYPos = parseFloat(e.target?.parentElement?.style.top || '0');
  57. // 设置拖拽时的光标样式
  58. document.body.style.cursor = getResizeCursor(dir);
  59. function onMove(ev: MouseEvent) {
  60. let newWidth = startWidth,
  61. newHeight = startHeight,
  62. newX = startXPos,
  63. newY = startYPos;
  64. // 处理边缘控制点
  65. if (dir === 'r') {
  66. newWidth += ev.clientX - startX;
  67. } else if (dir === 'l') {
  68. newWidth -= ev.clientX - startX;
  69. newX += ev.clientX - startX;
  70. } else if (dir === 'b') {
  71. newHeight += ev.clientY - startY;
  72. } else if (dir === 't') {
  73. newHeight -= ev.clientY - startY;
  74. newY += ev.clientY - startY;
  75. }
  76. // 处理角落控制点
  77. else {
  78. if (dir.includes('r')) newWidth += ev.clientX - startX;
  79. if (dir.includes('l')) newWidth -= ev.clientX - startX;
  80. if (dir.includes('b')) newHeight += ev.clientY - startY;
  81. if (dir.includes('t')) newHeight -= ev.clientY - startY;
  82. }
  83. emit('resize', {
  84. width: Math.max(20, newWidth),
  85. height: Math.max(20, newHeight),
  86. x: newX,
  87. y: newY
  88. });
  89. }
  90. function onUp() {
  91. document.removeEventListener('mousemove', onMove);
  92. document.removeEventListener('mouseup', onUp);
  93. // 恢复光标样式
  94. document.body.style.cursor = '';
  95. }
  96. document.addEventListener('mousemove', onMove);
  97. document.addEventListener('mouseup', onUp);
  98. }
  99. // 根据拖拽方向获取对应的光标样式
  100. function getResizeCursor(dir: string) {
  101. const cursors: Record<string, string> = {
  102. 't': 'n-resize',
  103. 'r': 'e-resize',
  104. 'b': 's-resize',
  105. 'l': 'w-resize',
  106. 'tr': 'ne-resize',
  107. 'tl': 'nw-resize',
  108. 'br': 'se-resize',
  109. 'bl': 'sw-resize'
  110. };
  111. return cursors[dir] || 'move';
  112. }
  113. const text = computed(() => props.text || '文本内容');
  114. </script>
  115. <style scoped>
  116. .text-board-wrapper.selected {
  117. outline: 2px dashed #409eff;
  118. outline-offset: 0;
  119. }
  120. .text-board-wrapper {
  121. cursor: pointer;
  122. }
  123. .text-board-wrapper.selected {
  124. cursor: move;
  125. }
  126. .resize-handle {
  127. width: 8px;
  128. height: 8px;
  129. background: #fff;
  130. border: 1.5px solid #409eff;
  131. border-radius: 50%;
  132. position: absolute;
  133. z-index: 2;
  134. }
  135. /* 角落控制点 */
  136. .resize-handle.tr {
  137. top: -4px;
  138. right: -4px;
  139. cursor: ne-resize;
  140. }
  141. .resize-handle.tl {
  142. top: -4px;
  143. left: -4px;
  144. cursor: nw-resize;
  145. }
  146. .resize-handle.br {
  147. bottom: -4px;
  148. right: -4px;
  149. cursor: se-resize;
  150. }
  151. .resize-handle.bl {
  152. bottom: -4px;
  153. left: -4px;
  154. cursor: sw-resize;
  155. }
  156. /* 边缘控制点 */
  157. .resize-handle.edge.t {
  158. top: -4px;
  159. left: 50%;
  160. transform: translateX(-50%);
  161. cursor: n-resize;
  162. }
  163. .resize-handle.edge.r {
  164. top: 50%;
  165. right: -4px;
  166. transform: translateY(-50%);
  167. cursor: e-resize;
  168. }
  169. .resize-handle.edge.b {
  170. bottom: -4px;
  171. left: 50%;
  172. transform: translateX(-50%);
  173. cursor: s-resize;
  174. }
  175. .resize-handle.edge.l {
  176. top: 50%;
  177. left: -4px;
  178. transform: translateY(-50%);
  179. cursor: w-resize;
  180. }
  181. .text-board {
  182. width: 100%;
  183. height: 100%;
  184. background: transparent;
  185. outline: none;
  186. cursor: text;
  187. user-select: text;
  188. }
  189. </style>