VUE移動端音樂APP學習【五】:歌手組件開發

歌手列表數據獲取

歌手列表數據接口依舊使用前面的API,使用axios獲取歌手列表數據css

singer.js:vue

 

import axios from 'axios';

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

singer.vue:ios

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

運行結果:axios

歌手數據處理和 Singer 類的封裝

有上圖運行結果可知須要對歌手數據進行一些簡單的處理,須要將歌手ID、歌手名字和歌手圖片進行封裝,而且因爲返回的歌手數據沒有相應的鍵名(即歌手姓名的首字母)因此還須要獲取其鍵名,這樣纔可以經過鍵名進行分類。api

獲取歌手首字母方法:使用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);
}

封裝歌手數據:app

singer.jsecmascript

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

singer.vueflex

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);
    },
  },
};

運行結果:

爲了獲得有序的歌手列表,還須要對 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>

運行結果:

右側快速入口實現

類比於手機通信錄,懸浮於屏幕右側的 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>

運行結果:右側出現快速入口

接下來就爲其添加點擊事件:點擊對應字母時,須要獲取其索引,這裏直接獲取 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>

  總體運行效果圖

相關文章
相關標籤/搜索