多行文本的文字展示全部和收起功能

组件代码:

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值