uni-app + Vue3 实现折叠文本(超出省略 + 展开收起)

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 行 省略 + 展开 ✅
展开后 显示全文 ✅
切换语言 按钮可更换 ✅

相关推荐
冴羽2 小时前
JavaScript 异步循环踩坑指南
前端·javascript·node.js
小禾青青2 小时前
uniapp安卓打包遇到报错:Uncaught SyntaxError: Invalid regular expression: /[\p{L}\p{N}]/
android·uni-app
jump6802 小时前
commonjs 和 ES Module
前端
旧曲重听12 小时前
前端需要掌握多少Node.js?
前端·node.js
Mr.Jessy2 小时前
Web APIs 学习第四天:DOM事件进阶
开发语言·前端·javascript·学习·ecmascript
云枫晖2 小时前
前端工程化实战:手把手教你构建项目脚手架
前端·前端工程化
醉方休2 小时前
开发一个完整的Electron应用程序
前端·javascript·electron
环信即时通讯云3 小时前
实现小程序 uniApp 输入框展示自定义表情包
小程序·uni-app
故作春风3 小时前
手把手实现一个前端 AI 编程助手:从 MCP 思想到 VS Code 插件实战
前端·人工智能