上两个章节,猿人君教会了你如何通过设计落地实现了属性库,今天我们继续来实现系统的另一个基石地位的模块——后台类目。
为什么会有后台类目
大家都知道,商品是可以分类的,比如手机、电脑、数码、生鲜、这些名词的背后,都代表着不同类别的商品。那么我们对商品做分类,按照类别去分就可以了。那搞出后台类目这几个字,您是几个意思?
其实呀,能够称得上主流电商网站的商品都非常多(jd亿级,tb十亿级都挡不挡得住另外讲),商品和类目是有关联的,就不用掰扯了。就说类目,它是一棵树,商品的细分会导致树的层级越来越深。如果买家直接使用后台类目,那么查找商品会变得越来越困难。从运营层面去考虑,如果运营人员在调整类目时,都需要去变更商品的类目,那么工作量绝对是海量的。但是运营这个事情又是长期的,经常随着节日、季节变化而做出相应的调整,如果不解决这个问题,离关门大吉也不远了。
所以从设计上来看,前台类目和后台类目,必须分离了。后台类目相对固定,建立了就不要轻易改动了。用后台类目去应对商家或供应商(自家玩耍的也算),大家都做生意,朝令夕改,会导致都不想和你玩耍的,这样子做也便于建立标准化的商品服务,也利用后续仓储的库存分类分区管理。
前台类目面向用户,方便用户查找商品,方便运营根据销售策略及时调整,甚至可以针对不同的客户端进行不同的设置(PC\M\APP终端大小都不同),前、后台类目之间可以通过建立关联关系,方便以后台类目为基础,快速调整去适应前台的运营策略。
功能概览
在之前的文章中猿设计5——真电商之颠覆你的类目认知,我们其实已经分析过类目的设计了,简而言之,类目更像是一片森林,而属性更像是森林里的各种元素,动物,植物,而后台类目将作为一条主线,负责串联着这些最近本的元素,维护着生态平衡。
聊这些话题,可能有些为时过早,不过在这之前,我们可以先看一看类目的功能。
在后台类目模块中,后台类目体现了一种典型的层级关系,每一级类目都有对应的列表、新增/编辑功能。同时,在每一级后台类目都可以绑定相应的属性,用于赋能类目下的商品特性(本章节暂不讨论)。
数据库设计
根据之前的后台类目设计文章,我们很清楚的获知了后台类目的一些特征,我们根据之前的设计,将这些设计落地为数据库的物理设计,大家可以看一下。
就类目而言,类目就像是一棵树,每一级类目都有可能有下一级类目,像这种特性,在数据库的设计上,是一种典型的自关联结构。但是类目用于检索时,往往伴随着层级关系的查找,比如提取某一级类目的信息。所以,我们在设计时,给予了一个level字段,用于方便提取固定层级的信息。
后台类目整体前端
类目的层级维护,从功能上讲,依然是一种整体和部分的关系,出于业务的考虑,在规模不是特别庞大的情况下,三级已经足够支撑你的运营。我们可以参考下之前的实现思路——将一级类目、二级类目、三级类目分别定义成小的组件。最后,由一个view来组织和整合它们就好了。
<template>
<div id="backgroundCategoryDiv">
<div v-if="oneCategoryShow">
<backgroundCategoryOneSearch ref="backgroundCategoryOneSearch" @lookSubordinate="lookOneSubordinate" />
</div>
<div v-if="twoCategoryShow">
<backgroundCategoryTwoSearch ref="backgroundCategoryTwoSearch" :pid="parentId" :pname="parentName" @lookSubordinate="lookTwoSubordinate" @returnBack="returnTwoBack" />
</div>
<div v-if="threeCategoryShow">
<backgroundCategoryThreeSearch ref="backgroundCategoryThreeSearch" :pid="parentId" :pname="parentName" @returnBack="returnThreeBack" />
</div>
</div>
</template>
<script>
import backgroundCategoryOneSearch from '@/components/productManage/backgroundCategoryOneSearch'
import backgroundCategoryTwoSearch from '@/components/productManage/backgroundCategoryTwoSearch'
import backgroundCategoryThreeSearch from '@/components/productManage/backgroundCategoryThreeSearch'
export default {
components: {
backgroundCategoryOneSearch,
backgroundCategoryTwoSearch,
backgroundCategoryThreeSearch },
data() {
return {
// 一级类目
oneCategoryShow: false,
// 二级类目
twoCategoryShow: false,
// 三级类目
threeCategoryShow: false,
parentId: 0,
parentName: '',
catOneId: 0
}
},
created() {
// 显示一级类目
this.oneCategoryShow = true
},
methods: {
// 二级回退
returnTwoBack() {
// 一级二级三级类目显示设置
this.oneCategoryShow = true
this.twoCategoryShow = false
this.threeCategoryShow = false
},
// 三级回退
returnThreeBack(pid, pname) {
// 一级二级三级类目显示设置
this.oneCategoryShow = false
this.twoCategoryShow = true
this.threeCategoryShow = false
this.parentId = this.catOneId
this.parentName = pname
console.log(this.parentId)
},
// 一级查看下级类目
lookOneSubordinate(row) {
// 一级二级三级类目显示设置
this.oneCategoryShow = false
this.twoCategoryShow = true
this.threeCategoryShow = false
this.parentId = row.categoryId
this.parentName = row.categoryName
this.catOneId = row.categoryId
},
// 二级查看下级类目
lookTwoSubordinate(row) {
// 一级二级三级类目显示设置
this.oneCategoryShow = false
this.twoCategoryShow = false
this.threeCategoryShow = true
console.log(row.categoryId)
this.parentId = row.categoryId
this.parentName = row.categoryName
}
}
}
</script>
<style scoped>
</style>
值得注意的是,在查看下级和返回上级种,涉及组件之间的参数传递。您需要将,本级类目的ID作为父ID传入下级,而在下级返回上级的操作种,您需要将上上级的id作为父ID传入上一级列表页面(二级除外)。
关于父子组件之间的通信问题,在之前的文章中已经讲述过很多了,这里就不再赘述了。
后台类目实现
其实就后台类目的功能而言,目前来说相对简单,最主要的是建立父子关联这样一个自关联的概念。
由于之前已经给出了我们自己定义的代码生成器,属性组的实现也相对简单,考虑到篇幅问题,这一部分我们给出Controller层面的功能实现,service、和dao,还是希望你自行实现,在初学时期,多谢代码,对你熟悉掌握代码编写的技巧,是一个必不可少的环节。
/**
* Copyright(c) 2004-2020 pangzi
* com.pz.basic.mall.controller.product.category.MallCategoryController.java
*/
package com.pz.basic.mall.controller.product.category;
import com.pz.basic.mall.domain.base.Result;
import com.pz.basic.mall.domain.product.category.MallCategory;
import com.pz.basic.mall.domain.product.category.query.QueryMallCategory;
import com.pz.basic.mall.service.product.category.MallCategoryService;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
*
* @author pangzi
* @date 2020-06-22 20:47:27
*
*
*/
@RestController
@RequestMapping("/category")
public class MallCategoryController {
private MallCategoryService mallCategoryService;
public void setMallCategoryService(MallCategoryService mallCategoryService) {
this.mallCategoryService = mallCategoryService;
}
/**
* 新增类目
* @param mallCategory
* @return
*/
@RequestMapping("/addMallCategory")
public Result<MallCategory> addMallCategory(@RequestBody MallCategory mallCategory){
try{
return mallCategoryService.addMallCategory(mallCategory);
}catch(Exception e){
e.printStackTrace();
return new Result(false);
}
}
/**
* 修改类目
* @param mallCategory
* @return
*/
@RequestMapping("/updateMallCategory")
public Result updateMallCategory(@RequestBody MallCategory mallCategory){
try{
return mallCategoryService.updateMallCategoryById(mallCategory);
}catch(Exception e){
e.printStackTrace();
return new Result(false);
}
}
/**
* 分页返回类目列表
* @param queryMallCategory
* @return
*/
@RequestMapping("/findByPage")
public Result<List<MallCategory>> findByPage(@RequestBody QueryMallCategory queryMallCategory){
return mallCategoryService.getMallCategorysByPage(queryMallCategory);
}
/**
* 不分页返回类目列表
* @param queryMallCategory
* @return
*/
@RequestMapping("/findByQuery")
public Result<List<MallCategory>> findByQuery(@RequestBody QueryMallCategory queryMallCategory){
return mallCategoryService.getMallCategorysByQuery(queryMallCategory);
}
}
需要注意的是这个“不分页返回类目列表”的实现,因为在后续的很多场景中,往往要求获取整个查询条件下的后台类目,此时封装一个区别于分页的接口,算是一种预先考虑的目的。
后台类目前端实现
聊完了后端数据接口的事情,我们一起来看看前端实现的问题,考虑到大部分朋友前端并不是很熟悉,我们再讲讲后台类目前端API组件的封装。
我们先封装访问后端的数据接口:
// 类目
export function fetchCategoryList(query) {
return request({
url: '/category/findByPage',
method: 'post',
data: query
})
}
// 类目
export function fetchCategoryListNoPage(query) {
return request({
url: '/category/findByQuery',
method: 'post',
data: query
})
}
export function createMallCategory(data) {
return request({
url: '/category/addMallCategory',
method: 'post',
data
})
}
export function updateMallCategory(data) {
return request({
url: '/category/updateMallCategory',
method: 'post',
data
})
}
然后在组件里引用它。
考虑到篇幅问题,各级类目功能相似的问题以及你只有真的自己动手才能学到东西的问题,我们给出二级类目的实现页面给你。
<template>
<div id="backgroundCategoryTwoSearchDiv">
<div style="float:right">
<span @click="returnBack">< <span style="font-size:15px;margin-top:50px;line-height: 30px;">返回上一级</span></span>
<el-button type="primary" icon="el-icon-edit" style="margin-bottom:20px;float: left;margin-right: 40px;" @click="addDate()">新增二级类目</el-button>
</div>
<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.categoryId }}</template>
</el-table-column>
<el-table-column label="类目名称">
<template slot-scope="scope">{{ scope.row.categoryName }}</template>
</el-table-column>
<el-table-column label="父类目ID">
{{ pid }}
</el-table-column>
<el-table-column label="父类目名称">
{{ pname }}
</el-table-column>
<el-table-column label="类目级别">
二级类目
</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">{{ scope.row.active == 1 ? "有效" : "无效" }}</template>
</el-table-column>
<el-table-column label="类目属性" width="230px;">
<template slot-scope="scope">
<!-- <div style="display: inline-flex;"> -->
<el-select v-model="scope.row.active" clearable placeholder="请选择" style="width:50%;">
<el-option
v-for="item in propertyTypeList"
:key="item.value + '^-^'"
:label="item.label"
:value="item.value"
/>
</el-select>
<a style="color:#4395ff" @click="handleManagement(scope.row)">管理</a>
<!-- </div> -->
</template>
</el-table-column>
<el-table-column label="操作" width="200">
<template slot-scope="scope">
<el-button
type="primary"
size="mini"
@click="handleSubordinate(scope.row)"
>查看下级
</el-button>
<el-button
type="primary"
size="mini"
@click="handleUpdate(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.limit" @pagination="getList" />
<!-- 新增/编辑弹框 -->
<el-dialog :title="textMap[dialogStatus]" :visible.sync="dialogFormVisible">
<el-form ref="dataForm" :rules="rules" :model="temp" label-position="right" label-width="120px" style="width: 320px; margin-left:50px;">
<el-form-item label="类目名称:" prop="categoryName">
<el-input v-model="temp.categoryName" placeholder="请输入类目名称" />
</el-form-item>
<el-form-item label="类目排序:" prop="sortOrder">
<el-input-number
v-model="temp.sortOrder"
:min="0"
:max="100"
placeholder="请输入类目排序"
/>
</el-form-item>
<el-form-item label="是否上架:" prop="status">
<el-select v-model="temp.status" placeholder="请选择">
<el-option
v-for="(item,index) in valueList"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
<el-form-item label="是否有效:" prop="active">
<el-select v-model="temp.active" placeholder="请选择">
<el-option
v-for="(item,index) in valueList"
:key="index"
:label="item.label"
:value="item.value"
/>
</el-select>
</el-form-item>
</el-form>
<div slot="footer">
<el-button @click="dialogFormVisible = false">
取消
</el-button>
<el-button type="primary" @click="dialogStatus==='create'?createData():updateData()">
确定
</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import Pagination from '@/components/Pagination' // secondary package based on el-pagination
import { fetchCategoryList, createMallCategory, updateMallCategory } from '@/api/product-manage'
export default {
components: { Pagination },
props: {
pid: Number,
pname: String
},
data() {
return {
dialogStatus: '',
// 弹框是否显示
dialogFormVisible: false,
// 弹框校验规则
rules: {
categoryName: [{ required: true, message: '请输入类目名称', trigger: 'change' }],
status: [{ required: true, message: '请选择是否上架', trigger: 'change' }],
sortOrder: [{ required: true, message: '请输入排序号', trigger: 'blur' }],
active: [{ required: true, message: '请选择是否有效', trigger: 'blur' }]
},
temp: {
categoryId: undefined,
// 类目名称:
categoryName: '',
// 是否上柜
status: 1,
// 类目排序
sortOrder: 0,
// 是否有效:
active: 1,
parentId: this.pid
},
// 状态
valueList: [
{
value: 1,
label: '是'
}, {
value: 0,
label: '否'
}
],
textMap: {
update: '二级类目修改',
create: '二级类目新增'
},
// table集合
list: null,
multipleSelection: [],
// 分页
total: 0,
// loading
listLoading: true,
// 属性类型集合
propertyTypeList: [
{
value: 1,
label: '公共属性'
}, {
value: 2,
label: '特殊属性'
}, {
value: 3,
label: '文字销售属性'
}, {
value: 4,
label: '图片销售属性'
}
],
listQuery: {
// 类目属性
page: 1,
pageSize: 20,
level: 1,
parentId: this.pid
}
}
},
created() {
// 列表查询
this.getList()
},
methods: {
/**
* 回退
*/
returnBack() {
this.$emit('returnBack')
},
// 管理
handleManagement(row) {
// 实际需要传参
this.$router.push({ path: '/shops/background-category-attribute' })
},
// 查看下级
handleSubordinate(row) {
this.$emit('lookSubordinate', row)
},
// 编辑
handleUpdate(row) {
this.temp = Object.assign({}, row) // copy obj
this.dialogStatus = 'update'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
// 重置
resetTemp() {
this.temp = {
categoryId: undefined,
// 类目名称:
categoryName: '',
// 是否上柜
status: 1,
// 类目排序
sortOrder: 0,
// 是否有效:
active: 1,
level: 1,
parentId: this.pid
}
},
// 新增一级类目
addDate() {
this.resetTemp()
this.dialogStatus = 'create'
this.dialogFormVisible = true
this.$nextTick(() => {
this.$refs['dataForm'].clearValidate()
})
},
// 更新保存方法
updateData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
const tempData = Object.assign({}, this.temp)
updateMallCategory(tempData).then(() => {
const index = this.list.findIndex(v => v.id === this.temp.id)
this.list.splice(index, 1, this.temp)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Update Successfully',
type: 'success',
duration: 2000
})
})
}
})
},
// 创建保存方法
createData() {
this.$refs['dataForm'].validate((valid) => {
if (valid) {
createMallCategory(this.temp).then((res) => {
this.temp.categoryId = res.model.categoryId
this.list.unshift(this.temp)
this.dialogFormVisible = false
this.$notify({
title: 'Success',
message: 'Created Successfully',
type: 'success',
duration: 2000
})
})
}
})
},
// 列表查询
getList() {
this.listLoading = true
fetchCategoryList(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)
})
}
}
}
</script>
<style scoped>
</style>
值得注意的是,在父子组件通信参数传递上,和之前有一点点不一样噢。
出于信息展示的目的,我们需要把父类目的名称,进行传递,从而满足页面展示的需求同时,达到减少非必要信息查询的目的。
到目前为止,后台类目的基本功能我们就开发完毕了。大家一定要自己动手去实际操作,记得多多联调和测试噢。