先上效果
没错看着界面很像黄轶老师webapp的,其实就是想用小程序实现一个,可以根据界面滚动位置字母也实时高亮,也可以点击字母索引跳转到对应的位置。没有使用scroll-view组件,因为不可能什么功能都有现成的。话不多说下面上代码
1、处理后台返回数据
先看下后台返回的数据是什么样子的,原来是这样子的(大家看到效果图上第一个是周董,因为这是我手动添加的,周董必须放在第一位哦)
有时候啊,后台看心情还不一定给你返回A-Z的字母呢,说不定返回了还不给你排序(我就遇到过),所以呢我还是自己处理了一遍,这是处理后的数据
这里说一下思路吧
- 后端返回的数据中前10条我们认定为是热门歌手,定义一个Map歌手对象,例如
singerMap[] = {
hot: {
title: 'hot',
list: [...]
}
}
- 将歌手名字转为拼音,可以借助
pinyin
这个npm包,获取歌手名拼音的首字母,例如
(tips:最好做一个判断,如果后台返回的数据为空或者返回错误的状态码,直接return,防止报错)
// key即是这个歌手属于那个字母下面
const key = pinyin.getCamelChars(item.name).substr(0,1)
if (key) {
if (!singerMap[key]) {
singerMap[key] = {
title: key,
list: []
}
}
// 每个字母下面会有多名歌手,获取歌手名拼音的首字母
singerMap[key].list.push(map([item])[0])
}
// 这里我们用到了一个自己封装的map方法,主要是做一个映射,构造单个歌手的数据结构
function map(arr) {
if (!Array.isArray(arr)) return
return arr.map(item => ({
id: item.id,
name: item.name,
pic: item.picUrl,
alias: item.alias,
picId_str: item.picId_str,
img1v1Url: item.img1v1Url
}))
}
- 定义两个数组用来存储热门、字母的歌手;循环遍历处理后的
singerMap
for (const key in singerMap) {
const item = singerMap[key]
if (item.title.match(/[a-zA-Z]/)) {
letter.push(item)
} else if (item.title === HOT) {
hot.push(item)
}
}
- 最后将字母按顺序排序,将最终处理后的result返回出去
// 按字母顺序排序
letter.sort((a, b) => a.title.charCodeAt(0) - b.title.charCodeAt(0))
result = {singers: hot.concat(letter)}
return result
好了,处理数据就说到这里,大家那么聪明的程序员应该都能处理好数据
2、编写基础页面
wxml代码
<!--components/SingerList/SingerList.wxml-->
<view class="index-list">
<view id="groupRef">
<view wx:for="{{data}}" wx:key="title" class="group">
<view id="{{item.title}}" class="title">{{item.title}}</view>
<view>
<view wx:for="{{item.list}}" wx:key="id" wx:for-item="t" class="item">
<image class="avatar" src="{{t.pic}}" lazy-load="{{true}}" />
<span class="name">{{t.name}}</span>
</view>
</view>
</view>
</view>
<!-- <div class="fixed" v-show="fixedTitle" :style="fixedStyle">
<div class="fixed-title">{{fixedTitle}}</div>
</div> -->
<view class="shortcut">
<view>
<view
wx:for="{{data}}"
wx:key="index"
data-id="{{item.title}}"
data-index="{{index}}"
catchtap="onScrollTo"
class="shortcut-item {{curIndex === index ? 'active': ''}}"
>
{{item.title}}
</view>
</view>
</view>
</view>
wxss代码
/* components/SingerList/SingerList.wxss */
.index-list {
position: relative;
width: 100%;
height: 100%;
overflow: hidden;
background: #222;
}
.group {
padding-bottom: 30rpx;
}
.title {
height: 60rpx;
line-height: 60rpx;
padding-left: 20rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
background-color: #333;
}
.item {
display: flex;
align-items: center;
padding: 40rpx 0 0 40rpx;
}
.avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
}
.name {
margin-left: 40rpx;
color: rgba(255, 255, 255, 0.5);
font-size: 28rpx;
}
.fixed {
position: absolute;
top: 0;
left: 0;
width: 100%;
}
.fixed-title {
height: 60rpx;
line-height: 60rpx;
padding-left: 40rpx;
font-size: 24rpx;
color: rgba(255, 255, 255, 0.5);
background: #333;
}
.shortcut {
position: fixed;
right: 18rpx;
top: 50%;
transform: translateY(-50%);
width: 40rpx;
padding: 40rpx 0;
border-radius: 20rpx;
text-align: center;
background-color: rgba(0, 0, 0, 0.3);
font-family: Helvetica;
}
.shortcut-item {
padding: 3px;
line-height: 1;
color: rgba(255, 255, 255, 0.5);
font-size: 24rpx;
}
.active {
color: #ffcd32;
}
3、处理逻辑部分(数据请提前在data中定义好)
1、首先这个组件接👋两个数据
scrollTop即父组件实时滚动的top值
onPageScroll(e) {
this.setData({
scrollTop: e.scrollTop
})
}
2、刚进入页面的时候我们需要获取group类中的dom信息,这是为了滚动高亮显示字母做准备
pageLifetimes: {
show() {
const groupRef = wx.createSelectorQuery().in(this)
groupRef.selectAll('.group').boundingClientRect(rect => {
const listHeights = rect.map(item => item.top)
this.setData({
listHeights,
groupRef: rect
})
}).exec()
}
},
3、先处理点击字母跳转到对应位置
methods: {
onScrollTo({currentTarget: {dataset}}) {
const {id, index} = dataset
const query = wx.createSelectorQuery().in(this)
// 这里的动态id就是我们在wxml中给每个item绑定的id
query.select(`#${dataset.id}`).boundingClientRect(rect => {
this.setData({curIndex: index})
this.triggerEvent('scrollY', {
scrollTop: rect.top,
alphaId: id
})
}).exec()
},
}
然后在父组件中接受不了子组件传来的值,并进行跳转。为什么不在子组件中完成这个逻辑,
因为这个列表组件只是为了渲染,任何功能性的都会放在父组件中处理
// 接受组件传来的滚动值,并滚动到指定位置
onScrollTo(e) {
console.log(e)
const {scrollTop} = e.detail
wx.pageScrollTo({scrollTop})
},
4、处理滚动页面时,字母高亮
在模板中,我们是使用index作为标志来显示高亮的(你也可以使用title),现在我们要监听scrollTop
的值,计算每一项列表的高度区间;然后用新的scrollTop和高度区间的top、bottom进行一个判断
observers: {
data() {
this.calculate()
},
scrollTop(newVal) {
const singersVal = this.data.listHeights
// 在最开始的时候push了一个0,这里就需要-1,相对应
for (let i = 0; i < singersVal.length - 1; i++) {
const heightTop = singersVal[i]
const heightBottom = singersVal[i + 1]
console.log(heightTop)
if (newVal >= heightTop && newVal <= heightBottom) {
this.setData({
curIndex: i
})
}
}
}
},
methods: {
// 计算每一项列表的高度区间
calculate() {
const {listHeights, groupRef} = this.data
// 区间高度
let height = 0
// 每次计算前都要先清空,从0开始
listHeights.length = 0
listHeights.push(height)
for (let i = 0; i < groupRef.length; i++) {
height += groupRef[i].height
listHeights.push(height)
}
this.setData({listHeights})
}
}
完整代码
// components/SingerList/SingerList.js
Component({
properties: {
data: {
type: Array,
value: []
},
scrollTop: {
type: Number,
value: 0
}
},
data: {
defaultId: '热',
curIndex: 0,
listHeights: [],
groupRef: []
},
pageLifetimes: {
show() {
const groupRef = wx.createSelectorQuery().in(this)
groupRef.selectAll('.group').boundingClientRect(rect => {
const listHeights = rect.map(item => item.top)
this.setData({
listHeights,
groupRef: rect
})
}).exec()
}
},
observers: {
data() {
this.calculate()
},
scrollTop(newVal) {
const singersVal = this.data.listHeights
// 在最开始的时候push了一个0,这里就需要-1,相对应
for (let i = 0; i < singersVal.length - 1; i++) {
const heightTop = singersVal[i]
const heightBottom = singersVal[i + 1]
console.log(heightTop)
if (newVal >= heightTop && newVal <= heightBottom) {
this.setData({
curIndex: i
})
}
}
}
},
methods: {
onScrollTo({currentTarget: {dataset}}) {
const {id, index} = dataset
const query = wx.createSelectorQuery().in(this)
query.select(`#${dataset.id}`).boundingClientRect(rect => {
this.setData({curIndex: index})
this.triggerEvent('scrollY', {
scrollTop: rect.top,
alphaId: id
})
}).exec()
},
// 计算每一项列表的高度区间
calculate() {
const {listHeights, groupRef} = this.data
// 区间高度
let height = 0
// 每次计算前都要先清空,从0开始
listHeights.length = 0
listHeights.push(height)
for (let i = 0; i < groupRef.length; i++) {
height += groupRef[i].height
listHeights.push(height)
}
this.setData({listHeights})
}
}
})