600字范文,内容丰富有趣,生活中的好帮手!
600字范文 > 【Vue知识点- No8.】网易云音乐案例(vant组件库的使用)

【Vue知识点- No8.】网易云音乐案例(vant组件库的使用)

时间:2024-06-03 18:20:24

相关推荐

【Vue知识点- No8.】网易云音乐案例(vant组件库的使用)

No8.网易云音乐案例

知识点自测

知道reset.css和flexible.js的作用。什么是组件库-例如bootstrap的作用。yarn命令的使用。组件名字用name属性方式注册。如何自定义组件库样式。

1.本地接口项目部署

下载网易云音乐node接口项目, 在本地启动, 为我们vue项目提供数据支持。

项目地址

备用地址

下载git clone git@:Binaryify/NeteaseCloudMusicApi.git

下载后,安装所有依赖:npm install 或者 yarn,

在本地启动起来:node app.js 不能使用npm run serve启动,因为这不是vue的脚手架项目,

测试访问此地址是否有数据:http://localhost:3000, 看到如下页面就成功了。

总结:Node搭建的服务,如何把数据请求回来?前端请求本地的node项目, 收到请求后,node服务器伪装请求去拿网易云音乐服务器数据转发回给自己前端。(如何做反向代理解决跨域问题?本地node服务器开启cors,负责请求的转发和数据接收回传。)

学习目标

1.能够掌握vant组件库的使用。

2.能够掌握vant组件自定义样式能力。

3.能够掌握组件库使用和文档使用能力。

4.能够完成网易云音乐案例。

2.前端项目准备

2-1. 前端项目初始化

目标: 初始化项目, 下载必备包, 引入初始文件, 配置按需自动引入vant, 创建页面组件。

1》初始化工程:vue create music-demo

2》下载需要的所有第三方依赖包:yarn add axios vant vue-router

3》下载Vant自动按需引入插件:yarn add babel-plugin-import -D

本次vant使用自动按需引入的方式:https://vant-contrib.gitee.io/vant/#/zh-CN/quickstart文档babel-plugin-import 是一款 babel 插件,它会在编译过程中将 import 的写法自动转换为按需引入的方式。

4》在babel.config.js配置–看Vant文档。

plugins: [['import', {libraryName: 'vant',libraryDirectory: 'es',style: true}, 'vant']]

5》引入笔记代码里准备好的reset.css和flexible.js- 实现样式初始化和适配问题 -引入到main.js中

import "@/mobile/flexible" // 适配import "@/styles/reset.css" // 初始化样式

2-2. 需求分析

根据需求, 创建路由所需要的5个页面的组件:

Layout(布局, 顶部导航和底部导航) > 二级路由 Home 、Search和Play。

布局图

创建需要的views下的页面组件4个:

src/views/Layout/index.vue - 负责布局(上下导航 - 中间二级路由切换首页和搜索页面)

<style scoped>/* 中间内容区域 - 容器样式(留好上下导航所占位置) */.main {padding-top: 46px;padding-bottom: 50px;}</style>

views/Home/index.vue - 标题和歌名样式

/* 标题 */.title {padding: 0.266667rem 0.24rem;margin: 0 0 0.24rem 0;background-color: #eee;color: #333;font-size: 15px;}/* 推荐歌单 - 歌名 */.song_name {font-size: 0.346667rem;padding: 0 0.08rem;margin-bottom: 0.266667rem;word-break: break-all;text-overflow: ellipsis;display: -webkit-box; /** 对象作为伸缩盒子模型显示 **/-webkit-box-orient: vertical; /** 设置或检索伸缩盒对象的子元素的排列方式 **/-webkit-line-clamp: 2; /** 显示的行数 **/overflow: hidden; /** 隐藏超出的内容 **/}

views/Search/index.vue

/* 搜索容器的样式 */.search_wrap {padding: 0.266667rem;}/*热门搜索文字标题样式 */.hot_title {font-size: 0.32rem;color: #666;}/* 热搜词_容器 */.hot_name_wrap {margin: 0.266667rem 0;}/* 热搜词_样式 */.hot_item {display: inline-block;height: 0.853333rem;margin-right: 0.213333rem;margin-bottom: 0.213333rem;padding: 0 0.373333rem;font-size: 0.373333rem;line-height: 0.853333rem;color: #333;border-color: #d3d4da;border-radius: 0.853333rem;border: 1px solid #d3d4da;}

views/Play/index.vue - 直接从预习资料里复制(节省时间) - 可自己扩展阅读代码

<template><div class="play"><!-- 模糊背景(靠样式设置), 固定定位 --><divclass="song-bg":style="`background-image: url(${songInfo && songInfo.al && songInfo.al.picUrl}?imageView&thumbnail=360y360&quality=75&tostatic=0);`"></div><!-- 播放页头部导航 --><div class="header"><van-iconname="arrow-left"size="20"class="left-incon"@click="$router.back()"/></div><!-- 留声机 - 容器 --><div class="song-wrapper"><!-- 留声机本身(靠css动画做旋转) --><divclass="song-turn ani":style="`animation-play-state:${playState ? 'running' : 'paused'}`"><div class="song-img"><!-- &&写法是为了防止报错, 有字段再继续往下访问属性 --><imgstyle="width: 100%":src="`${songInfo && songInfo.al && songInfo.al.picUrl}?imageView&thumbnail=360y360&quality=75&tostatic=0`"alt=""/></div></div><!-- 播放按钮 --><div class="start-box" @click="audioStart"><span class="song-start" v-show="!playState"></span></div><!-- 播放歌词容器 --><div class="song-msg"><!-- 歌曲名 --><h2 class="m-song-h2"><span class="m-song-sname">{{songInfo.name }}-{{songInfo && songInfo.ar && songInfo.ar[0].name}}</span></h2><!-- 歌词部分-随着时间切换展示一句歌词 --><div class="lrcContent"><p class="lrc">{{curLyric }}</p></div></div><!-- 留声机 - 唱臂 --><div class="needle" :style="`transform: rotate(${needleDeg});`"></div></div><!-- 播放音乐真正的标签看接口文档: 音乐地址需要带id去获取(但是有的歌曲可能404)https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%e8%8e%b7%e5%8f%96%e9%9f%b3%e4%b9%90-url--><audioref="audio"preload="true":src="`/song/media/outer/url?id=${id}.mp3`"></audio></div></template><script>// 获取歌曲详情和 歌曲的歌词接口import {getSongByIdAPI, getLyricByIdAPI } from '@/api'import {Icon } from 'vant'export default {components: {[Icon.name]: Icon,},name: 'play',data() {return {playState: false, // 音乐播放状态(true暂停, false播放)id: this.$route.query.id, // 上一页传过来的音乐idsongInfo: {}, // 歌曲信息lyric: {}, // 歌词枚举对象(需要在js拿到歌词写代码处理后, 按照格式保存到这个对象)curLyric: '', // 当前显示哪句歌词lastLy: '' // 记录当前播放歌词}},computed: {needleDeg() {// 留声机-唱臂的位置属性return this.playState ? '-7deg' : '-38deg'}},methods: {async getSong() {// 获取歌曲详情, 和歌词方法const res = await getSongByIdAPI(this.id)this.songInfo = res.data.songs[0]// 获取-并调用_formatLyr方法, 处理歌词const lyrContent = await getLyricByIdAPI(this.id)const lyricStr = lyrContent.data.lrc.lyricthis.lyric = this._formatLyr(lyricStr)// 初始化完毕先显示零秒歌词this.curLyric = this.lyric[0]},_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, 对应歌词作为valuelyricObj[ms + Number(ss)] = contentArr[index]})// 返回得到的歌词对象(可以打印看看)console.log(lyricObj);return lyricObj},audioStart() {// 播放按钮 - 点击事件if (!this.playState) {// 如果状态为falsethis.$refs.audio.play() // 调用audio标签的内置方法play可以继续播放声音} else {this.$refs.audio.pause() // 暂停audio的播放}this.playState = !this.playState // 点击设置对立状态},showLyric() {// 监听播放audio进度, 切换歌词显示this.$refs.audio.addEventListener('timeupdate', () => {let curTime = Math.floor(this.$refs.audio.currentTime)// 避免空白出现if (this.lyric[curTime]) {this.curLyric = this.lyric[curTime]this.lastLy = this.curLyric} else {this.curLyric = this.lastLy}})}},mounted() {this.getSong()this.showLyric()console.log(this.$route.query.id);}}</script><style scoped>.header {height: 50px;}.play {position: fixed;top: 0;left: 0;right: 0;bottom: 0;z-index: 1000;}.song-bg {background-color: #161824;background-position: 50%;background-repeat: no-repeat;background-size: auto 100%;transform: scale(1.5);transform-origin: center;position: fixed;left: 0;right: 0;top: 0;height: 100%;overflow: hidden;z-index: 1;opacity: 1;filter: blur(25px); /*模糊背景 */}.song-bg::before{/*纯白色的图片做背景, 歌词白色看不到了, 在背景前加入一个黑色半透明蒙层解决 */content: " ";background: rgba(0, 0, 0, 0.5);position: absolute;left: 0;top: 0;right: 0;bottom:0;}.song-wrapper {position: fixed;width: 247px;height: 247px;left: 50%;top: 50px;transform: translateX(-50%);z-index: 10001;}.song-turn {width: 100%;height: 100%;background: url("./img/bg.png") no-repeat;background-size: 100%;}.start-box {position: absolute;width: 156px;height: 156px;position: absolute;left: 50%;top: 50%;transform: translate(-50%, -50%);display: flex;justify-content: center;align-items: center;}.song-start {width: 56px;height: 56px;background: url("./img/start.png");background-size: 100%;}.needle {position: absolute;transform-origin: left top;background: url("./img/needle-ab.png") no-repeat;background-size: contain;width: 73px;height: 118px;top: -40px;left: 112px;transition: all 0.6s;}.song-img {width: 154px;height: 154px;position: absolute;left: 50%;top: 50%;overflow: hidden;border-radius: 50%;transform: translate(-50%, -50%);}.m-song-h2 {margin-top: 20px;text-align: center;font-size: 18px;color: #fefefe;overflow: hidden;white-space: nowrap;text-overflow: ellipsis;}.lrcContent {margin-top: 50px;}.lrc {font-size: 14px;color: #fff;text-align: center;}.left-incon {position: absolute;top: 10px;left: 10px;font-size: 24px;z-index: 10001;color: #fff;}.ani {animation: turn 5s linear infinite;}@keyframes turn {0% {-webkit-transform: rotate(0deg);}100% {-webkit-transform: rotate(360deg);}}</style>

2-3. 路由准备

目标: 准备路由配置, 显示不同路由页面。

src/router/index.js - 准备路由 - 以及默认显示Layout, 然后Layout默认显示二级路由的首页。

// 路由-相关模块import Vue from 'vue'import VueRouter from 'vue-router'import Layout from '@/views/Layout'import Home from '@/views/Home'import Search from '@/views/Search'import Play from '@/views/Play'Vue.use(VueRouter)const routes = [{path: '/',redirect: '/layout'},{path: '/layout',component: Layout,redirect: '/layout/home',children: [{path: 'home',component: Home,meta: {// meta保存路由对象额外信息的title: "首页"}},{path: 'search',component: Search,meta: {title: "搜索"}}]},{path: '/play',component: Play}]const router = new VueRouter({routes})export default router

main.js中引入路由对象, 注册到new Vue中

import router from '@/router'new Vue({render: h => h(App),router}).$mount('#app')

App.vue中留好router-view挂载点显示路由页面

<template><div><!--一级路由显示的地方--><router-view></router-view></div></template>

3.TabBar+NavBar+网络请求封装

3-1. Tabbar组件

目标: 点击底部导航, 切换路由页面显示。

文档: https://vant-contrib.gitee.io/vant/#/zh-CN/tabbar

在main.js中注册Tabbar组件,

import {Tabbar, TabbarItem } from 'vant';Vue.use(Tabbar);Vue.use(TabbarItem);

在Layout.vue中使用

<template><div><div class="main"><!-- 二级路由-挂载点 --><router-view></router-view></div><van-tabbar route><van-tabbar-item replace to="/layout/home" icon="home-o">首页</van-tabbar-item><van-tabbar-item replace to="/layout/search" icon="search">搜索</van-tabbar-item></van-tabbar></div></template><script>export default {}</script><style scoped>/* 中间内容区域 - 容器样式(留好上下导航所占位置) */.main {padding-top: 46px;padding-bottom: 50px;}</style>

开启路由模式route属性, 和to属性指向要切换的路由路径:

<van-tabbar route><van-tabbar-item icon="home-o" replace to="/layout/home">首页</van-tabbar-item><van-tabbar-item icon="search" replace to="/layout/search">搜索</van-tabbar-item></van-tabbar>

总结:van-tabbar开启route。

3-2. NavBar导航组件

目标: 实现顶部标题展示。

文档: https://vant-contrib.gitee.io/vant/#/zh-CN/nav-bar

1.main.js - 注册NavBar组件

import {NavBar } from 'vant';Vue.use(NavBar);

2.复制文档里的, 然后删删只留标题,Layout/index.vue

<van-nav-bar title="标题"/>

3-3. NavBar标题切换

目标: 实现点击底部导航/刷新非第一页面, 导航标题正确显示。

网页打开默认显示。侦听路由切换显示对应标题。meta元信息:给当前路由对象绑定值。 在router/index.js - 给$route里需要导航标题的添加meta元信息属性

{path: '/layout',component: Layout,redirect: '/layout/home',children: [{path: 'home',component: Home,meta: {// meta保存路由对象额外信息的title: "首页"}},{path: 'search',component: Search,meta: {title: "搜索"}}]},

Layout/index.vue 中监听$route改变,给导航active的值设置$route里的元信息的标题。当前路由对象this.$route

<van-nav-bar :title="activeTitle" fixed />export default {data() {return {activeTitle: this.$route.meta.title, // "默认"顶部导航要显示的标题 (默认获取当前路由对象里的meta中title值)}},// 路由切换 - 侦听$route对象改变watch: {$route() {this.activeTitle = this.$route.meta.title; // 提取切换后路由信息对象里的title显示},},}

总结: 点击底部导航和刷新当前网页, 都能保证导航标题的正确显示。

3-4. 网络请求封装

目标: 不想把网络请求散落在各个逻辑页面里, 不然以后找起来改起来很麻烦。

1.封装 src/utils/request.js - 基于axios进行二次封装 -设置基础地址

// 网络请求 - 二次封装import axios from 'axios'axios.defaults.baseURL = "http://localhost:3000"export default axios

封装完成后也可以直接在各个vue文件中使用:axios(url:“/a/b”),但是这样也不太方便更改(文件太分散)。所以需要集中管理。

2.封装 src/api/Home.js 统一封装网络请求方法(文件名Home.js尽量和模块页面文件名统一,方便查找。)

本地接口项目:https://binaryify.github.io/NeteaseCloudMusicApi/#

// 文件名-尽量和模块页面文件名统一(方便查找)import request from '@/utils/request'// 首页 - 推荐歌单export const recommendMusic = params => request({url: '/personalized',params // params: params// 将来外面可能传入params的值 {limit: 20}})

3.在 src/api/index.js -统一导出接口供外部使用

// api文件夹下 各个请求模块的js, 都统一来到index.js再向外导出import {recommendMusic} from './Home'// 请求推荐歌单的方法导出export const recommendMusicAPI = recommendMusic

4.在main.js - 测试使用一下:npm run serve 和 node app.js

import {recommendMusicAPI } from '@/api/index'async function myFn(){const res = await recommendMusicAPI({limit: 6}); // api方法原地会得到axios请求在原地的Promise对象(里面有一个ajax请求)// await等待数据返回console.log(res);}myFn();

总结: 封装网络请求方法目的, 方便我们统一管理。

解决报错

【解决报错1】Can’t import the named export ‘useWindowSize’ from non EcmaScript module (only default export is available)

【解决报错2】"export ‘default’ (imported as ‘VueRouter’) was not found in ‘vue-router’ 这有可能是vue-router版本装错了,解决办法是重新安装@3.1.3。

【解决报错3】"export ‘createVNode’ (imported as ‘_createVNode’) was not found in ‘vue’

"export ‘mergeProps’ (imported as ‘_mergeProps’) was not found in ‘vue’

这些都是Vant版本装的太高了

4.推荐歌单+搜索关键字

4-1. 首页-推荐歌单

接口地址:/personalized

1.布局:采用van-row和van-col,布局文档:https://vant-contrib.gitee.io/vant/#/zh-CN/col

2.使用vant内置的图片组件来显示图片

在main.js注册使用的组件

import {Col, Row, Image as VanImage } from 'vant';Vue.use(Col);Vue.use(Row);Vue.use(VanImage);

在api/index.js下定义推荐歌单的接口方法

// 首页 - 推荐歌单export const recommendMusic = params => request({url: '/personalized',params// 将来外面可能传入params的值 {limit: 20}})

src/views/Home/index.vue,把数据请求回来, 用van-image和p标签展示推荐歌单和歌单名字:

【注意】:width=“100%” height=“3rem” fit="cover"中,height:设计图的高度除以html中box的大小得到rem单位的高度。fit表示填充模式

<template><div><p class="title">推荐歌单</p><van-row gutter="6"><van-col span="8" v-for="obj in reList" :key="obj.id"><van-image width="100%" height="3rem" fit="cover" :src="obj.picUrl" /><p class="song_name">{{obj.name }}</p></van-col></van-row></div></template><script>import {recommendMusicAPI } from "@/api";export default {data() {return {reList: [], // 推荐歌单数据};},async created() {const res = await recommendMusicAPI({limit: 6,});console.log(res);this.reList = res.data.result;},};</script>

4-2. 首页-最新音乐

目标: van-cell单元格使用。

请求地址:/personalized/newsong

1.引入van-cell使用 - 注册组件main.js中

import {Cell, Icon} from 'vant';Vue.use(Cell);Vue.use(Icon);

2.定义接口请求方法 - api/Home.js

// 首页 - 推荐最新音乐export const newMusic = params => request({url: "/personalized/newsong",params})

3.将推荐最新音乐的接口请求方法放到统一导出的地方:api/index.js

import {recommendMusic, newMusic} from './Home'export const newMusicAPI = newMusic

4.src/views/Home/index.vue列表数据铺设 - 插入自定义标签。

<template><div><p class="title">推荐歌单</p><div><van-row gutter="5"><van-col span="8" v-for="obj in recommendList" :key="obj.id"><van-image fit="cover" :src="obj.picUrl" /><p class="song_name">{{obj.name }}</p></van-col></van-row></div><p class="title">最新音乐</p><van-cell center v-for="obj in songList" :key="obj.id" :title="obj.name" :label="obj.song.artists[0].name + ' - ' + obj.name"><template #right-icon><van-icon name="play-circle-o" size="0.6rem"/></template></van-cell></div></template><script>import {recommendMusicAPI, newMusicAPI } from "@/api";export default {data() {return {reList: [], // 推荐歌单数据songList: [], // 最新音乐数据};},async created() {const res = await recommendMusicAPI({limit: 6,});console.log(res);this.reList = res.data.result;const res2 = await newMusicAPI({limit: 20})console.log(res2);this.songList = res2.data.result},}</script><style scoped>/*给单元格设置底部边框*/.van-cell{border-bottom: 1px solid lightgray;}</style>

4-3. 搜索-热搜关键字

目标: 完成热搜关键字铺设。

搜索框 – van-search组件api/Search.js – 热搜关键字 - 接口方法Search/index.vue引入-获取热搜关键字 - 铺设页面点击文字填充到输入框

1.准备搜索界面标签:Vant组件库-Search 搜索。(搜索框形状为圆角round)底下的结构自己搭建

<template><div><van-searchshape="round"placeholder="请输入搜索关键词"/><!-- 搜索下容器 --><div class="search_wrap"><!-- 标题 --><p class="hot_title">热门搜索</p><!-- 热搜关键词容器 --><div class="hot_name_wrap"><!-- 每个搜索关键词 --><span class="hot_item">热搜关键字</span></div></div></div></template><script>export default {}</script><style scoped>............../* 给单元格设置底部边框 */.van-cell {border-bottom: 1px solid lightgray;}</style>

2.api/Search.js - 定义热门搜索接口方法

import request from '@/utils/request'// 热搜关键字export const hotSearch = () => request({url: '/search/hot'})

3.api/index.js - 导入使用并统一导出

// 统一出口// 你也可以在逻辑页面里.vue中直接引入@/api/Home下的网络请求工具方法// 为什么: 我们可以把api所有的方法都统一到一处. import {hotSearch, searchResult} from './Search'export const hotSearchAPI = hotSearch // 搜索页面-热搜关键词export const searchResultAPI = searchResult // 搜索结果

4.created中请求接口-拿到热搜关键词列表

<!-- 每个搜索关键词 --><spanclass="hot_item"v-for="(obj, index) in hotArr":key="index">{{obj.first}}</span><script>// 目标: 铺设热搜关键字// 1. 搜索框van-search组件, 关键词标签和样式// 2. 找接口, api/Search.js里定义获取搜索关键词的请求方法// 3. 引入到当前页面, 调用接口拿到数据循环铺设页面// 4. 点击关键词把值赋予给van-search的v-model变量import {hotSearchAPI } from "@/api";export default {data(){return {hotArr: [], // 热搜关键字}},async created() {const res = await hotSearchAPI();console.log(res);this.hotArr = res.data.result.hots;},}</script>

5.点击热搜关键词填充到输入框(点击关键词把值赋予给van-search的v-model变量

<div><van-searchshape="round"v-model="value"placeholder="请输入搜索关键词"/><!-- 每个搜索关键词 --><spanclass="hot_item"v-for="(obj, index) in hotArr":key="index"@click="fn(obj.first)">{{ obj.first }}</span></div><script>export default {data(){return {value: "",hotArr: [], // 热搜关键字}},// ...省略了createdmethods: {fn(val) {// 点击热搜关键词this.value = val; // 选中的关键词显示到搜索框},}}</script>

总结: 写好标签和样式, 拿到数据循环铺设, 点击关键词填入到van-search中的v-model中。

4-4. 搜索-点击热词-搜索结果获取

目标: 点击热词填充到输入框-出现搜索结果。

api/Search.js - 搜索结果, 接口方法。Search/index.vue引入-获取搜索结果 - 铺设页面。和热搜关键字容器 – 互斥显示。点击文字填充到输入框, 请求搜索结果铺设。

1.api/Search.js 定义搜索结果方法

// 搜索结果列表export const searchResultList = params => request({url: '/cloudsearch',params})

2.这个搜索结果列表的方法放到统一的出口处api/index.js

import {hotSearch,searchResultList} from './Search'export const searchResultListAPI = searchResultList // 搜索-搜索结果

3.搜索结果显示区域标签+样式(直接复制/vant文档找)

<!-- 搜索结果 --><div class="search_wrap"><!-- 标题 --><p class="hot_title">最佳匹配</p><van-cellcentertitle='结果名字'><template #right-icon><van-icon name="play-circle-o" size="0.6rem"/></template></van-cell></div>

4.点击 - 获取搜索结果 - 循环铺设页面(最佳匹配下面都是一行行的单元格-和首页一样的)

<template><div><van-search shape="round" v-model="value" placeholder="请输入搜索关键词" /><!-- 搜索下容器 --><div class="search_wrap">...........</div><!-- 搜索结果 --><div class="search_wrap"><!-- 标题 --><p class="hot_title">最佳匹配</p><van-cellcenterv-for="obj in resultList":key="obj.id":title="obj.name":label="obj.ar[0].name + ' - ' + obj.name"><template #right-icon><van-icon name="play-circle-o" size="0.6rem"/></template></van-cell></div></div></template><script>// 目标: 铺设热搜关键字// 1. 搜索框van-search组件, 关键词标签和样式// 2. 找接口, api/Search.js里定义获取搜索关键词的请求方法// 3. 引入到当前页面, 调用接口拿到数据循环铺设页面// 4. 点击关键词把值赋予给van-search的v-model变量// 目标: 铺设搜索结果// 1. 找到搜索结果的接口 - api/Search.js定义请求方法// 2. 再定义methods里getListFn方法(获取数据)// 3. 在点击事件方法里调用getListFn方法拿到搜索结果数据// 4. 铺设页面(首页van-cell标签复制过来)// 5. 把数据保存到data后, 循环van-cell使用即可(切换歌手字段)// 6. 互斥显示搜索结果和热搜关键词import {hotSearchAPI, searchResultListAPI } from "@/api";export default {data() {return {value: "", // 热搜关键词hotArr: [], // 热搜关键字resultList: [], // 搜索结果};},async created() {const res = await hotSearchAPI();console.log(res);this.hotArr = res.data.result.hots;},methods: {async getListFn() {return await searchResultListAPI({keywords: this.value,limit: 20}) // 把搜索结果return出去// (难点):// async修饰的函数 -> 默认返回一个全新Promise对象// 这个Promise对象的结果就是async函数内return的值// 意思就是:拿到getListFn的返回值用await提取结果},async fn(val) {// 点击热搜关键词this.value = val; // 选中的关键词显示到搜索框const res = await this.getListFn();console.log(res);this.resultList = res.data.result.songs;},},};</script><style scoped>................/*给单元格设置底部边框*/.van-cell {border-bottom: 1px solid lightgray;}</style>

5.互斥显示, 热搜关键词和搜索结果列表

总结: 点击热词后, 调用接口传入关键词, 返回数据铺设。

4-5. 输入框-搜索结果

目标: 监测输入框改变-拿到搜索结果

观察van-search组件是否支持和实现input事件绑定@input事件和方法在事件处理方法中获取对应的值使用、如果搜索不存在的数据-要注意接口返回字段不同

1.绑定@input事件在van-search上

<van-search shape="round" v-model="value" placeholder="请输入搜索关键词" @input="inputFn"/>

2.实现输入框改变 - 获取搜索结果铺设

async inputFn() {// 输入框值改变if (this.value.length === 0) {// 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)this.resultList = [];return;}const res = await this.getListFn();console.log(res);// 如果搜索结果响应数据没有songs字段-无数据if (res.data.result.songs === undefined) {this.resultList = [];return;}this.resultList = res.data.result.songs;},

总结: 监测输入框改变-保存新的关键词去请求结果回来铺设。input事件和change事件的区别?input:只要内容改变实时触发;change:失去焦点时内容改变才触发。

4-6. 搜索结果-加载更多

目标: 触底后, 加载下一页数据。

观察接口文档: 发现需要传入offset和分页公式van-list组件监测触底执行onload事件 配合后台接口, 传递下一页的标识拿到下一页数据后追加到当前数组末尾即可

1.设置van-list组件实现相应的属性和方法, 让page++去请求下页数据

<van-listv-model="loading":finished="finished"finished-text="没有更多了"@load="onLoad"><van-cellcenterv-for="obj in resultList":key="obj.id":title="obj.name":label="obj.ar[0].name + ' - ' + obj.name"><template #right-icon><van-icon name="play-circle-o" size="0.6rem" /></template></van-cell></van-list><script>// 目标: 加载更多// 1. 集成list组件-定义相关的变量(搞懂变量的作用) -监测触底事件// 2. 一旦触底, 自动执行onload方法// 3. 对page++, 给后台传递offset偏移量参数-请求下一页的数据// 4. 把当前数据和下一页新来的数据拼接起来用在当前 页面的数组里// (切记) - 加载更多数据后,一定要把loading改成false, 保证下一次还能触发onload方法export default {data() {return {value: "",hotArr: [], // 热搜关键字resultList: [], // 搜索结果loading: false, // 加载中 (状态) - 只有为false, 才能触底后自动触发onload方法finished: false, // 未加载全部 (如果设置为true, 底部就不会再次执行onload, 代表全部加载完成)page: 1, // 当前搜索结果的页码};},// ...省略其他methods: {async getListFn() {return await searchResultListAPI({keywords: this.value,limit: 20,offset: (this.page - 1) * 20, // 固定公式}); // 把搜索结果return出去// (难点):// async修饰的函数 -> 默认返回一个全新Promise对象// 这个Promise对象的结果就是async函数内return的值// 拿到getListFn的返回值用await提取结果},async onLoad() {// 触底事件(要加载下一页的数据咯), 内部会自动把loading改为truethis.page++;const res = await this.getListFn();this.resultList = [...this.resultList, ...res.data.result.songs];this.loading = false; // 数据加载完毕-保证下一次还能触发onload},},}</script>

总结: list组件负责UI层监测触底, 执行onload函数, page++, 请求下页数据, 和现在数据合并显示更多, 设置loading为false, 确保下次触底还能执行onLoad

4-7. 加载更多-bug修复

目标: 如果只有一页数据/无数据判断

无数据/只有一页数据, finished为true防止list组件触底再加载更多还要测试-按钮点击/输入框有数据情况的加载更多

正确代码

async fn(val) {// 点击热搜关键词this.finished = false; // 点击新关键词-可能有新的数据this.value = val; // 选中的关键词显示到搜索框const res = await this.getListFn();console.log(res);this.resultList = res.data.result.songs;this.loading = false; // 本次数据加载完毕-才能让list加载更多},async inputFn() {this.finished = false // 输入框关键字改变-可能有新数据(不一定加载完成了)// 输入框值改变if (this.value.length === 0) {// 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)this.resultList = [];return;}const res = await this.getListFn();console.log(res);// 如果搜索结果响应数据没有songs字段-无数据if (res.data.result.songs === undefined) {this.resultList = [];return;}this.resultList = res.data.result.songs;this.loading = false;},async onLoad() {// 触底事件(要加载下一页的数据咯), 内部会自动把loading改为truethis.page++;const res = await this.getListFn();if (res.data.result.songs === undefined) {// 没有更多数据了this.finished = true; // 全部加载完成(list不会在触发onload方法)this.loading = false; // 本次加载完成return;}this.resultList = [...this.resultList, ...res.data.result.songs];this.loading = false; // 数据加载完毕-保证下一次还能触发onload},

总结: 在3个函数 上和下, 设置finished还未完成, 最后要把loading改成false, 判断songs字段, 对这里的值要非常熟悉才可以。

4-8. 输入框-防抖

目标: 输入框触发频率过高

输入框输入"asdfghjkl"

​ 接着快速的删除

​ 每次改变-马上发送网络请求

​ 网络请求异步耗时 – 数据回来后还是铺设到页面上

解决:

​ 引入防抖功能

async inputFn() {// 目标: 输入框改变-逻辑代码-慢点执行// 解决: 防抖// 概念: 计时n秒, 最后执行一次, 如果再次触发, 重新计时// 效果: 用户在n秒内不触发这个事件了, 才会开始执行逻辑代码if (this.timer) clearTimeout(this.timer);this.timer = setTimeout(async () => {this.finished = false; // 输入框关键字改变-可能有新数据(不一定加载完成了)// 输入框值改变if (this.value.length === 0) {// 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)this.resultList = [];return;}const res = await this.getListFn();console.log(res);// 如果搜索结果响应数据没有songs字段-无数据if (res.data.result.songs === undefined) {this.resultList = [];return;}this.resultList = res.data.result.songs;this.loading = false;}, 900);},

总结: 降低函数执行频率

4-9. 页码bug修复

目标: 第一个关键词page已经+到了10, 再第二个关键词应该从1开始

加载更多时, page已经往后计数了

重新获取时, page不是从第一页获取的

点击搜索/输入框搜索时, 把page改回1

代码如下:

async fn(val) {// 点击热搜关键词+this.page = 1; // 点击重新获取第一页数据this.finished = false; // 点击新关键词-可能有新的数据this.value = val; // 选中的关键词显示到搜索框const res = await this.getListFn();console.log(res);this.resultList = res.data.result.songs;this.loading = false; // 本次数据加载完毕-才能让list加载更多},async inputFn() {// 目标: 输入框改变-逻辑代码-慢点执行// 解决: 防抖// 概念: 计时n秒, 最后执行一次, 如果再次触发, 重新计时// 效果: 用户在n秒内不触发这个事件了, 才会开始执行逻辑代码if (this.timer) clearTimeout(this.timer);this.timer = setTimeout(async () => {+ this.page = 1; // 点击重新获取第一页数据this.finished = false; // 输入框关键字改变-可能有新数据(不一定加载完成了)// 输入框值改变if (this.value.length === 0) {// 搜索关键词如果没有, 就把搜索结果清空阻止网络请求发送(提前return)this.resultList = [];return;}const res = await this.getListFn();console.log(res);// 如果搜索结果响应数据没有songs字段-无数据if (res.data.result.songs === undefined) {this.resultList = [];return;}this.resultList = res.data.result.songs;this.loading = false;}, 900);},

总结: 切换时, 让page页面回到1

4-10. Layout边距优化

目标: 上下导航会盖住中间内容

我们的头部导航和底部导航挡住了中间内容

给中间路由页面设置上下内边距即可

在Layout/index.vue中

/* 中间内容区域 - 容器样式(留好上下导航所占位置) */.main {padding-top: 46px;padding-bottom: 50px;}

4-11. SongItem封装

目标: 把首页和搜索结果的歌曲cell封装起来

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vTU25MUt-1658606506177)(images/image-0512144538038.png)]

创建src/components/SongItem.vue

<template><van-cell center :title="name" :label="author + ' - ' + name"><template #right-icon><van-icon name="play-circle-o" size="0.6rem"/></template></van-cell></template><script>export default {props: {name: String, // 歌名author: String, // 歌手id: Number, // 歌曲id (标记这首歌曲-为将来跳转播放页做准备)}};</script><style scoped>/* 给单元格设置底部边框 */.van-cell {border-bottom: 1px solid lightgray;}</style>

Home/index.vue - 重构

注意: author字段不同

<SongItem v-for="obj in songList":key="obj.id":name="obj.name":author="obj.song.artists[0].name":id="obj.id"></SongItem>

Search/index.vue - 重构

注意: author字段不同

<SongItemv-for="obj in resultList":key="obj.id":name="obj.name":author="obj.ar[0].name":id="obj.id"></SongItem>

总结: 遇到重复标签要封装

4-12. 播放音乐

目标: 从预习资料拿到播放的api和页面, 配置好路由规则

时间关系,这个页面不用写, 直接用, 注释在备课代码里写好了

组件SongItem里 – 点击事件

api/Play.js – 提前准备好 – 接口方法

跳转到Play页面 – 把歌曲id带过进去

在SongItem.vue - 点击播放字体图标

methods: {playFn(){this.$router.push({path: '/play',query: {id: this.id // 歌曲id, 通过路由跳转传递过去}})}}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bJmxj10T-1658606506178)(images/image-0512144906051.png)]

总结: 准备好播放页, 点击播放传歌曲id过去, 到播放页-再请求响应数据和歌曲地址用audio标签播放

4-13. vant适配

目标: 切换不同机型,刷新后看看标签大小适配吗

postcss – 配合webpack翻译css代码postcss-pxtorem – 配合webpack, 自动把px转成rem新建postcss.config.js – 设置相关配置重启服务器, 再次观察Vant组件是否适配

下载postcss和postcss-pxtorem@5.1.1

postcss作用: 是对css代码做降级处理

postcss-pxtorem: 自动把所有代码里的css样式的px, 自动转rem

src/新建postcss.config.js

module.exports = {plugins: {'postcss-pxtorem': {// 能够把所有元素的px单位转成Rem// rootValue: 转换px的基准值。// 例如一个元素宽是75px,则换成rem之后就是2rem。rootValue: 37.5,propList: ['*']}}}

以iphone6为基准, 37.5px为基准值换算rem

今日总结

掌握vant组件库的使用 - 找组件, 引组件, 用组件能够对vant组件自带样式进行覆盖自定义遇到重复的标签, 自己也封装了一个复用的组件掌握查询文档和使用每个属性的方式

今日作业

把网易云音乐案例-从0再来一遍, 为下阶段移动端项目铺垫

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。