| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232 |
- <script setup lang="ts">
- import type { CSSProperties } from 'vue';
- import {
- computed,
- onBeforeUnmount,
- onMounted,
- onUpdated,
- ref,
- watchEffect,
- } from 'vue';
- import { VbenTooltip } from '@vben-core/shadcn-ui';
- import { useElementSize } from '@vueuse/core';
- interface Props {
- /**
- * 是否启用点击文本展开全部
- * @default false
- */
- expand?: boolean;
- /**
- * 文本最大行数
- * @default 1
- */
- line?: number;
- /**
- * 文本最大宽度
- * @default '100%'
- */
- maxWidth?: number | string;
- /**
- * 提示框位置
- * @default 'top'
- */
- placement?: 'bottom' | 'left' | 'right' | 'top';
- /**
- * 是否启用文本提示框
- * @default true
- */
- tooltip?: boolean;
- /**
- * 是否只在文本被截断时显示提示框
- * @default false
- */
- tooltipWhenEllipsis?: boolean;
- /**
- * 文本截断检测的像素差异阈值,越大则判断越严格
- * @default 3
- */
- ellipsisThreshold?: number;
- /**
- * 提示框背景颜色,优先级高于 overlayStyle
- */
- tooltipBackgroundColor?: string;
- /**
- * 提示文本字体颜色,优先级高于 overlayStyle
- */
- tooltipColor?: string;
- /**
- * 提示文本字体大小,单位px,优先级高于 overlayStyle
- */
- tooltipFontSize?: number;
- /**
- * 提示框内容最大宽度,单位px,默认不设置时,提示文本内容自动与展示文本宽度保持一致
- */
- tooltipMaxWidth?: number;
- /**
- * 提示框内容区域样式
- * @default { textAlign: 'justify' }
- */
- tooltipOverlayStyle?: CSSProperties;
- }
- const props = withDefaults(defineProps<Props>(), {
- expand: false,
- line: 1,
- 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(() => {
- if (typeof props.maxWidth === 'number') {
- return `${props.maxWidth}px`;
- }
- return props.maxWidth;
- });
- 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) {
- defaultTooltipMaxWidth.value =
- props.tooltipMaxWidth ?? eleWidth.value + 24;
- }
- },
- { flush: 'post' },
- );
- function onExpand() {
- isExpand.value = !isExpand.value;
- emit('expandChange', isExpand.value);
- if (props.tooltipWhenEllipsis) {
- checkEllipsis();
- }
- }
- function handleExpand() {
- props.expand && onExpand();
- }
- </script>
- <template>
- <div>
- <VbenTooltip
- :content-style="{
- ...tooltipOverlayStyle,
- maxWidth: `${defaultTooltipMaxWidth}px`,
- fontSize: `${tooltipFontSize}px`,
- color: tooltipColor,
- backgroundColor: tooltipBackgroundColor,
- }"
- :disabled="
- !props.tooltip || isExpand || (props.tooltipWhenEllipsis && !isEllipsis)
- "
- :side="placement"
- >
- <slot name="tooltip">
- <slot></slot>
- </slot>
- <template #trigger>
- <div
- ref="ellipsis"
- :class="{
- '!cursor-pointer': expand,
- ['block truncate']: line === 1,
- [$style.ellipsisMultiLine]: line > 1,
- }"
- :style="{
- '-webkit-line-clamp': isExpand ? '' : line,
- 'max-width': textMaxWidth,
- }"
- class="cursor-text overflow-hidden"
- @click="handleExpand"
- v-bind="$attrs"
- >
- <slot></slot>
- </div>
- </template>
- </VbenTooltip>
- </div>
- </template>
- <style module>
- .ellipsisMultiLine {
- display: -webkit-box;
- -webkit-box-orient: vertical;
- }
- </style>
|