Vue3 多行文本溢出隐藏与展开收起功能实现总结
在 Vue3 中实现多行文本溢出隐藏并显示展开/收起功能是常见的 UI 需求。本文将总结两种高效的实现方法,分析它们的实现原理、优缺点以及适用场景。
方法一:ExpandableContent 组件(基于 CSS -webkit-line-clamp
)


实现原理
- 使用 CSS 的
-webkit-line-clamp
属性实现多行文本截断 - 动态计算文本高度判断是否需要显示展开按钮
- 通过响应式状态管理展开/收起状态
- 使用 ResizeObserver 监听内容变化
核心代码
js
<template>
<div class="expandable-content">
<div
ref="contentRef"
:class="{ expanded: isExpanded }"
:style="{
'--max-lines': isExpanded ? 'unset' : props.maxLines,
'--line-height': lineHeight,
'--max-height': isExpanded ? 'unset' : maxHeight
}"
class="text-content"
>
<slot></slot>
</div>
<div
v-show="shouldShowButton"
class="toggle-btn"
>
<div class="centered-container" @click="toggleExpand">
{{ buttonText }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
interface Props {
maxLines?: number
expandText?: string
collapseText?: string
}
const props = withDefaults(defineProps<Props>(), {
maxLines: 3,
expandText: '展开',
collapseText: '收起'
})
const contentRef = ref<HTMLElement | null>(null)
const isExpanded = ref(false)
const buttonText = computed(() => (isExpanded.value ? props.collapseText : props.expandText))
const lineHeight = 1.5
const maxHeight = computed(() => `${props.maxLines * lineHeight}em`)
// 切换展开状态
const toggleExpand = () => {
isExpanded.value = !isExpanded.value
}
const hasOverflow = ref(false) // 标记内容是否超出
// 判断内容是否超出最大行数
const checkOverflow = () => {
if (contentRef.value) {
const lineHeight = parseFloat(getComputedStyle(contentRef.value).lineHeight)
const maxHeight = props.maxLines * lineHeight
hasOverflow.value = contentRef.value.scrollHeight > maxHeight
}
}
// 初始化时和内容变化时检查
const observer = ref<ResizeObserver | null>(null)
onMounted(() => {
checkOverflow()
if (contentRef.value) {
observer.value = new ResizeObserver(checkOverflow)
observer.value.observe(contentRef.value)
}
})
onUnmounted(() => {
observer.value?.disconnect()
})
// 控制按钮显示
const shouldShowButton = computed(() => hasOverflow.value)
</script>
<style scoped lang="scss">
.expandable-content {
position: relative;
.text-content {
display: -webkit-box;
-webkit-line-clamp: var(--max-lines);
-webkit-box-orient: vertical;
overflow: hidden;
line-height: var(--line-height);
max-height: var(--max-height);
transition: max-height 0.3s;
}
.text-content.expanded {
-webkit-line-clamp: unset;
}
.toggle-btn {
display: flex;
justify-content: center;
.centered-container {
cursor: pointer;
display: flex;
justify-content: center;
align-items: center;
color: #1890ff;
}
}
}
</style>
优点
- 实现简单直观:使用标准 CSS 属性
- 动态内容支持:自动检测内容变化
- 平滑过渡:收起/展开状态切换流畅
- 插槽支持:可以包含任意 HTML 内容
缺点
- 依赖
-webkit-line-clamp
,兼容性有一定限制 - 需要动态计算高度判断是否显示按钮
方法二:TextEllipsis 组件(纯 CSS 实现)


实现原理
- 利用浮动元素和伪元素创建省略效果
- 使用 CSS 阴影覆盖实现视觉上的省略号
- 通过隐藏的 checkbox 控制展开状态
- 利用 CSS 选择器切换状态样式
核心代码
js
<script setup lang="ts">
import { ref, computed, defineSlots } from 'vue'
interface Props {
text?: string
maxLines?: number
expandText?: string
collapseText?: string
shadowColor?: string
}
const props = withDefaults(defineProps<Props>(), {
text: undefined,
maxLines: 3,
shadowColor: '#ffffff',
expandText: '展开',
collapseText: '收起'
})
defineSlots<{
default?: () => string
toggle?: (props: { expanded: boolean; toggle: () => void }) => any
}>()
const expanded = ref(false)
const toggleId = ref(`exp-${Math.random().toString(36).slice(2, 11)}`)
const textRef = ref<HTMLElement | null>(null)
const buttonText = computed(() => expanded.value ? props.collapseText : props.expandText)
const lineHeight = 1.5
const maxHeight = computed(() => `${props.maxLines * lineHeight}em`)
const toggle = () => {
expanded.value = !expanded.value
}
</script>
<template>
<div class="text-ellipsis-wrapper">
<input :id="toggleId" class="text-ellipsis-exp" type="checkbox" v-model="expanded">
<div ref="textRef" class="text-ellipsis-text"
:style="{ '--max-lines': props.maxLines, '--line-height': lineHeight, '--max-height': maxHeight, '--shadow-color': props.shadowColor }">
<label class="text-ellipsis-label" :for="toggleId">
<slot name="toggle" :expanded="expanded" :toggle="toggle">
<button class="default-btn" @click="toggle">
{{ buttonText }}
</button>
</slot>
</label>
<slot :msg="text">{{ text }}</slot>
</div>
</div>
</template>
<style scoped>
.text-ellipsis-wrapper {
display: flex;
overflow: hidden;
border-radius: 8px;
padding: 15px;
}
.text-ellipsis-text {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
text-align: justify;
position: relative;
line-height: var(--line-height);
max-height: var(--max-height);
transition: .3s max-height;
}
.text-ellipsis-text::before {
content: '';
height: calc(100% - 24px);
float: right;
}
.text-ellipsis-text::after {
content: '';
width: 999vw;
height: 999vw;
position: absolute;
box-shadow: inset calc(100px - 999vw) calc(30px - 999vw) 0 0 var(--shadow-color);
margin-left: -100px;
}
.text-ellipsis-label {
position: relative;
float: right;
clear: both;
margin-left: 20px;
}
.default-btn {
background: none;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
color: #1890ff;
}
.text-ellipsis-label::before {
content: '...';
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%)
}
.text-ellipsis-exp {
display: none;
}
.text-ellipsis-exp:checked+.text-ellipsis-text {
max-height: none;
}
.text-ellipsis-exp:checked+.text-ellipsis-text::after {
visibility: hidden;
}
.text-ellipsis-exp:checked+.text-ellipsis-text .text-ellipsis-label::before {
visibility: hidden;
}
</style>
优点
- 纯 CSS 实现:不依赖 JavaScript 计算
- 良好兼容性:支持更多浏览器
- 高度自定义:提供具名插槽自定义按钮
- 视觉效果统一:省略号位置精准
缺点
- 实现相对复杂,理解成本高
- 依赖浮动布局,可能受父容器影响
- 需要设置阴影颜色匹配背景
两种方法对比
特性 | ExpandableContent | TextEllipsis |
---|---|---|
实现原理 | CSS -webkit-line-clamp + JS 计算 | 纯 CSS 浮动 + 阴影覆盖 |
兼容性 | 现代浏览器(Chrome, Safari, Edge) | 更广泛的浏览器支持 |
动态内容 | 自动检测变化(ResizeObserver) | 需要外部更新 |
按钮位置 | 独立于文本容器下方 | 内联在文本末尾 |
自定义按钮 | 通过默认插槽简单实现 | 提供具名插槽高度自定义 |
背景适配 | 无限制 | 需要设置阴影颜色匹配背景 |
实现复杂度 | 中等 | 较高 |
使用示例
xml
<template>
<!-- 方法一组件 -->
<ExpandableContent
class="custom-container"
:max-lines="3"
>
<div v-html="longContent1"></div>
</ExpandableContent>
<!-- 方法二组件 -->
<TextEllipsis
class="custom-container"
:text="longContent"
:maxLines="3"
shadowColor="#f0f2f5"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import ExpandableContent from './ExpandableContent.vue';
import TextEllipsis from './TextEllipsis.vue';
const longContent = ref(`
这是一段很长的文本内容,用于测试多行文本溢出省略和展开收起功能。
当文本内容超过指定行数时,会显示展开按钮,点击后可查看完整内容。
该组件基于 Vue3 + TypeScript 实现,支持动态文本更新和自定义样式。
`);
const longContent1 = ref(`这是一段很长的文本内容,用于测试多行文本溢出省略和展开收起功能。<br>当文本内容超过指定行数时,会显示展开按钮,点击后可查看完整内容。<br>该组件基于 Vue3 + TypeScript 实现,支持动态文本更新和自定义样式。`)
</script>
<style>
.custom-container {
width: 400px;
margin: 20px auto;
padding: 15px;
background-color: #f0f2f5;
border-radius: 8px;
}
</style>
如何选择
- 现代浏览器项目 :推荐使用 ExpandableContent,实现简洁且功能完善
- 需要兼容旧浏览器 :推荐使用 TextEllipsis,纯 CSS 方案兼容性更好
- 高度自定义按钮 :TextEllipsis 提供更灵活的插槽定制
- 动态内容变化频繁 :ExpandableContent 自动监听变化更方便
最佳实践建议
- 行高设置:保持一致的文本行高(建议 1.5em)
- 过渡动画:添加 max-height 过渡效果提升用户体验
- 无障碍支持:为按钮添加适当的 ARIA 属性
- 响应式设计:考虑不同屏幕尺寸下的显示效果
- 性能优化:对长文本使用虚拟滚动技术
总结
两种实现多行文本溢出隐藏的方法各有优势:
- ExpandableContent 方案采用现代 CSS 特性结合响应式编程,实现直观且功能完善
- TextEllipsis 方案通过纯 CSS 技巧实现,兼容性好且提供高度自定义能力
开发者可根据项目需求选择合适的方案。对于大多数现代 Web 应用,ExpandableContent 方案更为推荐;而对于需要兼容旧浏览器或特殊样式需求的项目,TextEllipsis 方案则是更好的选择。