This commit is contained in:
2025-08-12 15:04:01 +08:00
parent df3f5f5269
commit 6708fb8eeb
544 changed files with 75169 additions and 17 deletions
@@ -0,0 +1,53 @@
export default {
props: {
// 文本内容
content: {
type: String,
default: ()=> uni.$u.props.ellipsis.content
},
// 省略位置:start, end, middle
position: {
type: String,
default: ()=> uni.$u.props.ellipsis.position
},
lineHeight: {
type: [Number, String],
default: ()=> uni.$u.props.ellipsis.lineHeight
},
// 行数
rows: {
type: [Number, String],
default: ()=> uni.$u.props.ellipsis.rows
},
// 展开文本
expandText: {
type: String,
default: ()=> uni.$u.props.ellipsis.expandText
},
// 收起文本
collapseText: {
type: String,
default: ()=> uni.$u.props.ellipsis.collapseText
},
// 省略符号
symbol: {
type: String,
default: ()=> uni.$u.props.ellipsis.symbol
},
// 文本颜色
color: {
type: String,
default: ()=> uni.$u.props.ellipsis.color
},
// 文本大小
fontSize: {
type: [String, Number],
default: ()=> uni.$u.props.ellipsis.fontSize
},
// 展开/收起按钮颜色
actionColor: {
type: String,
default: ()=> uni.$u.props.ellipsis.actionColor
}
}
}
@@ -0,0 +1,271 @@
<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>