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

  • 相关阅读:
    Java 编程基础
    LING 实战
    C# 3.0\3.5 新特性
    EF Code First 入门
    C# 4.0 新特性
    JavaScript学习(二)
    JavaScript学习(一)
    csdn的blog后台程序的导航菜单的实现
    HashTable的遍历
    开通啦
  • 原文地址:https://www.cnblogs.com/Small-Windmill/p/14970229.html
Copyright © 2020-2023  润新知