仿网易云音乐(微信小程序版)

1 篇文章 5 订阅
订阅专栏

项目部分截图(Gif)




前言

前一阵子学习了微信小程序,为了巩固所学的知识和提高实战经验,决定自己手撸一款小程序。因为听歌一直在用网易云音乐,所以突发奇想就做一款仿网易云音乐的小程序吧!开发中遇到了很多在学习中没有遇到过的坑,也很感谢在我改不出BUG时给予帮助的老师同学!本着学习和分享的目的,写下以下的文字,希望能给初学小程序的你带来一点帮助,大佬轻点喷。


关于项目的使用

本文的最后我贴出了项目的源码地址,需要的可以去GitHub上clone。完成后直接在微信开发者工具上打开就能使用。

开发前准备

  • VScode代码编辑器。
  • 微信开发者工具
  • ios网易云音乐(V5.9.1版本)
  • 酷狗音乐小程序(提供了一些思路)
  • 网易云音乐API
  • ( 阿里巴巴矢量图标库)提供一些图标icon

tabBar部分

自定义tabBar

一般在开发中,微信小程序给我们的tabBar就能满足需求。但是,有些特别的需求必须使用自定义tabBar才能满足。
比如tabBar实现半透明。那么,如何才能自定义tabBar呢?
1.首先,在 app.json里的"tabBar"里声明
"tabBar": { "custom": true }
2.接着在项目的根目录下新建一个custom-tab-bar文件夹。里面包含index.wxml index.js index.json index.wxss四个文件。更多细节参考 微信小程序文档

<!-- index.html -->

<!-- 自定义tabbar页面 -->
<cover-view class="tab-bar">
   <cover-view class="tab-bar-border"></cover-view><!--tabBal边框样式  -->
<!-- 乐库tabbar -->
  <cover-view class='tab-bar-item' >
    <cover-image src='../images/music.png' hidden='{{isShow_index}}' bindtap='switchTab_index'></cover-image>
    <cover-image src='../images/selected-music.png' hidden='{{!isShow_index}}' bindtap='switchTab_index'></cover-image>
    <cover-view style="color:{{isShow_index ? selectedColor : color}}">乐库</cover-view>
  </cover-view>

<!-- 播放tabbar -->
    <cover-view class='tab-bar-item' bindtap='switchTab_playing'>
    <cover-image src='../images/selected-playing.png' hidden='{{isShow_playing}}'></cover-image>
    <cover-image src='../images/playing.png' hidden='{{!isShow_playing}}'></cover-image>
    <cover-view></cover-view>
  </cover-view>

<!-- 我的tabbar -->
    <cover-view class='tab-bar-item' bindtap='switchTab_me'>
    <cover-image src='../images/me.png' hidden='{{isShow_me}}'></cover-image>
    <cover-image src='../images/selected-me.png' hidden='{{!isShow_me}}'></cover-image>
    <cover-view style="color:{{isShow_me ? selectedColor : color}}">我的</cover-view>
  </cover-view>
</cover-view>
// index.js
Component({
  data: {
    isShow_index:true,
    isShow_playing:false,
    isShow_me:false,
    selected: 0, //首页
    color: "#8D8D8D",
    selectedColor: "#C62F2F",
    list: [{
      pagePath: "/pages/index/index",
      iconPath: "/images/music.png",
      selectedIconPath: "/images/selected-music.png",
      text: "乐库"
    }, {
      pagePath: "/pages/love/love",
        iconPath: "/images/selected-playing.png",
      selectedIconPath: "/images/playing.png",
      text: ""
    },
      {
        pagePath: "/pages/me/me",
        iconPath: "/images/me.png",
        selectedIconPath: "/images/selected-me.png",
        text: "我的"
      }]
  },

  methods: {
    switchTab_index:function(){
      wx.switchTab({
        url:'/pages/index/index'
      })
      this.setData({
        isShow_index: true,
        isShow_me: false,
        isShow_playing: false
      })
    },
    switchTab_playing: function () {
      wx.switchTab({
        url: '/pages/love/love'
      })
      this.setData({
        isShow_playing: true,
        isShow_index: false,
        isShow_me: false
      })
    },
    switchTab_me: function () {
      wx.switchTab({
        url: '/pages/me/me'
      })
      this.setData({
        isShow_me:true,
        isShow_playing: false,
        isShow_index: false
      })
    }
  }
})

tabBar半透明

/* custom-tab-bar/index.wxss */
.tab-bar {
  height:7%;
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  height: 48px;
  background:#FAFBFD;
  opacity: 0.93;
  display: flex;
  padding-bottom: env(safe-area-inset-bottom);
}

API封装

一般我们https请求都是通过wx.request来请求,但是这种方法只能请求一次数据,如果首页用wx.request来请求的话,代码看起来会很冗长和杂乱。不仅自己容易搞糊涂,其他人看代码时也会很累。因此为了代码的整洁干净,我在这里新建了一个文件专门存放API。一般在根目录下的utils文件夹下新建一个api.js,但我在根目录下新建了文件夹API,里面包含api.js

// api.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const request = (url, data) => { 
  let _url = API_BASE_URL  + url;
  return new Promise((resolve, reject) => {
    wx.request({
      url: _url,
      method: "get",
      data: data,
      header: {
        'Content-Type': 'application/x-www-form-urlencoded'
      },
      success(request) {
        resolve(request.data)
        
      },
      fail(error) {
        reject(error)
      }
    })
  });
}


module.exports ={
  gethotsongs:(data) =>{
    return request('/search/hot',data)//热搜接口
  },
  searchSuggest:(data)=>{
    return request('/search/suggest',data)//搜索建议接口
  },
  searchResult:(data)=>{
    return request('/search',data)//搜索结果接口
  },
  getBanner:(data)=>{
    return request('/banner',data)//个性推荐轮播
  },
  getsongsheet:(data)=>{
    return request('/top/playlist',data)//热门歌单接口
  },
  getNewSong:(data)=>{
    return request('/personalized/newsong',data)//最新音乐接口
  },
  getDjRadios:(data)=>{
    return request('/dj/recommend',data)//电台推荐接口
  },
  getProgramRecommend:(data)=>{
    return request('/program/recommend',data)//推荐节目接口
  },
  getRecommendType:(data)=>{
    return request('/dj/recommend/type',data)//所有电台分类推荐
  },
  getRecommendMV:(data)=>{
    return request('/personalized/mv',data)//推荐MV
  },
  getNewMv:(data)=>{
    return request('/mv/first',data)//最新MV
  },
  getNewEst:(data)=>{
    return request('/album/newest',data)//最新专辑
  },
  getTopList:(data)=>{
    return request('/top/list',data)//排行榜
  },
  getDjList:(data)=>{
    return request('/dj/catelist',data) //电台分类
  },
  getPay:(data)=>{
    return request('/dj/paygift',data)//付费精品
  },
  getSonger:(data)=>{
    return request('/toplist/artist',data)//歌手排行
  }
}

api.js只能通过module.exports来暴露,那个页面要数据就从这拿。如果在哪个页面要用到它,还需要在头部引入一下:

const API = require('../../API/api')

以个性推荐轮播图为例,

  getBanner: function() {
    API.getBanner({
      type: 2
    }).then(res => {
      if (res.code === 200) { //更加严谨
        this.setData({
          banner: res.banners
        })
      }
    })
  }

这样就把请求到的数据存储到banner中了。


搜索部分

输入框样式


我这里是引入了WEUI的样式,
1.下载weui.wxss,链接我找不到了,所以我放上了我的 github上的weui.wxss
2.把下载好的weui.wxss放到根目录下。
3.在app.wxss@import "weui.wxss";引入一下就可以使用微信提供给我们的样式了。
4. WeUI样式库

热门搜索


上面已经提到我从api.js中拿数据。

 // 从接口到获取到数据导入到hotsongs
  gethotsongs() {
    API.gethotsongs({ type: 'new' }).then(res => {
      wx.hideLoading()
      if (res.code === 200) {  //严谨
        this.setData({
          hotsongs: res.result.hots
        })
      }
    })
  }

搜索历史


思路:当在输入框输入完成后–>失去焦点–> 利用wx.setStorageSync存进缓存中–>wx.getStorageSync获取到并把它打印出来。

  // input失去焦点函数
  routeSearchResPage: function(e) {
    console.log(e.detail.value)
    let history = wx.getStorageSync("history") || [];
    history.push(this.data.searchKey)
    wx.setStorageSync("history", history);
  },

//每次显示变动就去获取缓存,给history,并for出来。
  onShow: function () {
    this.setData({
      history: wx.getStorageSync("history") || []
    })
  },

清空搜索历史


思路:×图标绑定事件->呼出对话框wx.showModal->确定则把history赋值为空

  // 清空page对象data的history数组 重置缓存为[]
clearHistory: function() {
  const that = this;
  wx.showModal({
    content: '确认清空全部历史记录',
    cancelColor:'#DE655C',
    confirmColor: '#DE655C',
    success(res) {
      if (res.confirm) {
        that.setData({
          history: []
        })
        wx.setStorageSync("history", []) //把空数组给history,即清空历史记录
      } else if (res.cancel) {
      }
    }
  })
},

实时搜索建议


思路:实时获取输入框的值->把值传给搜索建议API,发起网络请求->请求之后拿到搜索建议->打印结果并隐藏其他组件只保留搜索建议的组件(类似于Vue里的v-show)

 //获取input文本并且实时搜索,动态隐藏组件
  getsearchKey:function(e){
    console.log(e.detail.value) //打印出输入框的值
    let that = this;
    if(e.detail.cursor != that.data.cursor){ //实时获取输入框的值
      that.setData({
        searchKey: e.detail.value
      })
    }
    if(e.value!=""){ //组件的显示与隐藏
      that.setData({
        showView: false
      })
    } else{
      that.setData({
        showView: ""
      })
    }
    if(e.detail.value!=""){ //解决 如果输入框的值为空时,传值给搜索建议,会报错的bug
      that.searchSuggest();
    }  
  }
// 搜索建议
searchSuggest(){
  API.searchSuggest({ keywords: this.data.searchKey ,type:'mobile'}).then(res=>{
    if(res.code === 200){
      this.setData({
        searchsuggest:res.result.allMatch
      })
    }
  })
}

点击热搜或历史,执行搜索


思路:关键是event,点击通过e.currentTarget.dataset.value拿到所点击的值,再交给其他方法执行搜索行为。

// 点击热门搜索值或搜索历史,填入搜索框
  fill_value:function(e){
    let that = this;
    console.log(history)
    // console.log(e.currentTarget.dataset.value)
    that.setData({
      searchKey: e.currentTarget.dataset.value,//点击吧=把值给searchKey,让他去搜索
      inputValue: e.currentTarget.dataset.value,//在输入框显示内容
      showView:false,//给false值,隐藏 热搜和历史 界面
      showsongresult: false, //给false值,隐藏搜索建议页面
    })
    that.searchResult(); //执行搜索功能
  }

搜索结果


思路:输入结束->确认键->调用searchResult请求到结果

// 搜索完成点击确认
  searchover:function(){
    let that = this;
    that.setData({
      showsongresult: false
    })
    that.searchResult();
  }
 // 搜索结果
searchResult(){
  console.log(this.data.searchKey)
  API.searchResult({ keywords: this.data.searchKey, type: 1, limit: 100, offset:2 }).then(res => {
    if (res.code === 200) {
      this.setData({
        searchresult: res.result.songs
      })
    }
  })
}

乐库部分


乐库部分其实没什么逻辑很难的部分,以结构和样式为主,在这里就不赘述了。可以到我的 github上查看。在这里分享一些小功能的实现和踩到的坑。

个性推荐,主播电台切换


1.个性推荐和主播电台是两个swiper-item所以他们才可以左右滑动,就像轮播图一样,不过轮播图放的是图片,而这里放的是整个页面。
2.我要实现的效果是左右滑动的同时,个性推荐主播电台下面的白色方块也要跟着滑动。
1. 第一种方法
给包裹两个swiper-itemswiper添加一个bindchange="changeline"事件,把事件对象event打印出来发现,console.log(e.detail.current),当我们左右滑动的时候cuurrent的值会在01之间切换。所以我给白色方块添加

class="{{changeline?'swiper_header_line_before':'swiper_header_line_after'}}"
    if(e.detail.current === 0){
    this.setData({
       changeline:true
      })
    }else{
    this.setData({
       changeline:false
      })
    }

current为0,即页面在个性推荐时,让changelinetrue;当current为1,即页面在主播电台时,让changelinefalse;为true时,给白色方块加持swiper_header_line_before的样式,为false时,加持swiper_header_line_after的样式。这样就可以跟随swiper-item的滑动而切换了。但是,这种切换方式太僵硬了,没有那种流畅的切换效果,而且不适合多swiper-item页面。
2. 第二种方法


让一半宽度,四分之一宽度设置为变量是为了兼容不同的手机型号。因为写死数据肯定会有BUG,所以才要计算宽度。

<view class="weui-navbar-slider" style="transform:translateX({{slideOffset}}px);"></view>
.weui-navbar-slider{
  width:28px;
  height: 5px;
  background: #ffffff;
  border-radius:10rpx;
  transition: transform .6s;
 }

slideOffset为变量,动态接受从data传来的数据。

onLoad:function(){
    wx.getSystemInfo({
      success: function (res) {
        // console.log(res.windowWidth)
        // console.log(res.windowWidth / 2 / 2)
        half = res.windowWidth / 2 ;
        quarter = res.windowWidth / 2 / 2;
        that.setData({
          slideOffset: quarter - 14 //onLoad的时候让 quarter - 14 给slideOffset,即一开始就让他在个性推荐的下面,否则onLoad的时候一开始在0的位置
        })
      }
    })
}

  changeline:function(e){
    // console.log(e)
    // console.log(e.detail.current)
    let current = e.detail.current; //获取swiper的current值
    if(e.detail.current === 0){
      this.setData({
        slideOffset: quarter - 14
      })
    }
    if(e.detail.current === 1){
      this.setData({
        slideOffset: (quarter - 14) + half
      })
    }
    if(e.detail.current === null){
      this.setData({
        slideOffset: quarter - 14
      })
    }
  }

MV播放


主要是结构和样式,我直接上代码了。

<!-- play_mv.wxml -->
<view class="mv_box">
    <video src="{{mv.brs['480']}}" class="mv" autoplay="{{autoplay}}" loop="{{loop}}" direction="{{0}}" show-fullscreen-btn="{{showfullscreenbtn}}"
    show-center-play-btn="{{showcenterplaybtn}}" enable-progress-gesture="{{enableprogressgesture}}" show-mute-btn="{{showmutebtn}}" title="{{mv.name}}"
    play-btn-position="{{center}}" object-fit="{{objectfit}}"></video>
</view>

<view class="mv_name">{{mv.name}}</view>
<view class="mv_time"> 发行:  {{mv.publishTime}}</view>
<view class="mv_time mv_times">播放次数:  {{mv.playCount}}</view>
<view class="mv_time mv_desc">{{mv.desc}}</view>
<view class="mv_time mv_desc mv_other">点赞: {{mv.likeCount}}</view>
<view class="mv_time mv_desc mv_other">收藏: {{mv.subCount}}</view>
<view class="mv_time mv_desc mv_other">评论: {{mv.commentCount}}</view>
<view class="mv_time mv_desc mv_other">分享: {{mv.shareCount}}</view>
/* play/play_mv.wxss */
.mv_box{
    width: 100%;
    height: 480rpx;
    margin-top:-2rpx;
}
.mv{
    width: 100%;
    height: 100%;
    border-radius:15rpx;
}
.mv_name{
    margin-top:20rpx;
    margin-left:20rpx;
}
.mv_time{
    font-size: 12px;
    margin-left:20rpx;
    color:#979798;
    display:initial;
}
.mv_times{
    margin-left: 100rpx;
}
.mv_desc{
    display: block;
    color:#6A6B6C;
}
.mv_other{
    display: block;
}
// play_mv.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
  data: {
    mv: [],
    autoplay: true,
    loop: true,
    showfullscreenbtn: true,
    showcenterplaybtn: true,
    enableprogressgesture: true,
    showmutebtn: true,
    objectfit: 'contain',
  },
  onLoad: function (options) {
    // console.log(mv_url);
    const mvid = options.id; // onLoad()后获取到歌曲视频之类的id

    // 请求MV的地址,失败则播放出错,成功则传值给createBgAudio(后台播放管理器,让其后台播放)
    wx.request({
      url: API_BASE_URL + '/mv/detail',
      data: {
        mvid: mvid    
      },
      success: res => {
        console.log(res.data.data.brs['480'])
        console.log('歌曲音频url:', res)
        if (res.data.data.brs === null) {  //如果是MV 电台 广告 之类的就提示播放出错,并返回首页
          console.log('播放出错')
          wx.showModal({
            content: '服务器开了点小差~~',
            cancelColor: '#DE655C',
            confirmColor: '#DE655C',
            showCancel: false,
            confirmText: '返回',
            complete() {
              wx.switchTab({
                url: '/pages/index/index'
              })
            }
          })
        } else {
          this.setData({
            mv: res.data.data
          })
        }
      }
    })
  },
})

歌手榜

// 歌手榜的js
const API = require('../../API/api');
const app = getApp();
Page({

  data: {
    songers: [], //歌手榜
  },

  onLoad: function (options) {
    wx.showLoading({
      title: '加载中',
    });
    this.getSonger();
  },

  getSonger: function () {
    API.getSonger({}).then(res => {
      wx.hideLoading()
      this.setData({
        songers: res.list.artists.slice(0, 100)
      })
    })
  },
  handleSheet: function (event) { //event 对象,自带,点击事件后触发,event有type,target,timeStamp,currentTarget属性
    const sheetId = event.currentTarget.dataset.id; //获取到event里面的歌曲id赋值给audioId
    wx.navigateTo({                                 //获取到id带着完整url后跳转到play页面
      url: `./moremore_songer?id=${sheetId}`
    })
  },
})
<!-- 歌手榜结构 -->
<view wx:for="{{songers}}" wx:key="" class='songer_box' data-id="{{item.id}}" bindtap='handleSheet'>
  <view class='songer_index_box'>
    <text class='songer_index'>{{index + 1}}</text>
  </view>
  <view class='songer_img_box'>
  <image src="{{item.picUrl}}" class='songer_img'></image>
  </view>
  <view class='songer_name_box'>
  <text class='songer_name'>{{item.name}}</text>
  <text class='songer_score'>{{item.score}}热度</text>
  </view>
</view>
// 歌手下级路由歌曲列表
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
  data: {
    songList: []
  },
  onLoad: function (options) {
    wx.showLoading({
      title: '加载中',
    });
    const sheetId = options.id;
    wx.request({
      url: API_BASE_URL + '/artists',
      data: {
        id: sheetId    
      },
      success: res => {
        const waitForPlay = new Array;
        for (let i = 0; i <= res.data.hotSongs.length - 1; i++) { //循环打印出其id
          waitForPlay.push(res.data.hotSongs[i].id) //循环push ID 到waitForPlay数组
          app.globalData.waitForPlaying = waitForPlay  //让waitForPlay数组给全局数组
          // console.log(app.globalData.waitForPlaying)
        }
        wx.hideLoading()
        console.log(res.data.hotSongs)
        this.setData({
          songList: res.data.hotSongs
        })
      }
    })
  },
  handlePlayAudio: function (event) { //event 对象,自带,点击事件后触发,event有type,target,timeStamp,currentTarget属性
    const audioId = event.currentTarget.dataset.id; //获取到event里面的歌曲id赋值给audioId
    wx.navigateTo({                                 //获取到id带着完整url后跳转到play页面
      url: `../../play/play?id=${audioId}`
    })
  }
})
<!-- more/more_songer/moremore_songer.wxml歌手下面的歌曲 -->
<view class='search_result_songs'>
  <view wx:for="{{songList}}" wx:key="" class='search_result_song_item songer_box' data-id="{{item.id}}" bindtap='handlePlayAudio'>
    <view class='songer_index_box'>
      <text class='songer_index'>{{index + 1}}</text>
    </view>
    <view class='songer_img_box'>
      <view class='search_result_song_song_name'>{{item.name}}</view>
      <view class='search_result_song_song_art-album'>{{item.ar[0].name}} - {{item.al.name}}</view>
    </view>
  </view>
</view>

推荐歌单


因为样式与排行榜类似,所以只放出图片,源码可以到我的 github上查看。

榜单排行


请查看源码

换一换功能


思路:绑定点击事件->选取随机的三个数->给空值->push三个随机数进数组中->重新赋值。

  // 换一换
  change_1:function(){
    let maxNum = this.data.more_recommend_create.length  //计算数据长度
    let r1 = parseInt(Math.random() * (maxNum - 0) + 0); //取【0-数据长度】内的整数随机数
    let r2 = parseInt(Math.random() * (maxNum - 0) + 0);
    let r3 = parseInt(Math.random() * (maxNum - 0) + 0);
    this.setData({
      recommend_create: []
    })
    //重新取3组数据
    this.data.recommend_create.push(this.data.more_recommend_create[r1])
    this.data.recommend_create.push(this.data.more_recommend_create[r2])
    this.data.recommend_create.push(this.data.more_recommend_create[r3])
    //重新赋值
    this.setData({
      recommend_create: this.data.recommend_create
    })
  }

播放界面


图片太大,因此加快了播放。

播放功能

思路:利用data-id="{{item.id}}"获取到歌曲ID放在event中-> 通过event对象事件获取ID并跳转到播放页面 ->wx.request获取到歌曲的音频地址及detail->背景音频管理器 wx.getBackgroundAudioManager()->播放
以歌手榜下级路由歌曲列表为例,

<view wx:for="{{songList}}" wx:key="" class='search_result_song_item songer_box' data-id="{{item.id}}" bindtap='handlePlayAudio'>
  handlePlayAudio: function (event) { //event 对象,自带,点击事件后触发,event有type,target,timeStamp,currentTarget属性
    const audioId = event.currentTarget.dataset.id; //获取到event里面的歌曲id赋值给audioId
    wx.navigateTo({                                 //获取到id带着完整url后跳转到play页面
      url: `../../play/play?id=${audioId}`
    })
  }
// play.js
const API_BASE_URL = 'http://musicapi.leanapp.cn';
const app = getApp();
Page({
data: {
  isPlay: '',
  song:[],
  innerAudioContext: {},
  show:true,
  showLyric:true,
  songid:[],
  history_songId:[]
},
onLoad: function (options) {
  const audioid = options.id; // onLoad()后获取到歌曲视频之类的id
  this.play(audioid); //把从wxml获取到的值传给play()
},
play: function (audioid){
  const audioId = audioid;
  app.globalData.songId = audioId;  //让每一个要播放的歌曲ID给全局变量的songId
  const innerAudioContext = wx.createInnerAudioContext();
  this.setData({
    innerAudioContext,
    isPlay: true
  })
  // 请求歌曲音频的地址,失败则播放出错,成功则传值给createBgAudio(后台播放管理器,让其后台播放)
  wx.request({
    url: API_BASE_URL + '/song/url',
    data: {
      id: audioId
    },
    success: res => {
      if (res.data.data[0].url === null) {  //如果是MV 电台 广告 之类的就提示播放出错,并返回首页
        wx.showModal({
          content: '服务器开了点小差~~',
          cancelColor: '#DE655C',
          confirmColor: '#DE655C',
          showCancel: false,
          confirmText: '返回',
          complete() {
            wx.switchTab({
              url: '/pages/index/index'
            })
          }
        })
      } else {
        this.createBgAudio(res.data.data[0]);
      }
    }
  })
  //获取到歌曲音频,则显示出歌曲的名字,歌手的信息,即获取歌曲详情;如果失败,则播放出错。
  wx.request({
    url: API_BASE_URL + '/song/detail',
    data: {
      ids: audioId    //必选参数ids
    },
    success: res => {
      if (res.data.songs.length === 0) {
        wx.showModal({
          content: '服务器开了点小差~~',
          cancelColor: '#DE655C',
          confirmColor: '#DE655C',
          showCancel: false,
          confirmText: '返回',
          complete() {
            wx.switchTab({
              url: '/pages/index/index'
            })
          }
        })
      } else {
        this.setData({
          song: res.data.songs[0],  //获取到歌曲的详细内容,传给song
        })
        app.globalData.songName = res.data.songs[0].name;
      }
    },
  })
},
createBgAudio(res) {
  const bgAudioManage = wx.getBackgroundAudioManager(); //获取全局唯一的背景音频管理器。并把它给实例bgAudioManage
  app.globalData.bgAudioManage = bgAudioManage;         //把实例bgAudioManage(背景音频管理器) 给 全局
  bgAudioManage.title = 'title';                        //把title 音频标题 给实例
  bgAudioManage.src = res.url;                          // res.url 在createBgAudio 为 mp3音频  url为空,播放出错
  const history_songId = this.data.history_songId
  const historySong = {
    id: app.globalData.songId,
    songName:app.globalData.songName
  }
  history_songId.push(historySong)
  bgAudioManage.onPlay(res => {                         // 监听背景音频播放事件
    this.setData({
      isPlay: true,
      history_songId
    })
  });
  bgAudioManage.onEnded(() => {                  //监听背景音乐自然结束事件,结束后自动播放下一首。自然结束,调用go_lastSong()函数,即歌曲结束自动播放下一首歌
    this.go_lastSong();
  })
  wx.setStorageSync('historyId', history_songId); //把historyId存入缓存
},
})

暂停/播放

  <!-- 暂停播放图标 -->
<view class="play_suspend">
  <view class="icon_playing"><image bindtap="handleToggleBGAudio" src="../images/suspend.png" hidden="{{!isPlay}}" class="{{'img_play_suspend'}}" />  <!-- 暂停图标-->
  <image bindtap="handleToggleBGAudio" src="../images/play.png" hidden="{{isPlay}}" class="{{'img_play_suspend'}}" /></view> <!--播放图标-->
</view>
// 播放和暂停
handleToggleBGAudio() {
  // const innerAudioContext = app.globalData.innerAudioContext;
  const bgAudioManage = app.globalData.bgAudioManage;
  const {isPlay} = this.data;
  if (isPlay) {
    bgAudioManage.pause();
    // innerAudioContext.pause();handleToggleBGAudio
  } else {
    bgAudioManage.play();
    // innerAudioContext.play();
  }
  this.setData({
    isPlay: !isPlay
  })
  console.log(this.data.isPlay)
}

上一首/下一首(随机播放)

思路:点击歌单或歌手页,获取到对应的歌单/歌手id->wx.request请求数据获取到所有的歌单内/歌手热门歌曲音频地址->给全局变量globalData->点击上一首/下一首随机获取到全局变量的一则数据->给play()方法->播放

<!--歌单-->
  onLoad: function (options) {
  wx.showLoading({
    title: '加载中',
  });
  const sheetId = options.id;
  wx.request({
    url: API_BASE_URL + '/playlist/detail',
    data: {
      id: sheetId    
    },
    success: res => {
      const waitForPlay = new Array;
      for (let i = 0; i <= res.data.playlist.trackIds.length - 1;i++){ //循环打印出其id
        waitForPlay.push(res.data.playlist.trackIds[i].id) //循环push ID 到waitForPlay数组
        app.globalData.waitForPlaying = waitForPlay  //让waitForPlay数组给全局数组
      }
      wx.hideLoading()
      this.setData({
        songList: res.data.playlist.tracks
      })  
    }
  })
}
<view class="icon_playing "><image src="../images/lastSong.png" class=" icon_play" bindtap="go_lastSong" /></view>
<view class="icon_playing "><image src="../images/nextSong.png" class=" icon_play" bindtap="go_lastSong" /></view>
  go_lastSong:function(){ 
  let that = this;
  const lastSongId = app.globalData.waitForPlaying;
  const songId = lastSongId[Math.floor(Math.random() * lastSongId.length)]; //随机选取lastSongId数组的一个元素
  that.data.songid = songId;
  this.play(songId)//传进play()方法中
  app.globalData.songId=songId;
}

歌词/封面切换


因为网易云API的歌词接口崩溃,请求不到歌词,所以我只能把歌词写死为纯音乐,请欣赏。类似于v-show

 <!-- 封面 -->
<!-- 一开始onload时,showLyric=true, 显示为转动的图标,点击图标,切换为歌词-->
<view class="sing-show" bindtap="showLyric" >
  <view class="moveCircle {{isPlay ? 'play' : ''}}" hidden="{{!showLyric}}">
    <image src="{{song.al.picUrl}}" class="coverImg {{isPlay ? 'play' : ''}}" hidden="{{!showLyric}}"/>
  </view>
  <text  hidden="{{showLyric}}" class="songLyric">纯音乐,请欣赏</text>
</view>
  // 点击切换歌词和封面
showLyric(){
  const {showLyric} = this.data;
  this.setData({
    showLyric: !showLyric
  })
}

破产版的孤独星球动效


封面旋转:

@keyframes rotate {
  0%{
    transform: rotate(0);
  }
  100%{
    transform: rotate(360deg);
  }
}

扩散的圆形线条:
其实就是外面套一个盒子,盒子宽高变大以及透明度逐渐变低。

@keyframes moveCircle {
  0%{
    width: 400rpx;
    height: 400rpx;
    border: 1px solid rgba(255, 255, 255, 1)
  }
  30%{
    width: 510rpx;
    height: 510rpx;
    border: 1px solid rgba(255, 255, 255, 0.8)
  }
  50%{
    width: 610rpx;
    height: 610rpx;
    border: 1px solid rgba(255, 255, 255, 0.6)
  }
  80%{
    width: 700rpx;
    height: 700rpx;
    border: 1px solid rgba(255, 255, 255, 0.4)
  }
  99%{
    width: 375px;
    height: 375px;
    border: 1px solid rgba(255, 255, 255, 0.1)
  }
  100%{
    width: 0px;
    height: 0px;
    border: 1px solid rgba(255, 255, 255, 0)
  }
}

背景毛玻璃

<!-- play.wxml -->
<image src="{{song.al.picUrl}}" class="background_img" ></image>
/* 播放界面毛玻璃效果 */
.background_img{ 
  position: fixed;
  top: 0;
  left: 0;
  bottom: 0;
  width: 100%;
  height: 100%;
  filter: blur(20px);
  z-index: -1;
  transform: scale(1.5); /*和网易云音乐对比了一下,发现也是放大1.5倍*/
}

播放tabBar


思路是参考酷狗音乐小程序。这个tabBar的js,wxml与播放功能界面的js,wxml相同。因为音乐播放是用wx.getBackgroundAudioManager()背景音频播放器管理的,所以才能同步。


我的tabBar

播放历史

思路:play.js中一旦播放成功就把歌名及歌曲ID传入全局变量->push到play.js里的数组中->wx.setStorageSync把数据存入缓存->在需要的页面wx.getStorageSync获取到缓存。

<!--play.js-->
const history_songId = this.data.history_songId
const historySong = {
      // id: res.id
      id: app.globalData.songId,
      songName:app.globalData.songName
    }
    history_songId.push(historySong)
    wx.setStorageSync('historyId', history_songId); //把historyId存入缓存
<!--me.js-->
 onShow:function(){
    var history = wx.getStorageSync('historyId');
    // console.log(history)
     this.setData({
      hidden:true,
      //  historyId: app.globalData.songName
       historyId: history
    })
    console.log(this.data.historyId)
  }

结语

做项目的过程总的来说痛并快乐,因为改不出BUG的样子真的很狼狈,但实现了某一个功能的那一刻真的很欣慰。再次感谢给予帮助的老师同学。如果你喜欢这篇文章或者可以帮到你,不妨点个赞吧!同时也非常希望看到这篇文章的你在下方给出建议!

源码

本项目源码

微信小程序-仿网易云音乐
02-23
微信小程序-仿网易云音乐
仿网易云音乐播放列表、皮肤样式、歌词滚动、轮播图等
WenRouDG的博客
07-26 4862
前言: 今天是我第二次写博客,打算把以前做过的一些小东西记录下来,今天介绍的是我的毕业设计《小罡音乐》是简仿网易云播放器的一些界面和播放音乐功能。 是基于ASP.NET的小罡音乐的设计与实现 ,也并不是抄袭网易云,我认为《网易云》等任何一个成功上线的平台,它们的交互设计,页面设计,等都是我们作为学生,新手拿来练手的好项目。同时是展现自身的设计能力,逻辑能力,和专业能力以及专业知识的时候,那么最后展现出来的《小罡音乐》整个作品集将是自身的综合体现。 效果截图 1:主页界面 2:皮肤切换样式这里举例
仿网易云音乐移动端html模板,使用jQuery仿网易云音乐移动端
weixin_42514627的博客
06-03 1781
2018年05月01日21:37:28完成了主页的推荐音乐、热歌榜、搜索和播放页面。2018年4月20日15:18:13这是一个项目笔记,用于记录制作这个项目的点点滴滴。明确需求左一开始:主页(推荐音乐)、热歌榜、搜索、歌单和播放界面。制作历程自定义一个工作流我选择了browser-sync,因为这个项目比较轻量(不需要引入很多的模块),所以就直接用一个browoser-sync配合开发实时刷新就...
Android 仿网易云音乐App
最新发布
2401_84149368的博客
04-14 732
效果展示注:因为视频太模糊,每日推荐、个人信息、歌单的AppBarLayout下的圆角没有好好的展现出来。App介绍Android仿网易云音乐App。是非常适合Android初级使用的App。包括 在线歌曲、FM电台、本地歌曲。通过输入关键词,搜索对应的 歌曲/专辑/歌手/用户/歌单/电台等。可以查看 歌曲信息/歌曲评论/用户动态/用户信息等等。使用的技术/框架功能非常全的Api,大赞本项目的整体架构(1)存储更新日推的时间(2)存储 搜索历史。
仿网易云音乐微信小程序
04-26
仿网易云音乐微信小程序,可看图片介绍:https://blog.csdn.net/jiang18238032891/article/details/89574844
微信小程序仿网易云音乐
07-11
微信小程序仿网易云音乐,与真正的网易云音乐还是有点差距,仅供参考
微信小程序仿网易云音乐(使用云开发,提供源码)
yingshuangtu的博客
07-20 6373
源码 文章目录前言一、实现页面(1)云村首页(2)云村中的云圈(3)歌曲搜索功能(4)达人页面(5)手机型号自适应性二、源码文件打开方式(1)修改_openid(2)打开云数据库并导入数据三、总结(1)通过数据库查询实现路由切换菜单栏(2)navigator跳转页面(3)点击实现页面跳转并传参(4)实现弹幕,视频的播放(5)对数据库的查询(6)swiper轮播图(7)简单的移动(8)css实现左侧边栏(9)瞄点实现分类菜单栏(10)点击隐藏出现 前言 一、实现页面 (1)云村首页 (2)云村中的云圈
微信小程序-仿网易云音乐APP的微信小程序
08-07
netmusic-app 仿网易云音乐APP的微信小程序 node后台接口代码已经发到github 有需求的可以自己部署,欢迎star,请勿使用我的服务器地址。 后台项目启动后 utils文件中新建bsurl.js文件 输入module.exports="启动地址" 动图演示地址: 目前实现功能 用户 歌单 FM 播放 评论 MV 专辑 歌手 登录 歌曲红心,FM trash,收藏单曲至歌单 收听记录 歌单歌曲推荐 迷你播放条 电台,节目 搜索 TODO 增加评论,评论点赞等 歌词翻译 收藏(歌单,歌手,专辑,电台 音质切换 用户动态,粉丝 新歌 新专 分类电台 目前代码结构混乱 是时候来一波大重构了。
微信小程序源码 仿网易云音乐(学习)
06-18
微信小程序源码 仿网易云音乐(学习)微信小程序源码 仿网易云音乐(学习)微信小程序源码 仿网易云音乐(学习)微信小程序源码 仿网易云音乐(学习)微信小程序源码 仿网易云音乐(学习)微信小程序源码 仿网易云...
微信小程序 仿网易云音乐 (源码)
05-30
微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云音乐 (源码)微信小程序 仿网易云...
微信小程序-毕设期末大作业】仿网易云音乐微信小程序源码.zip
05-05
微信小程序-毕设期末大作业】微信小程序源码 【微信小程序-毕设期末大作业】微信小程序源码 【微信小程序-毕设期末大作业】微信小程序源码 【微信小程序-毕设期末大作业】微信小程序源码 【微信小程序-毕设期末大...
微信小程序仿网易云音乐案例
11-28
微信小程序仿网易云音乐案例微信小程序仿网易云音乐案例 微信小程序仿网易云音乐案例
微信小程序-仿网易云音乐源代码
01-03
微信小程序-仿网易云音乐源代码 微信小程序(wei xin xiao cheng xu),简称小程序,缩写XCX,英文名Mini Program,是一种不需要下载安装即可使用的应用,它实现了应用“触手可及”的梦想,用户扫一扫或搜一下即可打开应用。
微信小程序demo:仿网易云音乐
06-09
微信小程序精品demo:仿网易云音乐:歌单,FM,播放,评论,主体登录功能没有实现,登录方法已经有。
微信小程序——仿网易云音乐(截图+源码).zip
04-17
微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip 微信小程序——仿网易云音乐(截图+源码).zip
仿网易云音乐应用的微信小程序
01-26
仿网易云音乐APP的微信小程序 需后端支持 下载启动即可 目前实现功能 用户 歌单 FM 播放 评论 MV 专辑 歌手 登录 歌曲红心,FM trash,收藏单曲至歌单 收听记录 歌单歌曲推荐 迷你播放条 电台,节目 搜索 TODO 增加...
仿仿网易云音乐 微信小程序源码
10-20
微信小程序源码,前端源码,简单展示源码,原生小程序源码 非uniapp,仅供参考,如有涉及权问题请联系作者。微信小程序源码,前端源码,简单展示源码,原生小程序源码 非uniapp,仅供参考,如有涉及权问题请联系...
微信小程序-(仿网易云音乐
qq_45938852的博客
06-06 980
前言 从微信小程序近期开发出来的功能可以明显看出,小程序开发的发展前景一片大好,并且随着时间的流逝,相信未来微信小程序开发公司也会开发出更多优质的小程序,开放更多功能,实现更多的需求。未来小程序制作和微信将实现更加的链接,用户搜索小程序也会越来越方便,这就是小程序开发、小程序制作的未来发展方向及优势。微信小程序开发的发展方向不仅仅是于微信更好地结合,更重要的是与各行各业的链接。小程序开发的发展是站在微信的用户基础上,与微信更好地结合就可以实现更多的功能,成功吸引到更多的用户和流量。通过和各个行业的链接,每个
仿网易云音乐微信小程序授予登录
05-10
首先,你需要创建一个微信小程序账号,并在开发者后台中设置好小程序的信息和权限。然后,你需要在小程序中添加登录授权功能,可以使用微信提供的 wx.login() 接口来获取用户的登录凭证(code),并将该凭证发送到你的服务器后台进行验证。在服务器后台中,你可以通过微信提供的接口获取用户的 openid 和 session_key。最后,你需要将用户的 openid 和 session_key 存储在你的数据库中,用于后续的业务逻辑处理。在用户下次访问小程序时,可以通过判断用户的登录状态来控制访问权限,并提供个性化的服务和推荐。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
写文章

热门文章

  • 仿网易云音乐(微信小程序版) 19379
  • 当get请求带参带的是对象怎么传 14632
  • 监听屏幕大小变化 3421
  • Echarts.js常见参数设置 3374
  • 监听滚动距离 2297

分类专栏

  • 笔记 / BUG 50篇
  • JavaScript 20篇
  • Vue.js 14篇
  • 数据可视化 5篇
  • 性能优化 1篇
  • Angular.js 11篇
  • HTML5 1篇
  • 浏览器 2篇
  • ES6
  • CSS 7篇
  • 计算机网络 8篇
  • 微信小程序 1篇
  • Webpack 3篇
  • Git 1篇

最新评论

  • 当get请求带参带的是对象怎么传

    qq_38947596: 后端接收对象queryVo呢?咋接收呀

  • 仿网易云音乐(微信小程序版)

    keji092511: 所以怎么搞api接口

  • 仿网易云音乐(微信小程序版)

    咩の烦恼: 接口不能怎么办哇

  • 仿网易云音乐(微信小程序版)

    青团社-郝帅: 接口无用,换了也无效

  • 仿网易云音乐(微信小程序版)

    Kilry: 做的确实好 表情包

您愿意向朋友推荐“博客详情页”吗?

  • 强烈不推荐
  • 不推荐
  • 一般般
  • 推荐
  • 强烈推荐
提交

最新文章

  • Vue中静态资源或css文件没有加载出来的说明
  • JSON.parse和JSON.stringify
  • echarch.js的基本使用
2020年74篇

目录

目录

评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

4617作文网批八字算命准吗水果商标起姓名算算人的命运起名五行属于金的字大全金钱姓宝宝起名马永什么起名字天使之梦解释算命微信公众平台祁姓男宝起名周公解梦梦到被追杀2006年属狗女孩起名周易卜易居免费测名字打分李起名字男孩耿如何起名字四柱八字算命排盘孙起名洋气男公司起名带水木的名字精灵宝可梦go破解爱周易八字排盘胶行起名大全肖姓 起名女孩的名字怎么起名字周易起名宋派人卖酱菜起个什么名字好周易六爻预测法姓汤 起名字太原周易预测天天周公解梦免费app做梦解码梦幻水族馆破解版淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男子给前妻转账 现任妻子起诉要回网友建议重庆地铁不准乘客携带菜筐月嫂回应掌掴婴儿是在赶虫子重庆警方辟谣“男子杀人焚尸”国产伟哥去年销售近13亿新的一天从800个哈欠开始男孩疑遭霸凌 家长讨说法被踢出群高中生被打伤下体休学 邯郸通报男子持台球杆殴打2名女店员被抓19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警两大学生合买彩票中奖一人不认账德国打算提及普京时仅用姓名山西省委原副书记商黎光被逮捕武汉大学樱花即将进入盛花期今日春分张家界的山上“长”满了韩国人?特朗普谈“凯特王妃P图照”王树国3次鞠躬告别西交大师生白宫:哈马斯三号人物被杀代拍被何赛飞拿着魔杖追着打315晚会后胖东来又人满为患了房客欠租失踪 房东直发愁倪萍分享减重40斤方法“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火手机成瘾是影响睡眠质量重要因素考生莫言也上北大硕士复试名单了妈妈回应孩子在校撞护栏坠楼网友洛杉矶偶遇贾玲呼北高速交通事故已致14人死亡西双版纳热带植物园回应蜉蝣大爆发男孩8年未见母亲被告知被遗忘张立群任西安交通大学校长恒大被罚41.75亿到底怎么缴沈阳一轿车冲入人行道致3死2伤奥运男篮美国塞尔维亚同组周杰伦一审败诉网易国标起草人:淀粉肠是低配版火腿肠外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万杨倩无缘巴黎奥运男子被猫抓伤后确诊“猫抓病”春分“立蛋”成功率更高?记者:伊万改变了国足氛围奥巴马现身唐宁街 黑色着装引猜测

4617作文网 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化