Vue3 多行文本溢出隐藏与展开收起功能实现总结

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>

优点

  1. 实现简单直观:使用标准 CSS 属性
  2. 动态内容支持:自动检测内容变化
  3. 平滑过渡:收起/展开状态切换流畅
  4. 插槽支持:可以包含任意 HTML 内容

缺点

  1. 依赖 -webkit-line-clamp,兼容性有一定限制
  2. 需要动态计算高度判断是否显示按钮

方法二:TextEllipsis 组件(纯 CSS 实现)

实现原理

  1. 利用浮动元素和伪元素创建省略效果
  2. 使用 CSS 阴影覆盖实现视觉上的省略号
  3. 通过隐藏的 checkbox 控制展开状态
  4. 利用 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>

优点

  1. 纯 CSS 实现:不依赖 JavaScript 计算
  2. 良好兼容性:支持更多浏览器
  3. 高度自定义:提供具名插槽自定义按钮
  4. 视觉效果统一:省略号位置精准

缺点

  1. 实现相对复杂,理解成本高
  2. 依赖浮动布局,可能受父容器影响
  3. 需要设置阴影颜色匹配背景

两种方法对比

特性 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>

如何选择

  1. 现代浏览器项目 :推荐使用 ExpandableContent,实现简洁且功能完善
  2. 需要兼容旧浏览器 :推荐使用 TextEllipsis,纯 CSS 方案兼容性更好
  3. 高度自定义按钮TextEllipsis 提供更灵活的插槽定制
  4. 动态内容变化频繁ExpandableContent 自动监听变化更方便

最佳实践建议

  1. 行高设置:保持一致的文本行高(建议 1.5em)
  2. 过渡动画:添加 max-height 过渡效果提升用户体验
  3. 无障碍支持:为按钮添加适当的 ARIA 属性
  4. 响应式设计:考虑不同屏幕尺寸下的显示效果
  5. 性能优化:对长文本使用虚拟滚动技术

总结

两种实现多行文本溢出隐藏的方法各有优势:

  • ExpandableContent 方案采用现代 CSS 特性结合响应式编程,实现直观且功能完善
  • TextEllipsis 方案通过纯 CSS 技巧实现,兼容性好且提供高度自定义能力

开发者可根据项目需求选择合适的方案。对于大多数现代 Web 应用,ExpandableContent 方案更为推荐;而对于需要兼容旧浏览器或特殊样式需求的项目,TextEllipsis 方案则是更好的选择。

相关推荐
一斤代码1 小时前
vue3 下载图片(标签内容可转图)
前端·javascript·vue
中微子1 小时前
React Router 源码深度剖析解决面试中的深层次问题
前端·react.js
光影少年2 小时前
从前端转go开发的学习路线
前端·学习·golang
中微子2 小时前
React Router 面试指南:从基础到实战
前端·react.js·前端框架
3Katrina2 小时前
深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器
前端·javascript·面试
前端_学习之路3 小时前
React--Fiber 架构
前端·react.js·架构
伍哥的传说3 小时前
React 实现五子棋人机对战小游戏
前端·javascript·react.js·前端框架·node.js·ecmascript·js
qq_424409193 小时前
uniapp的app项目,某个页面长时间无操作,返回首页
前端·vue.js·uni-app
我在北京coding3 小时前
element el-table渲染二维对象数组
前端·javascript·vue.js
布兰妮甜3 小时前
Vue+ElementUI聊天室开发指南
前端·javascript·vue.js·elementui