vue实战----网易云音乐案例

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

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

    import { Col, Row, Image as VanImage } from 'vant';
    ​
    Vue.use(Col);
    Vue.use(Row);
    Vue.use(VanImage);
  2. 在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>
  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
})

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 // 搜索 - 热搜关键词

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/635262.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Golang创建文件夹

方法 package zdpgo_fileimport ("os" )// AddDir 创建文件夹 func AddDir(dir string) error {if !IsExist(dir) {return os.MkdirAll(dir, os.ModePerm)}return nil }测试 package zdpgo_fileimport "testing"func TestAddDir(t *testing.T) {data : […

二叉树(详解)

在了解二叉树之前呢我们先来了解一下树形结构&#xff0c;因为二叉树就是树形结构的一种特殊情况&#xff0c;它有这非常好的性质&#xff0c;是很常用的一种结构。 目录 一.什么是树形结构&#xff1f; 二.树形结构常见的名词 三.树的存储 四.二叉树 1.二叉树的概念 2.…

学习记录16-反电动势

一、反电动势公式 在负载下反电势和端电压的关系式为&#xff1a;&#x1d448;&#x1d43c;&#x1d445;&#x1d43f;*&#xff08;&#x1d451;&#x1d456; / &#x1d451;&#x1d461;&#xff09;&#x1d438; E为线圈电动势、 &#x1d713; 为磁链、f为频率、N…

网络协议——Modbus-TCP

目录 1、简介 2、Modbus-TCP与Modbus-RTU的区别 3、消息格式 4、功能码01H 5、功能码02H 6、功能码03H 7、功能码04H 8、功能码05H 9、功能码06H 10、功能码0FH 11、功能码10H 1、简介 Modbus-TCP&#xff08;Modbus Transmission Control Protocol&#xff09;是一…

基于Django的美团药品数据分析与可视化系统,有多用户功能,可增删改查数据

背景 随着电子商务和健康产业的迅速发展&#xff0c;药品行业数据的分析和可视化变得愈发重要。基于Django的美团药品数据分析与可视化系统的研究背景凸显了对药品数据的深入挖掘和分析的需求。该系统不仅具备多用户功能&#xff0c;允许不同角色的用户进行数据管理和分析&…

2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版

简介&#xff1a; 2024最新流媒体在线音乐系统网站源码| 音乐社区 | 多语言 | 开心版 下载地址 https://www.kuaiyuanya.com/product/article/index/id/33.html 图片&#xff1a;

【无重复字符的最长子串】python,滑动窗口+哈希表

滑动窗口哈希表 哈希表 seen 统计&#xff1a; 指针 j遍历字符 s&#xff0c;哈希表统计字符 s[j]最后一次出现的索引 。 更新左指针 i &#xff1a; 根据上轮左指针 i 和 seen[s[j]]&#xff0c;每轮更新左边界 i &#xff0c;保证区间 [i1,j] 内无重复字符且最大。 更新结…

高铁VR虚拟全景展示提升企业实力和形象

步入VR的神奇世界&#xff0c;感受前所未有的汽车展示体验。VR虚拟现实技术以其独特的沉浸式模拟&#xff0c;让你仿佛置身于真实展厅之中&#xff0c;尽情探索汽车的每一处细节。 一、定制化展示&#xff0c;随心所欲 VR汽车虚拟展厅打破空间束缚&#xff0c;让汽车制造商能够…

区块链开发:区块链软件开发包装相关解析

区块链开发是指设计、构建和维护基于区块链技术的应用程序或系统的过程。区块链是一种分布式账本技术&#xff0c;它通过去中心化的方式记录和验证数据&#xff0c;确保数据的透明性、不可篡改性和安全性。区块链开发者使用各种编程语言和框架来创建这些应用程序。 在加密货币领…

C++ sort排序的总和应用题

第1题 sort排序1 时限&#xff1a;1s 空间&#xff1a;256m 输入n个数&#xff0c;将这n个数从小到大排序&#xff0c;输出。 输入格式 第1行&#xff0c;一个正整数n&#xff08;n<100&#xff09; 第2行&#xff0c;n个正整数&#xff0c;小于100 输出格式 n个整…

Windows安装mingw32/w64

1.下载 MinGW-w64 WinLibs - GCCMinGW-w64 compiler for Windows Releases niXman/mingw-builds-binaries (github.com) MinGW-w64、UCRT 和 MSVCRT 是 Windows 平台上常用的 C/C 运行库&#xff0c;它们有以下不同点&#xff1a; MinGW-w64&#xff1a;是一个基于 GCC 的…

【Hive SQL 每日一题】分析电商平台的用户行为和订单数据

需求描述 假设你是一位数据分析师&#xff0c;负责分析某电商平台的用户行为和订单数据&#xff0c;平台上有多个用户&#xff0c;用户可以在不同的日期下单&#xff0c;每个订单包含多个商品。请你完成相关业务分析&#xff0c;帮助平台优化运营策略和用户体验。 数据准备 …

NDIS小端口驱动(五)

在需要的时候&#xff0c;我们也许需要NDIS微型端口程序信息&#xff0c;下面会从多个方面来讨论如何查询NDIS微型端口驱动。 查询无连接微型端口驱动程序 若要查询无连接微型端口驱动程序维护的 OID&#xff0c;绑定协议调用 NdisOidRequest 并传递 一个NDIS_OID_REQUEST 结…

【SQL每日一练】查询“OCCUPATIONS”中的“Occupation”列并按Doctor、Professor、Singer、Actor列输出

文章目录 题目一、分析二、题解1.SqlServer2.MySQL3.Oracle 总结 题目 查询“OCCUPATIONS”中的“Occupation”列&#xff0c;使每个姓名按字母顺序排序&#xff0c;并显示在其相应的“职业》下方。输出列标题应分别为Doctor、Professor、Singer和Actor。 注意&#xff1a;当不…

【ChatGPT】 Microsoft Edge 浏览器扩展使用 GPT

【ChatGPT】添加 Microsoft Edge 浏览器插件免费使用 GPT 文章目录 准备工作添加扩展注意事项 使用 ChatGPT 可以更高效的搜索到想要的内容&#xff0c;有效节约在搜索引擎中排查正确信息的时间。 准备工作 准备一台可上网的电脑电脑上安装有 Windows 自带的 Microsoft Edge …

【Makefile】Makefile 编译 Keil 工程(Linux 环境)

本文使用的开发板为 stm32f103C8T6&#xff0c;使用的驱动库为stm32标准库。 目录 一、软件下载 1、stm32 标准库 2、arm-none-eabi 工具链 3、烧录器 二、Keil 工程改造 1、Keil 工程 2、基本 Makefile 工程 3、添加启动文件 4、添加链接脚本 5、去掉 core_cm3.c 三…

App Inventor 2 如何接入ChatGPT:国内访问OpenAI的最佳方式

如何接入OpenAI 由于国内无法访问OpenAI&#xff0c;KX上网可选大陆及香港&#xff08;被屏蔽&#xff09;以外才行。因此对于大多数人来说&#xff0c;想体验或使用ChatGPT就不太便利&#xff0c;不过App Inventor 2 为我们提供了相对便利的一种方式&#xff0c;即“试验性质…

基于STM32F407的项目迁移到STM32F427

提示&#xff1a;此文档迁移教程使用的是IAR&#xff0c;有关代码的修改使用的是Vscode,基于STM32F407的项目迁移到STM32F427 基于STM32F407的项目迁移到STM32F427 前言一、硬件区别1.1.区别&#xff1a;1.2.需要注意以下硬件区别&#xff1a; 二、引脚配置三、STM32F4273.1.晶…

安卓高级控件(下拉框、列表类视图、翻页类视图、碎片Fragment)

下拉框 此小节介绍下拉框的用法以及适配器的基本概念&#xff0c;结合对下拉框Spinner的使用说明分别阐述数组适配器ArrayAdapter、简单适配器SimpleAdapter的具体用法与展示效果。 下拉框控件Spinner Spinner是下拉框控件&#xff0c;它用于从一串列表中选择某项&#xff0…

41-4 DDOS攻击防护实战

一、UDP FLOOD攻击 # hping3 -q -n -a <攻击IP> -S -s <源端口> --keep -p <目的端口> --flood <被攻击IP> hping3 --udp -s 6666 -p 53 -a 192.168.1.6 --flood 192.168.1.13 这个命令是使用hping3工具进行UDP Flood攻击的命令。下面是各个选项的作…