vue3+lodash+ts+tailwin 实现多行文本的展开收起代码

2 阅读1分钟
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
import { debounce } from 'lodash-es'

interface Props {
text: string
maxLines?: number
expandText?: string
collapseText?: string
expandClass?: string
collapseClass?: string
}

const props = withDefaults(defineProps<Props>(), {
maxLines: 3,
expandText: '展开',
collapseText: '收起',
expandClass: 'text-blue-500',
collapseClass: 'text-blue-500',
})

const containerRef = ref<HTMLElement>()
const expanded = ref(false)
const isTruncated = ref(false)
const truncatedText = ref(props.text)

function getLineHeight(el: HTMLElement): number {
const lh = parseFloat(getComputedStyle(el).lineHeight)
return isNaN(lh) ? parseFloat(getComputedStyle(el).fontSize) \* 1.5 : lh
}

function createMeasureEl(el: HTMLElement, width: number): HTMLDivElement {
const cs = getComputedStyle(el)
const div = document.createElement('div')
div.style.cssText = `     position: absolute;
    visibility: hidden;
    pointer-events: none;
    width: ${width}px;
    font-size: ${cs.fontSize};
    font-family: ${cs.fontFamily};
    font-weight: ${cs.fontWeight};
    line-height: ${cs.lineHeight};
    letter-spacing: ${cs.letterSpacing};
    word-break: ${cs.wordBreak};
    white-space: ${cs.whiteSpace};
  `
document.body.appendChild(div)
return div
}

function calcTruncation() {
const el = containerRef.value
if (!el || expanded.value) return

const cs = getComputedStyle(el)
const width = el.clientWidth
\- parseFloat(cs.paddingLeft)
\- parseFloat(cs.paddingRight)
if (width <= 0) return

const lineHeight = getLineHeight(el)
const maxHeight = lineHeight \* props.maxLines

const measureEl = createMeasureEl(el, width)

// 检查是否需要截断
measureEl.textContent = props.text
const fullHeight = measureEl.scrollHeight

if (fullHeight <= maxHeight) {
document.body.removeChild(measureEl)
isTruncated.value = false
truncatedText.value = props.text
return
}

isTruncated.value = true

// 二分搜索,suffix 多加一个占位字符抵消 ml-0.5 偏差
const suffix = `...${props.expandText}x`
let lo = 0
let hi = props.text.length

while (lo < hi) {
const mid = Math.floor((lo + hi + 1) / 2)
measureEl.textContent = props.text.slice(0, mid) + suffix
if (measureEl.scrollHeight <= maxHeight) {
lo = mid
} else {
hi = mid - 1
}
}

document.body.removeChild(measureEl)
truncatedText.value = props.text.slice(0, lo)
}

const debouncedCalc = debounce(calcTruncation, 100)

let resizeObserver: ResizeObserver | null = null

onMounted(() => {
nextTick(() => {
calcTruncation()
if (containerRef.value) {
resizeObserver = new ResizeObserver(debouncedCalc)
resizeObserver.observe(containerRef.value)
}
})
})

onUnmounted(() => {
resizeObserver?.disconnect()
debouncedCalc.cancel()
})

watch(
() => \[props.text, props.maxLines],
() => {
expanded.value = false
nextTick(calcTruncation)
},
)

function expand() {
expanded.value = true
}

function collapse() {
expanded.value = false
nextTick(calcTruncation)
} </script>

<template>
  <div ref="containerRef">
    <!-- 收起状态:文字...展开 全部在同一行内联流 -->
    <template v-if="!expanded">{{ truncatedText }}<template v-if="isTruncated">...<button
          :class="expandClass"
          class="inline ml-0.5 cursor-pointer bg-transparent border-none p-0 [font-family:inherit] [font-size:inherit] [line-height:inherit]"
          @click="expand"
        >{{ expandText }}</button></template></template>

    <!-- 展开状态:全文收起 按钮跟在末尾 -->
    <template v-else>{{ text }}<button
        :class="collapseClass"
        class="inline ml-0.5 cursor-pointer bg-transparent border-none p-0 [font-family:inherit] [font-size:inherit] [line-height:inherit]"
        @click="collapse"
      >{{ collapseText }}</button></template>

  </div>
</template>

51b8d8b590283490c023b210001b30ba.jpg

e9437be06c69b910f86a2b153fd08eae.jpg