采用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>
登陆成功效果 可以显示所有功能