文章目录
- 一、Search模块
- 1、Search模块的api
- 2、Vuex保存数据
- 3、组件获取vuex数据并渲染
- (1)、分析请求数据的数据结构
- (2)、getters简化数据、渲染页面
- 4、Search模块根据不同的参数获取数据
- (1)、 派发actions的操作封装为函数
- (2)、设置带给服务器的参数
- (3)、Object.assign整理参数值(该方法在JS高级的浅拷贝中提到过)
- (4)、监听路由以实现根据不同参数 多次发送请求(有意思)
- 5、SearchSelector读取动态数据
- 二、面包屑
- 1、展示面包屑(关键词+分类名)
- 2、面包屑删除事件(关键词+分类名)
- (1) 删除分类面包屑 (Search页面通过路由跳自己)
- (2) 删除关键词面包屑
- 3、品牌名的面包屑
- (1)、添加品牌面包屑--自定义事件
- (2)、删除品牌面包屑
- 4、售卖属性的面包屑
- (1)、添加售卖属性面包屑
- (2)、删除售卖属性面包屑
- 三、排序
- 1、order属性
- 2、高亮设置:绑定class样式
- 3、添加箭头
- 4、点击改变高亮和箭头升降
- 四、分页器 (重重重点)
- 1. 分页器参数
- 2. 连续页码的起始数字与结束数字
- (1)、计算总页数
- (2)、连续页码的起始数字和结束数字
- (3)、页码展示(省略号等什么时候展示)(好好琢磨)
- 3. 分页器绑定动态数据
- 4. 点击页码获取新的数据
- 5. 上一页和下一页按钮禁用
- 6. 当前页的页码高亮
一、Search模块
模块开发的几个步骤
- 静态页面+静态组件拆分
- 发请求(API)
- 数据保存到vuex中(配置好vuex中的actions、mutations、state)
- 组件获取vuex动态数据,渲染页面
1、Search模块的api
注意传递的参数至少应该是一个空对象,否则请求会出错。
// src/api/index.js 本文件对于API接口进行统一管理
import requests from './request'
import mockRequests from './mockRequest'
...
// 搜索模块数据,地址:/api/list 请求方式:post, 参数:需要带参数
// 当前这个接口,给服务器传递参数params,params至少应该是一个空对象,否则请求会出错
// 即: reqSearchInfo()----reqSearchInfo({})------不会出错
export const reqSearchInfo = (params) => {return requests({ url: '/list', method: 'post', data: params })
}
在main.js中测试该api:
import { reqSearchInfo } from './api'
reqSearchInfo({})
得到的返回信息为:
2、Vuex保存数据
获取的是Search模块的数据,所以应该保存在search小仓库中:
// src/store/search/index.js
import { reqSearchInfo } from '@/api'
export default {namespaced: true,state: {searchList: {} // 仓库初始状态},actions: {async getSearchInfo (context, param = {}) {const result = await reqSearchInfo(param)if (result.code === 200) {console.log(result.data);context.commit('GETSEARCHINFO', result.data)}}},mutations: {GETSEARCHINFO (state, searchList) {state.searchList = searchList}},getters: {...}
3、组件获取vuex数据并渲染
(1)、分析请求数据的数据结构
对应在页面上:
如果Search组件通过mapState读取这些数据(麻烦且易出错):
...mapState('search', {goodsList: state => state.search.searchList.goodsList,attrsList: state => state.search.searchList.attrsList,trademarkList: state => state.search.searchList.trademarkList
})
(2)、getters简化数据、渲染页面
然后将数据渲染到页面即可,没什么重要的点。
4、Search模块根据不同的参数获取数据
(1)、 派发actions的操作封装为函数
目前派发actions的操作放在mounted里,组件挂载完毕会执行一次,但也只能发送一次。当搜索的参数发生变化时,应该再次发送请求,所以应该将请求封装成一个函数。
Search/index.vue
(2)、设置带给服务器的参数
之前写Search模块的api时,为了测试,参数传的是空对象。此处要配置参数具体的值。
观察api接口文档,发现向服务器发请求时可以带10个参数。我们将这些参数的默认值配置在Search组件的data中,以对象的形式存储,然后在派发actions请求时把这个对象传过去,就能够作为axios发送ajax请求的请求体参数。(这段话的原文链接:https://blog.csdn.net/weixin_42044763/article/details/126817322)
(3)、Object.assign整理参数值(该方法在JS高级的浅拷贝中提到过)
从首页的三级联动跳转到Search页面时,路由携带了query参数(categoryId与categoryName)。
Header组件搜索关键词跳转到Search页面时,路由携带了params参数(keyword)。
Search组件在挂载完时(mounted)就要发送一次请求获取页面数据以渲染页面。在发送请求之前,应该将携带的参数带给服务器。
// 封装参数,其实在mounted里也可以,这里是为了回顾一下生命周期函数beforeMount () {// 复杂this.searchParams.category1Id = this.$route.query.category1Idthis.searchParams.category2Id = this.$route.query.category2Idthis.searchParams.category3Id = this.$route.query.category3Idthis.searchParams.categoryName = this.$route.query.categoryName},mounted () {// 在这里整理参数也可以,只要在发送请求之前整理好即可this.getData()},
这种整理参数的方式也可以,但比较复杂,不是最优。采用Object.assign
优化一下。
Object.assign(target, source)
:可以合并具有相同属性的对象,返回修改后的对象。
beforeMount () {Object.assign(this.searchParams, this.$route.query, this.$route.params)},mounted () {// 在这里整理参数也可以,只要在发送请求之前整理好即可this.getData()}
至此实现了:从其他页面跳转到Search页面时,Search组件会根据已有的参数值向服务器请求对应的数据。渲染到页面上。但是当输入关键字或进行其他操作时,应该根据参数值再次发起请求,获取数据。
(4)、监听路由以实现根据不同参数 多次发送请求(有意思)
当路由发生变化时,说明发生了路由跳转,
5、SearchSelector读取动态数据
数据在进入Search页面时都请求到了,并且在Vuex中也用过getter简化了,所以这里直接从Vuex中取数据即可。不用父子组件传值。
SearchSelector.vue
这里用slice是因为返回的一些数据是测试数据,不好看,就不展示了。
二、面包屑
1、展示面包屑(关键词+分类名)
面包屑的值包括好几类,先看这两类:三级联动里的分类名,搜索输入框里的关键字。
可通过查看SearchParams
里是否包含分类名或者关键字来判断是否显示面包屑。
为什么SearchParams
里会包含这两个信息呢?在Search组件挂载完成(mounted)或者再次发送请求信息时,用户搜索的参数已整合到SearchParams
里了。
<!--Search.vue--><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>
2、面包屑删除事件(关键词+分类名)
点击面包屑的叉号,删除面包屑。删除面包屑相当于改变了用户搜索的条件,所以需要重新发送请求。上边的代码已经分别给这两类面包屑添加了点击事件。
(1) 删除分类面包屑 (Search页面通过路由跳自己)
首先是让控制面包屑的v-if
为false,即categoryName
值为空。
其次,由于删除了面包屑,说明用户的搜索不包含这个分类了,则需要重新发送一次请求。为了获取全部的数据,对应的对应的categoryId也需要清空。
小Tips:参数值为null时这些参数还会发给服务器,值为undefined时,这些参数就不会发送给服务器了。(这是视频里说的,但是我自己试的时候还是都会发给服务器)
// 删除分类面包屑deleteCategory () {// 1. categoryName置空,以让v-if的值为false,不显示面包屑this.searchParams.categoryName = ''// 2. 对应的categoryId也需要清空。但由于不清楚是哪个分类Id,就全都置空// 清空可以赋值为null,也可赋值为undefined。//由于删除了面包屑,说明再次请求时不需要包含这些条件了,所以发送请求可以不携带这些参数,以减少资源的消耗。this.searchParams.category1Id = undefinedthis.searchParams.category2Id = undefinedthis.searchParams.category3Id = undefined// 重新发送请求this.getData()},
此时页面变化了,但是地址栏仍旧是
http://localhost:8080/#/search/华为?categoryName=手机&category3Id=61
此时需要去掉地址栏中的query
参数(三级联动分类),保留params
参数(关键词)。地址栏的内容是路由跳转时形成的。所以发送请求之后,需要再次进行路由跳转。上面的代码改为:
// 删除分类面包屑deleteCategory () {// 1. categoryName置空,以让v-if的值为false,不显示面包屑this.searchParams.categoryName = undefined// 2. 跳转路由,自己跳自己this.$router.push({ name: 'search', params: this.$route.params })},
(1)、为什么不调用getData
方法了
之前写过一个监视路由,当路由发生变化时,重新发送请求。这里为了改变地址栏的内容,重新进行了路由跳转。相比原来的路由,此次路由跳转里取消了query
参数,所以路由发生了变化,也会触发watch里的代码,watch里已经由重新发送请求了,这里就不用写了。
(2)categoryId
怎么不置空了。
(2) 删除关键词面包屑
删除流程为:
- 点击×号、
- 清除面包屑、
- 输入框里的关键词清空、
- 重新发送请求。
关键词清空涉及到了兄弟组件Header
,这里采用全局事件总线(vue(九)全局事件总线):
安装全局事件总线:
new Vue({// KV一致时省略V[router小写的r]router,store,render: h => h(App),beforeCreate () {// 全局事件总线Vue.prototype.$bus = this}
}).$mount('#app')
接收消息的组件(Header)绑定事件,发送消息的组件(Search)触发事件:
Search组件删除关键词的回调函数:
// 删除关键词面包屑
deleteKeyWord () {// 1. 关键词置空,让v-if值为falsethis.searchParams.keyWord = '' // 兄弟组件Header组件的输入框清空--触发事件this.$bus.$emit('clear')// 改变路由地址,进行路由跳转,如果还有query参数则携带query参数this.$router.push({ name: 'search', query: this.$route.query })// this.getData() // 重新发送请求,上边那行触发了对路由的监视,在监视里也会重新发送请求,所以写了上面那行就不用写这行了。
},
同样的,改变路由地址就会被watch监视到路由发生变化,进而重新整理参数、发送请求。
3、品牌名的面包屑
点击品牌名,生成品牌名的面包屑,配置品牌名的参数,重新发送请求
1、品牌信息是在子组件
SearchSelector
里。SearchSelector
通过mapGetter
获取仓库里的品牌列表信息trademarkList
。页面上通过v-for
循环渲染了品牌信息。
2、发送请求时,需要将品牌信息作为参数带给服务器。问题是这个请求应该子组件SearchSelector
发,还是父组件Search
发
答:应该是父组件发。携带给服务器的参数都整理在SearchParams
里了,而这个属性在父组件里。所以应该子给父传递品牌信息参数,然后由父组件发请求。
(1)、添加品牌面包屑–自定义事件
页面结构:
自定义事件
父组件的回调函数为:
// 自定义事件,获取品牌信息trademarkInfo (trademark) {// 为什么要这么拼接字符串,接口要求: "ID:名称" this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`// 2. 发送请求以获取search模块列表数据进行展示this.getData()},
(2)、删除品牌面包屑
绑定点击事件,具体见上边给出的页面结构截图
// 回调函数deleteTrademark () {// 1. 置空信息,让v-if的值为false,取消显示面包屑this.searchParams.trademark = ''// 2. 再次发送请求this.getData()},
4、售卖属性的面包屑
(1)、添加售卖属性面包屑
页面结构:
思路和上边的品牌面包屑差不多。根据接口要求来确定子组件向父组件要传递什么值。
仍旧采用自定义事件实现子传父:
SearchSelector.vue
:
attrHandler (attr, attrValue) {// 传递给父组件,触发事件,传参数this.$emit('attrInfo', attr, attrValue)}
父组件SearchInfo
:
<!--绑定自定义事件-->
<SearchSelector @trademarkInfo="trademarkInfo" @attrInfo="attrInfo" />
<script>// 自定义事件-属性attrInfo (attr, attrValue) {// 售卖属性["属性ID:属性值:属性名"]let prop = `${attr.attrId}:${attrValue}:${attr.attrName}`//判断是否已存在此属性,如果存在,也不会再添加到props里if (this.searchParams.props.indexOf(prop) === -1) {this.searchParams.props.push(prop)}// 2. 发送请求以获取search模块列表数据进行展示this.getData()},
</script>
与品牌面包屑不同的是,SearchParams
里的售卖属性props
是个数组,因此点击售卖属性时,需要判断数组中是否已有该属性
(2)、删除售卖属性面包屑
就是从数据里删除这个属性。没别的重要的点。
deleteAttr (index) {// 删除属性this.searchParams.props.splice(index, 1)// 重新发送请求this.getData()},
三、排序
1、order属性
1
:表示按综合排序
2
:表示按价格排序
desc
:降序
asc
:增序
2、高亮设置:绑定class样式
综合标签高亮还是价格标签高亮,取决于当前的order
属性值。如果order
里是1,则综合标签高亮,如果是2,则价格标签高亮。此处采用计算属性来动态绑定class样式。
computed: {isOne () {// 等于-1说明order里没有1return this.searchParams.order.indexOf('1') !== -1 },isTwo () {return this.searchParams.order.indexOf('2') !== -1}}
3、添加箭头
如何判断谁应该有箭头?谁高亮谁就有箭头显示。我们用一个span标签来包裹箭头。用v-show
来控制。当前标签高亮时,就显示箭头,否则就不显示。所以箭头与高亮的显示是同步的,那就也用计算属性来控制。
此处箭头的样式采用阿里巴巴矢量图(具体步骤看博客:Vue中使用iconfont-阿里巴巴矢量图标库)。箭头是升还是降还是取决于order
里的值,所以还是通过计算属性来控制
computed(){// 升序isAsc () {return this.searchParams.order.indexOf('asc') !== -1},// 降序isDesc () {return this.searchParams.order.indexOf('desc') !== -1},
}
采用动态绑定class样式来显示上升的箭头或下降的箭头
4、点击改变高亮和箭头升降
这里的需求是:
(1) 如果当前的高亮标签是综合:
点击综合标签,该标签的排序方式改变(由升变降或由降变升);
点击价格 标签,则改为价格标签高亮,且排序方式改为默认的降序desc。
(2) 如果当前的高亮标签是价格,则同理。
根据不同的排序方式重新发送请求,获取数据。
changeOrder (orderNum) {console.log('点的是', orderNum);// 记录原本的排序let originOrder = this.searchParams.orderlet originNum = originOrder.split(':')[0] // 选的是综合还是价格let originType = originOrder.split(':')[1] // 增序还是降序// 改变if (originNum == orderNum) { // 类别没变,则只变排序方式this.searchParams.order = `${orderNum}:${originType === 'desc' ? 'asc' : 'desc'}`} else { // 类别变了,默认的还是降序this.searchParams.order = `${orderNum}:desc`}//再次发送请求 this.getData()}
四、分页器 (重重重点)
分页器在很多地方都用的到,封装为全局组件
// src/components/Pagination/index/vue
<template><div class="pagination"><button>上一页</button><button>1</button><button>···</button><button>3</button><button>4</button><button>5</button><button>6</button><button>7</button><button>···</button><button>9</button><button>上一页</button><button style="margin-left: 30px">共 60 条</button></div>
</template><script>export default {name: "Pagination",}
</script><style lang="less" scoped>.pagination {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>
main.js
目前是在Search组件中用到了。
1. 分页器参数
展示分页器,至少应该有以下几项数据
pageNo
:当前是第几页pageSize
:每一页展示多少条数据total
:一共多少条数据continues
:分页器连续的页码数。一般为5或7(奇数),因为这样对称好看。以下图为例,这样就是continues值为5的情况。
这几个属性一般由父组件给子组件传过去
<!-- Search.vue 组件 -->
<Pagination :pageNo="27" :pageSize="3" :total="91" :continues="5" />
子组件接收
// 当前页码,每页多少数据,总共多少条数据,分页器的连续页码数props: ["pageNo", "pageSize", "total", "continues"]
2. 连续页码的起始数字与结束数字
自定义的分页器要先用假数据进行调试,等调试好了,再换成真数据
(1)、计算总页数
computed(){// 总共多少页totalPage () {// 总数/每页的数据条数return Math.ceil(this.total / this.pageSize) //向上取整},
}
(2)、连续页码的起始数字和结束数字
computed(){
// 计算出连续页码的起始数字和结束数字startNumAndEndNum () {let start = 0;let end = 0// 1. 如果总页数 < 连续页码数, 则展示所有的页数if (this.totalPage < this.continues) {start = 1end = this.totalPage} else {//2. 如果总页数 >= 连续页码数 分不同的情况讨论//2.1 正常情况,假设连续页码数是5,pageNo是6,则这部分连续页码是4 5 6 7 8start = this.pageNo - Math.floor(this.continues / 2) // 6-(5/2) end = this.pageNo + Math.floor(this.continues / 2) // 6-(5/2) // 2.2 如果上边计算出来,start是负数,说明pageNo比较靠前,那么就展示前几页 if (start <= 0) {start = 1end = this.continues}// 如果尾页数超过总页数,说明pageNo很靠后,那就将最后continues页展示出来if (end > this.totalPage) {start = this.totalPage - this.continues + 1end = this.totalPage}}let numObj = { start, end }return numObj}
}
(3)、页码展示(省略号等什么时候展示)(好好琢磨)
整个分页器可分为三个区域:
1、中间部分
已知连续页码的起始数字和结束数字,所以考虑采用循环生成button。
老师的写法:
<button v-for="page in startNumAndEndNum.end" :key="page" v-if="page >= startNumAndEndNum.start">{{ page }}</button>
v-for也可以用来遍历数字。假设这里startNumAndEndNum.end
的值是10, startNumAndEndNum.start
的值是6。v-for遍历数字10,生成的是值为1~ 10的10个button。而1~5是不需要的,所以有了if判断。但是我自己写的时候,vue提示不允许v-for与v-if同时使用。所以采取下面这种做法。
<button v-for="page in middlePage" :key="page">{{ page }}</button><script>// 中间页码middlePage () {let start = this.startNumAndEndNum.startlet end = this.startNumAndEndNum.end// 构造一个array数组let array = []for (let index = start; index <= end; index++) {array.push(index)}return array}</script>
就是手动生成连续页码这个数组,然后在html里遍历。目前想不到别的方法,有好的想法可以评论区交流一下。
2、页码前半部分
为了避免这样的bug:
-
当
startNumAndEndNum.start
为1时,前边的1不应该展示
-
当
startNumAndEndNum.start
为1或2时,前边的...
按钮也不该展示:
于是进行了这样的v-if
判断<button>上一页</button><button v-if="startNumAndEndNum.start > 1">1</button><button v-if="startNumAndEndNum.start > 2">···</button>
3、页码后半部分
和上边同样的问题
如果startNumAndEndNum.end
的值是总页数totalPage
,就会出现:
如果startNumAndEndNum.end
的值是总页数totalPage
-1,就会出现:
同样加了v-if
判断
3. 分页器绑定动态数据
父组件给子组件传递的当前页pageNo
等数据需要换成真实值
4. 点击页码获取新的数据
和之前一样,服务器携带的参数SearchParams
在组件Search
里,所以此处需要分页子组件将数据传递给父组件。采用自定义事件的方式。
父组件Search
绑定自定义事件:
回调函数为:
// 得到页码 getPageNo (pageNo) {// 整理参数带给服务器this.searchParams.pageNo = pageNo// 重新发送请求this.getData()}
子组件Pagination
:
5. 上一页和下一页按钮禁用
6. 当前页的页码高亮
只给这部分的页码加高亮是因为:比如上半部分的页码,当开始页面是1是,这个1的页码按钮来自于中间部分的循环遍历,而不是上半部分中的那个1(v-if条件不满足,被隐藏了)。同理,下半部分的页码也不用加高亮
Pagination
页面结构的完整代码:
<template><div class="pagination"><!-- 上半部分 --><button @click="$emit('getPageNo', pageNo - 1)" :disabled="pageNo == 1">上一页</button><button v-if="startNumAndEndNum.start > 1" @click="$emit('getPageNo', 1)">1</button><button v-if="startNumAndEndNum.start > 2">···</button><!-- 中间 --><buttonv-for="page in middlePage":key="page"@click="$emit('getPageNo', page)":class="{ active: pageNo == page }">{{ page }}</button><!-- 下半部分 --><button v-if="startNumAndEndNum.end < totalPage - 1">···</button><buttonv-if="startNumAndEndNum.end < totalPage"@click="$emit('getPageNo', totalPage)">{{ totalPage }}</button><button@click="$emit('getPageNo', pageNo + 1)":disabled="pageNo == totalPage">下一页</button><button style="margin-left: 30px">共 {{ total }}条</button></div>
</template>