This project is purely an exercise and there are many places that need to be optimized. I hope you can give more suggestions
Technology stack
- vue3.0
- Vue – router4.0
- vuex4.0
- typescript
- Vite2.0
- vant
Postcss-pxtorem, sASS, vConsole
Project initialization
Vite requires node.js version >= 12.0.0
# npm 6.x
npm init @vitejs/app my-vue-app --template vue-ts
# npm 7+, require extra double line NPM init. @vitejs/app my-vue-app -- --template vue-ts CD my-vue-app NPM install NPM run devCopy the code
Netease Cloud music service
The installation
git clone [email protected]:Binaryify/NeteaseCloudMusicApi.git
npm install
Copy the code
or
git clone https://github.com/Binaryify/NeteaseCloudMusicApi.git
npm install
Copy the code
run
node app.js
Copy the code
The default boot port is 3000. If you do not want to use port 3000, run the following command: Mac/Linux
PORT=4000 node app.js
Copy the code
In Windows, run the following command using a terminal such as git-bash or cmder:
set PORT=4000 && node app.js
Copy the code
For more details, please visit the netease Cloud API
Home page implementation
The relevant API involved in the project will not be described in detail in this article. Students who do not understand can go to the official information.
First, analyze the home page of netease Cloud as shown in the figure:
- Top search function (not implemented yet)
- banner
- Navigation in a page
- Recommend the playlist
- Featured video (currently unable to call due to the interface has not been updated in time)
- player
- At the bottom of the navigation
//main.ts
import { createApp } from 'vue'
import App from './App.vue'
import Router from "./router/index"
import "./utils/rem"
import ant from "vant"
import 'vant/lib/index.css'
import store from "./state/index"
import vConsole from "vconsole"
new vConsole();
createApp(App).use(Router).use(store).use(ant).mount('#app')
Copy the code
// Base size
const baseSize = 37.5
// Note that this value must be consistent with rootValue in the postcss.config.js file
// Set the rem function
function setRem () {
// The current page width relative to the 375 width scale can be modified according to your own needs, generally the design is 750 wide.
const scale = document.documentElement.clientWidth / 375
// Set the font size of the root node of the page (" math.min (scale, 2) "means the maximum zoom ratio is 2, which can be adjusted according to actual business requirements)
document.documentElement.style.fontSize = baseSize * Math.min(scale, 2) + 'px'
}
/ / initialization
setRem()
// Resets rem when changing the window size
window.onresize = function () {
setRem()
}
Copy the code
// Create a new banner component
<template>
<div class='banner'>// I use van framework to swipe here, the relevant API can be found on the official website<van-swipe class="swipe" :autoplay="3000" indicator-color="white">
<van-swipe-item>
<img src=' ' />
</van-swipe-item>
</van-swipe>
</div>
</template>
<script lang='ts' setup>
Setup here is the new syntax sugar for Vue3.0
/ / import Vue
import { reactive } from "vue"
// Declare a variable to hold the banner array
const banner = reactive({
lists:[]
})
</script>
Copy the code
// Navigate to a new page
<template>
<div class='nav'>
<div class='nav-items'>
<div class='nav-icon'></div>
<div class='nav-name'></div>
</div>
</div>
</template
<script lang='ts' setup>
import { reactive } from "vue"
const nav = reactive({
lists:[]
})
</script>
Copy the code
The MINI version of the home page of the player click on the song name to open a new song details page
During the experience process, MINI player will appear in multiple pages, and the corresponding playing status will be synchronized in the player details page, so first<audio>
The label on theApp.vue
In the.
// app.vue
<template>
<div class="app">
<transition :name="transitionName">
<router-view></router-view>
</transition>
<! Bottom navigation -->
<bottomNav></bottomNav>
<audio id="audio" controls="controls" ref='audio' style="displat:none" />
</div>
</template>
Copy the code
Here we need to hide the operation for the audio label, customize all the buttons for the visual interface behind, and pass Auido to the offspring for the convenience of subsequent operations.
Vex3.0’s provide is used here
<script lang="ts">
import { defineComponent , watch ,ref ,provide , onMounted ,computed} from 'vue'
import bottomNav from "./components/nav.vue"
import { useRoute,useRouter } from 'vue-router'
export default defineComponent({
name: 'App'.components:{bottomNav},
setup(){
onMounted(() = >{
const audio = document.getElementById('audio')
provide('audio',audio)
})
const Router = useRouter()
const Route = useRoute();
const transitionName = ref("fade")
// The listener route slides up to the playback details page separately
watch(() = > Route.path,(n,o) = >{
if(n == '/play'){
transitionName.value = "top"
}else{
transitionName.value = ""}})return {
transitionName
}
}
})
</script>
Copy the code
OK, design the state store for the player
//store/modules/play.ts
const state = () = > {
return {
songId:"".// ID of the current song
url:"".// The URL of the currently playing song
songInfo: {},// The song's trust message
playStatus:false.// Song playing status
playTimeOut:0.// The remaining peak time of the song.
list: []./ / song list}}const getters = {
getSongurl: (state:any) = > state.url,
getSongInfo:(state:any) = > state.songInfo,
getPlayTimeOut:(state:any) = > state.playTimeOut,
getPlayStatus:(state:any) = > state.playStatus,
getSongId:(state:any) = > state.songId
}
const mutations = {
setSongInfo : (state:any,info:any) = >{
state.songInfo = info
},
setSongId :(state:any,songid:string) = > {
state.songId = songid
},
setSongUrl :(state:any,url:string) = >{
state.url = url
},
setPlayTimeOut:(state:any,time:any) = >{
state.playTimeOut = time
},
setPlayStatus: (state:any,status:any) = >{
state.playStatus = status
}
}
Copy the code
//miniPlay.vue
<template>
// Check whether there is any song information in the current state
<div class="miniPlay" v-if="songInfo && songInfo.al">
<div class="icon">
<img v-if="songInfo && songInfo.al" :class="{'play':playStatus}" class="picUrl" :src="songInfo.al.picUrl" alt="">
</div>
<div class="name" @click="goToPlay">{{songInfo.name}}</div>
<div class="playHanled">
<div @click="playHandle" v-if=! "" playStatus">Here is the icon that plays</div>
<div v-else @click="pausedHanle">Here is the pause icon</div>
</div>
<div class="list">Here is the icon of the list</div>
</div>
</template>
<script lang="ts" setup>
import { computed } from "vue"
import { useStore } from "vuex"
import { useRouter } from 'vue-router'
//auiod. Ts store audio related methods
import {pausedHanle ,playHandle} from "./audio"
const Store = useStore()
// Get information about the song
const songInfo = computed(() = >{
return Store.getters['play/getSongInfo']})// Get the status of the song
const playStatus = computed(() = >{
return Store.getters['play/getPlayStatus']})const Router = useRouter()
// Go to the details page and pass in the song ID
function goToPlay (){
Router.push({
path:'/play'.query: {songid:songInfo.value.id
}
})
}
</script>
<style scoped lang='scss'>
.miniPlay{
position: fixed;
bottom: 50px;
width: 100%;
left: 0;
z-index: 5;
background: #ffffff;
display: flex;
height: 50px;
align-items: center;
font-size: 14px;
padding: 0 20px 10px;
.name{
width: 200px;
margin-left: 10px;
white-space:nowrap;
overflow: hidden;
text-overflow: ellipsis;
color: # 999;
}
.icon {
width: 40px;
height: 40px;
border-radius: 40px;
overflow: hidden;
img.picUrl{
width: 100%;
height:100%; }}.playHanled{
margin-right: 10px; }}.play{
animation:turn 5s linear infinite;
}
@keyframes turn {
0%{-webkit-transform:rotate(0deg); }25%{-webkit-transform:rotate(90deg); }50%{-webkit-transform:rotate(180deg); }75%{-webkit-transform:rotate(270deg); }100%{-webkit-transform:rotate(360deg);}
}
Copy the code
//audio
import Vuex from ".. /state/index"
const audio = document.getElementById('audio')
/ / pause
export function pausedHanle(){
Vuex.commit('play/setPlayStatus'.false)
audio.pause()
}
/ / play
export function playHandle () {
Vuex.commit('play/setPlayStatus'.true)
audio.play()
}
Copy the code
Play page implementation
<img class="img" v-if="songs. PicUrl! = "" : SRC =" songs. PicUrl "Alt =" "> < div class =" playBg "> < / div > < / div > < style > / / falsified processing, magnified area, Bg {transform: scale(1.5,1.5); transform: scale(1.5,1.5); filter: blur(20px); position: absolute; z-index: 2; width: 100vw; height: 100vh; overflow: hidden; // a gray mask. PlayBg {background: rgba(0, 0, 0,.2); z-index: 3; width: 100vw; height: 100vh; position: absolute; top: 0; left: 0; } // img{height: 100%; position: relative; z-index: 2; } } </style>Copy the code
<script lang="ts" setup> import {defineProps, onMounted ,ref ,reactive ,onUnmounted,computed,inject,watch } from "vue" import { useRoute,useRouter } from "vue-router" import { useStore , mapGetters} from "vuex" import {getSongDetail ,getSongUrl} from ".. /api/login" import {formatSecToDate} from ".. /utils/utils" import { playHandle } from ".. /components/audio" const Router = useRouter() const Route = useRoute() const songId = defineProps({ID :{ Type :String}}) const store = useStore() const audio:any = inject("audio") const songs = reactive({ Details :{},// song details picUrl:"",// song path name:""// song name}) // Play progress rotation let timer:any = null let playStatus:any = null let Duration :any = ref(") let currentTime:any = ref(") let line:any = null // Dom let pointLeft = ref(0) // Play progress bar let BufferedLine = ref(0) // Run progress bar // Remaining time let timeOut = computed(() => {return formatSecToDate((duration.value - Currenttime.value)))}) // Play temporary animation const transitionName = ref('fold-left') // CD animation const selfPlayyStatus = computed(()=>{ return store.getters['play/getPlayStatus'] }) const songPlayId = store.getters['play/getSongId'] const songInfo = store.getters['play/getSongInfo'] function getSongDetails (){ getSongDetail(`/song/detail? ids=${Route.query.songid}`,{}).then(res =>{ if(res.data.code == 200){ songs.name = res.data.songs[0].name songs.picUrl = res.data.songs[0].al.picUrl store.commit('play/setSongInfo',res.data.songs[0]) getSongUrls(Route.query.songid) } }) } function getSongUrls (id) { getSongUrl(`/song/url?id=${id}`,{}).then(res =>{ if(res.data.code == 200) { audio.src = res.data.data[0].url } }) } function pausedHanle (){ clearInterval(timer) store.commit('play/setPlayStatus',false) Function pointLeftLoop(){clearInterval(timer) timer = setInterval(() =>{if(audio-currentTime) == audio.duration){// Stop playing pausedHanle()} CurrentTime.value = audio.currentTime; pointLeft.value = Number((line.offsetWidth * audio.currentTime)/ audio.duration) Store.com MIT ('play/setPlayTimeOut',audio. Duration-audio. CurrentTime)},1000)} // Cache progress bar status function bufferedLineHandle(){ var z = audio.buffered.end(audio.buffered.length-1); bufferedLine.value = Number((line.offsetWidth * z)/ audio.duration) } onMounted(() =>{ line = document.getElementById("line") if(! selfPlayyStatus.value || (songPlayId ! = Route.query.songid && selfPlayyStatus.value) ){ getSongDetails() }else{ songs.picUrl = songInfo.al.picUrl songs.name = songInfo.name pointLeftLoop() currentTime.value = store.getters["play/getPlayTimeOut"]; duration.value = audio.duration; timeOut = computed(() => { return formatSecToDate(((duration.value - currentTime.value))) }) bufferedLineHandle() } Audio. AddEventListener ("canplay", function(){ Duration playHandle() pointLeftLoop() store.com MIT ("play/setSongUrl","") store.commit("play/setSongId",Route.query.songid) }); audio.addEventListener('progress',function(){ bufferedLineHandle() }) audio.addEventListener('loadedmetadata',function(){ console.log('loadedmetadata') }) audio.addEventListener('loadeddata',function(){ console.log('loadeddata') }) audio.addEventListener('loadstart',function(){ console.log('loadstart') }) audio.addEventListener('playing',function(){ console.log('playing') }) audio.addEventListener('stalled',function(){ console.log('stalled') }) audio.addEventListener("waiting",function(){ console.log('waiting') }) }) onUnmounted(() =>{ // clearInterval(playStatus) clearInterval(timer) }) </script>Copy the code
Overall page implementation
<template> <div id="playContent"> <div class="songName">{{songs.name}}</div> <! -- Hide player --> <! -- <audio id="audio" controls="controls" ref='audio' style="displat:none" /> --> <div class="bg"> <img class="img" v-if="songs.picUrl ! = ''" :src="songs.picUrl" alt=""> <div class="playBg"></div> </div> <div class="plate" :class="{'play':selfPlayyStatus}"> <img v-if="songs.picUrl ! = ''" :src="songs.picUrl" alt=""> </div> <div class="playController"> <div class="oth"></div> <div class="progress"> <div class='line' id="line"> <div class="point" :style="{left:pointLeft+ 'px'}"></div> <div class="bufferedLine" :style="{width:bufferedLine + 'px'}"></div> </div> <div class="time">{{timeOut}}</div> </div> <div class="Controller"> <div @click="playHandle" v-if="! SelfPlayyStatus "> selfPlayyStatus </div> <div v-else @click="pausedHanle"> selfPlayyStatus </div> </div> </div> </div> </template>Copy the code
First put a link, the big guys to give more advice github.com/Eryoo/wy-mu…