主要实现
文本超长时悬浮展示完整内容,不超长时不显示悬浮窗" 的交互,且悬浮窗样式需完全自定义(避免浏览器原生 title 样式不可控的问题)。本文基于 Vue3+CSS 实现这一需求,涵盖单行 / 多行文本溢出省略号、智能判断文本是否超长、自定义悬浮窗样式、悬浮窗交互优化等核心要点。
实现效果


需求分析
- 文本超出指定行数(如 5 行)时,显示省略号;未超出时正常展示。
- 文本超长时,鼠标 hover触发悬浮窗展示完整内容;
- 未超长时,无悬浮窗、无鼠标指针提示。
- 悬浮窗样式完全自定义(背景、字体、阴影、宽高),且不占用原容器高度。
- 鼠标移入悬浮窗时,悬浮窗不消失;鼠标离开文本 / 悬浮窗后,悬浮窗延迟隐藏(避免快速划过导致体验差)。
- 窗口大小变化时,重新计算文本高度,适配响应式布局
实现方法
1.单行
typescript
.single-line-ellipsis {
width: 100%;
white-space: nowrap; /* 强制单行 */
overflow: hidden; /* 隐藏溢出内容 */
text-overflow: ellipsis; /* 溢出显示省略号 */
}
2. CSS 多行文本溢出省略号(webkit 内核)
适用于 Chrome/Edge/Safari 等 webkit 内核浏览器,主要通过-webkit-line-clamp设置文本行数
typescript
.multi-line-ellipsis {
width: 100%;
line-height: 1.5em;
font-size: 14px;
display: -webkit-box;
-webkit-line-clamp: 5; /* 限制显示行数 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
3.悬浮窗超长展示/不超长隐藏
本文以固定5行文本为例
关键
- 克隆容器:通过隐藏的克隆容器渲染完整文本,计算其实际高度,判断是否超出指定行数的高度。
- 使用Teleport组件:将悬浮窗渲染到下,脱离原容器布局流,避免悬浮窗占用原容器高度。
- 鼠标交互优化:延迟判断mouseleave事件,避免鼠标快速划过文本时悬浮窗闪显闪藏;标记鼠标是否移入悬浮窗,确保悬浮窗内操作时不消失。
实现
1.基础布局与 CSS 样式
先实现文本溢出省略号的基础样式,同时准备 "克隆容器"(用于计算完整文本高度,隐藏不显示)
typescript
<template>
<!-- 外层容器:承载显示文本+克隆容器 -->
<div class="business-scope-container" ref="scopeContainer"
@mouseenter="() => isTextOverFlow && (showScopePopover = true, updatePopoverFixedPos())"
@mouseleave="handleContainerLeave">
<!-- 显示用:多行文本溢出省略号 -->
<div class="text-limit-5lines" v-html="detailData.businessScope" ref="textContentRef" />
<!-- 隐藏用:克隆容器,计算完整文本高度 -->
<div class="text-clone" ref="textCloneRef" v-html="detailData.businessScope" />
</div>
<!-- 自定义悬浮窗:teleport到body,脱离原布局流 -->
<teleport to="body">
<div class="custom-scope-popover"
v-show="isTextOverFlow && showScopePopover"
:style="popoverFixedStyle"
@mouseenter="isHoverPopover = true"
@mouseleave="handlePopoverLeave">
<div class="popover-content" v-html="detailData.businessScope" />
</div>
</teleport>
</template>
<style scoped>
/* 外层容器:避免溢出截断悬浮窗 */
.business-scope-container {
width: 100%;
height: fit-content; /* 高度仅适配显示文本,不包含悬浮窗 */
overflow: visible !important;
position: relative;
}
/* 多行文本溢出省略号(核心样式) */
.text-limit-5lines {
width: 100%;
line-height: 1.5em;
font-size: 14px;
display: -webkit-box;
-webkit-line-clamp: 5; /* 限制5行,可自定义 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
max-height: calc(1.5em * 5); /* 5行最大高度,与-webkit-line-clamp对应 */
cursor: default; /* 默认无指针,超长时通过JS改为pointer */
}
/* 克隆容器:隐藏,仅用于计算完整文本高度 */
.text-clone {
position: absolute;
top: 0;
left: 0;
width: 100%;
line-height: 1.5em; /* 与显示容器一致 */
font-size: 14px; /* 与显示容器一致 */
visibility: hidden; /* 隐藏但保留布局,用于计算高度 */
white-space: pre-wrap; /* 保留换行符,计算真实高度 */
word-break: break-all; /* 超长文本换行,保证高度计算准确 */
pointer-events: none; /* 不响应鼠标事件,避免干扰 */
height: auto;
overflow: visible;
}
/* 自定义悬浮窗样式(完全可控) */
.custom-scope-popover {
position: fixed; /* 固定定位,脱离原布局流 */
z-index: 3000; /* 避免被其他元素覆盖 */
max-width: 500px;
max-height: 300px;
padding: 12px;
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 6px;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
box-sizing: border-box;
overflow-y: auto; /* 超长内容内部滚动 */
pointer-events: auto; /* 允许鼠标悬停/选中文本 */
}
/* 悬浮窗内容样式 */
.popover-content {
line-height: 1.6;
white-space: pre-wrap; /* 保留换行符 */
word-break: break-all;
color: #333;
font-size: 14px;
}
</style>
2.判断文本是否超长 + 悬浮窗控制
核心:通过克隆容器的scrollHeight(完整文本高度)与显示容器的maxHeight(指定行数的最大高度)对比,判断文本是否超长;结合鼠标事件控制悬浮窗显隐。
typescript
<script setup>
import { ref, watch, nextTick } from 'vue';
// 模拟业务数据(如接口返回的经营范围)
const detailData = ref({
businessScope: `第一行:经营范围示例
第二行:计算机软硬件销售、技术服务
第三行:电子产品研发、生产、销售
第四行:货物进出口、技术进出口
第五行:企业管理咨询、商务信息咨询
第六行:超出5行的内容,悬浮窗展示完整文本
第七行:支持HTML格式文本,比如<br/>换行标签
第八行:鼠标移入悬浮窗可正常选中文本`
});
// 标记文本是否超过5行
const isTextOverFlow = ref(false);
// 悬浮窗显隐控制
const showScopePopover = ref(false);
// 标记鼠标是否悬停在悬浮窗上(避免移入悬浮窗后消失)
const isHoverPopover = ref(false);
// 容器
const scopeContainer = ref(null);
// 显示文本
const textContentRef = ref(null);
// 克隆文本
const textCloneRef = ref(null);
// 悬浮窗固定位置样式
const popoverFixedStyle = ref({ left: '0px', top: '0px' });
/**
* 计算文本是否超过指定行数的核心函数
* 原理:克隆容器渲染完整文本,对比其高度与显示容器的最大高度
*/
const calcTextOverFlow = () => {
if (!textContentRef.value || !textCloneRef.value) return;
// 获取显示容器的最大高度(与CSS中的max-height一致)
const maxHeight = parseFloat(getComputedStyle(textContentRef.value).maxHeight);
// 获取克隆容器的完整文本高度
const actualHeight = textCloneRef.value.scrollHeight;
// 更新是否超行的标记
isTextOverFlow.value = actualHeight > maxHeight;
// 未超行时,强制隐藏悬浮窗
if (!isTextOverFlow.value) {
showScopePopover.value = false;
isHoverPopover.value = false;
}
// 未超行时移除鼠标指针提示
textContentRef.value.style.cursor = isTextOverFlow.value ? 'pointer' : 'default';
};
/**
* 计算悬浮窗固定位置(显示在文本容器下方10px)
*/
const updatePopoverFixedPos = () => {
if (!scopeContainer.value || !isTextOverFlow.value) return;
const rect = scopeContainer.value.getBoundingClientRect(); // 获取容器视口位置
popoverFixedStyle.value = {
left: `${rect.left}px`,
top: `${rect.bottom + 10}px`, // 文本下方10px显示
maxWidth: `${Math.min(500, document.documentElement.clientWidth - rect.left - 20)}px` // 避免超出视口右侧
};
};
/**
* 延迟判断是否隐藏悬浮窗
* 延迟100ms,鼠标可移入
*/
const handleContainerLeave = () => {
if (!isTextOverFlow.value) return;
setTimeout(() => {
if (!isHoverPopover.value) {
showScopePopover.value = false;
}
}, 100);
};
/**
* 隐藏悬浮窗并重置标记
*/
const handlePopoverLeave = () => {
isHoverPopover.value = false;
showScopePopover.value = false;
};
/**
* 监听文本数据变化,重新计算是否超行
*/
watch(
() => detailData.businessScope,
async () => {
await nextTick();
calcTextOverFlow();
},
{ immediate: true }
);
/**
* 重新获取文本高度和悬浮窗位置
*/
window.addEventListener('resize', async () => {
await nextTick();
calcTextOverFlow();
if (isTextOverFlow.value && showScopePopover.value) {
updatePopoverFixedPos();
}
});
</script>
4.注意
克隆容器的字号、行高、宽度必须与显示容器完全一致,否则高度计算错误。