猿实战是一个原创系列文章,通过实战的方式,采用前后端分离的技术结合SpringMVC Spring Mybatis,手把手教你撸一个完整的电商系统,跟着教程走下来,变身猿人找到工作不是问题。想要一起实战吗?关注公号即可获取基础代码!
上一个章节,猿人君教会了你实现了后台类目,今天开始我们来讲述,类目和属性之间的关系绑定。
为什么需要这种绑定关系
大家都知道,商品是有类别的,而类别之所以能够分门别类,是因为这些类别,本身就具备一些特性,而这些特性就是我们之前提到过的商品属性。
关于这个话题还不太理解的朋友们,建议您回过头去看看之前的文章,搞明白之后,相信可以解决您的疑惑。
功能概览
在之前的文章中,我们其实已经分析过类目和属性关系的设计了,简而言之,属性描述着类目的特性,而类目在将来,会把这些特性赋予具体的商品。
有的朋友可能比较着急,像快速的去知道这是怎样的一个链路,不过胖子认为,要搞清这样一个链路,还是先做一些比较实际的工作为好。量变之所能够质变,是量让你学会思考,产生了质的东西,才是属于你自己的。
我们先看看这一块儿的整体功能概览。
在类目与属性的关系中,在整体功能上,可以分为类目属性列表、类目属性值列表,在列表页面都提供了新增编辑功能。类目的选择,支持多级联动,新增/编辑属性或者属性值,支持输入检索,勾选检索内容,实现快速新增属性/或者属性值的功能。检索支持模糊查询,而且需要同时支持检索属性/属性组,属性值/属性值组。
数据库设计
根据之前的后台类目设计文章,我们很清楚的获知了后台类目的一些特征,我们根据之前的设计猿设计5——真电商之颠覆你的类目认知,猿设计6——真电商之属性的套路你了解吗将这些设计落地为数据库的物理设计,大家可以看一下。
就类目属性的整体设计而言,类目属性更加明确了属性的类型,我们之前的属性库,作为基础数据,为类目属性提供数据来源。类目的特性,在绑定类目属性关系时来确定某一类属性的特性。
类目属性整体前端
类目属性的绑定,从功能上讲,依然是一种整体和部分的关系,属性值列表的展示依赖于,类目属性列表的选择,而属性/属性值的新增和编辑功能,则依赖着各自的组件。最后,由一个view来组织和整合它们就好了。
<template>
<div id="attributeLibraryDiv">
<div v-if="backgroundCategory">
<el-card shadow="never">
<div>
<el-form ref="listQuery" :model="listQuery" :inline="true">
<el-form-item label="所属类目:" style="width:46%;" prop="stair">
<el-select v-model="listQuery.fristCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatOne">
<el-option
v-for="item in fristList"
:key="item.categoryId + '^-^'"
:label="item.categoryName"
:value="item.categoryId"
/>
</el-select>
<el-select v-model="listQuery.secondCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatTwo">
<el-option
v-for="item in secondList"
:key="item.categoryId + '^-^'"
:label="item.categoryName"
:value="item.categoryId"
/>
</el-select>
<el-select v-model="listQuery.thridCategoryId" clearable filterable allow-create placeholder="请选择" style="width:30%;" @change="selectCatThree">
<el-option
v-for="item in thirdList"
:key="item.categoryId + '^-^'"
:label="item.categoryName"
:value="item.categoryId"
/>
</el-select>
</el-form-item>
<el-form-item>
<el-button type="primary" icon="el-icon-search" @click="fetchData()">查询</el-button>
<el-button type="primary" icon="el-icon-edit" @click="addDate()">新增属性</el-button>
<el-button icon="el-icon-s-tools" @click="resetForm('listQuery')">重置</el-button>
</el-form-item>
</el-form>
</div>
</el-card>
<div style="height:20px;" />
<div>
<el-table
ref="table"
v-loading="listLoading"
:data="list"
style="width: 100%"
border
>
<el-table-column label="类目属性ID">
<template slot-scope="scope">{{ scope.row.categoryPropertyId }}</template>
</el-table-column>
<el-table-column label="属性ID">
<template slot-scope="scope">{{ scope.row.propertyId }}</template>
</el-table-column>
<el-table-column label="属性名">
<template slot-scope="scope">{{ scope.row.propertyName }}</template>
</el-table-column>
<el-table-column label="排序">
<template slot-scope="scope">{{ scope.row.sortOrder }}</template>
</el-table-column>
<el-table-column label="状态">
<template slot-scope="scope">{{ scope.row.status == 1 ? "启用" : "停用" }}</template>
</el-table-column>
<el-table-column label="属性类型">
<template slot-scope="scope">{{ getPropertyType(scope.row.propertyType) }}</template>
</el-table-column>
<el-table-column label="录入方式">
<template slot-scope="scope">
{{ getInputType(scope.row.inputType) }}
</template>
</el-table-column>
<el-table-column label="是否导航属性">
<template slot-scope="scope">{{ scope.row.nav == 1 ? "是" : "否" }}</template>
</el-table-column>
<el-table-column label="是否必填">
<template slot-scope="scope">
{{ scope.row.required == 1 ? "必填" : "选填" }}
</template></el-table-column>
<el-table-column label="操作" width="300">
<template slot-scope="scope">
<el-button
type="primary"
size="mini"
@click="handleLook(scope.$index,scope.row)"
>查看属性值
</el-button>
<el-button
type="primary"
size="mini"
@click="handleUpdate(scope.row)"
>修改
</el-button>
<el-button
size="mini"
type="danger"
@click="handleDelete(scope.$index, scope.row)"
>删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<pagination v-show="total>0" :total="total" :page.sync="listQuery.page" :limit.sync="listQuery.pageSize" @pagination="getList" />
</div>
<div v-if="attributeValue">
<attributeValueListSearch ref="attributeValueListSearch" :showflag.sync="showflag" :cpid="categoryPropertyId" :cid="categoryId" @returnBack="returnBack" />
</div>
<!-- 新增/编辑弹框 -->
<el-dialog v-if="dialogFormVisible" :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<attributeCUpdate ref="attributeCUpdate" :attributeflag.sync="attributeFlag" @closeClick="closeClick" @addClick="addClick" />
</el-dialog>
</div>
</template>
<script>
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import attributeValueListSearch from '@/components/productManage/categoryAttributeValueListSearch'
import attributeCUpdate from '@/components/productManage/attributeCUpdate'
import { fetchCategoryList, fetchCategoryPropertyList, deleteMallCategoryProperty } from '@/api/product-manage'
export default {
components: {
attributeValueListSearch, Pagination, attributeCUpdate },
data() {
return {
attributeFlag: true,
// 弹框是否显示
dialogFormVisible: false,
dialogStatus: '',
textMap: {
update: '属性修改',
create: '新增属性'
},
categoryPropertyId: 0,
categoryId: 0,
// 标识
showflag: true,
backgroundCategory: false,
attributeValue: false,
// 分页
total: 0,
// loading
listLoading: true,
// table集合
list: null,
listCategoryQuery: {
parentId: null,
page: 1,
pageSize: 500
},
listQuery: {
// 所属类目:
stair: '',
//
fristCategoryId: null,
//
secondCategoryId: null,
//
thridCategoryId: null,
categoryId: 0,
page: 1,
pageSize: 10
},
// 一级
fristList: [],
// 二级
secondList: [],
// 三级
thirdList: [],
propertyTypeList: [
{
value: 1,
label: '普通属性'
}, {
value: 2,
label: '关键属性'
},
{
value: 3,
label: '文字销售属性'
}, {
value: 4,
label: '图片销售属性'
}
],
inputTypeList: [
{
value: 0,
label: '单选'
}, {
value: 1,
label: '多选'
},
{
value: 2,
label: '输入'
}
]
}
},
created() {
this.backgroundCategory = true
// 列表查询
this.getList()
this.initFristList()
},
methods: {
// 编辑新增
addClick(data, flag) {
this.dialogFormVisible = true
},
// 关闭
closeClick() {
this.dialogFormVisible = false
this.getList()
},
// 返回
returnBack() {
this.backgroundCategory = true
this.attributeValue = false
},
// 删除
handleDelete(index, row) {
console.log(row)
deleteMallCategoryProperty(row).then(response => {
this.$notify({
title: 'Success',
message: 'Delete Successfully',
type: 'success',
duration: 2000
})
this.getList()
})
},
// 修改
handleUpdate(row) {
console.log(row)
this.attributeFlag = true
this.dialogStatus = 'update'
this.dialogFormVisible = true
setTimeout(() => {
this.$refs.attributeCUpdate.updateDate(this.attributeFlag, row)
}, 50)
},
// 查看属性值
handleLook(index, row) {
this.backgroundCategory = false
this.categoryPropertyId = row.categoryPropertyId
this.categoryId = row.categoryId
this.attributeValue = true
},
// 列表查询
getList() {
this.listLoading = true
this.chooseCategoryId()
fetchCategoryPropertyList(this.listQuery).then(response => {
this.list = response.model
this.total = response.totalItem
// Just to simulate the time of the request
setTimeout(() => {
this.listLoading = false
}, 1.5 * 1000)
})
},
// 查询方法
fetchData() {
this.getList()
},
// 重置表单
resetForm(formName) {
this.listQuery.fristCategoryId = null
//
this.listQuery.secondCategoryId = null
//
this.listQuery.thridCategoryId = null
this.listQuery.categoryId = 0
this.getList()
},
// 新增
addDate() {
this.chooseCategoryId()
if (this.listQuery.categoryId === 0) {
this.$message({
message: '请先选择需要新增属性的类目!',
type: 'success'
})
return false
}
const row = {}
this.attributeFlag = false
this.dialogStatus = 'create'
this.dialogFormVisible = true
setTimeout(() => {
this.$refs.attributeCUpdate.updateDate(this.attributeFlag, row, this.listQuery.categoryId, this.listQuery.fristCategoryId, this.listQuery.secondCategoryId, this.listQuery.thridCategoryId)
}, 50)
},
// 列表查询
initFristList() {
this.listLoading = true
fetchCategoryList(this.listCategoryQuery).then(response => {
this.fristList = response.model
// Just to simulate the time of the request
setTimeout(() => {
this.listLoading = false
}, 1.5 * 1000)
})
},
selectCatOne(option) {
this.listLoading = true
this.listCategoryQuery.parentId = option
this.listQuery.fristCategoryId = option
// this.listCategoryQuery.parentId = item.categoryId
fetchCategoryList(this.listCategoryQuery).then(response => {
this.secondList = response.model
// Just to simulate the time of the request
setTimeout(() => {
this.listLoading = false
}, 1.5 * 1000)
this.getList()
})
},
selectCatTwo(option) {
this.listLoading = true
this.listCategoryQuery.parentId = option
this.listQuery.secondCategoryId = option
this.secondCategoryId = option
// this.listCategoryQuery.parentId = item.categoryId
fetchCategoryList(this.listCategoryQuery).then(response => {
this.thirdList = response.model
// Just to simulate the time of the request
setTimeout(() => {
this.listLoading = false
}, 1.5 * 1000)
this.getList()
})
},
selectCatThree(option) {
this.listQuery.thridCategoryId = option
this.thridCategoryId = option
this.listLoading = true
this.getList()
},
chooseCategoryId() {
if (this.listQuery.fristCategoryId !== null && this.listQuery.fristCategoryId !== '' && this.listQuery.fristCategoryId > 0) {
this.listQuery.categoryId = this.listQuery.fristCategoryId
}
if (this.listQuery.secondCategoryId !== null && this.listQuery.secondCategoryId !== '' && this.listQuery.secondCategoryId > 0) {
this.listQuery.categoryId = this.listQuery.secondCategoryId
}
if (this.listQuery.thridCategoryId !== null && this.listQuery.thridCategoryId !== '' && this.listQuery.thridCategoryId > 0) {
this.listQuery.categoryId = this.listQuery.thridCategoryId
}
},
getPropertyType(propertyType) {
var rowData = this.propertyTypeList.filter(itmer => {
if (itmer.value === propertyType) {
return itmer.label
}
})
if (undefined !== rowData[0]) {
return (rowData[0].label)
}
},
getInputType(inputType) {
var rowData = this.inputTypeList.filter(itmer => {
if (itmer.value === inputType) {
return itmer.label
}
})
if (undefined !== rowData[0]) {
return (rowData[0].label)
}
}
}
}
</script>
<style scoped>
#attributeLibraryDiv /deep/ .el-dialog__body {
padding: 0px 20px;
}
</style>
类目的联动实现
考虑到页面的功能较多,我们依然采用先部分再到整体的实现策略,比如三级类目联动的实现。
在实现之前,我们先想一想,联动是怎样一个过程?首先需要显示一级类目吧?二级类目的展示,自然需要一级类目的数据支持,只有当确定了一级类目,才知道需要一级类目下的二级类目数据,同样的,三级类目,也就依赖于二级类目的选择了。
既然是根据数据做再做选择,那么自然离不开el-select组件了。
既然是三个类目列表,自然少不了存储和记录数据。话说在类似VUE这类前端框架下开发,真的比以前省事儿多了,准备好数据,组件帮你渲染就好了。
查询之间也有依赖关系,自然也需要保存每次选择的结果。
接下来,自然是要考虑数据获取的问题了。一级类目选择之后,自然要加载二级类目的数据了。
嗯,似乎在选择类目数据时,是支持搜索的,这一点el-select组件已经帮我们实现好了。加上clearable filterable 两个属性就可以了。
等等,还忘记了一件事情,API在哪里呢?
// 类目
export function fetchCategoryList(query) {
return request({
url: '/category/findByPage',
method: 'post',
data: query
})
}
这个不是之前分页的时候用过吗?嗯,可以考虑改变页码大小的方式继续使用嘛,稍微灵活变通下,满足要求就可以了。当然,你也可以开发一个专用的不分页API进行查询。
listCategoryQuery: {
parentId: null,
page: 1,
pageSize: 500
}
需要注意的是这个“不分页返回类目列表”的实现,因为在后续的很多场景中,往往要求获取整个查询条件下的后台类目,此时封装一个区别于分页的接口,算是一种预先考虑的目的。
类目联动后端实现
后端的实现就简单多了,我们只用返回对应的数据就可以了。用到的api也不多,之前已经实现过了,见MallCategoryController。
/**
* 分页返回类目列表
* @param queryMallCategory
* @return
*/
@RequestMapping("/findByPage")
public Result<List<MallCategory>> findByPage(@RequestBody QueryMallCategory queryMallCategory){
return mallCategoryService.getMallCategorysByPage(queryMallCategory);
}
service以及dao层面的实现,搞定代码生成器猿实战05——手把手教你拥有自己的代码生成器,就可以得到你想要的功能了。想要一起实战吗?关注公号即可获取基础代码!