猿实战是一个原创系列文章,通过实战的方式,采用前后端分离的技术结合SpringMVC Spring Mybatis,手把手教你撸一个完整的电商系统,变身猿人找到工作不是问题。还等什么呢?关注公号,取基础代码,一起实战吧。
经过之前一些列章节的实战,我们终于离我们的目标系统越来越接近了,从今天开始到接下来的六个章节,我们一起来重点关注商品的那点儿事儿,今天我们先重点讲解商品发布时的类目选择功能。
功能概览
在商品发布的功能中,商品发布有三个步骤,选择类目,填写信息,提交信息。
今天我们要讲到的选择类目功能,其实和之前讲的类目联动选择并没有很大的区别,只是我们在选择的展示上已经数据的获取方式上有细微差别。
类目选择依然是保持了三级联动的方式进行,用户必须选择完整的三级类目才可以发布商品信息。
数据库设计
发布商品时,使用的是后台类目,后台类目的数据库设计之前已经有了,这里就不一一讲解了。
后端功能实现
在之前的过程中,我们灵活使用了带分页的类目查询来实现类目的下拉选择,其实这样做只是一个狗皮膏药而已,正确的做法是,每一级都需要查出所有的数据,之前之所以那样去处理,目的是告诉你问题的灵活解决之道。
废话不多讲了,我们先来看看后端数据功能是怎样实现的。
/**
* 不分页返回类目列表
* @param queryMallCategory
* @return
*/
@RequestMapping("/findByQuery")
public Result<List<MallCategory>> findByQuery(@RequestBody QueryMallCategory queryMallCategory){
return mallCategoryService.getMallCategorysByQuery(queryMallCategory);
}
由于service层和dao层是透传的,我们主要看下mapper层的区别。
通过mysql数据库做分页查询的本质在于使用limit关键字,第一个参数告知数据库从第几行取,第二个参数告知数据库取多少条记录。
前端功能实现
前端功能的实现,主要还是基于element-ui来实现的。这里主要使用的是el-cascader-panel组件。el-cascader-panel被称作联级选择器。最广泛和深入的用法,大家可以访问官网进行学习。
https://element.eleme.cn/2.0/#/zh-CN/component/cascader
我们先重点关注,需要掌握的核心知识。
选择类目时的联级选择器的使用很简单代码如下:
<el-cascader-panel ref="cascaderCategory" v-model="cascaderValue" :options="options" :props="props" @change="change" @active-item-change="handleItemChange" />
ref:定义了用于引用访问的组件的id.
v-model:数据源。
options:可选项数据源,键名可通过 props 属性配置。
props:配置项目,具体见下表。
change:当绑定值变化时触发的事件。
active-item-change:当父级选项变化时触发的事件,仅在 change-on-select 为 false 时可用。
在选择每一级类目时,我们需要记录已经选择的类目,所以我们需要为change事件绑定函数。
注意噢,类目名称很重要,不光仅仅时展示噢,怎么获取呢?这就需要你对数据组件的数据结构有一定掌握了。其实,我们在接触一个组件时,对这些并不熟悉,有一个简易的办法就是使用console.log取查看。
数据联动的事件处理,大家可以先看一下:
注意,只有当选择了具体的类目时,才会去动态出发数据加载的功能。在选择类目时,还需要区分选择的类目层级,如果是二级则需要给下一句创建一个空的数组,否则选择了二级类目不会触发三级类目的数据加载函数。
知道你不是很熟悉前端,而这部分工作,对你来说是一个长期而又漫长的过程,这里把前端代码给到你。
<template>
<div id="goodsReleasedDiv">
<el-container>
<el-card shadow="never">
<div>
<span style="font-weight: 700;">发布商品</span>
<div style="float:right;padding-right:20px;font-size: 15px;">
<a style="color:#4395ff">1.选择类目</a>
-> 2.填写详细信息
-> 3.提交信息</div>
</div>
<div style="padding-left:20px;padding-right:20px;">
<div style="margin-top:20px;margin-bottom:20px;">选择您要使用的类目:
</div>
<el-cascader-panel ref="cascaderCategory" v-model="cascaderValue" :options="options" :props="props" @change="change" @active-item-change="handleItemChange" />
<div>
您当前选择的类目是:<span v-if="oneCategory">{{ oneCategory }} > {{ twoCategory }} > {{ threeCategory }} </span>
</div>
<div style="text-align: center;margin-top:20px;">
<el-button type="primary" @click="addClick">
我已阅读以下规则,现在发布商品
</el-button>
</div>
</div>
</el-card>
</el-container>
</div>
</template>
<script>
import { fetchCategoryListNoPage } from '@/api/product-manage'
export default {
data() {
return {
props: {
label: 'categoryName',
value: 'categoryId',
children: 'child'
},
oneCategory: '',
twoCategory: '',
threeCategory: '',
oneCategoryId: 0,
twoCategoryId: 0,
threeCategoryId: 0,
cascaderValue: [],
options: [],
category: '',
listQuery: {
// 类目属性
page: 1,
pageSize: 20,
level: 1
}
}
},
created() {
this.getFristCategoryList()
},
methods: {
// 商品发布按钮
addClick() {
this.$router.push({
path: '/shops/goods-released-detail',
query: { oneCategory: this.oneCategory, twoCategory: this.twoCategory,
threeCategory: this.threeCategory, oneCategoryId: this.oneCategoryId,
twoCategoryId: this.twoCategoryId, threeCategoryId: this.threeCategoryId }
})
},
change(value) {
console.log(this.$refs.cascaderCategory.getCheckedNodes()[0].pathLabels)
if (value[0]) {
this.oneCategory = this.$refs.cascaderCategory.getCheckedNodes()[0].pathLabels[0]
this.oneCategoryId = value[0]
}
if (value[1]) {
this.twoCategory = this.$refs.cascaderCategory.getCheckedNodes()[0].pathLabels[1]
this.twoCategoryId = value[1]
}
if (value[2]) {
this.threeCategory = this.$refs.cascaderCategory.getCheckedNodes()[0].pathLabels[2]
this.threeCategoryId = value[2]
}
console.log(value)
},
getFristCategoryList() {
fetchCategoryListNoPage(this.listQuery).then(response => {
this.options = response.model
this.options.map((item, index, array) => {
// 因为数组和对象更新后不会更新视图,这里必须用$set方法
this.$set(array[index], 'child', [])
})
})
},
handleItemChange(value) {
// 动态/异步加载分类数据
let parentId
if (value.length === 1) {
// 如果点击的是一级分类
parentId = value[0]
this.options.map((item, index) => {
if (item.categoryId === parentId && item.child.length === 0) {
this.listQuery.parentId = parentId
this.listQuery.level = 2
fetchCategoryListNoPage(this.listQuery).then(response => {
this.$set(this.options[index], 'child', response.model)
item.child.map((innerItem, innerIndex) => {
// 二级分类下必须要设置一个空的child数组,不然点击@active-item-change没反应
this.$set(item.child[innerIndex], 'child', [])
})
})
}
})
} else {
// 如果点击的是二级分类,则直接将三级分类绑定到platOptions
parentId = value[1]
console.log(parentId)
this.options.map((item, index) => {
if (item !== null) {
item.child.map((innerItem, innerIndex) => {
if (innerItem.categoryId === parentId && innerItem.child.length === 0) {
this.listQuery.parentId = parentId
this.listQuery.level = 3
// 当二级分类的的child不为空时才请求一次数据
fetchCategoryListNoPage(this.listQuery).then(response => {
this.$set(item.child[innerIndex], 'child', response.model)
})
}
})
}
})
}
}
}
}
</script>
<style scoped>
#goodsReleasedDiv .form-container {
width: 1200px;
margin: 0 auto;
min-height: 770px;
}
#goodsReleasedDiv /deep/ .el-card__body {
padding: 0px 0px 20px 0px ;
}
#goodsReleasedDiv .issueDiv {
font-size: 18px;
line-height: 18px;
padding-top: 10px;
padding-bottom: 10px;
padding-left: 20px;
background: #f9f9f9;;
}
#goodsReleasedDiv /deep/ .el-cascader-menu__wrap {
height: 400px;
}
#goodsReleasedDiv .el-cascader-panel.is-bordered {
height: 400px;
}
#goodsReleasedDiv .categoryDiv {
padding-top: 10px;
border: solid 1px #dfe4ed;
border-radius: 4px;
padding-bottom: 10px;
margin-top: 20px;
padding-left: 20px;
}
</style>