Эх сурвалжийг харах

feat: ellipsis text automatically displays tooltip based on ellipsis (#6244)

* feat: ellipsis text automatically displays tooltip based on ellipsis

* feat: ellipsis text automatically displays tooltip based on ellipsis

---------

Co-authored-by: sqchen <9110848@qq.com>
Co-authored-by: sqchen <chenshiqi@sshlx.com>
panda7 4 сар өмнө
parent
commit
a2bdcd6e49

+ 8 - 0
docs/src/components/common-ui/vben-ellipsis-text.md

@@ -26,6 +26,12 @@ outline: deep
 
 <DemoPreview dir="demos/vben-ellipsis-text/tooltip" />
 
+## 自动显示 tooltip
+
+通过`tooltip-when-ellipsis`设置,仅在文本长度超出导致省略号出现时才触发 tooltip。
+
+<DemoPreview dir="demos/vben-ellipsis-text/auto-display" />
+
 ## API
 
 ### Props
@@ -37,6 +43,8 @@ outline: deep
 | maxWidth | 文本区域最大宽度 | `number \| string` | `'100%'` |
 | placement | 提示浮层的位置 | `'bottom'\|'left'\|'right'\|'top'` | `'top'` |
 | tooltip | 启用文本提示 | `boolean` | `true` |
+| tooltipWhenEllipsis | 内容超出,自动启用文本提示 | `boolean` | `false` |
+| ellipsisThreshold | 设置 tooltipWhenEllipsis 后才生效,文本截断检测的像素差异阈值,越大则判断越严格,如果碰见异常情况可以自己设置阈值 | `number` | `3` |
 | tooltipBackgroundColor | 提示文本的背景颜色 | `string` | - |
 | tooltipColor | 提示文本的颜色 | `string` | - |
 | tooltipFontSize | 提示文本的大小 | `string` | - |

+ 16 - 0
docs/src/demos/vben-ellipsis-text/auto-display/index.vue

@@ -0,0 +1,16 @@
+<script lang="ts" setup>
+import { EllipsisText } from '@vben/common-ui';
+
+const text = `
+Vben Admin 是一个基于 Vue3.0、Vite、 TypeScript 的后台解决方案,目标是为开发中大型项目提供开箱即用的解决方案。
+`;
+</script>
+<template>
+  <EllipsisText :line="2" :tooltip-when-ellipsis="true">
+    {{ text }}
+  </EllipsisText>
+
+  <EllipsisText :line="3" :tooltip-when-ellipsis="true">
+    {{ text }}
+  </EllipsisText>
+</template>

+ 86 - 2
packages/effects/common-ui/src/components/ellipsis-text/ellipsis-text.vue

@@ -1,7 +1,14 @@
 <script setup lang="ts">
 import type { CSSProperties } from 'vue';
 
-import { computed, ref, watchEffect } from 'vue';
+import {
+  computed,
+  onBeforeUnmount,
+  onMounted,
+  onUpdated,
+  ref,
+  watchEffect,
+} from 'vue';
 
 import { VbenTooltip } from '@vben-core/shadcn-ui';
 
@@ -33,6 +40,16 @@ interface Props {
    * @default true
    */
   tooltip?: boolean;
+  /**
+   * 是否只在文本被截断时显示提示框
+   * @default false
+   */
+  tooltipWhenEllipsis?: boolean;
+  /**
+   * 文本截断检测的像素差异阈值,越大则判断越严格
+   * @default 3
+   */
+  ellipsisThreshold?: number;
   /**
    * 提示框背景颜色,优先级高于 overlayStyle
    */
@@ -62,12 +79,15 @@ const props = withDefaults(defineProps<Props>(), {
   maxWidth: '100%',
   placement: 'top',
   tooltip: true,
+  tooltipWhenEllipsis: false,
+  ellipsisThreshold: 3,
   tooltipBackgroundColor: '',
   tooltipColor: '',
   tooltipFontSize: 14,
   tooltipMaxWidth: undefined,
   tooltipOverlayStyle: () => ({ textAlign: 'justify' }),
 });
+
 const emit = defineEmits<{ expandChange: [boolean] }>();
 
 const textMaxWidth = computed(() => {
@@ -79,9 +99,67 @@ const textMaxWidth = computed(() => {
 const ellipsis = ref();
 const isExpand = ref(false);
 const defaultTooltipMaxWidth = ref();
+const isEllipsis = ref(false);
 
 const { width: eleWidth } = useElementSize(ellipsis);
 
+// 检测文本是否被截断
+const checkEllipsis = () => {
+  if (!ellipsis.value || !props.tooltipWhenEllipsis) return;
+
+  const element = ellipsis.value;
+
+  const originalText = element.textContent || '';
+  const originalTrimmed = originalText.trim();
+
+  // 对于空文本直接返回 false
+  if (!originalTrimmed) {
+    isEllipsis.value = false;
+    return;
+  }
+
+  const widthDiff = element.scrollWidth - element.clientWidth;
+  const heightDiff = element.scrollHeight - element.clientHeight;
+
+  // 使用足够大的差异阈值确保只有真正被截断的文本才会显示 tooltip
+  isEllipsis.value =
+    props.line === 1
+      ? widthDiff > props.ellipsisThreshold
+      : heightDiff > props.ellipsisThreshold;
+};
+
+// 使用 ResizeObserver 监听尺寸变化
+let resizeObserver: null | ResizeObserver = null;
+
+onMounted(() => {
+  if (typeof ResizeObserver !== 'undefined' && props.tooltipWhenEllipsis) {
+    resizeObserver = new ResizeObserver(() => {
+      checkEllipsis();
+    });
+
+    if (ellipsis.value) {
+      resizeObserver.observe(ellipsis.value);
+    }
+  }
+
+  // 初始检测
+  checkEllipsis();
+});
+
+// 使用onUpdated钩子检测内容变化
+onUpdated(() => {
+  if (props.tooltipWhenEllipsis) {
+    checkEllipsis();
+  }
+});
+
+onBeforeUnmount(() => {
+  if (resizeObserver) {
+    resizeObserver.disconnect();
+    resizeObserver = null;
+  }
+});
+
 watchEffect(
   () => {
     if (props.tooltip && eleWidth.value) {
@@ -91,9 +169,13 @@ watchEffect(
   },
   { flush: 'post' },
 );
+
 function onExpand() {
   isExpand.value = !isExpand.value;
   emit('expandChange', isExpand.value);
+  if (props.tooltipWhenEllipsis) {
+    checkEllipsis();
+  }
 }
 
 function handleExpand() {
@@ -110,7 +192,9 @@ function handleExpand() {
         color: tooltipColor,
         backgroundColor: tooltipBackgroundColor,
       }"
-      :disabled="!props.tooltip || isExpand"
+      :disabled="
+        !props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)
+      "
       :side="placement"
     >
       <slot name="tooltip">