网易云音乐案例

网易云音乐案例

一、本地接口项目部署

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

项目地址:https://binaryify.github.io/NeteaseCloudMusicApi/#/?id=%e5%ae%89%e8%a3%85

**备用地址:**https://github.com/Binaryify/NeteaseCloudMusicApi/tree/master/docs

下载后,安装所有依赖,在本地启动起来,测试访问此地址是否有数据

http://locallhost:3000,看到如下页面就成功了

总结:前端请求本地的node项目,node服务器伪装请求去拿网易云音乐服务器数据转发会给自己前端

二、学习目标

  1. 能够掌握vant组件库的使用
  2. 能够掌握vant组件自定义样式能力
  3. 能够掌握组件库使用和文档使用能力
  4. 能够完成网易云音乐案例

三、案例-网易云音乐

1. 本地接口

目的:请求网易云音乐服务器API接口 - 获取数据

在这里插入图片描述

总结:反向代理就是用本地开启cors的服务器去转发请求拿到数据

2. 本地接口启动

目的:启动本地网易云音乐API服务

3. 前端项目初始化

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

  1. 初始化工程

    vue create music-demo
    
  2. 下载需要的所有第三方依赖包

    yarn add axios vant@latest -v2 vue-router -v2
    
  3. 引入准备好的reset.css和flexible.js - 实现样式初始化和适配问题 - 引入到main.js

  4. 本次vant使用自动按需引入的方式

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

    yarn add babel-plugin-import -D
    

    在babel.config.js - 添加插件配置

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

4. 需求分析

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

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

在这里插入图片描述

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

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

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

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">
    <!-- 模糊背景(靠样式设置), 固定定位 -->
    <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>

5. 路由准备

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

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

// 路由-相关模块
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({
    router,
    render: h => h(App)
}).$mount('#app')

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

<template>
  <div>
    <router-view></router-view>
  </div>
</template>

总结:把项目的框架搭好,逐个攻破

6. Tabbar组件

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

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

  1. 注册Tabbar组件,在main.js中

    import{ Tabbar, TabbarItem } from 'vant'
    Vue.use(Tabbar)
    Vue.use(TabbarItem)
    
  2. 在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>
    
  3. 开启路由模式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>
    

总结:van-tabbar开启route

7 . NavBar导航组件

目标:实现顶部标题展示

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

  1. main.js - 注册NavBar组件

    import { NavBar } from 'vant'
    Vue.use(NavBar)
    
  2. 复制文档里的,然后删除只留标题

    <van-nav-bar :title="activeTitle" fixed />
    
    <script>
        export default {
            activeTitle: "首页"
        }
    </script>
    

8.NavBar标题切换

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

  • 在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.vue中监听$route改变:

    给导航active的值设置$route里的元信息的标题

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

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

9. 网络请求封装

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

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

    // 网络请求 - 二次封装
    import axios from 'axios'
    axios.default.baseURL = 'http://localhost:3000'
    export default axios
    
  2. 封装src/api/Home.js统一封装网络请求方法

    // 文件名- 尽力和模块页面文件名统一(方便查找)
    import request from '@/utils/request'
    
    // 首页 - 推荐歌单
    export const recommendMusic = params => 
    request({
        url: '/personalized',
        params
        // 将来外面可能传入params的值 {limit: 20}
    })
    
  3. 在src/api/index.js - 统一导出接口供外部使用

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

    import {recommendMusicAPI} from '@/api/index
    async function myFn() {
        const res = await recommendMusicAPI({
            limit: 6
        })
        console.log(res)
    }
    myFn()
    

总结:封装网络请求方法目的,方便外面统一管理

10. 首页- 推荐歌单

  • 接口地址:/personalized

    1. 布局采用van-row和van-col

    2. 布局文档https://vant-contrib.gitee.io/vant/#/zh-CN/col

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

    4. 在main.js注册使用的组件

      import {Col, Row, Image as VanImage } from 'vant'
      
      Vue.use(Col)
      Vue.use(Row)
      Vue.use(VanImage)
      
    5. 在api/index.js下定义推荐歌单的接口方法

      // 首页 - 推荐歌单
      export const recommondMusic = params =>
      request({
          url: '/personalized',
          params
           // 将来外面可能传入params的值 {limit: 20}
      })
      
    6. 把数据请求回来,用van-image和p标签展示推荐歌单和歌单名字

      <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>
      

11. 首页 - 最新音乐

目标:van-cell单元格使用

  • 请求地址:/personalized/hewsong

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

      import {Cell} from 'vant'
      Vue.use(Cell)
      
    2. 定义接口请求方法 - api/index.js

      // 首页 - 推荐最新音乐
      export const newMusic = params => 
      request({
          url:'/personalized/newsong'
          params
      })
      
    3. 列表数据铺设 - 插入自定义标签

      <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>
      

12. 搜索 - 热搜关键字

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

搜索框 - van-search组件

api/Search.js - 热搜关键字 - 接口方法

Search/index.vue引入 - 获取热搜关键字 - 铺设页面

点击文字填充到输入框

  1. 准备搜索界面标签

    <template>
      <div>
        <van-search
          shape="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>
    /* 搜索容器的样式 */
    .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;
    }
    
    /* 给单元格设置底部边框 */
    .van-cell {
      border-bottom: 1px solid lightgray;
    }
    </style>
    
  2. api/Search.js - 定义热门搜索接口方法和搜索结果方法

    import request from '@/utils/request'
    
    // 热搜关键字
    export const hotSearch = () => request({
        url: '/search/hot'
    })
    
    // 搜索结果列表
    export const searchResult = params => request({
        url: '/cloudsearch',
        params
    })
    
  3. api/index.js - 导入使用并统一导出

    // 统一出口
    // 你也可以在逻辑页面里.vue中直接引入@/api/Home下的网络请求工具方法
    // 为什么: 我们可以把api所有的方法都统一到一处. 
    
    import {recommendMusic, hotMusic} from '@/api/Home'
    import {hotSearch, searchResult} from '@/api/Search'
    
    
    export const recommendMusicAPI = recommendMusic // 把网络请求方法拿过来 导出
    export const hotMusicAPI = hotMusic // 把获取最新音乐的, 网络请求方法导出
    
    export const hotSearchAPI = hotSearch // 热搜
    export const searchResultAPI = searchResult // 搜索结果
    
  4. created中请求接口 - 拿到热搜关键词列表

    
    <!-- 每个搜索关键词 -->
    <span
          class="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
                shape="round"
                v-model="value"
                placeholder="请输入搜索关键词"
                />
    <!-- 每个搜索关键词 -->
    <span
          class="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: [], // 热搜关键字
                }
            },
            // ...省略了created
            methods: {
                async fn(val) {
                    // 点击热搜关键词
                    this.value = val; // 选中的关键词显示到搜索框
                },
            }
        }
    </script>
    

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

13. 搜索 - 点击热词 - 搜索结果

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

api/Search.js - 搜索结果,接口方法

Search/index.vue引入 - 获取搜索结果 - 铺设页面

和热搜关键字容器 - 互斥显示

点击文字填充到输入框,请求搜索结果铺设

  1. 搜索结果显示区域标签+样式

    <!-- 搜索结果 -->
        <div class="search_wrap">
          <!-- 标题 -->
          <p class="hot_title">最佳匹配</p>
          <van-cell
            center
            title='结果名字'
          >
            <template #right-icon>
              <van-icon name="play-circle-o" size="0.6rem"/>
            </template>
          </van-cell>
        </div>
    
  2. 点击 - 获取搜索结果 - 循环铺设页面

    <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>
    
  3. 互斥显示,热搜关键词和搜索结果列表

在这里插入图片描述

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

14. 输入框 - 搜索结果

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

观察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;
    },
    

总结:检测输入框改变 - 保存新的关键词去请求结果回来铺设

15. 搜索结果 - 加载更多

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

观察接口文档,发现需要传入offset和分页公式

van-list组件检测触底执行onload事件

配合后台接口,传递下一页的标识

拿到下一页数据后追加到当前数组末尾即可

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

    
          <van-list
            v-model="loading"
            :finished="finished"
            finished-text="没有更多了"
            @load="onLoad"
          >
            <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>
          </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改为true
          this.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

16. 加载更多 - 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改为true
      this.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字符,对这里的值要非常熟悉才可以

17. 输入框 - 防抖

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

输入框输入’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);
},

总结:降低函数执行频率

18.页码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

19. Layout边距优化

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

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

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

在Layou/index.vue中

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

20. SongItem封装

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

在这里插入图片描述

创建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字段不同

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

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

21. 播放音乐

目标:配置好路由规则

组件SongItem里 - 点击事件

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

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

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

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

在这里插入图片描述

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

22. vant适配

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

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

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

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

  2. src/新建postcss.config.js

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

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字段不同

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

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

21. 播放音乐

目标:配置好路由规则

组件SongItem里 - 点击事件

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

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

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

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

[外链图片转存中…(img-i6fJNZJE-1661393216718)]

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

22. vant适配

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

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

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

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

  2. src/新建postcss.config.js

module.exports = {
  plugins: {
    'postcss-pxtorem': {
      // 能够把所有元素的px单位转成Rem
      // rootValue: 转换px的基准值。
      // 例如一个元素宽是75px,则换成rem之后就是2rem。
      rootValue: 37.5,
      propList: ['*']
    }
  }
}
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值