VUE移动端音乐APP学习【五】:歌手组件开发

歌手列表数据获取

歌手列表数据接口依旧使用前面的API,使用axios获取歌手列表数据

singer.js:

 

import axios from ‘axios‘;

export function getSingerList() {
  return axios.get(‘/api/getSingerList‘);
}

singer.vue:

import { getSingerList } from ‘../../api/singer‘
import { ERR_OK } from ‘../../api/config‘

export default {
  data() {
    return {
      singers: []
    }
  },
  created() {
    this._getSingerList()
  },
  methods: {
    _getSingerList() {
      getSingerList().then((res) => {
        res = res.data.response.singerList;
        if (res.code === ERR_OK) {
          this.singers = res.data.singerList
          console.log(this.singers)
        }
      })
    }
  }
}

运行结果:

VUE移动端音乐APP学习【五】:歌手组件开发

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

有上图运行结果可知需要对歌手数据进行一些简单的处理,需要将歌手ID、歌手名字和歌手图片进行封装,并且由于返回的歌手数据没有相应的键名(即歌手姓名的首字母)所以还需要获取其键名,这样才能够通过键名进行分类。

获取歌手首字母方法:使用js-pinyin插件

singerName.js

import pinyin from ‘js-pinyin‘;

export function Getinitial(string) {
  let pinyin = require(‘js-pinyin‘);
  pinyin.setOptions({ checkPolyphone: false, charCase: 0 });
  return pinyin.getCamelChars(string).substring(0, 1);
}

封装歌手数据:

singer.js

export default class Singer {
  constructor({ id, name, avatar }) {
    this.id = id;
    this.name = name;
    this.avatar = avatar;
  }
}

singer.vue

const HOT_NAME = ‘热门‘;
const HOT_SINGER_LEN = 10;
export default {
  name: ‘singer‘,
  components: {
    ListView,
  },
  data() {
    return {
      singers: [],
    };
  },
  created() {
    this._getSingerList();
  },
  methods: {
    _getSingerList() {
      getSingerList().then((res) => {
        res = res.data.response.singerList;
        if (res.code === ERR_OK) {
          this.singers = this._normalizeSinger(res.data.singerlist);
        }
      });
    },
    _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.singer_mid,
            name: item.singer_name,
            avatar: item.singer_pic,
          }));
        }
        const key = Getinitial(item.singer_name);
        if (!map[key]) {
          map[key] = {
            title: key,
            items: [],
          };
        }
        map[key].items.push(new Singer({
          id: item.singer_mid,
          name: item.singer_name,
          avatar: item.singer_pic,
        }));
      });
      console.log(map);
    },
  },
};

运行结果:

VUE移动端音乐APP学习【五】:歌手组件开发

为了得到有序的歌手列表,还需要对 map 进行处理

_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.singer_mid,
            name: item.singer_name,
            avatar: item.singer_pic,
          }));
        }
        const key = Getinitial(item.singer_name);
        if (!map[key]) {
          map[key] = {
            title: key,
            items: [],
          };
        }
        map[key].items.push(new Singer({
          id: item.singer_mid,
          name: item.singer_name,
          avatar: item.singer_pic,
        }));
      });
      // 为了得到有序列表,我们需要处理map
      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);
      });
      // concat() 方法用于连接两个或多个数组。该方法不会改变现有的数组,而仅仅会返回被连接数组的一个副本。
      return hot.concat(ret);
    },

显示歌手列表

歌手列表基础组件:listview.vue

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

<script>
    import Scroll from base/scroll/scroll

    export default {
        props: {
            data: {
                type: Array,
                default() {
                    return [];
                  },
            },
        },
        components: {
            Scroll
        }
    }
</script>

<style lang="scss" scoped>
.listview {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: $color-background;

  .list-group {
    padding-bottom: 30px;

    .list-group-title {
      height: 30px;
      line-height: 30px;
      padding-left: 20px;
      font-size: $font-size-small;
      color: $color-text-l;
      background: $color-highlight-background;
    }

    .list-group-item {
      display: flex;
      align-items: center;
      padding: 20px 0 0 30px;

      .avatar {
        width: 50px;
        height: 50px;
        border-radius: 50%;
      }

      .name {
        margin-left: 20px;
        color: $color-text-l;
        font-size: $font-size-medium;
      }
    }
  }

  .list-shortcut {
    position: absolute;
    z-index: 30;
    right: 0;
    top: 50%;
    transform: translateY(-50%);
    width: 20px;
    padding: 20px 0;
    border-radius: 10px;
    text-align: center;
    background: $color-background-d;
    font-family: Arial, Helvetica, sans-serif;

    .item {
      padding: 3px;
      line-height: 1;
      color: $color-text-l;
      font-size: $font-size-small;
      //  &表示当前元素
      &.current {
        color: $color-theme;
      }
    }
  }

  .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;
    }
  }

  .loading-container {
    position: absolute;
    width: 100%;
    top: 50px;
    transform: translateY(-50%);
  }
}
</style>

在singer.vue中使用该组件

// singer.vue

<template>
  <div class="singer">
    <list-view :data="singerList"></list-view>
  </div>
</template>

<script type="text/ecmascript-6">
  import ListView from ../../base/listview/listview

  export default {
    ...
    components: {
      ListView
    }
  }
</script>

运行结果:

VUE移动端音乐APP学习【五】:歌手组件开发

右侧快速入口实现

类比于手机通讯录,悬浮于屏幕右侧的 A-Z 可以帮助我们快速找到对应的歌手。

在listview.vue中添加

<div class="list-shortcut">
    <ul>
        <li v-for="(item, index) in shortcutList" :key="index" class="item">{{item}}</li>
    </ul>
</div>

<script>
    export default {
        ...
        computed: {
            shortcutList() {
                return this.data.map((group) => {
                    return group.title.substr(0, 1)
                })
            }
        }
    }
</script>

运行结果:右侧出现快速入口

VUE移动端音乐APP学习【五】:歌手组件开发

接下来就为其添加点击事件:点击对应字母时,需要获取其索引,这里直接获取 v-for 提供的 index 即可

export default {
    ...
    methods: {
        onShortcutTouchStart(e, index) {
            this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
        }
    }
    scrollTo() {
        this.scroll && this.scroll.scrollTo.apply(this.scroll, arguments)
  },
      scrollToElement() {
        this.scroll && this.scroll.scrollToElement.apply(this.scroll, arguments)
  }
}    

接着就是实现右侧快速入口滑动滚动效果了:

在 onShortcutTouchStart 事件中记录触碰点的初始位置,以及 onShortcutTouchMove 事件中触碰点的位置,通过两个位置的像素差,来滚动歌手列表。在给右侧添加滑动滚动的同时,需要阻止歌手列表滚动,以及浏览器原生滚动,所以要使用 @touchmove.stop.prevent 阻止原生的 touchmove。

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

<script>
    const ANCHOR_HEIGHT = 18

    export default {
        created() {
            this.touch = {}
        },
        ...
        methods: {
            onShortcutTouchStart(e, index) {
                let firstTouch = e.touches[0]
                this.touch.y1 = firstTouch.pageY
                this.touch.anchorIndex = index
                this._scrollTo(index)
            },
            onShortcutTouchMove(e) {
                let firstTouch = e.touches[0]
                this.touch.y2 = firstTouch.pageY
                let delta = (this.touch.y2 - this.touch.y1) / ANCHOR_HEIGHT | 0
                let anchorIndex = this.touch.anchorIndex + delta
                this._scrollTo(anchorIndex)
            },
            _scrollTo(index) {
                this.$refs.listview.scrollToElement(this.$refs.listGroup[index], 0)
            }
        },
        components: {
            Scroll
        }
    }
</script>

最后就是高亮当前显示的 title以及滚动固定标题:

  • 高亮当前显示的 title,需要监听 scroll 组件的滚动事件,来获取当前滚动的位置在屏幕滑动的过程中,并且需要实时派发 scroll 事件,所以在 listview 中将 probeType 的值设为 3
<script >
  export default {
    props: {
      ...
      listenScroll: {
        type: Boolean,
        default: false
      }
    },
    methods: {
      _initScroll() {
        ...
        if (this.listenScroll) {
          let me = this
          this.scroll.on(‘scroll‘, (pos) => {
            me.$emit(‘scroll‘, pos)
          })
        }
      }
    }
  }
</script>
  • 滚动固定标题:滚动歌手列表页时,当前歌手对应的title固定不动,滚动到下一个 title 时,新的 title 将旧的 title 顶替掉,这里就需要计算一个 title 的高度
// listview.vue

<template>
    <scroll class="listview" 
            :data="data" 
            ref="listview"
            :probe-type="probeType"
            :listenScroll="listenScroll"
            @scroll="scroll">
        ...
        <div class="list-fixed" ref="fixed" v-show="fixedTitle">
            <div class="fixed-title">{{fixedTitle}}</div>
        </div>
    </scroll>
</template>

<script>
    import Scroll from ../../base/scroll/scroll

    const TITLE_HEIGHT = 30
    const ANCHOR_HEIGHT = 18

    export default {
        ...
        data() {
            return {
                scrollY: -1,
                currentIndex: 0,
                diff: -1
            }
        },
        computed: {
            ...
            fixedTitle() {
                if (this.scrollY > 0) {
                    return ‘‘
                }
                return this.data[this.currentIndex] ? this.data[this.currentIndex].title : ‘‘
            }
        },
        watch: {
            ...
            scrollY(newY) {
                ...
                for (let i = 0; i < listHeight.length - 1; i++) {
                    ...
                    if (-newY >= height1 && -newY < height2) {
                        ...
                        this.diff = height2 + newY
                        return
                    }
                }
                ...
            },
            diff(newVal) {
                let fixedTop = (newVal > 0 && newVal < TITLE_HEIGHT) ? newVal - TITLE_HEIGHT : 0
                if (this.fixedTop === fixedTop) {
                    return
                }
                this.fixedTop = fixedTop
                this.$refs.fixed.style.transform = `translate3d(0,${fixedTop}px,0)`
            }
        }
    }
</script>

  整体运行效果图

VUE移动端音乐APP学习【五】:歌手组件开发

VUE移动端音乐APP学习【五】:歌手组件开发

上一篇:Oracle表空间数据文件移动的方法


下一篇:cnpm : 无法加载文件 C:\Users\Raytine\AppData\Roaming\npm\cnpm.ps1,因为在此系统上禁止运行脚本。