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
  1. vue3.0
  2. Vue – router4.0
  3. vuex4.0
  4. typescript
  5. Vite2.0
  6. 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:

  1. Top search function (not implemented yet)
  2. banner
  3. Navigation in a page
  4. Recommend the playlist
  5. Featured video (currently unable to call due to the interface has not been updated in time)
  6. player
  7. 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.vueIn 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…