移动端音乐WebApp
版本:Node– v10.15.0Vue– 2.5.2目录所有目录src:├─api->放置和后端请求相关的代码,包括ajax等├─base│├─confirm->删除时候的提示框,是否真正删除│├─listview->歌...
·
这篇文章是写给自己今后复习用的笔记,如果有哪些不懂的,可以私信,不喜也勿喷,感谢阅读~~~
github源码地址:Music
慕课原课程地址
版本:
Node – v10.15.0
Vue – 2.5.2
部分功能
目录
- 所有目录
src: ├─api ->放置和后端请求相关的代码,包括ajax等 ├─base │ ├─confirm ->删除时候的提示框,是否真正删除 │ ├─listview ->歌手列表的核心组件(singer中使用) │ ├─loading ->等待数据加载时显示的组件(就是一个转圈圈的图案) │ ├─no-result ->搜索结果不存在时候显示的组件 │ ├─progress-bar ->播放器页面的进度条 │ ├─progress-circle ->mini播放器的播放按钮 │ ├─scroll ->用于给组件实现滚动 │ ├─search-box ->搜索框组件 │ ├─search-list ->根据搜索结果显示的搜索列表 │ ├─slider ->轮播图组件 │ ├─song-list ->只用于显示所有歌曲(只要获取歌曲就需要这个组件) │ ├─switches ->最近播放/搜索历史 │ └─top-tip ->成功添加歌曲提示 ├─common ->放置静态资源,像图片,字体等 │ ├─fonts │ ├─image │ ├─js │ ├─scss │ └─stylus ├─components ->公共组件 │ ├─add-song ->添加歌曲到队列 │ ├─disc ->歌单详情页 │ ├─m-header ->首页顶部 │ ├─music-list ->歌手详情页主要组件(在singer-detail中被使用) │ ├─player ->播放器页面(大的播放器,mini播放器) │ ├─playlist ->mini播放器右下角的歌曲列表 │ ├─rank ->所有榜单页面 │ ├─recommend ->歌单页面 │ ├─search ->整个搜索页面的实现 │ ├─singer ->所有歌手列表页(引用listview) │ ├─singer-detail ->歌手详情页(引用music-list) │ ├─suggest ->搜索页面核心组件,数据获取等 │ ├─tab ->导航栏(歌手,排行等) │ ├─top-list ->榜单详情页面 │ └─user-center ├─router ->路由相关文件 ├─store ->vuex相关代码 ├─main.js ->入口相关文件 └─App.vue index.html ->添加meta标签,移动端常见设置 package.json ->添加了 ->"fastclick": "^1.0.6":解决移动端点击300毫秒延迟 ->"babel-runtime": "^6.26.0",:对es语法进行转义 ->"babel-polyfill": "^6.26.0",对es6api进行转义,例如promise等
项目初始化
- 解决300毫秒延迟的问题
src/main.js目录: import fastclick from 'fastclick' fastclick.attach(document.body)
- fastclick解决其他问题
https://segmentfault.com/a/1190000003848737
https://www.jamescathy.top/p/113.html
- needsclick
- fastclick解决其他问题
- 别名的配置:在webpack.base.conf.js下配置
- common:src/common
resolve: { alias: { 'common': resolve('src/common'), } } function resolve (dir) { return path.join(__dirname, '..', dir) }
- 同理,也可以多配制几个别名
module.exports = { resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', 'src': resolve('src'), 'api': resolve('src/api'), 'base': resolve('src/base'), 'common': resolve('src/common'), 'components': resolve('src/components'), 'router': resolve('src/router'), 'store': resolve('src/store') } }, }
- common:src/common
- 改变默认的样式
- 背景色:src/common/stylus/variable.styl的background
- 图标:src/components/mHeader
- 与本项目有关的数据的获取:大部分利用jsonp方式
- jsonp原理:
利用动态创建一个script标签,因为script标签是没有同源策略限制的,然后把script的src指向我们请求支持的服务端地址,地址后面有个callback函数 - jsonp使用:
- 安装: npm i jsonp
- 封装一个jsonp方法:src/common/js/jsonp.js(具体见代码注释)
- 封装具体的获取数据(包含实际的的url和请求参数)的jsonp的方法:
src/api/recommend.js(具体见代码注释) - 在组件中调用方法获取数据:src/components/recommend.vue
src/components/recommend.vue 1. 引入:import { getRecommend} from 'api/recommend' import { ERR_OK } from 'api/config' 2. 创建方法 methods:{ _getRecommend(){ getRecommend().then((res) => { 不是直接使用数字,是为了语义化更好 if(res.code == ERR_OK){ console.log(res.data.slider); } }) }, } 3. 在生命周期函数created时候执行,获取数据 created(){ this._getRecommend() },
- 参考网址:
https://github.com/webmodules/jsonp
- jsonp原理:
- vuex
- 用state管理状态,详情都见store文件夹,都有注释
actions.js 对mutation进行封装 getters.js 对状态获取的封装 mutation-types.js 保存一些常量(mutations中函数的函数名) mutations.js 用于更改状态(state中的数据) state.js 用于存储状态信息,管理vuex数据的 vuex.js 引入以上所有文件
- 举个栗子:调用vuex存储数据
1. 在main.js中引入import store from './store' 2. 在main.js中注册 new Vue({ store }) 3. 在src/components/singer中使用 import {mapMutations} from 'vuex' methods:{ // 经过这个映射,在代码中就可以调用this.setSinger ...mapMutations({ setSinger: 'SET_SINGER' }) }
- 用state管理状态,详情都见store文件夹,都有注释
header部分
- 创建组件src/components/mHeader
- 在src/App.vue中引入
src/App.vue中引入 1. import MHeader from 'components/mHeader/mHeader' 2. components: { MHeader, } 3. 在组件中引入 <template> <m-header></m-header> </template>
推荐(recommend),歌手(singer),排行(rank),搜索(search)
- 创建组件(src/components下):
- 推荐(src/components/recommend.vue),
- 歌手(src/components/singer.vue),
- 排行(src/components/rank.vue),
- 搜索(src/components/search.vue)
- 在src/router/index.js下,创建这些组件对应的路由并注册
import Router from 'vue-router' import Recommend from 'components/recommend/recommend' import Singer from 'components/singer/singer' import Rank from 'components/rank/rank' import Search from 'components/search/search' Vue.use(Router) export default new Router({ routes:[ {path: '/', redirect:'/recommend'}, {path: '/recommend', component: Recommend,}, {path: '/singer', component: Singer,}, {path: '/rank', component: Rank,}, {path: '/search', component:Search,} ] })
- 在src/main.js添加路由文件,并注册到实例身上
import router from './router' new Vue({ router })
- 添加router-view到src/App.vue
- src/components/tab下引入router-link
<div class="tab"> // tag:将router-link渲染成什么标签 <router-link tag="div" class="tab-item" to="/recommend"> <span class="tab-link">推荐</span> </router-link> <router-link tag="div" class="tab-item" to="/singer"> <span class="tab-link">歌手</span> </router-link> <router-link tag="div" class="tab-item" to="/rank"> <span class="tab-link">排行</span> </router-link> <router-link tag="div" class="tab-item" to="/search"> <span class="tab-link">搜索</span> </router-link> </div> 样式:当跳转到对应路由的时候,当前路由显示高亮样式 &.router-link-active { .tab-link { color: $color-theme; border-bottom: 2px solid $color-theme; } }
- src/App.vue下引入tab组件
1. import Tab from 'components/tab/tab' 2. components: { MHeader, Tab, } 3. 在组件中引入 <template> <m-header></m-header> <tab></tab> <router-view></router-view> </template>
- 默认显示推荐组件recommend.vue
- router/index.js中routes下配置{path: ‘/’, redirect:’/recommend’},
推荐(recommend)
轮播图组件
- src下创建base目录,base下创建silder文件夹,然后创建silder.vue组件(具体见代码注释)
- 在components/recommend.vue文件中引入轮播图组件
1. 引入:import Slider from 'base/slider/slider' 2. components: { Slider, }, 3. 在template中添加插件 <div class="slider-wrapper"> <slider> <div v-for=""> </div> </slider> </div>
- 渲染轮播图组件
- jsonp获取数据成功后,用recommends接收数据
data(){ return{ recommends: [], } }, if(res.code == ERR_OK){ // 当jsonp获取数据成功后,用数组接收数据,然后用来渲染轮播图组件 this.recommends = res.data.slider; }
- 用得来的数据渲染轮播图组件
<slider> <div v-for="item in recommends" :key="item.id"> <a :href="item.linkUrl"> <img :src="item.picUrl"> </a> </div> </slider>
- jsonp获取数据成功后,用recommends接收数据
- 轮播图逻辑:见src/base/slider.vue
- 在slider.vue中使用better-scroll:
官网:https://github.com/ustbhuangyi/better-scroll https://ustbhuangyi.github.io/better-scroll/doc/zh-hans/ 1. 引入:import BScroll from 'better-scroll' 2. 定义相关方法 3. 在生命周期函数mounted时候执行方法 4. 使用scroll的时候一定一定要注意: 必须保证当前 能得到组件的正确高度才能滚动!!!!
- 完成轮播图需要实现的功能:
- 正确计算每个子元素宽度,保证样式正确,能够正确获取整个轮播图的图片总宽度
举个栗子,比如说5个图片的循环轮播图屏幕宽度 100px 设置子元素宽度:100px 设置父元素宽度:100 * 5 + 2 * 100 = 700px
- 实现轮播图下面点dots的问题,怎么确定当前页,并且更改的点的样式
设置样式:class="{ active: currentPageIndex === index }" 通过维护一个变量:currentPageIndex(默认值为0),实现轮播图和dots相对应 // 轮播图每次在切换下一张图片,会触发一个scrollEnd事件 this.slider.on('scrollEnd', () => { // better-scroll scrollEnd trigger let pageIndex = this.slider.getCurrentPage().pageX // pageIndex: stop this.currentPageIndex = pageIndex })
- 设置轮播图属性,例如:是否能循环播放,间隔等
- 自动播放方法autoPlay实现,
_autoPlay() { this.timer = setTimeout(() => { this.slider.next(); }, this.interval) }
- 正确计算每个子元素宽度,保证样式正确,能够正确获取整个轮播图的图片总宽度
- 在slider.vue中使用better-scroll:
- 出现问题:报错
- 单纯报错
原因:见src/base/slider和src/components/recommend组件 因为我获取数据执行周期是created,而且是异步的 数据渲染的执行周期是mounted 但是此时可能还没有 得到数据,所以会报错 解决:src/components/recommend中的slider组件,添加如下逻辑 v-if="recommends.length"
- 轮播到第一页就停止了
原因:只调用了一次autoplay 解决:在每次scrollEnd事件触发的时候就调用一次 if (this.autoPlay) { clearTimeout(this.timer) this._autoPlay() }
- 轮播图滑动不连续,轮播图小点不在图片下面,在整个页面最底部
better-scroll源码有更新,现在的使用方法,和原来的有差异 小点父元素position没有定位,相对的元素是body,所以在页面最底部
- 轮播图自动播放到最后一页就停了,
新版api改变了,或者就继续用这种方法,或者用next,不过动画效果没有了
- 手机端,电脑端切换时候轮播图宽度出现错误
监听"窗口改变"事件,当改变时候重新计算宽度 但是每次重新计算时候,_setSliderWidth中的width都会width += 2 * sliderWidth,这样是不对 所以,我们在_setSliderWidth中传入一个参数,判断此时方法的执行是不是重新resize过来的,然后再重新渲染轮播图 剩余详细见代码注释src/base/slider
- 在手机模式下,点击轮播图,不能进行页面跳转
由于better-scroll传了一个参数是click为true 然后它在better-scroll内部实现的时候,会阻止浏览器默认的click 然后自己派发一个click,然后,它派发的click恰好又被fastClick这个库监听到 ,然后fastClick又阻止了这个事件,导致click不能被执行, 解决:就是删掉click:true,因为a链接本身就能实现跳转,所以不需要监听click
- 当我们推荐页面切换到歌手页面,再切回来的时候,会出现闪烁的效果
原因:我们每切换一次的话,都会重新发送一个数据请求,所有dom都会重新渲染, 生命周期都会重新走一遍获取数据,然后重新初始化slider,体验不好,且没有必要 解决:给App.vue中添加keep-alive,这样就可以将他们的dom都缓存到内存中 这样就不会多发送请求,其次也不会有闪的效果 <keep-alive> <router-view></router-view> </keep-alive> keep-alive:https://segmentfault.com/a/1190000010546663
- 当slider被切走的时候实际上会调用destroyed(),然后在这里把定时器等资源进行清理,有利于内存的释放
- 单纯报错
歌单部分(src/components/recommend.vue包含歌单)
- 数据的抓取见上面的jsonp
- 在src/api/recommend.js中封装获取歌单数据的jsonp的方法
- 在src/components/recommend.vue中调用歌单数据
- 用这种方法报错
- 问题:用1的方法获取数据失败
- 原因:
因为有一个host和referer的限制,前端之间不能修改request-header的,通过后端代理的方式处理 那么后端代理怎么做呢?
- 解决:
开发vue时会启动一个server,这个server就是nodejs气的devServer的逻辑 那么需要我们手动代理这样的请求 1. 在build/webpack.dev.conf.js下写服务器代理 第一步 const express = require('express') const app = express() // 接口代理绕过主机和引用程序 // browser: XMLHttpRequst, node.js: http const axios = require('axios') var apiRoutes = express.Router() // 最后一步,将api注册使用 app.use('/api', apiRoutes) 第二步 在devServer中配置接口,因为直接请求会出错,referer和host不一样,不让请求 通过axios该改变headers之后带着浏览器发送过来的参数,重新发送请求 2. src/api/recommend.js封装获取歌单数据的ajax的方法 见webpack.dev.conf.js文件和src/api/recommend.js,有详细注释
- 补充:大公司是怎么防止我们抓取数据的呢?
可以设置:获取数据时候带一个独有的签名,我才给你数据
- 原因:
- 数据的渲染,见src/components/recommend.vue,其实和轮播图渲染数据一样的
- 滚动部分的优化:
- 想实现可滑动,但是总写很多次better-scroll很麻烦,单独封装成一个单独的组件
那我就将可滑动的部分封装成一个单独的组件,然后将所有可滑动的部分都放scroll组件中去:src/base/scroll.vue better-scroll详细解释在src/base/slider.vue中有
- 什么时候应该重新计算refresh
(1)在src/components/recommend.vue组件中,为什么向scroll组件中传的是discList,而不是recommends? 因为轮播图接口获取的数据优先于歌单获取的数据,就是当歌单获取的discList, 再调用refresh的时候轮播图部分高度已经撑开了,也就是说scroll是可以正确计算到高度的 若是我们延迟1s获取轮播图数据,那么就是discList先获取到,然后计算完scroll的高度之后,recommends才获取到数据 那么造成的问题就是,我们的歌单下面有一部分滚动不了,因为它把轮播图的高度算在了歌单scroll高度中 那么歌单部分就有差不多轮播图高度的部分不能滑动 那么说明,我refresh时机不对,不应该是watch到data改变之后refresh() (2)那么什么时候重新计算滚动区高度也就是refresh呢? 在图片加载完,将上面的部分撑起来了,这个时候我才refresh, 但是我不需要等到所有图片加载完,只要一个图片撑开就可以了, 所以设置一个变量记录this.checkloaded
- 想实现可滑动,但是总写很多次better-scroll很麻烦,单独封装成一个单独的组件
- 图片懒加载:src/main.js和src/components/recommend.vue
官网:https://github.com/hilongjw/vue-lazyload 1. 安装:npm i vue-lazyload 2. src/main.js中引入组件 3. 注册 Vue.use(VueLazyload, { loading: require('common/image/default.png') }) 4. 图片路径不用src,用v-lazy 5. 最好看一下lazyload源码
- 问题:src/components/recommend.vue
better-scroll和fastclick冲突时候,可以利用class="needsclick"解决 就是当fastclick监听到某个dom结点上的点击事件的时候,发现有class="needsclick" 它就不会手动拦截这个过程,详细见recommend.vue
- loading组件,就是当歌单列表没有获取来之前,展示一个转圈的loading
见src/base/loading和src/components/recommend.vue
- 歌单样式:看看,很经典的flex布局
歌手组件
- 抓取歌手
- 封装获取数据的jsonp的方法:src/api/singer.js(具体见代码注释)
- 获取数据:在src/components/singer.vue中,具体见此文件中的注释
data() { return { singers: [] } }, created() { this._getSingerList() // get singer data }, methods:{ _getSingerList() { getSingerList().then(res => { if (res.code === ERR_OK) { console.log(res.data.list); } }) }, }
- 数据处理:src/components/singer.vue
- 将数据分类:
1. 热门数据,(前10条数据) 2. 按字母分类的数据('A', 'B', 'C' ......)
- 因为获取数据过程中,有很多重复的操作或者数据,所以需要优化:单独封装一个歌手类,见src/common/singer.js
- 将数据格式化:将数据处理成我们需要的数据格式
1. 先格式化为 { hot:{ title:'hot' item:[{},{},{}], }, A:{ title:'A' item:[{},{},{}], } ... ... } 2. 然后:[{},{},{},{}] 第一个对象是热门数据,后面依次按字母表排列的对象
- 将数据分类:
- 数据渲染:
将歌手列表单独封装成一个组件src/base/listview.vue,数据由父组件传递,就是上面格式化好的数据 然后在src/components/singer.vue中引入组件
快速入口组件,字母列表(src/base/listview.vue)
- 根据title,得到字母集合
computed: { shortcutList() { // ['A','B','C'...] return this.data.map(group => { return group.title.substr(0, 1) }) },
- 绑定事件,当触摸字母列表时候可以实现一些功能
- 点击某个字母,左侧可以直接定位到这个字母下的歌手名(详细见src/base/listview.vue)
better-scroll本身有个api就是scrollToElement,可以滚动到某个元素 (1)先将每个字母绑定一个属性data-index,用来唯一标识字母 (2)再给每个字母绑定方法,touchstart触发"onShortCutTouchStart"方法 (3)"onShortCutTouchStart":获取当前字母data-index (封装了一个单独的类(src/common/js/dom.js)用来获取data-index) (4)scrollToElement跳到左侧这个索引指定位置
- 指尖滑动时候,左侧也跟着一起滑动:通过计算pageY值
记录手指开始的位置,和手指挪到的位置,中间差值除以每个字母设定的高度值 然后跳到指定位置
- 左右联动效果:左侧滚动,右侧字母表有高亮的效果
- 先在src/base/scroll/scroll.vue中添加相应属性及方法
// 要不要监听滚动事件 props: { listenScroll: { type: Boolean, default: false }, }, methods: { _initScroll() { if (this.listenScroll) { const that = this // 监听scroll的滚动事件,并且拿到位置,也就是事件的回调 this.scroll.on('scroll', pos => { that.$emit('scroll', pos) }) } } }
- src/base/listview中的相应组件传递给子组件值,并且获取子组件传来的值
<scroll @scroll="scroll" :listenScroll="listenScroll"></scroll> 然后大致思路就是根据手指的位置,计算当前在哪个字母下 然后让右侧该字母高亮,详细将代码listview.vue
- 当歌手列表滚动到最顶部的时候将滚动逻辑进行优化,src/base/listview.vue
分为三种情况 (1) 当滚动到最顶部的时候 (2) 当滚动到中间部分 (3) 当滚动到最底部的时候
- 先在src/base/scroll/scroll.vue中添加相应属性及方法
- 点击某个字母,左侧可以直接定位到这个字母下的歌手名(详细见src/base/listview.vue)
- 滚动固定标题实现src/base/listview.vue
- 实现
<div class="list-fixed" ref="fixed"> <!-- list-fixed使用了绝对定位,所以会定到那里-- > <h2 class="fixed-title">{{fixedTitle}}</h2> </div> fixedTitle() { if (this.scrollY > 0) { return '' } return this.data[this.currentIndex] ? this.data[this.currentIndex].title : '' }
- 问题:
- 当我们滑动到最底部,又滑回到最顶部的时候,会出现两个热门的title,这样是不对的
解决:当 当前滑动的scrollY为正数时,设置fixedTitle为'', 然后上面的list-fixed结构,添加v-show,如果fixedTitle为空就不显示
- 当滑动时候,两个fixedTitle相遇后的效果不好
是下面那个慢慢上去,然后一点一点覆盖的,这样的体验是不好的解决:通过计算两个title的差值diff, 当差值大于0小于title,就让上面的title偏移差值距离 否则,什么都不变, 就会实现下一个往上顶的效果
- 当我们滑动到最底部,又滑回到最顶部的时候,会出现两个热门的title,这样是不对的
- 实现
- 出现其他小问题和优化:
- 手指滑动,左侧跟着动,但是滑动位置不对,索引相加的时候没有转化为整型,记得加上一个parseInt
- 写字母高亮功能的时候,左侧计算出来的索引位置不对,因为src/base/scroll/scroll.vue中设置probeType:1,改为3就好了
- 直接点击右侧字母表,并不会高亮
因为直接点击并不会触发滚动事件,所以不会派发一个pos事件,我们这里手动设置 this.scrollY = -this.listHeight[index],实现高亮
- 点击字母表上下两个区块的时候是有问题的,是可以点击的,然而我们不想让那部分可点击
所以加上判断,如果点击部分为null那么不做任何事情,返回 if (!index && index !== 0) { // click on the blank return }
- 当拖动时候,拖动到顶部,或者拖动到底部时候做一个边界处理
- 整体思路:5-8的6:36部分重新捋了一下
- 数据没有请求到的时候,有一个loading的效果
(1)先引入import loading from 'base/loading/loading' (2)注册组件 components: { loading }, (3)如果没有数据,就显示这个组件 <div class="loading-container" v-show="!data.length"> <loading></loading> </div>
- 难点:左右联动
根据左边滚动位置,计算在哪个group区间,然后就知道右侧对应哪个索引
歌手详情页面组件src/components/singer-detail
- 跳转路由至歌手详情页
- 添加路由:先在router/index.js中引入,添加路由
import SingerDetail from 'components/singer-detail/singer-detail' { path: '/singer', component: Singer, children: [ { // 以id为变量,可以传入不同的id值,然后去渲染不同的歌手详情页 path: ':id', component: SingerDetail, } ] }
- router-view:写在src/components/singer.vue中
- 点击歌手跳转到详情页
- 在src/base/listview.vue中添加事件
当某个歌手被点击了,向外派发一个事件,告诉外部我被点击了,并且被点击的元素是什么 selectItem(item) { this.$emit('select', item) },
- 父组件src/components/singer.vue接受子组件传来的值,然后利用this.$router.push,跳转到该歌手页面
- 路由跳转动画效果
在src/components/singer-detail上给路由加上动画效果 <transition name="slide"> <div class="singer-detail"></div> </transition> .slide-enter-active, .slide-leave-active { transition: all .3s; } .slide-enter, .slide-leave-to { transform: translate3d(100%, 0, 0); }
- 在src/base/listview.vue中添加事件
- 添加路由:先在router/index.js中引入,添加路由
- 使用vuex管理状态,将歌手存进去
- 存数据src/components/singer.vue
import {mapMutations} from 'vuex' ...mapMutations({ setSinger: 'SET_SINGER' })
- 取数据:src/components/singer-detail.vue
import { mapGetters } from 'vuex' ...mapGetters('singer')
- 存数据src/components/singer.vue
- 抓取数据
- 利用jsonp抓取数据,见src/api/singer.js中getSingerDetail,有详细注释
- 在src/components/singer-detail中获取数据
(1)先引入 import { getSingerDetail } from 'api/singer' import { ERR_OK } from 'api/config' (2)定义方法_getDetail,然后再created生命周期时候执行
- 获取歌曲的vkey(只有得到vkey才能获取歌曲播放地址)
1. 在build/webpack.dev.conf.js中配置获取vkey的接口,更改referer和host 2. 在src/api/song.js中定义获取全部歌曲的方法getMusic,调用接口/api/music 3. 在src/components/singer-detail中使用该方法 获取每个歌曲的vkey放到song对象中,用于获取歌曲播放地址 getMusic(musicData.songmid).then((res) => { // 这里需要先获取vkey if (res.code === ERR_OK) { const svkey = res.data.items const songVkey = svkey[0].vkey const newSong = createSong(musicData, songVkey) ret.push(newSong) } })
- 封装歌曲数据
因为有很多组件都需要整个歌曲的数据,所以将歌曲的数据封装成一个单独的src/api/song.js文件,用来获取歌曲的数据 见src/common/js/song.js有详细注释 1. 先创建一个歌曲类Song 2. 每次都需要new,然后传进去很多参数,很麻烦,所以,再新建一个方法createSong,在这里返回一个new的实例对象 实际上这也是一种工厂方法模式,就是不直接new,是调用一个方法,返回一个实例对象 3. 在src/components/singer-detail中使用
歌手详情页面核心页面(可以复用):src/components/music-list.vue
- 在singer-detail.vue中引入并传入数据
import MusicList from 'components/music-list/music-list' <music-list :songs="songs" :title="title" :bg-image="bgImage"></music-list> components: { MusicList },
- 写dom结构,完善歌手详情页面
- 将显示歌曲列表的部分封装成一个单独的组件src/base/song-list.vue,因为有很多地方都需要用
- 还有一点就是,把这个歌曲列表放到scroll组件中,因为歌单列表也是需要滚动效果的
- 在滑动歌曲列表时,想要实现在手指往上滑的时候,歌曲列表是要适当覆盖最上面歌手图片的
(1) 先将scroll组件样式中的overflow: hidden去掉 (2) 只滚上去是不够的,要实现当手指往上滑的时候,字的后面背景部分(不是图片,是灰色背景)也要一起向上滚 在scroll组件上面放一个div,然后监听滚动距离,然后让这个div和字一起滚动 <div class="bg-layer" ref="layer"></div> this.minTransalteY = -this.imageHeight scrollY(newVal) { // newVal:整个滑动部分相对于初始位置的偏移 // 手指往上滑为负,往下滑为正 // 滑动到最底部,pos值为"负值绝对值"最大的时候 const translateY = Math.max(this.minTransalteY, newVal) this.$refs.layer.style[transform] = `translate3d(0,${translateY}px,0)` }
- 但是,当向上滑了一段距离之后,就出现问题了
因为bg-layer高度设置的是100%,也就是屏幕高度 所以当手指滑动过屏幕高这么多距离之后,这个bg-layer就滑走了,就没有背景颜色显示了 将最上方图片的高度记录下来,然后设置bg-layer最多滚动的距离不超过图片的高度 代码与上面一样,不过minTransalteY的值进行了更改 this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT
- 但是我们要实现这样的效果:就是不让歌曲列表滚动到顶部,顶部预留一些地方来
那么只要调整bg-layer背景的最多偏移量就可以了,就是设置最多偏移量小一点 this.minTransalteY = -this.imageHeight + RESERVED_HEIGHT, // bg-layer minTransalteY
- 当向上滚动的时候,顶部的歌词名字覆盖在了图片的上面,我想要实现的效果是图片覆盖在字上面
并且更改图片的zIndex值和paddingTop值和width值,因为图片的大小我是通过设置宽高比的,见该页面的scss文件 如果没滚动到顶部的时候,一切恢复原样(图片的高度),就是将图片的z-index值和height等值改回来 if (newVal < this.minTransalteY) { // scroll to top滚动到顶部 zIndex = 10 this.$refs.bgImage.style.paddingTop = 0 this.$refs.bgImage.style.height = `${RESERVED_HEIGHT}px` } else { this.$refs.bgImage.style.paddingTop = '70%' this.$refs.bgImage.style.height = 0 } this.$refs.bgImage.style.zIndex = zIndex
- 当往底下拉的时候,我想要实现图片跟随者放大或者缩小的效果
当往底下拉的时候,通过percent = newY/this.imageHeight来设置图片放大的比例 当new>0的时候,设置放大比例scale为scale = 1 + percent,并且zIndex为10, 这样保证图片放大的时候,歌单列表不会覆盖图片 为什么是这个比例呢?因为当往下拉的时候,以这个比例,图片增加的高度就是newY的大小 const percent = Math.abs(newVal / this.imageHeight) if (newVal > 0) { // scroll down scale = 1 + percent // 如果不设置zIndex = 10,那么当向下拉的时候,歌曲列表会覆盖图片 zIndex = 10 } this.$refs.bgImage.style[transform] = `scale(${scale})`
- 为什么图片放大的时候总是从头部开始放大,因为bg-image设置了transform-origin为top,所以图片放大的时候总是从头部开始放大
- 当我们手指往上滑的过程中,想要实现图片的模糊效果,并且,越往上,就越模糊
设置一个属性blur,代表模糊的程度 通过css3来设置模糊效果:this.$refs.filter.style[backdrop] = `blur(${blur}px)` const percent = Math.abs(newVal / this.imageHeight) if (newVal > 0) { // scroll down ... ... } else { //模糊程度最大是20 blur = Math.min(percent * 20, 20) }
- 因为不同浏览器前缀不一样,js中怎么自动加上呢?
css中不用写浏览器前缀的原因是因为,vue-loader用到了autoprefixer插件,会帮我们自动补全前缀 但是js中就不会了,就需要我们自己判断,那么我们可以封装一个方法来判断浏览器前缀是什么 见src/common/js/dom.js文件
- 随机播放全部按钮
首先要等得到歌曲列表数据之后才显示 其次,当我们手指向上滑动的过程中,因为按钮设置的位置是bottom为20,所以当图片高度改变了,按钮位置也改变了,但是我们要的效果是: 当手指滑动到最顶部的时候,整个按钮是消失的,所以 // 设置随机播放全部的按钮,在滑动到最顶端时候消失 this.$refs.playBtn.style.display = 'none' // 设置随机播放全部的按钮在正常情况下,display = '',让它正常显示 this.$refs.playBtn.style.display = ''
- 在没有获取到数据的时候显示loading
(1)引入import Loading from 'base/loading/loading' (2)注册组件 components: { Loading }, (3)在组件中应用 <div v-show="!songs.length" class="loading-container">s <loading></loading> </div>
显示歌曲列表的组件src/base/song-list.vue(会在music-list中使用)
- 很简单,就是从引入它的父组件获得歌曲列表所有值,然后在组件中进行渲染
- 问题:src/components/music-list中歌曲列表显示位置不对,直接跑到了最顶部
原因:scroll没有正确计算高度, 解决:将上面歌手背景图片的高度计算出来,然后让显示歌曲列表组件的top设置成图片高度
播放器页面src/components/player/player.vue(App.vue中引入)
- 数据的存放:考虑多个组件都可以操作播放器,所以控制播放器的数据一定是个全局的,所以通过vuex来管理
src/store/state.js 定义和组件相关的,最底层的数据 src/store/getter.js 对数据的映射,可以是一个函数,函数类似计算属性,根据state.js中的值计算出新的值 src/store/mutation-types.js 见代码,理清和mutations.js的关系 src/store/mutations.js 定义数据修改的逻辑,但是定义mutations之前,先定义mutation-types
- 显示播放器组件的流程:src/components/player/player(见代码)
- 在app.vue下引用src/components/player/player
因为不是和路由相关的组件,切换到任何组件都不影响播放 - 在app.vue下不能默认展示player.vue这个组件,要在src/components/player/player中引入vuex,
通过取vuex中的变量playlist,计算长度length,来控制player组件的显示,不能默认显示import { mapGetters } from 'vuex' ...mapGetters([ // 播放列表,当playlist的length>0之后才能显示播放器页面 'playlist', ]), <div class="player" v-show="playlist.length>0">
- 因为点击歌曲,才会触发这个组件,所以src/base/song.list组件要添加部分事件逻辑
当点击歌曲时候,触发事件selectItem,然后这个函数将触发的事件和当前歌曲索引传递给父组件src/music-list/music-list.vueselectItem(item, index) { this.$emit('select', item, index) },
- 为什么传递索引呢:因为播放歌曲列表的时候,要从当前歌曲开始播放,所以要获取索引
- 当点击歌曲列表的某首歌的时候要触发
- 设置playlist和sequencelist
- 根据点击的索引设置currentlist
- 设置playing,设置歌曲播放还是暂停的状态,因为点击的时候,歌曲要播放,
- 默认展开大的播放器,设置fullScreen
- 注意:因为要改变很多state,所以封装一个action(见src/store/action.js)
- 因为要改变state、索引,要使用action,所以要在src/components/music-list.vue中引入mapActions
import { mapActions } from 'vuex' ...mapActions(['selectPlay']), selectItem(item, index) { this.selectPlay({ list: this.songs, index }) // console.log(this.currentIndex); },
- 总结流程:
- 点击歌曲(src/base/song-list)
- 提交action(src/components/music-list)
- 提交mutation(src/store/action.js)修改state
- playlist.length > 0(src/components/player.vue)
- 然后显示player.vue组件
- 在app.vue下引用src/components/player/player
- 给播放器添加交互动画和样式
- 样式1(player.vue):
当切换展开的播放器或者缩小的播放器时有动画效果,样式详见player.scss- 给展开的播放器添加transition样式为normal
- 给缩小版的播放器添加transition样式为mini
- 样式2 (player.vue):
当由缩小版的播放器切换为展开版的播放器时,左下的CD封面也会由小拉到大的 动画
我们要通过js方式获取css动画,所以引入了create-keyframe-animation插件 因为设置动画需要获取参数,我们要定义的_getPosAndScale函数来获得相应参数 在enter中设置动画
- 样式3(player.vue):
当由放大版的的播放器切换为缩的播放器时,CD封面从中间移到左下角的实现
不过没有样式2那种动画,是直接用js操作css样式实现的
因为设置动画需要获取参数,我们要定义的_getPosAndScale函数
来获得相应参数在leave中设置动画 - 样式4(player.scss):
当别的页面切换为大的播放器页面的时候动画效果- 效果1:背景有一个渐隐渐现的效果,纯利用css3,transition实现的
- 效果2:头部和底部又一个回弹的效果,这个效果是利用贝塞尔曲线cubic-bezier实现的
- 给样式填充数据
- 填充数据
- 唱片图片:src=“currentSong.image”
- 歌曲名: v-html=“currentSong.name”
- 歌曲名称: v-html=“currentSong.singer”
- 添加简单方法
- 点击右上角的按钮实现返回:添加back方法,back方法中用Vuex的mutations设置fullScreen为false
- 点击小型播放器后显示大的播放器:在小型播放器样式上绑定@click=“open”,open方法中设置fullScreen为true
- 填充数据
- 样式1(player.vue):
- 播放功能:利用html5的audio实现
-
添加audio标签,当currentSong改变时候play方法执行
- 错误:
<audio ref="audio" :src="currentSong.url"></audio> watch: { currentSong(newSong, oldSong) { this.$refs.audio.play()//只写这一句是会报错的 } } 报错:The play() request was interrupted by a new request
- 原因:dom异常,这时候调用play时候,我们同时请求src是不可以的,这个dom还没有ready
- 解决:我们设置一个延迟
// 设置一个延迟 this.$nextTick(() => { this.$refs.audio.play() })
- 错误:
-
设置歌曲播放和暂停:
(1)在class名为i-center的dom元素上绑定方法togglePlaying,切换暂停或者播放状态 (2)togglePlaying() { this.setPlayingState(!this.playing) //setPlayingState: 获取更改state中playing的mutations方法 //playing:这个playing就是通过getters从vuex state获取的playing } (3)仅通过改变状态是不能控制歌曲的播放或者暂停的 我们可以watch这个playing,然后触发audio的play或者pause方法 但是因为我点击歌曲的时候,就会触发watch的playing属性 然后就会执行play,所以还是会出现上面的问题,解决方法也是一样,设置一个延迟
-
播放或着暂停的圆圈圈样式改变
1. 添加计算属性 playIcon() { return this.playing ? 'icon-pause' : 'icon-play' }, 2. 添加样式<i :class="playIcon" @click="togglePlaying"></i>
-
小的播放器图标的改变:miniIcon 同理 大播放器
注意:当点击小的播放器播放按钮的时候,会产生冒泡事件,需要阻止冒泡事件 <i class="icon-mini" :class="miniIcon" @click.stop="togglePlaying"></i>
-
播放时候CD封面转圈,停止的时候CD封面停止
- 样式:
&.play { animation: rotate 20s linear infinite; } &.pause { animation-play-state: paused; } @keyframes rotate { 0% { transform: rotate(0); } 100% { transform: rotate(360deg); } }
- DOM中添加样式:
<img class="image" :src="currentSong.image" :class="cdcls"/>
- 事件逻辑:
cdCls() { return this.playing ? 'play' : 'play pause' },
- mini播放器同理
- 样式:
-
点击播放下一首或者上一首歌曲
因为我们在state中记录了当前歌曲的索引,并且我们也有当前歌曲列表
所以向上或者向下只要改变索引就可以了(1)给播放下一首歌曲按钮绑定事件(播放上一首歌曲按钮同理) <div class="icon i-right"> <i @click="next" class="icon-next"></i> </div> (2)事件逻辑 next() { let index = this.currentIndex + 1 // 如果当前歌曲是最后一首歌 if (index === this.playlist.length) { index = 0 } this.setCurrentIndex(index) this.songReady = false },
- 问题1:点击按钮(播放上一首)后,发现歌曲虽然跳到了下一首,但是图标并没有,图标还是处于暂停状态
- 解决:在next方法下添加如下语句
if (!this.playing) { this.togglePlaying() }
- 解决:在next方法下添加如下语句
- 问题2:不停点击下一首那个按钮,会报问题1的错误(pre同理)
- 解决:查看audio官方文档,发现audio标签会派发两个事件,一个是canplay 和 error
(1)<audio ref="audio" :src="currentSong.url" @canplay="ready" @error="error"> (2)所以根据这个,我们data中设置一个标志位, data() { return { songReady: false, } } (3)然后 ready() { this.songReady = true this.savePlayHistory(this.currentSong) }, (4)然后next next(){ if (!this.songReady) { return } // 点击之后this.songReady = false,确保下一首歌曲准备好时 才可以点击也就是ready时候 this.songReady = false } (5)容错处理:因为只有ready的时候才会触发this.songReady = true, 如果报错就会阻止继续播放,所以我们这里有一个容错处理 当前歌曲播放不出来的话也可以实现点击下一首的功能 事件: error(){ this.songReady = true } 样式也要体现:当 当前歌曲出错的时候,图标有一个变灰的样式 <div class="icon i-left" :class="disableCls"> <i @click="pre" class="icon-prev" ></i> </div>
- 解决:查看audio官方文档,发现audio标签会派发两个事件,一个是canplay 和 error
- 问题1:点击按钮(播放上一首)后,发现歌曲虽然跳到了下一首,但是图标并没有,图标还是处于暂停状态
-
- 大的播放器底部进度条src/base/progress-bar/progress-bar.vue:
- 获取当前播放时间和总时间
(1)设置标签实现进度条<div class="progress-wrapper"> (2)因为当audio标签中歌曲播放的时候会派发一个事件就是@timeupdate="updateTime" (3)updateTime(e) { // update会传进来一个e的事件,这个事件有一个target属性就是audio标签 // 这个audio还有一个可以获取到当前播放时间的属性就是currenTime, // 这个currentTime是一个时间戳的形式,是可读写属性 this.currentTime = e.target.currentTime // <audio> current time }, (4)编写一个函数format,将currentTime格式化为分和秒的形式 // 给时间格式化的函数 format(interval) { //interval|0就是一个正数的向下取整 interval = interval | 0 // | 0: math.floor const minute = (interval / 60) | 0 // 当前播放时长我们要显示0.06不是0.6,也就是当余数为个位数的时候,需要一个补0的函数pad const second = this._pad(interval % 60) // _pad: Use 0 to fill 2 bits return `${minute}:${second}` }, (5)_pad(num, n = 2) { // _pad: use 0 to fill 2 bits // 获取字符串长度 let len = num.toString().length while (len < n) { num = '0' + num len++ } return num },
- 设置单独的组件显示进度条,接受父组件src/components/player/player.vue传过来的百分比的值
(1)根据百分比,求走过的距离,然后设置样式 (2)根据百分比,用transform设置小球的偏移
- 拖拽或者点击,小球实现歌曲的实时播放
@touchstart.prevent="progressTouchStart" @touchmove.prevent="progressTouchMove" @touchend="progressTouchEnd" progressTouchStart(){ 用一个变量init表示touch已经初始化了 记录touch的开始位置 记录当前进度条的的偏移距离 } progressTouchMove(e) { 手指移动的距离 最初的位置加上手指移动的距离,不能比0小,不能比进度条整个宽度大 设置进度条和小球偏移到该位置 }, progressTouchEnd(){ 设置init变量为false,表示结束,不能操作了 } 点击同理progressClick(e)
- 进度条的小球拖拽过程中,进度条会出现跳的情况
- 原因:当拖拽过程中,有两个事件改变进度条,
一个是拖拽事件,一个是歌曲播放事件
所以我们要设置拖拽过程中,拖拽事件权重更大一点 - 解决:if (newPercent >= 0 && !this.touch.init)
当没有拖拽事件的时候,才设置歌曲播放改变进度条
- 原因:当拖拽过程中,有两个事件改变进度条,
- 进度条的小球拖拽过程中,进度条会出现跳的情况
- 当拖拽完之后,进度条虽然同步了,但是已播放秒数还是没有改变
拖拽完之后,重新计算进度条的百分比,然后向父组件派发一个事件 父组件player监听percent的改变,然后改变audio的currentTime,然后进一步改变秒数 子组件: progressTouchEnd() { this._triggerPercent() }, _triggerPercent() { const progressBarWidth = this.$refs.progressBar.clientWidth - progressBtnWidth const percent = this.$refs.progress.clientWidth / progressBarWidth this.$emit('percentChange', percent) }, 父组件: onProgressBarChange(percent) { const currentTime = this.currentSong.duration * percent this.$refs.audio.currentTime = currentTime // 拖动完之后还要继续播放 if (!this.playing) { this.togglePlaying() } },
- 小的播放器转圈圈的播放进度条:src/base/progress-circle/progress-circle.vue:
引入:import ProgressCircle from 'base/progress-circle/progress-circle' 注册: components: { ProgressCircle, }, dom中使用 <progress-circle :radius="radius" :percent="percent"> <i class="icon-mini" :class="miniIcon" @click.stop="togglePlaying"></i> </progress-circle> 这个i标签会在progress-circle组件中的slot标签中生效 progress-circle里面放置了两个circle,一个是默认全部灰度圆,一个是设置偏移量,剩下部分高亮
- 获取当前播放时间和总时间
- 播放模式
- 样式的改变:
从state中引入mode变量,根据mode给dom结构增加样式, 点击切换模式时,引入mutations改变mode changeMode(){ const mode = (this.mode + 1) % 3 this.setPlayMode(mode) },
- 播放列表的改变
(1)如果是随机播放,需要将列表打乱,也就是洗牌,那么封装一个函数(src/common/js/util.js)完成洗牌 (2)引入setPlayList:'SET_PLAY_LIST',为了改变当前播放列表 如果是随机播放则将打乱(shuffle函数)的歌曲列表赋给playList 如果不是随机播放,那么playList就等于sequenceList (3)因为当前歌曲是根据歌曲列表和索引设置的,所以如果歌曲列表改变的话, 当前播放的歌曲也可能发生变化,所以我们要重新计算currentIndex resetCurrentIndex(list){ let index = list.findIndex((item) => { return item.id == this.currentSong.id }) this.setCurrentIndex = index; }
- 问题: 我把歌曲暂停了之后,点击切换播放模式,会发生歌曲还是继续播放的情况
- 原因:当我切换播放模式之后,因为重新计算歌曲index,也算是改变了currentSong,所以就会被watch监听到,就会继续播放歌曲
- 解决:
watch: { currentSong(newSong, oldSong) { if (newSong.id === oldSong.id) { return } } }
- 问题: 我把歌曲暂停了之后,点击切换播放模式,会发生歌曲还是继续播放的情况
- 当前歌曲播放完了之后,如果是循环模式就继续播放当前歌曲,如果不是那就播放下一首
audio标签:ended="end": 歌曲播放结束了会派发一个事件 end() { if (this.mode === playMode.loop) { // 如果是单曲循环那就调用loop函数 this.loop() } else { // 否则(顺序或者随机)就直接跳到下一首 this.next() } }, loop() { this.$refs.audio.currentTime = 0 this.$refs.audio.play() },
- 随机播放全部按钮实现:src/components/music-list.vue
1. 给按钮绑定事件:random random() { this.randomPlay({ list: this.songs }) } 2. 整个randomPlay是从action.js(src/store/action.js)中获取的
- 当我点击随机播放了之后,再去点击歌曲列表,就会出现,点击的歌曲和实际播放的歌曲不一样
- 原因:
当我点击随机播放全部了之后,此时playList列表已经变为了随机播放列表,原本是顺序列表的时候,playList和currentIndex确定了当前播放的歌曲, 但是现在playList已经变为了随机播放列表,所以不能获取到正确的歌曲
- 解决: 在actions的selectPlay中添加逻辑:
if (state.mode === playMode.random) { const randomList = shuffle(list) //经过shuffle后不改变原数组 commit(types.SET_PLAYLIST, randomList) index = findIndex(randomList, list[index]) } else { commit(types.SET_PLAYLIST, list) }
- 原因:
- 当我点击随机播放了之后,再去点击歌曲列表,就会出现,点击的歌曲和实际播放的歌曲不一样
- 样式的改变:
- 歌词(大部分在src/components/player.vue中完成)
- 获取歌词数据
(1)定义获取歌词数据接口src/api/song.js --->getLyric (2)在webpack.dev.conf.js中定义路由api/lyric,因为在src/api/song.js中是通过api/lyric来获取数据的 注意:因为得到的好像还是一个jsonp形式的数据,所以我们这里要做一个小小的处理 (3)在src/common/js/song.js中调用getLyric,把歌词和歌曲的其他数据都封装在一个song对象中,因为他们都是歌曲整体的一部分 (4)需要将数据解码,所以 利用插件base64 src/common/js/song.js中引入import { Base64 } from 'js-base64' 将得到的lyric解码 this.lyric = Base64.decode(res.lyric) (5)因为得到的是各一个很长的字符串,我们需要转化为我们需要的格式,所以引入插件lyric-parser 在player.vue中解码 引入:import Lyric from 'lyric-parser' 将歌词格式化,并且设置处理函数(this.handleLyric): this.currentSong.getLyric().then(lyric => { this.currentLyric = new Lyric(lyric, this.handleLyric) })
- 设置歌词高亮
(1) 因为当歌词每一行发生"改变"时,会触发this.handleLyric这个方法, 因为我们解析到的歌词是有很多数据的,包括每行歌词的开始时间, 所以说这里的改变是指随着时间改变,歌词的改变 这个方法handleLyric({ lineNum, txt }) 传入两个参数 // lineNum:当前行 // txt:当前行的歌词 (2)设置一个变量记录当前行this.currentLineNum this.currentLineNum = lineNum (3)在dom中遍历并显示所有歌词,如果遍历到的歌词等于当前行歌词,那就设置样式高亮显示 <p v-for="(line,index) in currentLyric.lines" :key="index" :class="{'current': currentLineNum === index}" > {{line.txt}} </p>
- 设置歌词可以滚动
引入scroll组件,然后传入数据:data="currentLyric && currentLyric.lines" 传入数据目的就是确保歌词数据存在,并且当歌词数据改变的时候调用scroll的refresh方法
- 保证当前歌词总在中间的位置上
handleLyric({ lineNum, txt }) { this.currentLineNum = lineNum if (lineNum > 5) { const lineEl = this.$refs.lyricLine[lineNum - 5] // 滚动到指定行,时间为1秒 this.$refs.lyricList.scrollToElement(lineEl, 1000) } else { // 滚动到指定位置,时间为1秒 this.$refs.lyricList.scrollTo(0, 0, 1000) } // this.playingLyric = txt },
- 歌词和唱片左右滑动效果
(1)先设置左右滑动的时候,底部小圆圈的样式 <span class="dot" :class="{'active':currentShow==='cd'}"></span> <span class="dot" :class="{'active':currentShow==='lyric'}"></span> (2)CD页面,往左滑时候,歌词列表页面可以滚过来,然后CD页面有渐隐的效果 先绑定事件,设定变量 在.middle"上绑定三个事件 @touchstart.prevent="middleTouchStart" @touchmove.prevent="middleTouchMove" @touchend="middleTouchEnd" 设置变量touch = {},关联touchdown和touchmove 事件逻辑:就那三个函数,自己看吧 包括:左滑右滑dom元素位置的变化 offsetWidth 中间动画的效果 duration dom元素渐隐渐现的的效果 opcity
- 当拖动进度条的时候,歌词部分高亮部分也要相应改变
// 歌曲拖动的时候,歌词高亮部分也相应改变 onProgressBarChange(percent) { if (this.currentLyric) { // currentTime:是秒形式的 this.currentLyric.seek(currentTime * 1000) } }
- 问题1:当切换很多歌曲的时候,会出现高亮部分一直在跳
- 原因:开启了多个计时器,没有关掉
- 解决:当监听到currentSong改变了之后,我们要关掉当前歌词的计时器
if (this.currentLyric) { this.currentLyric.stop() }
- 问题2:当歌曲暂停的时候歌词并没有暂停,还在继续走
- 解决:
当togg随机播放lePlaying的时候,将歌词也暂停 if (this.currentLyric) { // 将歌词暂停 this.currentLyric.togglePlay() }
- 解决:
- 问题3 循环播放的时候,歌曲播放完了,歌词并没有回到最开始
- 解决:
loop() { if (this.currentLyric) { // 将歌词偏移到最开始 this.currentLyric.seek(0) // <audio>, song jump to begin } },
- 解决:
- 问题1:当切换很多歌曲的时候,会出现高亮部分一直在跳
- 在CD封面下面会显示当前歌词这一行
创建显示结构 <div class="playing-lyric-wrapper"> <div class="playing-lyric">{{playingLyric}}</div> </div> 当歌词改变时候,给playingLyric赋值 handleLyric({ lineNum, txt }) { this.playingLyric = txt }
- 当获取不到歌词,做容错处理
this.currentSong.getLyric().then(lyric => { ...... }) // 当获取不到歌词的时候,变量都清空 .catch(() => { this.currentLyric = null this.playingLyric = '' this.currentLineNum = 0 })
- 获取歌词数据
- 其他问题
- 获取播放源错误:
https://blog.csdn.net/a151913232/article/details/85034283
+ 当只获取到一首歌的时候index = 0,我们点击next时候,下面这行语句执行let index = this.currentIndex + 1 //index = 1 if (index === this.playlist.length) { index = 0 //index = 0 } 也就是index的值并没有变,所以,watch监听到的currentSong也没变,也就是往后的语句都不会
- 执行解决:在next()和prev()上加个逻辑判断,当只获取到一首歌的时候,调用loop函数,转为单曲循环模式
- 当程序在微信端运行的时候,js是不执行的,所以audio只会把当前歌曲播放完,之后就不执行了
// 不用$nextTick,而是用setTimeout,这样就保证了我们微信从后台切到前台的时候,我们的歌曲又可以重新播放了 setTimeout(() => { this.$refs.audio.play()//只写这一句是会报错的,因为调用play时候,我们同时请求src是不可以的,这个dom还没有ready this.getLyric() }, 1000)
- 底部mini播放器会占据正常播放列表页面最后一行
就比如说,薛之谦所有音乐,最后一个是"演员",
整个mini播放器就会把演员列表那一行遮挡- 解决:将解决问题的逻辑封装成一个单独的文件src/common/js/mixin.js
关于mixin:https://www.jianshu.com/p/f34863f2eb6d
- 思路:当playlist中有数据的时候,就将滚动组件的bottom设置为60px
实现三部曲: 先引入:import { playlistMixin } from 'common/js/mixin' 再注册:export default { mixins: [playlistMixin], } 实现handlePlaylist方法 handlePlaylist(playlist) { // 当playlist中有数据的时候,就将滚动组件的bottom设置为60px const bottom = playlist.length > 0 ? '60px' : '' this.$refs.list.$el.style.bottom = bottom // console.log(this.$refs.list, '-----------') this.$refs.list.refresh() },
- 具体实现见:
src/common/js/mixin.js src/components/music-list src/components/singer src/base/listview src/components/recommend
- 解决:将解决问题的逻辑封装成一个单独的文件src/common/js/mixin.js
- 获取播放源错误:
歌单详情页src/components/disc.vue(类似歌手详情页)
- 定义二级路由
- 定义在router/index.js的recommend的children中
- 在recommend.vue中引入路由组件router-view
- 点击歌单路由跳转:src/components/recommend.vue
- 点击li触发:@click=“selectItem(item)” selectItem(item)
- 路由跳转:
// 点击li,带参数的路由跳转 selectItem(item) { this.$router.push({ path: `/recommend/${item.dissid}` }) }
- 歌单部分数据的存放和获取
- 在vuex中的state,mutations,mutations-type中都设置有关歌单的变量
- 在组件中使用和更改vuex中的数据
src/components/recommend.vue中,引入mutations 在src/components/recommend.vue中利用this.setDisc(item)将vuex中的disc值设置为当前点击的li 然后利用getters得到 src/components/disc.vue中,引入getters 从vuex中获取当前歌单的标题,背景图等,传入到music-list组件中
- 从qq音乐抓取每个歌单全部歌曲
1. 在build/webpack.dev.conf.js中配置获取所有歌曲的接口,更改referer和host 2. 在src/api/recommend.js中定义获取全部歌曲的方法getSongList,调用接口/api/getSongList 2. 在src/components/disc.vue中使用该方法 getSongList(this.disc.dissid).then(res => { if (res.code === ERR_OK) { this.songs = this._normalizeSongs(res.cdlist[0].songlist) } }) 3. 获取到songs之后,将songs传入到子组件music-list ```
- 问题:当刷新了之后,暂时获取不到数据,不能一直在这个页面等着,所以设置跳转到父组件
_getSongList() { if (!this.disc.dissid) { this.$router.push('/recommend') return } },
所有排行榜页面src/components/rank/rank.vue
- 抓取数据src/api/rank.js(rank.vue中有具体实现,一看应该就能懂)
- 在页面内获取数据并使用(rank.vue中有具体实现,一看应该就能懂)
- 设置组件可以滚动(rank.vue中有具体实现,一看应该就能懂)
需要把数据传递给scroll,当数据全部存在时候,才能正确判断可滑动部分的高度
- 数据未加载时候,显示一个转圈圈的图标loading
- 当底部有mini播放器占位的时候,需要处理底部高度:使用mixin.js
对于数据获取的总结:
- json
如果获取数据的接口要求传入的的参数中format:json,那么就用axios配合更改referer和host的方式 先在webpack.dev.conf.js中 定义接口改变referer和host, 并且 获取来自 请求这个接口时 传过来的数据, 然后再从qq音乐上获取数据 由src/api/....js文件中的方法取得最终获取的数据
- jsonp
直接在src/api下的js文件中定义获取数据方法 然后通过jsonp将所有请求数据连在一起 并请求网页 然后src/api/js文件中的方法 中得到jsonp取得的数据
- 注意:如果需要获取播放源的,需要先引入获取vkey的函数,在获取歌曲
排行榜详情页面src/components/top-list.vue(和disc.vue很像很像)
- 路由
- 编写路由:
是一个二级路由,在route.js中的rank下面定义 children: [ { // 以id为变量,可以传入不同的id值,然后去渲染不同的歌手详情页 path: ':id', component: TopList, } ]
- 跳转路由src/components/rank/rank.vue
给遍历排行榜的li添加点击事件@click="selectItem(item)" selectItem(item) { this.$router.push({ path: `/rank/${item.id}` }) this.setTopList(item) },
- 添加在src/components/rank/rank.vue中
- 编写路由:
- 关于排行榜内的所有歌曲数据
- 在src/api/rank.js中获取数据
- src/components/top-list下使用数据
- 将获取的song传进music-list,然后设置没有数据时候(this.$route.push)默认返回上一级父元素
- 设置榜单样式src/base/song-list.vue
- 设置变量,默认rank为false,代表默认没有排行的样式
<div class="rank" v-show="rank"> <span :class="getRankCls(index)">{{getRankText(index)}}</span> </div> props: { rank: { type: Boolean, default: false } }
- 设置变量,默认rank为false,代表默认没有排行的样式
- 设置事件改变样式,当排行在前三名时显示图片,往后显示数字
// 和排行榜有关的样式 getRankCls(index) { // 前三名,是图片的样式 if (index <= 2) { // 在scss文件中有对应的样式 return `icon icon${index}` } else { // 否则就是文字样式,文字的实现在getRankText(index)中 return 'text' } }, getRankText(index) { if (index > 2) { //因为排名是从1开始的,不是索引0开始的 return index + 1 } }
- rank的值
- top-list引入music-list
- music-list引入rank
- 所以由top-list传入rank值为true
搜索页面src/components/search.vue
搜索框组件src/base/search-box.vue(在src/components/search.vue中引用)
- 默认搜索框中的内容是"搜索歌曲、歌手",但是可以更改,所以在src/base/search-box.vue设置
<input ref="query" class="box" :placeholder="placeholder"/> props: { placeholder: { type: String, default: '搜索歌曲、歌手' } },
- 获取来自搜索框中输入的内容
<input ref="query" class="box" :placeholder="placeholder" v-model="query"/> data() { // 获取来自输入搜索框中的内容,利用双向绑定v-model return { query: '' } },
- 通过是否输入内容,控制输入框后面的"×"图标是否显示,当输入内容不为空的时候才显示
<i class="icon-dismiss" v-show="query"></i>
- 点击×图标的时候,输入框中的内容为空
<i class="icon-dismiss" v-show="query" @click="clear"></i> clear() { this.query = '' },
- 监听query的改变,传递给父元素
created() { // 这种写法和直接在下面写watch差不多 this.$watch('query', debounce(newQuery => { this.$emit('query', newQuery) }, 200) ) },
热门搜索
- 数据:
- 获取数据:因为不是jsonp方式,见数据获取方式
- 使用数据,在src/components/search.vue中调用方法getHotKey
- 引入import { getHotKey } from ‘api/search’
- 在created时调用
created() { this._getHotKey() },
- this._getHotKey
_getHotKey() { getHotKey().then(res => { if (res.code === ERR_OK) { this.hotKey = res.data.hotkey.slice(0, 10) } }) }
- 将hotkey数据填入到dom结构 中
<li class="item" v-for="(item,index) in hotKey" :key="index"> <span>{{item.k}}</span> </li>
- 逻辑:点击热门搜索的数据,可以自动将数据填充到搜索框中
- 在src/base/search-box中设置改变query的方法
- 在父组件src/components/search中,
//query为 当前点击的 热门搜索中的内容 this.$refs.searchBox.setQuery(query)
- 当搜索框中有关键词的时候,应该显示一个搜索结果的列表见src/components/suggest.vue
搜索组件src/components/suggest.vue(当点击搜索框中内容不为空的时候,显示搜索列表)
- 获取数据
- search: 用于获取数据
- _genResult:将获取到的数据格式化,得到我们想要的格式
如果根据检索词返回数据中zhida的值不为空,那么说明检索到了包含检索词的歌手 将zhida中的内容加上type:singer键值对形成一个新的对象加入到ret中 再将所有和检索词有关的歌曲放入到ret中
- 遍历数据
<li class="suggest-item" v-for="(item,index) in result" :key="index">
- 设置样式,因为歌手和歌曲显示的图标和文字都是不一样的
<div class="icon"> <!-- 图标是动态的,也就是歌手和歌曲显示的图标是不一样的--> <i :class="getIconCls(item)"></i> </div> <div class="name"> <!-- 歌手与歌曲显示的数据也是不一样的 --> <p class="text" v-html="getDisplayName(item)"></p> </div> getIconCls(item) { if (item.type === TYPE_SINGER) { return 'icon-mine' } else { return 'icon-music' } } getDisplayName(item) { if (item.type === TYPE_SINGER) { // 如果当前对象中含有歌手名,那就显示歌手名 return item.singername } else { // 否则显示歌手名 // item中song已经被处理过了,所以直接写item.name就行不,用写item.songname return `${item.name}-${item.singer}` } },
- 搜索逻辑:
- 在src/components/search.vue中引入组件
src/base/SearchBox: 负责搜索框的内容 src/components/Suggest: 负责根据搜索框中内容的查找相关歌曲或者歌手
- 逻辑:
1. 在searchbox组件中,监听搜索框中内改变,然后将改变的值传递给父组件search组件 2. 父组件接受事件,并将query的值传递给子组件suggest.vue 3. suggest.vue子组件监听到query的改变,调用search方法从qq音乐端请求数据
- 在src/components/search.vue中引入组件
- 优化:将根据检索词返回的数据中的res.data.song.list格式化
利用common/js/song.js中的createSong处理this._normalizeSongs(data.song.list): _normalizeSongs(list) { // filter const ret = [] list.forEach(musicData => { if (musicData.songid && musicData.albummid) { ret.push(createSong(musicData)) } }) return ret },
- 搜索列表实现滚动功能:引入scroll组件
- 上拉刷新功能(扩展scroll组件)(视频:10-5:17:30)
- 是否开启上拉刷新功能,默认是false
src/base/scroll.vue props: { // 是否开启上拉刷新功能,默认是false pullup: { type: Boolean, default: false }, }
- 在scroll组件初始化的时候,判断是否开启了上拉刷新该功能
如果开启了,监听scrollEnd事件,就是当停止滚动的时候派发一个scrollEnd事件
如果此时滚动到了底部,那就向父组件派发一个事件,代表父元素可以进行上拉刷新功能了if (this.pullup) { // pullup: drop-down refresh // scrollEnd:停止滚动了 // scrollToEnd:滚动到底部了 this.scroll.on('scrollEnd', () => { //当满足这个条件的时候,表示当前已经滚动到底部了 if (this.scroll.y <= this.scroll.maxScrollY + 50) { this.$emit('scrollToEnd') } }) }
- 当滚动到底部,scroll向父组件suggust派发事件,父组件suggest接收事件,并触发searchMore方法
- searchMore:获取更多数据,加载更多
<scroll :pullup="pullup" @scrollToEnd="searchMore"> //上拉刷新 searchMore() { // 如果此时数据已经加载完了,就不能实现上拉刷新的功能了 // _checkMore来检测是否数据都请求完毕,也就是 是否改变hasMore if (!this.hasMore) { return } this.page++ // 刷新一次,page++,再请求page对应页数的数据 search(this.query, this.page, this.showSinger, perpage).then(res => { if (res.code === ERR_OK) { this.result = this.result.concat(this._genResult(res.data)) this._checkMore(res.data) } }) },
- _checkMore:检查是否还有数据,以此确定是否可以上拉刷新
_checkMore(data) { const song = data.song // data中的song有几个参数: // 1. curnum:当前页的歌曲数量 // 2. curpage:当前是第几页 // 3. 和检索词有关的所有歌曲数量 // 不能上拉刷新的条件:当前歌曲长度为0,已经检索到最后数据了 if (!song.list.length || song.curnum + (song.curpage) * perpage > song.totalnum) { this.hasMore = false } },
- searchMore:获取更多数据,加载更多
- 当上拉刷新时候显示转圈圈的样式,
1. 引入loading组件:import Loading from 'base/loading/loading' 2. loading显示的条件是hasmore为true 3. 改变hasMore的条件是 if(!song.list.length || song.curnum + (song.curpage) * perpage > song.totalnum){ this.hasMore = false }
- 当滑动到页面底部的时候,scroll给父组件传递事件
父组件调用searchMore方法获取下一页的数据
也就是请求数据时候,将page++传递过去
每次请求,获取数据之后都判断一下是否数据 全部都已经请求:_checkMore(){} - 10-5-17:48重新捋清思路
- 优化:每次都搜素列表顶部开始显示数据
search() { // query改变的时候,第一次调用search,page都要从第一个开始 this.page = 1 // query改变的时候,第一次调用search,都要滚动到底部 this.$refs.suggest.scrollTo(0, 0) }
- 是否开启上拉刷新功能,默认是false
- 点击搜索到的内容,跳转相应页面
- 对搜索列表中歌手的点击:
- 设置路由:当搜索内容含有歌手的时候,跳转路由,跳到歌手详情页
- 当点击click搜索列表的时候,绑定selectItem事件
- 如果点击的是歌手,那么跳转路由,并且设置vuex中singer的值改变
selectItem(item) { if (item.type === TYPE_SINGER) { this.$router.push({ path: `/search/${singer.id}` }) //调用mutations改变vuex中state中的值 //...mapMutations({ // setSinger: 'SET_SINGER' //}), this.setSinger(singer) } }
- 对搜索列表中歌曲的点击:
如果点击歌曲,state中的playlist、sequencelist、currentIndex这三个变量都要改变,所以我们要在action中进行一个封装,见src/store/action.js的insertSong
注意,点击歌曲要实现:- 歌曲播放
- playlist和sequenceList中要添加歌曲
判断这两个列表中是否已经含有了这首歌 如果有还要判断这首歌在currentSong的前面还是后面 以便调用删除这首歌之前的位置 只保留当前新添加的歌曲(位置)
- 问题1:报do not mutate vuex store state outside mutations…
- 原因:没有通过mutations方式修改vuex
let playlist = state.playlist 后来直接操作playlist是不行
- 解决:在actions中,拷贝出一个新的数组进行操作
let playlist = state.playlist.slice() currentIndex就不会报这个错,因为基本数据类型是值传递
- 原因:没有通过mutations方式修改vuex
- 不知道loading组件在这里显示是怎么个情况,反正我是没看懂
- 对搜索列表中歌手的点击:
- 优化:
- 当搜索列表为空的时候,显示相应的组件src/base/no-result.vue代表没有搜索到相关内容
- 每输入一个字符都会派发一个事件,但是我们不想派发这么频繁,所以编写防抖函数
- 防抖函数:
传入一个函数,返回一个防抖函数 function debounce(func, delay) { let timer return function(...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { func.apply(this, args) }, delay) } }
- 在src/base/search-box中监听query改变后,应用防抖函数
this.$watch( 'query', // debounce节流函数,在一定时间改变多次,以最后一次为准 debounce(newQuery => { this.$emit('query', newQuery) }, 200) )
- 防抖函数:
- 移动端,输入搜索内容的时候,会调起键盘,当搜索结束后不会收起键盘
我们需要实现的是:监听到滚动事件,input失去焦点,键盘收起- 由scroll.vue组件在滚动前(beforeScroll)派发事件
- 由suggest.vue组件接受来自scroll组件的事件,并传递给父组件search
- 由search接收来自suggest组件的事件,并调用search-box中的事件来使search-box中的搜索框失去焦点,
- 当搜索列表为空的时候,显示相应的组件src/base/no-result.vue代表没有搜索到相关内容
- 最大的问题:因为getmusic获取vkey需要时间,在没有获到数据的时候,是拿不到返回的歌曲列表的,所以在ret.concat(this._normalizeSongs(dta.song.list))是拿不到数据的,就没办法渲染
解决:将ret传进去,把之前返回值的初始值(我设置的是[])直接设置成ret而不是[]
搜索历史
- 数据的存取因为很多地方都用到了搜索历史,所以我们将他保存到全局的vuex中
state,mutation-type, mutations - 在suggest中点击搜索列表,派发事件(因为每个组件都实现特定的功能,实现功能分离)
- 要将结果缓存到localStorage中,所以封装一个单独的js文件src/common/js/cache.js,专门操作localStorage
- 安装插件good-storage操作localStorage
- 网站:
https://github.com/ustbhuangyi/storage
- 具体见src/common/js/cache.js的saveSearch和loadSearch方法
- saveSearch
在不超过存储长度的情况下,将后搜索的值,填入到数组的前面,先搜索的值,填入到数组的后面 将搜索历史列表存入到localStorage 将搜索历史列表返回给vuex
- loadSearch:从本地拿取搜索历史的值,用于:赋给state中的搜索历史的初始值
- saveSearch
- 总结逻辑:
- 当点击搜索列表的时候,suggest向父组件search传递当前被点击元素item
- 父组件调用actions,将query加入到搜索历史中
- 由actions调用commit方法将的新的数组提交给mutations,
- mutations将最终的数组存到localStorage和vuex中
- 将vuex中的搜索历史 数据渲染到dom上
- 取数据:mapGetters
- 引用组件:search-list
- 因为很多地方都要用显示列表的组件,所以单独封装一个组件src/base/search-list
当需要显示列表的时候,引入并将列表数据传给组件,就可以了
- 当点击搜索历史中的数据的时候,要进行搜索
- 在search-list组件中,绑定点击事件,向父元素search传当前点击的item
- 父组件search接受子组件search-list传来的事件,并且触发addQuery事件,完成逻辑
- 当点击搜索历史中数据后面的×的时候,将该条历史删除,或者点击垃圾桶,清空历史
和存储搜索历史差不多- 点击×:
- 由search-list派发事件给父组件search
- 由search处理事件,调用actions中的deleteSearchHistory方法
- 在actions封装方法deleteSearchHistory
- deleteSearchHistory方法中调用cache.js中的deleteSearch方法
- 经过deleteSearch后,将处理过的数组重新赋给vuex state中的searchHistory
- 点击垃圾桶图标:
- 点击时,触发事件actions中的clearSearchHistory方法
- 在actions封装方法clearSearchHistory
- clearSearchHistory方法中调用cache.js中的clearSearch方法
- 经过clearSearch后,将处理过的数组重新赋给vuex state中的searchHistory
- 点击×:
- 优化:
- 点击垃圾桶时候,有一个弹出的框提醒用户是否全部删除->弹框组件:src/base/confirm
- 当点击清空按钮的时候,触发的不是清空历史的操作了,是显示弹窗的操作
- 当点击弹窗中的确定或者取消的时候,confirm会触发相应的的事件
事件中定义:弹窗的隐藏 向父组件传递当前点击的是确定还是取消操作
- 当搜索历史有点多的时候,设置滚动事件
给热门搜索和搜索历史加上滚动事件 因为它们两个是两个div,所以我们应该在最外层添加一个div 否则scroll默认是第一个div添加滚动事件, 还有就是数据的问题,我们不能只监听一个div数据的改变,所以设置计算属性 shortcut() { return this.hotKey.concat(this.searchHistory) }
- 问题:
- 描述:当搜索历史歌曲 高度在屏幕大小的边缘的时候,再添加一首的话不出现滚动情况
- 原因:因为这个时候,dom是在搜索结果页面,而不是在search页面
- 解决:要加一个逻辑,在search中watch query的改变,
因为在组件切换的过程中,query有一个从有到无的状态,
所以如果newquery为空的话,手动刷新scroll
- 问题:
- 当歌曲播放的时候,也就是最下方的mini播放器显示的时候,要重新计算搜索列表和搜索历史的高度
mixin配合handlePlaylist 见player组件的"其他问题"标题
- 点击垃圾桶时候,有一个弹出的框提醒用户是否全部删除->弹框组件:src/base/confirm
歌曲列表组件src/components/playlist.vue(在src/components/player.vue中引入)
- 显示和隐藏
<div class="playlist" v-show="showFlag"> showFlag默认是false,可以通过shiw和hide这两个方法进行改变 show() { this.showFlag = true }, hide() { this.showFlag = false },
- 显示:点击歌曲列表按钮(src/components/player.vue的control)时触发方法show()
- 隐藏:点击歌曲列表蒙层(.playlist)的时候,或者点击关闭(.list-close)的时候
在playlist.vue中的蒙层部分和关闭部分绑定事件,点击就触发hide方法
- 补充:因为蒙层代表的是整个playlist组件,我们要是点击弹出框内容的时候,也会造成playlist组件消失,所以在.list-wrapper上添加阻止默认事件@click.stop
- 数据:
- vuex中导出sequenceList,在dom中遍历
- 当数据很多的时候,要实现滚动效果,引入scroll组件
- 保证获取到所有数据正确的高度,scroll才能正常滚动:当调用show方法的时候,延迟20秒获取数据
show() { this.showFlag = true // 当点击按钮显示组件的时候,要延迟20秒之后刷新一下scroll,因为这样才能正确的到数据的高度,才能确保滚动 setTimeout(() => { this.$refs.listContent.refresh() }, 20) }
- 当前歌曲样式:遍历的歌曲中如果某一首歌曲和vuex中currentSong匹配上的话,那么这首歌曲设置特殊样式,其余的样式都一样
click: getCurrentIcon(item) { if (this.currentSong.id === item.id) { return 'icon-play' } return '' },
- 切歌:如果点击歌曲列表中的某首歌,那么当前播放歌曲要改变,,并且设置歌曲状态改变为true
selectItem(item, index) { // 当前遍历的是sequenceList,然而如果是随机模式的话,playList中是被打乱的数组 // 那么只能通过找到索引,因为歌曲的播放是依赖于数组和索引的,然后通过playList[index]找到歌曲 if (this.mode === playMode.random) { index = this.playlist.findIndex(song => { return song.id === item.id }) } this.setCurrentIndex(index) // 点击完歌曲,同时设置歌曲状态为true this.setPlayingState(true) },
- 这里注意一个点:playlist正常是和sequenceList中是一样的,如果是随机模式就不一样了
如果切歌,必须在随机列表中找这首歌的位置,然后playList[index]才能播放, - 还有:并不是,随机模式对应歌曲列表中就是随机歌曲列表了,只是播放的时候随机播放而已
- 这里注意一个点:playlist正常是和sequenceList中是一样的,如果是随机模式就不一样了
- 当点击歌曲列表的某一首歌之后,也要实现滚动效果,并且,当前播放的歌曲始终在列表的顶部显示
- 当监听到当前播放歌曲改变的时候或者当显示这个playlist组件的时候,触发scrollToCurrent事件
- 定义跳转(到顶部)方法:
scrollToCurrent(current) { // 找到当前播放歌曲对应在sequenceList中的位置 const index = this.sequenceList.findIndex(song => { return current.id === song.id }) // 跳到指定位置 // this.$refs.listContent.scrollToElement(this.$refs.list.$el.children[index], 300) this.$refs.listContent.scrollToElement(this.$refs.listItem[index]) },
- 跳转(到顶部)时机1:当前播放歌曲改变的时候
watch: { currentSong(newSong, oldSong) { if (!this.showFlag || newSong.id === oldSong.id) { return } // setTimeout(() => { this.scrollToCurrent(newSong) // }, 20) } }
- 跳转(到顶部)时机2:显示这个playlist组件的时候
show() { this.showFlag = true // 当点击按钮显示组件的时候,要延迟20秒之后刷新一下scroll,因为这样才能正确的到数据的高度,才能确保滚动 setTimeout(() => { this.$refs.listContent.refresh() this.scrollToCurrent(this.currentSong) }, 20) },
- 当监听到当前播放歌曲改变的时候或者当显示这个playlist组件的时候,触发scrollToCurrent事件
- 点击×的时候,将这首从当前播放列表中删除
- 点击按钮,触发deleteOne事件
- deleteOne调用actions中的deleteSong事件
- delteSong:删除playlist,sequenceList中的歌曲,重新计算currentIndex
- 问题
- 描述:当删掉歌曲列表唯一的一首歌时候,报错,
- 原因:因为歌曲改变会触发player的watch
- 解决:
watch中: newSong根本没有,所以newSong != oldsong 所以所有逻辑都会执行,所以我们应该当newSong没有的时候,直接返回 动画: 将ul替换为transition-group
- playlist.vue中当删除最后一首歌的时候需要将歌曲列表hide掉
deleteOne(item) { this.deleteSong(item) if (!this.playlist.length) { this.hide() } },
- 点击垃圾桶图标,删除播放列表所有歌曲(playlist组件)
- 使用actions中封装好的方法deleteSongList
- 引用confirm组件
- 注意:要将confirm的点击事件阻止冒泡,并且删除最后一首歌的时候隐藏playlist
- 在player和playlist组件中有很多相同的逻辑,所以将相同的逻辑放在mixin中,实现复用
在player和playlist组件中引入mixinimport { playerMixin } from 'common/js/mixin' mixins: [playerMixin] 然后在提取组件的过程中,在mixin中该引入引入,该配置配置,最后大功告成
添加歌曲列表页面src/components/add-song
- 控制组件的显示或者隐藏
- 设置v-show=“showflag”
<div class="add-song" v-show="showFlag" @click.stop>
- showFlag默认是false
- show和hide可以实现改变showflag的值,控制组件隐藏
show() { this.showFlag = true }, hide() { this.showFlag = false },
- 在playlist组件中点击"添加歌曲…"按钮,触发add-song中的show方法
addSong() { this.$refs.addSong.show() }
- 设置v-show=“showflag”
- 搜索歌曲
- 引用search-box,suggest
- 监听query的改变,设置query(搜索框中的关键词)
<search-box ref="searchBox" placeholder="搜索歌曲" @query="onQueryChange" > </search-box> onQueryChange(query) { this.query = query },
- 还可以利用query设置最近播放/搜索历史的显示与隐藏
<!-- .shortcut:最近播放和搜索历史 --> <div class="shortcut" v-show="!query"></div> <!-- .search-result:搜索结果 --> <div class="search-result" v-show="query"></div>
- 注意suggest中是将歌手一起搜索到的,但是我们这里不需要搜索歌手
设置变量showSinger(false)并传递给子组件: suggest组件, <suggest :query="query" :showSinger="showSinger"></suggest>
- 因为这里很多功能很search组件都是I重复的,所以我们定义在mixin中,然后又是一顿大整改,然后就没有然后了
- 最近播放和搜索历史(src/base/switches)
- 切换和样式的改变:由currentIndex控制哪个部分高亮(最近播放/搜索历史)
currentIndex默认是0,代表最近播放 当点击:最近播放/搜索历史,任何一部分的话, 都会给父组件add-song派发事件,参数为当前索引index 由父组件监听事件,改变currentIndex,再传入子组件,进而改变哪部分高亮
- 切换和样式的改变:由currentIndex控制哪个部分高亮(最近播放/搜索历史)
- 最近播放和搜索历史列表:(src/components/add-list)
- 最近播放部分:
- player中,audio标签在歌曲准备好的时候就会派发一个ready事件
在ready方法中,利用mapActions将当前歌曲加入到最近播放player.vue中: this.savePlayHistory(this.currentSong) mapActions中; // 最近播放 export const savePlayHistory = function({ commit }, song) { // 调用cache中的方法,将数组在本地缓存一份 commit(types.SET_PLAY_HISTORY, savePlay(song)) } cache.js中: export function savePlay(song) {} ``` - 拿到数据之后渲染 + 数据利用mapgetters从vuex中取 + 组件可滚动:scroll + 数据渲染:song-list组件 + 点击歌曲:添加到歌曲列表,播放当前歌曲 ```javascript // 将 从本地取出来的对象转化为Song实例 import Song from 'common/js/song' ...mapActions(['insertSong']), selectSong(song, index) { if (index !== 0) { // song自己并不是一个Song实例,它只是拥有Song的属性, this.insertSong(new Song(song)) // new Song() -> 传入实例, song 不是实例是对象 this.$refs.topTip.show() } } ```
- player中,audio标签在歌曲准备好的时候就会派发一个ready事件
- 搜索历史:复用search-list
search-list中很多方法和数据都在mixin共享中
- 最近播放部分:
- 优化
- 删除搜索历史的时候添加动画效果src/base/search-list -->transition-group
- 有时最近播放是不能滚动的src/components/add-song
- 原因:因为scroll没有正确计算高度
- 解决:每次add-song调用show()的时候,都重新计算高度
setTimeout(() => { // 保证scroll可以正确计算时间 if (this.currentIndex === 0) { this.$refs.songList.refresh() } else { this.$refs.searchList.refresh() } }, 20)
- 还是发现歌曲列表高度不对
- 原因:监听到数据改变,需要100ms刷新数据,20ms不够
- 解决:改变scroll组件中的刷新的时间,之前是20,是个定值
- 我们这里改为变量,变量的值默认是20ms,但是可以更改
- 然后在playlist组件中的scroll组件传入值refreshDelay: 120
- 同理 search和add-Song中要改变刷新时间
- 因为这两个组件都用到了mixin,所以在mixin中设置refreshDelay的值就可以了
- 这个refreshDelay目的就是保证在这个时间内去refresh,我们的高度已经计算出来了
- 当添加歌曲到歌曲列表的时候,有一个提示框src/base/top-tips
- 显示和隐藏,参见add-song的显示和隐藏,一样一样的
在top-tips中定义事件控制自己的显示和隐藏,由父元素调用事件改变该组件的状态data() { return { // 默认隐藏 showFlag: false } }, methods: { show() { this.showFlag = true // 过2秒自动关闭,避免产生多个timer,所以要清理 clearTimeout(this.timer) this.timer = setTimeout(() => { this.hide() }, this.delay) }, hide() { this.showFlag = false } }
- 当点击最近播放中的歌曲,或者搜索结果中歌曲 都能触发show方法显示toptip组件
- 几秒自动关闭,或者点击直接隐藏
this.timer = setTimeout(() => { this.hide() }, 2000)
- 显示和隐藏,参见add-song的显示和隐藏,一样一样的
用户中心页面src/componennts/user-center和收藏功能
- 在m-header页面添加路由,点击个人中心图标,进入个人中心页面
- src/componennts/m-header中
<router-link tag="div" class="mine" to="/user"> <i class="icon-mine"></i> </router-link>
- 添加路由,router/index.js
{ path:'/user', component: UserCenter }
- src/componennts/m-header中
- 我喜欢的/最近播放
- user-center中引入switches组件,并传入两个参数,以便正确显示高亮样式
:currentIndex="currentIndex" :switches="switches"
- user-center中引入switches组件,并传入两个参数,以便正确显示高亮样式
- 收藏列表:favoriteList
- 数据:
- 在state中设置变量存储数组
- 在mutations,actions,getters等vuex相关的文件,依次添加数据
- 在cache.js中创建存储,删除,加载 收藏列表的方法,然后再actions中被使用
- 收藏事件逻辑:(player和playlist)因为逻辑不仅是player中需要的,也是playlist中需要的,所以定义在mixin中
<i class="icon" :class="getFavoriteIcon(currentSong)" @click="toggleFavorite(currentSong)"></i> mixin中: 1. toggleFavorite(song) { if (this.isFavorite(song)) { this.deleteFavoriteList(song) } else { this.saveFavoriteList(song) } }, 2. getFavoriteIcon(song) { if (this.isFavorite(song)) { return 'icon-favorite' } return 'icon-not-favorite' }, 3. // 判断当前歌曲是否已经在收藏列表中了 isFavorite(song) { const index = this.favoriteList.findIndex((item) => { return item.id === song.id }) return index > -1 }
- 在user-center的个人中心,我喜欢的/播放历史 部分进行渲染数据
和add-song组件一样一样的,照样搬就行,只改了数据就可以了
注意:点击列表的时候,依旧完成两个事件:播放歌曲,添加到播放历史中
- 数据:
- 剩余部分的逻辑user-center
- 点击返回,回到来时的页面,也就是主页
<!-- .back返回按钮 --> <div class="back" @click="back"> <i class="icon-back"></i> </div> back() { this.$router.back() },
- 我喜欢的/最近播放下面的随机播放按钮
<div class="play-btn" ref="playBtn" @click="random"> <i class="icon-play"></i> <span class="text">随机播放全部</span> </div> ...mapActions(['randomPlay']), random() { let list = this.currentIndex === 0 ? this.favoriteList : this.playHistory // 如果没有数据,那么什么都不做,节省性能 if (list.length === 0) { return } // list不是一个song实例,需要包装 list = list.map(song => { return new Song(song) }) this.randomPlay({ list }) }
- 注意:并不是随机播放全部,那个播放列表中就是随机列表了,只是播放的时候是随机的而已
- 而且:在最近播放页面,点击随机播放按钮,歌曲列表排序和最近播放列表排序并不一样,因为,最近播放会利用insertSong将最近播放的排在前面
- 歌曲播放的时候,scroll正确计算高度(利用mixin.js)
- import { playlistMixin } from ‘common/js/mixin’
- mixins: [playlistMixin],
- handlePlaylist
// scroll正确计算高度 handlePlaylist(playlist) { const bottom = playlist.length > 0 ? '60px' : '' this.$refs.listWrapper.style.bottom = bottom // favoriteList:有可能不存在 this.$refs.favoriteList && this.$refs.favoriteList.refresh() // $refs.favoriteList use v-if, make sure it is not undefined this.$refs.playList && this.$refs.playList.refresh() },
- 当我喜欢的/最近播放没有数据,为空的时候,给出提示
- import引入,components注册
- 两个计算属性noResult,noResultDesc
<!-- 当我喜欢的/最近播放 没有数据,为空的时候,给出提示 --> <div class="no-result-wrapper" v-show="noResult"> <!-- noResult:这个组件是否显示 --> <!-- noResultDesc:提示内容 --> <no-result :title="noResultDesc"></no-result> </div> noResult() { if (this.currentIndex === 0) { return !this.favoriteList.length } else { return !this.playHistory.length } }, noResultDesc() { if (this.currentIndex === 0) { return '暂无收藏歌曲' } else { return '你还没有听过歌曲' } }
- 点击返回,回到来时的页面,也就是主页
收尾工作
- 当切了很多歌的时候,最后停下来,发现CD和歌词都在继续走
- 原因:
在src/components/player.vue中,在监听到当前歌曲改变后,是推迟了1s才播放的 setTimeout(() => { this.$refs.audio.play()//只写这一句是会报错的,因为调用play时候,我们同时请求src是不可以的,这个dom还没有ready this.getLyric() }, 1000) 也就是如果此时我们在这1s之内按了暂停键,1s后这个逻辑还是会执行,所以产生了这样的问题
- 解决:
clearTimeout(this.timer) this.timer = setTimeout(() => { this.$refs.audio.play()//只写这一句是会报错的,因为调用play时候,我们同时请求src是不可以的,这个dom还没有ready this.getLyric() }, 1000) 同时audio中监听 @play="ready" 这样才能保证,先play调用,改变播放状态,然后才能执行audio.pause()方法,也就是pause肯定在play之后执行 依旧保留1000延迟是因为,如果歌曲在手机上后台播放,切换到前台的时候,,为了能请求成功,所以设置1s延迟
- 原因:
- 当连续切了很多歌的时候,歌词显示错误
- 原因:歌词获取是异步获取,所以当不停切歌的时候
可能这歌词刚请求回来,歌都已经不知道跳了多少个了 - 解决:在player中填入下面语句
if (this.currentSong.lyric !== lyric) { return } 当 当前歌词不等于获取来的歌词的时候,就什么都不做,否则才将歌词格式化操作
- 原因:歌词获取是异步获取,所以当不停切歌的时候
- 当歌曲列表中只有一首歌的时候,切换下一首,出现 下面样式是都是灰色的
- 解决:当只有一首歌的时候,next或者prev方法 调用loop方法后直接返回,不执行下面的this.songReady = false这条语句
更多推荐
已为社区贡献2条内容
所有评论(0)