包括search-box 组件开发、热门搜索数据抓取和应用、suggest 组件开发、搜索结果保存功能实现、search-list 组件开发、confirm 组件开发。
base\search-Box\search-Box.vue 多个页面的搜索部分是相似的,我们将它抽离出来放在base里面
<div class="search-box-wrapper"> <div class="search-box"> <i class="icon-search"></i> <input :placeholder="placeholder" class="box" v-model="query"> <i class="icon-dismiss" v-show="query" @click="clear"></i> </div> </div>不同的页面的placeholder不一致的,所以我们需要props placeholder 。 判断query的长度,当query有值的才展示清除按钮,当点击的清除按钮,清除文本框的值。
<script> export default { props:{ placeholder:{type:String,default:'搜索歌曲、歌手'} }, data(){ return{ query:'' } }, methods:{ clear(){ this.query='' } } } </script>在search\search.vue,引入search-box组件
热门搜索布局
<!--搜索框没有搜索关键之的展示--> <div class="shortcut-wrapper" v-show="!query"> <div class="shortcut"> <div class="hot-key" style="pointer-events: auto;"> <h1 class="title">热门搜索</h1> <ul > <li class="item"><span >该死的温柔 </span></li> <li class="item"><span >不能说的秘密 </span></li> </ul> </div> </div> </div>定义请求 api\search.js
import {options,commonParams} from '../api/config'; import jsonp from '../common/js/jsonp'; export function getHotKey(params) { let url='https://c.y.qq.com/splcloud/fcgi-bin/gethotkey.fcg'; let data=Object.assign({},commonParams,{ g_tk: '1928093487', inCharset: 'utf-8', outCharset: 'utf-8', notice: 0, format: 'jsonp', uin: 0, needNewCode: 1, platform:'h5', jsonpCallback: 'jp0', }) return jsonp(url,data,options) }search\search.vue 发送请求
data() { return { hotKey:[] } }, created(){ this._getHotKey() }, methods:{ _getHotKey(){ getHotKey().then(res=>{ if(ERR_OK==res.code){ this.hotKey=res.data.hotkey.slice(0,10);//取得前面十条数据 } }) } }渲染数据
<!-- 搜索框没有搜索关键之的展示--> <div class="shortcut-wrapper" v-show="!query"> <div class="shortcut"> <div class="hot-key"> <h1 class="title">热门搜索</h1> <ul v-show="hotKey"> <li class="item" v-for="item in hotKey" :key="item.n"><span >{{item.k}} </span></li> </ul> </div> </div> </div>当点击数据的热门搜索的列表项,我们希望在搜索框填入搜索关键字
<li class="item" v-for="item in hotKey" :key="item.n" @click="selectItem(item)"><span>{{item.k}} </span></li> selectItem(item){ //那么我们需要像searchBox传递我们想要搜索的值 this.$refs.search.set_query(item.k); }在search-Box.vue中定义改变query的方法
set_query(query){ this.query=query }一旦query发生变化的时候,我们需要发送请求,我们需要在search-Box.vue中向外派发query。search-Box.vue只负责自身的功能,当外部需要数据时,向外暴露力所能及的基本的数据,不对自身进行额外的操作(思考:为什么不在watch中监听query的变化,而是在created中watch数据的变化?)
//在create中watch query的变化 created() { this.$watch('query', (newQuery) => { this.$emit('query', newQuery) }) }在search.vue中接收query
<search-box ref="search" @query="onQueryChange"></search-box> onQueryChange(query){ //根据得到的关键字 this.query=query },因为搜索的列表页 和 添加歌曲到列表 特别相似。我们将这个列表抽取。做suggest组件。 在search页面中引入suggest.vue组件,因为suggest.vue组件的结果是依赖 query 的,所以我们在获取到search-Box.vue的query的值的时候,同时也把query传给suggest.vue
<!-- 搜索框有关键字的展示 --> <div class="search-result" v-show="query"> <suggest :query="query" :showSinger="showSinger"></suggest> </div>suggest.vue 页面布局
<div class="suggest"> <ul class="suggest-list"> <li class="suggest-item" v-for="(item,index) in list" :key="index"> <div class="icon"> <i :class="getIconCls(item)"></i> </div> <div class="name"> <p class="text" v-html="getDisplayName(item)"></p> </div> </li> </ul> </div>定获取搜索列表的api
import axios from 'axios' //列表的搜索处理(需要传入参数 keyworld:关键字,pageIndex:当前第几页,pageSize:一页多少条数据,isShowSinger是否需要展示歌手), export function search(keyworld,pageIndex,pageSize,isShowSinger){ const url = '/api/search' let data=Object.assign({},commonParams,{ w: keyworld, p: pageIndex, perpage:pageSize, n:pageSize, catZhida: isShowSinger? 1 : 0, g_tk: '1928093487', inCharset:' utf-8', outCharset: 'utf-8', notice: 0, format: 'json', zhidaqu: 1, t: 0, flag: 1, ie: 'utf-8', sem: 1, aggr: 0, remoteplace: 'txt.mqq.all', uin: 0, needNewCode: 1, platform: 'h5', }) return new Promise(function(resolve,reject){ axios.get(url,{params:data}).then(res=>{ resolve(res.data) }).catch(err=>{ reject(err) }) }) }配置代理(config\index.js)
//根据关键字获取到搜索列表,做跨域处理 '/api/search':{ target:'https://c.y.qq.com/soso/fcgi-bin/search_for_qq_cp', secure:false, changeOrigin:true, bypass:function(req,proxyOptions){ req.headers.referer='https://y.qq.com/m/index.html', req.headers.origin='https://y.qq.com' }, pathRewrite:{ '^/api/search': '' } }suggest\suggest.vue
引入search
import {search} from '../../api/search'定义一个常量表示页展多少条
const pageSize = 20 //抓取数据一页有多少数据在页面上维护一list和当前的页码
data(){ return{ pageIndex:1, list:[]//当前的列表的数据 } },需要父组件传递 搜索的值,并且需要父组件传递是否需要展示歌手
props:{ query:{ type:String, default:'' }, showSinger:{ type:Boolean, default:false } },监听query的变化并且发起请求
watch: { query(newQuery) { //监听到query的变化,发送请求 this.search(newQuery); } } const SINGER_TYPE="singer"//定义一个代表当前类型的常量,判断是歌手还是歌曲 import {filterSinger} from '../../common/js/song'//引用filterSinger方法,做文本拼接 methods:{ //发送请求 search(newQuery){ //关键字,当前页(在data中维护当前页),一页多少条,是否展示歌手(根据props传递过来) search(newQuery,this.pageIndex,pageSize,this.showSinger).then(res=>{ if(res.code==ERR_OK){ this.list=this.getResult(res.data) } }) }, getResult(data){ let ret=[]; if(data.zhida && data.zhida.singerid){ //这里用展开运算符,相当于pbject.assgin ret.push({...data.zhida,...{type:SINGER_TYPE}}) } if(data.song){ ret=ret.concat(data.song.list); } return ret }, //做图标处理 getIconCls(item){ if(item.type==SINGER_TYPE){ return 'icon-mine' }else{ return 'icon-music' } }, //做文本处理 getDisplayName(item){ if(item.type === SINGER_TYPE) { return item.singername }else{ return `${item.songname}-${filterSinger(item.singer)}` } } },优化 我们在做文本处理的时候用了filterSinger函数,并且将其在common/js/song中暴露了出来,但是filterSinger函数是 createSong 函数的一个内部函数,我们不应该将其暴露出来,这里我们将 getResult处理一下
import createSong from '../../common/js/song' import {getMusicVkey} from '../../api/singer' getResult(data){ let ret=[]; if(result.data.zhida && result.data.zhida.singerid){ this.list.push({...result.data.zhida,...{type:SINGER_TYPE}}); } if(data.song){ //将data.song.list转化成一个歌曲类 ret=ret.concat(this._normallizeSong(data.song.list));//将两个数组进行合并 } return ret }, //将data.song.list转化成一个歌曲类 _normallizeSong(list){ let ret=[]; list.forEach(item => { if(item.songid && item.songmid){ //异步2 getMusicVkey(item.songmid).then(res=>{ if(res.code==ERR_OK){ ret.push(createSong(item,res.data.items[0].vkey)); } }) } }); //在这里我们出现了一个问题,我们得到了一个song类的集合,但是ret的长度却是为[] //是因为_normallizeSong的list是异步获取的,而ret 也是经过异步获取的 console.log(ret); return ret }, //做图标处理 getIconCls(item){ if(item.type==SINGER_TYPE){ return 'icon-mine' }else{ return 'icon-music' } },这里我们得到了一个song类的集合,但是集合的长度却是0,这是因为异步内使用异步 导致数组操作不能执行。歌曲数组数据是异步获取的,其中处理数据时拼接播放源url的vkey也是异步获取的,异步内部再异步导致
现在我们用async,await 来解决这个问题,当请求完成的时候才concat 原来的list(在forEach 用await是有问题的,我们用for of),现在我们去到 getResult,重写search
//发送请求 async search(newQuery){ this.pageIndex=1; this.list=[]; //关键字,当前页(在data中维护当前页),一页多少条,是否展示歌手(根据props传递过来) let result=await search(newQuery,this.pageIndex,PAGESIZE,this.showSinger) if(result.code==ERR_OK){ if(result.data.zhida && result.data.zhida.albumid){ this.list.push({...result.data.zhida,...{type:SINGER_TYPE}}); } // console.log(result.data.song.list) if(result.data.song.list){ let res=await this._nomalizeSong(result.data.song.list); this.list=this.list.concat(res) this._checkMore(result.data.song); } } }, //将data.song.list转化成一个歌曲类 async _nomalizeSong(list){ let ret=[]; for(let item of list){ if(item.songid && item.songmid){ let result=await getMusicVkey(item.songmid); if(result.code==ERR_OK){ ret.push(createSong(item,result.data.items[0].vkey)) } } } return ret }, //做文本处理 getDisplayName(item){ if(item.type === SINGER_TYPE) { return item.singername }else{ return `${item.name}-${item.singer}` } },至此,页面列表可以正常显示了
上拉加载功能
1、扩展scroll组件,在scroll组件上接收pullup参数。当pullup 为ture的时候,派发一个scrollEnd事件。告诉外部组件,已经滚动到底部了(scroll\scroll.vue)
//是否需要上拉刷新 pullup:{ type:Boolean, default:false }2、在初始化_initScroll的时候,判断是否需要派发scrollToEnd事件
//判断是否需要上拉刷新 if(this.pullup){ this.scroll.on('scrollEnd',(pos)=>{ if(this.scroll.y<=(this.scroll.maxScrollY+50)){//当滑动到底部50px处 this.$emit('scrollToEnd');//向外暴露一个scrollEnd 事件 } }) }3、引入scroll组件,替换掉根元素div
<scroll class="suggest" :data="result" :pullup="pullup" @scrollToEnd="searchMore" ref="suggest">4、在data中维护一个hasMore表示是否还有更多数据,定义searchMore函数和 _checkMore函数。searchMore函数用于下拉异步获取到更多的数据, _checkMore函数用于实时hasMore的值,当hasMore为false的时候,就无法继续上拉加载更多了。
async searchMore(){//加载更多 if(!this.hasMore){ return } this.pageIndex++; //关键字,当前页(在data中维护当前页),一页多少条,是否展示歌手(根据props传递过来) let result=await search(this.query,this.pageIndex,PAGESIZE,this.showSinger) if(result.code==ERR_OK){ if(result.data.song.list){ let res=await this._nomalizeSong(result.data.song.list); this.list=this.list.concat(res) this._checkMore(result.data.song); } } }, _checkMore(data){ // console.log("当前页:"+data.curpage,"当前条数"+data.curnum,'总数'+data.totalnum); if(!data.list || (data.curpage-1)*PAGESIZE+data.curnum>=data.totalnum){ this.hasMore=false }else{ this.hasMore=true } }暂无数据处理
做暂无数据展示,我们可以在base里面扩展 一个no-result的组件,在suggest\suggest.vue中引入
<!-- 展示暂无数据--> <div v-show="!hasMore && !list.length" class="no-result-wrapper"> <noResult title="暂无数据"></noResult> </div>点击歌手列
在search组件上添加router-view标签,给search组件配置子路由。
{ path:'/search', component:search, name:'搜索', children:[ { path:'/search/:id', component:singerDetail //指向歌曲详情页 } ] },当点击歌手列的时候,改变当前路由,并且修改vuex的singer状态
<li class="suggest-item" v-for="(item,index) in list" :key="index" @click="selectItem(item)"> selectItem(item){ if(item.type===SINGER_TYPE){ //跳转到当前的路由 let singer=new Singer({ name:item.singername, id:item.singermid }) //改变vuex的 singer this.set_singer(singer); this.$router.push({ path:`/search/${singer.id}` }); } }点击歌曲列
点击歌曲列,在selectItem时,当点击的是歌曲的时候,this.insert_song(item); suggest\suggest.vue
...mapActions(['insert_song']) selectItem(item){ if(item.type===SINGER_TYPE){ //省略... }else{ //需要改变vuex的playlist sequenceList currentIndex //因为需要改变多个vuex的state。我们需要将这些mutation封装在一个actions里面 this.insert_song(item); } }在当前的播放列表中加入该歌曲,播放默认列表中加入该歌曲,改变当前的播放的索引,展示播放页面,播放歌曲。因为我们需要改变多个vuex的属性,所以在这里我们使用actions
actions.js
function findIndex(list, song) { return list.findIndex((item) => { return item.id === song.id }) } //在当前列表插入一首歌曲 export const insert_song=function({commit,state},song){ let playlist=state.playlist.slice(0);//当前的播放的列表 let sequenceList=state.sequenceList.slice(0);//当前的播放默认的列表 let currentIndex=state.currentIndex;//当前的播放歌曲的索引 let playSong=playlist[currentIndex]//当前的播放的歌曲 let findplayIndex=findIndex(playlist,song);//判断播放列表中是否有这首歌 currentIndex++;//因为长度多了一首,让当前的索引++ playlist.splice(currentIndex,0,song);//在当前的播放列表的位置插入一首歌 //当播放列表中有这首歌,做删除重复歌曲操作 if(findplayIndex >-1 ){ //当播放列表中有这首歌 if(currentIndex>findplayIndex){ playlist.splice(findplayIndex,1) currentIndex-- }else{ playlist.splice(findplayIndex+1,1) } } //需要插入的位置 let sequenceIndex=findIndex(sequenceList,playSong)+1; //判断是否有这首歌 let findSequenceIndex=findIndex(sequenceList,song); sequenceList.splice(sequenceIndex,0,song)//插入一首歌 if(findSequenceIndex>-1){//存在重复的歌曲 if(findSequenceIndex < sequenceIndex){ sequenceList.splice(findSequenceIndex,1); }else{ sequenceList.splice(findSequenceIndex+1,1) } } //commit提交更改 commit(types.SET_PLAYLIST,playlist) commit(types.SET_SEQUENCELIST,sequenceList) commit(types.SET_CURRENTINDEX,currentIndex) commit(types.SET_PLAYING, true) commit(types.SET_FULLSCREEN, true) }节流
优化,当我们回退的时候,query在变化,就导致了页面不断地进行异步请求,在这里我们做节流处理,找到改变query的根源,做节流处理 common->js->util.js中:定义截流函数
//截流 //对一个函数做截流,就会返回新的函数,新函数是在延迟执行原函数 //如果很快的多次调用新函数,timer会被清空,不能多次调用原函数,实现截流 export function debounce(func, delay){ let timer return function (...args) { if (timer) { clearTimeout(timer) } timer = setTimeout(() => { func.apply(this, args) }, delay) } }search-Box\search-Box.vue(从根本解决问题,在watch中使用节流函数)
//在create中watch query的变化 created() { this.$watch('query',debounce((newQuery) => { this.$emit('query', newQuery) },300)) }手机端焦点处理
当我们在文本框输入内容的时候,在手机端会出现一个软键盘,而我们想要当我们滑动的时候,软键盘消失,即文本框失去焦点,所以我们需要在滚动的时候,监听到滚动的是否开始了,当滚动开始的时候,向外派发一个beforeScrollStart事件
对scroll进行扩展 scroll\scroll.vue 由外部传入的isBeforeScroll来确定是否需要向外派发beforeScrollStart事件
//一旦开始滚动列表的时候,是否需要向外部派发beforeScrollStart事件 isBeforeScroll:{ type:Boolean, default:false }在初始化方法_initScroll中做判断
//是否向外派发一个beforeScrollStart事件 if(this.isBeforeScroll){ this.scroll.on('beforeScrollStart',()=>{ this.$emit('beforeScrollStart') }) }suggest\suggest.vue。传入isBeforeScroll和接收beforeScrollStart事件
<scroll class="suggest" :data="list" @scrollToEnd='searchMore' :pullup="pullup" :isBeforeScroll="isBeforeScroll" :beforeScrollStart="beforeScrollStart" >在data中维护isBeforeScroll
isBeforeScroll:true,//是否需要滚动的时候派发滚动开始事件因为suggest.vue只做自己相关的逻辑,我们不在这个组件上做 input框失去焦点的操作,所以我们得到子组件派发的beforeScrollStart之后,继续向上派发事件给search组件
beforeScrollStart(){ //一旦滚动开始的时候,让search的input失去焦点 this.$emit('beforeScrollStart'); }search\search.vue 在search组件上获取的到search-Box.vue组件,并且调用search-Box.vue的nputBlur()失去焦点方法
//滚动的时候让input框失去焦点 beforeScrollStart(){ this.$refs.search.inputBlur() },search-Box\search-Box.vue
inputBlur(){ this.$refs.query.blur(); }除了搜索页面的搜索历史在其他页面也是有搜索历史的(添加歌曲到列表页面),多个组件共用搜索历史,我们可以将搜索历史放在vuex里面做处理 store\state.js
searchHistory:[],//存搜索历史store\getters.js
//暴露出searchHistory export const searchHistory=(state)=>state.searchHistorytore\mutation-types.js
//搜索历史 export const SET_SEARCHHISTORY='SET_SEARCHHISTORY';mutations.js
//改变当前的搜索历史 [types.SET_SEARCHHISTORY](state,list){ state.searchHistory=list }suggest\suggest.vue
在selectItem点击事件中,派发saveSearch事件
//一旦点击任何一个列表项的时候,派发事件,将当前的搜索的关键字作为参数派发出去 this.$emit('saveSearch',this.query)search\search.vue
<suggest :query="query" :showSinger="showSinger" @beforeScrollStart="beforeScrollStart" @saveSearch="saveSearch"></suggest> ...mapActions(['set_searchHistoty']), //保存搜索关键字 saveSearch(query){ //改变vuex的searchHistory数组 //将searchHistory存入本地 //因为进行了多个操作,这里我们需要在actions里面写 this.set_searchHistoty(query) }当点击搜索列表的时候,我们除了需要更改searchHistory,还需要将searchHistory存进到本地存储中,这里,我们进行了多个操作,所以我们将更改searchHistory和本地存储操作在actions里面写。
1、首先我们在 common\js\cache.js 封装本地存储的方法 用到的库:https://github.com/ustbhuangyi/storage src\common\js\cache.js
import storage from 'good-storage' const SEARCH_KEY='_search_' //双划线避免与外部冲突 const MAXLENGTH=15 //最大的长度 //插入搜索历史的方法 //参数:当前的数组,插入的值,比较方法(目的在于获取到之前搜索过的那个val的位置),最大长度 function insertArray(arr,val,compare,maxlength){ let index=arr.findIndex(compare)//获取到之前搜索的index(要是之前搜索过了index为-1) if(index==0){//当当前的是第一项,之前的也是在第一项,那么就不做任何添加操作 return } arr.unshift(val) //将我们需要插入的项插第一位 //之前有搜索过的记录 if(index>0){ arr.splice(index+1,1)//将之前的删除 } if(maxlength && arr.length>maxlength){ arr.pop(); } } export function saveSearch(val) { //取出来 let searchArr= storage.get(SEARCH_KEY,[])//获取到内存中的_search_。找不到的话就给默认值为[] //得到要存的数据(直接对searchArr做插入操作) insertArray(searchArr,val,(item)=>{ return item.trim()==val.trim() },MAXLENGTH) //存进去(得到新的searchArr,存进本地) storage.set(SEARCH_KEY,searchArr) //将searchArr返回 return searchArr }2、store\actions.js
//引入本地缓存的方法 import {saveSearch} from '../common/js/cache' //对搜索列表进行修改并且保存在本地 export function set_searchHistoty({commit,state},query){ commit(types.SET_SEARCHHISTORY,saveSearch(query)) }3、当我们刷新页面的时候,vuex的searchHistory为空数组,我们需要在state.js中将searchHistory设置为本地缓存的值 在cache.js中暴露 获取到缓存_search_的值的方法
//获取到本地存储的值 export function getSearchStorage(){ return storage.get(SEARCH_KEY,[]) }在state.js中使用
import {getSearchStorage} from 'common/js/cache' searchHistory:getSearchStorage(),//存搜索历史基础实现 除了搜索页面的搜索列表。添加到歌曲列表也有搜索列表,我们将这个列表抽取出来作为基础组件共享 base\search-list\search-list.vue
接收外层传入的数据
props:{ searches:{ type:Array, default:[] } }页面遍历列表
<div class="search-list"> <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>search\search.vue 获取到vuex的searchHistory
computed:{ ...mapGetters(['searchHistory']), },引入并且使用search-list组件,向组件传入searchHistory
<search-list :searches='searchHistory'></search-list>当点击删除当前列,当点击删除删除全部 当点击删除列或者删除全部。因为涉及到本地缓存,我们在common\js\cache.js封装删除的方法
//删除当前项的本地缓存 export function deleteSearchOne(val){ //获取到当前的 let searchArr= storage.get(SEARCH_KEY,[])//获取到内存中的_search_。找不到的话就给默认值为[] let index=searchArr.findIndex((item)=>{ return item==val }) searchArr.splice(index,1); //将当前的存进去 storage.set(SEARCH_KEY,searchArr) return searchArr; } //删除所有的本地缓存 export function removeAllSearch(){ storage.remove(SEARCH_KEY) return [] }store\actions.js
//根据当前的值去删除相应的缓存数据 export function delete_SearchOne({commit,state},item){ commit(types.SET_SEARCHHISTORY,deleteSearchOne(item)) } //删除所有的缓存的数据 export function delete_SearchAll({commit,state}){ commit(types.SET_SEARCHHISTORY,removeAllSearch()) }点击了删除当前项 search-list\search-list.vue作为基础的组件,我们只需要给外部派发事件,不要在基础组件做vuex的处理操作,只需要告诉外部,自己被点击了 因为删除图标是li的子元素,而li本身也绑定了自己的点击事件,所以这里需要用.stop修饰符来停止事件冒泡
<li class="search-item" v-for="(item,index) in searches" :key="index" @click="addquery(item)"> <span class="text">{{item}}</span> <span class="icon"><i class="icon-delete" @click.stop="selectItem(item)"></i></span> </li>当点击的是li,需要告诉外部点击了li,外部应该要根据li的值重新搜索了 当点击的是icon,需要告诉外部点击了icon删除按钮,外部应该要删除相关的项了
selectItem(item){ this.$emit('delectOne',item) }, addquery(item){ this.$emit('addquery',item); }在search\search.vue中,得到子组件派发的事件和参数,给全部删除绑定全部删除事件delectAll
<!-- 搜索历史部分--> <div class="search-history" v-show="searchHistory.length"> <h1 class="title"> <span class="text">搜索历史</span> <span class="clear" @click="delectAll"><i class="icon-clear"></i></span> </h1> <!--这里是搜索列表组件 --> <search-list :searches='searchHistory' @delectOne="delectOne" @addquery="addquery"></search-list> </div>发起actions
...mapActions( {'delete_SearchOne':'delete_SearchOne'}, {'delete_SearchAll':'delete_SearchAll'} ), //删除一个 delectOne(item){ this.delete_SearchOne(item); }, //删除全部 delectAll(){ this.delete_SearchAll(); }, addquery(query){ this.$refs.search.set_query(query); },在项目中,删除全部的数据是一个非常敏感的操作,当一不小心点错了,就会删除了批量数据,所以我们需要在点击全部删除的时候,友好提示客户,是否需要删除全部。 提示弹窗是一个基础组件,可能其他的组件也会频繁用到,我们把它放在base文件夹 base\confirm\confirm.vue
<transition name="confirm-fade"> <div class="confirm" v-show="isShow"> <div class="confirm-wrapper"> <div class="confirm-content"> <p class="text"> {{text}} </p> <div class="operate"> <div class="operate-btn left" @click="cancel"> {{cancelBtnText}} </div> <div class="operate-btn" @click="confirm"> {{confirmBtnText}} </div> </div> </div> </div> </div> </transition>动画效果
&.confirm-fade-enter-active animation: confirm-fadein 0.3s .confirm-content animation: confirm-zoom 0.3s @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)通过props可由外部传入要修改的相关文本。 confirm的显示和隐藏是有自己的内部维护的,我们在confirm组件的data维护一个isShow 当点击确顶或者取消的时候,只需要向外派发相关的事件,告诉外部,目前被点击了。不在基础组件写外部组件的相关逻辑
props:{ text:{ type:String, default:'' }, cancelBtnText:{ type:String, default:"取消" }, confirmBtnText:{ type:String, default:"确定" } }, data() { return { isShow:false } }, methods:{ hide(){ this.isShow=false }, show(){ this.isShow=true }, cancel(){ this.hide() this.$emit('cancel') }, confirm(){ this.hide() this.$emit('confirm'); } }通常,基础组件是不会放置和vuex相关的任何逻辑它一般是通过props,data和methods来实现自身的逻辑以及实现和外部的通讯。
现在我们来修改search\search.vue
<confirm text="您确定删除全部吗?" ref="confirm" @confirm="confirm" ></confirm> //删除全部 delectAll(){ this.$refs.confirm.show()//展示弹窗 }, confirm(){ this.delete_SearchAll() },1、让shortcut-wrapper (热门搜索和搜索历史)这部分可以滚动,引入scrol并使用scroll。
<scroll>里面包含多个同级元素,要在外层再嵌套一个<div> <scroll class="shortcut" ref="shortcut" :data="shortcut"> <div> <!--热门搜索部分 --> <!-- 搜索历史部分--> </div> </scroll>热门搜索和搜索历史部分的数据都是动获取的,那么在scroll里面传入的data属性。以哪个为准呢?当数据发生变化的时候,让scroll refresh()一下,在这里我么用计算属性shortcut来concat, hotKey和searchHistory。
shortcut(){ return this.hotKey.concat(this.searchHistory) }2、底部适配处理 shortcut-wrapper,search-result 的底部自适应 引入
import {playlistMixin} from 'common/js/mixin.js'注册
mixins:[playlistMixin],应用
//做底部自适应 handlePlayList(playlist){ let bottom= playlist.length ? 60 :0 this.$refs.shortcut_wrapper.style.bottom = `${bottom}px` this.$refs.search_result.style.bottom = `${bottom}px` this.$refs.shortcut.refresh() this.$refs.suggest.refresh()//在suggest组件暴露出一个refresh的方法。当底部bottom样式变化的时候,suggest组件的scroll重刷新 }优化 当搜索列表页面,切换到热门搜索页面scroll不能正常滚动,这是因为切换之前热门搜索页面是一个从隐藏到展示的过程。所以子啊这里我们需要监听query的变化,当query的值为空的时候,这个时候 热门搜索页面应该是展示的,这个时候,我们就让热门搜索页面的scroll重刷新
watch: { query(newQuery) { if (!newQuery) { setTimeout(() => { this.$refs.shortcut.refresh() }, 20) } } },bug: 加载更多的,每一条列表的数据异步获取到播放源,用了getMusicVkey。之前我在这里用了await处理,但是这里有一个问题,就是当不断上拉加载。关掉了搜索页面,去到其他的页面,getMusicVkey的异步请求还在继续。这里暂缓一下,所以把这部分的代码注释了,后续学了axios取消请求的相关知识再补上。
github chapter10