组件代码:
<template>
<!-- 外层容器,使用相对定位 -->
<div class="relative">
<!-- 文本容器,根据 expanded 状态决定是否应用 line-clamp-4 类 -->
<div :class="{ 'line-clamp-4': !expanded }" ref="textContainer">
{{ text }}
</div>
<!-- 展开/收起按钮,只有在文本需要折叠时才显示,使用绝对定位 -->
<button
v-if="isCollapsible"
@click="toggleExpand"
class="text-blue-500 absolute -right-6 bottom-0"
>
{{ expanded ? '收起' : '展开' }}
</button>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
// 定义组件的 props
const props = defineProps({
text: {
type: String,
required: true
},
fontSize: {
type: String,
required: false,
default: '12px'
}
})
const expanded = ref(false) // 控制文本是否展开
const isCollapsible = ref(false) // 控制文本是否需要折叠
const textContainer = ref(null) // 引用文本容器
// 切换展开/收起状态的函数
const toggleExpand = () => {
expanded.value = !expanded.value
}
// 检查文本是否需要折叠的函数
const checkCollapsible = () => {
nextTick(() => {
const width = textContainer.value.clientWidth // 获取文本容器的宽度
const dummyElement = document.createElement('div')
dummyElement.style.position = 'absolute'
dummyElement.style.visibility = 'hidden'
dummyElement.style.width = `${width}px`
dummyElement.style.fontSize = props.fontSize // 设置字体大小
dummyElement.style.lineHeight = getComputedStyle(textContainer.value).lineHeight
dummyElement.innerText = props.text
document.body.appendChild(dummyElement)
const lineHeight = parseInt(getComputedStyle(dummyElement).lineHeight) // 获取行高
const totalHeight = dummyElement.clientHeight // 获取文本总高度
document.body.removeChild(dummyElement)
const maxHeight = lineHeight * 4 + 5 // 设定最大高度(假设最多显示4行)
isCollapsible.value = totalHeight > maxHeight // 判断是否需要折叠
})
}
// 组件挂载时执行的操作
onMounted(() => {
checkCollapsible() // 检查文本是否需要折叠
window.addEventListener('resize', checkCollapsible) // 监听窗口大小变化,重新检查
})
</script>
<style scoped>
</style>
效果图:
优化版本,展开全部文字 展示在文本域后面。需要做一个截取文本操作。
<template>
<div>
<div ref="textContainer" :class="{ 'line-clamp-4': !expanded }">
<span v-if="!expanded">{{ truncatedText }}</span>
<span v-else>{{ text }}</span>
<button v-if="isCollapsible" @click="toggleExpand" class="text-blue-500 ml-2">
{{ expanded ? '收起' : '展开全部' }}
</button>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick } from 'vue'
// 定义组件的 props
const props = defineProps({
text: {
type: String,
required: true
},
fontSize: {
type: String,
required: false,
default: '12px'
},
targetLines: {
type: Number,
required: false,
default: 4
}
})
// 定义响应式变量
const expanded = ref(false) // 控制文本是否展开
const isCollapsible = ref(false) // 控制文本是否需要折叠
const textContainer = ref(null) // 引用文本容器
const truncatedText = ref('') // 存储截断的文本
// 切换展开/收起状态的函数
const toggleExpand = () => {
expanded.value = !expanded.value
}
// 检查文本是否需要折叠的函数
const checkCollapsible = () => {
nextTick(() => {
const width = textContainer.value.clientWidth
const dummyElement = document.createElement('div')
dummyElement.style.position = 'absolute'
dummyElement.style.visibility = 'hidden'
dummyElement.style.width = `${width}px`
dummyElement.style.fontSize = props.fontSize // 设置字体大小
dummyElement.style.lineHeight = getComputedStyle(textContainer.value).lineHeight
dummyElement.style.whiteSpace = 'pre-wrap'
dummyElement.style.wordWrap = 'break-word'
dummyElement.innerText = props.text
document.body.appendChild(dummyElement)
const lineHeight = parseInt(getComputedStyle(dummyElement).lineHeight) // 获取行高
const maxHeight = lineHeight * props.targetLines // 设定最大高度
isCollapsible.value = dummyElement.clientHeight > maxHeight // 判断是否需要折叠
if (isCollapsible.value) {
// 截断文本以适应最大高度
let totalHeight = 0
let truncated = ''
const words = props.text.split(' ')
for (let i = 0; i < words.length; i++) {
const word = words[i]
dummyElement.innerText = truncated + word + ' ' + '... 展示全部'
if (dummyElement.clientHeight > maxHeight) break
truncated += word + ' '
}
truncatedText.value = truncated.trimEnd() + '...'
} else {
truncatedText.value = props.text
}
document.body.removeChild(dummyElement)
})
}
// 组件挂载时执行的操作
onMounted(() => {
checkCollapsible() // 检查文本是否需要折叠
window.addEventListener('resize', checkCollapsible) // 监听窗口大小变化,重新检查
})
</script>
<style scoped>
</style>
效果图:
修复中英文显示异常问题
if (isCollapsible.value) {
// 截断文本以适应最大高度
let truncated = ''
const chars = props.text.split('')
for (let i = 0; i < chars.length; i++) {
const char = chars[i]
if (char.match(/\s/)) {
// 如果字符是空白字符(如空格),直接追加
truncated += char
} else if (char.match(/[a-zA-Z0-9]/)) {
// 如果字符是英文或数字,找出完整单词
let word = char
while (i + 1 < chars.length && chars[i + 1].match(/[a-zA-Z0-9]/)) {
word += chars[++i]
}
dummyElement.innerText = truncated + word + '... 展示全部'
if (dummyElement.clientHeight > maxHeight) break
truncated += word
} else {
// 如果字符是中文或其他字符,逐个处理
dummyElement.innerText = truncated + char + '... 展示全部'
if (dummyElement.clientHeight > maxHeight) break
truncated += char
}
}
truncatedText.value = truncated.trimEnd() + '...'
} else {
truncatedText.value = props.text
}
实用时发现,当列表数据量过大,超过200时,计算宽度会导致页面卡顿。遂决定使用IntersectionObserver
来监听元素是否可见,只在可见区域的才让其计算文字宽高,是否需要截断。
优化后代码如下:
<template>
<div ref="textContainer">
<slot></slot>
<span v-if="!expanded">{{ displayedText }}</span>
<span v-else>{{ text }}</span>
<button v-if="isCollapsible" @click="toggleExpand" class="text-blue-500 ml-1">
{{ expanded ? '收起' : '展开全部' }}
</button>
</div>
</template>
<script setup>
import { ref, onMounted, computed, watch } from 'vue'
// 定义组件的 props
const props = defineProps({
text: {
type: String,
required: true
},
fontSize: {
type: String,
required: false,
default: '12px'
},
targetLines: {
type: Number,
required: false,
default: 4
}
})
// 响应式变量
const expanded = ref(false) // 控制文本是否展开
const isCollapsible = ref(false) // 控制文本是否需要折叠
const textContainer = ref(null) // 引用文本容器
const truncatedText = ref('') // 存储截断的文本
const isVisible = ref(false) // 判断元素是否可见
// 缓存 Canvas 上下文,避免重复创建
const canvas = document.createElement('canvas')
const context = canvas.getContext('2d')
// 切换展开/收起状态的函数
const toggleExpand = () => {
expanded.value = !expanded.value
}
// 获取文本宽度的函数
const getTextWidth = (text, fontSize) => {
context.font = `${fontSize}px Arial`
return context.measureText(text).width
}
// 计算截断文本的函数
const calculateTruncatedText = () => {
const width = textContainer.value.clientWidth
const lineHeight = parseInt(props.fontSize) * 1.5 // 估算行高
const maxHeight = lineHeight * props.targetLines
// 测量全文高度
const fullTextHeight = Math.ceil(getTextWidth(props.text, parseInt(props.fontSize)) / width) * lineHeight
isCollapsible.value = fullTextHeight > maxHeight
if (isCollapsible.value) {
let truncated = ''
let currentHeight = 0
const chars = props.text.split('')
for (let i = 0; i < chars.length; i++) {
const char = chars[i]
truncated += char
const truncatedHeight = Math.ceil(getTextWidth(truncated + '... ', parseInt(props.fontSize)) / width) * lineHeight
if (truncatedHeight > maxHeight) {
truncatedText.value = truncated.trimEnd() + '... '
return
}
currentHeight = truncatedHeight
}
} else {
truncatedText.value = props.text
}
}
// 通过 IntersectionObserver 监听元素是否可见
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
isVisible.value = true
observer.disconnect() // 元素可见后取消观察,防止重复触发
}
})
})
// 监听 isVisible,当元素可见时计算文本
watch(isVisible, (visible) => {
if (visible) {
calculateTruncatedText()
}
})
// 组件挂载时执行的操作
onMounted(() => {
observer.observe(textContainer.value) // 开始观察文本容器的可见性
})
// 计算显示文本
const displayedText = computed(() => (expanded.value ? props.text : truncatedText.value))
</script>
<style scoped></style>