ellipsis-text.vue 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. <script setup lang="ts">
  2. import type { CSSProperties } from 'vue';
  3. import {
  4. computed,
  5. onBeforeUnmount,
  6. onMounted,
  7. onUpdated,
  8. ref,
  9. watchEffect,
  10. } from 'vue';
  11. import { VbenTooltip } from '@vben-core/shadcn-ui';
  12. import { useElementSize } from '@vueuse/core';
  13. interface Props {
  14. /**
  15. * 是否启用点击文本展开全部
  16. * @default false
  17. */
  18. expand?: boolean;
  19. /**
  20. * 文本最大行数
  21. * @default 1
  22. */
  23. line?: number;
  24. /**
  25. * 文本最大宽度
  26. * @default '100%'
  27. */
  28. maxWidth?: number | string;
  29. /**
  30. * 提示框位置
  31. * @default 'top'
  32. */
  33. placement?: 'bottom' | 'left' | 'right' | 'top';
  34. /**
  35. * 是否启用文本提示框
  36. * @default true
  37. */
  38. tooltip?: boolean;
  39. /**
  40. * 是否只在文本被截断时显示提示框
  41. * @default false
  42. */
  43. tooltipWhenEllipsis?: boolean;
  44. /**
  45. * 文本截断检测的像素差异阈值,越大则判断越严格
  46. * @default 3
  47. */
  48. ellipsisThreshold?: number;
  49. /**
  50. * 提示框背景颜色,优先级高于 overlayStyle
  51. */
  52. tooltipBackgroundColor?: string;
  53. /**
  54. * 提示文本字体颜色,优先级高于 overlayStyle
  55. */
  56. tooltipColor?: string;
  57. /**
  58. * 提示文本字体大小,单位px,优先级高于 overlayStyle
  59. */
  60. tooltipFontSize?: number;
  61. /**
  62. * 提示框内容最大宽度,单位px,默认不设置时,提示文本内容自动与展示文本宽度保持一致
  63. */
  64. tooltipMaxWidth?: number;
  65. /**
  66. * 提示框内容区域样式
  67. * @default { textAlign: 'justify' }
  68. */
  69. tooltipOverlayStyle?: CSSProperties;
  70. }
  71. const props = withDefaults(defineProps<Props>(), {
  72. expand: false,
  73. line: 1,
  74. maxWidth: '100%',
  75. placement: 'top',
  76. tooltip: true,
  77. tooltipWhenEllipsis: false,
  78. ellipsisThreshold: 3,
  79. tooltipBackgroundColor: '',
  80. tooltipColor: '',
  81. tooltipFontSize: 14,
  82. tooltipMaxWidth: undefined,
  83. tooltipOverlayStyle: () => ({ textAlign: 'justify' }),
  84. });
  85. const emit = defineEmits<{ expandChange: [boolean] }>();
  86. const textMaxWidth = computed(() => {
  87. if (typeof props.maxWidth === 'number') {
  88. return `${props.maxWidth}px`;
  89. }
  90. return props.maxWidth;
  91. });
  92. const ellipsis = ref();
  93. const isExpand = ref(false);
  94. const defaultTooltipMaxWidth = ref();
  95. const isEllipsis = ref(false);
  96. const { width: eleWidth } = useElementSize(ellipsis);
  97. // 检测文本是否被截断
  98. const checkEllipsis = () => {
  99. if (!ellipsis.value || !props.tooltipWhenEllipsis) return;
  100. const element = ellipsis.value;
  101. const originalText = element.textContent || '';
  102. const originalTrimmed = originalText.trim();
  103. // 对于空文本直接返回 false
  104. if (!originalTrimmed) {
  105. isEllipsis.value = false;
  106. return;
  107. }
  108. const widthDiff = element.scrollWidth - element.clientWidth;
  109. const heightDiff = element.scrollHeight - element.clientHeight;
  110. // 使用足够大的差异阈值确保只有真正被截断的文本才会显示 tooltip
  111. isEllipsis.value =
  112. props.line === 1
  113. ? widthDiff > props.ellipsisThreshold
  114. : heightDiff > props.ellipsisThreshold;
  115. };
  116. // 使用 ResizeObserver 监听尺寸变化
  117. let resizeObserver: null | ResizeObserver = null;
  118. onMounted(() => {
  119. if (typeof ResizeObserver !== 'undefined' && props.tooltipWhenEllipsis) {
  120. resizeObserver = new ResizeObserver(() => {
  121. checkEllipsis();
  122. });
  123. if (ellipsis.value) {
  124. resizeObserver.observe(ellipsis.value);
  125. }
  126. }
  127. // 初始检测
  128. checkEllipsis();
  129. });
  130. // 使用onUpdated钩子检测内容变化
  131. onUpdated(() => {
  132. if (props.tooltipWhenEllipsis) {
  133. checkEllipsis();
  134. }
  135. });
  136. onBeforeUnmount(() => {
  137. if (resizeObserver) {
  138. resizeObserver.disconnect();
  139. resizeObserver = null;
  140. }
  141. });
  142. watchEffect(
  143. () => {
  144. if (props.tooltip && eleWidth.value) {
  145. defaultTooltipMaxWidth.value =
  146. props.tooltipMaxWidth ?? eleWidth.value + 24;
  147. }
  148. },
  149. { flush: 'post' },
  150. );
  151. function onExpand() {
  152. isExpand.value = !isExpand.value;
  153. emit('expandChange', isExpand.value);
  154. if (props.tooltipWhenEllipsis) {
  155. checkEllipsis();
  156. }
  157. }
  158. function handleExpand() {
  159. props.expand && onExpand();
  160. }
  161. </script>
  162. <template>
  163. <div>
  164. <VbenTooltip
  165. :content-style="{
  166. ...tooltipOverlayStyle,
  167. maxWidth: `${defaultTooltipMaxWidth}px`,
  168. fontSize: `${tooltipFontSize}px`,
  169. color: tooltipColor,
  170. backgroundColor: tooltipBackgroundColor,
  171. }"
  172. :disabled="
  173. !props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)
  174. "
  175. :side="placement"
  176. >
  177. <slot name="tooltip">
  178. <slot></slot>
  179. </slot>
  180. <template #trigger>
  181. <div
  182. ref="ellipsis"
  183. :class="{
  184. '!cursor-pointer': expand,
  185. ['block truncate']: line === 1,
  186. [$style.ellipsisMultiLine]: line > 1,
  187. }"
  188. :style="{
  189. '-webkit-line-clamp': isExpand ? '' : line,
  190. 'max-width': textMaxWidth,
  191. }"
  192. class="cursor-text overflow-hidden"
  193. @click="handleExpand"
  194. v-bind="$attrs"
  195. >
  196. <slot></slot>
  197. </div>
  198. </template>
  199. </VbenTooltip>
  200. </div>
  201. </template>
  202. <style module>
  203. .ellipsisMultiLine {
  204. display: -webkit-box;
  205. -webkit-box-orient: vertical;
  206. }
  207. </style>