- 1 . 能够掌握Vant组件库使用
- 2 . 熟练查阅Vant组件库文档
- 3 . 能够完成网易云音乐案例
案例-网易云音乐
1.本地接口
目标:下载网易云音乐node接口项目, 在本地启动, 为我们vue项目提供数据支持
2.本地接口启动
目标:启动本地node服务_拿到数据
- 文档: https://binaryify.github.io/NeteaseCloudMusicApi/#/
- 下载项目并启动 – 测试
3.前端项目初始化
目标:初始化项目, 下载必备包, 引入初始文件, 配置按需自动引入Vant
- 初始化工程 (vue create music-demo)
- 在此地方我使用的是vue3模式,当项目运行时会有很多不一样的配置错误,所以这边建议使用vue2模式会更方便一点
- 下载所需第三方包 axios vant vue-router
- 下载Vant自动按需引入插件 babel-plugin-import
- 创建完文件夹后需要配置vue项目,在项目中创建vue.config.js文件
-
module.exports = { lintOnSave: false }
- 在babel.config.js配置 – 看Vant文档
-
module.exports = { presets: [ '@vue/cli-plugin-babel/preset' ], plugins:[ ['import',{ libraryName:'vant', libraryDirectory:'es', style:true },'vant'] ] }
- 引入提前准备好的reset.css, flexible.js 到 main.js使用
- mobile/flexible.js
-
// 首先是一个立即执行函数,执行时传入的参数是window和document (function flexible (window, document) { var docEl = document.documentElement // 返回文档的root元素 var dpr = window.devicePixelRatio || 1 // 获取设备的dpr,即当前设置下物理像素与虚拟像素的比值 // 调整body标签的fontSize,fontSize = (12 * dpr) + 'px' // 设置默认字体大小,默认的字体大小继承自body function setBodyFontSize () { if (document.body) { document.body.style.fontSize = (12 * dpr) + 'px' } else { document.addEventListener('DOMContentLoaded', setBodyFontSize) } } setBodyFontSize(); // set 1rem = viewWidth / 10 // 设置root元素的fontSize = 其clientWidth / 10 + ‘px’ function setRemUnit () { var rem = docEl.clientWidth / 10 docEl.style.fontSize = rem + 'px' } // 移动端的适配如何做 // (1): 所有的css单位, rem (vscode可以自动把px转成rem, pxtorem插件设置基准值37.5) - 1rem等于37.5px // 原理: rem要根据html的font-size换算 // 目标: 网页宽度变小, html的font-size也要变小, ...网页变大, html的font-size变大. // (2): flexible.js (专门负责当网页宽度改变, 会修改html的font-size) setRemUnit() // 当我们页面尺寸大小发生变化的时候,要重新设置下rem 的大小 window.addEventListener('resize', setRemUnit) // pageshow 是我们重新加载页面触发的事件 window.addEventListener('pageshow', function(e) { // e.persisted 返回的是true 就是说如果这个页面是从缓存取过来的页面,也需要从新计算一下rem 的大小 if (e.persisted) { setRemUnit() } }) // 检测0.5px的支持,支持则root元素的class中有hairlines if (dpr >= 2) { var fakeBody = document.createElement('body') var testElement = document.createElement('div') testElement.style.border = '.5px solid transparent' fakeBody.appendChild(testElement) docEl.appendChild(fakeBody) if (testElement.offsetHeight === 1) { docEl.classList.add('hairlines') } docEl.removeChild(fakeBody) } }(window, document))
style/reset.css
-
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, form, fieldset, input, textarea, p, blockquote, th, td { padding: 0; margin: 0; } table { border-collapse: collapse; border-spacing: 0; } fieldset, img { border: 0; } address, caption, cite, code, dfn, em, strong, th, var { font-weight: normal; font-style: normal; } ol, ul { list-style: none; } caption, th { text-align: left; } h1, h2, h3, h4, h5, h6 { font-weight: normal; font-size: 100%; } q:before, q:after { content: ''; } abbr, acronym { border: 0; }
main.js
import { createApp } from 'vue'
import App from './App.vue'
import '@/mobile/flexible' //适配
import "@/styles/reset.css" //初始化样式
import router from '@/router' //路由对象
import { Tabbar, TabbarItem } from 'vant';
const app = createApp(App)
// 安装 Vant 插件
app.use(Tabbar)
app.use(TabbarItem)
// 使用路由
app.use(router)
// 挂载实例
app.mount('#app')
4.需求分析
根据需求, 创建4个页面组件
- views/Layout/index.vue
-
<template> <div> <p>我是Layout</p> </div> </template> <script> export default { data(){ return { } }, methods: { } } </script> <style scoped> /* 中间内容区域 - 容器样式(留好上下导航所占位置) */ .main { padding-top: 46px; padding-bottom: 50px; } </style>
- views/Home/index.vue
-
<template> <div> <p>我是首页</p> </div> </template> <script> export default { data(){ return { } }, methods: { } } </script> <style scoped> /* 标题 */ .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; /** 隐藏超出的内容 **/ } </style>
- views/Search/index.vue
-
<template> <div> <p>我是搜索界面</p> </div> </template> <script> export default { data(){ return { } }, methods: { } } </script> <style scoped> /* 搜索容器的样式 */ .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; } </style>
- views/Play/index.vue
-
<template> <div class="play"> <!-- 模糊背景(靠样式设置), 固定定位 --> <div class="song-bg" :style="`background-image: url(${ songInfo && songInfo.al && songInfo.al.picUrl }?imageView&thumbnail=360y360&quality=75&tostatic=0);`" ></div> <!-- 播放页头部导航 --> <div class="header"> <van-icon name="arrow-left" size="20" class="left-incon" @click="$router.back()" /> </div> <!-- 留声机 - 容器 --> <div class="song-wrapper"> <!-- 留声机本身(靠css动画做旋转) --> <div class="song-turn ani" :style="`animation-play-state:${playState ? 'running' : 'paused'}`" > <div class="song-img"> <!-- &&写法是为了防止报错, 有字段再继续往下访问属性 --> <img style="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 --> <audio ref="audio" preload="true" :src="`https://music.163.com/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, // 上一页传过来的音乐id songInfo: {}, // 歌曲信息 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.lyric this.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, 对应歌词作为value lyricObj[ms + Number(ss)] = contentArr[index] }) // 返回得到的歌词对象(可以打印看看) console.log(lyricObj); return lyricObj }, audioStart() { // 播放按钮 - 点击事件 if (!this.playState) { // 如果状态为false this.$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>
- 里面还需要一些配置文件
- api/Home.js
-
// 文件名-尽量和模块页面文件名统一(方便查找) import request from '@/utils/request' // 首页 - 推荐歌单 export const recommendMusic = params => request({ url: '/personalized', params // 将来外面可能传入params的值 {limit: 20} }) // 首页 - 推荐最新音乐 export const newMusic = params => request({ url: "/personalized/newsong", params })
/api/index.js
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {recommendMusic, newMusic} from './Home'
import {hotSearch, searchResultList} from './Search'
import {getSongById, getLyricById} from './Play'
export const recommendMusicAPI = recommendMusic // 请求推荐歌单的方法导出
export const newMusicAPI = newMusic // 首页 - 最新音乐
export const hotSearchAPI = hotSearch // 搜索 - 热搜关键词
export const searchResultListAPI = searchResultList // 搜索 = 搜索结果
export const getSongByIdAPI = getSongById // 歌曲 - 播放地址
export const getLyricByIdAPI = getLyricById // 歌曲 - 歌词数据
/api/play.js
import request from '../utils/request'
// 播放页 - 获取歌曲详情
export const getSongById = (id) => request({
url: `/song/detail?ids=${id}`,
method: "GET"
})
// 播放页 - 获取歌词
export const getLyricById = (id) => request({
url: `/lyric?id=${id}`,
method: "GET"
})
/api/Search.js
// 搜索模块
import request from '@/utils/request'
// 热搜关键字
export const hotSearch = params => request({
url: '/search/hot',
params
})
// 搜索结果
export const searchResultList = params => request({
url: '/cloudsearch',
params
})
utils/request.js
// 网络请求 - 二次封装
import axios from 'axios'
axios.defaults.baseURL = "http://localhost:3000"
export default axios
创建四个界面
5.路由准备
目标:准备路由配置, 显示不同路由页面
- router/index.js – 配置路由规则和对应路由页面
-
import { createWebHistory, createRouter } from 'vue-router' import Layout from '@/views/Layout' import Home from '@/views/Home' import Search from '@/views/Search' import Play from '@/views/Play' const routes = [ { path: '/', redirect: '/layout' }, { path: '/layout', component: Layout, redirect: '/layout/home', children: [ { path: 'home', component: Home, meta: { title: "首页" } }, { path: 'search', component: Search, meta: { title: "搜索" } } ] }, { path: '/play', component: Play } ] const router = createRouter({ history: createWebHistory(), routes }) export default router
- main.js – 引入路由对象注入到vue中
-
import { createApp } from 'vue' import App from './App.vue' import '@/mobile/flexible' //适配 import "@/styles/reset.css" //初始化样式 import router from '@/router' //路由对象 import { Tabbar, TabbarItem } from 'vant'; const app = createApp(App) // 安装 Vant 插件 app.use(Tabbar) app.use(TabbarItem) // 使用路由 app.use(router) // 挂载实例 app.mount('#app')
- App.vue – 留好router-view显示路由页面
-
<template> <div> <!-- 一级路由-显示地方 --> <router-view></router-view> </div> </template> <script> export default { } </script> <style></style>
6.Tabbar组件
目标:点击底部导航, 切换路由页面显示
- Tabbar组件文档: https://vant-contrib.gitee.io/vant/#/zh-CN/tabbar
- 使用: 根据图示, 把Tabbar组件在Layout.vue底部显示出来
- 功能: 点击以后, 要配合路由切换页面功能
注册tabbar组件,在main.js中
import { createApp } from 'vue'
import App from './App.vue'
import '@/mobile/flexible' //适配
import "@/styles/reset.css" //初始化样式
import router from '@/router' //路由对象
import { Tabbar, TabbarItem } from 'vant';
const app = createApp(App)
// 安装 Vant 插件
app.use(Tabbar)
app.use(TabbarItem)
// 使用路由
app.use(router)
// 挂载实例
app.mount('#app')
在/views/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" to="/layout/home">首页</van-tabbar-item>
<van-tabbar-item icon="search" to="/layout/search">搜索</van-tabbar-item>
</van-tabbar>
7.NavBar导航组件
- NavBar文档: https://vant-contrib.gitee.io/vant/#/zh-CN/nav-bar
- 使用: 把NavBar注册引入使用, 静态效果
main.js - 注册NavBar组件
import { createApp } from 'vue'
import App from './App.vue'
import '@/mobile/flexible' //适配
import "@/styles/reset.css" //初始化样式
import router from '@/router' //路由对象
import { Tabbar, TabbarItem,NavBar} from 'vant';
const app = createApp(App)
// 安装 Vant 插件
app.use(Tabbar)
app.use(TabbarItem)
app.use(NavBar)
// 使用路由
app.use(router)
// 挂载实例
app.mount('#app')
在Layout文件中使用插件NavBar
<van-nav-bar :title="activeTitle" fixed />
<script>
export default {
activeTitle: "首页"
}
</script>
8.NavBar标题切换
目标:顶部标题切换显示
- 网页打开默认显示
- 侦听路由切换显示对应标题
在router/index.js - 给$route里需要导航标题的添加meta元信息属性
import { createWebHistory, createRouter } from 'vue-router'
import Layout from '@/views/Layout'
import Home from '@/views/Home'
import Search from '@/views/Search'
import Play from '@/views/Play'
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 = createRouter({
history: createWebHistory(),
routes
})
export default router
Layout.vue中监听$route改变:给导航active的值设置$route里的元信息的标题
<template>
<div>
<van-nav-bar :title="activeTitle" fixed />
<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 {
data() {
return {
activeTitle: this.$route.meta.title, // "默认"顶部导航要显示的标题 (默认获取当前路由对象里的meta中title值)
};
},
// 路由切换 - 侦听$route对象改变
watch: {
$route() {
this.activeTitle = this.$route.meta.title; // 提取切换后路由信息对象里的title显示
},
},
};
</script>
<style scoped>
/* 中间内容区域 - 容器样式(留好上下导航所占位置) */
.main {
padding-top: 46px;
padding-bottom: 50px;
}
</style>
9.网络请求封装
目标:网络请求, 不散落在各个逻辑页面里, 封装起来方便以后修改
- utils/request.js – 对axios进行二次封装, 并且制定项目的根地址
-
// 网络请求 - 二次封装 import axios from 'axios' axios.defaults.baseURL = "http://localhost:3000" export default axios
- api/Home.js – 统一管理所有需要的url地址, 封装网络请求的方法并导出
// 文件名-尽量和模块页面文件名统一(方便查找)
import request from '@/utils/request'
// 首页 - 推荐歌单
export const recommendMusic = params => request({
url: '/personalized',
params
// 将来外面可能传入params的值 {limit: 20}
})
// 首页 - 推荐最新音乐
export const newMusic = params => request({
url: "/personalized/newsong",
params
})
- api/index.js – 统一导出接口
-
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出 import {recommendMusic} from './Home' export const recommendMusicAPI = recommendMusic // 请求推荐歌单的方法导出
- 在main.js – 引入API方法请求测试
-
import { createApp } from 'vue' import App from './App.vue' import '@/mobile/flexible' //适配 import "@/styles/reset.css" //初始化样式 import router from '@/router' //路由对象 import { Tabbar, TabbarItem, NavBar } from 'vant'; import { recommendMusicAPI } from '@/api/index'; async function myFn() { try { const res = await recommendMusicAPI({ limit: 6 }); console.log(res); } catch (error) { console.error('网络请求发生错误:', error); } } myFn(); const app = createApp(App) // 安装 Vant 插件 app.use(Tabbar) app.use(TabbarItem) app.use(NavBar) // 使用路由 app.use(router) // 挂载实例 app.mount('#app')
10.首页-推荐歌单
- 布局van-row和van-col
- van-image显示图片, p标签显示歌名
- 引入api里的网络请求方法, 把数据请求回来, 循环铺设
把数据请求回来, 用van-image和p标签展示推荐歌单和歌单名字 /Home/Home.js
<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>
// 目标:铺设推荐歌单
// 1.van-row和van-col来搭建外框布局
// 2.van-col里内容(van-image和p)
// 3.调整间距和属性值
// 4.调用封装得api/index.js-推荐歌单api方法
// 5.拿到数据保存到data里变量-去上面循环标签
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>
<style scoped>
/* 标题 */
.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; /** 隐藏超出的内容 **/
}
</style>
-
在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} })
11.首页-最新音乐
目标:完成最新音乐单元格列铺设
- 引入注册使用van-cell, 并且设置一套标签和样式准备
- 在api/Home.js –最新音乐的接口方法
- 引入到Home/index.vue中, 数据铺设到页面
列表数据铺设 - 插入自定义标签/Home/home.js
<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>
-
引入van-cell使用 - 注册组件main.js中
import {Cell} from 'vant'; Vue.use(Cell);
-
定义接口请求方法 - api/index.js
// 首页 - 推荐最新音乐
export const newMusic = params => request({
url: "/personalized/newsong",
params
})
12.热搜关键字
目标:完成搜索框和热搜关键字显示
- 搜索框 – van-search组件
- api/Search.js – 热搜关键字 - 接口方法
- Search/index.vue引入-获取热搜关键字 - 铺设页面
- 点击文字填充到输入框
点击 - 获取搜索结果 - 循环铺设页面views/Search/index.vue
<template>
<div>
<van-search shape="round" v-model="value" placeholder="请输入搜索关键词" />
<!-- 搜索下容器 -->
<div class="search_wrap">
<!-- 标题 -->
<p class="hot_title">热门搜索</p>
<!-- 热搜关键词容器 -->
<div class="hot_name_wrap">
<!-- 每个搜索关键词 -->
<span class="hot_item" v-for="(obj, index) in hotArr" :key="index" @click="fn(obj.first)">{{ obj.first }}</span>
</div>
</div>
<!-- 搜索结果 -->
<div class="search_wrap">
<!-- 标题 -->
<p class="hot_title">最佳匹配</p>
<van-cell center v-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>
/* 搜索容器的样式 */
.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;
}
</style>
在api文件夹下创建Search.js搜索模块得地址
// 搜索模块
import request from '@/utils/request'
// 热搜关键字
export const hotSearch = params => request({
url: '/search/hot',
params
})
在api/index.js文件中进行导入
// api文件夹下 各个请求模块js, 都统一来到index.js再向外导出
import {recommendMusic, newMusic} from './Home'
import {hotSearch, searchResultList} from './Search'
import {getSongById, getLyricById} from './Play'
export const recommendMusicAPI = recommendMusic // 请求推荐歌单的方法导出
export const newMusicAPI = newMusic // 首页 - 最新音乐
export const hotSearchAPI = hotSearch // 搜索 - 热搜关键词