目录
(一)search模块界面搭建
1.三级联动导航的隐藏和出现
在search模块中一级菜单是隐藏的,鼠标进入全部商品分类标签时出现;
思路:使用v-show隐藏一级菜单,创建一个isShow数据存储是否显示的布尔值,默认为真;每次search组件加载时判断路由路径是不是home,不是则隐藏一级菜单。
typeNav组件中:
data() {
return {
isShow: true, //一级菜单是否显示
}
},
mounted() {
// 在search组件隐藏一级菜单
if (this.$route.path != '/home') {
this.isShow = false
}
},
methods:{
// 从全部商品分类标签中进入一级菜单(对于home组件来说这一步无所谓)
enterList() {
if (this.$route.path != '/home') {
this.isShow = true
}
},
}
再给进入全部商品分类的标签显示一级菜单的操作加一个过渡
// 过渡动画 注意:要与sort同级才有效果
.sort-enter {
height: 0;
opacity: 0;
}
.sort-enter-to {
height: 461px;
opacity: 1;
}
.sort-enter-active {
transition: all .1s linear;
}
2.合并参数
两种情形:输入关键字后再点击分类;点击分类后再输入关键字;
关键字保存在params参数中,分类信息保存在query参数中,如果不合并参数,在输入关键字之后再点击分类,会导致params参数消失;使用合并参数可以将query和params都保存起来
[1]输入关键字后再点击分类
在typeNav组件中:
methods:{
// 三级联动列表 确定和发送点击对象的名字和id 以及合并query和params参数
goSearch(event) {
// 点击列表传输query数据
// 存在自定义事件则不为空
let { categoryname, categoryid1, categoryid2, categoryid3 } = event.target.dataset
// 判断点击事件是否为标签
if (categoryname) {
const query = { categoryName: categoryname }
if (categoryid1) { ... } else if (categoryid2) { ... } else { ...}
// 编程式导航路由跳转 并传送信息
const location = {
name: 'search',
query,
}
// 当params参数为真时 合并参数
if (this.$route.params) {
location.params = this.$route.params
this.$router.push(location)
}
}
},
}
[2]点击分类后再输入关键字
在header/index中:
methods: {
goSearch() {
// 输入框传输params参数
// 点击搜素跳转路由使用编程式导航路由,可以处理数据
const location = { //合并参数
name: 'search',
params: { //传输params参数
keywords: this.keywords || undefined
},
query: this.$route.query
}
this.$router.push(location, () => { }, () => { }) // push函数本质是返回一个promise函数,只需要用两个参数来承接promise的resolve和reject参数即可捕获到异常并不做处理
this.searchData = ''
}
},
3.静态页面的搭建
和之前搭建home组件静态页面一样,就不多说了
4.配置ajax和vuex请求search组件数据
search组件数据要向/api/list通过post请求,必须向该路径传入一个参数,默认是空对象{}
在api/index中:
// 获取搜索模块的信息 post请求
// 必须传递参数params,默认是{}
export const searchInfo = (params) => requests({ url: '/list', method: 'post', data: params })
在store/Search/index中:
// search组件的store仓库
import { searchInfo } from '@/api'
// 用于异步操作 不进行数据操作
const actions = {
async getSearchList(context, params = {}) {
const result = await searchInfo(params)
if (result.code == 200) {
console.log(result.data);
context.commit('GETSEARCHLIST', result)
}
}
}
// 进行数据操作
const mutations = {
GETSEARCHLIST(state, result) {
state.searchList = result.data
}
}
// 用于加工state内的数据 类似于computed
const getters = {
// 这里的形参state是小仓库的state
attrsList(state) {
return state.searchList.attrsList || []
},
goodsList(state) {
return state.searchList.goodsList || [] //没有读取到goodsList就设置为空数组,页面就不会报错
},
trademarkList(state) {
return state.searchList.trademarkList || []
},
}
// 存储共享数据
const state = {
searchList: {},
}
5.动态展示数据和选择性展示数据
请求到的数据:
[1]动态展示数据
search组件展示商品列表信息:
<li class="yui3-u-1-5" v-for="goods in goodsList" :key="goods.id">
<div class="list-wrap">
<div class="p-img">
<a href="item.html" target="_blank"><img :src="goods.defaultImg" /></a>
</div>
<div class="price">
<strong>
<em>¥ </em>
<i>{{ goods.price }}</i>
</strong>
</div>
<div class="attr">
<a target="_blank" href="item.html" :title="goods.title">{{ goods.title }}</a>
</div>
...
</li>
computed: {
// 使用getters更方便
...mapGetters(['goodsList'])
},
searchSelector子组件展示品牌和商品属性信息:
也是用mapGetters获取然后v-for展示出来,略
[2]选择性展示数据
searchParams用于存储发送到服务器获取对应搜索结果的数据
searchParams: {
// 一级id
"category1Id": "",
// 二级id
"category2Id": "",
// 三级id
"category3Id": "",
// 名字
"categoryName": "",
// 关键字
"keyword": "",
// 排序
"order": "1:desc",
// 页号
"pageNo": 1,
// 一页产品数量
"pageSize": 10,
// 平台售卖属性操作带的参数
"props": [],
// 品牌
"trademark": ""
}
通过路由的query和params参数可以确定categoryid和categoryName和keyword
通过从home进入search组件会在mounted时请求一次数据,后续路由改变后想要让组件刷新数据就需要使用监视属性监视$route的query或者params的改变
beforeMount() {
// 在searchParams数据传入store请求商品列表数据之前将$route的query和params参数传入searchParams
// this.searchParams.category1Id = this.$route.query.category1Id
// this.searchParams.category2Id = this.$route.query.category2Id
// this.searchParams.category3Id = this.$route.query.category3Id
// this.searchParams.categoryName = this.$route.query.categoryName
Object.assign(this.searchParams, this.$route.query, this.$route.params) //利用assign更简便
},
mounted() {
this.getData()
},
computed: {
// 使用getters更方便
...mapGetters(['goodsList'])
},
methods: {
// 将向store请求search数据封装为函数 便于随时调用
getData() {
this.$store.dispatch('getSearchList', this.searchParams)
}
},
watch: {
// 监视路由数据变化 一旦变化就重新获取路由的query和params,然后重新请求search数据
$route(oldValue, newValue) {
// 请求完数据后清空id数据 以防止下次重新获取路由时产生错误
this.searchParams.category1Id = ''
this.searchParams.category2Id = ''
this.searchParams.category3Id = ''
// 将当前路由所带搜索数据传给searchParams
Object.assign(this.searchParams, this.$route.query, this.$route.params)
// 再次发起ajax请求
this.getData()
}
}
6.面包屑的处理
[1]面包屑的动态数据展示和删除
(1)分类标签数据的面包屑
删除面包屑时:只删除query数据,对params数据进行保留;
这里无需再根据新的searchParams请求新的商品信息,因为当路由跳转到search后,监视属性会自动触发getData
在pages/search/index中:
<ul class="fl sui-tag">
<!-- 分类标签的面包屑 -->
<li class="with-x" v-if="searchParams.categoryName">{{ searchParams.categoryName }}
<i @click="removeCategoryName">×</i>
</li>
</ul>
methods: {
// 将向store请求search数据封装为函数 便于随时调用
getData() {
this.$store.dispatch('getSearchList', this.searchParams)
},
// 点击x移除分类标签的面包屑
removeCategoryName() {
// 将query数据清空
// 性能优化:因为传入请求数据的10个参数都是可写可不写的,将数据改成undefined后就不会传入服务器中
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
// 路由转到search同时params数据保留
// watch会监视到路由的改变从而重新发送请求商品数据
this.$router.push({
name: 'search',
params: this.$route.params
})
},
watch: {
// 监视路由数据变化 一旦变化就重新获取路由的query和params,然后重新请求search数据
$route(oldValue, newValue) {
// 请求完数据后清空id数据 以防止下次重新获取路由时产生错误
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
Object.assign(this.searchParams, this.$route.query, this.$route.params)
// 再次发起ajax请求
this.getData()
}
}
(2)搜索关键字的面包屑
和分级标签的面包屑做法相同
在pages/search/index中:
<ul class="fl sui-tag">
<!-- 搜索关键字的面包屑 -->
<li class="with-x" v-if="searchParams.keyword">{{ searchParams.keyword }}
<i @click="removeKeyword">×</i>
</li>
</ul>
methods: {
// 将向store请求search数据封装为函数 便于随时调用
getData() {
this.$store.dispatch('getSearchList', this.searchParams)
},
// 点击x移除关键字的面包屑
removeKeyword() {
// 将params数据清空
this.searchParams.keyword = undefined
// 将header输入框内的keyword也要清空 兄弟组件之间通信使用全局事件总线
this.$bus.$emit('clearKeyword')
// 路由转到search同时query数据保留
// watch会监视到路由的改变从而重新发送请求商品数据
this.$router.push({
name: 'search',
query: this.$route.query
})
}
},
watch: {
// 监视路由数据变化 一旦变化就重新获取路由的query和params,然后重新请求search数据
$route(oldValue, newValue) {
// 请求完数据后清空id数据 以防止下次重新获取路由时产生错误
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
Object.assign(this.searchParams, this.$route.query, this.$route.params)
// 再次发起ajax请求
this.getData()
}
}
但清空searchParams里的keyword后,也需要将header输入框里的keyword同步删除,兄弟组件的通信用到了全局事件总线
在main.js中:
new Vue({
beforeCreate() {
// 全局事件总线$bus配置
Vue.prototype.$bus = this
},
...
}).$mount('#app')
在header/index中:
mounted() {
// 通过全局事件总线 清除keyword
this.$bus.$on('clearKeyword', () => {
this.keyword = ''
})
},
[2]品牌信息的面包屑展示
品牌信息存储在searchSelector子组件中,将品牌信息从子组件发送到父组件中可以通过组件自定义事件
自定义事件getTrademarkInfo
在pages/search/index中:
<!--selector-->
<!-- 绑定自定义事件 -->
<SearchSelector @getTrademarkInfo="getTrademarkInfo" />
getTrademarkInfo(trademarkInfo) {
// 以“ID:品牌名称”形式传给searchParams
this.searchParams.trademark = `${trademarkInfo.tmId}:${trademarkInfo.tmName}`
// 请求新的数据
this.getData()
},
点击品牌信息将对应的id和名称发送给父组件
在pages/search/searchSelectors/index中:
<ul class="logo-list">
<li v-for="trademark in trademarkList"
:key="trademark.tmId"
@click="serveTrademarkInfo(trademark)">{{trademark.tmName }}</li>
</ul>
methods: {
// 将品牌信息传给父组件
serveTrademarkInfo(trademark) {
// 通过组件自定义事件将品牌信息传给父组件 子传父
this.$emit('getTrademarkInfo', trademark)
}
},
在父组件中处理品牌信息面包屑的删除功能
searchParams中的trademark存储的是字符串:“ID:品牌名”,通过计算属性trademarkName可以获取到单独的品牌名进行面包屑展示
在pages/search/index中:
<!-- 品牌信息的面包屑 -->
<li class="with-x" v-if="searchParams.trademark">
{{ searchParams.trademark.split(":")[1] }}
<i @click="removeTrademark">×</i>
</li>
methods: {
removeTrademark() {
// 清空品牌信息
this.searchParams.trademark = undefined
// 重新获取数据
this.getData()
}
},
[3]平台售卖属性的面包屑
和品牌信息的面包屑展示差不多
利用组件自定义事件将平台售卖属性传递给父组件
在pages/search/searchSelector/index中:
<li v-for="(attrValue, index) in attrs.attrValueList" :key="index">
<!-- 发送平台售卖属性信息给父组件 -->
<a @click="serveAttrsInfo(attrs.attrId, attrValue, attrs.attrName)">{{ attrValue }}</a>
</li>
methods: {
// 将平台售卖信息传给父组件
serveAttrsInfo(attrId, attrValue, attrName) {
// 自定义事件 getAttrsInfo
this.$emit('getAttrsInfo', attrId, attrValue, attrName)
}
},
父组件接收数据后动态展示
searchParams中的props接收的是一个数组,可以容纳多个售卖属性
在pages/search/index中:
<!-- 平台售卖属性的面包屑 -->
<li class="with-x" v-for="prop in searchParams.props">{{ prop.split(":")[1] }}
<i @click="removeAttrs(prop)">×</i>
</li>
methods:{
// 组件自定义事件 获取平台销售属性并加工
getAttrsInfo(...attrsInfo) {
// 以“属性ID:属性值:属性名”形式传给searchParams
let prop = `${attrsInfo[0]}:${attrsInfo[1]}:${attrsInfo[2]}`
// 数据去重
if (this.searchParams.props.indexOf(prop) == -1) {
this.searchParams.props.push(prop)
}
// 请求新的数据
this.getData()
},
}
点击叉后移除指定的数据
methods:{
// 移除面包屑
removeAttrs(prop) {
// 清空对应的平台售卖属性
this.searchParams.props = this.searchParams.props.filter((item) => {
return item != prop
})
// 获取新数据
this.getData()
},
}
7.排序操作
[1]点击排序选项加上active样式
使用计算属性判断是综合排序还是价格排序
在pages/search/index中:
<ul class="sui-nav">
<!-- searchParams里的order包含1时 展示active样式 -->
<li :class="{ active: order1 }">
<a>综合</a>
</li>
<!-- searchParams里的order包含2时 展示active样式 -->
<li :class="{ active: !order1 }">
<a>价格</a>
</li>
</ul>
computed:{
// order中包含编号1
order1() {
return this.searchParams.order.includes('1')
},
}
[2]排序图标和排序功能
使用在线链接引入上下排序图标,同样通过计算属性order1判断图标是否展示,再通过计算属性orderAsc判断哪种图标展示出来
在public/index.html:
<!-- 引入阿里在线图标 -->
<link rel="stylesheet" href="//at.alicdn.com/t/c/font_4255847_2o3fo5x2zq8.css">
在pages/search/index:
<ul class="sui-nav">
<!-- searchParams里的order包含1时 展示active样式 -->
<li :class="{ active: order1 }" >
<a>综合<span v-show="order1" class="iconfont "
:class="orderAsc ? 'icon-direction-up' : 'icon-direction-down'"></span>
</a>
</li>
<!-- searchParams里的order包含2时 展示active样式 -->
<li :class="{ active: !order1 }" >
<a>价格<span v-show="!order1" class="iconfont "
:class="orderAsc ? 'icon-direction-up' : 'icon-direction-down'"></span>
</a>
</li>
</ul>
computed:{
// order中包含排序方式asc
orderAsc() {
return this.searchParams.order.includes('asc')
}
}
[3]实现点击切换排序选项和排序方式
searchParams中order的传入格式是“排序序号:排序方式”,默认为“1:desc”
实现功能:点击排序选项就切换为该排序选项。若点击的是同一个选项则切排序方式;
通过判断点击的目标排序选项继而判断是切换选项还是改变排序方式
在pages/search/index:
<ul class="sui-nav">
<!-- searchParams里的order包含1时 展示active样式 -->
<li :class="{ active: order1 }" @click="changeOrder(1)">
<a>综合<span v-show="order1" class="iconfont "
:class="orderAsc ? 'icon-direction-up' : 'icon-direction-down'"></span>
</a>
</li>
<!-- searchParams里的order包含2时 展示active样式 -->
<li :class="{ active: !order1 }" @click="changeOrder(2)">
<a>价格<span v-show="!order1" class="iconfont "
:class="orderAsc ? 'icon-direction-up' : 'icon-direction-down'"></span>
</a>
</li>
</ul>
methods:{
// 改变排序(综合1/价格2)
changeOrder(flag) {
let originOrder = this.searchParams.order
// 改变排序顺序
if (originOrder.includes(flag)) {
// 三元运算判断 如果是asc就变为dessc,反之变成asc
this.searchParams.order = originOrder.includes('asc') ? `${flag}:desc` : `${flag}:asc`
} else { //改变排序选项
this.searchParams.order = `${flag}:desc`
}
// 获取新的数据
this.getData()
}
}
8.分页器功能实现
[1]分离静态组件
分页器在搜索组件、我的订单组件等地方都会用到,所以分离成一个全局组件可以更好地实现复用
[2]实现分页器的动态展示
先传入固定值进行测试
在pages/search/index中
<!-- 分页器组件 -->
<!-- 先传入虚拟数据进行测试 -->
<Pagination :pageSize="5" :pageNo="7" :total="77" :totalPages="16" :continues="5" />
在pages/search/pagination/index中:
props: ['pageSize', 'pageNo', 'total', 'totalPages', 'continues'], // continues是连续展示页码数
实现连续页码的逻辑判断
在pages/search/pagination/index中:
computed: {
// 计算连续页码序列的起始值和结束值
getStartAndEnd() {
let start = 0 // 记录连续开始页码
let end = 0 // 记录连续末尾页码
// 总页数小于连续页码序列长度时
if (this.totalPages < this.continues) {
start = 1
end = this.totalPages
} else { // 大于等于连续页码序列长度
start = this.pageNo - parseInt(this.continues / 2)
end = this.pageNo + parseInt(this.continues / 2)
// 起始页码小于1 说明所在页码小于3
if (start < 1) {
start = 1
end = start + this.continues - 1
}
// 如果结束页码大于总页数 说明所在页码在总页数前三页
if (end > this.totalPages) {
start = this.totalPages - this.continues + 1
end = this.totalPages
}
}
return { start, end }
}
}
在网页中动态展示
<ul>
<li class="prev disabled">
<a>«上一页</a>
</li>
<!-- 当存在一段start不等于1的连续页码段时展示 -->
<li v-if="getStartAndEnd.start != 1"><a>1</a></li>
<li v-if="getStartAndEnd.start != 1" class="dotted"><span>...</span></li>
<!-- 连续页码段 从start到end 数字也可以用v-for -->
<li v-for="(page, index) in getStartAndEnd.end" :key="index"
v-if="page >= getStartAndEnd.start"
:class="{ active: page == pageNo }">
<a>{{ page }}</a>
</li>
<!-- 当存在一段end不等于最后页码的连续页码段时展示 -->
<li v-if="getStartAndEnd.end != totalPages" class="dotted"><span>...</span></li>
<li v-if="getStartAndEnd.end != totalPages"><a>{{ totalPages }}</a></li>
<li class="next">
<a>下一页»</a>
</li>
</ul>
<div><span>共{{ totalPages }}页 </span></div>
如下:
[3]分页器的功能实现
将页码数据传递给search父组件,子传父可以使用自定义事件
父组件收到page后要先检查有没有上一页或有没有下一页或目标页是不是所在页这三种情况
在pages/search/index中:
<!-- 分页器组件 -->
<Pagination :pageSize="searchParams.pageSize" :pageNo="searchParams.pageNo"
:total="total" :totalPages="totalPages" :continues="5"
@getPageNo="getPageNo" />
methods:{
// 跳转到指定页码
getPageNo(page) {
// 没有上一页或没有下一页或点击所在页时
if (page == 0 || page > this.totalPages || page == this.searchParams.pageNo) return
this.searchParams.pageNo = page
this.getData()
// 使用原生js将滚动条滚到最上方
window.scrollTo(0, 0)
}
}
点击上一页下一页或者页码实际都是传递目标page,所以使用一个函数即可;
点击页码触发点击事件只有一个$emit操作所以直接写在标签内即可
在pages/search/pagination/index中:
<ul>
<!-- 没有上一页的时候启用disabled样式 -->
<li class="prev " :class="{ disabled: pageNo == 1 }"
@click="$emit('getPageNo', pageNo - 1)">
<a>«上一页</a>
</li>
<!-- 当存在一段start不等于1的连续页码段时展示 -->
<li v-if="getStartAndEnd.start != 1" @click="$emit('getPageNo', 1)">
<a>1</a>
</li>
<li v-if="getStartAndEnd.start != 1" class="dotted">
<span>...</span>
</li>
<!-- 连续页码段 从start到end 数字也可以用v-for -->
<li v-for="(page, index) in getStartAndEnd.end"
:key="index" v-if="page >= getStartAndEnd.start"
:class="{ active: page == pageNo }" @click="$emit('getPageNo', page)">
<a>{{ page }}</a>
</li>
<!-- 当存在一段end不等于最后页码的连续页码段时展示 -->
<li v-if="getStartAndEnd.end != totalPages" class="dotted">
<span>...</span>
</li>
<li v-if="getStartAndEnd.end != totalPages"
@click="$emit('getPageNo', totalPages)">
<a>{{ totalPages }}</a>
</li>
<!-- 没有下一页的时候启用disabled样式 -->
<li class="next" :class="{ disabled: pageNo == totalPages }"
@click="$emit('getPageNo', pageNo + 1)">
<a>下一页»</a>
</li>
</ul>