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