child::
自用多行省略与Tooltip组件
指向原始笔记的链接 <script setup lang="ts"> import { computed, nextTick, onMounted, onUnmounted, ref, watch } from 'vue' const props = withDefaults( defineProps<{ text?: string rows?: number tooltip?: boolean | string tooltipProps?: Record<string, unknown> reserve?: boolean }>(), { text: undefined, rows: 1, tooltip: undefined, tooltipProps: undefined, reserve: false, }, ) const textRef = ref<HTMLElement | null>(null) const isActuallyTruncated = ref(false) const innerTextContent = ref('') const lineHeightPx = ref(0) const reservedMinHeightPx = ref<number | null>(null) const displayText = computed<string>(() => { return (props.text ?? '').toString() }) const tooltipEnabled = computed<boolean>(() => { if (props.tooltip === false) return false // undefined or true or string means allowed when truncated return true }) const tooltipContent = computed<string>(() => { if (typeof props.tooltip === 'string') return props.tooltip // Prefer the rendered slot/text innerText const content = (innerTextContent.value || '').trim() if (content) return content // Fallback to prop text when innerText not yet measured return displayText.value }) let resizeObserver: ResizeObserver | null = null function updateTruncationLater(): void { // Measure after DOM updates/styles applied nextTick(() => { const el = textRef.value if (!el) { isActuallyTruncated.value = false return } // Read live innerText if slot content used innerTextContent.value = el.innerText ?? '' const hasText = innerTextContent.value.trim().length > 0 if (!hasText) { isActuallyTruncated.value = false // still measure min-height if reserve is enabled measureAndReserve(el) return } const heightOverflow = el.scrollHeight > el.clientHeight + 1 const widthOverflow = el.scrollWidth > el.clientWidth + 1 isActuallyTruncated.value = heightOverflow || widthOverflow measureAndReserve(el) }) } function measureAndReserve(el: HTMLElement): void { if (!props.reserve || props.rows <= 1) { reservedMinHeightPx.value = null return } const cs = window.getComputedStyle(el) const lh = cs.lineHeight const fs = cs.fontSize let px = 0 if (lh && lh !== 'normal') { px = parseFloat(lh) } else if (fs) { px = parseFloat(fs) * 1.5 } else { px = 20 } lineHeightPx.value = px reservedMinHeightPx.value = Math.max(0, Math.round(px * props.rows)) } onMounted(() => { updateTruncationLater() if (window && 'ResizeObserver' in window) { resizeObserver = new ResizeObserver(() => updateTruncationLater()) if (textRef.value) resizeObserver.observe(textRef.value) } }) onUnmounted(() => { if (resizeObserver && textRef.value) { resizeObserver.unobserve(textRef.value) } resizeObserver = null window.removeEventListener('resize', updateTruncationLater) }) watch( () => [props.text, props.rows, props.tooltip, props.reserve], () => updateTruncationLater(), { flush: 'post' }, ) const showTooltip = computed<boolean>(() => tooltipEnabled.value && isActuallyTruncated.value) </script> <template> <el-tooltip :disabled="!showTooltip" :content="tooltipContent" placement="top" v-bind="tooltipProps" > <div ref="textRef" class="ellipsis-paragraph__content" :class="{ 'is-single-line': rows === 1 }" :style="{ '--line-clamp': String(rows), minHeight: reserve && reservedMinHeightPx != null ? `${reservedMinHeightPx}px` : undefined, }" > <slot>{{ text }}</slot> </div> </el-tooltip> </template> <style scoped> .ellipsis-paragraph__content { display: -webkit-box; -webkit-box-orient: vertical; overflow: hidden; text-overflow: ellipsis; /* multiline clamp */ line-clamp: var(--line-clamp); -webkit-line-clamp: var(--line-clamp); } .ellipsis-paragraph__content.is-single-line { display: block; white-space: nowrap; } </style>