TextBoard.vue 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. <template>
  2. <div class="text-board-wrapper" :class="{ selected }"
  3. :style="{ width: '100%', height: '100%', position: 'relative' }">
  4. <div class="text-board" :style="textStyle">{{ text }}</div>
  5. <template v-if="selected">
  6. <div v-for="dir in ['tr', 'tl', 'br', 'bl']" :key="dir" class="resize-handle" :class="dir"
  7. @mousedown.stop="onResizeMouseDown($event, dir)"></div>
  8. </template>
  9. </div>
  10. </template>
  11. <script setup lang="ts">
  12. import { computed } from 'vue';
  13. const emit = defineEmits(['resize']);
  14. interface Props {
  15. text?: string;
  16. color?: string;
  17. fontSize?: string | number;
  18. fontWeight?: string | number;
  19. align?: 'left' | 'center' | 'right';
  20. width?: number;
  21. height?: number;
  22. selected?: boolean;
  23. }
  24. const props = defineProps<Props>();
  25. const selected = computed(() => !!props.selected);
  26. const textStyle = computed(() => ({
  27. color: props.color || '#222',
  28. fontSize: typeof props.fontSize === 'number' ? props.fontSize + 'px' : props.fontSize || '24px',
  29. fontWeight: props.fontWeight || 'normal',
  30. textAlign: props.align || 'center',
  31. width: '100%',
  32. height: '100%',
  33. display: 'flex',
  34. alignItems: 'center',
  35. justifyContent: props.align === 'left' ? 'flex-start' : props.align === 'right' ? 'flex-end' : 'center',
  36. userSelect: 'none',
  37. }));
  38. function onResizeMouseDown(e: MouseEvent, dir: string) {
  39. e.stopPropagation();
  40. const startX = e.clientX,
  41. startY = e.clientY;
  42. const startWidth = Number(props.width) || 200;
  43. const startHeight = Number(props.height) || 40;
  44. function onMove(ev: MouseEvent) {
  45. let newWidth = startWidth,
  46. newHeight = startHeight;
  47. if (dir.includes('r')) newWidth += ev.clientX - startX;
  48. if (dir.includes('l')) newWidth -= ev.clientX - startX;
  49. if (dir.includes('b')) newHeight += ev.clientY - startY;
  50. if (dir.includes('t')) newHeight -= ev.clientY - startY;
  51. emit('resize', { width: Math.max(20, newWidth), height: Math.max(20, newHeight) });
  52. }
  53. function onUp() {
  54. document.removeEventListener('mousemove', onMove);
  55. document.removeEventListener('mouseup', onUp);
  56. }
  57. document.addEventListener('mousemove', onMove);
  58. document.addEventListener('mouseup', onUp);
  59. }
  60. const text = computed(() => props.text || '双击编辑文本');
  61. </script>
  62. <style scoped>
  63. .text-board-wrapper.selected {
  64. outline: 2px dashed #409eff;
  65. outline-offset: 0;
  66. }
  67. .resize-handle {
  68. width: 8px;
  69. height: 8px;
  70. background: #fff;
  71. border: 1.5px solid #409eff;
  72. border-radius: 50%;
  73. position: absolute;
  74. z-index: 2;
  75. }
  76. .resize-handle.tr {
  77. top: -4px;
  78. right: -4px;
  79. cursor: ne-resize;
  80. }
  81. .resize-handle.tl {
  82. top: -4px;
  83. left: -4px;
  84. cursor: nw-resize;
  85. }
  86. .resize-handle.br {
  87. bottom: -4px;
  88. right: -4px;
  89. cursor: se-resize;
  90. }
  91. .resize-handle.bl {
  92. bottom: -4px;
  93. left: -4px;
  94. cursor: sw-resize;
  95. }
  96. .text-board {
  97. width: 100%;
  98. height: 100%;
  99. background: transparent;
  100. outline: none;
  101. cursor: text;
  102. user-select: text;
  103. }
  104. </style>