目录
仿网易云移动端项目Vue3.2+Pinia+Vant+axios
前期准备(Pinia,rem,初始化样式,图标引入,vant组件,axios)
安装pinia
1.在main.js引入注册
2.创建store
rem移动适配
1.创建rem.js实现移动适配布局
2.在index.html引入
3.可以用px to rem 插件进行px和rem转换
阿里图标引入(Symbol方法)
1.在官网添加项目后复制symbol代码在index.html引入
2.使用svg
3.初始化样式,设置图标大小(在App.vue)
使用vant组件库
1.安装vant3组件
2.引入vant(按需引入,安装插件)
3.测试按钮(在main.js进行注册)
4.对引入vant组件库进行集中管理(封装)
安装axios
1.对axios进行封装
2.在组件中调用方法请求数据
组件开发(知识点)
路由
1.独享路由守卫
2.全局前置守卫
首页
1.在组件全局编程式路由跳转(无需引入)
2.flex布局
3.懒加载的轮播图组件(v-for循环请求回来的图片)
4.用vue2和vue3的写法获取数据
5.组件的模板内使用router-link(路由传参)
歌单详情页面
2.在刷新时读取不到store的数据,则先存储到本地
3.对播放量处理(定义一个函数对渲染的数据进行处理返回出去)
4.Css对图片进行虚化,并将其层级放底层(要定位)
5.Css文本超出几行就进行省略号表示
6.用v-for渲染数组时,数组内还有数组的元素可以再v-for进行渲染
1.vue2中的this.$ref在Vue3的使用
2.定时器的使用和清除
3.模板字符串的使用
歌词和磁盘播放页面
1.旋转图片动画(活跃和不活跃)
2.歌词
3.计算属性歌词处理(计算属性在vue3.2的使用)
4.使用vue3marquee实现歌名走马灯效果
5.watch在vue3.2的使用
搜索页面
1.页面渲染时读取本地历史记录
2.回车搜索需要进行去重和数组追加(unshift和Set语法)
登录组件
1.登录的接口
2.登录需要的数据共享
3.登录路由组件
基于vue-cli创建,rem移动适配方案
npm install pinia -S
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
const app = createApp(App)
app.use(router).use(createPinia()).mount('#app')
import { defineStore } from 'pinia'
export const useStore = defineStore('main', {
state: () => ({
}),
getters: {
},
actions: {
}
})
在public存放静态资源新建js文件夹,在js文件夹创建rem.js实现移动适配布局:
function remSize () {
/* 获取设备宽度 */
let deviceWith = document.documentElement.clientWidth || window.innerWidth
/* 设计稿宽度 */
if (deviceWith >= 750) {
deviceWith = 750
}
if (deviceWith <= 320) {
deviceWith = 320
}
/* 设置rem,
750px--> 1rem=100px
375px--> 1rem=50px
*/
document.documentElement.style.fontSize = (deviceWith / 7.5) + 'px'
/* 设置字体大小 */
document.querySelector('body').style.fontSize = 0.3 + 'rem'
}
remSize()
/* 当窗口发生变化 */
window.onresize = function () {
remSize()
}
需要对插件进行配置,基准font-size这里设置为50
那就是1rem=50px
在官网:https://www.iconfont.cn/manage/index?manage_type=myprojects&projectId=3415587
查看使用帮助,这里用到的是symbol
组件内使用:
注意:类名要加#号
symbol是设置width和height,而font class是设置fontsize
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
.icon {
width: .4rem;
height: .4rem;
}
a{
color: black;
}
官网:https://vant-contrib.gitee.io/vant/#/zh-CN
npm i vant
参照官网步骤
npm i babel-plugin-import -D
在.babelrc 或 babel.config.js 中添加配置
import { Button } from 'vant';
app.use(Button);
在src目录创建plugins文件夹,在plugins文件夹创建index.js:
import { Swipe, SwipeItem, NavBar, Button, Search, Popup } from 'vant'
/* 放入数组中 */
const plugins = [
Swipe, NavBar, SwipeItem, Button, Popup, Search
]
/* 循环将每一个插件注册到app上,main.js直接调用这个方法即可 */
export default function getVant (app) {
plugins.forEach((item) => {
return app.use(item)
})
}
在main.js引入调用方法即可
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'
/* 引入插件 */
import getVant from './plugins'
const app = createApp(App)
getVant(app)
app.use(router).use(createPinia()).mount('#app')
npm i axios -S
创建utils文件夹,在其文件夹下创建request.js用于封装请求根路径:
import axios from 'axios'
const service = axios.create({
baseURL: 'http://localhost:3000',
timeout: 3000
})
export default service
创建api文件夹,在其文件夹下创建homeApi.js模块对关于首页api接口进行封装:
import service from '@/utils/request.js'
/* 获取首页轮播图数据 */
/* 第一种写法
export function getBanner () {
return service({
method: 'GET',
url: '/banner?type=2'
})
} */
// 第二种写法
export function getBanner (type) {
return service.get('/banner', {
/* 请求参数 */
params: {
type
}
})
}
判断进入个人中心页面是否有登录或者是否有token,如果没有就跳转到登录页面
{
path: '/infoUser',
name: 'InfoUser',
// 独享路由守卫
beforeEnter: (to, from, next) => {
// pinia要在这里定义
const store = FooterMusicStore()
if (store.isLogin || store.token || localStorage.getItem('token')) {
next()
} else {
next('/login')
}
},
component: () => import('../views/InfoUser.vue')
}
如果跳转到登录页面就将隐藏底部播放栏组件
const router = createRouter({
history: createWebHashHistory(),
routes
})
// 全局前置守卫
router.beforeEach((to, from) => {
const store = FooterMusicStore()
if (to.path === '/login') {
store.isFooterMusic = false
} else {
store.isFooterMusic = true
}
})
export default router
路由组件:HomeView
组件:
点击我的就跳转到个人中心页面
$router.push('路径')
我的
发现
云村
视频
display: flex;
//设置盒子距离
justify-content: space-between;
//垂直居中
align-items: center;
/* 修改主轴对齐方向 */
flex-direction: column;
vue2
export default {
data () {
return {
musicList: []
}
},
methods: {
// 获取发现歌单
async getGnedan () {
const res = await getMusicList()
console.log(res)
this.musicList = res.data.result
},
// 对播放量进行处理
changeCount: function (num) {
if (num >= 100000000) {
// toFixed(1)显示一位小数
return (num / 100000000).toFixed(1) + '亿'
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
}
},
mounted () {
this.getGnedan()
}
}
vue3
import { getMusicList } from '@/api/homeApi'
import { reactive, onMounted, toRefs } from 'vue'
export default {
setup () {
const state = reactive({
musicList: []
})
onMounted(async () => {
const res = await getMusicList()
// console.log(res)
state.musicList = res.data.result
})
// 对播放量进行处理
const changeCount = function (num) {
if (num >= 100000000) {
// toFixed(1)显示一位小数
return (num / 100000000).toFixed(1) + '亿'
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
}
return {
...toRefs(state),
changeCount
}
}
}
from组件:
to跳转的路由组件进行接收:
可以通过useRoute()获取到路由信息
import { useRoute } from 'vue-router'
import { onMounted } from 'vue'
// useRoute可以拿到路由的参数
onMounted( () => {
/* 可以调用useRoute方法的query拿到id */
const id = useRoute().query.id
console.log(id)
})
路由组件:ItemMusic.vue
组件:
1.通过父传子值props(Vue3.2语法糖和Vue3写法)
父组件:
子组件:
vue3.0写法:
vue3.2写法:
保存:
/* 为防止页面刷新,数据丢失,将数据保存到sessionStorage */
sessionStorage.setItem('itemDetail', JSON.stringify(state))
使用:
creator = JSON.parse(sessionStorage.getItem('itemDetail')).playlist.creator
删除:
sessionStorage.removeItem('itemDetail')
// 对播放量进行处理
const changeCount = (num) => {
if (num >= 100000000) {
// toFixed(1)显示一位小数
return (num / 100000000).toFixed(1) + '亿'
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万'
}
}
在模板使用:
{{ changeCount(playlist.playCount) }}
.bgimg {
width: 100%;
height: 11rem;
position: absolute;
z-index: -1;
//虚化
filter: blur(0.6rem);
}
span {
width: 80%;
height: .6rem;
text-overflow: ellipsis;
overflow: hidden;
display: -webkit-box; //使用自适应布局
-webkit-line-clamp: 2; //设置超出行数,要设置超出几行显示省略号就把这里改成几
-webkit-box-orient: vertical;
}
如渲染歌曲列表每一首歌曲,而每一首歌曲又有多少歌手
{{ index + 1 }}
{{ item.name }}
{{ item1.name }}
在App.vue进行引入使用
需要用到Pinia进行全局数据共享,如底部组件是否显示
在模板中:
import { FooterMusicStore } from '@/store/FooterMusic.js'
import { ref, onMounted, watch } from 'vue'
/* vue3中this.$ref的使用变化 */
const audio = ref(null)
onMounted(() => {
console.log(audio)
})
注意改变audio需要audio.value,因为ref
// 定时器
let interVal = ref(0)
/* vue3中this.$ref的使用变化 */
const audio = ref(null)
onUpdated(() => {
// 渲染的时候也需要同步歌词时间
updateTime()
})
const play = () => {
/* 判断是否已暂停 */
if (audio.value.paused) {
// 触发定时器
updateTime()
} else {
// 清除定时器
clearInterval(interVal)
}
}
// 设置定时器方法来触发更新歌词时间
const updateTime = () => {
interVal = setInterval(() => {
store.udpateCurrentTime(audio.value.currentTime)
}, 1000)
}
// 获取歌曲歌词/lyric?id=33894312
export function getMusicLyric (data) {
return service({
method: 'GET',
/* 这里用到模板字符串,将data参数传进来 */
url: `/lyric?id=${data}`
})
}
在样式定义好样式和动画
.ar {
width: 3.2rem;
height: 3.2rem;
border-radius: 50%;
position: absolute;
bottom: 3.14rem;
/* 使用动画匀速,无限循环 */
animation: rotate_ar 10s linear infinite;
}
.ar_active {
animation-play-state: running;
}
.ar_paused {
animation-play-state: paused;
}
/* 定义图片旋转动画 */
@keyframes rotate_ar {
0% {
transform: rotateZ(0deg);
}
100% {
transform: rotateZ(360deg);
}
}
通过改变类名进行动画的开始暂停:
/* 歌词 */
.musiclyricList{
width: 100%;
height: 8rem;
display: flex;
flex-direction: column;
align-items: center;
margin-top: .2rem;
//溢出滚动
overflow: scroll;
p{
color:rgb(195, 239, 244);
margin-bottom: .4rem;
}
//高亮显示的歌词
.active{
color: white;
font-size: .4rem;
}
}
改变类名实现高亮:
{{item.lrc}}
import { computed, defineProps, onMounted, ref, watch } from 'vue'
import { Vue3Marquee } from 'vue3-marquee'
import { FooterMusicStore } from '@/store/FooterMusic.js'
import 'vue3-marquee/dist/style.css'
const store = FooterMusicStore()
const props = defineProps(['musicList', 'isbtnShow', 'play', 'addDuration'])
const isLyricShow = ref(false)
// 计算属性歌词处理
const lyric = computed(() => {
let arr
if (store.lyricList.lyric) {
/* 将歌词进行换行符分割 */
/* 1.先用数组split方法对歌词的换行进行分割
2.用map方法,遍历数组并对其进行操作返回一个新数组
3.以对象形式返回为新数组
*/
arr = store.lyricList.lyric.split(/[(\r\n)\r\n]+/).map((item, i) => {
// 分钟,切割第一到第三
const min = item.slice(1, 3)
// 秒钟切割
const 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)
}
// console.log(min, sec, Number(mill), lrc)
// 返回对象组成数组
return { min, sec, mill, lrc, time }
})
// 遍历拿到pre,即后一句歌词的时间
arr.forEach((item, i) => {
if (i === arr.length - 1 || isNaN(arr[i + 1].time)) {
item.pre = 100000
} else {
item.pre = arr[i + 1].time
}
})
}
return arr
}
)
下载地址:https://www.npmjs.com/package/vue3-marquee
npm install vue3-marquee@latest --save
先引入:
import { Vue3Marquee } from 'vue3-marquee'
import 'vue3-marquee/dist/style.css'
{{ musicList.name }}
// 监听歌词时间
watch(() => store.currentTime, (newValue) => {
const p = document.querySelector('p.active')
// console.log([p])
if (p) {
if (p.offsetTop > 300) {
musicLyric.value.scrollTop = p.offsetTop - 300
}
}
// console.log([musicLyric.value])
if (newValue === store.duration) {
if (store.playListIndex === store.playlist.length - 1) {
store.updateplayListIndex(0)
props.play()
} else {
store.updateplayListIndex(store.playListIndex + 1)
}
}
})
onMounted(() => {
// 页面渲染时读取本地历史记录
keyWorldList.value = JSON.parse(localStorage.getItem('keyWorldList')) ? JSON.parse(localStorage.getItem('keyWorldList')) : []
})
// 输入框回车操作进行搜索
const enterKey = async () => {
if ((searchKey.value !== '')) {
// 数组向前追加元素
keyWorldList.value.unshift(searchKey.value)
// 去重,这里用到Set语法
keyWorldList.value = [...new Set(keyWorldList.value)]
console.log([...new Set(keyWorldList.value)])
// 固定长度
if (keyWorldList.value.length > 10) {
keyWorldList.value.splice(keyWorldList.value.length - 1)
}
// 将历史记录保存到本地
localStorage.setItem('keyWorldList', JSON.stringify(keyWorldList.value))
const res = await getSearchMusic(searchKey.value)
console.log(res)
// 将请求回来的数据进行接收
searchList.value = res.data.result.songs
searchKey.value = ''
}
}
// 登录/login/cellphone?phone=xxx&password=yyy
export function getPhoneLogin (data) {
return service({
method: 'GET',
url: `/login/cellphone?phone=${data.phone}&password=${data.password}`
})
}
import { getPhoneLogin } from '@/api/homeApi.js'
import { defineStore } from 'pinia'
export const FooterMusicStore = defineStore('musicstore', {
state: () => {
return {
isLogin: false, // 登录状态
isFooterMusic: true, // 判断底部组件是否显示
token: '', // 接收后台返回的token字段
user: {}// 用户信息
}
},
getters: {
},
actions: {
// 登录请求
async getLogin (value) {
const res = await getPhoneLogin(value)
console.log('登录返回的数据:', res)
return res
},
// 更新登录状态
udpateIsLogin (value) {
this.isLogin = value
},
// 更新token字段
updateToken (value) {
this.token = value
localStorage.setItem('token', this.token)
},
// 更新用户信息
updateUser (value) {
this.user = value
localStorage.setItem('mydata', JSON.stringify(this.user))
}
}
})
欢迎登录
登录
返回首页
源码https://gitee.com/zi1726517395/emo_music.git