前言
基于 Vue + vuex + vue-router + vue-axios +better-scroll + Stylus + px2rem
等开发的移动端音乐App,UI 界面是看着自己手机的网易云音乐写的、flex 布局适配常见移动端。
也许是服务器原因有时进入页面首页,左右滑动切换页面效果没有用。这时请重新刷
新页面,有时页面加载的有点慢,请耐心等待。
写这个呢是因为最近学了Vue,想巩固下自己所学的知识。个人认为写项目可以更好的发现自己的不足。写音乐呢是因为网易有开源的接口,还有自己喜欢听歌!
炎炎夏日,你在家里吹着空调吃着冰镇西瓜没准还躺在沙发上看着自己喜欢的电影。而我却在学校看着电脑敲着代码没准还正抓着头皮改着不知道哪里出错了的bug。来自大三在校生的感慨,所以大佬请轻喷!!但我的问题请大佬务必告知,或者有更好的解决方案也一样,毕竟谁不想学到更多的知识呢?
页面预览
没有切图就是看着手机自己写的,所以样式有点难看。
我的 发现 朋友(还没写) 视频
歌单 排行榜
歌单详情 个人中心
全屏播放 搜索页面
开发目的
记录自己的学习,因为有些东西你不用它遗忘的是很快的。希望以后想捡起来的时候看看自己的笔记能有所帮助。就算没啥帮助,等多年以后回头看看自己曾经写的代码,也挺有意思的!
技术栈
Vue: 用于构建用户界面的 MVVM 框架
Vue-router: 单页面应用提供的路由管理
vuex:Vue集中状态管理,大型项目所必需的数据存储,让多个组件相互交互、共享状态时非常便捷
better-scroll:解决移动端各种滚动场景需求的插件,使移动端滑动体验更加流畅
stylus: 一种Css预编译处理器
px2rem: 移动端自适应的一种方法
Vue-axios: 用于请求API接口
NeteaseCloudMusicApi:网易云音乐 NodeJS 版 API,提供音乐数据
iconfont :阿里巴巴图标库,十分强大的一个图标库
页面架构
api
axios方法封装
axios.defaults.timeout = 10000
axios.defaults.baseURL = 'http://localhost:3000'
// 返回状态判断
axios.interceptors.response.use((res) => {
if (res.data.code !== 200) {
console.log('网络异常')
vue.$toast('网络异常')
vue.$hideLoading()
return Promise.reject(res)
}
return res
}, (error) => {
console.log('网络异常')
vue.$toast('网络异常')
vue.$hideLoading()
return Promise.reject(error)
})
export function fetchGet(url, param) {
return new Promise((resolve, reject) => {
axios.get(url, {
params: param
})
.then(response => {
resolve(response.data)
}, err => {
reject(err)
})
.catch((error) => {
reject(error)
})
})
}
axios接口请求(只展示几条避免太冗长)
export default {
//歌单
DiscLists(params) {
return fetchGet('/top/playlist', params)
},
// 歌单详情
SongList(params) {
return fetchGet('/playlist/detail', params)
},
//歌曲搜索
MusicSearch(params){
return fetchGet('/search',params)
},
//获取歌曲的url
MusicUrl (id){
return fetchGet('/top/playlist',{
id
})
},
//根据id获取mv数据
getMVDetail(params){
return fetchGet('/mv/detail', params)
},
//手机号登录
phoneLogin(params){
return fetchGet('/login/cellphone', params)
},
//登录后根据用户id获取用户歌单
getUserSonglist(params){
return fetchGet('/user/playlist', params)
},
}
router(只展示部分)
routes: [
主页面
{path: '/', component: Main},
{
//歌单详情页
path: '/songdetail',
name:songdetail,
component: songdetail
},
//歌单广场
{
path: '/songSquare/songSquare',
name:songSquare,
component: songSquare
},
//搜索页
{
path: '/search',
name:search,
component: search
},
//搜索详情页
{
path: '/searchDetail',
name:searchDetail,
component: searchDetail
},]
Vuex
modules(只展示music)
import * as types from '../types'
const state = {
songlist : [],
songs : '可乐',
searchHistory : ['薛之谦']
}
const mutations = {
// 上拉刷新歌单
[types.RESH_SONG_LIST](state, songlist) {
state.songlist = songlist;
// console.log(state.songlist)
},
//保存搜索历史
[types.SAVE_SEARCH_HISTORY](state, searchHistory) {
//数组去重,避免用户搜索相同关键字多次而加入相同的关键字
if(!state.searchHistory.includes(searchHistory)){
state.searchHistory.unshift(searchHistory)
}
// console.log(state.searchHistory)
},
//删除搜索历史 DELETE_SEARCH_HISTORY
[types.DELETE_SEARCH_HISTORY](state) {
state.searchHistory = []
},
}
const actions = {
// 上拉刷新歌单
reshSongList ({commit, state}, songlist) {
console.log(songlist);
// let playHistory = state.playHistory.slice()
// playHistory = [...playHistory, song]
commit(types.RESH_SONG_LIST, songlist)
},
//保存搜索历史
saveSearchHistory({commit, state}, searchHistory) {
console.log(searchHistory);
commit(types.SAVE_SEARCH_HISTORY, searchHistory)
},
//删除搜索历史 DELETE_SEARCH_HISTORY
deleteSearchHistory({commit, state}) {
commit(types.DELETE_SEARCH_HISTORY)
},
}
const getters = {
songlist: state => state.songlist,
songs : state => state.songs,
searchHistory : state => state.searchHistory,
}
export default {
state,
mutations,
actions,
getters
}
types(刚开始还记得常量应该大写,写着写着就忘了!尴尬)
export const RESH_SONG_LIST = 'RESH_SONG_LIST' // 刷新歌单
export const SAVE_SEARCH_HISTORY = 'SAVE_SEARCH_HISTORY'//保存搜索历史
export const DELETE_SEARCH_HISTORY = 'DELETE_SEARCH_HISTORY'//删除搜索历史
export const yuncun_video = 'yuncun_video'//获取云村精选视频
export const inland_video = 'inland_video'//获取内地视频
export const hongkong_video = 'hongkong_video'//获取港台视频
export const occident_video = 'occident_video'//获取欧美视频
export const japan_video = 'japan_video'//获取日本视频
export const update_login = 'update_login'//修改登录状态
export const save_userId = 'save_userId'//保存用户id
export const save_profile = 'save_profile'//保存用户信息
export const save_songlist = 'save_songlist'//保存用户歌单
export const save_userDetail = 'save_userDetail'//保存用户详情
store.js(将模块导出)
import Vue from 'vue'
import Vuex from 'vuex'
// import com from './modules/com'
import music from './modules/music'
import video from './modules/video'
import myLogin from './modules/myLogin'
Vue.use(Vuex)
export default new Vuex.Store({
modules : {
music,
video,
myLogin,
}
})
主要功能
推荐页面、歌单详情、歌单广场页面、排行榜详情、搜索页面、歌手列表、播放列表、个人中心等功能。
发现页面
页面切换
左右滑动实现页面切换,随着页面的切换顶部的tab也随之切换高亮。想了解请看这篇文章
上拉刷新
上拉刷新,推荐歌单会更新,数据是由Vuex管理的。还用了随机获取6条数据并做了数组去重,以免获取到两条相同的歌单数据。最大的败笔就是这个下拉刷新的位置其实是写错了位置的,导致不管在哪个tab页下拉刷新,发现页面的歌单都会更新,导致我好多页面不好写下拉刷新功能。我TM后面才发现的,很难受!上拉加载方法本来定义好了搞得也不能用了,当时脑袋发热觉得放在这其他页面就不用再重复写了。
api.Recommend(params).then(res =>{
if(res.code === 200){
this.songlist = [];
let length = res.playlists.length
while(this.songlist.length<6){
var random = Math.floor(Math.random()*(length-6+1)+1);
//数组去重
if (!this.songlist.includes(res.playlists[random])) {
this.songlist.push(res.playlists[random]);
}
}
this.$store.dispatch("reshSongList",this.songlist)
}
})
歌单详情页
这个页面的父子组件的传值方式用了好几种方法
- <i-button size=“large” @click.native=“handleClick”>提交
- 在子组件中加上ref即可通过this.$refs.ref.method调用
想了解更多方法请点这里
之前学习的时候以为只有一种,但当时感觉用第一种这种方法有点麻烦,就去百度了一下,发现还有好多方法。所以写项目还是可以学到更多知识的
这个页面应该最复杂的一个页面了,当时没考虑的好,知道页面要复用,但还是没有设计的很好,导致有几个页面还是复制代码改了传入的数据。如果当时数据全部是由父组件传过来的话,设计的好一点,一个详情页就可以了。
下面是这个页面的部分方法,比较多我就用图片形式展示了
歌单详情页中用了个组件,也就是底部播放
watch:{
index (val){
const params = {
id: this.songs[val].id
};immediate: true;
this.play();
api.SongUrl(params).then( (res)=>{
this.songdata = res.data;
this.$refs.audio.src = this.songdata[0].url;
this.songAvtart = this.songs[val].al.picUrl;
this.songname = this.songs[val].name;
this.singer = this.songs[val].ar[0].name;
console.log(res.data)
}).then(()=>{
// this.$refs.audio.play();
this.play();
this.$emit('play');
// this.con = false;
})
},
},
用watch监听,父组件传过来的index值,从而点击哪首歌播放哪首歌
下面是底部播放器中的方法
methods:{
//歌曲播放完自动播放下一首
ended(){
//子组件调用父组件的方法
console.log('father');
this.$parent.next();
},
//播放音乐
play () {
console.log(this.$refs.audio)
this.$refs.audio.play();
this.con = false;
//图片旋转
this.rotate = true;
//告诉父组件正在播放
this.$emit('play');
},
//暂停音乐
pause(){
this.con = true;
this.$refs.audio.pause();
//停止旋转
this.rotate = false;
//告诉父组件暂停播放
this.$emit('pause');
},
//全屏播放
fullPlay(){
this.$emit('fullPlay');
}
},
歌单广场页
这个页面基本和发现页面一样,就不赘述了.
排行榜详情
排行榜页面和歌单广场页面类似不再赘述
搜索页面
1.可以在输入框中输入你想搜索的歌曲
2.也可以点击搜索历史搜索
3.点击热搜榜也可以搜索
歌手页
只有个上拉加载功能
歌手页最开始是想写成有字母索引导航的,但当时被一个better-scroll搞崩了。
后来百度才知道,用better-scroll有四个必须条件,具体使用方法请点这里
- 必须包含两个大的div,外层和内层div
- 外层div设置可视的大小(宽或者高)-有限制宽或高
- 内层div,包裹整个可以滚动的部分
- 内层div高度一定大于外层div的宽或高,才能滚动
我的页面
第一次去到我的页面,如果没登录的话,需要先登录。登录完之后用户id会保存在
Vuex中。
视频页面
也没啥好说的,点击视频播放就完事了。每次只能播放一个视频
未来想写的
- 把朋友界面写一下
- 有些功能还是有问题的,需要完善
- 优化下重复的代码
- 再增加一些功能,因为还有好多功能没写。目前正在学react,没有太多时间
- github地址
致谢
十分感谢蜗牛老师教的Vue课程,蜗牛老师讲课是真的透彻!
最后
最后送自己一句话:你抓头改bug的样子虽然有些狼狈,但你努力写代码的样子真的很帅!!共勉。