用Vue搭建一个应用盒子(三):音乐播放器
来源:互联网 发布:腾讯视频 for mac官方 编辑:程序博客网 时间:2024/05/15 01:46
这个播放器的开发历时2个多月,并不是说它有多复杂,相反它的功能还非常不完善,仅具雏形。之所以磨磨蹭蹭这么久,一是因为拖延,二也是实习公司项目太紧。8月底结束实习前写完了样式,之后在家空闲时间多了,集中精力就把JS部分做完了。
这个播放器确实比当初构想的复杂,开始只打算做一个搜歌播放的功能。现在做出来的这个播放器,可以获取热门歌曲,可以搜歌,可以调整播放进度条,功能确实完善不少。
这次完成这个项目也是收获颇丰,点了不少新的技能点,当然,这个简陋的小项目也挖了不少坑,不知道啥时候能填上……
话不多说,看代码吧。
Muse-ui
不记得在哪个网站看到这个组件库的了,觉得好酷炫,于是用起来~
这是官网:地址
使用这个组件库的原因除了漂亮,还因为这是基于Vue 2.0,无缝对接,方便。
使用方法跟之前的插件一样,npm安装:
npm install --save muse-ui
安装好后,在main.js
中注册。
import MuseUi from 'muse-ui'import 'muse-ui/dist/muse-ui.css'import 'muse-ui/dist/theme-light.css'Vue.use(MuseUi)
就可以在项目中使用了。
PS:Muse-ui的icon是基于谷歌的Material icons,大家可以根据自己的需求到官网找icon的代码。
组件结构
接着我们就该搭建这个播放器的组件了。
结构如下:
||-- player.vue // 主页面| |-- playerBox.vue // 播放器组件| |-- popular.vue // 热门歌曲页面| |-- songList.vue // 歌曲列表页面 | |-- play.vue // 播放器页面| |-- search.vue // 搜索页面
PS:热门歌曲、搜索页面都能进入歌曲列表页面,播放器组件playerBox.vue
是放<audio>
标签的组件,是功能性组件。
我们来分别叙述:
1.player.vue
直接看代码吧:
<template> <div class="player"> <!-- banner here--> <router-view></router-view> <!-- navbar here --> <mu-paper> <mu-bottom-nav :value="bottomNav" @change="handleChange"> <mu-bottom-nav-item value="popular" title="流行" icon="music_note" to="/popular"/> <mu-bottom-nav-item value="play" title="播放" icon="play_arrow" to="/play"/> <mu-bottom-nav-item value="search" title="搜索" icon="search" to="/search"/> </mu-bottom-nav> </mu-paper> <!-- html5 player here --> <playerBox></playerBox> </div></template><script>import playerBox from './playerBox.vue'export default { name: 'player', data(){ const pa=this.$route.path; const Pa=pa.slice(1); return{ bottomNav: Pa } }, components: { playerBox }, methods:{ handleChange (val) { this.bottomNav = val }, changebar(){ const va=this.$route.path; const Va=va.slice(1); this.bottomNav = Va } }, watch:{ "$route":"changebar" }}</script><style lang="less" > .mu-bottom-nav{ position: fixed!important; bottom: 0px; background: #fafafa!important; z-index: 5; }</style>
解释一下:
- 由于Muse-ui有部分样式用到了less,所以在这里我们需要npm安装一个less的依赖,安装好后即可使用。
npm install less less-loader --save
- 这里我们加载了一个底部导航,muse-ui的,官网可以查到相关代码。这里要注意的是,为了让用户体验更好,我们需要让我们的底部导航随当前路由变化而高亮。具体是用了一段JS代码。
watch监视路由变化并触发一个method:changebar(),这个函数会获取当前的路由名,并把bottomNav的值设置为当前路由名——即高亮当前的路由页面 playerBox.vue组件之所以放在主组件里,就是为了音乐在每一个子页面都能播放,而不会因为跳转路由而停止播放。
2.popular.vue
这是推荐歌单界面,这里用到了一个轮播图插件,是基于vue的,使用起来比较方便,直接用npm安装:
npm install vue-awesome-swiper --save
安装好后,同样在main.js
中注册:
import VueAwesomeSwiper from 'vue-awesome-swiper'Vue.use(VueAwesomeSwiper)
然后我们来看页面的代码:
<template> <div class="popular"> <!-- navbar here --> <mu-appbar> <div class="logo"> iPlayer </div> </mu-appbar> <!-- banner here--> <mu-card> <swiper :options="swiperOption"> <swiper-slide v-for="(item,index) in banners" :key="index"> <mu-card-media> <img :src="item.pic"> </mu-card-media> </swiper-slide> <div class="swiper-pagination" slot="pagination"></div> </swiper> </mu-card> <div class="gridlist-demo-container" > <mu-grid-list class="gridlist-demo"> <mu-sub-header>热门歌单</mu-sub-header> <mu-grid-tile v-for="(item, index) in list" :key="index"> <img :src="item.coverImgUrl"/> <span slot="title">{{item.name}}</span> <mu-icon-button icon="play_arrow" slot="action" @click="getListDetail(item.id)"/> </mu-grid-tile> </mu-grid-list> </div> <div class="footer-rights"> <h4>版权归Godown Huang所有,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div></template><script>import {swiper,swiperSlide} from 'vue-awesome-swiper'import axios from 'axios'export default { name: 'popular', data(){ return{ swiperOption: { pagination: '.swiper-pagination', paginationClickable: true, autoplay: 4000, loop:true }, banners:[], list: [] } }, components: { swiper, swiperSlide }, computed:{ }, created(){ this.initPopular() }, methods:{ initPopular(){ axios.get('http://localhost:3000/banner').then(res=> { this.banners=res.data.banners; }), axios.get('http://localhost:3000/top/playlist/highquality?limit=8').then(res=> { this.list=res.data.playlists; }) }, getListDetail(id){ this.$router.push({path: '/songsList'}) this.$store.commit('playlist',id); } }}</script><style lang="css"> @media screen and (min-width: 960px){ .mu-card-media>img{ height: 400px!important; } .mu-grid-list>div:nth-child(n+2){ width:25%!important; } } .mu-grid-tile>img{ width: 100%; } .gridlist-demo-container{ display: flex; flex-wrap: wrap; justify-content: space-around; } .gridlist-demo{ width: 100%; overflow-y: auto; } .footer-rights>h4{ color: #e1e1e1; font-weight: 100; font-size:.056rem; height:90px; padding-top: 10px; text-align: center; }</style>
这里要说明一下,上面的这些组件除了
playerBox
之外都要在main.js中注册才能使用。注册方法忘记的了话,回头看看我之前写的todolist的项目是怎么注册的。
在store.js
中添加playList函数:
playlist(state,id){ const url='http://localhost:3000/playlist/detail?id='+id; axios.get(url).then(res=> { state.playlist=res.data.playlist; }) },
这里的页面mu
开头的基本都是用Muse-ui搭建起来的,Swiper
开头的则是轮播图插件。界面不复杂,主要是三个部分,上面的轮播图,中间的热门歌单推荐,底部的版权信息。样式基本是模板,这里做了一个简单的移动端适配:在PC端歌单会以每排4个分两排的形式排列,在移动端歌单则会以每排2个分四排的形式排列,适配的方法是媒体查询,通过改变歌单div
的宽度改变每行歌单的数目。
这里要注意的:
- 歌单的数据和轮播图都是用的网易云数据,所以没有开api是无法读取的,引入
axios
的部分可以先不写,也可以写好先放着。 - 这里
methods
和created
里面的内容都涉及到axios的请求,所以可以先不写,不影响样式呈现。数据可以先用假数据代替。 - playList的目的是点击歌单的时候,进入歌单详情页,同时根据传递进去的歌单id获取歌单的具体数据,axios的地址是api的地址,需要加载api插件才能使用。
3.play.vue
终于到了最核心的组件,之所以说它核心是因为这是播放界面,音频播放的长度、音频信息都会在这里被呈现,而播放器的核心功能——播放——也是在这里被操作(播放/暂停)。
看具体代码:
<template> <div class="play"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo"> iPlayer </div> </mu-appbar> <!-- player here--> <div class="bgImg"> <img :src="audio.picUrl" /> <!-- 封面CD --> <mu-avatar slot="left" :size="300" :src="audio.picUrl"/> </div> <div class="controlBar"> <mu-content-block> {{audio.songName}} - {{audio.singer}} </mu-content-block> <div class="controlBarSlide"> <span class="slideTime">{{audio.currentTime}}</span> <mu-slider v-bind:value="progressPercent" @change="editprogress" class="demo-slider"/> <span class="slideTime">{{audio.duration}}</span> </div> </div> </div></template><script>export default { name: 'play', data(){ return{ } }, components: { }, computed:{ audio(){ return this.$store.getters.audio; }, progressPercent(){ return this.$store.getters.audio.progressPercent; } }, methods:{ backpage(){ window.history.go(-1); }, editprogress(value){ this.$store.commit('editProgress',value) } }}</script><style lang="css"> @media screen and (max-width: 414px){ .bgImg .mu-avatar{ height: 260px!important; width: 260px!important; margin-left: -130px!important; } } .bgImg{ position:fixed; height:100%; width:100%; background: #fff; z-index:-1; } .bgImg>img{ width: 100%; filter:blur(15px); -webkit-filter: blur(15px); -moz-filter: blur(15px); -ms-filter: blur(15px); } .bgImg .mu-avatar{ position: absolute; left: 50%; margin-left: -150px; top: 30px; } .controlBar{ position: fixed; width: 100%; height: 180px; background: #fff; bottom: 0; z-index: 11; text-align:center; } .mu-slider{ width: 70%!important; display: inline-block!important; margin-bottom: -7px!important; } .slideTime{ width: 29px; display: inline-block; } .mu-content-block{ font-size: 18px; color: #777 } .mu-slider{ display: inline-block; margin:0 3px -7px; width: 70%; }</style>
store.js
添加代码:
play(state){ clearInterval(ctime); const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); if(playerBar.paused){ playerBar.play(); eve.innerHTML="pause"; state.audio.duration=duraMinute; state.audio.currentTime=currentMinute; ctime=setInterval( function(){ currentTime++; currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); },1000 ) }else { playerBar.pause(); eve.innerHTML="play_arrow"; clearInterval(ctime); } }, audioEnd(state){ const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; eve.innerHTML="play_arrow"; clearInterval(ctime); playerBar.currentTime=0; let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; }, editProgress(state,progressValue){ const playerBar=document.getElementById("playerBar"); const eve=$('.addPlus i')[0]; let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); // console.log(progressValue); clearInterval(ctime); if(playerBar.paused){ playerBar.play(); eve.innerHTML="pause" state.audio.duration=duraMinute; } let currentTime=playerBar.duration*(progressValue/100); ctime=setInterval( function(){ currentTime++; currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); },1000 ) playerBar.currentTime=currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); state.audio.currentTime=currentMinute; },
- 如代码所示,我在顶部导航添加了一个
icon button
,样式来自Muse-ui
绑定了一个点击事件backpage,点击后会回到上一个路由页面。这个需要配合之前的高亮底部导航icon,才能实现返回上一路由的同时高亮相对应的icon。 - 还要注意的是,computed里有两个方法,第一个是获取vuex里面的当前曲目信息;第二个则是获取进度条的百分比信息,这个方法实现了数据的双向绑定,随着后台设定的计时器,不断地更新,从而实现播放时进度条的变化。同样,这里的样式也是来自
Muse-ui
的Slider
。 - 这里有一个需要注意的坑是,Muse-ui自带了许多的函数,第一次写的时候没有注意,在进度条上绑定了一个
mouseup
事件,结果无效,后来才发现,其实已经自带了change
事件,还可以实现移动端的兼容。所以写代码的时候一定要多看看官网文档。 - 关于
store.js
里的方法,play
是播放/暂停,具体会根据当前音频文件的paused
(即是否暂停)来判断。总的原理是首先获取音频的持续时间,然后通过一个定时器,不断更新显示时间,播放完成时,计时器停止。 - 计时器很关键,进度条和显示时间的更新都需要它。但是计时器有个坑,如果把计时器声明放在
play
方法里,则无法在audioEnd
方法里停止计时器,所以这里我们需要在最外层先声明一个ctime
,然后再在play
方法里把定时器赋值给ctime
,这样我们就可以随时停止计时器了。 audioEnd
方法是播放停止时要做的事情,我们会把停止按钮切换成播放,把显示时间修改掉,别忘了停止计时器。editProgress
方法是点击或拖动进度条时做的事情,我们会改变当前音频的currentTime
,即当前时间,如果音频是暂停状态,我们要让它继续播放。
4.search.vue
这也是一个比较核心的一个功能,毕竟推荐的歌单只有几个。看代码:
<template> <div class="search"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo searchLogo"> iPlayer </div> <mu-text-field icon="search" class="appbar-search-field" slot="right" hintText="想听什么歌?" v-model="searchKey"/> <mu-flat-button color="white" label="搜索" slot="right" @click="getSearch(searchKey)"/> </mu-appbar> <!-- banner here--> <mu-list> <template v-for="(item,index) in result.songs"> <mu-list-item :title="item.name" @click="getSong(item.id,item.name,item.artists[0].name,item.album.name,item.artists[0].id)"> <mu-avatar slot="leftAvatar" backgroundColor="#fff" color="#bdbdbd">{{index+1}}</mu-avatar> <span slot="describe"> <span style="color: rgba(0, 0, 0, .87)">{{item.artists[0].name}} -</span> {{item.album.name}} </span> </mu-list-item> <mu-divider/> </template> </mu-list> <div class="footer-rights"> <h4>版权归Godown Huang所有,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div></template><script>export default { name: 'search', data(){ return{ searchKey:'' } }, computed:{ result(){ return this.$store.getters.result; } }, components: { }, methods:{ backpage(){ window.history.go(-1); }, getSearch(value){ this.$store.commit('getSearch',value); }, getSong(id,name,singer,album,arid){ this.$store.commit('getSong',{id,name,singer,album,arid}); this.$store.commit('play'); } }}</script><style lang="less"> @media screen and (max-width: 525px){ .searchLogo{ display: none; } .appbar-search-field{ width: 200px!important; } } .appbar-search-field { color: #FFF; margin-top: 10px; margin-bottom: 0; &.focus-state { color: #FFF; } .mu-icon { color: #FFF; } .mu-text-field-hint { color: fade(#FFF, 54%); } .mu-text-field-input { color: #FFF; } .mu-text-field-focus-line { background-color: #FFF; } } .footer-rights>h4{ color: #e1e1e1; font-weight: 100; font-size:.056rem; height:90px; padding-top: 10px; text-align: center; }</style>
在store.js
里添加:
getSearch(state,value){ const url='http://localhost:3000/search?keywords='+value+'?limit=30'; axios.get(url).then(res=>{ state.result=res.data.result; }) }, getSong(state,{id,name,singer,album,arid}){ const url="http://localhost:3000/music/url?id="+id; const imgUrl="http://localhost:3000/artist/album?id="+arid; const playerBar=document.getElementById("playerBar"); axios.get(url).then(res=>{ state.audio.location=res.data.data[0].url; state.audio.flag=res.data.data[0].flag; state.audio.songName=name; state.audio.singer=singer; state.audio.album=album; }) axios.get(imgUrl).then(res=>{ state.audio.picUrl=res.data.artist.picUrl; }) let currentTime=playerBar.currentTime; let currentMinute=Math.floor(currentTime/60)+":"+(currentTime%60/100).toFixed(2).slice(-2); let duraTime=playerBar.duration; let duraMinute=Math.floor(duraTime/60)+":"+(duraTime%60/100).toFixed(2).slice(-2); state.audio.duration=duraMinute; state.audio.currentTime=currentMinute; state.audio.progressPercent=((playerBar.currentTime/playerBar.duration)*100).toFixed(1); }
注意,在有需要使用
axios
的组件一定要import
,npm下载安装不用多说了。
解释一下这个组件的两个方法:
getSearch
是获取搜索结果,它被绑定再搜索按钮上,初始页面是空白,通过传递关键字,用axios
从api获取搜索结果,再把结果显示在页面上。getSong
绑定在每一个搜索的结果上,有两个步骤,第一是getSong
,会把点击的歌曲设置为要播放的歌曲,并把相关信息传递给play.vue
,让它显示在相应的地方;第二个步骤,会播放歌曲,也就是上面的play
方法,具体不必再说。- 这里有一个坑,我们可能需要通过vuex传递参数,但是有时候传递多个参数会出现
undefined
的情况,这时候我们要把参数们写成{参数一,参数二,参数三}
的形式。
5.songList
这个组件主要是歌单详情页,基本的样式和搜索页一样,就是获取歌单的内容不同,搜索页面的列表是根据关键词获取的,歌单详情页的列表是根据歌单id获取的,获取的方式都是通过axios。
<template> <div class="songsList"> <!-- navbar here --> <mu-appbar> <mu-icon-button icon="navigate_before" slot="left" v-on:click="backpage"/> <div class="logo"> iPlayer </div> </mu-appbar> <!-- banner here--> <div class="listBgImg"> <img :src="playlist.coverImgUrl" /> <!-- 封面CD --> <mu-avatar slot="left" :size="120" :src="playlist.coverImgUrl"/> </div> <mu-list> <mu-sub-header>{{playlist.name}}</mu-sub-header> <template v-for="(item,index) in playlist.tracks"> <mu-list-item :title="item.name" @click="getSong(item.id,item.name,item.ar[0].name,item.al.name,item.ar[0].id)"> <mu-avatar :src="item.al.picUrl" slot="leftAvatar"/> <span slot="describe"> <span style="color: rgba(0, 0, 0, .87)">{{item.ar[0].name}} -</span> {{item.al.name}} </span> </mu-list-item> <mu-divider/> </template> </mu-list> <div class="footer-rights"> <h4>版权归Godown Huang所有,请<a href="https://github.com/WE2008311">联系我</a>。</h4> </div> </div></template><script>export default { name: 'songsList', data(){ return{ } }, components: { }, computed:{ playlist(){ return this.$store.getters.playlist; } }, methods:{ backpage(){ window.history.go(-1); }, getSong(id,name,singer,album,arid){ this.$store.commit('getSong',{id,name,singer,album,arid}); this.$store.commit('play'); } }}</script><style lang="css"> .listBgImg{ height:200px; width:100%; background: #fff; overflow: hidden; } .listBgImg>img{ width: 100%; filter:blur(30px); -webkit-filter: blur(30px); -moz-filter: blur(30px); -ms-filter: blur(30px); } .listBgImg .mu-avatar{ position: absolute; left: 50%; margin-left: -60px; top: 130px; } .mu-list .mu-sub-header{ /* position: absolute; */ top: 260px; font-size: 16px; /* text-align: center; */ }</style>
没什么需要解释的,注意我们在getSong
里面传递的多个参数。
6.playerBox.vue
<template> <div class="playerBox"> <audio ref="myAudio" :src="audio.location" @ended="audioEnd" id="playerBar"></audio> <div class="controlBarBtn" v-show="judgement()"> <mu-icon-button icon="skip_previous"/> <mu-icon-button class="addPlus" icon="play_arrow" @click="play"/> <mu-icon-button icon="skip_next"/> </div> </div></template><script>export default { name: 'playerBox', data(){ return{ } }, components: { }, computed:{ audio(){ return this.$store.getters.audio; } }, methods:{ play(){ this.$store.commit('play'); }, audioEnd(event){ this.$store.commit('audioEnd',event); }, judgement(){ let path=this.$route.path; if(path=="/play"){ return true; }else{ return false; } } }}</script><style lang="less" > .controlBarBtn{ position: absolute; z-index:12; width: 243px; margin-left: -121.5px; top: 83%; left: 50%; } .controlBarBtn i.mu-icon{ font-size: 36px; color: #03a9f4; left: 50%; margin-left: -18px; position: absolute; top: 10%; } .controlBarBtn .addPlus{ top: 16px; width: 80px!important; height: 80px!important; margin: 0 30px!important; } .controlBarBtn .addPlus i.mu-icon{ font-size: 60px; margin-left: -30px; top: 10%; }</style>
这个页面比较简单,播放器audio
标签,绑定了ended事件,即播放完成后执行。
这里有一个坑,解释一下:我把播放器按钮放在这里了,为什么呢?之前我是放在play.vue
里的,但是我发现一个问题,就是通过点击歌单的歌曲播放时,无法改变播放/暂停按钮,为什么呢?因为我改变按钮的方法是用innerHTML
改变,我为什么要用这种方法呢?因为Muse-ui的icon经过渲染,是以标签的值的形式出现的。这就不得不获取DOM了,但是如果把按钮写在play.vue
里,在歌单页面时是获取不到指定DOM的,因为当前页面根本没有这个DOM!只有把按钮写在在主组件里的playerBox.vue
里,才能获取到指定DOM。
但是写在playBox.vue
里又有一个问题,按钮会出现在每一个页面里,但是我们只要它出现在播放页面就好了,所以我们在这里要给按钮绑定一个v-show
,里面的内容就是判断是不是在指定路由,如果是播放页面,就显示按钮,不是,就隐藏按钮。
axios和网易云api
axios具体的配置我都在上面讲了,这里介绍一款网易云的api和使用方法。
文档在此
介绍一下使用方法,进入git把它下下来,在命令行执行:
$ node app.js
在浏览器输入地址:
localhost:3000
看到弹出的页面就说明服务器启动成功了。然后我们可以在文档里查到具体请求的数据,比如banner啊,歌单啊,搜索啊,都能请求。我们看到前面写的axios请求里的地址,都是具体请求的地址。
这里要注意的是,这个api默认的是没有开启跨域的,看app.js
里有一段被隐藏的代码就是跨域的相关设置,解除隐藏即可。
bug和未实现功能
目前还存在一个比较大的bug,就是在歌单点击播放时,点击第一次因为没办法获取个去的url,无法播放,只有再点击一次才能播放,这个bug暂时还没有时间解决,会尽快解决。
然后目前还没有实现的功能是播放列表,自然上一曲/下一曲按钮也没有用了,歌曲播放一遍也就停止了,这个功能不算难,抽空把它做出来。
参考资料
这个app参考了一些技术文章,给了我很大的启发,附上链接。
用vue全家桶写一个“以假乱真”的网易云音乐
DIY 一个自己的音乐播放器 2.0 来袭
结语
这个app前前后后,磨磨蹭蹭做了两个月,好歹总算是做完了。学习还是得找项目来做,虽然这个项目还很简陋,但是还是get到很多知识点,对于我的提高还是蛮大的。
这种项目不算难,写过的人也多,所以百分之八十的问题都能百度出来,剩下的百分之二十,技术社区里提个问基本能够解决。项目还是得自己写一遍,写的过程中才能发现问题,也才能想办法找到解决办法,事情总是会比你想象的要简单一点。
项目不算大,但要一步步写下来总有可能有所遗漏,这里是我的GitHub,大家可以对照着看看有没有遗漏。如果你喜欢我的项目,也希望star或者fork一波~
- 用Vue搭建一个应用盒子(三):音乐播放器
- 用Vue搭建一个应用盒子(一):todo-list
- 用Vue搭建一个应用盒子(二):datetime-picker
- 用Vue搭建一个网易云播放器(一)
- Vue.js写一个音乐播放器.A music player by Vue.js
- 解析service(三):音乐播放器
- 本地音乐播放器(三)
- 如何写一个正经的Android音乐播放器 三
- 用vue-cli 与vuex一步一步搭建一个笔记应用(三)
- vue全家桶-音乐播放器
- 一个功能齐全的IOS音乐播放器应用源码
- 一个功能齐全的IOS音乐播放器应用源码
- Android应用开发-MP3音乐播放器代码实现(三)
- Android应用开发-MP3音乐播放器代码实现(三)
- 一个 Flash 音乐播放器
- 建立一个音乐播放器
- 一个Java音乐播放器
- 实现一个音乐播放器
- 笔试题——Unix路径简化
- stm32f103x建工程错误问题
- 回文字符串
- python总结(二):控制台中文乱码的解决办法
- jQuery实现固定导航栏效果
- 用Vue搭建一个应用盒子(三):音乐播放器
- 201612-1中间数
- 视觉系统选型及系统搭建—工业相机篇
- 【C++】单链表的增删查改实现
- 算法面试题
- Week 2算法分析作业
- 各种RAM、Rom类型
- 数据库设计
- 一维小波多尺度分解及重构的MATLAB实现