建立singer-detail.vue组件css
<template> <div class="singer-detail"></div> </template> <script> export default { name: 'singer-detail', }; </script> <style lang="scss" scoped> .singer-detail { position: fixed; z-index: 100; top: 0; left: 0; right: 0; bottom: 0; background: $color-background; } </style>在route --> index.js中:引入并配置Singer子路由SingerDetailhtml
import SingerDetail from '../components/singer-detail/singer-detail'; { path: '/singer', name: 'Singer', component: Singer, children: [{ path: ':id', component: SingerDetail, }], }在singer.vue中添加<router-view></router-view>vue
<template> <div class="singer"> <list-view :data="singers"></list-view> <router-view></router-view> </div> </template>在listview.vue中给<li class="list-group-item">添加点击事件,并在methods中定义selectItem方法ios
<ul> <li v-for="(group,index) in data" class="list-group" ref="listGroup" :key="index"> <h2 class="list-group-title">{{group.title}}</h2> <ul> <li class="list-group-item" @click="selectItem(item)" v-for="item in group.items" :key="item.id"> <img v-lazy="item.avatar" class="avatar"> <span class="name">{{item.name}}</span> </li> </ul> </li>
</ul> methods: { selectItem(item) { this.$emit('select', item); }, }在singer.vue中监听select事件,触发selectSingergit
<list-view @select="selectSinger" :data="singers"></list-view> methods: { selectSinger(singer) { this.$router.push({ path: `/singer/${singer.id}`, }); }, }
点击歌手跳转到歌手详情页能够加个过渡动画效果实现转场web
vue中的transition标签能够方便得进行动画过渡vuex
<template> <transition name="slide"> <div v-if="show" class="singer-detail"></div> </transition> </template> <script> export default { name: 'singer-detail', data() { return { show: false, }; }, created() { setTimeout(() => { this.show = true; }, 20); }, }; </script> .slide-enter-active, .slide-leave-active { transition: all 0.3s; } .slide-enter, .slide-leave-to { transform: translate3d(100%, 0, 0);//100% 彻底移动到屏幕右侧 动画开始后向左滑入 }
效果图npm
子路由SingerDetail须要从父路由页面Singer获取不少数据,都用参数获取内容太多,因此须要使用Vuex来进行管理。json
Vuex是一个用来管理组件之间通讯的插件,它是一个专为【vue.js】应用程序开发的状态管理模式,它解决了组件之间同一状态的共享问题,它可以更好地在组件外部管理状态。axios
安装 : npm install vuex --save
在src -->store目录下建立如下js文件:
- index.js:入口文件
- state.js:管理全部状态 state
- mutations.js:管理全部mutation —— 更改 Vuex 的 store 中状态state的惟一方法
- mutation-types.js:管理全部mutation 事件类型(type)--字符串常量
- actions.js:处理异步操做和修改、以及对mutation的封装
- getters.js:对获取的state 作一些映射
在state.js中定义singer数据
const state = { singer: {}, }; export default state;在mutation-types中定义字符串常量
// 定义一些字符串常量 export const SET_SINGER = 'SET_SINGER';在mutations.js中引入mutation-types做关联,并可对state进行修改
import * as types from './mutation-types'; const mutations = { [types.SET_SINGER](state, singer) { state.singer = singer; }, }; export default mutations;在getters.js中对state进行包装和输出
// 从state里取数据 export const singer = (state) => state.singer;初始化 index.js入口文件
import Vue from 'vue'; import Vuex from 'vuex'; // Vuex 内置日志插件用于通常的调试 import createLogger from 'vuex/dist/logger'; import * as actions from './actions'; import * as getters from './getters'; import mutations from './mutations'; import state from './state'; Vue.use(Vuex); // 只在开发环境时启动严格模式 const debug = process.env.NODE_ENV !== 'production'; export default new Vuex.Store({ actions, getters, state, mutations, strict: debug, plugins: debug ? [createLogger()] : [], });注意:在严格模式下,不管什么时候发生了状态变动且不是由 mutation 函数引发的,将会抛出错误。这能保证全部的状态变动都能被调试工具跟踪到。不要在发布环境下启用严格模式!严格模式会深度监测状态树来检测不合规的状态变动——请确保在发布环境下关闭严格模式,以免性能损失。
在main.js中引入Store
import store from './store'; new Vue({ router, store, render: (h) => h(App), }).$mount('#app');在singer.vue中调用mapMutations做对象映射,传递参数singer
import { mapMutations } from 'vuex'; ...mapMutations({ setSinger: 'SET_SINGER', }), selectSinger(singer) { this.$router.push({ path: `/singer/${singer.id}`, }); this.setSinger(singer); },在singer-detail.vue中经过引入mapGetters,取到vuex中存储的singer数据
import { mapGetters } from 'vuex'; computed: { ...mapGetters([ 'singer', ]), }, created() { ... console.log(this.singer); },在singer.js获取歌手歌曲接口数据:
export function getSingerDetail(singerId) { return axios.get(`/api/artists?id=${singerId}`); }在singer-detail中引入getSingerDetail方法和ERR_OK常量,并在methods中调用该方法获取singer相关歌曲数据
import { getSingerDetail } from '../../api/singer'; import { ERR_OK } from '../../api/config'; methods: { _getDetail() { // 用户刷新时,mapGetters获取到的singer为空 须要回退歌手列表页 if (!this.singer.id) { this.$router.push('/singer'); return; } getSingerDetail(this.singer.id).then((res) => { if (res.status === ERR_OK) { this.songs = this._normalizeSongs(res.data.hotSongs); console.log(this.songs); } }); },注意:只有从singer页面选择歌手跳转到对应singer-detail路由中,才能获得singer数据。当用户刷新时,歌手详情页面将会自动返回到歌手列表页面
一样须要对获取到的歌手相关歌曲数据进行封装处理,建立song.js,构造一个Song类
export default class Song { // song的id,mid,歌手,歌曲名name,专辑名album,歌曲长度duration,歌曲图片img,歌曲的真实路径url constructor({ id, singer, name, album, duration, image, url, }) { this.id = id; this.singer = singer; this.name = name; this.album = album; this.duration = duration; this.image = image; this.url = url; } }
在song.js中处理musicData数据:
export function createSong(musicData) { return new Song({ id: musicData.id, singer: filterSinger(musicData.ar), name: musicData.name, album: musicData.al.name, duration: (musicData.dt / 1000) | 0, image: musicData.al.picUrl, url: `https://music.163.com/song/media/outer/url?id=${musicData.id}.mp3`, }); } function filterSinger(singer) { let ret = []; if (!singer) { return ''; } singer.forEach((s) => { ret.push(s.name); }); return ret.join('/'); }在singer-detail.vue中调用createSong方法,将处理好的数据赋值给this.songs
_normalizeSongs(list) { let ret = []; list.forEach((item) => { if (item.id && item.al.id) { ret.push(createSong(item)); } }); return ret; },建立music-list.vue
<div class="music-list"> <div class="back"> <i class="icon-back"></i> </div> <h1 class="title" v-html="title"></h1> <div class="bg-image" :style="bgStyle"> <div class="filter"></div> </div> </div> export default { name: 'music-list', props: { bgImage: { type: String, default: '', }, songs: { type: Array, // eslint-disable-next-line vue/require-valid-default-prop default: [], }, title: { type: String, default: '', }, }, }在singer-detail中引入该组件,向该组件传递singer数据中的songs,name,avatar
<template> <transition name="slide"> <music-list v-if="show" :title="title" :bgImage="bgImage" :songs="songs"></music-list> </transition> </template> computed: { title() { return this.singer.name; }, bgImage() { return this.singer.avatar; }, ...mapGetters([ 'singer', ]), },
因为歌曲列表这一部分在后续开发页面(例如歌曲排行榜)中都会使用到,因此在这里将它看成一个基础组件进行开发
<div class="song-list"> <ul> <li v-for="(song, index) in songs" :key="index" class="item"> <div class="content"> <h2 class="name">{{song.name}}</h2> <p class="desc">{{getDesc(song)}}</p> </div> </li> </ul> </div>props: { songs: { type: Array, default: [] } }methods: { getDesc(song){ return `${song.singer} 。${song.album}` } }在music-list.vue中引用该组件和scroll组件
<scroll :data="songs" class="list" ref="list"> <div class="song-list-wrapper"> <song-list :songs="songs"></song-list> </div> </scroll> import Scroll from '../../base/scroll/scroll'; import SongList from '../../base/song-list/song-list';
music-list组件实现了列表能够往上滚动,也能够往下滚动;图片随着列表滚动实现缩小放大的效果。
music-list.vue完整代码:
music-list.vue1 <template> 2 <div class="music-list"> 3 <div class="back" @click="back"> 4 <i class="iconfont icon-back"></i> 5 </div> 6 <h1 class="title" v-html="title"></h1> 7 <div class="bg-image" :style="bgStyle" ref="bgImage"> 8 <div class="play-wrapper"> 9 <div class="play" v-show="songs.length > 0" ref="playBtn"> 10 <i class="icon-play"></i> 11 <span class="text">随机播放所有</span> 12 </div> 13 </div> 14 <div class="filter" ref="filter"></div> 15 </div> 16 <div class="bg-layer" ref="layer"></div> 17 <scroll @scroll="scroll" :probe-type="probeType" :listen-scroll="listenScroll" :data="songs" class="list" ref="list"> 18 <div class="song-list-wrapper"> 19 <song-list :songs="songs"></song-list> 20 </div> 21 <div class="loading-container" v-show="!songs.length"> 22 <loading></loading> 23 </div> 24 </scroll> 25 </div> 26 </template> 27 28 <script> 29 import Scroll from '../../base/scroll/scroll'; 30 import SongList from '../../base/song-list/song-list'; 31 import { prefixStyle } from '../../common/js/dom'; 32 import Loading from '../../base/loading/loading'; 33 34 const RESERVED_HEIGHT = 40; 35 const transform = prefixStyle('transform'); 36 export default { 37 name: 'music-list', 38 components: { 39 Scroll, 40 SongList, 41 Loading, 42 }, 43 props: { 44 bgImage: { 45 type: String, 46 default: '', 47 }, 48 songs: { 49 type: Array, 50 // eslint-disable-next-line vue/require-valid-default-prop 51 default: [], 52 }, 53 title: { 54 type: String, 55 default: '', 56 }, 57 }, 58 computed: { 59 bgStyle() { 60 return `background-image:url(${this.bgImage})`; 61 }, 62 }, 63 data() { 64 return { 65 scrollY: 0, 66 }; 67 }, 68 created() { 69 this.probeType = 3; 70 this.listenScroll = true; 71 }, 72 mounted() { 73 this.imageHeight = this.$refs.bgImage.clientHeight; 74 this.minTranslateY = -this.imageHeight + RESERVED_HEIGHT; 75 this.$refs.list.$el.style.top = `${this.imageHeight}px`; 76 }, 77 methods: { 78 scroll(pos) { 79 this.scrollY = pos.y; 80 }, 81 back() { 82 this.$router.back(); 83 }, 84 }, 85 watch: { 86 scrollY(newY) { 87 let translateY = Math.max(this.minTranslateY, newY); 88 let zIndex = 0; 89 // 图片放大 90 let scale = 1; 91 // 图片模糊 92 let blur = 0; 93 this.$refs.layer.style[transform] = `translate3d(0,${translateY}px,0)`; 94 const percent = Math.abs(newY / this.imageHeight); 95 // 图片往下拉时 96 if (newY > 0) { 97 scale = 1 + percent; 98 zIndex = 10; 99 } else { 100 blur = Math.min(20 * percent, 20); 101 } 102 // CSS高斯模糊属性 只有iphone看获得效果 103 this.$refs.filter.style['backdrop-filter'] = `blur(${blur}px)`; 104 this.$refs.filter.style['webkiBackdrop-filter'] = `blur(${blur}px)`; 105 // 滚到顶部时 106 if (newY < this.minTranslateY) { 107 zIndex = 10; 108 // 因为bgImage是宽高比,因此要先把paddingTop设为0 109 this.$refs.bgImage.style.paddingTop = 0; 110 this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px`; 111 this.$refs.playBtn.style.display = 'none'; 112 } else { // 还没滚动到顶部时 113 this.$refs.bgImage.style.paddingTop = '70%'; 114 this.$refs.bgImage.style.height = 0; 115 this.$refs.playBtn.style.display = ''; 116 } 117 this.$refs.bgImage.style.zIndex = zIndex; 118 this.$refs.bgImage.style[transform] = `scale(${scale})`; 119 }, 120 }, 121 }; 122 </script> 123 124 <style lang="scss" scoped> 125 .music-list { 126 position: fixed; 127 z-index: 100; 128 top: 0; 129 left: 0; 130 bottom: 0; 131 right: 0; 132 background: $color-background; 133 134 .back { 135 position: absolute; 136 top: 0; 137 left: 6px; 138 z-index: 50; 139 140 .icon-back { 141 display: block; 142 padding: 10px; 143 font-size: $font-size-large-x; 144 color: $color-theme; 145 } 146 } 147 148 .title { 149 position: absolute; 150 top: 0; 151 left: 10%; 152 z-index: 40; 153 width: 80%; 154 155 @include no-wrap(); 156 157 text-align: center; 158 line-height: 40px; 159 font-size: $font-size-large; 160 color: $color-text; 161 } 162 163 .bg-image { 164 position: relative; 165 width: 100%; 166 height: 0; 167 padding-top: 70%; 168 //设置旋转元素的基点位置 169 transform-origin: top; 170 background-size: cover; 171 172 .play-wrapper { 173 position: absolute; 174 bottom: 20px; 175 z-index: 50; 176 width: 100%; 177 178 .play { 179 box-sizing: border-box; 180 width: 135px; 181 padding: 7px 0; 182 margin: 0 auto; 183 text-align: center; 184 border: 1px solid $color-theme; 185 color: $color-theme; 186 border-radius: 100px; 187 font-size: 0; 188 189 .icon-play { 190 display: inline-block; 191 vertical-align: middle; 192 margin-right: 6px; 193 font-size: $font-size-medium-x; 194 } 195 196 .text { 197 display: inline-block; 198 vertical-align: middle; 199 font-size: $font-size-small; 200 } 201 } 202 } 203 204 .filter { 205 position: absolute; 206 top: 0; 207 left: 0; 208 width: 100%; 209 height: 100%; 210 background: rgba(7, 17, 27, 0.4); 211 } 212 } 213 214 .bg-layer { 215 position: relative; 216 height: 100%; 217 background: $color-background; 218 } 219 220 .list { 221 position: fixed; 222 top: 0; 223 bottom: 0; 224 width: 100%; 225 background: $color-background; 226 227 .song-list-wrapper { 228 padding: 20px 30px; 229 } 230 231 .loading-container { 232 position: absolute; 233 width: 100%; 234 top: 50%; 235 transform: translateY(-50%); 236 } 237 } 238 } 239 </style>
因为以前使用的QQ音乐API ko2部分接口失效,改换成网易云接口。
推荐页面接口(轮播图和热门歌单推荐):
import axios from './axios'; // import jsonp from '../common/js/jsonp'; export function getRecommend() { // const url = '/api/getDigitalAlbumLists'; // return jsonp(url); return axios.get('/api/banner'); } export function getDiscList() { return axios.get('/api/personalized'); }歌手列表接口:
import axios from 'axios'; export function getSingerList() { return axios.get('/api/top/artists'); }歌手歌曲接口:
export function getSingerDetail(singerId) { return axios.get(`/api/artists?id=${singerId}`); }须要优化的地方:
①采用网易云接口后发现获取到的歌手头像变形
解决办法:使用object-fit保持图片尺寸
在listview.vue中添加object-fit: cover;
.avatar { width: 50px; height: 50px; border-radius: 50%; // 保持原有尺寸比例 object-fit: cover; }②获取到的头像过大,致使加载过慢
解决办法:在singer.vue中每一个url后加 ?param=300x300实现压缩图片大小
③修改获取歌手姓名首字母方法,当getCamelChars()中传入的参数不是汉字时,不会进行转换,仍然输出源字符串
修改代码:
export function Getinitial(string) { let pinyin = require('js-pinyin'); pinyin.setOptions({ checkPolyphone: false, charCase: 0 }); // getCamelChars()中传入的参数不是汉字时,不会进行转换,仍然输出源字符串。 return pinyin.getCamelChars(string).substring(0, 1).toUpperCase(); }