|
@@ -5,6 +5,7 @@
|
|
|
:popper-class="popperClass"
|
|
:popper-class="popperClass"
|
|
|
:placement="placement"
|
|
:placement="placement"
|
|
|
v-bind="tooltipProps"
|
|
v-bind="tooltipProps"
|
|
|
|
|
+ :offset="20"
|
|
|
ref="tooltipRef"
|
|
ref="tooltipRef"
|
|
|
>
|
|
>
|
|
|
<div
|
|
<div
|
|
@@ -12,20 +13,20 @@
|
|
|
class="ellipsis-container"
|
|
class="ellipsis-container"
|
|
|
:class="{ 'is-clamp-multi': lineClamp > 1 }"
|
|
:class="{ 'is-clamp-multi': lineClamp > 1 }"
|
|
|
:style="finalContainerStyle"
|
|
:style="finalContainerStyle"
|
|
|
- @mouseenter="handleMouseEnter"
|
|
|
|
|
|
|
+ @mouseenter.capture="handleMouseEnter"
|
|
|
@mouseleave="handleMouseLeave"
|
|
@mouseleave="handleMouseLeave"
|
|
|
>
|
|
>
|
|
|
- <slot>
|
|
|
|
|
- <span ref="textRef" class="ellipsis-text" :style="textStyle">
|
|
|
|
|
- {{ content }}
|
|
|
|
|
- </span>
|
|
|
|
|
- </slot>
|
|
|
|
|
|
|
+ <div ref="textRef" class="ellipsis-text" :style="finalTextStyle">
|
|
|
|
|
+ <slot>
|
|
|
|
|
+ <span>{{ content }}</span>
|
|
|
|
|
+ </slot>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</el-tooltip>
|
|
</el-tooltip>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
|
|
|
|
|
|
+import { ref, computed, onMounted, onUnmounted, onUpdated, watch, nextTick } from 'vue'
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
const props = defineProps({
|
|
|
content: String,
|
|
content: String,
|
|
@@ -71,13 +72,11 @@ const tooltipRef = ref(null)
|
|
|
// --- 是否应显示 tooltip ---
|
|
// --- 是否应显示 tooltip ---
|
|
|
const shouldShowTooltip = ref(false)
|
|
const shouldShowTooltip = ref(false)
|
|
|
|
|
|
|
|
-// --- 动态计算样式类 ---
|
|
|
|
|
-const containerClasses = computed(() => ({
|
|
|
|
|
- 'is-clamp-multi': props.lineClamp > 1
|
|
|
|
|
-}))
|
|
|
|
|
|
|
+// --- 插槽文案(无 content prop 时从 DOM 读取,供 tooltip 使用)---
|
|
|
|
|
+const slotTooltipText = ref('')
|
|
|
|
|
|
|
|
// --- 实际用于 Tooltip 显示的内容 ---
|
|
// --- 实际用于 Tooltip 显示的内容 ---
|
|
|
-const tooltipContent = computed(() => props.content || '')
|
|
|
|
|
|
|
+const tooltipContent = computed(() => props.content || slotTooltipText.value)
|
|
|
|
|
|
|
|
// --- 容器样式(max-width / max-height)---
|
|
// --- 容器样式(max-width / max-height)---
|
|
|
const finalContainerStyle = computed(() => ({
|
|
const finalContainerStyle = computed(() => ({
|
|
@@ -114,31 +113,51 @@ const finalTextStyle = computed(() => {
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// --- 检查是否溢出 ---
|
|
|
|
|
-const checkOverflow = async () => {
|
|
|
|
|
- await nextTick() // 确保 DOM 更新
|
|
|
|
|
- const container = containerRef.value
|
|
|
|
|
- const textEl = textRef.value || container?.querySelector('.ellipsis-text')
|
|
|
|
|
|
|
+// --- 实际发生省略的测量节点:插槽包一层 div/span 时,溢出体现在子节点 scrollWidth 上 ---
|
|
|
|
|
+const getMeasureEl = (textEl) => {
|
|
|
|
|
+ if (!textEl) return null
|
|
|
|
|
+ const { children } = textEl
|
|
|
|
|
+ if (children.length === 1) {
|
|
|
|
|
+ return children[0]
|
|
|
|
|
+ }
|
|
|
|
|
+ return textEl
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- if (!container || !textEl) return
|
|
|
|
|
|
|
+// --- 同步测量(mouseenter 必须用同步,否则 el-tooltip 先看到 disabled=true 不会弹出)---
|
|
|
|
|
+const measureOverflow = () => {
|
|
|
|
|
+ const textEl = textRef.value
|
|
|
|
|
|
|
|
- let isOverflowing = false
|
|
|
|
|
|
|
+ if (!textEl) return
|
|
|
|
|
+
|
|
|
|
|
+ slotTooltipText.value = (textEl.textContent || '').trim()
|
|
|
|
|
+
|
|
|
|
|
+ const measureEl = getMeasureEl(textEl)
|
|
|
|
|
+ if (!measureEl) return
|
|
|
|
|
|
|
|
if (props.lineClamp <= 1) {
|
|
if (props.lineClamp <= 1) {
|
|
|
- isOverflowing = textEl.scrollWidth > container.clientWidth
|
|
|
|
|
|
|
+ // 与 clientWidth 比较;子像素取整可能导致相等,故留 1px 容差
|
|
|
|
|
+ shouldShowTooltip.value = measureEl.scrollWidth > measureEl.clientWidth + 1
|
|
|
} else {
|
|
} else {
|
|
|
- // 多行看高度是否溢出(line-clamp 截断)
|
|
|
|
|
- isOverflowing = textEl.scrollHeight > container.clientHeight
|
|
|
|
|
|
|
+ shouldShowTooltip.value = measureEl.scrollHeight > measureEl.clientHeight + 1
|
|
|
}
|
|
}
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
- shouldShowTooltip.value = isOverflowing
|
|
|
|
|
|
|
+// --- 检查是否溢出(DOM 更新后)---
|
|
|
|
|
+const checkOverflow = async () => {
|
|
|
|
|
+ await nextTick()
|
|
|
|
|
+ measureOverflow()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// --- 鼠标事件用于手动触发检查(防抖)---
|
|
|
|
|
|
|
+// --- 捕获阶段先更新 shouldShowTooltip,再交给 el-tooltip 处理悬停 ---
|
|
|
let resizeTimer
|
|
let resizeTimer
|
|
|
const handleMouseEnter = () => {
|
|
const handleMouseEnter = () => {
|
|
|
clearTimeout(resizeTimer)
|
|
clearTimeout(resizeTimer)
|
|
|
- resizeTimer = setTimeout(checkOverflow, 50)
|
|
|
|
|
|
|
+ measureOverflow()
|
|
|
|
|
+ resizeTimer = setTimeout(measureOverflow, 50)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleMouseLeave = () => {
|
|
|
|
|
+ clearTimeout(resizeTimer)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// 可选:也可监听 resize
|
|
// 可选:也可监听 resize
|
|
@@ -147,6 +166,11 @@ onMounted(() => {
|
|
|
checkOverflow()
|
|
checkOverflow()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+// 插槽内容变化(如 v-for 文案)不会触发 props watch,需在更新后重算溢出
|
|
|
|
|
+onUpdated(() => {
|
|
|
|
|
+ nextTick(checkOverflow)
|
|
|
|
|
+})
|
|
|
|
|
+
|
|
|
onUnmounted(() => {
|
|
onUnmounted(() => {
|
|
|
window.removeEventListener('resize', checkOverflow)
|
|
window.removeEventListener('resize', checkOverflow)
|
|
|
clearTimeout(resizeTimer)
|
|
clearTimeout(resizeTimer)
|
|
@@ -165,30 +189,35 @@ watch(
|
|
|
<style lang="scss" scoped>
|
|
<style lang="scss" scoped>
|
|
|
.ellipsis-container {
|
|
.ellipsis-container {
|
|
|
display: inline-block;
|
|
display: inline-block;
|
|
|
|
|
+ box-sizing: border-box;
|
|
|
|
|
+ width: 100%;
|
|
|
max-width: v-bind('finalContainerStyle.maxWidth');
|
|
max-width: v-bind('finalContainerStyle.maxWidth');
|
|
|
max-height: v-bind('finalContainerStyle.maxHeight');
|
|
max-height: v-bind('finalContainerStyle.maxHeight');
|
|
|
|
|
+ min-width: 0;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
vertical-align: top;
|
|
vertical-align: top;
|
|
|
- line-height: 32px;
|
|
|
|
|
|
|
+ /* 不固定行高:否则在继承较小字号时行盒约 13px,会裁掉加粗/降部笔画 */
|
|
|
|
|
+ line-height: normal;
|
|
|
|
|
+ font: inherit;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.ellipsis-text {
|
|
.ellipsis-text {
|
|
|
- width: 100%; // 利用父容器宽度
|
|
|
|
|
-}
|
|
|
|
|
-.ellipsis-container {
|
|
|
|
|
- display: inline-block;
|
|
|
|
|
- max-width: v-bind('finalContainerStyle.maxWidth');
|
|
|
|
|
- max-height: v-bind('finalContainerStyle.maxHeight');
|
|
|
|
|
|
|
+ width: 100%;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ display: block;
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
- line-height: 1.5;
|
|
|
|
|
|
|
+ text-overflow: ellipsis;
|
|
|
|
|
+ white-space: nowrap;
|
|
|
|
|
+ line-height: inherit;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.ellipsis-text {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- display: block;
|
|
|
|
|
|
|
+/* 插槽根节点常为 div(如 .title),省略与 nowrap 需作用在真正承载文字的节点上 */
|
|
|
|
|
+.ellipsis-container:not(.is-clamp-multi) .ellipsis-text > :deep(*) {
|
|
|
overflow: hidden;
|
|
overflow: hidden;
|
|
|
text-overflow: ellipsis;
|
|
text-overflow: ellipsis;
|
|
|
white-space: nowrap;
|
|
white-space: nowrap;
|
|
|
|
|
+ min-width: 0;
|
|
|
|
|
+ max-width: 100%;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
.ellipsis-container.is-clamp-multi .ellipsis-text {
|
|
.ellipsis-container.is-clamp-multi .ellipsis-text {
|
|
@@ -199,12 +228,17 @@ watch(
|
|
|
white-space: normal;
|
|
white-space: normal;
|
|
|
word-break: break-all;
|
|
word-break: break-all;
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+.ellipsis-container.is-clamp-multi .ellipsis-text > :deep(*) {
|
|
|
|
|
+ white-space: normal;
|
|
|
|
|
+ word-break: break-all;
|
|
|
|
|
+}
|
|
|
</style>
|
|
</style>
|
|
|
|
|
|
|
|
<!-- 全局样式建议提取到全局 SCSS 文件 -->
|
|
<!-- 全局样式建议提取到全局 SCSS 文件 -->
|
|
|
<style>
|
|
<style>
|
|
|
.ellipsis-tooltip {
|
|
.ellipsis-tooltip {
|
|
|
- max-width: 200px;
|
|
|
|
|
|
|
+ max-width: 260px;
|
|
|
word-break: break-word;
|
|
word-break: break-word;
|
|
|
white-space: pre-line;
|
|
white-space: pre-line;
|
|
|
margin-bottom: -15px;
|
|
margin-bottom: -15px;
|