循环列表内容超出时隐藏部分,点击阅读全文展开,内容不超出不显示该按钮。
javascript
<template>
<div class="collapse-list-container">
<el-card shadow="hover" class="list-card">
<el-list border :data="listData" class="content-list">
<el-list-item
v-for="(item, index) in listData"
:key="index"
class="list-item"
>
<!-- 标题 -->
<div class="item-title">{{ item.title }}</div>
<!-- 内容区域:仅渲染一次 -->
<div
class="item-content"
ref="contentRefs"
:style="{
height: item.isExpand ? 'auto' : `${twoLineHeight}px`,
maxHeight: item.isExpand ? 'none' : `${twoLineHeight}px`
}"
>
{{ item.content }}
</div>
<!-- 仅内容超两行时显示按钮 -->
<el-button
v-if="item.isOverTwoLine"
type="text"
class="toggle-btn"
@click="toggleExpand(index)"
>
{{ item.isExpand ? '收起' : '阅读全文' }}
</el-button>
</el-list-item>
</el-list>
</el-card>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, computed } from 'vue'
// 基础样式参数(与CSS保持一致,保证计算精准)
const fontSize = 14 // 字体大小(px)
const lineHeight = 1.6 // 行高倍数
// 计算两行文本的高度(行高*字体大小*行数)
const twoLineHeight = computed(() => lineHeight * fontSize * 2)
// 内容DOM引用
const contentRefs = ref([])
// 模拟列表数据(移除冗余字段,初始化isOverTwoLine为false)
const listData = ref([
{
title: '列表项1(超过两行)',
content: 'Vue3 是一套用于构建用户界面的渐进式框架,与其他大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。',
isExpand: false,
isOverTwoLine: false
},
{
title: '列表项2(刚好两行)',
content: 'Element Plus 是基于 Vue 3,面向设计师和开发者的组件库。Element Plus 延续了 Element UI 的设计理念',
isExpand: false,
isOverTwoLine: false
},
{
title: '列表项3(一行内容)',
content: '多行文本折叠是中后台系统常见的交互需求',
isExpand: false,
isOverTwoLine: false
}
])
// 切换展开/收起状态
const toggleExpand = (index) => {
listData.value[index].isExpand = !listData.value[index].isExpand
}
/**
* 计算文本真实高度(核心优化:纯JS计算,无DOM克隆)
* 原理:创建临时DOM,计算高度后立即销毁,不影响页面渲染
*/
const calculateTextHeight = (text, containerWidth) => {
// 创建临时div(仅在内存中,不挂载到DOM)
const tempDiv = document.createElement('div')
// 设置与真实内容完全一致的样式(保证计算精准)
tempDiv.style.cssText = `
font-size: ${fontSize}px;
line-height: ${lineHeight};
width: ${containerWidth}px;
position: absolute;
visibility: hidden;
height: auto;
overflow: hidden;
white-space: normal;
`
// 赋值文本(仅一次)
tempDiv.textContent = text
// 临时挂载到body(必须挂载才能计算高度)
document.body.appendChild(tempDiv)
// 获取真实高度
const height = tempDiv.offsetHeight
// 立即销毁临时DOM,避免内存泄漏
document.body.removeChild(tempDiv)
return height
}
// 初始化判断每个列表项是否超过两行
const initContentStatus = () => {
nextTick(() => {
contentRefs.value.forEach((contentRef, index) => {
if (!contentRef) return
// 获取容器宽度(保证计算的高度与实际显示一致)
const containerWidth = contentRef.offsetWidth
// 获取文本内容
const text = listData.value[index].content
// 计算文本真实高度
const realHeight = calculateTextHeight(text, containerWidth)
// 判断是否超过两行,并更新状态
listData.value[index].isOverTwoLine = realHeight > twoLineHeight.value
})
})
}
// 组件挂载后初始化
onMounted(() => {
initContentStatus()
})
// 可选:监听窗口大小变化,重新计算(适配响应式场景)
// import { onMounted, onUnmounted } from 'vue'
// let resizeObserver
// onMounted(() => {
// initContentStatus()
// resizeObserver = new ResizeObserver(initContentStatus)
// contentRefs.value.forEach(ref => ref && resizeObserver.observe(ref))
// })
// onUnmounted(() => {
// resizeObserver?.disconnect()
// })
</script>
<style scoped>
.collapse-list-container {
width: 800px;
margin: 20px auto;
}
.list-card {
padding: 20px;
}
.content-list {
--el-list-item-padding: 16px 0;
}
.list-item {
border-bottom: 1px solid #ebeef5;
padding-bottom: 16px;
margin-bottom: 16px;
}
.list-item:last-child {
border-bottom: none;
margin-bottom: 0;
}
.item-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
color: #1f2937;
}
/* 核心:可视内容区域 */
.item-content {
font-size: 14px; /* 与JS中fontSize一致 */
color: #4b5563;
line-height: 1.6; /* 与JS中lineHeight一致 */
overflow: hidden;
transition: height 0.3s ease-in-out; /* 流畅动画 */
width: 100%;
position: relative;
}
/* 折叠状态添加省略号 */
.item-content:not([style*="auto"])::after {
content: '...';
position: absolute;
right: 0;
bottom: 0;
background-color: #ffffff;
padding-left: 8px;
}
.toggle-btn {
color: #409eff;
padding: 0;
margin-top: 8px;
}
.toggle-btn:hover {
color: #66b1ff;
}
</style>