建立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(); }