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