第5章 歌手页面开发

第5章 歌手页面开发

包括歌手数据的抓取和处理、Singer 类的封装、类通讯录组件 listview开发和应用。

5-1 歌手页面布局和设计讲解

做一个类似于通讯录的功能,当滑动左边的列表,右侧的相应的字母分类加上选中的样式,当点击右侧的字母的时候,滑动到左侧相应的项,头部标题固定。
在这里插入图片描述
在这里插入图片描述

5-2 歌手数据接口抓取

src\api\singer.js

import {commonParams,options} from './config'
import jsonp  from 'common/js/jsonp.js'

export function getsingerList(){
const url = 'https://c.y.qq.com/v8/fcg-bin/v8.fcg'
const data=Object.assign({},commonParams,{
    channel: 'singer',
    page: 'list',
    key: 'all_all_all',
    pagesize: 100,
    pagenum: 1,
    hostUin: 0,
    needNewCode: 0,
    platform: 'yqq',
    g_tk: 1664029744, 
})   
return jsonp(url,data,options)

在singer.vue中请求数据

created(){
    this._getsingerList()  
},
methods:{
 _getsingerList(){
    getsingerList().then(res=>{
        if(res.code===ERR_OK){
        	console.log(res.data.list)
            //将数据做过滤,过滤出我们需要的数据,我们将前10当是热门的数据
            // this._normalizeSinger(res.data.list)
        }
    }).catch(err=>{
        console.log(err);
    })
},

得到的数据如下
在这里插入图片描述

5-3 歌手数据处理和 Singer 类的封装

对歌手数据进行处理

 _normalizeSinger(list){
            //首先我们先对数据进行分类
            let map={
                hot:{
                   title: HOT_NAME,
                   items: []
                }
            }
            //首先我们先得到热门的歌曲,我们将得到的数据的前十条作为热门数据
            list.forEach((item,index) => {
                //在这里我们得到了热门的歌手
                if(index<HOT_SINGER_LEN){
                  map.hot.items.push({
                       id: item.Fsinger_mid,
                       name: item.Fsinger_name,
                       avatar: `https://y.gtimg.cn/music/photo_new/T001R300x300M000${item.Fsinger_mid}.jpg?max_age=2592000`
                  });
                }
                //在这里我们得到各个字母的的分类的歌手
                let key=item.Findex;
                if(!map[key]){
                    //当没有当前的字母项,我们创建一个
                    map[key]={
                        title: key,
                        items: []
                    }
                }
                //把相应的项添加到map[key]中
                map[key].items.push({
                     id: item.Fsinger_mid,
                    name: item.Fsinger_name,
                    avatar: `https://y.gtimg.cn/music/photo_new/T001R300x300M000${item.Fsinger_mid}.jpg?max_age=2592000`
                })

            });
        }

我们发现在push歌手信息的时候,avatar的计算是根据id而得到的,我们希望构造一个歌手类,通过传入id和name得到我们想要的数据格式。提高代码的复用性
common\js\singer.js

export default class Singer {
    constructor({name,id}){
        this.name=name,
        this.id=id
        this.avatar = `https://y.gtimg.cn/music/photo_new/T001R300x300M000${id}.jpg?max_age=2592000`
    }
}

做调整

//引入歌手类文件
import Singer from 'common/js/singer'

 //对歌手数据进行处理
        _normalizeSinger(list){
            //首先我们先对数据进行分类
            let map={
                hot:{
                   title: HOT_NAME,
                   items: []
                }
            }
            //首先我们先得到热门的歌曲,我们将得到的数据的前十条作为热门数据
            list.forEach((item,index) => {
                //在这里我们得到了热门的歌手
                if(index<HOT_SINGER_LEN){
                  map.hot.items.push(new Singer({
                       id: item.Fsinger_mid,
                       name: item.Fsinger_name,
                    })
                  );
                }
                //在这里我们得到各个字母的的分类的歌手
                let key=item.Findex;
                if(!map[key]){
                    //当没有当前的字母项,我们创建一个
                    map[key]={
                        title: key,
                        items: []
                    }
                }
                //把相应的项添加到map[key]中
                map[key].items.push(new Singer({
                       id: item.Fsinger_mid,
                       name: item.Fsinger_name,
                    })
                )

            });

            console.log(map);
        }

我们发现这个顺序是杂乱无章的,我们其实希望的是按照顺序排列,并且数据的结构是一个数组,现在我们来处理一下这些数据。对上面的数据进行排序 热门>A>B>C…

let hot=[]
let ret=[]
//对数据进行分类,分成字母类数组和热门类数组
for(let key in map){
    let val=map[key];
    if(val.title.match(/[a-zA-Z]/)){
          ret.push(val);
    }else if(val.title===HOT_NAME){
        hot.push(val);
    }
}
//数组的字母类数组进行排序
ret.sort((a,b)=>{
    return a.title.charCodeAt(0) - b.title.charCodeAt(0)
})
return hot.concat(ret);

在_getsingerList()中得到ingerList

this.singerList=this._normalizeSinger(res.data.list)

5-4 listview 基础组件的开发和应用-滚动列表实现

得到ingerList之后,我们就可以做滚动列表了,把data传入到scroll里面
引入scroll

import scroll from '../scroll/scroll'
<template>
    <scroll :data="data" class="listview">
          <ul>
              <li class="list-group" v-for="(item,index) in data" :key="index">
              <h2 class="list-group-title">{{item.title}}</h2>
              <ul>
                  <li class="list-group-item" v-for="singer in item.items" :key="singer.id">
                    <img class="avatar" v-lazy="singer.avatar">
                    <span class="name">{{singer.name}}</span>
                  </li>
              </ul>
              </li>
          </ul>
    </scroll>
</template>

5-5 listview 基础组件的开发和应用-右侧快速入口实现(1)

布局

//计算属性中
shortcut(){////得到title的集合数组,‘热门’取1个字
    let arr=[];
    this.data.forEach(item => {
      arr.push(item.title.substr(0,1));
    });
    return arr
}
 <!-- 右侧点点部分-->
  <div class="list-shortcut">
      <ul>
        <li :data-index="index" class="item" v-for="(item,index) in shortcut" :key="index">
          {{item}}
        </li>
      </ul>
  </div>

5-6 listview 基础组件的开发和应用-右侧快速入口实现(2)

触摸右侧标题
为了得到当前触摸的是哪一个点点,我们给加上了属性data-index

<div class="list-shortcut">
      <ul  @touchstart="onShortcutTouchStart" @touchmove.stop.prevent="onShortcutTouchMove">
        <li 
            v-for="(item,index) in shortcut" 
            :key="index"
            :data-index="index" class="item"
            :class="{current:index==currentIndex}"
        >
          {{item}}
        </li>
      </ul>
  </div>

当我们点击ul的时候我们需要获取到当前的点击元素的data-index来判断点击的是哪一个元素,那么怎么获取到data-index 这个属性呢,我们在dom,.js中export一个getData的方法。来设置或者获取属性

//给元素添加属性或者得到属性的值
export function getData(el,attrname,value){
    let prefix='data-'
    let  name=prefix+attrname
    if(value){
        el.setAttribute(name,value);
    }else{
        return el.getAttribute(name);
    }
}

在scroll组件中定义两个方法

  scrollTo() {
        // 滚动到指定的位置;这里使用apply 将传入的参数,传入到this.scrollTo()
        this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
    },
    scrollToElement(){
        //滚动到指定的元素
        this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
    }

在listview的右侧字母列表中,绑定触摸事件。在触摸事件中记录开始的位置,和当前点击的位置。用this.touch来记录位置信息。
去到对应的项

onShortcutTouchStart(e) {
 	let anchorIndex=getData(e.target,'index')
  //记录开始的位置
  this.touch.y1=e.touches[0].pageY;//开始的y坐标
   this.touch.anchorIndex=anchorIndex;
  //去到listHeigth数组的某一项
  this._scollTo(anchorIndex);
},

listHeigth用来存 左侧的每一个项的 的clientHeight。这样我们可以计算当前的元素索引在左侧的哪个区间。

 watch: {
    data(){
      setTimeout(()=>{
          this.listHeigth=[];
          let height=0;
          this.listHeigth.push(0);
          let list=[...this.$refs.listGroup];
          for(let i=0;i<list.length;i++){
              height+=list[i].clientHeight;
              this.listHeigth.push(height);
          }
      },20)
    }
  },


 //滑动到哪里
  _scollTo(index){
    this.$refs.listview.scrollTo(0,-this.listHeigth[index],0);
    this.currentIndex=index;
  }

当手指滑动的时候,计算y2 和 y1的差值/字母块的高度,得到需要偏移多少个字母块,再加上当前的位置,得到需要滚动到的index,再滚动到相应的位置。
因为scroll本身可以滑动,当我们滑动右侧的时候,我们会阻止默认行为,阻止事件冒泡

@touchmove.stop.prevent="onShortcutTouchMove"
//计算手机放下的位置,和move之后的位置,比较,让左边的滚动,用this.touch来记录位置信息
  onShortcutTouchMove(e){
       this.touch.y2=e.touches[0].pageY;//开始的y坐标
       let delta=(this.touch.y2-this.touch.y1)/CUTHEIGHT | 0//向下取整。获取到最接近的值
       let anchorIndex=parseInt(this.touch.anchorIndex)+delta
       this._scollTo(anchorIndex);
  },

5-7 listview 基础组件的开发和应用-右侧快速入口实现(3)

当我们滑动左侧的时候,我们需要监听我们当前滑动的纵向y的值,来判断当前滑动的是哪一个区间,来确定应该指向的索引
所以在scroll组件中,我们可以props一个listenScroll

listenScroll:{
    type:Boolean,
    default:false
}

在_initScroll中去判断,当_initScroll为true的时候,即需要监听better-scroll的滚动时间,当为true的时候,需要派发一个滚动的事件,并且将相关数据emit给父组件。
在sroll.vue中并不需要写获取pos之后需要做的操作。sroll.vue只需要负责分发事件就可以。

 //判断是否需派发一个scroll事件
 if(this.listenScroll){//派发一个scroll事件
    let me=this;
    this.scroll.on('scroll',(pos)=>{
        //给父组件分发一个getScrollY事件。并且传递pos.y参数
        me.$emit('scroll',pos)
    })
 }

在listview中确定是否需要listenScroll

created(){
      //当不需要渲染到dom上,不需要实时监听的,我们不必要放在data上
      this.listenScroll=true;//是否监听滚动事件
      this.listHeight=[]
  },

在listview中定义scrollY,用来定义一个变量scrollY,实时记录歌手列表Y轴滚动的位置pos.y,

 data(){
    return{
        currentIndex:0,
        probeType:3,
        touch:{},//放右侧的字母的touchmove的位置信息
        scrollY:-1,
        diff: -1 //fixed title的偏移位置
    }
  },

注意。scroll组件中设置了probeType的默认值为1:滚动的时候会派发scroll事件,会截流,只能监听缓慢的滚动,监听不到swipe快速滚动
解决:需要在中传递:probeType=“3” 除了实时派发scroll事件,在swipe的情况下仍然能实时派发scroll事件

在listview中绑定scroll事件

 <scroll 
      class="listview" 
      ref="listview" 
      :data="data"
      :probeType="probeType" 
      :listenScroll="listenScroll"
      @scroll="scroll"
     >
     ...
     </scroll>
 //接受到子组件的pos.y
  scroll(pos){
      this.scrollY=pos.y;//获取到y值。并且给了scrollY
  },

注意:私有方法如_scrollTo()一般放在下面,公共方法或绑定事件的方法如scroll()放在上面

实时监听scrollY的变化,遍历listHeight得到每个group元素的【高度区间】上限height1和下限height2,对比scrollY和每个group元素的高度区间height2-height1,确定当前滚动位置currentIndex,映射到DOM中

 //监听scrollYDE变化
   scrollY(newY) {
          const listHeight = this.listHeight
          if(!listHeight){
            return
          }
          //当滚动到顶部,newY>0
          if(newY > 0) {
            this.currentIndex = 0
            return
          }
          //在中间部分滚动,遍历到最后一个元素,保证一定有下限,listHeight中的height比元素多一个
          for(let i = 0; i < listHeight.length-1; i++){
            let height1 = listHeight[i]
            let height2 = listHeight[i+1]

            //diff=下项+已经移动的距离
            //得到fixed title上边界距顶部的偏移距离 = 歌手列表title height下限 + newY(上拉为负值)
            this.diff = height2 + newY;

            if(-newY >= height1 && -newY < height2) { 
                this.currentIndex = i
                //  console.log(this.currentIndex)
                return
            }
          }
          //当滚动到底部,且-newY大于最后一个元素的上限
          //currentIndex 比listHeight中的height多一个, 比元素多2个
          this.currentIndex = listHeight.length - 2
    },

5-8 listview 基础组件的开发和应用-右侧快速入口实现(4)

优化处理
touch事件都是加在父元素div class="list-shortcut"上的,点击头尾–“热”“Z”之前和之后的边缘区块,会发现也是可以点击的,但它没有对应显示的歌手列表,这个点击是没有意义的。touchmove一直在执行,这个事件一直没有结束,它的Y值就会变大,这样算出来的delta加上之前的touch.anchorIndex得到的值就可能会超
解决:

 //滑动到哪里
  _scollTo(index){
    //做边界处理
    //当点击的不是右侧的热门和字母块,
    if(!index && index !== 0){
      return
    }
    //当touchmove一直在执行
    if(index < 0){
      index = 0
    }else if(index > this.listHeight.length - 2){
      index = this.listHeight.length - 2
    }
    this.$refs.listview.scrollTo(0,-this.listHeight[index],0);
    this.currentIndex=index;
  },

5-9 listview 基础组件的开发和应用-滚动固定标题实现(上)

当滚动列表,顶部的标题部分固定,展示当前滚动到的列表项的标题。当滚动到下一项时,展示到下一项的标题

  <!-- 头部固定处理 -->
  <div class="list-fixed" v-show="fixedTitle" ref="fixedTitle">
      <div class="fixed-title">{{fixedTitle}}</div>
  </div>

当向下拉使得左侧列表往下移动的时候,我们希望fixedTitle是隐藏起来的,不能出现两个一样的标题。我们需要做边界处理,这种情况下的this.scrollY是大于0的
在这里插入图片描述

fixedTitle(){
  //做边界处理
  if(this.scrollY>0){
      return ''
  }
  return this.data[this.currentIndex] ?  this.data[this.currentIndex].title : ''
}

 .list-fixed
      position: absolute
      top: 0
      left: 0
      width: 100%
      .fixed-title
        height: 30px
        line-height: 30px
        padding-left: 20px
        font-size: $font-size-small
        color: $color-text-l
        background: $color-highlight-background

5-10 listview 基础组件的开发和应用-滚动固定标题实现(下)

优化:歌手列表的title上边界滚动到fixed title下边界时,给fixed title添加一个上移效果,使两个title过渡顺滑
定义一个diff数据来记录偏移位置

 data(){
    return{
        currentIndex:0,
        probeType:3,
        touch:{},//放右侧的字母的touchmove的事件
        scrollY:-1,
        diff: -1 //fixed title的偏移位置
    }
  },

在监听scrollY的时候实时得到diff
diff=下项+已经移动的距离
得到fixed title上边界距顶部的偏移距离 = 歌手列下限+已经移动的距离newY(上拉为负值)

scrollY(newY) {
          const listHeight = this.listHeight
          if(!listHeight){
            return
          }
          //当滚动到顶部,newY>0
          if(newY > 0) {
            this.currentIndex = 0
            return
          }
          //在中间部分滚动,遍历到最后一个元素,保证一定有下限,listHeight中的height比元素多一个
          for(let i = 0; i < listHeight.length-1; i++){
            let height1 = listHeight[i]
            let height2 = listHeight[i+1]

            //diff=下项+已经移动的距离
            //得到fixed title上边界距顶部的偏移距离 = 歌手列表title height下限-已经移动的距离+ newY(上拉为负值)
            this.diff = height2 + newY;

            if(-newY >= height1 && -newY < height2) { 
                this.currentIndex = i
                //  console.log(this.currentIndex)
                return
            }
          }
          //当滚动到底部,且-newY大于最后一个元素的上限
          //currentIndex 比listHeight中的height多一个, 比元素多2个
          this.currentIndex = listHeight.length - 2
    },

浮动的标题的高度

const TITLE_HEIGHT = 30;//浮动的标题的高度

监听固定的标题的底部距离下一项的距离

  diff(newVal){
  //当距离大于0,并且当前的距离小于30,即下一项还没完全和固定标题粘合。
    let fixedTop=(newVal>0 && newVal<TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
    if(this.fixedTop === fixedTop){
        return 
    }
    this.fixedTop = fixedTop
     this.$refs.fixedTitle.style.transform = `translate3d(0, ${fixedTop}px, 0)`
}

5-11 loading组件

实时歌手列表的数据也是通过异步获取的,我们在此也需要加上一个loading的组件

  <!-- loading部分 -->
<div class="loading-container" v-show="!data.length">
  <loading></loading>
</div>
.loading-container
    position: absolute
    width: 100%
    top: 50%
    transform: translateY(-50%)
</div>

github chapter5

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值