|
|
@@ -0,0 +1,211 @@
|
|
|
+<template>
|
|
|
+ <el-tooltip
|
|
|
+ :disabled="!shouldShowTooltip"
|
|
|
+ :content="tooltipContent"
|
|
|
+ :popper-class="popperClass"
|
|
|
+ :placement="placement"
|
|
|
+ v-bind="tooltipProps"
|
|
|
+ ref="tooltipRef"
|
|
|
+ >
|
|
|
+ <div
|
|
|
+ ref="containerRef"
|
|
|
+ class="ellipsis-container"
|
|
|
+ :class="{ 'is-clamp-multi': lineClamp > 1 }"
|
|
|
+ :style="finalContainerStyle"
|
|
|
+ @mouseenter="handleMouseEnter"
|
|
|
+ @mouseleave="handleMouseLeave"
|
|
|
+ >
|
|
|
+ <slot>
|
|
|
+ <span ref="textRef" class="ellipsis-text" :style="textStyle">
|
|
|
+ {{ content }}
|
|
|
+ </span>
|
|
|
+ </slot>
|
|
|
+ </div>
|
|
|
+ </el-tooltip>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
+
|
|
|
+const props = defineProps({
|
|
|
+ content: String,
|
|
|
+ maxWidth: {
|
|
|
+ type: Number,
|
|
|
+ default: 200
|
|
|
+ },
|
|
|
+ maxHeight: {
|
|
|
+ type: [Number, String],
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ lineClamp: {
|
|
|
+ type: Number,
|
|
|
+ default: 1
|
|
|
+ },
|
|
|
+ tooltipProps: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ popperClass: {
|
|
|
+ type: String,
|
|
|
+ default: 'ellipsis-tooltip'
|
|
|
+ },
|
|
|
+ placement: {
|
|
|
+ type: String,
|
|
|
+ default: 'top'
|
|
|
+ },
|
|
|
+ containerStyle: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ },
|
|
|
+ textStyle: {
|
|
|
+ type: Object,
|
|
|
+ default: () => ({})
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// --- DOM refs ---
|
|
|
+const containerRef = ref(null)
|
|
|
+const textRef = ref(null)
|
|
|
+const tooltipRef = ref(null)
|
|
|
+
|
|
|
+// --- 是否应显示 tooltip ---
|
|
|
+const shouldShowTooltip = ref(false)
|
|
|
+
|
|
|
+// --- 动态计算样式类 ---
|
|
|
+const containerClasses = computed(() => ({
|
|
|
+ 'is-clamp-multi': props.lineClamp > 1
|
|
|
+}))
|
|
|
+
|
|
|
+// --- 实际用于 Tooltip 显示的内容 ---
|
|
|
+const tooltipContent = computed(() => props.content || '')
|
|
|
+
|
|
|
+// --- 容器样式(max-width / max-height)---
|
|
|
+const finalContainerStyle = computed(() => ({
|
|
|
+ maxWidth: props.maxWidth + 'px',
|
|
|
+ maxHeight: props.maxHeight
|
|
|
+ ? typeof props.maxHeight === 'number'
|
|
|
+ ? props.maxHeight + 'px'
|
|
|
+ : props.maxHeight
|
|
|
+ : 'none',
|
|
|
+ ...props.containerStyle
|
|
|
+}))
|
|
|
+
|
|
|
+// --- 文本样式(根据 lineClamp 应用不同 CSS)---
|
|
|
+const finalTextStyle = computed(() => {
|
|
|
+ const style = { ...props.textStyle }
|
|
|
+ if (props.lineClamp <= 1) {
|
|
|
+ return {
|
|
|
+ display: 'block',
|
|
|
+ overflow: 'hidden',
|
|
|
+ textOverflow: 'ellipsis',
|
|
|
+ whiteSpace: 'nowrap',
|
|
|
+ ...style
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ return {
|
|
|
+ display: '-webkit-box',
|
|
|
+ WebkitBoxOrient: 'vertical',
|
|
|
+ WebkitLineClamp: props.lineClamp,
|
|
|
+ overflow: 'hidden',
|
|
|
+ textOverflow: 'ellipsis',
|
|
|
+ wordBreak: 'break-all', // 多行建议启用
|
|
|
+ ...style
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// --- 检查是否溢出 ---
|
|
|
+const checkOverflow = async () => {
|
|
|
+ await nextTick() // 确保 DOM 更新
|
|
|
+ const container = containerRef.value
|
|
|
+ const textEl = textRef.value || container?.querySelector('.ellipsis-text')
|
|
|
+
|
|
|
+ if (!container || !textEl) return
|
|
|
+
|
|
|
+ let isOverflowing = false
|
|
|
+
|
|
|
+ if (props.lineClamp <= 1) {
|
|
|
+ isOverflowing = textEl.scrollWidth > container.clientWidth
|
|
|
+ } else {
|
|
|
+ // 多行看高度是否溢出(line-clamp 截断)
|
|
|
+ isOverflowing = textEl.scrollHeight > container.clientHeight
|
|
|
+ }
|
|
|
+
|
|
|
+ shouldShowTooltip.value = isOverflowing
|
|
|
+}
|
|
|
+
|
|
|
+// --- 鼠标事件用于手动触发检查(防抖)---
|
|
|
+let resizeTimer
|
|
|
+const handleMouseEnter = () => {
|
|
|
+ clearTimeout(resizeTimer)
|
|
|
+ resizeTimer = setTimeout(checkOverflow, 50)
|
|
|
+}
|
|
|
+
|
|
|
+// 可选:也可监听 resize
|
|
|
+onMounted(() => {
|
|
|
+ window.addEventListener('resize', checkOverflow)
|
|
|
+ checkOverflow()
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ window.removeEventListener('resize', checkOverflow)
|
|
|
+ clearTimeout(resizeTimer)
|
|
|
+})
|
|
|
+
|
|
|
+// 监听 props 变化
|
|
|
+watch(
|
|
|
+ () => [props.content, props.lineClamp, props.maxWidth, props.maxHeight],
|
|
|
+ () => {
|
|
|
+ checkOverflow()
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+)
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.ellipsis-container {
|
|
|
+ display: inline-block;
|
|
|
+ max-width: v-bind('finalContainerStyle.maxWidth');
|
|
|
+ max-height: v-bind('finalContainerStyle.maxHeight');
|
|
|
+ overflow: hidden;
|
|
|
+ vertical-align: top;
|
|
|
+ line-height: 32px;
|
|
|
+}
|
|
|
+
|
|
|
+.ellipsis-text {
|
|
|
+ width: 100%; // 利用父容器宽度
|
|
|
+}
|
|
|
+.ellipsis-container {
|
|
|
+ display: inline-block;
|
|
|
+ max-width: v-bind('finalContainerStyle.maxWidth');
|
|
|
+ max-height: v-bind('finalContainerStyle.maxHeight');
|
|
|
+ overflow: hidden;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+
|
|
|
+.ellipsis-text {
|
|
|
+ width: 100%;
|
|
|
+ display: block;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.ellipsis-container.is-clamp-multi .ellipsis-text {
|
|
|
+ display: -webkit-box;
|
|
|
+ -webkit-box-orient: vertical;
|
|
|
+ -webkit-line-clamp: v-bind('lineClamp');
|
|
|
+ white-space: normal;
|
|
|
+ word-break: break-all;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|
|
|
+<!-- 全局样式建议提取到全局 SCSS 文件 -->
|
|
|
+<style>
|
|
|
+.ellipsis-tooltip {
|
|
|
+ max-width: 200px;
|
|
|
+ word-break: break-word;
|
|
|
+ white-space: pre-line;
|
|
|
+ margin-bottom: -15px;
|
|
|
+}
|
|
|
+</style>
|