<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> 
 |