小妹随手有感而写,各位大佬嘴下留情!!!
下载网易云音乐node接口项目, 在本地启动, 为我们vue项目提供数据支持(需预先下载nodejs,这里就不详细说明了)
项目地址(https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%e5%ae%89%e8%a3%85)
下载并安装所有依赖后,打开项目终端,输入
node app.js
运行后,看到如下页面就成功了
![](https://i-blog.csdnimg.cn/blog_migrate/f492ef51cd14eb0a62903933a1e7fc50.png)
vant安装
右键项目名称,点击'使用命令行打开所在目录',通过npm安装
npm i @vant/weapp -S --production
详细教程参考(快速上手 - Vant Weapp (vant-ui.github.io))
router index.js路由配置
部分页面使用了路由懒加载功能,Login页面使用了路由拦截
import Vue from 'vue'
import VueRouter from 'vue-router'
import store from '@/store/index.js'
import Login from '@/views/Login.vue'
import Look from '@/views/Look.vue'
import Cloud from '@/views/Cloud.vue'
import ShiPing from '@/views/ShiPing.vue'
import FooterMusic from '@/views/FooterMusic.vue'
import Search from '@/views/Home/Search.vue'
import InforUser from '@/views/InforUser.vue'
import Commoent from '@/views/Home/Commoent.vue'
import Detailboder from '@/views/Home/DetailBoder.vue'
Vue.use(VueRouter)
const routes = [{
path: "/login",
component: Login,
meta: {
keepalive: false
}
},
{
path: "/look",
component: Look,
children: [
],
meta: {
keepAlive:false
},
},
{
path: "/cloud",
component: Cloud,
meta: {
keepalive: false
}
},
{
path: "/shiping",
component: ShiPing,
meta: {
keepalive: false
}
},
{
path: '/detail',
// 路由懒加载
component: () => import('@/views/Home/DetailMusic.vue'),
name: 'detail',
meta: {
keepAlive: true
},
},
{
path: '/lyrics',
component: () => import('@/views/LyRics.vue'),
name: 'lyrics',
},
{
path: '/footermusic',
component: FooterMusic,
meta: {
keepalive: false
}
},
{
path: '/search',
component: Search,
meta: {
keepalive: false
}
},
{
path: '/inforuser',
component:InforUser,
meta: {
keepalive: false
},
// 路由拦截
beforeEnter:(to,from,next)=>{
if(store.state.islogin){
next()
}else{
next('/login')
store.state.isfootermusic=false
}
}
},
{
path:'/commoent',
component:Commoent,
name:'commoent',
meta: {
keepalive: true
},
},
{
path:'/leaderboard',
component: () => import('@/views/Leaderboard.vue'),
meta: {
keepalive: false
},
},
{
path:'/detailboder',
component:Detailboder,
name:'detailboder'
},
{
path: '*',
redirect: '/look'
}
]
const router = new VueRouter({
mode: 'hash',
routes
})
export default router
store index.js配置
state为公共状态
mutations存储方法
getters,actions, modules暂时没有用到
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// state公共状态
state: {
list: [{
al: {
id: 145750496,
name: "镜象马戏团",
pic: 109951167493773280,
picUrl: "https://p1.music.126.net/jFkiKK1EtAmkOPeCjBK1Wg==/109951167493773275.jpg",
pic_str: "109951167493773275",
},
id: 1951980693,
name:"ONE"
}],
// 默认下标为0
playindex: 0,
// 歌曲详情页
detailshow:false,
//歌词
lyriclist:{},
//播放的当前时间
currentime:0,
//歌曲总时长
duration:0,
// 是否登录
islogin:false,
//判断底部组件是否显示
isfootermusic:true,
},
getters: {
},
mutations: {
upchangelogin:function(state){
state.islogin=true
},
upchangefooter:function(state){
state.isfootermusic=true
},
updatePlayList:function(state,value){
state.list=value
// console.log(state.list)
},
updateIndex:function(state,value){
state.playindex=value
},
UpdetailShow:function(state){
state.detailshow=!state.detailshow
},
uplyricslist:function(state,value){
state.lyriclist=value
// console.log(state.lyriclist)
},
upcurrentime:function(state,value){
state.currentime=value
},
upduration:function(state,value){
state.duration=value
// console.log(state.duration)
},
pushlist:function(state,value){
state.list.push(value)
// console.log(value)
},
},
actions: {
},
modules: {
}
})
各个组件详细代码
Look.vue 发现页面,也是首页面,如图
![](https://i-blog.csdnimg.cn/blog_migrate/d9a3355adc04f619e0055ac9b5aa0f56.png)
<template>
<div>
<tabar></tabar>
<swiper></swiper>
<icon></icon>
<music></music>
<router-view></router-view>
</div>
</template>
<script>
import tabar from '@/views/TaBar'
import swiper from '@/views/Home/SwiperTop.vue'
import icon from '@/views/Home/IconList'
import music from '@/views/Home/MusicList'
import Vue from 'vue'
import Vant from 'vant'
import 'vant/lib/index.css'
Vue.use(Vant)
export default{
components:{
tabar,
swiper,
icon,
music,
},
// activated是页面激活后的钩子函数,一进页面就会触发
activated () {
// 显示时
console.log(1)
},
// deactivated 离开页面的钩子函数,一离开页面就会触发
deactivated () {
// 不显示时
console.log(2)
},
}
</script>
<style>
</style>
Look页面包含的子组件
TaBar.vue
<template>
<div class="topnav">
<div class="topleft">
<van-icon name="apps-o" size="1.5rem" />
</div>
<ul class="top-content">
<!-- 声明式导航 -->
<li><router-link to="/inforuser" active-class="hhh">我的</router-link></li>
<li><router-link to="/look" active-class="hhh" >发现</router-link></li>
<li><router-link to="/cloud" active-class="hhh">云村</router-link></li>
<li><router-link to="/shiping" active-class="hhh">视频</router-link></li>
</ul>
<div class="topright">
<van-icon name="search" size="1.5rem" @click="$router.push('/search')"/>
</div>
</div>
</template>
<script>
</script>
<style>
.hhh{
color:red;
font-weight: 1000;
font-size: 1.25rem;
}
.topnav{
width:100%;
height:2.5rem;
display:flex;
justify-content: space-between;
line-height: 2.5rem;
}
.top-content{
width:65%;
display:flex;
height: 100%;
justify-content: space-around;
cursor: pointer;
}
.topleft{
padding-left: 0.625rem;
}
.topright{
padding-right: 0.625rem;
}
</style>
SwiperTop.vue 放在views下面新建一个Home文件夹里
<template>
<div id="swiperTop">
<van-swipe :autoplay="3000" indicator-color="red">
<van-swipe-item v-for="(item, index) in images" :key="index" >
<span class="left"><van-icon name="arrow-left" size="20px"/></span>
<img :src="item.pic"/>
<span class="right"><van-icon name="arrow" size="20px"/></span>
</van-swipe-item>
</van-swipe>
</div>
</template>
<script>
import Vue from 'vue'
import { Lazyload } from 'vant';
import axios from 'axios'
Vue.use(Lazyload);
export default {
data() {
return {
images: [],
};
},
created(){
axios.get(`http://localhost:3000/dj/banner`).then((item)=>{
// console.log(item.data.data)
this.images=item.data.data
})
}
};
</script>
<style scoped="">
#swiperTop .van-swipe{
width:100%;
height:18.75rem;
}
.van-swipe-item img{
width:100%;
height:18.75rem;
border-radius: 2rem;
}
.left{
float:left;
position: absolute;
left:0px;
top:9.375rem;
}
.right{
float:right;
position: absolute;
right:0px;
top:9.375rem
}
</style>
IconList.vue 放在Home文件夹下
<template>
<div class="iconlist">
<div class="iconitem">
<van-icon name="service" size="30px" color="red"/>
<span>每日推荐</span>
</div>
<div class="iconitem">
<van-icon name="like" size="30px"/>
<span>私人FM</span>
</div>
<div class="iconitem" @click="$router.push('/leaderboard')">
<van-icon name="fire" size="30px" color="red"/>
<span>排行榜</span>
</div>
<div class="iconitem">
<van-icon name="music" size="30px"/>
<span>歌单</span>
</div>
</div>
</template>
<script>
</script>
<style>
.iconlist{
width:100%;
height:2.5rem;
line-height: 2.5rem;
display:flex;
justify-content: space-around;
align-items: center;
}
.iconitem{
width:25%;
height:100%;
display:flex;
flex-direction: column;
align-items: center;
padding-top: 0.5rem;
}
</style>
MusicList.vue 放在Home文件夹下
<template>
<div class="music">
<div class="musictop">
<div class="title">发现好歌单</div>
<div class="more">查看更多</div>
</div>
<div class="music-content">
<van-swipe :width="150" class="my-swiper"
:show-indicators="false">
<van-swipe-item v-for="(item,index) in list" :key="index" @click="detail(item)">
<img :src="item.coverImgUrl" alt="" class="img">
<span class="playcount">
<van-icon name="play-circle-o" size="20px"/>
{{item.playCount|playcount-filter}}
</span>
<span class="name">
{{item.name}}
</span>
</van-swipe-item>
</van-swipe>
</div>
</div>
</template>
<script>
import axios from 'axios'
import Vue from 'vue'
Vue.filter('playcount-filter',function(data){
if(data>=100000000){
return (data/100000000).toFixed(1)+"亿"
}else if(data>=10000){
return (data/10000).toFixed(2)+"万"
}
})
export default{
data(){
return {
list:[]
}
},
created(){
//歌单网友精选碟
axios.get('http://localhost:3000/top/playlist/highquality').then((item)=>{
// console.log(item.data.playlists)
this.list=item.data.playlists
})
},
methods:{
detail (item) {
// console.log(item)
this.$router.push({ name: 'detail', params: { id: item.id } })
// this.$router.push({ name: 'commoent', params: { id: item.id } })
}
}
}
</script>
<style>
.music{
width:100%;
height:2.5rem;
padding:1.5rem;
margin-top: 1.25rem;
}
.musictop{
width:100%;
height:1rem;
display:flex;
justify-content: space-between;
margin-bottom:0.3125rem;
}
.title{
font-size: 1.0625rem;
font-weight: 800;
}
.more{
border:1px solid #000000;
text-align: center;
line-height: 1.5rem;
padding: 0 0.5rem;
border-radius: 0.9375rem;
height:1.5rem
}
.music-content{
width:100%;
height: 12.5rem;
}
.my-swiper{
height:100%;
}
.img{
width:100%;
height:8.5rem;
padding-right: 0.5rem;
padding-top: 8px;
border-radius: 0.8rem;
}
.name{
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
</style>
Search.vue 搜索功能,放在Home文件夹下,效果如图
注(返回图标我使用的是阿里SVG代码,比较长,可以修改为自己喜欢的图标)
![](https://i-blog.csdnimg.cn/blog_migrate/dbb8939e822b204f69bb088d4b8fff57.png)
<template>
<div>
<div class="searchTop">
<svg t="1675048038409" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2501"
width="30" height="30" @click="$router.push('/look')">
<path d="M929.70745 487.72513 167.942967 487.72513l358.793666-318.918493c12.390191-11.012821 13.505595-29.982872 2.493797-42.37204-11.010775-12.388145-29.979802-13.506619-42.369993-2.492774L74.839499 490.168786c-6.407943 5.695722-10.073426 13.859659-10.073426 22.432918 0 8.573259 3.665483 16.737196 10.073426 22.432918l412.019914 366.227985c5.717212 5.082762 12.83533 7.581676 19.926842 7.581676 8.275477-0.002047 16.515139-3.403516 22.443152-10.07445 11.012821-12.389168 9.897418-31.359218-2.493797-42.37204L179.893136 548.100196l749.814314 0c16.575514 0 30.013571-13.612019 30.013571-30.187533S946.283987 487.72513 929.70745 487.72513z"
fill="#272636" p-id="2502"></path>
</svg>
<input type="text" placeholder="王靖雯不胖" v-model="searchkey" @keydown.enter="enterkey">
</div>
<div class="search-history">
<span class="search-span">历史</span>
<span v-for="(item,index) in keyword" :key="index" class="history" @click="searchhistory(item)">
{{item}}
</span>
<van-icon name="delete-o" size="30px" style="position: absolute;right:0.625rem" @click="delhistory"/>
</div>
<div class="musiclist">
<div v-for="(item,index) in searchresult" :key="item.id">
<div class="music-detail" @click="dateIndex(item)">
<div class="detail-left">
<span style="font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
<span class="singer">
<span class="tomusic" v-if="item.al.name?item.al.name:item.al.name='佚歌'">{{item.al.name}}</span>
<div class="tomusic">
<span class="writer-music" style="" v-for="res in item.ar">{{res.name}} </span>
</div>
</span>
</div>
<van-icon name="play-circle-o" size="30px" v-if="item.mv!=0"/>
<van-icon name="bars" size="30px"/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import {mapState,mapMutations,mapAction } from 'vuex'
export default{
data(){
return{
keyword:[],
searchkey:"",
// 搜索结果
searchresult:[]
}
},
computed:{
...mapState(["list"])
},
created(){
},
mounted(){
// 判读本地数据是否存在
if(JSON.parse(localStorage.getItem('keyword')))
{
this.keyword=JSON.parse(localStorage.getItem('keyword'))
}
else{
this.keyword=[]
}
},
methods:{
// 回车添加
enterkey(){
if(this.searchkey!="")
{
// unshift添加到数组首位
this.keyword.unshift(this.searchkey)
//去重
this.keyword=[...new Set(this.keyword)]
// 固定长度
if(this.keyword.length>8)
{
this.keyword.splice(this.keyword.length-1,1)
}
localStorage.setItem("keyword",JSON.stringify(this.keyword))
// 搜索结果
axios.get(`http://localhost:3000/cloudsearch?keywords=${this.searchkey}`)
.then(item=>{
this.searchresult=item.data.result.songs
})
this.searchkey=""
}
},
// 删除按钮
delhistory(){
localStorage.removeItem("keyword")
this.keyword=[]
},
searchhistory(item){
axios.get(`http://localhost:3000/cloudsearch?keywords=${item}`)
.then(res=>{
console.log(res.data.result.songs)
this.searchresult=res.data.result.songs
})
},
dateIndex(item){
this.pushlist(item)
this.updateIndex(this.list.length-1)
},
...mapMutations(["pushlist","updateIndex"])
},
}
</script>
<style>
.writer-music {
font-size: 10px;
color: #65737E;
}
.tomusic {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
font-size: 1.3rem;
width: 10rem;
}
.detail-left {
width: 60%;
display: flex;
justify-content: space-around;
}
.singer {
display: flex;
flex-direction: column;
padding-top: 0.625rem;
padding-left: 0.625rem;
}
.music-detail {
display: flex;
justify-content: space-between;
align-items: center;
}
.musiclist {
width: 100%;
background-color: #fff;
padding-bottom: 4.25rem;
}
.aaa{
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
border:1px solid #000000;
}
.search-history{
width:100%;
padding: 0.625rem;
position: relative;
}
.search-span{
font-size: 1.25rem;
font-weight: 800;
}
.history{
background-color: #a5a5a54d;
padding: 0.5rem;
border-radius: 0.6rem;
margin:0.3125rem 0.625rem;
display: inline-block;
}
.searchTop{
width:100%;
height:3rem;
padding: 0.625rem;
display:flex;
align-items: center;
}
.searchTop input{
margin-left: 1rem;
border:none;
border-bottom: 1px solid #000000;
width:90%;
padding:0.4rem;
}
</style>
Leaderboard.vue 排行榜,放在view 文件夹下,效果如图
![](https://i-blog.csdnimg.cn/blog_migrate/8455b8cb0ecd5c21be5991458474b06b.png)
<template>
<div>
<!-- 头部 -->
<van-sticky>
<div class="header flex">
<div class="header-left flex">
<van-icon name="arrow-left" size="30px" color="black" @click="$router.go(-1)" />
<span style="letter-spacing: 0.2rem;">网易排行榜</span>
</div>
<div class="header-right">
<van-icon name="chat-o" size="40" />
</div>
</div>
</van-sticky>
<!-- 各个榜单 -->
<van-tabs swipeable animated>
<van-tab title="官方" title-style="font-size:1.3rem">
<div class="title">
榜单推荐
</div>
<div class="classify flex">
<span v-for="(item,index) in list.slice(0,3)" :key="index" class="classify-span" @click="Detailboder(item)">
<img :src="item.coverImgUrl" alt="" class="classify-img">
<span class="playcount">
<van-icon name="play-circle-o" size="30px" color="white" />
{{item.playCount|playcount-filter}}
</span>
</span>
</div>
<div class="title">
官方榜
</div>
<div style="padding-bottom: 5rem;">
<div v-for="(item,i) in list.slice(3,7)" :key="i" class="title-span" @click="Detailboder(item)">
<div class="title-left">
<span style="font-size: 25px;padding-bottom: 0.5rem;padding-left: 0.625rem;">{{item.name}}</span>
<div style="padding-top: 0.9375rem;">
<img :src="img[i]" alt="" class="title-img">
</div>
</div>
<div class="title-right">
<span class="state">{{item.updateFrequency}}</span>
<div class="title_cloum" v-for="(item,index) in music.slice(0,3)" :key="index">
<span style="padding-right: 0.625rem;">{{index+1}}</span>
<span>{{item.name}}</span>
</div>
</div>
</div>
</div>
</van-tab>
<van-tab title="精选" title-style="font-size:1.3rem">
<div v-for="(item,index) in list.slice(7,16)" :key="index" class="classify-span" @click="Detailboder(item)">
<img :src="item.coverImgUrl" alt="" class="classify_img">
<span class="play-count">
<van-icon name="play-circle-o" size="30px" color="white" />
{{item.playCount|playcount-filter}}
</span>
</div>
</van-tab>
<van-tab title="曲风" title-style="font-size:1.3rem">
<div v-for="(item,index) in list.slice(16,25)" :key="index" class="classify-span" @click="Detailboder(item)">
<img :src="item.coverImgUrl" alt="" class="classify_img">
<span class="play-count">
<van-icon name="play-circle-o" size="30px" color="white" />
{{item.playCount|playcount-filter}}
</span>
</div>
</van-tab>
<van-tab title="全球" title-style="font-size:1.3rem">
<div v-for="(item,index) in list.slice(25,32)" :key="index" class="classify-span" @click="Detailboder(item)">
<img :src="item.coverImgUrl" alt="" class="classify_img">
<span class="play-count">
<van-icon name="play-circle-o" size="30px" color="white" />
{{item.playCount|playcount-filter}}
</span>
</div>
</van-tab>
</van-tabs>
</div>
</template>
<script>
import axios from 'axios'
import Vue from 'vue'
Vue.filter('playcount-filter', function(data) {
if (data >= 100000000) {
return (data / 100000000).toFixed(1) + "亿"
} else if (data >= 10000) {
return (data / 10000).toFixed(2) + "万"
}
})
export default {
data() {
return {
list: [],
img: ["http://p4.music.126.net/IuyhHbgZVnFHLjknh3JriQ==/109951168223326749.jpg",
"http://p3.music.126.net/o_0_oXHXHUtkP-Y7x1JNHw==/109951168182087340.jpg",
"http://p4.music.126.net/74vJWfltitQodfT1VHImAQ==/109951168274510240.jpg",
"http://p4.music.126.net/as0q_BRqOMImCd0OsyU05Q==/109951168145373390.jpg"
],
music: []
}
},
created() {
axios.get(`http://localhost:3000/toplist`).then(res => {
console.log(res)
this.list = res.data.list
for (var i = 0; i < 4; i++) {
axios.get(`http://localhost:3000/playlist/detail?id=${this.list[i].id}`).then(item => {
// console.log(item)
for (var j = 0; j < 3; j++) {
this.music.push(item.data.playlist.tracks[j])
}
})
}
// console.log(this.list)
console.log(this.music)
})
},
methods: {
Detailboder(item) {
this.$router.push({
name: 'detailboder',
params: {
id: item.id,
img: item.coverImgUrl,
playcount: item.playCount,
subscribedCount: item.subscribedCount,
trackCount: item.trackCount
}
})
// this.$route.push('/detailboder')
}
}
}
</script>
<style scoped>
.play-count{
position: absolute;
top: 2rem;
left: 2rem;
color: white
}
.classify_img{
width: 100%;
height: 15rem;
border-radius: 0.9375rem;
padding-top: 10px;
}
.state {
position: absolute;
right: -0.5rem;
top: -2.5rem;
color: #637777;
}
.title_cloum {
display: flex;
align-items: center;
padding: 0.5rem 0.5rem;
font-size: 1.25rem;
position: relative;
}
.title-right {
position: absolute;
/* top:4rem;
right:3rem; */
top: 50%;
left: 60%;
transform: translate(-50%, -40%);
}
.title-left {
width: 48%;
height: 10rem;
display: flex;
flex-direction: column;
display: inline-block;
}
.title-span {
width: 100%;
height: 14rem;
/* line-height: 15.625rem; */
padding: 1rem 1.25rem;
position: relative;
margin-bottom: 2rem;
border-top: 1px solid #000000;
border-bottom: 1px solid #000000;
border-radius: 1.5rem;
box-shadow: 1px 5px 5px #7cb0b1;
}
.title-img {
width: 8rem;
height: 8rem;
border-radius: 1rem;
}
.playcount {
position: absolute;
top: 4.625rem;
left: 2rem;
color: white
}
.classify-span {
padding: 0px 1rem;
position: relative;
}
.classify-img {
width: 100%;
height: 7rem;
border-radius: 0.9375rem;
}
.classify {
width: 100%;
height: 8rem;
}
.title {
width: 100%;
height: 3rem;
line-height: 3rem;
font-size: 1.6rem;
font-weight: 600;
letter-spacing: 0.5rem;
}
.flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.header {
width: 100%;
height: 3.125rem;
background-color: white
}
.header-left {
width: 40%;
font-size: 1.3rem;
}
.header-right {
padding-right: 0.625rem;
}
</style>
DetailBoder.vue 排行榜详情,放在Home文件夹下,效果如图
![](https://i-blog.csdnimg.cn/blog_migrate/28480696e6538019e7e3195fc2e1013d.png)
<template>
<div>
<div class="itemMusictop">
<div class="itemleft">
<van-icon name="arrow-left" size="25px" color="black" @click="$router.go(-1)" />
</div>
<div class="itemright">
<van-icon name="search" color="black" size="30px" @click="$router.push('/search')"/>
<van-icon name="more-o" color="black" size="30px" />
</div>
</div>
<img :src="this.$route.params.img" alt="" class="bgimg">
<div class="middle flex">
<span>
<van-icon name="add-o" size="30px" />{{this.$route.params.playcount|playcount-filter}}<span class="a"></span></span>
<span>
<van-icon name="chat-o" size="30px" />{{this.$route.params.subscribedCount|playcount-filter}}<span class="a"></span></span>
<span>
<van-icon name="exchange" size="30px" />{{this.$route.params.trackCount|playcount-filter}}</span>
</div>
<div class="listtop">
<div class="listtop-left">
<van-icon name="play-circle" size="35px" color="red" />
<span>
<span class="listen-all">播放全部</span>
<span>(共{{list.length}}首)</span>
</span>
</div>
<div class="listtop-right flex">
<van-icon name="down" size="30px" />
<van-icon name="coupon-o" size="30px" />
</div>
</div>
<div class="itembottom">
<div v-for="(item,index) in list.slice(0,40)" :key="item.id" @click="playmusic(index)">
<div class="music-detail">
<div class="detail-left">
<span style=" font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
<span class="singer">
<span class="tomusic">{{item.al.name}}</span>
<span class="writer-music" style="">{{item.ar[0].name}}</span>
</span>
</div>
<van-icon name="bars" size="30px" />
</div>
</div>
</div>
</div>
</template>
<script>
import Vue from 'vue'
import axios from 'axios'
import {mapState,mapMutations,mapAction } from 'vuex'
Vue.filter('playcount-filter', function(data) {
if (data >= 100000000) {
return (data / 100000000).toFixed(1) + "亿"
} else if (data >= 10000) {
return (data / 10000).toFixed(2) + "万"
} else if (data < 1000) {
return data
}
})
export default {
data() {
return {
list: []
}
},
created() {
console.log(this.$route.params.id)
axios.get(`http://localhost:3000/playlist/detail?id=${this.$route.params.id}`).then(res => {
console.log(res)
this.list = res.data.playlist.tracks
console.log(this.list)
})
},
methods:{
playmusic:function(i){
this.updatePlayList(this.list)
this.updateIndex(i)
},
...mapMutations(["updatePlayList","updateIndex"])
}
}
</script>
<style scoped>
.itembottom {
position: absolute;
top:22rem;
display:flex;
flex-direction: column;
width:100%;
}
.writer-music {
font-size: 10px;
color: #65737E;
}
.tomusic {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
font-size: 1.3rem;
width: 10rem;
}
.detail-left {
width: 60%;
display: flex;
justify-content: space-around;
}
.singer {
display: flex;
flex-direction: column;
}
.music-detail {
display: flex;
justify-content: space-around;
align-items: center;
height: 3.75rem;
}
.listtop-right {
width: 30%;
}
.listen-all {
font-size: 19px;
font-weight: 800;
}
.listtop-left {
display: flex;
width: 45%;
justify-content: space-between;
}
.listtop {
display: flex;
justify-content: space-around;
height: 3rem;
width: 100%;
align-items: center;
font-size: 1.0625rem;
line-height: 3rem;
position: absolute;
top: 18rem;
border-bottom: 1px solid #000000;
}
.a {
padding-left: 0.9rem;
border-right: 1px solid #000000;
}
.middle {
width: 20rem;
height: 4rem;
border-top: 1px solid #000000;
border-bottom: 1px solid #000000;
border-radius: 1.5rem;
box-shadow: 10px 5px 5px #b4ae8b;
position: absolute;
top: 26%;
left: 50%;
transform: translate(-50%, -50%);
background-color: #FFFFFF;
}
.flex {
display: flex;
justify-content: space-around;
align-items: center;
}
.itemMusictop {
width: 100%;
height: 2.5rem;
display: flex;
justify-content: space-between;
align-items: center;
line-height: 2.5rem;
font-size: 18px;
position: relative;
}
.itemright {
width: 25%;
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.itemleft {
width: 10%;
padding-left: 0.625rem;
}
.bgimg {
width: 100%;
height: 15rem;
z-index: -1;
position: absolute;
top: 0px;
}
</style>
DetailMusic.vue 歌单详情,放在Home文件夹下,效果如图
![](https://i-blog.csdnimg.cn/blog_migrate/bd75b3a0a99fa1bcbbd11fda071088d9.png)
<template>
<div>
<!-- 顶部 -->
<van-sticky>
<div class="itemMusictop">
<!-- 虚化部分 -->
<img :src="list.coverImgUrl" alt="" class="bgimg">
<div class="itemleft">
<van-icon name="arrow-left" size="25px" color="white" @click="$router.back()" />
<span class="gedan" style="color:white">歌单</span>
</div>
<div class="itemright">
<van-icon name="search" color="white" size="30px" @click="$router.push('/search')"/>
<van-icon name="more-o" color="white" size="30px" />
</div>
</div>
</van-sticky>
<div class="itemmidle">
<!-- 中间图片 -->
<img :src="list.coverImgUrl" alt="" class="midleimg">
<!-- 中间右半部分 -->
<div class="midle-right">
<span class="content"> {{this.list.description}}</span>
<span class="writer">
<img :src="list.coverImgUrl" alt="" class="left-img">
<span class="count">
<van-icon name="play-circle-o" size="30px" />
{{this.list.playCount|playcount-filter}}
</span>
</span>
<span class="item-type" style="color:black">{{this.list.name}}</span>
</div>
</div>
<!-- tabar -->
<div class="itemtabar">
<div class="iconlist">
<div class="iconitem" @click="comment()">
<van-icon name="comment-o" :badge="list.commentCount" size="30px" color="black" />
<span>评论</span>
</div>
<div class="iconitem">
<van-icon name="share-o" :badge="list.shareCount" size="30px" color="black" />
<span>分享</span>
</div>
<div class="iconitem">
<van-icon name="down" size="30px" color="black" />
<span>下载</span>
</div>
<div class="iconitem">
<van-icon name="diamond-o" size="30px" color="black" />
<span>多选</span>
</div>
</div>
</div>
<!-- 歌曲列表 -->
<div class="musiclist">
<div class="listtop">
<div class="listtop-left">
<van-icon name="music-o" size="32px" />
<span>
<span class="listen-all">播放全部</span>
<span>(共{{music.length}}首)</span>
</span>
</div>
<van-button type="warning" size="normal" :round="true" class="collection">
<van-icon name="plus" size="20px" />
收藏
</van-button>
</div>
<div style="padding-bottom:80rem;height:76.25rem">
<div v-for="(item,index) in music" :key="item.id">
<div class="music-detail">
<div class="detail-left" @click="playmusic(index)">
<span style="font-size: 1.4rem;line-height: 3.75rem;">{{index+1}}</span>
<span class="singer">
<span class="tomusic">{{item.al.name}}</span>
<span class="writer-music" style="">{{item.ar[0].name}}</span>
</span>
</div>
<img src="../../assets/play.png" alt="" class="play">
<van-icon name="bars" size="30px"/>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
import Vue from 'vue'
import {mapState,mapMutations,mapAction } from 'vuex'
Vue.filter('playcount-filter', function(data) {
if (data >= 100000000) {
return (data / 100000000).toFixed(1) + "亿"
} else if (data >= 10000) {
return (data / 10000).toFixed(2) + "万"
}
})
export default {
data() {
return {
list: {},
music: [],
nickname:""
}
},
created(){
// console.log('created', this.$route.params.id)
axios.get(`http://localhost:3000/playlist/detail?id=${this.$route.params.id}`)
.then((item) => {
// console.log(item)
this.list = item.data.playlist
console.log(this.list)
this.nickname=this.list.creator.nickname
})
axios.get(`http://localhost:3000/playlist/track/all?id=${this.$route.params.id}&limit=20&offset=1`)
.then((item) => {
this.music = item.data.songs
// console.log(this.music)
// console.log(this.list.commentCount)
})
},
components: {},
methods:{
playmusic:function(i){
this.updatePlayList(this.music)
this.updateIndex(i)
},
// 传歌单id给commoent组件
comment(){
this.$router.push({name:'commoent',params:{id:this.$route.params.id,count:this.list.commentCount,coverImgUrl:this.list.coverImgUrl,
name:this.list.name,nickname:this.nickname}})
},
...mapMutations(["updatePlayList","updateIndex"])
}
}
</script>
<style scoped="">
.play{
width:1.8rem;
height:1.8rem;
display:none;
}
.music-detail:hover .play{
display:inline-block;
}
.writer-music {
font-size: 10px;
color: #65737E;
}
.tomusic {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
font-size: 1.3rem;
width: 10rem;
}
.detail-left {
width: 60%;
display: flex;
justify-content: space-around;
}
.singer {
display: flex;
flex-direction: column;
padding-top: 0.625rem;
}
.music-detail {
display: flex;
justify-content: space-between;
align-items: center;
height:3.75rem;
}
.listen-all {
font-size: 19px;
font-weight: 800;
}
.listtop-left {
display: flex;
width: 45%;
justify-content: space-between;
}
.listtop {
display: flex;
justify-content: space-around;
height: 2.5rem;
width: 100%;
align-items: center;
font-size: 1.0625rem;
line-height: 2.5rem;
}
.musiclist {
width: 100%;
height: 35rem;
background-color: #fff;
border-top-left-radius: 1rem;
border-top-right-radius: 1rem;
position: absolute;
top: 21rem;
}
.iconlist {
width: 100%;
height: 2.5rem;
line-height: 2.5rem;
display: flex;
justify-content: space-around;
align-items: center;
}
.iconitem {
width: 25%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 0.5rem;
}
.count {
height: 1.875rem;
line-height: 1.875rem;
margin-left: 0.625rem;
color: black;
}
.item-type {
position: absolute;
top: 6.5rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.itemMusictop {
width: 100%;
height: 2.5rem;
display: flex;
justify-content: space-between;
align-items: center;
line-height: 2.5rem;
font-size: 18px;
position: relative;
}
.itemleft,
.itemright {
width: 25%;
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.bgimg {
width: 100%;
height: 9rem;
position: fixed;
z-index: -1;
filter: blur(1.5rem);
}
.itemmidle {
width: 100%;
height: 13rem;
}
.midleimg {
width: 9rem;
height: 9rem;
position: absolute;
margin-top: 2.5rem;
margin-left: 1.25rem;
border-radius: 0.9375rem;
}
.content {
width: 14rem;
height: 2.5rem;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
color:black
}
.midle-right {
position: absolute;
margin: 3rem 0;
right: 0.625rem;
}
.left-img {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
}
.writer {
height: 3rem;
display: flex;
align-items: center;
position: absolute;
top: 3rem;
line-height: 3rem;
}
</style>
DetailMusic.vue所包含的子组件
Commoent.vue放在Home文件夹下,展示歌单评论,效果如图
![](https://i-blog.csdnimg.cn/blog_migrate/e096820c74428b8041fc90c54652ed74.png)
<template>
<div>
<!-- 头部 -->
<van-sticky>
<div class="header flex">
<div class="header-left flex">
<van-icon name="arrow-left" size="30px" color="black" @click="$router.push('/look')" />
<span style="letter-spacing: 0.2rem;">评论({{this.$route.params.count}})</span>
</div>
<div class="header-right">
<van-icon name="guide-o" size="40px" color="black" />
</div>
</div>
</van-sticky>
<!-- 中间 -->
<div class="middle flex">
<div class="middle-left">
<img :src="this.$route.params.coverImgUrl" alt="">
</div>
<div class="middle-right">
<span>{{this.$route.params.name}}</span>
<span style="padding-top: 0.5rem;">by——{{this.$route.params.nickname}}</span>
</div>
<div class="go">
<van-icon name="arrow" size="30px" />
</div>
</div>
<div class="hairlin"></div>
<!-- 评论区 -->
<div class="comment">
<div class="comment-header flex-around">
<div class="comment_header_left">
<span>评论区</span>
</div>
<div class="comment_header_right flex-around">
<span class="active" id="recommend" @click="change">推荐</span>
<span id="hot" @click="change">最热</span>
<!-- <span id="new">最新</span> -->
</div>
</div>
</div>
<div class="detail" v-if="isshow">
<div class="user" v-for="(item,index) in list" :key="index">
<div class="user-header flex">
<div class="user_header_left flex-around">
<!-- 头像 -->
<img :src="item.user.avatarUrl" alt="">
<div class="user-nickname">
<!-- 网名 -->
<span class="nick_name">{{item.user.nickname}}</span>
<!-- 时间 -->
<span>{{item.timeStr}}</span>
</div>
</div>
<div class="user_header_right">
<!-- 点赞 -->
<van-icon name="thumb-circle-o" size="35px" color="red" />
</div>
</div>
<div class="user_content">
<!-- 评论 -->
<span class="content">{{item.content}}</span>
</div>
<!-- 地点 -->
<!-- <span>{{item.ipLocation.location}}</span> -->
</div>
</div>
<div class="detail" v-if="!isshow">
<div class="user" v-for="(item,index) in hot" :key="index">
<div class="user-header flex">
<div class="user_header_left flex-around">
<!-- 头像 -->
<img :src="item.user.avatarUrl" alt="">
<div class="user-nickname">
<!-- 网名 -->
<span class="nick_name">{{item.user.nickname}}</span>
<!-- 时间 -->
<span>{{item.timeStr}}</span>
</div>
</div>
<div class="user_header_right">
<!-- 点赞 -->
<van-icon name="thumb-circle-o" size="30px" color="red" />
<span>{{item.likedCount}}</span>
</div>
</div>
<div class="user_content">
<!-- 评论 -->
<span class="content">{{item.content}}</span>
</div>
<!-- 地点 -->
<!-- <span>{{item.ipLocation.location}}</span> -->
</div>
</div>
</div>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
list: [],
hot:[],
isshow:true
}
},
created() {
axios.get(`http://localhost:3000/comment/playlist?id=${this.$route.params.id}&limit=30`).then(res => {
this.list = res.data.comments
// console.log(res)
// console.log(this.list)
}),
axios.get(`http://localhost:3000/comment/hot?id=${this.$route.params.id}&type=2`).then(item=>{
console.log(item)
this.hot=item.data.hotComments
console.log(this.hot)
})
},
mounted() {
},
methods: {
change(){
var recommend=document.querySelector("#recommend")
var hot=document.querySelector("#hot")
if(this.isshow==true)
{
recommend.classList.remove("active")
hot.classList.add("active")
this.isshow=false
}
else{
hot.classList.remove("active")
recommend.classList.add("active")
this.isshow=true
}
},
},
}
</script>
<style scoped>
.active{
font-weight: 800;
}
.detail {
padding-bottom: 4.8rem;
}
.user_header_right {
padding-right: 1.25rem;
}
.content {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
text-indent: 2rem;
}
.user_content {
padding: 0.625rem 3rem;
border-bottom: 1px solid #bababa;
}
.user_header_left {
width: 45%;
padding-left: 1.25rem;
}
.user_header_left img {
width: 2.5rem;
height: 2.5rem;
border-radius: 50%;
}
.nick_name {
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.user-nickname {
width: 5.5rem;
display: flex;
flex-direction: column;
}
.user-header {
width: 100%;
height: 4.375rem;
}
.user {
width: 100%;
}
.comment_header_right {
width: 40%;
}
.comment-header {
width: 100%;
height: 3.125rem;
}
.flex-around {
display: flex;
justify-content: space-around;
align-items: center;
}
.hairlin {
width: 100%;
height: 0.625rem;
background-color: #e4e4e3;
}
.middle-right {
display: flex;
flex-direction: column;
padding-left: 0.625rem;
font-size: 1.1rem;
}
.middle-left img {
width: 5rem;
height: 5rem;
border-radius: 0.625rem;
}
.middle-left {
width: 6rem;
height: 100%;
padding-left: 1.2rem;
padding-top: 0.625rem;
}
.middle {
width: 100%;
height: 6rem;
}
.flex {
display: flex;
justify-content: space-between;
align-items: center;
}
.header {
width: 100%;
height: 3.125rem;
background-color: white
}
.header-left {
width: 35%;
font-size: 1.25rem;
}
.header-right {
padding-right: 0.625rem;
}
</style>
FooterMusic.vue 放在views文件夹下,作为全局组件,固定于页面底部
<template>
<div class="footermusic">
<div class="footer-left" @click="showPopup">
<img :src="list[playindex].al.picUrl" alt="" class="footer-img">
<div>
<p class="al-name">{{list[playindex].al.name}}</p>
<span>横划切换上下首哦</span>
</div>
</div>
<div class="footer-right">
<van-icon name="play-circle-o" size="35px" @click="Play" id="musicicon" style="padding-right: 1.2rem;"/>
<van-icon name="orders-o" size="35px"/>
</div>
<audio id="music" :src="`https://music.163.com/song/media/outer/url?id=${list[playindex].id}.mp3`"></audio>
<van-popup v-model="show" position="right" :style="{ height: '100%' ,width:'100%',color:'white'}"
closeable close-icon-position="top-left"
close-icon="arrow-left">
<lyrics :musicList="list[playindex]" :Play="Play" :tem="tem" :islyric="islyric" :adduration="adduration"></lyrics>
</van-popup>
</div>
</template>
<script>
import {mapState,mapMutations,mapAction } from 'vuex'
import axios from 'axios'
import Vue from 'vue'
import lyrics from '@/views/LyRics.vue'
export default {
data() {
return {
tem:false,
music:[],
show:false,
lyriclist:{},
interval:0,
islyric:false
}
},
computed:{
// 数据解构
...mapState(["list","playindex","detailshow",]),
},
mounted(){
this.updatetime()
// console.log(this.islyric)
},
updated(){
axios.get(`http://localhost:3000/lyric?id=${this.list[this.playindex].id}`).then(res=>{
this.lyriclist=res.data.lrc
// console.log(this.lyriclist)
}),
this.adduration()
},
watch:{
playindex:function(){
var music=document.getElementById('music')
var musicicon=document.getElementById('musicicon')
music.autoplay="true"
if(music.paused){
musicicon.className='van-icon van-icon-pause-circle-o'
}
}
},
methods:{
Play(){
var music=document.getElementById('music')
var musicicon=document.getElementById('musicicon')
// console.log(musicicon)
// console.log('detail', this.$route.params.id)
if(this.tem==false)
{
music.play()
this.tem=true
// 歌词突出显示
this.updatetime()
musicicon.className='van-icon van-icon-pause-circle-o'
}
else
{
music.pause()
this.tem=false
//清除定时器
clearInterval(this.interval)
musicicon.className='van-icon van-icon-play-circle-o'
}
console.log(this.tem)
},
showPopup(){
this.show=true
this.uplyricslist(this.lyriclist)
// console.log(this.lyriclist)
},
// 计时器
updatetime(){
var music=document.getElementById('music')
this.interval=setInterval(()=>{
this.upcurrentime(music.currentTime)
},1000)
},
//歌曲总时长
adduration(){
var music=document.getElementById('music')
this.upduration(music.duration)
},
// 方法解构
...mapMutations(["uplyricslist","upcurrentime","upduration"])
},
components:{
lyrics,
}
}
</script>
<style scoped>
.al-name{
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 1;
-webkit-box-orient: vertical;
}
.footer-right{
width:20%;
height:100%;
display:flex;
justify-content: space-around;
align-items: center;
z-index: 1000;
}
.footermusic {
width: 100%;
height: 4.5rem;
position: fixed;
bottom: 0px;
border-top: 1px solid #000000;
display: flex;
padding: 0.8rem;
justify-content: space-between;
background-color:white;
}
.footer-left {
width: 60%;
height: 100%;
display: flex;
justify-content: space-around;
align-items: center;
}
.footer-img {
width: 3rem;
height: 3rem;
border-radius: 50%;
animation-name: music;
animation-timing-function: linear;
animation-duration: 3s;
animation-iteration-count: infinite;
}
@keyframes music{
0%{
transform: rotate(0deg);
}
50%{
transform: rotate(180deg);
}
100%{
transform: rotate(365deg);
}
}
</style>
FooterMusic.vue的相关子组件
LyRics.vue 放在views文件夹下,效果如图
包含播放动画,进度条实时移动,歌词滚动功能
![](https://i-blog.csdnimg.cn/blog_migrate/4e6e33baa110343cea8f64125c04ccec.png)
![](https://i-blog.csdnimg.cn/blog_migrate/0eba1fa4e9ccdc1c4bb9f1342d4319df.png)
<template>
<div>
<img :src="musicList.al.picUrl" alt="" class="all-img">
<div class="lyric-top">
<div class="lyric-topleft">
<div>
<!-- <marque :duration="600" :interval="2000">1111111111</marque> -->
<p style="color:white">{{musicList.al.name}}</p>
<span>
{{musicList.name}}
</span>
<van-icon name="arrow" />
</div>
</div>
<div class="lyric-topright">
<van-icon name="more-o" size="30px" color="white" />
</div>
</div>
<div class="lyric-middle" v-show="!islyricshow" @click="islyricshow=true">
<img src="../assets/needle-ab(1).png" alt=""
class="needle-img" :class="{needle_img_active:tem}">
<img src="../assets/disc-plus(1).png" alt="" class="disc-img">
<img :src="musicList.al.picUrl" alt="" class="img-al" :class="{img_al_active:tem,img_al_pause:!tem}">
</div>
<div class="musiclyric" ref="musicallyric" v-show="islyricshow" @click="islyricshow=false">
<p v-for="(item,index) in lyric" :key="index"
:class="{lyric_active:(currentime*1000>=item.time&¤time*1000<item.pre),lyric:!(currentime*1000>=item.time&¤time*1000<item.pre)}">
{{item.lrc}}
</p>
</div>
<div class="lyric-footer">
<div class="footer-top footer">
<van-icon name="like-o" size="35px" color="white" />
<van-icon name="down" size="30px" color="white" />
<van-icon name="good-job-o" size="30px" color="white" />
<van-icon name="chat-o" size="30px" color="white" />
<van-icon name="ellipsis" size="30px" color="white" />
</div>
<!-- 进度条 -->
<div class="footer-content">
<input type="range" class="range" min="0" :max="duration" v-model="currentime" step="0.05">
</div>
<div class="footer">
<van-icon name="replay" size="30px" color="white" />
<van-icon name="arrow-left" size="30px" color="white" @click="goPlay(-1)"/>
<van-icon name="pause-circle-o" size="60px" color="white" v-if="tem" @click="Play"/>
<van-icon name="play-circle-o" size="60px" color="white" v-else @click="Play"/>
<van-icon name="arrow" size="30px" color="white" @click="goPlay(1)"/>
<van-icon name="wap-home-o" size="30px" color="white" />
</div>
</div>
</div>
</template>
<script>
// import marque from 'vue-marquee';
import {
mapState,
mapMutations,
mapAction
} from 'vuex'
import Vue from 'vue'
export default {
props: ['musicList','tem','Play','adduration'],
data(){
return{
// 是否显示歌词
islyricshow:false,
}
},
watch:{
// 进度条
currentime:function(newvalue){
let p=document.querySelector("p.lyric_active")
//拿到p中距离顶部的距离,offsetTop
// console.log([p])
if(p){
if(p.offsetTop>300)
{
this.$refs.musicallyric.scrollTop=p.offsetTop-300
}
}
// 当进度条到达末尾时,
if(newvalue===this.duration)
{
// 如果是列表中最后一首歌,则跳回第一首
if(this.playindex==this.musicList.length-1)
{
this.updateIndex(0)
this.Play()
}
// 不是则下一首
else{
this.updateIndex(this.playindex+1)
}
}
}
},
computed:{
...mapState(["lyriclist","currentime","playindex","list","duration"]),
lyric:function(){
let arr;
if(this.lyriclist.lyric){
arr=this.lyriclist.lyric.split(/[(\r\n)\r\n]+/).map((item,i)=>{
let min=item.slice(1,3)
let sec=item.slice(4,6)
let mill=item.slice(7,10)
let lrc=item.slice(11,item.length)
// 总时长转为毫秒
let time=parseInt(min)*60*1000+parseInt(sec)*1000+parseInt(mill)
if(isNaN(Number(mill))){
mill=item.slice(7,9)
lrc=item.slice(10,item.length)
time=parseInt(min)*60*1000+parseInt(sec)*1000+parseInt(mill)
}
return {min,sec,mill,lrc,time}
})
}
arr.forEach((item,i)=>{
if(i==arr.length-1||isNaN(arr[i+1].time)){
item.pre=10000
}
else{
item.pre=arr[i+1].time
}
})
return arr
}
},
mounted() {
this.adduration()
},
methods: {
// 切换歌曲
goPlay:function(num){
let index=this.playindex+num
if(index<0)
{
index=this.list.length-1
}
else if(index==this.list.length)
{
index=0
}
this.updateIndex(index)
console.log(this.lyriclist)
},
...mapMutations(["updateIndex"]),
},
components: {
},
}
</script>
<style>
.range{
width:100%;
height:0.1875rem;
}
.lyric_active{
font-size:1.5rem;
font-weight: 900;
color:white;
margin-bottom: 1.875rem;
}
.lyric{
margin-bottom: 1.875rem;
color:#919191;
}
.musiclyric{
width:100%;
height:33rem;
display:flex;
flex-direction: column;
align-items: center;
margin-top: 1.25rem;
overflow: scroll;
}
.lyric-footer{
position: fixed;
bottom:0.625rem;
width:100%;
}
.footer-content {
width: 100%;
height: 7rem;
line-height: 7rem;
}
.footer {
width: 100%;
height: 3rem;
display: flex;
justify-content: space-around;
align-items: center;
}
.footer-top {
margin-top: 7.4rem;
}
.img-al {
width: 13rem;
height: 13rem;
border-radius: 50%;
position: absolute;
bottom: 3.14rem;
z-index: -1;
animation: rotate 10s linear infinite;
}
.img_al_active{
animation-play-state: running;
}
.img_al_pause{
animation-play-state:paused;
}
.disc-img {
width: 20rem;
height: 20rem;
position: absolute;
bottom: 0rem;
animation: rotate 10s linear infinite;
}
.disc_img_active{
animation-play-state: running;
}
.disc_img_pause{
animation-play-state:paused;
}
@keyframes rotate{
0%{
transform:rotateZ(0deg) ;
}
100%{
transform:rotateZ(360deg) ;
}
}
.needle-img {
width: 7rem;
height: 13rem;
position: absolute;
left: 46%;
transform: rotate(-10deg);
transform-origin: 0 0;
transition: all 2s;
z-index: 10;
}
.needle_img_active {
width: 7rem;
height: 13rem;
position: absolute;
left: 46%;
transform: rotate(6deg);
transform-origin: 0 0;
transition: all 2s;
z-index: 10;
}
.lyric-middle {
width: 100%;
height: 30rem;
display: flex;
flex-direction: column;
align-items: center;
position: relative;
}
.all-img {
width: 100%;
height: 100%;
position: absolute;
z-index: -1;
filter: blur(5rem);
}
.back {
width: 1.875rem;
height: 1.875rem;
}
.lyric-top {
height: 4.5rem;
width: 100%;
display: flex;
justify-content: space-between;
align-items: center;
}
.lyric-topleft {
width: 60%;
display: flex;
justify-content: space-between;
align-items: center;
margin-left:4rem;
}
.lyric-topright {
padding-right: 0.8rem;
}
</style>
Cloud.vue 云村,放在views文件夹下
功能还未完善,如果不写的话,需将router中的相关配置代码和TaBar中的声明导航代码删除
<template>
<div>
云村
</div>
</template>
<script>
</script>
<style>
</style>
ShiPing.vue 视频,放在views文件夹下
功能还未完善,如果不写的话,需将router中的相关配置代码和TaBar中的声明导航代码删除
<template>
<div>
视频
</div>
</template>
<script>
</script>
<style>
</style>
Login.vue 登录,放在views文件夹下
网易云手机登录功能调用登录接口的速度比调用其他接口慢,所以登录功能一般不成功,但可以用游客登录
<template>
<div>
<div class="container" id="container">
<div class="form-container sign-up-container">
<!-- 注册 -->
<form action="#">
<!-- <img :src="qrimgs" alt="" style="width:200px;height:200px"> -->
</form>
</div>
<div class="form-container sing-in-container">
<form action="#">
<input type="text" placeholder="请输入手机号" class="pwd" v-model="phone">
<input type="password" placeholder="请输入验证码" class="pwd" v-model="captcha">
<div class="check">
<span class="right"><a href="" @click="Captcha">短信验证码登录</a></span>
<span class="left"><a href="">用户注册</a></span>
</div>
<button @click="checkcaptcha">登陆</button>
<button @click="visitor">游客登陆</button>
</form>
</div>
<div class="overlay-container">
<div class="overlay">
<div class="overlay-pannel overlay-left">
<h1>知道账号?</h1>
<p>快快去登陆吧。</p>
<button class="ghost" id="signIn" @click="signInButton">登陆</button>
</div>
<div class="overlay-pannel overlay-right">
<h1>忘记帐号?</h1>
<p>点击扫码登录吧!</p>
<button class="ghost" id="signUp" @click="signUpButton">扫码登录</button>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
import {
mapState,
mapMutations,
mapAction
} from 'vuex'
import Vue from 'vue'
import axios from 'axios'
export default {
data() {
return {
phone: "",
captcha: "",
uid:""
}
},
computed: {
...mapState(["isfootermusic", "islogin"])
},
mounted() {
},
created() {},
methods: {
signUpButton() {
var signUpButton = document.querySelector('#signUp');
var container = document.querySelector('#container');
container.classList.add('right-panel-active')
},
signInButton() {
var signInButton = document.querySelector('#signIn');
var container = document.querySelector('#container');
container.classList.remove('right-panel-active')
},
// 判断登录成功
success(res){
// 表示登录成功
if (res.data.code === 200) {
this.upchangelogin()
// 和方法解构一样
// this.$store.commit("uptoken",res.data.)
this.$router.push('/inforuser')
this.upchangefooter()
} else {
alert("账号或密码不正确")
this.password = ""
}
},
// 验证码登录
login(res) {
if(res.data.data){
axios.get(`http://localhost:3000/login/cellphone?phone=${this.phone}&captcha=${this.captcha}`).then(item => {
console.log(item)
})
}
},
// 获取验证码
Captcha() {
axios.get(`http://localhost:3000/captcha/sent?phone=${this.phone}`).then(res => {
console.log(res)
})
},
// 判断验证码是否正确
checkcaptcha(){
axios.get(`http://localhost:3000/captcha/verify?phone=${this.phone}&captcha=${this.captcha}`).then(res=>{
console.log(1111,res)
this.login(res)
})
},
// 游客登录
visitor(){
axios.get(`http://localhost:3000/register/anonimous`).then(res=>{
console.log(res)
this.success(res)
this.getuser(res.data.userId)
})
},
// 获取用户信息
getuser(uid){
axios.get(`http://localhost:3000/user/account`).then(res=>{
console.log(res)
})
},
...mapMutations(["upchangefooter", "upchangelogin"])
},
watch: {
},
}
</script>
<style scoped>
* {
margin: 0;
padding: 0;
/*标准盒子 */
box-sizing: border-box;
}
body,
html {
background: #f8f8f8;
/* 锁定背景:*/
background-attachment: fixed;
/* 弹性布局 */
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 100%;
margin: 0 auto;
}
h1 {
margin: 15px;
font-size: 20px;
}
p {
font-size: 1rem;
line-height: 1.5rem;
/* 字体变淡 */
font-weight: 100;
margin: 1.2rem 0;
/* 字间距 */
letter-spacing: 0.1rem;
}
span {
font-size: 0.8rem;
margin: 1.2rem 0;
}
a {
color: #333;
font-size: 1rem;
/* 下划线消失 */
text-decoration: none;
}
.left {
float: left;
}
.check {
display: flow-root;
width: 80%;
}
.container {
/* 相对定位 */
position: absolute;
background: #fff;
border-radius: 10px;
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.25), 0 10px 10px rgba(0, 0, 0, 0.22);
padding: 0.6rem;
width: 50rem;
height: 28rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
/* 溢出隐藏 */
overflow: hidden;
max-width: 100vw;
min-height: 70vh;
}
.form-container form {
background: #fff;
/* 弹性布局 */
display: flex;
flex-direction: column;
padding: 0 1.8rem;
height: 100%;
justify-content: center;
align-items: center;
}
.social-container {
margin: 0.6rem 0;
}
.social-container a {
border: 1px solid #eee;
border-radius: 50%;
display: inline-flex;
justify-content: center;
align-items: center;
margin: 0 5px;
height: 1.8rem;
width: 1.8rem;
}
.social-container a:hover {
opacity: 0.8;
}
.form-container input {
width: 100%;
height: 2.2rem;
text-indent: 1rem;
border: 1px solid #ccc;
/* 把input上左右边框取消 */
border-left: none;
border-right: none;
border-top: none;
/* 点击input边框消失 */
outline: none;
margin: 0.6rem 0;
}
/* 被选中时候缩小 */
.form-container button:active {
transform: scale(0.95, 0.95);
}
.form-container button {
padding: 0.4rem 1rem;
background: #b2e4ff;
color: white;
border: 1px solid #fff;
outline: none;
/* 鼠标放上变小手 */
cursor: pointer;
width: 100%;
border-radius: 8px;
transition: all 100ms ease-in;
margin: 0.6rem 0;
font-size: 0.6rem;
padding: 0.5rem 0;
}
button#send_code {
width: 40%;
position: absolute;
top: 53%;
right: 20px;
}
button.ghost {
background: transparent;
border-color: #fff;
border: 1px solid #fff;
outline: none;
cursor: pointer;
width: 60%;
border-radius: 8px;
transition: all 800ms ease-in;
margin: 0.6rem 0;
padding: 0.5rem 0;
color: white;
font-size: 0.6rem;
}
button.ghost:active {
transform: scale(0.95, 0.95);
}
.pwd {
padding: 5px 9%;
background-size: 21px 21px;
background-position: 3% 10%;
}
.form-container {
/* 绝对定位 */
position: absolute;
top: 0;
height: 100%;
transition: all 0.5s ease-in;
}
.sing-in-container {
left: 0;
width: 50%;
z-index: 2;
}
.sign-up-container {
left: 0;
width: 50%;
opacity: 0;
z-index: 1;
}
.overlay {
background: #b2e4ff;
width: 200%;
height: 100%;
position: relative;
left: -100%;
transition: all 0.6s ease-in-out;
color: white;
}
.overlay-container {
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
overflow: hidden;
transition: all 0.6s ease-in-out;
z-index: 99;
}
.overlay-pannel {
position: absolute;
top: 0;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
width: 50%;
height: 100%;
padding: 0 2.2rem;
}
.overlay-right {
right: 0;
}
.container.right-panel-active .overlay-container {
transform: translateX(-100%);
}
.container.right-panel-active .sing-in-container {
transform: translateX(100%);
}
.container.right-panel-active .sign-up-container {
transform: translateX(100%);
opacity: 1;
z-index: 5;
transition: all 0.6s ease-in-out;
}
.container.right-panel-active .overlay {
transform: translateX(50%);
}
.container.right-panel-active .overlay-left {
transform: translateX(0);
transition: all 0.6s ease-in-out;
}
.container.right-panel-active .overlay-right {
transform: translateX(20%);
transition: all 0.6s ease-in-out;
}
</style>
Login.vue子组件
InforUser.vue 个人信息
功能还未完善,如果不写的话,需将router中的相关配置代码和Login.vue success函数中的相关代码删除
<template>
<div>
个人信息
</div>
</template>
<script>
</script>
<style>
</style>
最后,App.vue组件,也是根组件
<template>
<div id="app">
<router-view ></router-view>
<down v-if="isfootermusic"></down>
</div>
</template>
<style lang="scss">
* {
padding: 0px;
margin: 0px;
box-sizing: border-box;
}
a {
color: black;
}
</style>
<script>
import Vue from 'vue'
import Vant from 'vant'
import down from '@/views/FooterMusic.vue'
import MusicList from '@/views/Home/MusicList.vue'
import {mapState,mapMutations,mapAction } from 'vuex'
import 'vant/lib/index.css'
Vue.use(Vant)
export default {
components: {
down,
MusicList,
},
computed:{
...mapState(["isfootermusic"])
}
}
</script>
注意
运行项目时,可以取消eslint规则的检查,防止代码书写不规范问题
已经安装Vant,但是引用Vant里面组件库时候报错,可以重新安装vant2,并重启项目
npm i vant@latest-v2 -S
该项目也是我近几天所写,肯定还有许多需要完善的地方,还请各位多多指教,小妹谢谢各位咯!!!