vue search page development (hot search, historical search, Taobao interface demonstration)

Keywords: Javascript Vue axios JSON

Full effect demonstration

 

Complete the pseudo search box first

src/components/search/index.vue (general search box component)

<template>
  <div class="mine-search-box-wrapper">
    <i class="iconfont icon-search"></i>
    <div class="mine-search-box" v-if="fake">{{placeholder}}</div>
    <input
      class="mine-search-box"
      type="text"
      title="Search box"
      :placeholder="placeholder"
      ref="input"
      v-model="query"
      v-if="!fake"
    >
    <i
      class="iconfont icon-close"
      v-show="query"
      @click="reset"
    ></i>
  </div>
</template>

<script>
import {debounde} from 'assets/js/util';


export default {
    name:'Search',
    props:{//Parameters received
        placeholder:{
            type:String,
            default:'Please enter the search content'
        },
        fake:{
            type:Boolean,
            default:false
        }
    },
    data(){
        return{
            query:'',
        }
    },
    watch:{
        query:debounde(function(){
            this.$emit('query',this.query);
        })
    },
    methods:{
        focus(){
            this.$refs.input && this.$refs.input.focus();
        },
        clear(){
            this.query='';
        },
        reset(){//Reset
            this.clear();
            this.focus();
        }
    }
}
</script>

<style lang="scss" scoped>
    $search-box-height: 30px;
    $icon-color: #ccc;
    $icon-font-size-sm: 18px;

  .mine-search-box-wrapper {
    display: flex;
    align-items: center;
    width: 85%;
    height: $search-box-height;
    padding: 0 7px;
    background-color: #fff;
    border-radius: $search-box-height / 2;
    margin-left:15px;
  }

  .iconfont {
    color: $icon-color;
    font-size: $icon-font-size-sm;
    font-weight: bold;
  }

  .mine-search-box {
    flex: 1;
    background: none;
    border: none;
    margin: 0 6px;
    color: #666;
    line-height: 1.5;
  }
</style>

 

src/assets/js/util.js throttling function (to prevent excessive frequency of data request from consuming performance)

//Function throttling
export const debounde=(func,delay=200)=>{
    let timer=null;

    return function(...args){
        timer && clearTimeout(timer);

        timer=setTimeout(()=>{
            func.apply(this,args);
        },delay);
    }
}

 

Introducing the search box component into the header component of the classification page

src/pages/category/header.vue

<template>
    <div class="header">
        <i class="iconfont icon-scan header-left"></i>
        <div class="header-center">
            <search placeholder="You are welcome at the beginning of the school season" @query='getQuery' fake @click.native="goToSearch" />
        </div>
        <i class="iconfont icon-msg header-right"></i>
    </div>
</template>

<script>
import Search from 'components/search';

export default {
    name:'CategoryHeader',
    components:{
        Search
    },
    methods:{
        getQuery(query){
            console.log(query);
        },
        goToSearch(){
            this.$router.push('/search');
        }
    }
}
</script>

<style lang="scss" scoped>
    .header{
        background-color:rgba(222, 24, 27, 0.9);
        transition:background-color 0.5s;
        display: flex;
        justify-content: space-between;
        align-items: center;
        padding:5px 20px;

        .iconfont{
            font-size:24px;
            color:#fff;
        }

        .header-center{
            flex:1;
        }
    } 
</style>

 

Click the search box and you will jump to the real search page

 

 

Popular search components

 

 

src/pages/search/hot.vue

<template>
  <div class="hot">
    <h4 class="hot-title">Popular search</h4>
    <div class="loading-container" v-if="!hots.length">
      <me-loading/>
    </div>
    <ul class="hot-list" v-else>
      <li class="hot-item" v-for="(item,index) in hots" :key="index" @click="$_selectItem(item.hotWord)">
          {{item.hotWord}}
      </li>
    </ul>
  </div>
</template>

<script>
import Search from 'components/search';
import MeLoading from 'components/loading';
import {getHot} from 'api/search';
import {searchMixin} from 'api/mixins';

export default {
    name:'SearchHot',
    components:{
        MeLoading
    },
    data(){
        return{
            hots:[]
        }
    },
    mixins:[searchMixin],
    created(){
        this.getHot().then(()=>{
            this.$emit('loaded');
        })
    },
    methods:{
       getHot(){
           return getHot().then(data=>{
               return new Promise(resolve=>{
                   if(data){
                       this.hots=data;
                       resolve();
                   }
               })
           })
       }
    }
}
</script>

<style lang="scss" scoped>
$border-color: #e5e5e5;
$font-size-base: 12px;
$font-size-l: $font-size-base + 2;

 .hot {
    padding-left: 10px;
    background-color: #fff;
    border-bottom: 1px solid $border-color;
    margin-bottom: 10px;

    &-title {
      height: 34px;
      line-height: 34px;
      font-size: $font-size-l;
      font-weight: bold;
    }

    &-list {
      display: flex;
      flex-wrap: wrap;
    }

    &-item {
      padding: 8px;
      background-color: #f0f2f5;
      border-radius: 4px;
      margin: 0 10px 10px 0;
      color: #686868;
    }
  }

  .loading-container {
    padding: 10px 0;
  }
</style>

 

axios get popular search data

src/api/search.js

import axios from 'axios';

//Get popular search data ajax
export const getHot=()=>{
    return axios.get('http://www.imooc.com/api/search/hot').then(res=>{
        
        res=res.data.hotKeyWord;
        if(res && res.owner){
            return res.owner;
        }
        throw new Error('Failed to get data');

    }).catch(err=>{
        console.log(err);
    });
}

 

Click search keywords and jump to Taobao Search Program

src/api/mixins.js

import storage from 'assets/js/storage';
import {SEARCH_HISTORY_KEYWORD_KEY} from 'pages/search/config';

export const searchMixin={
    methods:{
        $_selectItem(keyword){
            let keywords=storage.get(SEARCH_HISTORY_KEYWORD_KEY,[]);//Find all search history
 
             if(keywords.length!=0){
                 keywords=keywords.filter(val=>val!=keyword);//If this keyword already exists in the search history, remove it first
             }
 
             keywords.unshift(keyword);//Put this keyword at the beginning of search history
            
            storage.set(SEARCH_HISTORY_KEYWORD_KEY,keywords);//Update search history
 
            //Jump to Taobao Search page
            location.href = `https://s.m.taobao.com/h5?event_submit_do_new_search_auction=1&_input_charset=utf-8&topSearch=1&atype=b&searchfrom=1&action=home%3Aredirect_app_action&from=1&sst=1&n=20&buying=buyitnow&q=${keyword}`;
        }
    }
}

 

Local storage file assets/js/storage.js

const storage = window.localStorage;

export default {
  set(key, val) {
    if (val === undefined) {
      return;
    }
    storage.setItem(key, serialize(val));
  },
  get(key, def) {
    const val = deserialize(storage.getItem(key));
    return val === undefined ? def : val;
  },
  remove(key) {
    storage.removeItem(key);
  },
  clear() {
    storage.clear();
  }
};

function serialize(val) {
  return JSON.stringify(val);
}

function deserialize(val) {
  if (typeof val !== 'string') {
    return undefined;
  }
  try {
    return JSON.parse(val);
  } catch (e) {
    return val || undefined;
  }
}

 

Search page configuration file src/pages/search/config.js

const prefix = 'mall-search';
const suffix = 'key';
export const SEARCH_HISTORY_KEYWORD_KEY = `${prefix}-history-keyword-${suffix}`;

 

History search component

 

 

src/pages/search/history.vue

<template>
  <div class="history" v-if="historys.length">
    <h4 class="history-title">History Search</h4>
    <transition-group class="g-list" tag="ul" name="list">
      <li class="g-list-item" v-for="item in historys" :key="item" @click="$_selectItem(item)">
          <span class="g-list-text">{{item}}</span>
          <!-- .stop No bubbling -->
          <i class="iconfont icon-delete" @click.stop="removeItem(item)"></i>
      </li>
    </transition-group>
    <a href="javascript:;" class="history-btn" @click="showConfirm">
        <i class="iconfont icon-clear" ></i>
        Clear history search
    </a>
  </div>
</template>

<script>
import storage from 'assets/js/storage';
import {SEARCH_HISTORY_KEYWORD_KEY} from 'pages/search/config';
import {searchMixin} from 'api/mixins';


export default {
    name:'SearchHistory',
    data(){
        return{
            historys:[]
        }
    },
    mixins:[searchMixin],
    created(){
        this.getKeyword();
    },
    methods:{
        update(){
          this.getKeyword();
        },
        getKeyword(){
            this.historys=storage.get(SEARCH_HISTORY_KEYWORD_KEY,[]);
            this.$emit('loaded');
        },
        removeItem(item){
          this.historys=this.historys.filter(val=>val!==item);//Click to delete the item
          storage.set(SEARCH_HISTORY_KEYWORD_KEY,this.historys);//Update cache
          this.$emit('remove-item');
        },
        showConfirm(){
          this.$emit('show-confirm');
        },
        clear(){
          storage.remove(SEARCH_HISTORY_KEYWORD_KEY);
        }
    }
}
</script>

<style lang="scss" scoped>

    $border-color: #e5e5e5;
    $font-size-base: 12px;
    $font-size-l: $font-size-base + 2;
    $border-color: #e5e5e5;

    @mixin flex-center($direction: row) {
        display: flex;
        justify-content: center;
        align-items: center;
        flex-direction: $direction;
    }

  .history {
    padding-bottom: 30px;
    background-color: #fff;

    &-title {
      height: 34px;
      line-height: 34px;
      padding: 0 10px;
      font-size: $font-size-l;
      font-weight: bold;
    }

    &-btn {
      @include flex-center();
      width: 80%;
      height: 40px;
      background: none;
      border: 1px solid #ccc;
      border-radius: 4px;
      margin: 0 auto;
      color: #686868;

      .iconfont {
        margin-right: 5px;
      }
    }
  }

  .g-list {
    border-top: 1px solid $border-color;
    border-bottom: 1px solid $border-color;
    margin-bottom: 20px;
  }

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

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

</style>

 

List style is pulled out uniformly

src/assets/scss/_list.scss

// list
@mixin flex-between() {
    display: flex;
    justify-content: space-between;
    align-items: center;
  }

//ellipsis
@mixin ellipsis() {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }

  $border-color: #e5e5e5;
  
.g-list {
  padding-left: 10px;
}
.g-list-item {
  overflow: hidden;
  @include flex-between();
  height: 44px;
  padding-right: 10px;
  border-bottom: 1px solid $border-color;
  color: #686868;

  &:last-child {
    border-bottom: none;
  }
}
.g-list-text {
  flex: 1;
  line-height: 1.5;
  @include ellipsis();
}

 

src/assets/scss/index.scss

@import 'icons';
@import 'list';

*{
    margin:0;
    padding:0;
}
html,body{
    // Must be set, otherwise content scrolling cannot be achieved
    width:100%;
    height:100%;
}
ul,li{
    list-style:none;
}
a{
    text-decoration: none;
    color:#333;
}

 

Confirmation box component

 

 

src/components/comfirm/index.vue

<template>
    <transition name="mine-confirm">
        <div class="mine-confirm-wrapper" v-show="visible">
            <div class="mine-confirm">
                <div class="mine-confirm-title">{{title}}</div>
                <div class="mine-confirm-msg">{{msg}}</div>
                <div class="mine-confirm-btns">
                    <button class="mine-confirm-btn mine-confirm-cancel" @click="cancel">{{cancelBtnText}}</button>
                    <button class="mine-confirm-btn mine-confirm-ok" @click="confirm">{{confirmBtnText}}</button>
                </div>
            </div>
        </div>
    </transition>
</template>

<script>
export default {
    name:'MineConfirm',
    props:{
        title:{
            type:String,
            default:''
        },
        msg:{
            type:String,
            default:'Are you sure you want to do this?'
        },
        cancelBtnText:{
            type:String,
            default:'cancel'
        },
        confirmBtnText:{
            type:String,
            default:'Determine'
        }
    },
    data(){
        return{
            visible:false
        }
    },
    methods:{
        show(){
            this.visible=true;
        },
        hide(){
            this.visible=false;
        },
        cancel(){
            this.hide();
            this.$emit('cancel');
        },
        confirm(){
            this.hide();
            this.$emit('confirm');
        }
    }
}
</script>

<style lang="scss" scoped>

$search-z-index: 1200;
$search-popup-z-index: $search-z-index + 10;
$modal-bgc: rgba(0, 0, 0, 0.4);

@mixin flex-center($direction: row) {
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: $direction;
}
@mixin ellipsis() {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

  .mine-confirm-wrapper {
    position: absolute;
    top: 0;
    right: 0;
    bottom: 0;
    left: 0;
    z-index: $search-popup-z-index;
    @include flex-center();
    background-color: $modal-bgc;
  }

  .mine-confirm {
    overflow: hidden;
    width: 88%;
    background-color: #fff;
    border-radius: 10px;
    font-size: 16px;

    &-title {
      padding: 20px 15px 0;
      font-size: 18px;
      text-align: center;
      @include ellipsis();

      & + .mine-confirm-msg {
        padding-top: 20px;
        padding-bottom: 20px;
      }
    }

    &-msg {
      padding: 40px 15px;
      text-align: center;
      line-height: 1.5;
    }

    &-btns {
      display: flex;
    }

    &-btn {
      flex: 1;
      height: 44px;
      line-height: 44px;
      background: none;
      border: none;
    }

    &-cancel {
      border-top: 1px solid #e3e5e9;
    }

    &-ok {
      background-color: #f23030;
      color: #fff;
    }
  }

  .mine-confirm {
    &-enter-active,
    &-leave-active {
      transition: opacity 0.3s;
    }

    &-enter,
    &-leave-to {
      opacity: 0;
    }

    &-enter-active {
      .mine-confirm {
        animation: bounce-in 0.3s;
      }
    }
  }

  // https://Cn.vuejs.org/v2/guide/transitions.html × CSS animation
  @keyframes bounce-in {
    0% {
      transform: scale(0);
    }
    50% {
      transform: scale(1.1);
    }
    100% {
      transform: scale(1);
    }
  }
</style>

 

Search results page

 

 

src/pages/search/result.vue

<template>
    <div class="result">
    <div class="loading-container" v-show="loading">
      <me-loading/>
    </div>
    <ul class="g-list" v-show="!loading && results.length">
      <li
        class="g-list-item"
        v-for="(item, index) in results"
        :key="index"
        @click="$_selectItem(item[0])"
      >
        <span class="g-list-text">{{item[0]}}</span>
      </li>
    </ul>
    <div class="no-result" v-show="!loading && !results.length">No result</div>
  </div>
</template>

<script>
  import MeLoading from 'components/loading';
  import {getSearchResult} from 'api/search';
  import {searchMixin} from 'api/mixins';

export default {
    name:'SearchResult',
    components:{
        MeLoading
    },
    data(){
        return{
            results:[],
            loading:false
        }
    },
    props:{
        query:{
            type:String,
            default:''
        }
    },
    mixins:[searchMixin],
    watch:{
        query(query){
            
            this.getResults(query);
        }
    },
    methods:{
        getResults(keyword){
            if(!keyword){
                return;
            }

            this.loading=true;
            getSearchResult(keyword).then(data=>{
                console.log(data);
                if(data){
                    
                    this.results=data;
                    this.loading=false;
                }
            })
        }
    }
}
</script>

 

Modify src/api/search.js

import axios from 'axios';
import jsonp from 'assets/js/jsonp';

//Get popular search data ajax
export const getHot=()=>{
    return axios.get('http://www.imooc.com/api/search/hot').then(res=>{
        
        res=res.data.hotKeyWord;
        if(res && res.owner){
            return res.owner;
        }
        throw new Error('Failed to get data');

    }).catch(err=>{
        console.log(err);
    });
}

//Get search results for search box
export const getSearchResult=keyword=>{
    const url='https://suggest.taobao.com/sug';
    
    const params={
        q:keyword,
        code:'utf-8',
        area:'c2c',
        nick:'',
        sid:null
    };
    //https://suggest.taobao.com/sug?q=apple&code=utf-8&area=c2c&nick=&sid=null&callback=jsonp5
    return jsonp(url, params, {
        param: 'callback'
      }).then(res => {
         console.log(res);
        if (res.result) {
            // console.log(res);
            return res.result;
        }

        throw new Error('Data not obtained successfully!');
    }).catch(err => {
        if (err) {
            console.log(err);
        }
    });
};

 

Finally, when you delete a history search, you also need to update the scroll bar

Modify src/pages/search/index.vue

 

 

Modify src/pages/search/history.vue

(because there is an animation delay of 100ms when the page is loaded, the same delay is required to update the scroll bar here.)

 

 

Note the update operation of the scroll bar component. You need to use $nextTick() to implement asynchronous operation

Posted by Lagreca on Sat, 11 Apr 2020 07:46:12 -0700