123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271 |
- <template>
- <view class="u-ellipsis" :style="customStyle">
- <view class="u-ellipsis__content">
- <text :style="[textStyle]">
- {{ expanded ? content : text }}
- <text v-if="hasAction" class="u-ellipsis__action" @click="handleToggle"
- :style="[{
- color: actionColor
- }]">
- {{ expanded ? collapseText : expandText }}
- </text>
- </text>
- </view>
- <text class="measureBox" :class="[measureId]" :style="[textStyle]"> {{ measureContent }}</text>
- </view>
- </template>
- <script>
- // #ifdef APP-NVUE
- const dom = uni.requireNativePlugin('dom')
- // #endif
- import props from './props.js';
- import mixin from '../../libs/mixin/mixin'
- import mpMixin from '../../libs/mixin/mpMixin';
- /**
- * ellipsis 文本省略
- * @description 文本过长时,自动省略多余的文本。支持展开/收起功能。
- * @tutorial https://uview.d3u.cn/components/ellipsis.html
- * @property {String} content 文本内容
- * @property {String} position 省略位置,可选值为 start、end、middle (默认 'end' )
- * @property {String | Number} rows 行数 (默认 1 )
- * @property {String} expandText 展开文本
- * @property {String} collapseText 收起文本
- * @property {String} symbol 省略符号 (默认 '...' )
- * @property {String} color 文本颜色
- * @property {String | Number} fontSize 文本大小 (默认 14 )
- * @property {String} actionColor 展开/收起按钮颜色
- * @property {Object} customStyle 定义需要用到的外部样式
- *
- * @event {Function} click 点击文本时触发
- * @event {Function} change 展开/收起状态改变时触发
- * @example <u-ellipsis content="这是一段很长的文本内容..." :rows="2" expand-text="展开" collapse-text="收起"></u-ellipsis>
- */
- export default {
- name: "u-ellipsis",
- mixins: [mpMixin, mixin, props],
- data() {
- return {
- text: '',
- expanded: false,
- hasAction: false,
- measureBoxVisible: false,
- measureContent: '',
- measureId: uni.$u.guid() // 生成唯一class
- }
- },
- computed: {
- textStyle() {
- return {
- color: this.color,
- fontSize: this.$u.addUnit(this.fontSize),
- lineHeight: this.$u.addUnit(this.lineHeight)
- }
- }
- },
- mounted() {
- this.$nextTick(() => {
- this.calcEllipsised()
- })
- },
- // #ifdef VUE3
- emits: ["click", "change"],
- // #endif
- methods: {
- getMeasureBox() {
- return new Promise(resolve => {
- // #ifndef APP-NVUE
- this.$uGetRect('.' + this.measureId).then(res => {
- resolve(res)
- })
- // #endif
- // #ifdef APP-NVUE
- const ref = this.$refs['measureBox']
- if (ref) {
- dom.getComponentRect(ref, (res) => {
- resolve({
- height: res.size.height,
- width: res.size.width
- })
- })
- } else {
- resolve({ height: 0, width: 0 })
- }
- // #endif
- })
- },
- async calcEllipsised() {
- if (!this.content || !this.content.length) return
-
- // 初始化测量容器
- this.measureBoxVisible = true
-
- try {
- // 计算基准行高和最大允许高度
- const sampleText = this.content.slice(0, 1)
- const { height, width } = await this.measureTextHeight(sampleText)
- const maxAllowedHeight = (height || 20) * Number(this.rows)
-
- // 检查原文本是否需要省略
- const originalTextHeight = await this.measureTextHeight(this.content)
-
- if (originalTextHeight <= maxAllowedHeight) {
- this.text = this.content
- this.hasAction = false
- return
- }
-
- // 执行文本截断处理
- this.hasAction = true
- this.text = await this.performTextTruncation(maxAllowedHeight)
-
- } catch (error) {
- this.text = this.content
- this.hasAction = false
- } finally {
- this.measureBoxVisible = false
- }
- },
- // 测量文本高度
- async measureTextHeight(text) {
- this.measureContent = text
- await this.$nextTick()
- return this.getMeasureBox()
- },
- // 执行文本截断处理
- async performTextTruncation(maxHeight) {
- if (this.position === 'middle') {
- return await this.truncateMiddle(maxHeight, this.symbol, this.expandText)
- } else {
- return await this.truncateEdge(maxHeight, this.symbol, this.expandText, this.position)
- }
- },
- // 中间截断处理
- async truncateMiddle(maxHeight, symbol, actionText) {
- const text = this.content
- const textLength = text.length
- const centerPoint = Math.floor(textLength / 2)
-
- let leftBoundary = [0, centerPoint]
- let rightBoundary = [centerPoint, textLength]
-
- while (this.shouldContinueTruncation(leftBoundary, rightBoundary)) {
- const leftMidpoint = Math.floor((leftBoundary[0] + leftBoundary[1]) / 2)
- const rightMidpoint = Math.ceil((rightBoundary[0] + rightBoundary[1]) / 2)
-
- const testText = text.slice(0, leftMidpoint) + symbol + text.slice(rightMidpoint) + actionText
- const { height } = await this.measureTextHeight(testText)
-
- if (height >= maxHeight) {
- // 文本过长,缩小范围
- leftBoundary = [leftBoundary[0], leftMidpoint]
- rightBoundary = [rightMidpoint, rightBoundary[1]]
- } else {
- // 文本适中,扩大范围
- leftBoundary = [leftMidpoint, leftBoundary[1]]
- rightBoundary = [rightBoundary[0], rightMidpoint]
- }
- }
-
- return text.slice(0, leftBoundary[0]) + symbol + text.slice(rightBoundary[1])
- },
- // 边缘截断处理(开始或结束)
- async truncateEdge(maxHeight, symbol, actionText, direction) {
- const text = this.content
- const isStartTruncation = direction === 'start'
-
- let searchStart = 0
- let searchEnd = text.length
- let bestPosition = -1
-
- while (searchStart <= searchEnd) {
- const midPosition = Math.floor((searchStart + searchEnd) / 2)
-
- const testText = isStartTruncation
- ? symbol + text.slice(midPosition) + actionText
- : text.slice(0, midPosition) + symbol + actionText
-
- const { height } = await this.measureTextHeight(testText)
-
- if (height <= maxHeight) {
- bestPosition = midPosition
- if (isStartTruncation) {
- searchEnd = midPosition - 1
- } else {
- searchStart = midPosition + 1
- }
- } else {
- if (isStartTruncation) {
- searchStart = midPosition + 1
- } else {
- searchEnd = midPosition - 1
- }
- }
- }
-
- return isStartTruncation
- ? symbol + text.slice(bestPosition)
- : text.slice(0, bestPosition) + symbol
- },
- shouldContinueTruncation(leftBoundary, rightBoundary) {
- return (leftBoundary[1] - leftBoundary[0] > 1) || (rightBoundary[1] - rightBoundary[0] > 1)
- },
- handleToggle(event) {
- this.expanded = this.expanded ? false : true;
- this.$emit('change', {
- expanded: this.expanded,
- content: this.expanded ? this.content : this.text
- })
- }
- }
- }
- </script>
- <style lang="scss" scoped>
- @import '../../libs/css/components.scss';
- .u-ellipsis {
- position: relative;
- .measureBox {
- position: absolute;
- top: -9999px;
- left: -9999px;
-
- /* #ifndef APP-NVUE */
- white-space: normal;
- word-wrap: break-word;
- visibility: hidden;
- width: 100%;
- /* #endif */
- }
- &__content {
- display: flex;
- flex-direction: row;
- align-items: flex-end;
- flex-wrap: wrap;
- }
- &__action {
- /* #ifndef APP-NVUE */
- flex-shrink: 0;
- cursor: pointer;
- user-select: none;
- /* #endif */
- &:hover {
- opacity: 0.8;
- }
- }
- }
- </style>
|