BaseTextMore.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414
  1. <template>
  2. <view class="t-wrap">
  3. <!-- 虚拟view用于计算,计算完成则消失 -->
  4. <view class="t-txt-hide" :id="hid" v-if="!isCompute" :style="[computeStyle(0),{'text-align':oneRowTextAlign}]">
  5. <text space="nbsp">{{testContent?testContent:content}}{{showSymbol?'...':''}}</text><text
  6. v-if="expandText && collapseText && showSymbol" class="t-button">
  7. {{expandText}}
  8. </text><text></text>
  9. </view>
  10. <!-- 真实显示的内容 -->
  11. <view class="t-ellipsis" :id="id"
  12. :style="[!isCompute?computeStyle(1):computeStyle(2),{'text-align':oneRowTextAlign}]" @click="contentClick">
  13. <text space="nbsp">{{(!isCompute || expand)?content:(actualContent+(showSymbol?'...':''))}}</text><text
  14. v-if="expandText && collapseText && showSymbol" class="t-button" @click.stop="changeCollapse"
  15. :style="{'color':actionFontColor}">{{!expand?expandText:collapseText}}</text><text></text>
  16. </view>
  17. <!-- 这里加入了自定义的动态骨架屏友好反馈 -->
  18. <view v-if="!isCompute && rows>0" class="t-skeleton">
  19. <view class="skeletons" v-for="(item,index) in rows" :key="index">
  20. <view class="empty"></view>
  21. </view>
  22. </view>
  23. </view>
  24. </template>
  25. <script>
  26. //为了兼容部分老机型,增加扰动因子参数
  27. const factor = 5;
  28. export default {
  29. name: "KevyEllipsis",
  30. props: {
  31. /**
  32. * 文本唯一标识符,非必填
  33. */
  34. textId: {
  35. type: [String, Number],
  36. default: ''
  37. },
  38. /**
  39. * 文本内容,默认''
  40. */
  41. content: {
  42. type: String,
  43. default: ''
  44. },
  45. /**
  46. * 字体大小,单位rpx,默认28
  47. */
  48. fontSize: {
  49. type: [String, Number],
  50. default: 28
  51. },
  52. /**
  53. * 字体颜色,默认#666666
  54. */
  55. fontColor: {
  56. color: String,
  57. default: '#666666'
  58. },
  59. /**
  60. * 收起操作的文案,默认''
  61. */
  62. collapseText: {
  63. type: String,
  64. default: ''
  65. },
  66. /**
  67. * 展开操作的文案,默认''
  68. */
  69. expandText: {
  70. type: String,
  71. default: ''
  72. },
  73. /**
  74. * 收起、展开操作文字颜色,默认'#007aff'
  75. */
  76. actionFontColor: {
  77. color: String,
  78. default: '#007aff'
  79. },
  80. /**
  81. * 展示行数,默认1
  82. */
  83. rows: {
  84. type: Number,
  85. default: 1
  86. },
  87. /**
  88. * 只有一行时文本对齐方式,支持left、right、justify
  89. */
  90. oneRowTextAlign: {
  91. type: String,
  92. default: "justify"
  93. },
  94. },
  95. data() {
  96. return {
  97. //是否展开
  98. expand: false,
  99. //是否已计算
  100. isCompute: false,
  101. //内容高度
  102. h: undefined,
  103. //内容宽度
  104. w: undefined,
  105. //实际显示内容
  106. actualContent: '',
  107. //高度探测内容
  108. testContent: undefined,
  109. //是否显示省略号
  110. showSymbol: false,
  111. //hid和id,唯一标识符
  112. hid: 'hid' + Math.random().toString(36).substr(2),
  113. id: 'id' + Math.random().toString(36).substr(2),
  114. };
  115. },
  116. mounted() {
  117. this.$nextTick(() => {
  118. this.initEllipsis();
  119. })
  120. },
  121. computed: {
  122. //动态计算组件样式
  123. computeStyle() {
  124. return b => {
  125. let lines = this.rows > 0 ? this.rows : 1;
  126. let obj = {};
  127. if (b == 1) {
  128. obj = {
  129. '-webkit-line-clamp': lines,
  130. 'display': '-webkit-box',
  131. 'text-overflow': 'ellipsis',
  132. 'overflow': 'hidden',
  133. '-webkit-box-orient': 'vertical'
  134. };
  135. } else if (b == 2) {
  136. obj = {
  137. 'position': 'relative',
  138. 'left': '0rpx',
  139. ...obj
  140. };
  141. }
  142. return {
  143. 'font-size': this.fontSize + 'rpx',
  144. 'color': this.fontColor,
  145. ...obj
  146. }
  147. }
  148. }
  149. },
  150. watch: {
  151. content(newVal, oldVal) {
  152. this.expand=false;
  153. this.isCompute=false;
  154. this.h=undefined;
  155. this.w=undefined;
  156. this.actualContent='';
  157. this.showSymbol=false;
  158. this.initEllipsis();
  159. }
  160. },
  161. methods: {
  162. //初始化
  163. initEllipsis() {
  164. if (this.content?.length > 0) {
  165. this.$nextTick(() => {
  166. this.init(this, () => {
  167. this.compute(this);
  168. })
  169. })
  170. }
  171. },
  172. //收起展开状态切换
  173. changeCollapse() {
  174. this.expand = !this.expand;
  175. },
  176. //文本点击事件
  177. contentClick() {
  178. this.$emit('contentClick', this.textId);
  179. },
  180. //组件参数初始化
  181. init($this, callback, isali) {
  182. if (isali) {
  183. uni.createSelectorQuery().in().select('#' + $this.id).boundingClientRect(d => {
  184. $this.h = Number(d.height.toFixed(1));
  185. $this.w = Number(d.width.toFixed(1));
  186. if (callback) {
  187. callback()
  188. }
  189. }).exec();
  190. } else {
  191. uni.createSelectorQuery().in($this).select('#' + $this.id).boundingClientRect(d => {
  192. $this.h = Number(d.height.toFixed(1));
  193. $this.w = Number(d.width.toFixed(1));
  194. if (callback) {
  195. callback()
  196. }
  197. }).exec();
  198. }
  199. },
  200. //动态计算组件内容
  201. computeContent($this, isali, dr) {
  202. $this.$nextTick(() => {
  203. $this.getH($this, isali, (ch) => {
  204. if (ch - factor > $this.h) {
  205. if (dr === -1) {
  206. $this.testContent = $this.content.substring(0, $this.testContent.length -
  207. 1);
  208. $this.computeContent($this, isali, dr);
  209. } else {
  210. $this.actualContent = $this.content.substring(0, $this.testContent.length -
  211. 1);
  212. $this.isCompute = true;
  213. }
  214. } else {
  215. if (dr === -1) {
  216. $this.actualContent = $this.testContent;
  217. $this.isCompute = true;
  218. } else {
  219. $this.testContent = $this.content.substring(0, $this.testContent.length +
  220. 1);
  221. $this.computeContent($this, isali, dr);
  222. }
  223. }
  224. })
  225. });
  226. },
  227. //计算工具方法
  228. compute($this, isali) {
  229. let {
  230. rows,
  231. fontSize,
  232. content,
  233. h,
  234. w
  235. } = $this;
  236. $this.testContent = content;
  237. $this.$nextTick(() => {
  238. $this.getH($this, isali, (ch) => {
  239. if (ch - factor > h) {
  240. let lh = h / rows;
  241. let fn = Math.floor(w / $this.rpx2px(fontSize));
  242. let sfn = fn * rows;
  243. let i = $this.fontNum(content, sfn * 2 - ($this.expandText ? $this.fontNum(
  244. $this.expandText) :
  245. 0) - 3);
  246. $this.showSymbol = true;
  247. $this.testContent = content.substring(0, i);
  248. $this.$nextTick(() => {
  249. $this.getH($this, isali, (ch1) => {
  250. if (ch1 - factor > h) {
  251. $this.testContent = content.substring(0, $this
  252. .testContent.length - 1);
  253. $this.computeContent($this, isali, -1);
  254. } else {
  255. $this.testContent = content.substring(0, $this
  256. .testContent.length + 1);
  257. $this.computeContent($this, isali, 1);
  258. }
  259. });
  260. });
  261. } else {
  262. $this.isCompute = true;
  263. $this.actualContent = content;
  264. }
  265. })
  266. });
  267. },
  268. //动态计算字符数
  269. fontNum(val, limit) {
  270. let c = 0;
  271. for (let i = 0; i < val.length; i++) {
  272. let a = val.charAt(i);
  273. if (a.match(/[^\x00-\xff]/ig) != null) {
  274. if (limit) {
  275. if (c + 2 > limit) {
  276. return i;
  277. } else {
  278. c += 2;
  279. }
  280. } else {
  281. c += 2;
  282. }
  283. } else {
  284. if (limit) {
  285. if (c + 1 > limit) {
  286. return i;
  287. } else {
  288. c += 1;
  289. }
  290. } else {
  291. c += 1;
  292. }
  293. }
  294. }
  295. if (!limit) {
  296. return c;
  297. }
  298. },
  299. rpx2px(rpx) {
  300. return uni.getSystemInfoSync().windowWidth * Number(rpx) / 750;
  301. },
  302. getH($, isali, callback) {
  303. if (isali) {
  304. uni.createSelectorQuery().in().select('#' + $.hid).fields({
  305. size: true
  306. }, d => {
  307. if(d && d.height){
  308. callback(Number(d.height.toFixed(1)));
  309. }
  310. }).exec();
  311. } else {
  312. uni.createSelectorQuery().in($).select('#' + $.hid).fields({
  313. size: true
  314. }, d => {
  315. if(d && d.height){
  316. callback(Number(d.height.toFixed(1)));
  317. }
  318. }).exec();
  319. }
  320. }
  321. }
  322. }
  323. </script>
  324. <style lang="scss" scoped>
  325. .t-wrap {
  326. width: 100%;
  327. box-sizing: border-box;
  328. position: relative;
  329. }
  330. .t-txt-hide {
  331. box-sizing: border-box;
  332. word-break: break-all;
  333. position: absolute;
  334. top: 999999px;
  335. left: 999999px;
  336. z-index: -1000;
  337. top: 0rpx;
  338. width: 100%;
  339. margin: 0rpx;
  340. text-align: justify;
  341. white-space: pre-line;
  342. line-height: 1.5 !important;
  343. .t-button {
  344. float: right;
  345. clear: both;
  346. }
  347. }
  348. .t-ellipsis {
  349. text-align: justify;
  350. box-sizing: border-box;
  351. width: 100%;
  352. word-break: break-all;
  353. position: relative;
  354. left: 99999px;
  355. white-space: pre-line;
  356. line-height: 1.5 !important;
  357. .t-button {
  358. float: right;
  359. clear: both;
  360. font-weight: 500;
  361. }
  362. }
  363. .t-skeleton {
  364. width: 100%;
  365. height: 100%;
  366. box-sizing: border-box;
  367. position: absolute;
  368. top: 0rpx;
  369. left: 0rpx;
  370. }
  371. .skeletons:first-child {
  372. margin-top: 0rpx !important;
  373. }
  374. .skeletons {
  375. position: relative;
  376. display: block;
  377. overflow: hidden;
  378. width: 100%;
  379. height: 32rpx;
  380. margin-top: 12rpx;
  381. background-color: rgba(0, 0, 0, 0.06);
  382. box-sizing: border-box;
  383. }
  384. .skeletons .empty {
  385. display: block;
  386. position: absolute;
  387. width: 100%;
  388. height: 100%;
  389. -webkit-transform: translateX(-100%);
  390. transform: translateX(-100%);
  391. background: linear-gradient(90deg, transparent, rgba(216, 216, 216, 0.753), transparent);
  392. -webkit-animation: loading .8s infinite;
  393. animation: loading .8s infinite;
  394. }
  395. @keyframes loading {
  396. 100% {
  397. -webkit-transform: translateX(100%);
  398. transform: translateX(100%);
  399. }
  400. }
  401. </style>