Vue实现web端仿网易云音乐 完成大部分功能

采用vue2+element,后台网易云接口为github某位大佬写的,上传到腾讯云服务器实现在线接口

1.代码结构

在这里插入图片描述

一.未登录页面

1.未登录效果预览

在这里插入图片描述

动图经过压缩可能渐变色不明显.....

由于图片资源过多,加载速度慢,可以采用element中图片自带的属性懒加载,再使用骨架屏后台无数据返回即显示骨架屏不至于白屏,看起来很奇怪,为啥不用数据懒加载?..好像vue2太麻烦了,没法像vue3使用现成的库,有知道的大佬望告知,但是应该可以通过页面滑动到一定高度再发请求;

2.播放预览

在这里插入图片描述

这部分是比较麻烦的点,由于插件功能太少只能自己封装,进度条采用elment的滑块组件,双向绑定当前歌词进度即可实现滚动,对返回的歌词进行格式化,以随歌曲进度进行滚动

在这里插入图片描述

音量调节也是采用element滑块,双向绑定实现调节音量功能,播放列表即为element的弹出层

通过audio的事件进行控制

<!--      audio标签-->
      <audio :src="song.length?song[index].url:''" autoplay ref="audio"
             @playing="isPlay=true"
             @ended="this.nextSong"
             @play="getDuration"
             @timeupdate="getCurrentTime" >
</audio>

歌词格式化代码

formatLyr (lyricStr) {
    // 可以看network观察歌词数据是一个大字符串, 进行拆分.
    let reg = /\[.+?\]/g //
    let timeArr = lyricStr.match(reg) // 匹配所有[]字符串以及里面的一切内容, 返回数组
    console.log(timeArr) // ["[00:00.000]", "[00:01.000]", ......]
    let contentArr = lyricStr.split(/\[.+?\]/).slice(1) // 按照[]拆分歌词字符串, 返回一个数组(下标为0位置元素不要,后面的留下所以截取)
    console.log(contentArr)
    let lyricObj = {} // 保存歌词的对象, key是秒, value是显示的歌词
    timeArr.forEach((item, index) => {
        // 拆分[00:00.000]这个格式字符串, 把分钟数字取出, 转换成秒
        let ms = item.split(':')[0].split('')[2] * 60
        // 拆分[00:00.000]这个格式字符串, 把十位的秒拿出来, 如果是0, 去拿下一位数字, 否则直接用2位的值
        let ss = item.split(':')[1].split('.')[0].split('')[0] === '0' ? item.split(':')[1].split('.')[0].split('')[1] : item.split(':')[1].split('.')[0]
        // 秒数作为key, 对应歌词作为value
        lyricObj[ms + Number(ss)] = contentArr[index]
    })
    return lyricObj
}

图片旋转以及订书针播放暂停切换不同效果

<div style="margin-top: 60px">
    <div class="needle" :style="`transform: rotate(${needleDeg});`"></div>
	<div ref="container" :style="`animation-play-state:${isPlay ? 'running' :'paused'}`">
    <el-image :src="currentSong.cover" fit="cover"></el-image>
</div>
</div>

可以通过vue的动态样式通过属性控制播放或者暂停动画

3.全部歌单

在这里插入图片描述

这个比较简单,发请求获取数据即可,点击不同页面发请求通过offset获取新的数据提交到vuex中,实现动态数据更新

示例代码

<template>
  <div class="back">


    <div class="box" style="position: relative">

      <HeadLine :title="title" style="margin: 0 auto" icon="iconfont icon-gedan2"></HeadLine>

      <el-popover
        placement="bottom"
        title="全部分类"
        width="600"
        :offset="100"
        trigger="click">
        <ul class="tags-container">
          <li v-for="(tag,index) in tags" :key="index" @click="getPlaylistsViaTag(tag.name)">
            {{ tag.name }}
          </li>
        </ul>
        <el-button type="danger" plain size="small" style="position: absolute;top: 20px;left: 220px" slot="reference">
          全部分类
        </el-button>
      </el-popover>


      <ul class="items" v-if="lists.playlists.length">

        <li v-for="item in lists.playlists" :key="item.id" @click="toListDetail(item)">
          <RecommendItem
            :listeners="item.playCount"
            :title="item.name"
            :image-url="item.coverImgUrl"
          ></RecommendItem>
        </li>
      </ul>
      <ul class="items" v-else>
        <li v-for="i in 30" :key="i">
          <Skeleton></Skeleton>
        </li>
      </ul>

      <Pagination :total="lists.total" :size="30" type="playlists" :tag="title"/>
    </div>
  </div>
</template>

<script>


import { getAllPlaylistTags, getHotPlaylist, getHotPlaylistByTags, getPlaylistViaTags } from '@/api/home'
import { mapState } from 'vuex'

export default {
  name: 'index',
  data () {
    return {
      title: '热门',
      tags: []
    }
  },
  computed: {
    ...mapState('playlists', ['lists'])
  },
  methods: {
    //跳转到歌单详情页
    toListDetail (item) {

      this.$store.commit('recommendList/setDetail', item)
      this.$router.push({
        name: 'listDetail',
        params: item
      })
    },
    //获取对应标签歌单
    async getPlaylistsViaTag (tag) {
      const {data} = await getPlaylistViaTags(tag, 30)
      this.title = tag
      this.$store.commit('playlists/setPlaylists',data)
    }
  },
  async created () {
    const {data} = await getPlaylistViaTags(this.title, 30)
    this.$store.commit('playlists/setPlaylists',data)
    const { data: { sub } } = await getAllPlaylistTags()
    this.tags = sub
  }
}
</script>

4.全部歌手

在这里插入图片描述

同样是发请求获取数据渲染页面即可,点击不同分类更新vuex中数据,实现视图更新

5.全部榜单

在这里插入图片描述

老套路,获取数据vuex更新歌单数据,视图随着更新

示例代码

<template>
  <div class="back">
    <div class="box">
      <el-col :span="6">
        <el-card class="classify-container">
          <p style="text-align: center">全部榜单</p>
          <ul>
            <li :class="{selected:detail.id===list.id}" v-for="list in topLists.list" @click="toSelectedList(list)">
              <el-image style="width: 50px;height: 50px;" :src="list.coverImgUrl"></el-image>
              <div>

              <p class="ellipsis" style="width: 140px">{{ list.name }}</p>
              <p>{{ list.updateFrequency }}</p>
              </div>

            </li>
          </ul>
        </el-card>
      </el-col>
      <el-card class="box-card">

        <div class="song-container">
          <el-image :src="detail.coverImgUrl"></el-image>
          <div class="desc">

            <div>
              <p style="font-size: 20px;margin-bottom: 10px">{{ detail.name }}</p>
              <p style="font-size: 16px;color:#888888"><i class="el-icon-time"></i>最近更新: {{ detail.updateTime |timeFormatter}}</p>
            </div>
            <div>
              <p style="width: 400px">{{detail.description}}</p>
            </div>

          </div>
        </div>
       <div style="display: flex;justify-content: space-between;border-bottom: 2px solid #f68f8f;padding: 5px">
        <div>

         <span>歌曲列表 </span>
         <span style="font-size: 12px;margin-left: 10px;color: #67676b">{{detail.trackCount}}首歌</span>
        </div>
         <span style="font-size: 14px">播放次数: <span style="color: #f68f8f">{{detail.playCount}}</span></span>
       </div>
        <SongList :songs="songs" :show-album="1>2"></SongList>
      </el-card>

    </div>
  </div>
</template>

<script>
import { getSingerList } from '@/api/singer'
import { mapState } from 'vuex'
import { getAllTopLists, getHotPlaylistDetail, getPlaylistComments, getPlaylistDetail } from '@/api/home'
import dayjs from 'dayjs'

export default {
  name: 'index',
  data () {
    return {
      title: '飙升榜'
    }
  },
  filters:{
    timeFormatter(time){
      return dayjs(time).format('MM月DD日')
    }
  },
  computed: {
    ...mapState('topLists', ['topLists']),
    ...mapState('recommendList', ['detail']),
    ...mapState('recommendList', ['songs']),
    comments:{
      get(){
        return this.$store.state.recommendList.comments
      },
      set(val){
      }
    }
  },
  methods: {
    //榜单跳转
    async toSelectedList (list) {
      const { data: { playlist } } = await getPlaylistDetail(list.id)

      this.$store.commit('recommendList/setDetail', playlist)

      const songs = await getHotPlaylistDetail(playlist.id)
      this.$store.commit('recommendList/setSongs', songs)


      const comments = await getPlaylistComments(playlist.id, 10)

      this.$store.commit('recommendList/setComments', comments.data)
    }
  },
  async created () {
    const { data } = await getAllTopLists()
    // console.log(data)
    this.$store.commit('topLists/setTopLists', data)

    const {data:{playlist}} = await getPlaylistDetail(this.topLists.list[0].id)
    console.log(playlist)
    this.$store.commit('recommendList/setDetail',playlist)

    const songs = await getHotPlaylistDetail(playlist.id)
    this.$store.commit('recommendList/setSongs', songs)
    console.log('@@@', this.songs)

    const comments = await getPlaylistComments(playlist.id, 10)
    console.log('!!', comments.data)
    this.$store.commit('recommendList/setComments', comments.data)
  }
}
</script>

6,搜索实现

在这里插入图片描述

依旧是element弹出层,输入框内容发生变化即发请求获取数据,记得节流!

示例代码

 <div class="search">
        <el-popover
          placement="bottom"
          width="200"
          trigger="manual"

          v-model="visible">
          <div class="result">
            <ul v-if="searchResult.songs">
              <li><i class="iconfont icon-yinle"></i>歌曲</li>
              <li class="ellipsis" @click="getSongUrl(song)" v-for="song in searchResult.songs" :key="song.id">
                {{ song.name }}-{{ song.artists[0].name }}
              </li>
            </ul>

            <ul v-if="searchResult.artists">
              <li><i class="iconfont icon-geshou1"></i>歌手</li>
              <li @click="toSingerDetail(singer)" v-for="singer in searchResult.artists" :key="singer.id">
                {{ singer.name }}
              </li>
            </ul>

            <ul v-if="searchResult.albums">
              <li><i class="iconfont icon-zhuanji2"></i>专辑</li>
              <li @click="toAlbumDetail(album)" class="ellipsis" v-for="album in searchResult.albums" :key="album.id">
                {{ album.name }}
              </li>
            </ul>

            <span v-if="!searchResult">暂无数据</span>
          </div>
          <el-input
            slot="reference"
            @input="visible = true"
            placeholder="请输入内容"
            v-model="keyword"
            prefix-icon="el-icon-search"
            @focus="visible = true&&keyword!==''"
            @blur="visible=false"
          >

          </el-input>
        </el-popover>
      </div>

<script>
async getResult (val) {
      if (this.timer) {
        clearTimeout(this.timer)
      }
      if (!this.keyword) return
      this.timer = setTimeout(async () => {
        const { data } = await getSearchResult(val)
        console.log(data)
        this.searchResult = data.result
      }, 500)

    },
</script>

7.登录

在这里插入图片描述

可以选择验证码或密码登录,通过element表单添加校验规则

示例代码

<template>
  <div>
    <el-dialog
      :visible="visible"
      ref="dialog"
      width="500px"
      @close="changeStatus"
      class="dialog-container"
    >

      <div class="left">
        <el-image :src="require('../../../assets/images/login.jpg')" style="width: 200px"></el-image>
      </div>
      <div class="right">
        <el-form :model="ruleForm" status-icon :rules="rules" ref="ruleForm" class="demo-dynamic">
          <el-form-item prop="mobile">
            <el-input maxlength="11" prefix-icon="el-icon-mobile-phone" placeholder="手机号"
                      v-model.number="ruleForm.mobile"></el-input>
          </el-form-item>
          <el-form-item v-if="isUsePass" prop="password" style="">
            <el-input type="password" maxlength="16" prefix-icon="el-icon-lock" placeholder="密码" v-model="ruleForm.password"
                      autocomplete="off"></el-input>
          </el-form-item>
          <el-form-item v-else prop="code" style="">
            <el-input type="text" maxlength="8" prefix-icon="el-icon-bell" placeholder="验证码" v-model="ruleForm.code"
                      autocomplete="off"></el-input>
            <el-button type="danger" plain size="small" :disabled="disabled" style="margin-left: 15px;width: 92px"
                       @click="getCode('ruleForm')"><p ref="getCode">获取验证码</p></el-button>


          </el-form-item>

          <el-form-item style="margin: 0;display: flex;align-items: center;vertical-align: middle">
            <el-button v-if="isUsePass" @click="isUsePass=false" style="font-size: 14px;color: #FAACA8" type="text">验证码登录</el-button>
            <el-button v-else @click="isUsePass=true"  style="font-size: 14px;color: #FAACA8" type="text">密码登录</el-button>

            <el-checkbox style="font-size: 14px;margin-left: 80px" label="自动登录"></el-checkbox>


          </el-form-item>

          <el-form-item>
            <el-button type="primary" style="width:50%;background-color: #f68f8f;" @click="submitForm('ruleForm')">登录
            </el-button>
            <el-button style="width:50%" type="warning" plain @click="">注册</el-button>
          </el-form-item>
        </el-form>
      </div>
      <div slot="title" class="login-title">
        <span>登录</span>
      </div>
    </el-dialog>
  </div>
</template>

<script>
import { getCode, userLogin } from '@/api/login'
import { setItem } from '@/utils/storage'
import { getUserInfo } from '@/api/user'

export default {
  name: 'Login',
  props: {

    visible: {
      type: Boolean,
      default: false
    }
  },
  data () {
    //注意validator位置需在data中 return外
    const checkMobile = (rule, value, callback) => {
      if (!value) {
        return callback(new Error('手机号不能为空'))
      }
      setTimeout(() => {
        if (!Number.isInteger(value)) {
          callback(new Error('请输入数字值'))
        } else {
          if (!/^1[3456789]\d{9}$/.test(value)) {
            callback(new Error('请输入正确手机号!'))
          } else {
            //执行成执行回调什么也不传
            callback()
          }
        }
      }, 1000)
    }
    const validateCode = (rule, value, callback) => {
      if (value === '') {
        callback(new Error('请输入验证码'))
      } else {
        if (this.ruleForm.checkPass !== '') {
          this.$refs.ruleForm.validateField('checkPass')
        }
        callback()
      }
    }
    return {
      disabled: false,
      isUsePass:false,
      ruleForm: {
        code: '',
        mobile: '',
        password:''
      },
      rules: {
        code: [
          {
            validator: validateCode,
            trigger: 'blur'
          }
        ],
        mobile: [
          {
            validator: checkMobile,
            trigger: 'blur'
          }
        ],
        password: [
          {
            required:true,
            trigger: 'blur',
            message:'密码不能为空'
          },{
          min:6,max:16,
            message:'密码长度为6-16位',
            trigger: 'blur',
          }


        ]
      }
    }
  },
  methods: {
    //发送验证码
    getCode (formName) {
      this.$refs[formName].validateField('mobile', (valid) => {
        if (!valid) {
          this.disabled = true
          //发送验证码
          try {
            getCode(this.ruleForm.mobile)
            this.$message({
              message: '发送验证码成功!',
              type: 'success'
            })
          } catch (err) {
            this.$message.error('发送失败,请重试!')
          }

          let second = 60
          // this.$refs.getCode.innerText=1
          const timer = setInterval(() => {
            if (!this.$refs.getCode) return clearTimeout(timer)
            this.$refs.getCode.innerText = second + 's'

            if (second === 0) {
              // return this.disabled=false
              clearTimeout(timer)
              this.disabled = false
              this.$refs.getCode.innerText = '获取验证码'
            }
            second--
          }, 1000)
          // clearTimeout(timer)

        } else {
          console.log('error submit!!')
          return false
        }
      })
    },
    //登录
    submitForm (formName) {
      this.$refs[formName].validate((valid) => {
        if (valid&&this.isUsePass===false) {
          userLogin(this.ruleForm.mobile,'', this.ruleForm.code).then(async res => {

              // console.log('@@@',res)
              this.$store.commit('user/setCookie', res.data.cookie)
              this.$store.commit('user/setToken', res.data.token)

              const usrInfo = await getUserInfo()


              const combinedInfo = { ...usrInfo.data, ...res.data.profile }
              console.log('@@',combinedInfo)
              this.$store.commit('user/setUserDetail', combinedInfo)
              setItem('Cookies', res.data.cookie)

              this.$message({
                message: '登录成功!',
                type: 'success'
              })
              this.$refs.dialog.close()
            },
            err => {
              this.$message({
                message: '登陆失败请重试!',
                type: 'warning'
              })
            })
        }else if(valid&&this.isUsePass){
          userLogin(this.ruleForm.mobile,this.ruleForm.password).then(async res => {

              // console.log('@@@',res)
              this.$store.commit('user/setCookie', res.data.cookie)
              this.$store.commit('user/setToken', res.data.token)

              const usrInfo = await getUserInfo()


              const combinedInfo = { ...usrInfo.data, ...res.data.profile }
              console.log('@@',combinedInfo)
              this.$store.commit('user/setUserDetail', combinedInfo)
              setItem('Cookies', res.data.cookie)

              this.$message({
                message: '登录成功!',
                type: 'success'
              })
              this.$refs.dialog.close()
            },
            err => {
              this.$message({
                message: '登陆失败请重试!',
                type: 'warning'
              })
            })
        }
        else {
          console.log('error submit!!')
          return false
        }
      })
    },

    handleClose (done) {
      this.$confirm('确认关闭?')
        .then(_ => {
          done()
        })
        .catch(_ => {
        })
    },
    changeStatus () {
      this.$emit('update:visible', false)
    }
  },

}
</script>

登陆成功效果 可以显示所有功能

在这里插入图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值