基于 VUE(Element UI) 的 PC 端 自定义索引栏

最近接到一个需求如下图,找类似的组件没找到,只能自己实现了。查阅资料并借鉴了 vant 组件库的 indexBar 组件实现思想最终实现了需求,功能基本可以满足,但肯定存在能优化的地方,仅供参考。

注意:本文滚动容器用的是Element UI 的 非官方 <el-scrollbar> 组件,涉及到一些与此相关的属性。

 

 1. 处理源数据,对数据按照首字母顺序进行分类。

源数据格式:

[
        {
          name: "哈哈哈"
        },
        {
          name: "嘻嘻嘻"
        },
        {
          name: "嘿嘿嘿"
        },
        {
          name: "哟哟哟"
        },
        {
          name: "aaaa"
        }
]

我们需要的数据格式:

  {
        A:[ 
            {  name: "aaa" }
        ],
        H:[
            {  name: "哈哈哈"  },
            {  name: "嘿嘿嘿"  },
        ],
        X: [
            {  name: "嘻嘻嘻"  }
        ],
        Y: [
             {  name: "哟哟哟"  }
        ]
  }

由父组件传递过来的数据:

props: {
    sourceData: {
      type: Array,
      default: () => []
    },
    name: {
      type: String,
      default: "shopName"
    }
}

sourceData 的处理方法(感觉方法写得太复杂了,待优化):

// 用 js-pinyin 获取汉字首字母
import pinyin from "js-pinyin"

getData() {
      pinyin.setOptions({ checkPolyphone: false, charCase: 0 })
      let alphabet = []
      let _charList = []

      for (let i = 0; i < this.sourceData.length; i++) {
        // this.name 是作为排序依据的字段名,由父组件传入,在这里就是 "name"
        // 获取原数组每一项的 name 值
        let name = this.sourceData[i][this.name]
        // 获取每一个name值第一个字的大写首字母(传入的 name 是中文时默认得到大写字母,name 是英文时按照原字符串输出,可能是小写)
        let initial = pinyin.getCamelChars(name).substring(0, 1).toUpperCase()
        // 给数组每一项增加名为 initial 的 key,值就是第一个字的大写首字母
        this.sourceData[i].initial = initial
        // 获取用于索引的字母
        if (alphabet.indexOf(initial) === -1) {
          alphabet.push(initial)
        }
      }
      // 按字母表顺序排序
      alphabet.sort()

      // 给每个字母增加唯一标识,后面定位时会用到
      for (var i = 0; i < alphabet.length; i++) {
        _charList.push({
          id: i,
          key: alphabet[i]
        })
      }
      this.charList = _charList
      let resultData = {}
      // 将源数据按照首字母分类
      for (let i = 0; i < alphabet.length; i++) {
        resultData[alphabet[i]] = this.sourceData.filter((item) => {
          return item.initial === alphabet[i]
        })
      }
      // 得到最终结果
      this.indexData = resultData
    },

2. 组件结构和样式(这部分没啥好说的)

<template>
  <div class="index-bar-content">
    <el-scrollbar style="height: 100%" ref="scrollbar">
      <div :id="key" class="main-list" v-for="(value, key) in indexData" :key="key" ref="listGroup">
        <div class="title-key">{{ key }}</div>
        <div class="content-container">
          <div class="content-item" v-for="(val, index) in value" :key="index">
            {{ val[name] }}
          </div>
        </div>
      </div>
    </el-scrollbar>
    <!-- 右侧字母列表 -->
    <ul class="char-list">
      <li v-if="totalPage > 1" @click="handlePreviousPage">
        <i class="iconfont iconshang"></i>
      </li>
      <li
        v-for="item in indexList"
        :key="item.id"
        @click="scrollToLetter(item)"
        :class="{ active: currentIndex === item.id }"
      >
        {{ item.key }}
      </li>
      <li v-if="totalPage > 1" @click="handleNextPage">
        <i class="iconfont iconxia"></i>
      </li>
    </ul>
  </div>
</template>
<style lang="scss">
.index-bar-content {
  position: relative;
  width: 400px;
  height: 304px;
  .el-scrollbar__wrap {
    overflow-x: hidden;
    .el-scrollbar__view {
      padding: 0 20px;
    }
  }
  .main-list {
    padding-top: 10px;
    .title-key {
      padding-bottom: 12px;
      font-size: 14px;
      font-weight: bold;
    }
    .content-container {
      display: flex;
      flex-wrap: wrap;
      .content-item {
        margin-right: 16px;
        margin-bottom: 12px;
        font-size: 12px;
      }
    }
  }
}
.el-popover {
  padding: 0;
}
.char-list {
  z-index: 99;
  width: 24px;
  height: 100%;
  background: #fafbfe;
  position: absolute;
  right: 0;
  top: 50%;
  transform: translateY(-50%);
  list-style: none;
  display: flex;
  flex-direction: column;
  text-align: center;
  li {
    height: 19px;
    display: inline;
    cursor: pointer;
    font-size: 14px;
  }
  .active {
    color: #fd4378;
  }
}
</style>

3.功能逻辑(最重要的部分)

首先来看点击右侧索引快速定位的功能,第一个点是如何实现左侧滚动。我这边利用的是scrollTop属性,只要计算出想要滚动的距离,给scrollTop赋值就可以定位到想要的位置了。

 

    // 计算每部分到容器顶部的距离,存入一个数组中
calculateHeight() {
    this.listHeight = []
    this.$nextTick(() => {
        const list = this.$refs.listGroup
        let height = 0
        this.listHeight.push(height)
        if (list) {
          for (let i = 0; i < list.length; i++) {
            let item = list[i]
            height += item.clientHeight
            this.listHeight.push(height)
          }
        }
    })
},
// 点击右侧索引实现左侧定位
scrollToLetter(item) {
    let scrollEle = this.$refs.scrollbar.wrap
    scrollEle.scrollTop = this.listHeight[item.id]
}

接下来实现右侧索引栏可翻页功能。每页显示多少条可以自己定,我这边容器高度304px,每个索引元素高19px,所以一页正好可以放置16个索引,除去上下翻页的箭头,就是14个。关键代码如下:

data() {
    return {
      // 当前页
      indexPage: 1,
      // 每页显示数量
      indexLimit: 14
    }
},
computed: {
    totalPage() {
      return Math.ceil(this.charList.length / this.indexLimit)
    },
    // 计算当前页的索引字母
    indexList() {
      return this.charList.slice(
        (this.indexPage - 1) * this.indexLimit,
        this.indexPage * this.indexLimit
      )
    }
},
methods: {
    handleNextPage() {
      if (this.indexPage < this.totalPage) {
        this.indexPage++
      }
    },
    handlePreviousPage() {
      if (this.indexPage > 1) {
        this.indexPage--
      }
    }
}

最后一个问题,左侧滚动到某部分右侧对应索引要高亮显示。这里需要监听页面的滚动事件并获取滚动距离来确定具体位置。

handleScroll() {
    let scrollEle = this.$refs.scrollbar.wrap
    scrollEle.onscroll = () => {
        let newY = scrollEle.scrollTop
        const listHeight = this.listHeight
        // 在中间部分滚动
        for (let i = 0; i < listHeight.length - 1; i++) {
          let height1 = listHeight[i]
          let height2 = listHeight[i + 1]
          if (height1 <= newY && newY < height2) {
            this.currentIndex = i
            // 注意翻页
            let currentPage = Math.floor(i / this.indexLimit) + 1
            this.indexPage = currentPage
            return
          }
      }
    }
}

总结:索引栏定位主要涉及到滚动距离的计算,具体到属性有scrollTop、clientHeight,这些平时接触得比较少,所以还是费了一番功夫,正好练一练。

完整代码:https://github.com/zdd2017/vue-components/blob/main/indexBar.vue

 

参考:https://www.cnblogs.com/marquess/p/12686500.html

posted @ 2021-09-28 19:53  Stroyer  阅读(1741)  评论(0编辑  收藏  举报