1.获取歌手数据
api->singer.js
import jsonp from 'common/js/jsonp'
import {commonParams, options} from './config.js'
export function getSingerList() {
const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg'
const data = Object.assign({}, commonParams, {
channel: 'singer',
page: 'list',
key: 'all_all_all',
pagesize: 100,
pagenum: 1,
hostUin: 0,
needNewCode: 0,
platform: 'yqq'
})
return jsonp(url, data, options)
}
common->singer.vue中定义data接收数据,并在created中调用方法获取数据
import {getSingerList} from 'api/singer'
import {ERR_OK} from 'api/config'
data() {
return {
singers: []
}
},
created() {
this._getSingerList()
},
methods: {
_getSingerList() {
getSingerList().then((res) => {
if (res.code === ERR_OK) {
this.singers = res.data.list
// console.log(this._normalizeSinger(this.singers))
this.singers = this._normalizeSinger(this.singers)
}
})
},
}
现在获取到的数据不是我们想要的数据,我们希望数据是一个二层数组,外层是a,b,c...,内层是以A开头的歌手的名字,然后是以B开头的数组的名字;在源数据中Findex是用来标志数据的,周杰伦标记为Z
在_normalizeSinger中规范化数据,将遍历的数据规范化为map中 的有用的数据,并将map转化为一个有序列表
js->singer.js,抽象成歌手类,类中包含歌手的id,name和avatar,这个类是在我们进行后面的map封装时,map[k].items.push(new singer{});要填充的对象
export default class Singer {
constructor({id, name}) {
this.id = id
this.name = name
this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`
}
}
singer.vue中,返回一个数组,并将其传给this.singer
import Singer from 'common/js/singer'
_normalizeSinger(list) { // 规范化数据,list=this.singers
let map = {
hot: {
title: HOT_NAME, // 以‘热门’开头的歌手名
items: [] // title下的歌手列表
}
}
list.forEach((item, index) => {
// 热门数据,十条之内都添加到map中
if (index < HOT_SINGER_LEN) {
map.hot.items.push(new Singer({ // 某个歌手热门歌曲的前十条
id: item.Fsinger_mid,
name: item.Fsinger_name
}))
}
const key = item.Findex // Findex: "X"
// 如果没有以这个k为对象的聚合的话,就添加一个k,创建这个对象,因为有的姓氏开头的字母很少见
// 表示以前没有这个歌手名的首字母就直接添加
if (!map[key]) { // 以字母A B C D开头的歌手,但是存到map里的ABCD是无序的
map[key] = {
title: key,
items: []
}
}
map[key].items.push(new Singer({
id: item.Fsinger_mid,
name: item.Fsinger_name
}))
})
// 为了得到有序列表,我们需要处理 map
let hot = []
let ret = []
for (let key in map) {
let val = map[key] // 对应一个title和items
if (val.title.match(/[a-zA-Z]/)) {
ret.push(val) // 将title对应的val push到ret数组中
} else if (val.title === HOT_NAME) {
hot.push(val)
}
}
// 对title是A-Z的ret数组进行排序
ret.sort((a, b) => {
return a.title.charCodeAt(0) - b.title.charCodeAt(0) // a-b>0 return true,升序
})
return hot.concat(ret) // 两部分数组拼接,ret即为一个有序的数组
}
2. singer组件中的数据处理好之后,开发listview.vue组件,类似于通讯录,首先在singer.vue中将数据传递出去
import ListView from 'base/listview/listview'
并注册
<list-view :data="singers"></list-view>
data() {
return {
singers: []
}
}
3. 在listview中
listview.vue组件通过props+:data接收数据,数据的数据类型是数组,将其填充到DOM中,img图片采用懒加载v-lazy="item.avatar"
props: {
data: {
type: Array,
defalut: []
}
},
我们在scroll组件中传入data,这样,当data发生变化时,scroll组件就可以自动计算高度refresh,正确滚动
<template>
<!-- 传入数据才可以滚动,没有高度的问题-->
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll"> <!-- 将listenScroll的值传给scroll,并监听子组件scroll传过来的事件scroll-->
<ul>
<li v-for="group in data" :key="group.id" class="list-group" ref="listGroup">
<h2 class="list-group-title">{{group.title}}</h2>
<ul>
<li v-for="item in group.items" :key="item.id" class="list-group-item">
<img :src="item.avatar" class="avatar" v-lazy="item.avatar">
<span class="name">{{item.name}}</span>
</li>
</ul>
</li>
</ul>
</scroll>
</template>
此处引入了scroll插件,完成滚动
在singer组件中引入listview组件,并注册,不要忘了将处理好格式的singers数据传递给listview组件,lsitviwe组件会在props中接受,上边已经写过了
<template>
<div class="singer">
<list-view @select="selectSinger" :data="singers"></list-view>
<router-view></router-view> <!-- 挂载歌手详情页的路由-->
</div>
</template>
4. listview.vue右侧快速入口的实现
1) 首先获取shortCutList列表,里面存储的是“热门”+(A-Z)
shortcutList() {
return this.data.map((group) => {
return group.title.substr(0, 1)
})
},
<div class="list-shortcut" @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove"> <!-- touchstart是BScroll自带的事件-->
<ul>
<li v-for="(item, index) in shortcutList"
:key="item.id, index"
class="item"
:class="{'current':currentIndex===index}"
:data-index="index"> <!-- 变量data-index要进行v-bind-->
{{item}}
</li>
</ul>
</div>
2)点击右侧shortcutList列表,左侧内容可以滚动到相应的位置
添加点击事件,并在js-dom.js中封装一个getData()方法,获取到data.index的值;在base->scroll.vue下扩展一个方法scrollTo(),使其滚动到相应的位置,扩展方法scrollToElement(),使其滚动到相应位置,方法都是BScroll自带的,在这里我们将他们重新封装,在listview.vue中事件onShortcutTouchStart中进行调用
export function getData(el, name, val) { // DOM对象,属性名,属性值
const prefix = 'data-'
if (val) { // 有val就设置新的值
return el.setAttribute(prefix + name, val)
} // 没有val就获取当前属性的值
return el.getAttribute(prefix + name)
}
在scroll.vue中重新封装scrollTo方法
scrollTo() { // 因为scrollTo是要传入某些参数的,所以我们要通过apply将函数上下文换成我们当前的scroll对象
this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments) // applay引用到上下文中
},
scrollToElement() {
this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
}
回到之前的onShortcutTouchStart编写触发函数
onShortcutTouchStart(e) {
// 点击的时候要先获取元素的索引
let anchorIndex = getData(e.target, 'index')
let firstTouch = e.touches[0] // 手指第一次触碰的位置
// 当前的y值
this.touch.y1 = firstTouch.pageY
// 当前的锚点
this.touch.anchorIndex = anchorIndex
// console.log(anchorIndex) // 0 1 2
// 然后利用BScroll将$refs.listview滚动到$refs.listGroup相应的位置,封装在_scrollTo中了
// this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0)
this._scrollTo(anchorIndex)
}
这样我们点到右侧abcd的值,歌手列表就会滚动到响应的位置;
两个ref的说明(this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0))
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll"> <!-- 将listenScroll的值传给scroll,并监听子组件scroll传过来的事件scroll-->
<ul>
<li v-for="group in data" :key="group.id" class="list-group" ref="listGroup">
我们将歌手列表根据index的位置进行滚动的语句封装成了一个函数,如下:
_scrollTo(index) {
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) // 第二个参数需不需要缓动时间
}
3) 手指滑动字母列表时,右侧也能相应的滚动(不只是能点击,还要可以滑动),监听touchmove事件实现,
首先我们要阻止右侧列表的touchmove事件的冒泡,因为如果事件冒泡到上层会在右侧列表滚动之后出现整个歌手列表区的滚动
<div class="list-shortcut" @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove"> <!-- touchstart是BScroll自带的事件-->
在onShortcutTouchMove事件中,要先找到Y轴上的偏移,在created()中定义一个参数,结合onShortcutTouchStart中的this.touch.y1数据和锚点(ANCHOR_HEIGHT)的高度,计算一共走过了几个锚点,就在_scrollTo()中将ref移动到指定位置
this.touch是在onShortcutTouchMove和onShortcutTouchStart共享的函数,data和props中的数据都会被自动的添加一个getter和setter,所以vue会观测到data,props和computed中数值的变化,只要是为dom做数据绑定用的,因为我们并不需要观测touch的变化,我们只是单纯的为了两个函数都能获取到这个数据,所以我们将touch定义到created中;
created() {
this.touch = {}
}
结合onShortcutTouchStart中的this.touch.y1数据和和当前锚点的索引(ABDC)
onShortcutTouchStart(e) {
// 点击的时候要先获取元素的索引
let anchorIndex = getData(e.target, 'index')
let firstTouch = e.touches[0] // 手指第一次触碰的位置
// 当前的y值
this.touch.y1 = firstTouch.pageY
// 当前的锚点
this.touch.anchorIndex = anchorIndex
// console.log(anchorIndex) // 0 1 2
// 然后利用BScroll将$refs.listview滚动到$refs.listGroup相应的位置,封装在_scrollTo中了
// this.$refs.listview.scrollToElement(this.$refs.listGroup[anchorIndex], 0)
this._scrollTo(anchorIndex)
}
onShortcutTouchMove和onShortcutTouchStart两个时间手指第一次触屏是的高度差delta然后除以锚点的高度为18
const ANCHOR_HEIGHT = 18 // 通过css样式计算出来一个锚点的高度为18
这样我们在onShortcutTouchMove就知道移动了多少个锚点,并更新滚动之后锚点的index,然后将歌手列表滚动到当前锚点的位置
onShortcutTouchMove(e) {
// touchstart的时候要获取当前滚动的一个Y值,touchmove的时候也要获取当前滚动的一个Y值
let firstTouch = e.touches[0]
this.touch.y2 = firstTouch.pageY
// 计算出偏移了几个锚点
let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0
// 滚动之后的锚点,更新anchorIndex的值
let anchorIndex = parseInt(this.touch.anchorIndex) + delta // 字符串转化为int类型
// console.log(anchorIndex)
this._scrollTo(anchorIndex)
}
这样,我们实现了右侧字母列表滚动时,歌手列表跟随滚动的效果,重点是计算出两次触屏index的差值delta和sart首次触屏时的index,从而计算出move之后的index值,让ref:listview移动到listview[index];
4)之后,我们现实歌手列表滚动,右侧字母列表也响应滚动的效果;滚动的时候产生联动效果,需要有一个变量计算位置落在哪个区间
在scroll.vue中,添加listenScroll,是否要监听滚动事件进滚动,在scroll.vue中
listenScroll: { // 要不要监听滚动事件
type: Boolean,
default: false
}
如果要监听滚动事件,在初始化BScroll的时候要设置滚动事件,并派发函数将pos传出去
_initScroll() {
if (!this.$refs.wrapper) { // wrapper没有值的时候,直接return
return
}
this.scroll = new BScroll(this.$refs.wrapper, {
probeType: this.probeType,
click: this.click
})
// 如果要监听滚动事件,在初始化列BScroll之后要派发一个监听事件
if (this.listenScroll) {
// BScroll 中的this是默认指向scroll的,所以要在me中保留vue实例的this
let me = this
// 监听scroll,拿到pos后,派发一个函数将pos传出去
this.scroll.on('scroll', (pos) => {
me.$emit('scroll', pos)
})
}
}
// BScroll 中的this是默认指向scroll的,所以要在me中保留vue实例的this let me = this // 监听scroll,拿到pos后,派发一个函数将pos传出去 this.scroll.on('scroll', (pos) => { me.$emit('scroll', pos) }) } },
之后回到listview.vue中,添加listenScroll属性
created() {
this.touch = {}
this.listenScroll = true
this.listHeight = []
this.probeType = 3 // 滑动很长的列表的时候右侧要连续给出高亮
}
将listenScroll传递给组件
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll">
<!-- 将listenScroll的值传给scroll,并监听子组件scroll传过来的事件scroll-->
在scroll组件中监听 @scroll="scroll",获取滚动的高度,定义为scrollY
data() {
return {
scrollY: -1,
currentIndex: 0, // 默认第一个是高亮的
diff: -1
}
}
scroll(pos) {
this.scrollY = pos.y // 实时获取到bscroll滚动到Y轴的网距离
},
拿到滚动条的高度scrollY之后,还要计算每个title块的位置,确定scrollY落在哪两个title块之间
<li v-for="group in data" :key="group.id" class="list-group" ref="listGroup">
_calculateHeight() {
this.listHeight = [] // 每次滚动时高度计算都重新开始
const list = this.$refs.listGroup
let height = 0
this.listHeight.push(height) // '热'的高度为0,第一个元素
for (let i = 0; i < list.length; i++) {
let item = list[i]
height += item.clientHeight
this.listHeight.push(height)
}
}
在watch中观测data和scrollY的变化,data是父组件传过来的数据,data变化时重新计算高度,并维护一个currentIndex,currentIndex即为高亮的元素
data() {
return {
scrollY: 1,
currentIndex: 0, // 默认第一个是高亮的
diff: -1
}
在watch中观测scrollY的变化
watch: {
data() {
setTimeout(() => { // 数据到DOM的变化有一个延时
this._calculateHeight()
}, 20)
},
scrollY(newY) {
const listHeight = this.listHeight
// 当滚动到顶部, newY > 0
if (newY > 0) {
this.currentIndex = 0
return
}
// 中间部分滚动
for (let i = 0; i < listHeight.length - 1; i++) {
let height1 = listHeight[i]
let height2 = listHeight[i + 1]
if (-newY >= height1 && -newY < height2) { // !height2表示列表的最后一项
this.currentIndex = i
this.diff = height2 + newY
// console.log(this.currentIndex)
return
}
}
// 当滚动到底部,且-newY大于最后一个元素的上限
this.currentIndex = listHeight.length - 2
}
},
连续滚动的时候回出现错误:‘那是因为probtype的原因,设置为3,使其可以连续监听滚动,并将值传入到组件中
created() {
this.touch = {}
this.listenScroll = true
this.listHeight = []
this.probeType = 3 // 滑动很长的列表的时候右侧要连续给出高亮
}
并将值传入到组件中
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll"> <!-- 将listenScroll的值传给scroll,并监听子组件scroll传过来的事件scroll-->
这样就可以连续的滚动监听了,之后再对scrollY中拿到的currentIndex进行判断,使其高亮
当currentIndex === index时会出现高亮
<li v-for="(item, index) in shortcutList"
:key="item.id, index"
class="item"
:class="{'current':currentIndex===index}"
:data-index="index"> <!-- 变量data-index要进行v-bind,获取元素的下标时用到的getData(e.target, 'index')-->
{{item}}
</li>
出现一个问题,拉到最后的时候,console.log(22),回到顶部就变成了23,
这是因为高度的计算不正确,在滚动到最顶部的时候newY的值是大于零的,所以我们-newY在height1和height2之间的判断就不符合了,所以他满足 !height2,所以他输出23是不对的。
new一共有三种情况,在顶部的时候,newY的值是大于零的,中间部分-newY是在height1和height2之间,最后是-newY有可能大于height2
scrollY(newY) {
const listHeight = this.listHeight
// 当滚动到顶部, newY > 0
if (newY > 0) {
this.currentIndex = 0
return
}
// 中间部分滚动,长度减1
for (let i = 0; i < listHeight.length - 1; i++) {
let height1 = listHeight[i]
let height2 = listHeight[i + 1]
// 往上滚动的时候newY是一个负值
if (-newY >= height1 && -newY < height2) { // !height2表示列表的最后一项
this.currentIndex = i
this.diff = height2 + newY
// console.log(this.currentIndex)
return
}
}
// 当滚动到底部,且-newY大于最后一个元素的上限
this.currentIndex = listHeight.length - 2
}
creatde()和scroll之间的传递
created() {
this.touch = {}
this.listenScroll = true
this.listHeight = []
this.probeType = 3 // 滑动很长的列表的时候右侧要连续给出高亮
}
<scroll class="listview" :data="data"
ref="listview"
:probeType = "probeType"
:listenScroll = "listenScroll"
@scroll="scroll">
<!-- 将listenScroll的值传给scroll组件,并监听子组件scroll.vue传过来的事件scroll-->
5) 滚动的时候可以产生高亮,但是点击右侧的时候不能产生高亮,因为点击时的滚动是通过refs获取相应的DOM来滚动的,没有用到scrollY,但是我们定义高亮是通过watch scrollY落到的区间判断的,所以在_scrollTo(index)中,我们要手动更新scrollY的值,然后让watch监听到;
_scrollTo(index) {
if (!index && index !== 0) { // 点击最顶部的情况
return
}
if (index < 0) { // 处理滑动时的边界情况
index = 0
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2
}
// 点击时更新scrollY的值才会出现高亮,定义为每一个listHeight的上限位置,是一个负值
this.scrollY = -this.listHeight[index]
this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) // 第二个参数需不需要缓动时间
}
6)2.单击‘热门’上边的那一部分时,Z会高亮,输出index,测试到输出结果是null,所以在_scrollTo中做好限制,滑动到最顶部的时候,Z也会高亮,那是因为movetouch事件一直没有停止,所以在_scrollTo中添加
if (!index && index !== 0) { // 点击最顶部的情况
return
}
if (index < 0) { // 处理滑动时的边界情况
index = 0
} else if (index > this.listHeight.length - 2) {
index = this.listHeight.length - 2
}
到此,listview组件开发完成
7)固定titile,并利用css将其规定在顶部
<div class="list-fixed" v-show="fixedTitle" ref="fixed"> <!-- fixedTitle为空时不显示-->
<h1 class="fixed-title">{{fixedTitle}}</h1>
</div>
fixedTitle() {
if (this.scroll > 0) { // 超出顶部了
return '' // 向下拉的时候title消失
}
return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ''
}
8) 滚动时出现上层title将下层的title盖住的情况,watch diff的变化
data() {
return {
scrollY: -1,
currentIndex: 0, // 默认第一个是高亮的
diff: -1
}
}
当title区块的上限减去scrollY的差,判断这个差值和title的高度,这个差值大于title时,title是不用变的
在中间滚动的时候定义diff的值,上限加上差值,newY是负值,实际上是减去
if (-newY >= height1 && -newY < height2) { // !height2表示列表的最后一项
this.currentIndex = i
this.diff = height2 + newY
// console.log(this.currentIndex)
return
}
title的高度为30px
const TITLE_HEIGHT = 30
然后在watch diff
diff(newVal) {
let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
if (this.fixedTop === fixedTop) {
return
}
this.fixedTop = fixedTop
this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
}
最后可以加一个loading组件
<div v-show="!data.length">
<loading></loading>
</div>
.loading-container
position: absolute
width: 100%
top: 50%
transform: translateY(-50%)
接下来是歌手的详情界面