uni-app + Vue3 实现折叠文本(超出省略 + 展开收起)
实现一个"最多展示 N 行文本 + 展开 / 收起"组件,支持 H5 / 微信小程序 / App,适用于 uni-app + Vue3 + TypeScript。
✅ 功能说明
- 自动识别文本是否超出
- 超出时显示"展开 ▼ / 收起 ▲"按钮
- 支持自定义显示行数(默认 4 行)
- 全平台通用(H5 / 小程序 / App)
- TypeScript 类型友好
✅ 最终效果
使用方式如下:
vue
<ClampText :text="item.name" :lines="4" />
短文本:不显示按钮
长文本:显示"展开 / 收起"并可切换
✅ 组件文件:components/ClampText.vue
建议放到
components/ClampText.vue
vue
<template>
<view class="clamp-text-wrapper">
<!-- 文本区域,折叠时应用 -webkit-line-clamp,多行省略 -->
<view
:id="textId"
data-clamp-text
class="text"
:class="{ [`clamp-${lines}-lines`]: !expanded }"
>
{{ text }}
</view>
<!-- 超出时显示展开 / 收起按钮 -->
<view
v-if="overflow"
class="toggle-btn"
@tap="toggle"
>
{{ expanded ? '收起 ▲' : '展开 ▼' }}
</view>
</view>
</template>
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue'
// Props 类型定义
interface Props {
text: string
lines?: number
}
// 设置默认显示行数为 4 行
const props = withDefaults(defineProps<Props>(), {
lines: 4
})
const expanded = ref(false) // 当前是否展开
const overflow = ref(false) // 文本是否超出
// 为每个组件生成唯一 ID,方便获取高度
const textId = `text-${Math.random().toString(36).slice(2)}`
/**
* 把 rpx 转成 px(基于当前设备宽度)
* 1 rpx = windowWidth / 750 px
*/
const rpxToPx = (rpx: number): number => {
try {
const info = uni.getSystemInfoSync()
const winWidth = info.windowWidth || 375
return (rpx * winWidth) / 750
} catch (e) {
// 兜底值
return rpx * (375 / 750)
}
}
/**
* 检测一组带 .desc 的元素是否超过 clampLines
* - clampRpxLine: CSS 中设置的每行行高(单位 rpx),务必与 CSS 中 line-height 保持一致
* - clampLines: 行数(这里是 4)
*/
const detectOverflow = async (clampRpxLine = 30, clampLines = 4) => {
await nextTick() // 等待 DOM 渲染
return new Promise<void>((resolve) => {
const lineHeightPx = rpxToPx(clampRpxLine)
const threshold = lineHeightPx * clampLines + 1 // +1 像素容错
const query = uni.createSelectorQuery()
// 选择所有 .desc 元素(跨端兼容)
query.selectAll(`#${textId}`).boundingClientRect((rects: Array<{ height: number }>) => {
if (!rects || !rects.length) {
resolve()
return
}
rects.forEach((rect, i) => {
const h = rect?.height || 0
// 如果高度大于阈值,认为超过 clampLines
overflow.value[i] = h > threshold
})
resolve()
}).exec()
})
}
/* 首次检测,和 menu 变化时重新检测 */
onMounted(async () => {
// 等待数据可能异步加载完:如果你的 menu 是异步加载,确保在获取后再调用 detectOverflow
await nextTick()
// 这里以 30rpx 行高(对应 CSS)和 4 行为例
detectOverflow(20, 4)
})
// 切换展开 / 收起
const toggle = () => {
expanded.value = !expanded.value
nextTick(detectOverflow)
}
// 文本变化时重新计算
watch(() => props.text, () => {
expanded.value = false
nextTick(() => setTimeout(() => detectOverflow(20, 4), 50))
}, { immediate: true })
</script>
<style scoped lang="scss">
/* 外层包裹 */
.clamp-text-wrapper {
margin-bottom: 10px;
}
/* 文本样式 */
.text {
font-size: 14px;
line-height: 24px; /* 与 JS 检测高度保持一致 */
}
/* 行数裁剪规则(支持 1-5 行,可扩展) */
.clamp-1-lines { display: -webkit-box; -webkit-line-clamp: 1; overflow: hidden; -webkit-box-orient: vertical; }
.clamp-2-lines { display: -webkit-box; -webkit-line-clamp: 2; overflow: hidden; -webkit-box-orient: vertical; }
.clamp-3-lines { display: -webkit-box; -webkit-line-clamp: 3; overflow: hidden; -webkit-box-orient: vertical; }
.clamp-4-lines { display: -webkit-box; -webkit-line-clamp: 4; overflow: hidden; -webkit-box-orient: vertical; }
.clamp-5-lines { display: -webkit-box; -webkit-line-clamp: 5; overflow: hidden; -webkit-box-orient: vertical; }
/* 展开 / 收起按钮 */
.toggle-btn {
font-size: 14px;
margin-top: 4px;
color: #007aff; /* iOS 风格蓝色 */
}
</style>
✅ 使用例子
vue
<template>
<view>
<ClampText
v-for="(item, index) in list"
:key="index"
:text="item.text"
:lines="4"
/>
</view>
</template>
<script setup lang="ts">
const list = ref([
{ text: "短文本,不会显示按钮" },
{ text: "一段非常非常非常非常非常非常长的文本......演示折叠效果......" }
])
</script>
🎯 亮点与细节
| 优势 | 说明 |
|---|---|
| ✅ 全平台通用 | H5 / 小程序 / App |
| ✅ 自动判断超出 | 短文不出现按钮 |
| ✅ TS 强类型 | 更符合工程化 |
| ✅ 真实高度检测 | 不依赖纯 CSS |
| ✅ nextTick + 延迟 | 解决渲染滞后 |
📦 下一步可扩展
🚀 v-clamp 指令版教程
✨ 功能目标
- 显示指定行数(如:3 行、4 行)
- 自动检测文本是否溢出
- 已溢出 → 显示省略号 + "展开"按钮
- 展开后 → 显示完整内容 + "收起"按钮
- "展开/收起" 按钮紧跟省略号之后
- 可配置显示行数、按钮文案,支持 i18n
- ✅ 不侵入 DOM 结构(比组件更优雅)
📂 目录结构
src/
└─ directives/
└─ clamp.ts
main.ts
✅ 最终使用效果
vue
<view
v-clamp="{
lines: 4,
moreText: '展开',
lessText: '收起'
}"
>
{{ longText }}
</view>
效果:默认最多显示 4 行,后面出现 ... 展开,点击变 收起
🧠 实现思路
| 步骤 | 说明 |
|---|---|
| 1️⃣ | 获取原始文本内容并记录 |
| 2️⃣ | 先限制行数 -webkit-line-clamp |
| 3️⃣ | 计算 scrollHeight > clientHeight → 判断是否溢出 |
| 4️⃣ | 如果溢出 → 显示省略号 + 展开按钮 |
| 5️⃣ | 点击展开 → 取消 line-clamp,展示全文 |
| 6️⃣ | 点击收起 → 恢复行数限制 + 省略号 |
UI 和逻辑全部由 自定义指令 控制,不改变模板结构。
🧩 v-clamp 指令源码 + 注释
📁 src/directives/clamp.ts
ts
import { DirectiveBinding } from "vue";
interface ClampOptions {
lines?: number; // 最大显示行数
moreText?: string; // 展开按钮文案
lessText?: string; // 收起按钮文案
}
function applyClamp(el: HTMLElement, options: ClampOptions) {
const { lines = 3, moreText = "展开", lessText = "收起" } = options;
const fullText = el.innerText; // 保存原始内容
let isExpanded = false; // 展开状态
const computeClamp = () => {
if (isExpanded) {
// ✅ 展开状态,显示全文
el.innerHTML = `
<span>${fullText}</span>
<a style="color:#007aff;margin-left:6px;">${lessText}</a>
`;
el.querySelector("a")?.addEventListener("click", () => {
isExpanded = false;
computeClamp();
});
} else {
// ✅ 折叠状态,应用 CSS 行数限制
el.style.display = "-webkit-box";
el.style.webkitBoxOrient = "vertical";
el.style.webkitLineClamp = String(lines);
el.style.overflow = "hidden";
el.innerHTML = `
<span class="clamp-text">${fullText}</span>
<a style="color:#007aff;margin-left:6px;">${moreText}</a>
`;
const span = el.querySelector("span") as HTMLElement;
// ⭐ 检测是否真的溢出,避免没超过还显示按钮
setTimeout(() => {
const needClamp = span.scrollHeight > span.clientHeight;
if (!needClamp) {
el.innerHTML = `<span>${fullText}</span>`;
return;
}
// ✅ 注册"展开"按钮事件
const btn = el.querySelector("a");
btn?.addEventListener("click", () => {
isExpanded = true;
el.style.webkitLineClamp = "unset";
computeClamp();
});
});
}
};
computeClamp();
}
export default {
mounted(el: HTMLElement, binding: DirectiveBinding) {
applyClamp(el, binding.value || {});
},
updated(el: HTMLElement, binding: DirectiveBinding) {
applyClamp(el, binding.value || {});
}
};
🌐 全局注册指令
📁 main.ts
ts
import { createApp } from "vue";
import App from "./App.vue";
import clamp from "@/directives/clamp";
const app = createApp(App);
app.directive("clamp", clamp);
app.mount("#app");
✅ 页面实际使用
vue
<template>
<view class="text-box"
v-clamp="{
lines: 4,
moreText: '展开',
lessText: '收起'
}"
>
{{ longText }}
</view>
</template>
<script setup lang="ts">
const longText = `
Uni-app 是一个使用 Vue 语法开发所有前端应用的框架,
可以编译到微信小程序、H5、APP 等多个端。
使用自定义指令能优雅实现文本折叠展开效果。
`;
</script>
🔍 原理讲解总结
| 技术点 | 说明 |
|---|---|
CSS -webkit-line-clamp |
实现多行省略 |
JS scrollHeight > clientHeight |
判断文本是否溢出 |
动态 innerHTML |
动态插入按钮 |
| 事件监听 | 控制展开/收起 |
自定义指令 directive |
让模板更干净,易复用 |
🧪 测试边界
| 情况 | 效果 |
|---|---|
| 文本 < N 行 | 不显示按钮 ✅ |
| 文本 > N 行 | 省略 + 展开 ✅ |
| 展开后 | 显示全文 ✅ |
| 切换语言 | 按钮可更换 ✅ |