VUE移动端音乐APP学习【二十三】:搜索历史模块开发

搜索历史模块不仅在搜索模块出现,还在后续的添加歌曲模块中出现,多个组件多个模块共用了它,这个数据应该保存在全局的vuex中。

在state.js中添加searchHistory

// 搜索历史
  searchHistory: [],

有了state就设置它的mutation-types、mutations以及getters

//mutation-types.js
export const SET_SEARCH_HISTORY = 'SET_SEARCH_HISTORY';

//mutations.js
 [types.SET_SEARCH_HISTORY](state, history) {
    state.searchHistory = history;
  }

//getters.js
export const searchHistory = (state) => state.searchHistory;

在suggest组件里,当某个元素被点击时,让它派发事件。我们不能在selectItem函数里写保存搜索历史的数据,因为这应该是外组件做的事情,suggest组件做完自己的事情可以派发事件给外组件,外部关心这个事件的组件就会去保存搜索历史。

selectItem(item) {
      this.insertSong(item);
      this.$emit('select');
    },

搜索历史除了要在组件中共享之外,还要进入到本地缓存,比如浏览器的localStorage中,这样在下次刷新页面的时候,还能拿到历史结果。要实现这样永久缓存的功能,得去封装一个action。

  • 由于需要localstorage缓存,在common->js下新建cache.js,主要用来操作localstorage存取缓存相关的一些逻辑。
  •  利用第三方库的方法保存搜索结果:npm install good-storage@1.0.1
import storage from 'good-storage';

const SEARCH_KEY = '__search__';
// 最大只能存15条数据
const SEARCH_MAX_LENGTH = 15;
// 最新的搜索结果总是展现在最前面
function insertArray(arr, val, compare, maxLen) {
  // 查找数据是否存在在数组中
  const index = arr.findIndex(compare);
  if (index === 0) { // 有该数据且为数组的第一条数据
    return;
  }
  if (index > 0) { // 数组有这条数据但不是第一条
    arr.splice(index, 1);// 删掉之前的数据
  }
  // 插到数组的第一个位置
  arr.unshift(val);
  // 限制数组长度
  if (maxLen && arr.length > maxLen) {
    arr.pop();
  }
}
// 利用第三方库的方法保存搜索结果
export function saveSearch(query) {
  let searches = storage.get(SEARCH_KEY, []);
  // 把query插入当前历史列表中
  insertArray(searches, query, (item) => {
    return item === query;
  }, SEARCH_MAX_LENGTH);
  // 插入完之后保存
  storage.set(SEARCH_KEY, searches);
  // 返回新数组
  return searches;
}
  • 有了这样的方法,在action里就可以提交mutation,state就会被更新
import { saveSearch } from '../common/js/cache';

export const saveSearchHistory = function ({ commit }, query) {
  commit(types.SET_SEARCH_HISTORY, saveSearch(query));
};

在search组件中就可以监听该事件,调用action把当前的query存进去

<div class="search-result" v-show="query">
      <suggest @select="saveSearch" @listScroll="blurInput" :query="query"></suggest>
</div>



import { mapActions } from 'vuex';
...mapActions([
      'saveSearchHistory',
    ]),
 saveSearch() {
      // 把当前的query存进去
      this.saveSearchHistory(this.query);
    },

因为state的searchHistory与本地缓存关联起来,所以在cache中添加一个方法,从本地缓存读取数据

export function loadSearch() {
  return storage.get(SEARCH_KEY, []);
}

有了这个loadSearch就可以在state里做初始值替换

import { loadSearch } from '../common/js/cache';

  // 搜索历史
  searchHistory: loadSearch(),

可以看到控制台已经有searchHistory的数据缓存

 接下来就是将存储在vuex的searchHistory渲染在dom上

首先在search里添加getter

 computed: {
    ...mapGetters([
      'searchHistory',
    ]),
  },

在热门搜索下添加静态dom

<div class="search-history" v-show="searchHistory.length">
          <h1 class="title">
            <span class="text">搜索历史</span>
            <span class="clear">
              <i class="icon-clear"></i>
            </span>
          </h1>
</div>

开发基础组件search-list用于显示搜索历史数据,它的结构就是一个列表,然后在dom上遍历searches

<template>
  <div class="search-list" v-show="searches.length">
    <ul>
      <li class="search-item" v-for="(item,index) in searches" :key="index">
        <span class="text">{{item}}</span>
        <span class="icon">
          <i class="icon-delete"></i>
        </span>
      </li>
    </ul>
  </div>
</template>

<script>
export default {
  props: {
    searches: {
      type: Array,
      // eslint-disable-next-line vue/require-valid-default-prop
      default: [],
    },
  },
};

</script>
<style scoped lang="scss">
.search-list {
  .search-item {
    display: flex;
    align-items: center;
    height: 40px;
    overflow: hidden;

    &.list-enter-active,
    &.list-leave-active {
      transition: all 0.1s;
    }

    &.list-enter,
    &.list-leave-to {
      height: 0;
    }

    .text {
      flex: 1;
      color: $color-text-l;
    }

    .icon {
      @include extend-click();

      .icon-delete {
        font-size: $font-size-small;
        color: $color-text-d;
      }
    }
  }
}
</style>
search-list.vue

在search引入这个组件,将searchHistory数据传递给searches这个属性

<search-list :searches="searchHistory"></search-list>

import SearchList from '../../base/search-list/search-list.vue';

  components: {
    SearchBox,
    Suggest,
    SearchList,
  },

 接下来是实现search-list的交互功能(点击列表的元素复原到搜索框;点击删除键删除当前记录,点击清空清空所有历史记录)

  • 点击列表的时候派发事件,告诉外面的容器“我被选择了”,因为是基础组件是不会执行任何逻辑的。
<li @click="selectItem(item)" class="search-item" v-for="(item,index) in searches" :key="index">


methods: {
    selectItem(item) {
      this.$emit('select', item);
    },
  },
  • 同理点击删除键的时候也派发事件
<span class="icon" @click.stop="deleteOne(item)">
          <i class="iconfont icon-delete"></i>
</span>

 deleteOne(item) {
      this.$emit('delete', item);
    },
  • 在search组件监听点击事件,之前点击hot列表的时候有一个addQuery方法,可以直接使用它
  <search-list @select="addQuery" :searches="searchHistory"></search-list>

  •  删除事件实际上是对vuex进行操作,最终修改的也是搜索结果列表,需要在cache.js扩展一个删除方法并在action中封装删除操作.
//cache.js
// 删除方法
function deleteFromArray(arr, compare) {
  const index = arr.findIndex(compare);
  if (index > -1) {
    arr.splice(index, 1);
  }
}
export function deleteSearch(query) {
  // 获取缓存中的search列表
  let searches = storage.get(SEARCH_KEY, []);
  // 删除
  deleteFromArray(searches, (item) => {
    return item === query;
  });
  // 保存
  storage.set(SEARCH_KEY, searches);
  // 返回
  return searches;
}
  • 在action调用deleteSearch
export const deleteSearchHistory = function ({ commit }, query) {
  commit(types.SET_SEARCH_HISTORY, deleteSearch(query));
};
  • 在search里map这个action,再监听delete事件调用action。当点击删除时search-list就派发delete事件,search组件监听到这个事件后就提交action,这个action的作用就是从vuex里缓存的searchHistory删除要被删除的记录。
 <search-list @select="addQuery" @delete="deleteSearchHistory" :searches="searchHistory"></search-list>

 ...mapActions([
      'saveSearchHistory',
      'deleteSearchHistory',
    ]),

  • 点击垃圾桶图标,把整个列表全都清空掉,和上面删掉单条记录的套路类似。先在cache.js定义一个方法:清空数组
export function clearSearch() {
  storage.remove(SEARCH_KEY);
  return [];
}
  • 在actions.js调用这个方法
export const clearSearchHistory = function ({ commit }) {
  commit(types.SET_SEARCH_HISTORY, clearSearch());
};
  • 在search里map这个action,给垃圾桶元素绑定点击事件,一旦被点击就调用这个action提交清空数组操作。
<span class="clear" @click="clearSearchHistory"> 
  <i class="iconfont icon-clear"></i>
</span>

...mapActions(
[
'saveSearchHistory',
'deleteSearchHistory',
'clearSearchHistory',
]),
  • 设置弹窗组件:当我们点击垃圾桶清空记录时,应弹出弹框警告用户是否清空记录避免用户的误操作。弹窗组件基本代码如下:
<template>
  <transition name="confirm-fade">
    <div class="confirm">
      <div class="confirm-wrapper">
        <div class="confirm-content">
          <p class="text"></p>
          <div class="operate">
            <div class="operate-btn left"></div>
            <div class="operate-btn"></div>
          </div>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {

};

</script>
<style scoped lang="scss">
.confirm {
  position: fixed;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  z-index: 998;
  background-color: $color-background-d;

  &.confirm-fade-enter-active {
    animation: confirm-fadein 0.3s;

    .confirm-content {
      animation: confirm-zoom 0.3s;
    }
  }

  .confirm-wrapper {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    z-index: 999;

    .confirm-content {
      width: 270px;
      border-radius: 13px;
      background: $color-highlight-background;

      .text {
        padding: 19px 15px;
        line-height: 22px;
        text-align: center;
        font-size: $font-size-large;
        color: $color-text-l;
      }

      .operate {
        display: flex;
        align-items: center;
        text-align: center;
        font-size: $font-size-large;

        .operate-btn {
          flex: 1;
          line-height: 22px;
          padding: 10px 0;
          border-top: 1px solid $color-background-d;
          color: $color-text-d;

          &.left {
            border-right: 1px solid $color-background-d;
          }
        }
      }
    }
  }
}

@keyframes confirm-fadein {
  0% {
    opacity: 0;
  }

  100% {
    opacity: 1;
  }
}

@keyframes confirm-zoom {
  0% {
    transform: scale(0);
  }

  50% {
    transform: scale(1.1);
  }

  100% {
    transform: scale(1);
  }
}
</style>
confirm.vue
  • 在search.vue放置弹窗组件
 <div class="search-result" v-show="query">
      <suggest @select="saveSearch" @listScroll="blurInput" :query="query"></suggest>
 </div>
 <confirm></confirm>
  • confirm的显示状态是它自身来维护的,所以在confirm.vue中设置个data同时对外提供方法控制它的显示和隐藏
 <div class="confirm" v-show="showFlag">


 data() {
    return {
      showFlag: false,
    };
  },
  methods: {
    show() {
      this.showFlag = true;
    },
    hide() {
      this.showFlag = false;
    },
  },
  • 回到search组件调用其方法来控制弹窗的显示和隐藏,同时点击垃圾桶不能直接调用action,改成新的方法
<span class="clear" @click="showConfirm">
       <i class="iconfont icon-clear"></i>
</span>


<confirm ref="confirm"></confirm>
 showConfirm() {
      this.$refs.confirm.show();
    },

  •  可以看到上图点击垃圾桶后弹窗弹出,但是没有内容。这个内容也是由外部去传入的,也就是这个基础组件需要提供props供外部来传入
        <div class="confirm-content">
          <p class="text">{{text}}</p>
          <div class="operate">
            <div class="operate-btn left">{{cancelBtnText}}</div>
            <div class="operate-btn">{{confirmBtnText}}</div>
          </div>
        </div>


props: {
    // 文案
    text: {
      type: String,
      default: '',
    },
    // 确定
    confirmBtnText: {
      type: String,
      default: '确定',
    },
    // 取消
    cancelBtnText: {
      type: String,
      default: '取消',
    },
  },
  • 文案由search组件传给它
<confirm ref="confirm" text="是否清空所有搜索历史" confirmBtnText="清空"></confirm>

 

 

 

  •  接下来就是完善这个弹窗组件的交互功能,在confrim.vue中添加几个点击事件,这里也是不做任何逻辑,只派发事件告诉外部组件用户点击了“取消”或”清空“
<div @click="cancel" class="operate-btn left">{{cancelBtnText}}</div>
<div @click="confirm" class="operate-btn">{{confirmBtnText}}</div>

 cancel() {
      this.hide();
      this.$emit('cancel');
    },
    confirm() {
      this.hide();
      this.$emit('confirm');
    },
  • 在search组件中监听事件并完成它们的逻辑,因为取消事件不做任何处理可以选择不监听
<confirm ref="confirm" text="是否清空所有搜索历史" confirmBtnText="清空" @confirm="clearSearchHistory"></confirm>

搜索优化:

  • 当搜索历史记录过多时,无法往下滚动。因为没有用到scroll组件,所以在search组件中引入scroll组件,将short-cut的div改为scroll,同时再添加一个div包裹2个子div,这样scroll就根据2块的模块作计算进行滚动。

仅仅这样写还是无法滚动,因为获取的hotKey和searchHistory都是异步获取的。给scroll传递data就可以根据data的变化重新计算高度,但是传递hotKey和searchHistory哪一个都不合适,可以利用计算属性定义一个新的值,当hotKey和searchHistory哪一个值发生变化它就会重新计算。

<scroll class="shortcut" :data="shortcut">

//computed
  shortcut() {
      return this.hotKey.concat(this.searchHistory);
    },

还有一种情况是点击搜索添加了歌曲之后需要滚动,想完善这个功能需要watch query的改变,因为我们在搜索时添加了歌曲,dom实际停留在搜索列表这块。

如果我们是搜索列表suggest切换到search的时候,query是一个从有到无的变化。

 watch: {
    query(newQuery) {
      if (!newQuery) {
        setTimeout(() => {
          // 手动刷新scroll组件
          this.$refs.shortcut.refresh();
        }, 20);
      }
    },
  },
  • 迷你播放器和底部的搜索列表以及suggest的列表高度自适应的功能,和之前一样都是通过playmixin配上handleplaylist方法来做的。

先给suggest组件添加一个refresh方法给外组件代理刷新。

 refresh() {
      this.$refs.suggest.refresh();
    },

在search中引入playmixin并定义handleplaylist方法

import { playlistMixin } from '../../common/js/mixin';


 handlePlaylist(playlist) {
      const bottom = playlist.length > 0 ? '60px' : '';

      this.$refs.shortcutWrapper.style.bottom = bottom;
      this.$refs.shortcut.refresh();

      this.$refs.searchResult.style.bottom = bottom;
      this.$refs.suggest.refresh();
    },

posted @ 2021-07-05 18:23  小风车吱呀转  阅读(180)  评论(0编辑  收藏  举报