| <template> | 
|   <!-- 支付宝小程序使用_tGetRect()获取组件的根元素尺寸,所以在外面套一个"壳" --> | 
|   <view> | 
|     <view class="tn-index-list-class tn-index-list"> | 
|       <slot></slot> | 
|        | 
|       <!-- 侧边栏 --> | 
|       <view | 
|         v-if="showSidebar" | 
|         class="tn-index-list__sidebar" | 
|         @touchstart.stop.prevent="onTouchMove" | 
|         @touchmove.stop.prevent="onTouchMove" | 
|         @touchend.stop.prevent="onTouchStop" | 
|         @touchcancel.stop.prevent="onTouchStop" | 
|       > | 
|         <view | 
|           v-for="(item, index) in indexList" | 
|           :key="index" | 
|           class="tn-index-list__sidebar__item" | 
|           :style="{ | 
|             zIndex: zIndex + 1, | 
|             color: activeAnchorIndex === index ? activeColor : '' | 
|           }" | 
|         > | 
|           {{ item }} | 
|         </view> | 
|       </view> | 
|        | 
|       <!-- 选中弹出框 --> | 
|       <view | 
|         v-if="touchMove && indexList[touchMoveIndex]" | 
|         class="tn-index-list__alert" | 
|         :style="{ | 
|           zIndex: selectAlertZIndex | 
|         }" | 
|       > | 
|         <text>{{ indexList[touchMoveIndex] }}</text> | 
|       </view> | 
|     </view> | 
|   </view> | 
| </template> | 
|   | 
| <script> | 
|   // 生成 A-Z的字母列表 | 
|   let indexList = function() { | 
|     let indexList = [] | 
|     let charCodeOfA = 'A'.charCodeAt(0) | 
|     for (var i = 0; i < 26; i++) { | 
|       indexList.push(String.fromCharCode(charCodeOfA + i)) | 
|     } | 
|     return indexList | 
|   } | 
|    | 
|   export default { | 
|     name: 'tn-index-list', | 
|     props: { | 
|       // 索引列表 | 
|       indexList: { | 
|         type: Array, | 
|         default() { | 
|           return indexList() | 
|         } | 
|       }, | 
|       // 是否自动吸顶 | 
|       sticky: { | 
|         type: Boolean, | 
|         default: true | 
|       }, | 
|       // 自动吸顶时距离顶部的距离,单位px | 
|       stickyTop: { | 
|         type: Number, | 
|         default: 0 | 
|       }, | 
|       // 自定义顶栏的高度,单位px | 
|       customBarHeight: { | 
|         type: Number, | 
|         default: 0 | 
|       }, | 
|       // 当前滚动的高度 | 
|       // 由于自定义组件无法获取滚动高度,所以依赖传入 | 
|       scrollTop: { | 
|         type: Number, | 
|         default: 0 | 
|       }, | 
|       // 选中索引时的颜色 | 
|       activeColor: { | 
|         type: String, | 
|         default: '#01BEFF' | 
|       }, | 
|       // 吸顶时的z-index | 
|       zIndex: { | 
|         type: Number, | 
|         default: 0 | 
|       } | 
|     }, | 
|     computed: { | 
|       // 选中索引列表弹出提示框的z-index | 
|       selectAlertZIndex() { | 
|         return this.$t.zIndex.toast | 
|       }, | 
|       // 吸顶的偏移高度 | 
|       stickyOffsetTop() { | 
|         // #ifdef H5 | 
|         return this.stickyTop !== '' ? this.stickyTop : 44 | 
|         // #endif | 
|         // #ifndef H5 | 
|         return this.stickyTop !== '' ? this.stickyTop : 0 | 
|         // #endif | 
|       } | 
|     }, | 
|     data() { | 
|       return { | 
|         // 当前激活的列表锚点的序号 | 
|         activeAnchorIndex: 0, | 
|         // 显示侧边索引栏 | 
|         showSidebar: true, | 
|         // 标记是否开始触摸移动 | 
|         touchMove: false, | 
|         // 当前触摸移动到对应索引的序号 | 
|         touchMoveIndex: 0, | 
|         // 滚动到对应锚点的序号 | 
|         scrollToAnchorIndex: 0, | 
|         // 侧边栏的信息 | 
|         sidebar: { | 
|           height: 0, | 
|           top: 0 | 
|         }, | 
|         // 内容区域高度 | 
|         height: 0, | 
|         // 内容区域top | 
|         top: 0 | 
|       } | 
|     }, | 
|     watch: { | 
|       scrollTop() { | 
|         this.updateData() | 
|       } | 
|     }, | 
|     created() { | 
|       // 只能在created生命周期定义childrens,如果在data定义,会因为循环引用而报错 | 
|       this.childrens = [] | 
|     }, | 
|     methods: { | 
|       // 更新数据 | 
|       updateData() { | 
|         this.timer && clearTimeout(this.timer) | 
|         this.timer = setTimeout(() => { | 
|           this.showSidebar = !!this.childrens.length | 
|           this.getRect().then(() => { | 
|             this.onScroll() | 
|           }) | 
|         }, 0) | 
|       }, | 
|       // 获取对应的信息 | 
|       getRect() { | 
|         return Promise.all([ | 
|           this.getAnchorRect(), | 
|           this.getListRect(), | 
|           this.getSidebarRect() | 
|         ]) | 
|       }, | 
|       // 获取列表内容子元素信息 | 
|       getAnchorRect() { | 
|         return Promise.all(this.childrens.map((child, index) => { | 
|           child._tGetRect('.tn-index-anchor__wrap').then((rect) => { | 
|             Object.assign(child, { | 
|               height: rect.height, | 
|               top: rect.top - this.customBarHeight | 
|             }) | 
|           }) | 
|         })) | 
|       }, | 
|       // 获取列表信息 | 
|       getListRect() { | 
|         return this._tGetRect('.tn-index-list').then(rect => { | 
|           Object.assign(this, { | 
|             height: rect.height, | 
|             top: rect.top + this.scrollTop | 
|           }) | 
|         }) | 
|       }, | 
|       // 获取侧边滚动栏信息 | 
|       getSidebarRect() { | 
|         return this._tGetRect('.tn-index-list__sidebar').then(rect => { | 
|           this.sidebar = { | 
|             height: rect.height, | 
|             top: rect.top | 
|           } | 
|         }) | 
|       }, | 
|       // 滚动事件 | 
|       onScroll() { | 
|         const { | 
|           childrens = [] | 
|         } = this | 
|         if (!childrens.length) { | 
|           return | 
|         } | 
|         const { | 
|           sticky, | 
|           stickyOffsetTop, | 
|           zIndex, | 
|           scrollTop, | 
|           activeColor | 
|         } = this | 
|         const active = this.getActiveAnchorIndex() | 
|         this.activeAnchorIndex = active | 
|         if (sticky) { | 
|           let isActiveAnchorSticky = false | 
|           if (active !== -1) { | 
|             isActiveAnchorSticky = childrens[active].top <= 0 | 
|           } | 
|           childrens.forEach((item, index) => { | 
|             if (index === active) { | 
|               let wrapperStyle = '' | 
|               let anchorStyle = { | 
|                 color: `${activeColor}` | 
|               } | 
|               if (isActiveAnchorSticky) { | 
|                 wrapperStyle = { | 
|                   height: `${childrens[index].height}px` | 
|                 } | 
|                 anchorStyle = { | 
|                   position: 'fixed', | 
|                   top: `${stickyOffsetTop}px`, | 
|                   zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`, | 
|                   color: `${activeColor}` | 
|                 } | 
|               } | 
|               item.active = true | 
|               item.wrapperStyle = wrapperStyle | 
|               item.anchorStyle = anchorStyle | 
|             } else if (index === active - 1) { | 
|               const currentAnchor = childrens[index] | 
|               const currentOffsetTop = currentAnchor.top | 
|               const targetOffsetTop = index === childrens.length - 1 ? this.top : childrens[index + 1].top | 
|               const parentOffsetHeight = targetOffsetTop - currentOffsetTop | 
|               const translateY = parentOffsetHeight - currentAnchor.height | 
|               const anchorStyle = { | 
|                 position: 'relative', | 
|                 transform: `translate3d(0, ${translateY}px, 0)`, | 
|                 zIndex: `${zIndex ? zIndex : this.$t.zIndex.indexListSticky}`, | 
|                 color: `${activeColor}` | 
|               } | 
|               item.active = false | 
|               item.anchorStyle = anchorStyle | 
|             } else { | 
|               item.active = false | 
|               item.wrapperStyle = '' | 
|               item.anchorStyle = '' | 
|             } | 
|           }) | 
|         } | 
|       }, | 
|       // 触摸移动 | 
|       onTouchMove(event) { | 
|         this.touchMove = true | 
|         const sidebarLength = this.childrens.length | 
|         const touch = event.touches[0] | 
|         const itemHeight = this.sidebar.height / sidebarLength | 
|         let clientY = touch.clientY | 
|         let index = Math.floor((clientY - this.sidebar.top) / itemHeight) | 
|         if (index < 0) { | 
|           index = 0 | 
|         } else if (index > sidebarLength - 1) { | 
|           index = sidebarLength - 1 | 
|         } | 
|         this.touchMoveIndex = index | 
|         this.scrollToAnchor(index) | 
|       }, | 
|       // 触摸停止 | 
|       onTouchStop() { | 
|         this.touchMove = false | 
|         this.scrollToAnchorIndex = null | 
|       }, | 
|       // 获取当前的锚点序号 | 
|       getActiveAnchorIndex() { | 
|         const { | 
|           childrens, | 
|           sticky | 
|         } = this | 
|         for (let i = this.childrens.length - 1; i >= 0; i--) { | 
|           const preAnchorHeight = i > 0 ? childrens[i - 1].height : 0 | 
|           const reachTop = sticky ? preAnchorHeight : 0 | 
|           if (reachTop >= childrens[i].top) { | 
|             return i | 
|           } | 
|         } | 
|         return -1 | 
|       }, | 
|       // 滚动到对应的锚点 | 
|       scrollToAnchor(index) { | 
|         if (this.scrollToAnchorIndex === index) { | 
|           return | 
|         } | 
|         this.scrollToAnchorIndex = index | 
|         const anchor = this.childrens.find(item => item.index === this.indexList[index]) | 
|         if (anchor) { | 
|           const scrollTop = anchor.top + this.scrollTop | 
|           this.$emit('select', { | 
|             index: anchor.index, | 
|             scrollTop: scrollTop | 
|           }) | 
|           uni.pageScrollTo({ | 
|             duration:0, | 
|             scrollTop: scrollTop | 
|           }) | 
|         } | 
|       } | 
|     } | 
|   } | 
| </script> | 
|   | 
| <style lang="scss" scoped> | 
|    | 
|   .tn-index-list { | 
|     position: relative; | 
|      | 
|     &__sidebar { | 
|       display: flex; | 
|       flex-direction: column; | 
|       position: fixed; | 
|       top: 50%; | 
|       right: 0; | 
|       text-align: center; | 
|       transform: translateY(-50%); | 
|       user-select: none; | 
|       z-index: 99; | 
|        | 
|       &__item { | 
|         font-weight: 500; | 
|         padding: 8rpx 18rpx; | 
|         font-size: 22rpx; | 
|         line-height: 1; | 
|       } | 
|     } | 
|      | 
|     &__alert { | 
|       display: flex; | 
|       flex-direction: row; | 
|       position: fixed; | 
|       width: 120rpx; | 
|       height: 120rpx; | 
|       top: 50%; | 
|       right: 90rpx; | 
|       align-items: center; | 
|       justify-content: center; | 
|       margin-top: -60rpx; | 
|       border-radius: 24rpx; | 
|       font-size: 50rpx; | 
|       color: #FFFFFF; | 
|       background-color: $tn-font-sub-color; | 
|       padding: 0; | 
|       z-index: 9999999; | 
|        | 
|       text { | 
|         line-height: 50rpx; | 
|       } | 
|     } | 
|   } | 
| </style> |