u-ellipsis.vue 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271
  1. <template>
  2. <view class="u-ellipsis" :style="customStyle">
  3. <view class="u-ellipsis__content">
  4. <text :style="[textStyle]">
  5. {{ expanded ? content : text }}
  6. <text v-if="hasAction" class="u-ellipsis__action" @click="handleToggle"
  7. :style="[{
  8. color: actionColor
  9. }]">
  10. {{ expanded ? collapseText : expandText }}
  11. </text>
  12. </text>
  13. </view>
  14. <text class="measureBox" :class="[measureId]" :style="[textStyle]"> {{ measureContent }}</text>
  15. </view>
  16. </template>
  17. <script>
  18. // #ifdef APP-NVUE
  19. const dom = uni.requireNativePlugin('dom')
  20. // #endif
  21. import props from './props.js';
  22. import mixin from '../../libs/mixin/mixin'
  23. import mpMixin from '../../libs/mixin/mpMixin';
  24. /**
  25. * ellipsis 文本省略
  26. * @description 文本过长时,自动省略多余的文本。支持展开/收起功能。
  27. * @tutorial https://uview.d3u.cn/components/ellipsis.html
  28. * @property {String} content 文本内容
  29. * @property {String} position 省略位置,可选值为 start、end、middle (默认 'end' )
  30. * @property {String | Number} rows 行数 (默认 1 )
  31. * @property {String} expandText 展开文本
  32. * @property {String} collapseText 收起文本
  33. * @property {String} symbol 省略符号 (默认 '...' )
  34. * @property {String} color 文本颜色
  35. * @property {String | Number} fontSize 文本大小 (默认 14 )
  36. * @property {String} actionColor 展开/收起按钮颜色
  37. * @property {Object} customStyle 定义需要用到的外部样式
  38. *
  39. * @event {Function} click 点击文本时触发
  40. * @event {Function} change 展开/收起状态改变时触发
  41. * @example <u-ellipsis content="这是一段很长的文本内容..." :rows="2" expand-text="展开" collapse-text="收起"></u-ellipsis>
  42. */
  43. export default {
  44. name: "u-ellipsis",
  45. mixins: [mpMixin, mixin, props],
  46. data() {
  47. return {
  48. text: '',
  49. expanded: false,
  50. hasAction: false,
  51. measureBoxVisible: false,
  52. measureContent: '',
  53. measureId: uni.$u.guid() // 生成唯一class
  54. }
  55. },
  56. computed: {
  57. textStyle() {
  58. return {
  59. color: this.color,
  60. fontSize: this.$u.addUnit(this.fontSize),
  61. lineHeight: this.$u.addUnit(this.lineHeight)
  62. }
  63. }
  64. },
  65. mounted() {
  66. this.$nextTick(() => {
  67. this.calcEllipsised()
  68. })
  69. },
  70. // #ifdef VUE3
  71. emits: ["click", "change"],
  72. // #endif
  73. methods: {
  74. getMeasureBox() {
  75. return new Promise(resolve => {
  76. // #ifndef APP-NVUE
  77. this.$uGetRect('.' + this.measureId).then(res => {
  78. resolve(res)
  79. })
  80. // #endif
  81. // #ifdef APP-NVUE
  82. const ref = this.$refs['measureBox']
  83. if (ref) {
  84. dom.getComponentRect(ref, (res) => {
  85. resolve({
  86. height: res.size.height,
  87. width: res.size.width
  88. })
  89. })
  90. } else {
  91. resolve({ height: 0, width: 0 })
  92. }
  93. // #endif
  94. })
  95. },
  96. async calcEllipsised() {
  97. if (!this.content || !this.content.length) return
  98. // 初始化测量容器
  99. this.measureBoxVisible = true
  100. try {
  101. // 计算基准行高和最大允许高度
  102. const sampleText = this.content.slice(0, 1)
  103. const { height, width } = await this.measureTextHeight(sampleText)
  104. const maxAllowedHeight = (height || 20) * Number(this.rows)
  105. // 检查原文本是否需要省略
  106. const originalTextHeight = await this.measureTextHeight(this.content)
  107. if (originalTextHeight <= maxAllowedHeight) {
  108. this.text = this.content
  109. this.hasAction = false
  110. return
  111. }
  112. // 执行文本截断处理
  113. this.hasAction = true
  114. this.text = await this.performTextTruncation(maxAllowedHeight)
  115. } catch (error) {
  116. this.text = this.content
  117. this.hasAction = false
  118. } finally {
  119. this.measureBoxVisible = false
  120. }
  121. },
  122. // 测量文本高度
  123. async measureTextHeight(text) {
  124. this.measureContent = text
  125. await this.$nextTick()
  126. return this.getMeasureBox()
  127. },
  128. // 执行文本截断处理
  129. async performTextTruncation(maxHeight) {
  130. if (this.position === 'middle') {
  131. return await this.truncateMiddle(maxHeight, this.symbol, this.expandText)
  132. } else {
  133. return await this.truncateEdge(maxHeight, this.symbol, this.expandText, this.position)
  134. }
  135. },
  136. // 中间截断处理
  137. async truncateMiddle(maxHeight, symbol, actionText) {
  138. const text = this.content
  139. const textLength = text.length
  140. const centerPoint = Math.floor(textLength / 2)
  141. let leftBoundary = [0, centerPoint]
  142. let rightBoundary = [centerPoint, textLength]
  143. while (this.shouldContinueTruncation(leftBoundary, rightBoundary)) {
  144. const leftMidpoint = Math.floor((leftBoundary[0] + leftBoundary[1]) / 2)
  145. const rightMidpoint = Math.ceil((rightBoundary[0] + rightBoundary[1]) / 2)
  146. const testText = text.slice(0, leftMidpoint) + symbol + text.slice(rightMidpoint) + actionText
  147. const { height } = await this.measureTextHeight(testText)
  148. if (height >= maxHeight) {
  149. // 文本过长,缩小范围
  150. leftBoundary = [leftBoundary[0], leftMidpoint]
  151. rightBoundary = [rightMidpoint, rightBoundary[1]]
  152. } else {
  153. // 文本适中,扩大范围
  154. leftBoundary = [leftMidpoint, leftBoundary[1]]
  155. rightBoundary = [rightBoundary[0], rightMidpoint]
  156. }
  157. }
  158. return text.slice(0, leftBoundary[0]) + symbol + text.slice(rightBoundary[1])
  159. },
  160. // 边缘截断处理(开始或结束)
  161. async truncateEdge(maxHeight, symbol, actionText, direction) {
  162. const text = this.content
  163. const isStartTruncation = direction === 'start'
  164. let searchStart = 0
  165. let searchEnd = text.length
  166. let bestPosition = -1
  167. while (searchStart <= searchEnd) {
  168. const midPosition = Math.floor((searchStart + searchEnd) / 2)
  169. const testText = isStartTruncation
  170. ? symbol + text.slice(midPosition) + actionText
  171. : text.slice(0, midPosition) + symbol + actionText
  172. const { height } = await this.measureTextHeight(testText)
  173. if (height <= maxHeight) {
  174. bestPosition = midPosition
  175. if (isStartTruncation) {
  176. searchEnd = midPosition - 1
  177. } else {
  178. searchStart = midPosition + 1
  179. }
  180. } else {
  181. if (isStartTruncation) {
  182. searchStart = midPosition + 1
  183. } else {
  184. searchEnd = midPosition - 1
  185. }
  186. }
  187. }
  188. return isStartTruncation
  189. ? symbol + text.slice(bestPosition)
  190. : text.slice(0, bestPosition) + symbol
  191. },
  192. shouldContinueTruncation(leftBoundary, rightBoundary) {
  193. return (leftBoundary[1] - leftBoundary[0] > 1) || (rightBoundary[1] - rightBoundary[0] > 1)
  194. },
  195. handleToggle(event) {
  196. this.expanded = this.expanded ? false : true;
  197. this.$emit('change', {
  198. expanded: this.expanded,
  199. content: this.expanded ? this.content : this.text
  200. })
  201. }
  202. }
  203. }
  204. </script>
  205. <style lang="scss" scoped>
  206. @import '../../libs/css/components.scss';
  207. .u-ellipsis {
  208. position: relative;
  209. .measureBox {
  210. position: absolute;
  211. top: -9999px;
  212. left: -9999px;
  213. /* #ifndef APP-NVUE */
  214. white-space: normal;
  215. word-wrap: break-word;
  216. visibility: hidden;
  217. width: 100%;
  218. /* #endif */
  219. }
  220. &__content {
  221. display: flex;
  222. flex-direction: row;
  223. align-items: flex-end;
  224. flex-wrap: wrap;
  225. }
  226. &__action {
  227. /* #ifndef APP-NVUE */
  228. flex-shrink: 0;
  229. cursor: pointer;
  230. user-select: none;
  231. /* #endif */
  232. &:hover {
  233. opacity: 0.8;
  234. }
  235. }
  236. }
  237. </style>