1、书写静态页面
2、拆分组件
3、获取服务器的数据动态展示:写api => Vuex调用api把数据放store => 组件拿到仓库store的数据 => 动态渲染
4、完成相应的动态业务逻辑
这里拆分组件比较简单,把中间这部分拆分出来,其他地方直接在Search组件中保留

流程:写api => Vuex调用api请求数据放store => 组件拿到仓库store的数据 => 动态渲染
这是向真正的服务器发请求,所以用封装好的axios实例:requests。带参数,且参数params至少有个默认值(空对象{})否则会请求失败。
src/api/index.js
//本文件用于:API的统一管理
import requests from './request'; //axios请求后台ajax数据
import mockRequests from './mockRequest'; //axios请求后台mock数据
........
........
//4.Search模块的接口
// /api/list post 需要带参数,且参数params至少有个默认值(空对象{})否则会请求失败
export const reqSearchInfo = function (params) {
return requests({
url: '/list',
method: 'post',
data: params //请求体参数
})
}
注意调用接口时要传参,默认值要设置为空对象,不然请求会失败的
src/store/search/index.js
//本文件用于配置search模块的数据
import { reqSearchInfo } from '@/api'
export const search = {
namespaced: true,
state: {
searchInfo: {}
},
actions: {
async getSearchInfo(context, params = {}) {
let result = await reqSearchInfo(params); //这里至少要传一个默认参数:空对象,否则请求不成功
if (result.code === 200) {
context.commit('GETSEARCHINFO', result.data);
}
}
},
mutations: {
GETSEARCHINFO(state, value) {
state.searchInfo = value;
}
},
getters: {}
}
观察数据,我们要拿到searchInfo中的attrsList,goodsList,trademarkList

对应的关系是这样滴

那么这样的话,如果用mapState来接收,实际上是很麻烦的:

所以这里我们选择在仓库中配置getters:
//getters类似计算属性,可以简化仓库中的数据
getters: {
//当前形参是当前仓库的state
goodsList(state) {
//要等searchInfo的数据拿回来了再返回值,要不然可能就直接拿空对象了
return state.searchInfo.goodsList || []; //如果没网返回空数组
},
attrsList(state) {
return state.searchInfo.attrsList || [];
},
trademarkList(state) {
return state.searchInfo.trademarkList || [];
},
}
注意这里如果没网的话请求不到数据,searchInfo就是空对象,再点儿什么什么会是undefined,这时候我们要加个或的条件,默认空数组,这样没网时遍历空数组就不会出问题
下面这里由于我开启了命名空间,所以从组件中读取getters数据是这样写的 ...mapGetters('search', ['goodsList', 'attrsList', 'trademarkList'])
图示:

v-for遍历数组,然后把对应的数据填进去就行了,这块儿没什么好说的


后端对数据处理完后,返回筛选后的响应体,然后仓库中拿着筛选后的响应体给state => state再给getters处理一下子 => 组件再用mapGetters接收 => 再把数据更新到页面
当用户点击搜索或三级联动的时候,需要根据关键字再发一次请求来获取相应的数据,而我们派发action请求数据的操作是放在Searchmounted挂载函数里面的,而我们在Search页再点搜索或三级联动时,mounted不会再执行了.
所以我们应该把派发actions请求的操作封装成函数,在需要时候调用。

观察api接口文档,发现向服务器发请求时可以带10个参数,我们将这些参数的默认值配置在Search组件的data中,以对象的形式存储,然后在派发actions请求时把这个对象传过去,就能够作为axios发送ajax请求的请求体参数,然后后台拿着这些传过来的参数进行一些筛选排序之类的操作(我猜的)

复习一个api:Object.assign(target, source),它有个功能是可以合并具有相同属性的对象,返回修改后的对象

用上面那个api,可以把我们点击三级联动或搜索时query和params参数拿过来(注意名字要一致),然后重名的属性相继覆盖。
这里要在mounted之前(created或beforeMount都行),因为要在派发请求之前拿到带三级联动或搜索带的query和params参数的searchParams,这样就可以将我们从三级联动或搜索获取到的参数覆盖掉初始带给服务器的参数

后面两个对象依次覆盖前面重复的属性值,最后第一个对象原值改变,返回第一个对象,这样就能把相应的参数带给后端,然后后端再一过滤啥的,就能实现搜索功能了
我们目前呢还是只发了一次请求,因为只在
mounted里面调用了getData(),但是我们必须实现点击三级联动或搜索就调用getData()(发送Ajax请求)
我们观察开发者工具,会发现其实跳转到路由组件后,组件身上的data中会有$route这个数据

每次用户点击三级联动或者搜索,都会触发push路由跳转和传参(如果已经跳了就只传参),那么我们在进入Search组件后,每次点击三级联动或者搜索都会带来$route中参数的变化,也就是$route的变化。
那么这样的话我们就可以去监视$route的变化,每次只要参数发生变化,就重新整理数据并派发请求,然后服务器根据传过去的参数做一些操作(筛选),然后把处理过的数据响应过来,就可以在页面展示了
watch: {
//$route竟然是data里的一个属性诶
//监听路由的信息是否发生变化,如果发生变化就再次请求
$route: {
immediate: true, //其实加上这句话,beforeMount和mounted里的东西都可以去掉
handler() {
//每次请求前,最好把相应的1,2,3级分类的id置空,不然有冗余不太好,本次请求就比较干净
//别的不用重置,因为这三个是只传一个,其他的会直接被新值覆盖,没必要重置
// Object.assign(this.searchParams, { category1Id: '' }, { category2Id: '' }, { category3Id: '' })
this.searchParams.category1Id = '';
this.searchParams.category2Id = '';
this.searchParams.category3Id = '';
//每次参数变化,都要重新整理数据然后派发请求才行
Object.assign(this.searchParams, this.$route.query, this.$route.params);
this.getData();
}
}
}
子组件用到的是getters中的attrsList和 trademarkList

这里和前面Floor是不同的,这里可以直接让子组件从仓库中拿数据,这是因为这个数据的结构是{ [], [], [] ...},而且我们已经通过getters把每个数组分出来了,且没有组件复用的情况,所以不用父传子(当然父传子也可以,但是没必要)

拿到数据之后v-for对应生成列表就行了,这块儿比较简单

这块儿挺多细节的,我感觉比较难,他喵的
所谓的面包屑其实就是这个东西:

我们可以根据用户请求的数据来决定是否展示面包屑,且删除某个面包屑时,数据要重新发起请求返回新的后台筛选数据放到页面上。
我们首先要解决面包屑的展示问题,如果携带的参数searchParams.categoryName为空,就是没有面包屑,有的话,面包屑就是这个参数的值(参数searchParams.categoryName是否有值是由路由跳转时是否传入categoryName决定的,是结合Object.assign)。所以这里可以用v-if来判断是否展示面包屑
<ul class="fl sui-tag">
<li class="with-x" v-if="searchParams.categoryName">
{{searchParams.categoryName}}<i @click="deleteCategory">×i>
li>
<li class="with-x" v-if="searchParams.keyword">
{{searchParams.keyword}}<i @click="deleteKeyword">×i>
li>
ul>
然后在❌的地方分别配置删除三级联动和搜索回调
注意:只有searchParams里的某属性为空时,才会返回该属性对应所有数据
1、首先名字置空(或undefined,如果属性值为""还是会把相应的字段带给服务器。但是你把相应的字段变为undefined,当前这个字段不会带给服务器-------性能更好,省带宽),这样的话面包屑先取消显示(v-if控制)
2、其次所有Id也要置空,这样删除面包屑后才能返回全部的服务器数据(之前监视$route时id置空却不会返回全部数据是因为categoryName和keyword没有置空,只要这几个不是全空,服务器就会做一些筛选并返回相关数据)当然其实一步也可以不写,因为下一步👇
3、自己跳自己,去掉地址栏中的query参数(三级导航的数据),但是要保留params参数(搜索框的数据),因为这个回调是点击三级联动的面包屑才触发的,如果有搜索条件还是要保留keyword带来的数据的。其实仔细想想这段代码的目的就是去掉地址栏中的参数(如果不考虑监视$route会自动派发的话)
4、派发请求返回数据。当然如果写了第三步,第二步和第四步都可以省略,因为我们之前监视了$route的变化,一旦变化就id置空=>重新整理数据=>派发请求
5、具体的细节见如下代码,里面注释好好琢磨吧
deleteCategory() {
//删除分类的名字和id,但置空的是本组件data内的数据,而不是$route中的query参数
this.searchParams.categoryName = ""; //点击x后,名字置空,这样v-if就生效把节点弄没
// this.searchParams.category1Id = undefined; //写成undefined就不会带给服务器了,省带宽
// this.searchParams.category2Id = undefined; //id也要置空,这样服务器才会返回全部数据
// this.searchParams.category3Id = '';
this.$router.push({ //去掉地址栏的query参数
name: 'sousuo', //自己跳自己
params: this.$route.params //只传params(华为),query就被拿掉了
});
// this.getData();//发请求返回全部数据,但是由于监视$route,有上边就不用这行了
},
和上边是类似的,一样的套路。这里有个需要注意的地方就是当我们去掉关键字对应的面包屑时,搜索框中的文字也要去除,所以这里我们要同步修改Header中的keyword
使用的是全局事件总线:复习全局事件总线

剩下的就是:
1、首先关键词置空(或undefined),这样的话面包屑先取消显示(v-if控制)
2、自己跳自己,去掉地址栏中的params参数(搜索关键字的数据),但是要保留query参数(三级联动的数据),因为这个回调是点击关键字的面包屑才触发的,如果有三级联动条件还是要保留categoryName带来的数据的。
3、派发请求返回数据。当然如果写了第二步,这一步可以省略,因为我们之前监视了$route的变化,一旦变化就重新整理数据=>派发请求
4、具体的细节见如下代码,里面注释好好琢磨吧
deleteKeyword() {
//删除关键词,但置空的是本组件data内的数据,而不是$route中的params参数
this.searchParams.keyword = undefined;//点击x后,关键字置空,这样v-if就生效把节点弄没
this.$router.push({ //去掉地址栏的params参数
name: 'sousuo',
query: this.$route.query //只传query(有就筛选,没有就返回全部数据),params(华为)就被拿掉了
});
this.$bus.$emit('deleteKeyword'); //触发全局事件总线,把Header中的输入框关键字置空
// this.getData(); //发请求返回全部数据,但是由于监视$route,有上边就不用这行了
}
点击品牌信息出现面包屑,并且更新页面数据

观察:
1、这个品牌信息是在Search的子组件SearchSelector中的,而且是v-for遍历生成的。
2、后台接口可以接收一个trademark参数,用来进行筛选操作
3、trademark等参数我们是配置在Search组件中的,并以一个完整对象的方式通过dispatch发送给仓库,仓库再异步请求数据拿到响应结果
结论:我们应该把子组件SearchSelector中的数据传给父组件Search,父组件就可以拿着数据修改data中传给后台的参数,再发送请求时就会把trademark参数带给服务器了。
1、父组件中给子组件标签添加自定义事件getTrademark:
<SearchSelector @getTrademark="getTrademark" />
2、子组件通过点击事件触发自定义事件并把当前trademark传过去

3、这样父组件就拿到了数据
注意:只有searchParams里的某属性为空时,才会返回该属性对应所有数据
拿到数据之后按照api接口的格式调整数据给data中的searchParams.trademark,然后发送请求传给服务器。
getTrademark(trademark) {
//整理品牌字段参数 按照接口文档格式写"ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`;
//要记住,只有searchParams里的某属性为空时,才会返回该属性对应所有数据
//所以下面这么写有bug是因为keyword置空了,但this.searchParams.trademark没有置空
// this.$router.push({
// name: 'sousuo',
// params: {keyword: trademark.tmName}
// });
this.getData();
}
这里我本来想直接把品牌名给keyword,这样触发params参数的改变,从而$route监测到就会发请求更新数据,但是这样会有bug,就是关闭面包屑时品牌信息不会重置(只剩下当前一个品牌信息)。这是因为只有searchParams里的某属性为空时,才会返回该属性对应所有数据(相当于重置数据),这里把品牌名字放入keyword,然后关闭面包屑时只把keyword置空了,但是this.searchParams.trademark没有置空,这样的话后台还是会根据trademark去筛选……所以正确的写法应该是和keyword,categoryName差不多的写法,就是点击❌时把trademark置空:
<li class="with-x" v-if="searchParams.trademark">
{{searchParams.trademark.split(':')[1]}}<i @click="deleteTrademark">×i>
li>
deleteTrademark() {
//删除品牌的面包屑并重新请求
this.searchParams.trademark = undefined;
this.getData(); //这里边不涉及$route里的参数修改,所以不能靠监视来派发请求
},
点击某个属性就会显示响应的面包屑,并发送相对应的数据给后台发请求,拿到新数据

大致的思路和上面的品牌信息面包屑是类似的,只是也有一些不同点。
1、先在父组件的子组件标签处写好自定义事件和它的回调
<SearchSelector @getTrademark="getTrademark" @getAttribute="getAttribute" />
......
<script>
......
getAttribute() {
//回调函数体
}
script>
2、观察接口文档

观察接口文档,发现我们需要拿到的数据在以下位置,我们要拿到它们并整理成示例的格式

3、去子组件传值并触发自定义事件。根据上一步的分析,我们应该把a1(每个大对象)和a2(每个attrValueList中的值)都传给父组件,方便它整理数据。并给每个a2绑定点击事件


4、回调中触发自定义事件并传值:

5、父组件自定义事件的回调中接收值并整理数据
getAttribute(attr, attrValue) {
let prop = `${attr.attrId}:${attrValue}:${attr.attrName}`; //整理成api文档规定的格式
......
}
分析:
1、props的参数格式默认为一个数组,需要存储多个属性元素
2、由于重复点击某个元素会重复添加,所以我们在往数组添加元素时应该进行重复判断
3、每次点击一个属性,都应该生成对应的面包屑且发送相应的请求
4、每次删除一个属性,都应该删除对应的面包屑且再次拿着props的剩余数据发送请求
通过以上分析,我们可以得出:
1、props的参数格式默认为一个数组,需要存储多个属性元素,所以在添加数据时应该使用数组的push方法。
2、在添加时判断数组内有没有该元素即可,有就不添加,没有就添加,使用indexOf
所以父组件的自定义事件回调应该这么写:
getAttribute(attr, attrValue) {
let prop = `${attr.attrId}:${attrValue}:${attr.attrName}`;
if (this.searchParams.props.indexOf(prop) === -1) {
//加个判断:解决重复点击会重复显示多个面包屑的bug:props数组去重
//如果props里没有该元素,就添加进去并发请求,如果已经有了就不发请求了
this.searchParams.props.push(prop);
this.getData();
}
}
3、每次点击一个属性,都应该生成对应的面包屑且发送相应的请求,由于数组内有多个元素,页面生成面包屑不能再使用v-if,而使用v-for,页面展示时通过split方法把字符串拆开拿到那个a2的值

4、每次删除一个属性,都应该删除对应的面包屑且再次拿着props的剩余数据发送请求,这里要删除数组中被点击的元素,我们用的方法是把index传过去,并使用数组的splice方法删除该元素,然后再次发送请求,这样就欧了
deleteProps(index) {
//点击叉号时删除当前数组中的元素
this.searchParams.props.splice(index, 1);//(start,deletecount),改变原数组
this.getData();
},
需求:默认综合高亮降序,如果点击综合改变排序方式,点击价格则价格高亮,再点击价格改变排序方式:

查阅接口文档,后台默认收到的参数order格式为:'排序字段 : 排序方式',其中1综合,2价格 asc升序 desc降序,如order: '1:desc'
高亮样式是否展示取决于当前order中的排序字段是1还是2,如果是1那么综合有active红色高亮样式,如果是2那么价格有active红色高亮样式,所以我们可以用计算属性来决定样式的展示

//决定排序属性是否高亮的两个计算属性
isOne() {
//如果order中包含1,那么就是当前按综合排序,返回true给avtive样式
return this.searchParams.order.includes('1');
},
isTwo() {
//如果order中包含2,那么就是当前按价格排序,返回true给avtive样式
return this.searchParams.order.includes('2');
},
1、箭头是否展示是和高亮同步的,如果高亮都没有,那么箭头肯定不展示,所以箭头这个标签外边要加上v-show,值和高亮的那个一致

2、箭头是升序还是降序取决于当前order中是asc还是desc,如果是asc那么上箭头显示,如果是desc就是下箭头展示。所以箭头这里的展示也要通过计算属性,方法是判断当前order中是否包含asc或desc
去阿里图标库找图标(操作教程),字体图标样式在
public/index.html文件中引入

//决定排序箭头显示上箭头还是下箭头
isDown() {
//如果order中包含desc,返回true,否则返回false
return this.searchParams.order.includes('desc');
},
isUp() {
//如果order中包含asc,返回true,否则返回false
return this.searchParams.order.includes('asc');
}
通过以上分析我们发现,不管是高亮还是箭头是否展示,还是箭头的方向,都是由order中的数据决定的,只要data中的数据searchParams.order改变,Vue就会重新解析模板,就会影响这些东西的显示效果。所以我们操作数据并再次发送请求,就能够实现高亮、箭头、商品数据的同步。
1、绑定点击事件并传参用来表示当前点击的是哪个板块儿

2、把传过来的参数结合排序类型取反直接给 this.searchParams.order重新赋值(通过模板字符串和三元表达式),然后再发请求就行了。这里老师写的复杂了,但其实直接改数据就行了,前面我们说了,数据改变Vue就会重新解析模板的。
changeOrder(orderNumber) {
//orderNumber是一个标记,代表用户点击的是综合(1)还是价格(2),用户点击时传过来
//1.获取当前状态的排序类型
let originOrderType = this.searchParams.order.split(':')[1];
//2.直接改数据,传入当前点击的参数,排序类型取反,然后就能引起Vue重新解析模板
this.searchParams.order = `${orderNumber}:${originOrderType === 'desc' ? 'asc' : 'desc'}`;
//3.再次发送请求
this.getData();
}
分页器因为好多地方都在用,所以我们将其封装为全局组件,步骤为创建引入注册使用

src/components/Pagination/index.vue
<template>
<div class="pagination">
<button>上一页button>
<button>1button>
<button>···button>
<button>3button>
<button>4button>
<button>5button>
<button>6button>
<button>7button>
<button>···button>
<button>9button>
<button>下一页button>
<button style="margin-left: 30px">共 60 条button>
div>
template>
<script>
export default {
name: 'Pagination',
};
script>
<style lang="less" scoped>
.pagination {
text-align: center;
button {
margin: 0 5px;
background-color: #f4f4f5;
color: #606266;
outline: none;
border-radius: 2px;
padding: 0 4px;
vertical-align: top;
display: inline-block;
font-size: 13px;
min-width: 35.5px;
height: 28px;
line-height: 28px;
cursor: pointer;
box-sizing: border-box;
text-align: center;
border: 0;
&[disabled] {
color: #c0c4cc;
cursor: not-allowed;
}
&.active {
cursor: not-allowed;
background-color: #409eff;
color: #fff;
}
}
}
style>

1、需要知道当前是第几页:
pageNo字段代表当前页数2、需要知道每一页需要展示多少条数据:
pageSize字段进行代表3、需要知道整个分页器一共有多少条数据:
total字段进行代表----【通过和每页放几个能计算获取另外一条数据:一共有几页】4、需要知道分页器连续页码个数:
continues字段进行代表 5 | 7 【为什么是奇数?因为对称好看】
分页器在开发的时候先自己传递假的数据进行调试,调试成功后再用服务器数据
<div class="fr page">
分页器子组件,用props传数据给子组件,先用一些写死的数据用来调试逻辑
<Pagination :pageNo="27" :pageSize="3" :total="91" :continues="5" />
div>
src/components/Pagination/index.vue
name: 'Pagination',
[当前页码,每页数据个数,总数据个数,连续页的个数]
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
totalPages() {
//小学数学,计算总页数,向上取整
return Math.ceil(this.total / this.pageSize);
},
}
经分析不难发现,首页和尾页都是固定的,但是连续页会不断变化,我们如果要展示连续页的内容,需要拿到连续页的首页start和尾页end,这样使用v-for遍历end生成1~end的全部按钮,再使用v-if把所有start之前的按钮删掉,连续页就表示出来了。所以我们首先要写一个计算属性来计算连续页的首尾数字
computed: {
totalPages() {
//小学数学,计算总页数,向上取整
return Math.ceil(this.total / this.pageSize);
},
//计算连续页的开始和结束页码,方便后续展示
startAndEndNumber() {
let start = 0, end = 0;
//1.如果总页数小于连续的页码,那么start应该是1,end应该是总页数
//比如一共就4页,而连续页是5页
if (this.totalPages < this.continues) {
start = 1;
end = this.total;
}
//2.如果总页数>=连续页码数,就属于正常且复杂的现象了
else {
// 1 ... 5 6 7 8 9 ... 30
start = this.pageNo - Math.floor(this.continues / 2); //7-2
end = this.pageNo + Math.floor(this.continues / 2); //7+2
//bug1:当前页是1,start计算成负数或0
if (start < 1) {
start = 1; //如果到最左边的,start就应该是1
end = this.continues;
}
//bug2:当前页是尾页,end计算成比尾页还大的数
if (end > this.totalPages) {
start = this.totalPages - this.continues + 1;
end = this.totalPages; //如果到最右边,end就应该是总页数
}
}
let numObj = { "start": start, end: end };
return numObj; //这个计算属性的值就是一个带有start和end的对象
}
}
其实这段代码并不难,仔细看看注释,你可以的,you got it
使用v-for遍历end生成1~end的全部按钮,再使用v-if把所有start之前的按钮删掉,连续页就表示出来了。

先说第一页这部分,连续页展示时如果当前页过于靠前,可能就会出现这样的情况:

这样很明显不合理,我们应该给第一页和它旁边的省略号添加v-if条件,在合适的时候显示。比较好的条件是:当连续页的首页start>1时显示第一页;当连续页的首页start>2时显示省略号。

当然,尾页这部分和它的省略号也是类似原理,连续页展示时如果当前页过于靠后,可能就会出现这样的情况:

这样明显不合理,我们应该给尾页和它旁边的省略号加v-if

我们去给分页器子组件传值的Search组件中把向后台请求数据的真实数据拿过来,方便实现响应式。pageNo和pageSize是服务器本来就接收的参数,continues先写5
<Pagination :pageNo="searchParams.pageNo"
:pageSize="searchParams.pageSize"
:total="total"
:continues="5"/>
而total是从仓库里捞过来的数据,我们可以用getters把它搞过来


这样的话页面中用到的数据就和我们要拿着发请求的数据同步了,分页器可以实现响应式
这里是子给父传值用到组件自定义事件
1、先在父组件中给分页器子组件绑定

需要传参的话父组件中绑定自定义事件不需要带(),传过来的参数会给它的回调
2、在分页器全局组件中传值吧,挨个儿传

3、然后父组件接过来把值传给服务器,然后发请求就欧了,这样在实现分页器响应式的基础上也实现了页面商品数据的同步。

使用disabled禁用上一页、下一页、省略号


分页器部分模板最终代码:
<template>
<div class="pagination">
<button :disabled="pageNo === 1" @click="$emit('getPageNo', pageNo-1)">上一页button>
<button v-if="startAndEndNumber.start > 1" @click="$emit('getPageNo', 1)">1button>
<button v-if="startAndEndNumber.start > 2" disabled>···button>
<button v-for="(n,index) in startAndEndNumber.end" :key="index" v-if="n>=startAndEndNumber.start"
@click="$emit('getPageNo', n)" :class="{active:pageNo === n}">{{n}}button>
<button v-if="startAndEndNumber.end < totalPages-1" disabled>···button>
<button v-if="startAndEndNumber.end < totalPages"
@click="$emit('getPageNo', totalPages)">{{totalPages}}button>
<button :disabled="pageNo === totalPages" @click="$emit('getPageNo', pageNo+1)">下一页button>
<button style="margin-left: 30px">共 {{total}} 条button>
div>
template>
Search搜索页完结,对于我这个小白来说挺难的说实话,但是奥里给!!