tui-index-list.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621
  1. <template>
  2. <view class="tui-index-list">
  3. <scroll-view class="tui-scroll__view" :style="{ height: getHeight }" scroll-y :scroll-top="scrollTop"
  4. @scroll="scroll">
  5. <slot name="header"></slot>
  6. <view class="tui-content__box">
  7. <view class="tui-item__select" v-for="(item, index) in listData" :key="index">
  8. <view v-if="index == listItemCur" class="tui-content__title"
  9. :class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
  10. <view class="tui-title__item"
  11. :style="{ background: background_cur, color: color_cur, fontSize: size, height: height, padding: padding }">
  12. {{ item.letter }}
  13. </view>
  14. </view>
  15. <view v-else-if="index == listItemCur + 1" class="tui-content__title"
  16. :class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
  17. <view class="tui-title__item"
  18. :style="{ background: background_next, color: color_next, fontSize: size, height: height, padding: padding }">
  19. {{ item.letter }}
  20. </view>
  21. </view>
  22. <view v-else class="tui-content__title"
  23. :class="{ 'tui-line__top': topLine, 'tui-line__bottom': bottomLine }">
  24. <view class="tui-title__item"
  25. :style="{ background: background, color: color, fontSize: size, height: height, padding: padding }">
  26. {{ item.letter }}
  27. </view>
  28. </view>
  29. <slot name="item" :entity="item.data" :index="index"></slot>
  30. </view>
  31. </view>
  32. <slot name="footer"></slot>
  33. </scroll-view>
  34. <view class="tui-index__indicator"
  35. :class="[touching && indicatorTop != -1 ? 'tui-indicator__show' : '', treeKeyTran ? 'tui-indicator__tran' : '']"
  36. :style="{ top: indicatorTop + 'px' }">
  37. {{ listData[treeItemCur] && listData[treeItemCur].letter }}
  38. </view>
  39. <view id="tui_index__letter" class="tui-index__letter" @touchstart.stop="touchStart"
  40. @touchmove.stop.prevent="touchMove" @touchend.stop="touchEnd" @touchcancel.stop="touchEnd">
  41. <view class="tui-letter__item" :class="[index === treeItemCur ? 'tui-letter__cur' : '']"
  42. v-for="(item, index) in listData" :key="index" @tap="letterClick(index,item.letter)">
  43. <view class="tui-letter__key"
  44. :style="{ background: index === treeItemCur ? activeKeyBackground : '', color: index === treeItemCur ? activeKeyColor : keyColor }">
  45. {{ item.letter }}
  46. </view>
  47. </view>
  48. </view>
  49. </view>
  50. </template>
  51. <script>
  52. let ColorUtil = {
  53. rgbToHex(r, g, b) {
  54. let hex = ((r << 16) | (g << 8) | b).toString(16);
  55. return '#' + new Array(Math.abs(hex.length - 7)).join('0') + hex;
  56. },
  57. hexToRgb(hex) {
  58. let rgb = [];
  59. if (hex.length === 4) {
  60. let text = hex.substring(1, 4);
  61. hex = '#' + text + text;
  62. }
  63. for (let i = 1; i < 7; i += 2) {
  64. rgb.push(parseInt('0x' + hex.slice(i, i + 2)));
  65. }
  66. return rgb;
  67. },
  68. /**
  69. * 生成渐变过渡色数组 {startColor: 开始颜色值, endColor: 结束颜色值, step: 生成色值数组长度}
  70. */
  71. gradient(startColor, endColor, step) {
  72. // 将hex转换为rgb
  73. let sColor = this.hexToRgb(startColor),
  74. eColor = this.hexToRgb(endColor);
  75. // 计算R\G\B每一步的差值
  76. let rStep = (eColor[0] - sColor[0]) / step,
  77. gStep = (eColor[1] - sColor[1]) / step,
  78. bStep = (eColor[2] - sColor[2]) / step;
  79. let gradientColorArr = [];
  80. for (let i = 0; i < step; i++) {
  81. // 计算每一步的hex值
  82. gradientColorArr.push(this.rgbToHex(parseInt(rStep * i + sColor[0]), parseInt(gStep * i + sColor[1]),
  83. parseInt(bStep * i + sColor[2])));
  84. }
  85. return gradientColorArr;
  86. },
  87. /**
  88. * 生成随机颜色值
  89. */
  90. generateColor() {
  91. let color = '#';
  92. for (let i = 0; i < 6; i++) {
  93. color += ((Math.random() * 16) | 0).toString(16);
  94. }
  95. return color;
  96. }
  97. };
  98. export default {
  99. name: 'tuiIndexList',
  100. emits: ['letterClick'],
  101. props: {
  102. // 数据源
  103. listData: {
  104. type: Array,
  105. default () {
  106. return [];
  107. }
  108. },
  109. // 顶部高度
  110. top: {
  111. type: Number,
  112. default: 0
  113. },
  114. // 底部高度
  115. bottom: {
  116. type: Number,
  117. default: 0
  118. },
  119. //top和bottom单位,可传rpx 或 px
  120. unit: {
  121. type: String,
  122. default: 'px'
  123. },
  124. //sticky letter 是否显示上边线条
  125. topLine: {
  126. type: Boolean,
  127. default: true
  128. },
  129. //sticky letter 是否显示下边线条
  130. bottomLine: {
  131. type: Boolean,
  132. default: true
  133. },
  134. height: {
  135. type: String,
  136. default: '60rpx'
  137. },
  138. color: {
  139. type: String,
  140. default: '#666'
  141. },
  142. activeColor: {
  143. type: String,
  144. default: '#5677fc'
  145. },
  146. size: {
  147. type: String,
  148. default: '26rpx'
  149. },
  150. background: {
  151. type: String,
  152. default: '#ededed'
  153. },
  154. activeBackground: {
  155. type: String,
  156. default: '#FFFFFF'
  157. },
  158. padding: {
  159. type: String,
  160. default: '0 20rpx'
  161. },
  162. keyColor: {
  163. type: String,
  164. default: '#666'
  165. },
  166. activeKeyColor: {
  167. type: String,
  168. default: '#FFFFFF'
  169. },
  170. activeKeyBackground: {
  171. type: String,
  172. default: '#5677fc'
  173. },
  174. //重新初始化[可异步加载时使用,设置大于0的数]
  175. reinit: {
  176. type: Number,
  177. default: 0
  178. }
  179. },
  180. computed: {
  181. getHeight() {
  182. return `calc(100vh - ${this.top + this.bottom + this.unit})`;
  183. },
  184. getChange() {
  185. return `${this.top}-${this.bottom}-${this.reinit}`;
  186. }
  187. },
  188. watch: {
  189. listData(val) {
  190. this.init();
  191. },
  192. getChange(val) {
  193. this.init();
  194. }
  195. },
  196. data() {
  197. return {
  198. remScale: 1, // 缩放比例
  199. realTop: 0, // 计算后顶部高度实际值
  200. realBottom: 0, // 计算后底部高度实际值
  201. treeInfo: {
  202. // 索引树节点信息
  203. treeTop: 0,
  204. treeBottom: 0,
  205. itemHeight: 0,
  206. itemMount: 0
  207. },
  208. indicatorTopList: [], // 指示器节点信息列表
  209. maxScrollTop: 0, // 最大滚动高度
  210. blocks: [], // 节点组信息
  211. /* 渲染数据 */
  212. treeItemCur: -1, // 索引树的聚焦项
  213. listItemCur: -1, // 节点树的聚焦项
  214. touching: false, // 是否在触摸索引树中
  215. scrollTop: 0, // 节点树滚动高度
  216. indicatorTop: -1, // 指示器顶部距离
  217. treeKeyTran: false,
  218. background_cur: '',
  219. color_cur: '',
  220. background_next: '',
  221. color_next: '',
  222. colors: [],
  223. backgroundColors: []
  224. };
  225. },
  226. methods: {
  227. scroll(e) {
  228. if (this.touching) return;
  229. let scrollTop = e.detail.scrollTop;
  230. if (scrollTop > this.maxScrollTop) return;
  231. let blocks = this.blocks,
  232. stickyTitleHeight = this.remScale * 30;
  233. let len = blocks.length - 1;
  234. this.background_cur = this.background;
  235. this.color_cur = this.color;
  236. for (let i = len; i >= 0; i--) {
  237. let block = blocks[i];
  238. // 判断当前滚动值 scrollTop 所在区间, 以得到当前聚焦项
  239. if (scrollTop >= block.itemTop && scrollTop < block.itemBottom) {
  240. // 判断当前滚动值 scrollTop 是否在当前聚焦项底一个 .block__title 高度范围内, 如果是则开启过度色值计算
  241. if (scrollTop > block.itemBottom - stickyTitleHeight) {
  242. let percent = Math.floor(((scrollTop - (block.itemBottom - stickyTitleHeight)) /
  243. stickyTitleHeight) * 100);
  244. this.background_cur = this.backgroundColors[percent];
  245. this.color_cur = this.colors[percent];
  246. this.background_next = this.backgroundColors[100 - percent];
  247. this.color_next = this.colors[100 - percent];
  248. this.treeItemCur = i;
  249. this.listItemCur = i;
  250. } else if (scrollTop <= block.itemBottom - stickyTitleHeight) {
  251. this.background_cur = this.activeBackground;
  252. this.color_cur = this.activeColor;
  253. this.background_next = this.background;
  254. this.color_next = this.color;
  255. this.treeItemCur = i;
  256. this.listItemCur = i;
  257. }
  258. break;
  259. }
  260. }
  261. },
  262. /**
  263. * tree 触摸开始
  264. */
  265. touchStart(e) {
  266. // 获取触摸点信息
  267. let startTouch = e.changedTouches[0];
  268. if (!startTouch) return;
  269. this.touching = true;
  270. let treeItemCur = this.getCurrentTreeItem(startTouch.pageY);
  271. this.setValue(treeItemCur);
  272. },
  273. /**
  274. * tree 触摸移动
  275. */
  276. touchMove(e) {
  277. // 获取触摸点信息
  278. let currentTouch = e.changedTouches[0];
  279. if (!currentTouch) return;
  280. // 滑动结束后迅速开始第二次滑动时候 touching 为 false 造成不显示 indicator 问题
  281. if (!this.touching) {
  282. this.touching = true;
  283. }
  284. let treeItemCur = this.getCurrentTreeItem(currentTouch.pageY);
  285. this.setValue(treeItemCur);
  286. },
  287. /**
  288. * tree 触摸结束
  289. */
  290. touchEnd(e) {
  291. let treeItemCur = this.treeItemCur;
  292. let listItemCur = this.listItemCur;
  293. if (treeItemCur !== listItemCur) {
  294. this.treeItemCur = listItemCur;
  295. this.indicatorTop = this.indicatorTopList[treeItemCur];
  296. }
  297. this.treeKeyTran = true;
  298. setTimeout(() => {
  299. this.touching = false;
  300. this.treeKeyTran = false;
  301. }, 300);
  302. },
  303. letterClick(index, letter) {
  304. // #ifdef H5
  305. this.setValue(index);
  306. this.touchEnd()
  307. // #endif
  308. this.$emit('letterClick', {
  309. index: index,
  310. letter: letter
  311. })
  312. },
  313. /**
  314. * 获取当前触摸的 tree-item
  315. * @param pageY: 当前触摸点pageY
  316. */
  317. getCurrentTreeItem(pageY) {
  318. let {
  319. treeTop,
  320. treeBottom,
  321. itemHeight,
  322. itemMount
  323. } = this.treeInfo;
  324. if (pageY < treeTop) {
  325. return 0;
  326. } else if (pageY >= treeBottom) {
  327. return itemMount - 1;
  328. } else {
  329. return Math.floor((pageY - treeTop) / itemHeight);
  330. }
  331. },
  332. /**
  333. * 触摸之后后设置对应value
  334. */
  335. setValue(treeItemCur) {
  336. if (treeItemCur === this.treeItemCur) return;
  337. let block = this.blocks[treeItemCur];
  338. if (!block) return;
  339. let {
  340. scrollTop,
  341. scrollIndex
  342. } = block,
  343. indicatorTop = this.indicatorTopList[treeItemCur];
  344. this.background_cur = this.activeBackground;
  345. this.color_cur = this.activeColor;
  346. this.background_next = this.background;
  347. this.color_next = this.color;
  348. this.treeItemCur = treeItemCur;
  349. this.scrollTop = scrollTop;
  350. this.listItemCur = scrollIndex;
  351. this.indicatorTop = indicatorTop;
  352. },
  353. /**
  354. * 清除参数
  355. */
  356. clearData() {
  357. this.treeItemCur = 0; // 索引树的聚焦项
  358. this.listItemCur = 0; // 节点树的聚焦项
  359. this.touching = false; // 是否在触摸索引树中
  360. this.scrollTop = 0; // 节点树滚动高度
  361. this.indicatorTop = -1; // 指示器顶部距离
  362. this.treeKeyTran = false;
  363. this.background_cur = this.background;
  364. this.color_cur = this.color;
  365. this.background_next = this.background;
  366. this.color_next = this.color;
  367. },
  368. /**
  369. * 初始化获取 dom 信息
  370. */
  371. initDom() {
  372. let {
  373. windowHeight,
  374. windowWidth
  375. } = uni.getSystemInfoSync();
  376. let remScale = (windowWidth || 375) / 375,
  377. realTop = (this.top * remScale) / 2,
  378. realBottom = (this.bottom * remScale) / 2,
  379. colors = ColorUtil.gradient(this.activeColor, this.color, 100),
  380. backgroundColors = ColorUtil.gradient(this.activeBackground, this.background, 100);
  381. this.remScale = remScale;
  382. this.realTop = realTop;
  383. this.realBottom = realBottom;
  384. this.colors = colors;
  385. this.backgroundColors = backgroundColors;
  386. uni.createSelectorQuery()
  387. .in(this)
  388. .select('#tui_index__letter')
  389. .boundingClientRect(res => {
  390. let treeTop = res.top,
  391. treeBottom = res.top + res.height,
  392. itemHeight = res.height / this.listData.length,
  393. itemMount = this.listData.length;
  394. let indicatorTopList = this.listData.map((item, index) => {
  395. return itemHeight / 2 + index * itemHeight + treeTop - remScale * 25;
  396. });
  397. this.treeInfo = {
  398. treeTop: treeTop,
  399. treeBottom: treeBottom,
  400. itemHeight: itemHeight,
  401. itemMount: itemMount
  402. };
  403. this.indicatorTopList = indicatorTopList;
  404. })
  405. .exec();
  406. uni.createSelectorQuery()
  407. .in(this)
  408. .select('.tui-content__box')
  409. .boundingClientRect(res => {
  410. let maxScrollTop = res.height - (windowHeight - realTop - realBottom);
  411. uni.createSelectorQuery()
  412. .in(this)
  413. .selectAll('.tui-item__select')
  414. .boundingClientRect(res => {
  415. let maxScrollIndex = -1;
  416. let blocks = res.map((item, index) => {
  417. // Math.ceil 向上取整, 防止索引树切换列表时候造成真机固定头部上边线显示过粗问题
  418. let itemTop = Math.ceil(item.top - realTop),
  419. itemBottom = Math.ceil(itemTop + item.height);
  420. if (maxScrollTop >= itemTop && maxScrollTop < itemBottom)
  421. maxScrollIndex = index;
  422. return {
  423. itemTop: itemTop,
  424. itemBottom: itemBottom,
  425. scrollTop: itemTop >= maxScrollTop ? maxScrollTop : itemTop,
  426. scrollIndex: maxScrollIndex === -1 ? index : maxScrollIndex
  427. };
  428. });
  429. this.maxScrollTop = maxScrollTop;
  430. this.blocks = blocks;
  431. })
  432. .exec();
  433. })
  434. .exec();
  435. },
  436. /**
  437. * 初始化
  438. */
  439. init() {
  440. this.clearData();
  441. // 避免获取不到节点信息报错问题
  442. if (this.listData.length === 0) {
  443. return;
  444. }
  445. // 异步加载数据时候, 延迟执行 initDom 方法
  446. setTimeout(() => this.initDom(), 1200);
  447. }
  448. },
  449. mounted() {
  450. this.init();
  451. }
  452. };
  453. </script>
  454. <style scoped>
  455. .tui-index-list {
  456. width: 100vw;
  457. overflow: hidden;
  458. position: relative;
  459. }
  460. .tui-scroll__view {
  461. width: 100vw;
  462. }
  463. .tui-content__box {
  464. position: relative;
  465. width: 100%;
  466. }
  467. .tui-content__title {
  468. position: sticky;
  469. top: 0;
  470. z-index: 10;
  471. font-weight: bold;
  472. }
  473. .tui-content__title .tui-title__item {
  474. width: 100%;
  475. position: relative;
  476. display: flex;
  477. align-items: center;
  478. }
  479. .tui-line__top::before {
  480. content: ' ';
  481. position: absolute;
  482. top: 0;
  483. right: 0;
  484. left: 0;
  485. border-top: 1px solid #ebedf0;
  486. -webkit-transform: scaleY(0.5) translateZ(0);
  487. transform: scaleY(0.5) translateZ(0);
  488. transform-origin: 0 0;
  489. z-index: 2;
  490. pointer-events: none;
  491. }
  492. .tui-line__bottom::after {
  493. content: ' ';
  494. position: absolute;
  495. border-bottom: 1px solid #ebedf0;
  496. -webkit-transform: scaleY(0.5) translateZ(0);
  497. transform: scaleY(0.5) translateZ(0);
  498. transform-origin: 0 100%;
  499. bottom: 0;
  500. right: 0;
  501. left: 0;
  502. }
  503. .tui-index__indicator {
  504. position: fixed;
  505. right: 100rpx;
  506. width: 100rpx;
  507. height: 100rpx;
  508. line-height: 100rpx;
  509. border-radius: 10rpx;
  510. text-align: center;
  511. color: #ffffff;
  512. font-size: 60rpx;
  513. font-weight: bold;
  514. display: none;
  515. z-index: 10;
  516. }
  517. .tui-index__indicator:after {
  518. content: '';
  519. position: absolute;
  520. top: 0;
  521. right: 0;
  522. width: 100%;
  523. height: 100%;
  524. z-index: -1;
  525. border-radius: 100% 0% 100% 100%;
  526. background: #c9c9c9;
  527. transform: rotate(45deg);
  528. }
  529. .tui-indicator__show {
  530. display: block;
  531. z-index: 10;
  532. }
  533. .tui-indicator__tran {
  534. display: block;
  535. opacity: 0;
  536. transition: opacity 0.3s linear;
  537. }
  538. .tui-index__letter {
  539. position: fixed;
  540. right: 0;
  541. top: 50%;
  542. transform: translateY(-50%);
  543. text-align: center;
  544. z-index: 10;
  545. }
  546. .tui-letter__item {
  547. padding: 0 8rpx;
  548. font-weight: bold;
  549. }
  550. .tui-letter__key {
  551. width: 40rpx;
  552. height: 40rpx;
  553. border-radius: 50%;
  554. font-size: 26rpx;
  555. transform: scale(0.8);
  556. transform-origin: center center;
  557. display: flex;
  558. align-items: center;
  559. justify-content: center;
  560. }
  561. .tui-list__item {
  562. width: 100%;
  563. display: flex;
  564. align-items: center;
  565. }
  566. .tui-list__item .tui-avatar {
  567. width: 68rpx;
  568. height: 68rpx;
  569. border-radius: 8rpx;
  570. flex-shrink: 0;
  571. background-color: #ccc;
  572. }
  573. .tui-list__item view {
  574. width: 90%;
  575. font-size: 32rpx;
  576. padding-left: 20rpx;
  577. padding-right: 40rpx;
  578. box-sizing: border-box;
  579. white-space: nowrap;
  580. overflow: hidden;
  581. text-overflow: ellipsis;
  582. }
  583. </style>