歌手列表數據接口依舊使用前面的API,使用axios獲取歌手列表數據css
singer.js:vue
import axios from 'axios'; export function getSingerList() { return axios.get('/api/getSingerList'); }singer.vue:ios
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) } }) } } }運行結果:axios
有上圖運行結果可知須要對歌手數據進行一些簡單的處理,須要將歌手ID、歌手名字和歌手圖片進行封裝,而且因爲返回的歌手數據沒有相應的鍵名(即歌手姓名的首字母)因此還須要獲取其鍵名,這樣纔可以經過鍵名進行分類。api
獲取歌手首字母方法:使用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); }封裝歌手數據:app
singer.jsecmascript
export default class Singer { constructor({ id, name, avatar }) { this.id = id; this.name = name; this.avatar = avatar; } }singer.vueflex
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>