歌手列表数据获取
歌手列表数据接口依旧使用前面的API,使用axios获取歌手列表数据
singer.js:
import axios from ‘axios‘; export function getSingerList() { return axios.get(‘/api/getSingerList‘); }singer.vue:
import { getSingerList } from ‘../../api/singer‘ import { ERR_OK } from ‘../../api/config‘ export default { data() { return { singers: [] } }, created() { this._getSingerList() }, methods: { _getSingerList() { getSingerList().then((res) => { res = res.data.response.singerList; if (res.code === ERR_OK) { this.singers = res.data.singerList console.log(this.singers) } }) } } }运行结果:
歌手数据处理和 Singer 类的封装
有上图运行结果可知需要对歌手数据进行一些简单的处理,需要将歌手ID、歌手名字和歌手图片进行封装,并且由于返回的歌手数据没有相应的键名(即歌手姓名的首字母)所以还需要获取其键名,这样才能够通过键名进行分类。
获取歌手首字母方法:使用js-pinyin插件
singerName.js
import pinyin from ‘js-pinyin‘; export function Getinitial(string) { let pinyin = require(‘js-pinyin‘); pinyin.setOptions({ checkPolyphone: false, charCase: 0 }); return pinyin.getCamelChars(string).substring(0, 1); }封装歌手数据:
singer.js
export default class Singer { constructor({ id, name, avatar }) { this.id = id; this.name = name; this.avatar = avatar; } }singer.vue
const HOT_NAME = ‘热门‘; const HOT_SINGER_LEN = 10; export default { name: ‘singer‘, components: { ListView, }, data() { return { singers: [], }; }, created() { this._getSingerList(); }, methods: { _getSingerList() { getSingerList().then((res) => { res = res.data.response.singerList; if (res.code === ERR_OK) { this.singers = this._normalizeSinger(res.data.singerlist); } }); }, _normalizeSinger(list) { let map = { hot: { title: HOT_NAME, items: [], }, }; list.forEach((item, index) => { if (index < HOT_SINGER_LEN) { map.hot.items.push(new Singer({ id: item.singer_mid, name: item.singer_name, avatar: item.singer_pic, })); } const key = Getinitial(item.singer_name); if (!map[key]) { map[key] = { title: key, items: [], }; } map[key].items.push(new Singer({ id: item.singer_mid, name: item.singer_name, avatar: item.singer_pic, })); }); console.log(map); }, }, };运行结果:
为了得到有序的歌手列表,还需要对
map
进行处理_normalizeSinger(list) { let map = { hot: { title: HOT_NAME, items: [], }, }; list.forEach((item, index) => { if (index < HOT_SINGER_LEN) { map.hot.items.push(new Singer({ id: item.singer_mid, name: item.singer_name, avatar: item.singer_pic, })); } const key = Getinitial(item.singer_name); if (!map[key]) { map[key] = { title: key, items: [], }; } map[key].items.push(new Singer({ id: item.singer_mid, name: item.singer_name, avatar: item.singer_pic, })); }); // 为了得到有序列表,我们需要处理map let hot = []; let ret = []; for (let key in map) { let val = map[key]; if (val.title.match(/[a-zA-Z]/)) { ret.push(val); } else if (val.title === HOT_NAME) { hot.push(val); } } ret.sort((a, b) => { return a.title.charCodeAt(0) - b.title.charCodeAt(0); }); // concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。 return hot.concat(ret); },
显示歌手列表
歌手列表基础组件:listview.vue
<template> <scroll class="listview" :data="data"> <ul> <li v-for="(group, index) in data" :key="index" class="list-group"> <h2 class="list-group-title">{{group.title}}</h2> <uL> <li v-for="(item, index) in group.items" :key="index" class="list-group-item"> <img class="avatar" v-lazy="item.avatar"> <span class="name">{{item.name}}</span> </li> </uL> </li> </ul> </scroll> </template> <script> import Scroll from ‘base/scroll/scroll‘ export default { props: { data: { type: Array, default() { return []; }, }, }, components: { Scroll } } </script> <style lang="scss" scoped> .listview { position: relative; width: 100%; height: 100%; overflow: hidden; background: $color-background; .list-group { padding-bottom: 30px; .list-group-title { height: 30px; line-height: 30px; padding-left: 20px; font-size: $font-size-small; color: $color-text-l; background: $color-highlight-background; } .list-group-item { display: flex; align-items: center; padding: 20px 0 0 30px; .avatar { width: 50px; height: 50px; border-radius: 50%; } .name { margin-left: 20px; color: $color-text-l; font-size: $font-size-medium; } } } .list-shortcut { position: absolute; z-index: 30; right: 0; top: 50%; transform: translateY(-50%); width: 20px; padding: 20px 0; border-radius: 10px; text-align: center; background: $color-background-d; font-family: Arial, Helvetica, sans-serif; .item { padding: 3px; line-height: 1; color: $color-text-l; font-size: $font-size-small; // &表示当前元素 &.current { color: $color-theme; } } } .list-fixed { position: absolute; top: 0; left: 0; width: 100%; .fixed-title { height: 30px; line-height: 30px; padding-left: 20px; font-size: $font-size-small; color: $color-text-l; background: $color-highlight-background; } } .loading-container { position: absolute; width: 100%; top: 50px; transform: translateY(-50%); } } </style>在singer.vue中使用该组件
// singer.vue <template> <div class="singer"> <list-view :data="singerList"></list-view> </div> </template> <script type="text/ecmascript-6"> import ListView from ‘../../base/listview/listview‘ export default { ... components: { ListView } } </script>运行结果:
右侧快速入口实现
类比于手机通讯录,悬浮于屏幕右侧的
A-Z
可以帮助我们快速找到对应的歌手。在listview.vue中添加
<div class="list-shortcut"> <ul> <li v-for="(item, index) in shortcutList" :key="index" class="item">{{item}}</li> </ul> </div> <script> export default { ... computed: { shortcutList() { return this.data.map((group) => { return group.title.substr(0, 1) }) } } } </script>运行结果:右侧出现快速入口
接下来就为其添加点击事件:点击对应字母时,需要获取其索引,这里直接获取
v-for
提供的index
即可export default { ... methods: { onShortcutTouchStart(e, index) { this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) } } scrollTo() { this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments) }, scrollToElement() { this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments) } }接着就是实现右侧快速入口滑动滚动效果了:
在
onShortcutTouchStart
事件中记录触碰点的初始位置,以及onShortcutTouchMove
事件中触碰点的位置,通过两个位置的像素差,来滚动歌手列表。在给右侧添加滑动滚动的同时,需要阻止歌手列表滚动,以及浏览器原生滚动,所以要使用@touchmove.stop.prevent
阻止原生的 touchmove。<div class="list-shortcut" @touchmove.stop.prevent="onShortcutTouchMove"> <ul> <li v-for="(item, index) in shortcutList" :key="index" @touchstart="onShortcutTouchStart($event, index)" class="item">{{item}}</li> </ul> </div> <script> const ANCHOR_HEIGHT = 18 export default { created() { this.touch = {} }, ... methods: { onShortcutTouchStart(e, index) { let firstTouch = e.touches[0] this.touch.y1 = firstTouch.pageY this.touch.anchorIndex = index this._scrollTo(index) }, onShortcutTouchMove(e) { let firstTouch = e.touches[0] this.touch.y2 = firstTouch.pageY let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0 let anchorIndex = this.touch.anchorIndex + delta this._scrollTo(anchorIndex) }, _scrollTo(index) { this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0) } }, components: { Scroll } } </script>最后就是高亮当前显示的
title以及滚动固定标题:
- 高亮当前显示的
title,需要监听
scroll
组件的滚动事件,来获取当前滚动的位置在屏幕滑动的过程中,并且需要实时派发 scroll 事件,所以在listview
中将probeType
的值设为 3<script > export default { props: { ... listenScroll: { type: Boolean, default: false } }, methods: { _initScroll() { ... if (this.listenScroll) { let me = this this.scroll.on(‘scroll‘, (pos) => { me.$emit(‘scroll‘, pos) }) } } } } </script>
滚动固定标题:滚动歌手列表页时,当前歌手对应的title固定不动,滚动到下一个
title
时,新的title
将旧的title
顶替掉,这里就需要计算一个title
的高度// listview.vue <template> <scroll class="listview" :data="data" ref="listview" :probe-type="probeType" :listenScroll="listenScroll" @scroll="scroll"> ... <div class="list-fixed" ref="fixed" v-show="fixedTitle"> <div class="fixed-title">{{fixedTitle}}</div> </div> </scroll> </template> <script> import Scroll from ‘../../base/scroll/scroll‘ const TITLE_HEIGHT = 30 const ANCHOR_HEIGHT = 18 export default { ... data() { return { scrollY: -1, currentIndex: 0, diff: -1 } }, computed: { ... fixedTitle() { if (this.scrollY > 0) { return ‘‘ } return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ‘‘ } }, watch: { ... scrollY(newY) { ... for (let i = 0; i < listHeight.length - 1; i++) { ... if (-newY >= height1 && -newY < height2) { ... this.diff = height2 + newY return } } ... }, 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)` } } } </script>