电商项目——初识电商——第一章——上篇
电商项目——分布式基础概念和电商项目微服务架构图,划分图的详解——第二章——上篇
电商项目——电商项目的虚拟机环境搭建_VirtualBox,Vagrant——第三章——上篇
电商项目——Linux虚拟机中安装docker,mysql,redis_VirtualBox——第四章——上篇
电商项目——电商项目的环境搭建_开发工具&环境搭建——第五章——上篇
电商项目——快速开发人人开源搭建后台管理系统&代码生成器逆向工程搭建——第六章——上篇
电商项目——分布式组件(SpringCloud Alibaba,SpringCloud)——第七章——上篇
电商项目——前端基础——第八章——上篇
电商项目——商品服务-API-三级分类——第九章——上篇
电商项目——商品服务-API-品牌管理——第十章——上篇
电商项目——商品服务-API-属性分组——第十一章——上篇
电商项目——商品服务-API-品牌管理——第十二章——上篇
电商项目——商品服务-API-平台属性——第十三章——上篇
电商项目——商品服务-API-新增商品——第十四章——上篇
电商项目——商品服务-API-商品管理——第十五章——上篇
电商项目——商品服务-API-仓库管理——第十六章——上篇
讲完了前面几章的内容,现在我们就可以对后台管理系统,和微服务项目进行开发了,我们先从三级分类说起
默认mall-product的环境已经全部配置好了
三级分类(电商里面经常用到的功能):所有的数据都是来源于数据库,我们要对三级分类进行维护,进行增删改查,我们首先就必须要后台管理系统来可以维护我们的整个数据。所以我们引出了下面的问题,解决了下面问题,我们就可以在搭建前端界面进行前后端连接
问题:如何查出所有三级分类以及子分类,并以树形结构组装起来?,我们看这篇的思路分析
电商项目——如何查出所有三级分类,并以树形结构组装起来?
接下来我们来编写后台管理系统的前端项目,来维护三级分类的增删改查
我们先进行测试看是否后台管理系统可以成功启动
启动renren-fast和renren-fast-vue
第一步:我们要写商品系统的相关内容,看如下操作
跟商品系统有关的项目都放在我自己新增的商品系统目录下,然后我们在商品系统的目录下,增加一个菜单
最终的效果如下
搭建分类维护功能,我们期望展示出整个三级分类,然后可以对三级分类进行增删改查,而我们想要做这个功能,就要先了解脚手架工程的一些基本规范
脚手架工程的基本规范1:
脚手架工程的基本规范2:
第二步:在renren-fast-vue中搭建分类维护路径的对应目录如下,并进行前端显示三级分类数据的搭建
我们就可以在category中使用ElementUI进行前端显示三级分类数据的搭建
第三步:在renren-fast-vue中的src/views/modules/product/路径下搭建category.vue
我们使用ElementUI组件中的Tree树形控件来完成我们的功能,并且还要向后端发送请求在返回数据给前端
代码如下
我们进行测试
解决办法:我们要改变基本路径http://localhost:8080/renren-fast的值把它变成网关的基本路径(我们以后要上线很多微服务项目,我们不肯能一直去renren-fast-vue中一直修改基本路径的端口,所以我们把所有的请求发送给网关,网关在分配到指定的路径的微服务中)
在进行测试,发现下面问题
解决办法:把renren-fast注册到注册中心中,并且配置网关的routes
renren-fast
<dependency>
<groupId>com.atstudying.mallgroupId>
<artifactId>mall-commonartifactId>
<version>0.0.1-SNAPSHOTversion>
dependency>
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: localhost:8848
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
从8001访问到88出现了跨域问题,默认拒绝跨域请求
我们下一章就来介绍如何解决跨域问题
上一小节,我们配置了网关路由和路径重写(renren-fast),我们的验证码也刷新出来了,但是我们在登录测试的时候,发现报了一个跨域的问题
什么是跨域呢?怎么解决跨域问题呢?下面这篇文章为大家一一解答
HTTP访问控制(CORS)——预检请求问题的解决
上面这篇文章的第四节就是解决网关统一配置跨域的一个案例演示
我们根据上面这篇文章成功解决跨域问题后,再次登录,发现登录,发现还是登录不了,进行检查,发现下面问题
解决办法:renren-fast中注释掉有关跨域的代码
重新启动renren-fast,并重新登录,发现登录成功
上节我们解决了跨域,我们现在来继续编写商品系统目录下分类维护菜单的代码
我么刷新上面界面,并检查元素发现,跨域问题解决了,可是请求却找不到
解决办法,我么以前配置网关路由默认是全部转到renren-fast的,所以我们的请求找不到,我们现在去网关配置一个mall-product的网关路由,让请求跳转到该路由下。
我们进行如下测试,发现数据成功获取
http://localhost:88/api/product/category/list/tree
如上的地址经过网关会变成如下地址
http://localhost:30000/product/category/list/tree
因为,我们配置的网关路由在起作用,如上两个地址的访问都会得到下面的图的效果
- id: mall-product
uri: lb://mall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{
segment}
我们看见数据已经成功返回给了前端界面,可是我们要怎么展示呢?
category.vue
methods: {
handleNodeClick(data){
console.log(data)
},
// 获取数据列表
getDataList () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then( data => {
console.log("成功获取到菜单数据:"+data)
})
}
created () {
//
this.getDataList();
},
我们发现data中有很多属性,可是我们要的数据是在data.data中,所以我们可以在前端的category.vue中进行项目解构
如下category.vue中部分代码演示片段({data})(解构)
// 获取数据列表
getDataList () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then( ({data}) => {
console.log("成功获取到菜单数据:"+data.data)
})
}
完整的代码如下
category.vue
我们先来编写菜单删除功能,哪些菜单可以被删除呢?那就是没有子菜单,并且没有被别的地方引用的菜单,操作如下
第一步:我们先来展示前端界面的删除效果,我们去eliment中找到Tree树形控件下的自定义节点内容,把它的代码引入,如下代码演示
{
{ node.label }}
append(data)">
Append
remove(node, data)">
Delete
就会变成如下效果
第二步:只有我们这个菜单是一级菜单或者是二级菜单的时候才显示append按钮(三级分类不可以追加元素);无论是一级分类还是二级分类,只要我们没子节点就可以删除;所以我们进行修改按钮,在合适的时机显示出来,添加如下v-if语句,v-if里面的判断值怎么得出呢?
我们要进行检查界面,观察点击按钮后,在控制台上打印值的变化,如下
append(data)"
v-if="data.catLevel<=2"
>
Append
remove(node, data)"
v-if="data.children.length==0"
>
这章我们就来编写发送真正的请求,来进行删除功能的完成
在mall-product中找到CategoryController.java
CategoryController.java
如下方法中我们必须要知道一些知识
@RestController
@RequestMapping("product/category")
public class CategoryController {
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
categoryService.removeByIds(Arrays.asList(catIds));
return R.ok();
}
我们推荐大家使用postman来发送请求(可以进行模拟测试)
进行如下测试
我们在数据库中插入一个id=10000的数据
insert into `pms_category`(`cat_id`,`name`,`parent_cid`,`cat_level`,`show_status`,`sort`,`icon`,`product_unit`,`product_count`) values (10000,'测试',0,1,1,0,NULL,NULL,0)
测试是否可以删除成功
返回如下值,说明删除成功,数据库中也没有10000的值
{
"msg": "success",
"code": 0
}
但是我们真正的删除功能没有这么简单,如下进行简单演示
CategoryController.java
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//1:检查当前删除的菜单,是否被其他地方引用
// categoryService.removeByIds(Arrays.asList(catIds));
//Arrays.asList()该方法是将数组转化成List集合的方法。
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
CategoryServiceImpl.java
@Override
public void removeMenuByIds(List<Long> asList) {
//1:todo 检查当前删除的菜单,是否被别的地方引用
//原生调用basemapper中的deleteBatchIds批量删除方法
//这个是物理删除,删除完是真的没了,
baseMapper.deleteBatchIds(asList);
}
我们用到上面的删除(物理删除),还要考虑到检查当前删除的菜单,是否被别的地方引用,以后考虑
我们在这里用逻辑删除的方法来实现
怎么配置逻辑删除呢?看下面步骤
3.1.1以后的mybatis-plus-boot-starter可以省略一二两步
第一步:配置全局的逻辑删除规则(可以省略)
mall-product中的nacos配置文件
第二步:配置逻辑删除的组件Bean(省略)
第三步:给实体类的字段上加上逻辑删除注解
CategoryEntity.java
//...
/**
* 是否显示[0-不显示,1显示]
* * String value() default "";
String delval() default "";
*/
@TableLogic(value = "1",delval="0")
private Integer showStatus;
进行测试:
我们可以调整日志级别:让dao下的日志也可以打印
接下来我们完成点击删除,发送请求,删除菜单的功能
后端有关的代码
@RestController
@RequestMapping("product/category")
public class CategoryController {
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
前端有关的代码进行编写
remove(node, data) {
console.log(node,data);
// 获取数据列表
var ids=[data.catId]
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids,false)
}).then( ({data}) => {
console.log("删除成功:")
this.menus=data.data;
})
},
未实现的功能:我们要完成删除一个按钮,提示框会弹出来(是否确认删除这个节点),删除完以后,还是要展开的状态,所以我们完成下面的操作来实现功能
category.vue
部分代码展示片段
<el-tree
:data="menus"
show-checkbox
:props="defaultProps"
node-key="catId"
:default-expanded-keys="expandkey"
:expand-on-click-node="false">
data () {
// 这里存放数据",
return {
expandkey: [],
menus: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
methods: {
remove(node, data) {
console.log(node,data);
// 获取数据列表
var ids=[data.catId]
this.$confirm('是否删除【${data.name}】菜单', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids,false)
}).then( ({
data}) => {
this.$message({
type: '菜单删除成功',
message: '删除成功!'
});
this.getDataList();
this.expandkey=node.parent.data.catId;
console.log(this.expandkey)
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
接下来我们编写点击Append为它增加子分类的功能
前端页面效果演示:希望点击append,可以出来一个对话框,点击确定按钮,完成新增,如下
思路:
我们就要在对话框中的确定按钮编写一个监听事件addCategory(),这个事件里面我们写一个发生新增路径的请求方法,然后测试就可以完成上面的演示
代码和第九章的代码全部整合了
前端页面效果演示:
当我们点击修改按钮的时候,弹出一个对话框,数据都会回显到对话框中,然后我们再来动态输入新的内容,输入完以后点击确定,完成真正的修改
思路:
监听按钮
alter(data)"
>
Alter
监听事件
// 修改按钮
alter(data){
console.log("修改按钮");
this.dialogVisible= true;
this.flag=1;
// console.log("当前节点的数据",data)
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get'
// 服务端传送的数据
}).then(({data})=>{
console.log("查询当前节点后的节信息",data)
this.category.productUnit=data.category.productUnit;
this.category.icon=data.category.icon;
this.category.name=data.category.name;
this.category.catId=data.category.catId;
this.category.catLevel=data.category.catLevel;
this.category.showStatus=data.category.showStatus;
this.category.sort=data.category.sort;
this.category.productCount=data.category.productCount;
console.log("发送查询请求后,赋值给category的数据展示",this.category)
})
},
对话框的代码(使用Element UI编写的)
对话框中按钮确 定中触发的点击事件updateCategory()的代码
//修改三级分类数据
updateCategory(){
console.log("修改当前节点的数据");
console.log("当前节点的category",this.category);
// 对象发送出去给后端,选择指定的对象的值
var {catId,productUnit,name,icon}=this.category;
this.$http({
url: this.$http.adornUrl(`/product/category/update`),
method: 'post',
data: this.$http.adornData(this.category,false)
// 服务端传送的数据
}).then(({data}) =>{
this.$message({
type: '数据修改成功',
message: '修改成功!'
});
this.getDataList();
this.dialogVisible=false;
// this.expandedKey = [this.category.parentCid]
console.log("数据修改成功")
})
},
如下是完整版的代码演示
category.vue
<template>
<!-- 使用 scoped slot 会传入两个参数node和data,分别表示当前节点的 Node 对象和当前节点的数据-->
<!--label 指定节点标签为节点对象的某个属性值,,children 指定子树为节点对象的某个属性值,,,data 展示数据-->
<!-- node-key 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的-->
<!-- default-expand-all 是否默认展开所有节点-->
<!-- default-expanded-keys 默认展开的节点的 key 的数组-->
<!-- :default-expanded-keys="expandkey"-->
<!-- expand-on-click-node 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。-->
<div>
<el-tree
:data="menus"
show-checkbox
:title="titlevalue"
:props="defaultProps"
node-key="catId"
:expand-on-click-node="false">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{
{
node.label }}</span>
<span>
<el-button
type="text"
size="mini"
@click="() => append(data)"
v-if="data.catLevel<=2"
>
Append
</el-button>
<el-button
type="text"
size="mini"
@click="() => remove(node, data)"
v-if="data.children.length==0"
>
Delete
</el-button>
<el-button
type="text"
size="mini"
@click="() => alter(data)"
>
Alter
</el-button>
</span>
</span>
</el-tree>
<!-- 需要设置visible属性,它接收Boolean,当为true时显示 Dialog-->
<el-dialog
title="商品数据"
:visible.sync="dialogVisible"
width="30%"
>
<el-form :model="category">
<el-form-item label="商品名称" :label-width="formLabelWidth">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="商品图标地址" :label-width="formLabelWidth">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="商品计量单位" :label-width="formLabelWidth">
<el-input v-model="category.productUnit" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisible= false">取 消</el-button>
<el-button type="primary" @click="flag==0?addCategory():updateCategory()">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
export default {
// import引入的组件需要注入到对象中才能使用",
components: {
},
data () {
// 这里存放数据",
return {
titlevalue: '',
flag: '',
dialogVisible: false,
expandkey: [],
menus: [],
defaultProps: {
children: 'children',
label: 'name'
},
category: {
name: '',
parentCid: '',
catLevel: '',
showStatus: 1,
sort: 1,
icon: '',
productUnit: '',
catId: ''
},
formLabelWidth: '120px'
}
},
// 监听属性 类似于data概念",
computed: {
},
// 监控data中的数据变化",
watch: {
},
// 方法集合",
methods: {
//修改三级分类数据
updateCategory(){
console.log("修改当前节点的数据");
console.log("当前节点的category",this.category);
// 对象发送出去给后端,选择指定的对象的值
var {
catId,productUnit,name,icon}=this.category;
this.$http({
url: this.$http.adornUrl(`/product/category/update`),
method: 'post',
data: this.$http.adornData(this.category,false)
// 服务端传送的数据
}).then(({
data}) =>{
this.$message({
type: '数据修改成功',
message: '修改成功!'
});
this.getDataList();
this.dialogVisible=false;
// this.expandedKey = [this.category.parentCid]
console.log("数据修改成功")
})
},
//添加三级分类数据
addCategory(){
console.log("增加三级分类数据");
console.log("提交三级分类数据",this.category);
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category,false)
// ,this.category.parentCid,this.category.catLevel,this.category.icon,this.category.productUnit,this.category.sort,this.category.showStatus
}).then( ({
data}) => {
this.$message({
type: '数据增加成功',
message: '增加成功!'
});
this.getDataList();
this.dialogVisible=false;
// this.expandedKey = [this.category.parentCid]
console.log("数据增加成功")
})
},
// 修改按钮
alter(data){
console.log("修改按钮");
this.dialogVisible= true;
this.flag=1;
this.titlevalue="修改商品数据";
// console.log("当前节点的数据",data)
this.$http({
url: this.$http.adornUrl(`/product/category/info/${
data.catId}`),
method: 'get'
// 服务端传送的数据
}).then(({
data})=>{
console.log("查询当前节点后的节信息",data)
this.category.productUnit=data.category.productUnit;
this.category.icon=data.category.icon;
this.category.name=data.category.name;
this.category.catId=data.category.catId;
this.category.catLevel=data.category.catLevel;
this.category.showStatus=data.category.showStatus;
this.category.sort=data.category.sort;
this.category.productCount=data.category.productCount;
console.log("发送查询请求后,赋值给category的数据展示",this.category)
})
},
//增加按钮
append(data) {
console.log("增加按钮")
this.dialogVisible= true;
this.flag=0;
this.titlevalue="增加商品";
// console.log(data);
this.category.parentCid=data.catId;
// 防止变成字符串,所以乘1+1
this.category.catLevel=data.catLevel*1+1;
// console.log(this.category.parentCid);
// console.log(this.category.catLevel);
},
//删除按钮
remove(node, data) {
console.log("删除按钮");
console.log(node,data);
// 获取数据列表
var ids=[data.catId]
// console.log(data.name)
// eslint-disable-next-line no-template-curly-in-string
this.$confirm('是否删除【${data.name}】菜单', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.$http({
url: this.$http.adornUrl('/product/category/delete'),
method: 'post',
data: this.$http.adornData(ids,false)
}).then( ({
data}) => {
this.$message({
type: '菜单删除成功',
message: '删除成功!'
});
this.getDataList();
this.expandedKey = [node.parent.data.catId]
console.log('删除成功')
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
});
});
},
// 获取数据列表
getDataList () {
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then( ({
data}) => {
console.log("成功获取到菜单数据:",data.data)
this.menus=data.data;
// console.log(this.menus)
})
}
},
// 生命周期 - 创建之前",数据模型未加载,方法未加载,html模板未加载
beforeCreate () {
},
// 生命周期 - 创建完成(可以访问当前this实例)",数据模型已加载,方法已加载,html模板已加载,html模板未渲染
created () {
//
this.getDataList();
},
}
</script>
完成之后的效果就和前端页面效果演示的一样
思路
核心点:无论拖动任何节点,这个节点的最终总层数不可以大于3
我们先去elementui中查找有关拖拽的文档资料如下,我们使用它来判断是否可以被拖拽放到指定位置,就可以编写
:allow-drop=“allowDrop”
allow-drop 拖拽时判定目标节点能否被放置。type 参数有三种情况:'prev'、'inner' 和 'next',分别表示放置在目标节点前、插入至目标节点和放置在目标节点后 Function(draggingNode, dropNode, type)
//拖拽方法
allowDrop(draggingNode, dropNode, type){
//1:被拖拽节点以及所在节点的父节点总层数不大于3
console.log("要拖拽的节点",draggingNode,dropNode,type);
this.countLevel(draggingNode.data)
//1:当前正在拖动的节点+父节点所在深度不大于3即可
let deep =this.maxLevel - draggingNode.level + 1
console.log("深度:"+deep)
if (type=="inner"){
//返回true,可以拖动
console.log("inner:"+(deep+dropNode.level))
return (deep+dropNode.level)<=3;
}else {
// console.log("dropNode.parent.level:"+dropNode.parent.level)
console.log("非inner:"+(deep+dropNode.parent.level))
return (deep+dropNode.parent.level)<=3;
}
return ;
},
countLevel(node){
//找到所有子节点并求出最大深度
console.log("要脱拽当前节点的子节点",node);
if (node.children!=null && node.children.length >0){
for (let i=0;i this.maxLevel){
this.maxLevel=node.children[i].catLevel;
}
this.countLevel(node.children[i]);
}
}
}
未完成:
实现:当我们完成拖拽的时候就应该把数据的最新信息发送到数据库进行保存,这些信息就包含最新的父节点id,排序,最新该节点的层级
思路:
node-drop 拖拽成功完成时触发的事件 共四个参数,依次为:被拖拽节点对应的 Node、结束拖拽时最后进入的节点、被拖拽节点的放置位置(before、after、inner)、event
未完成
实现:我们需要拖动的时候,我们在拖动它,不需要就禁止它
未完成
实现:我们要完成如下功能,点击一些数据,我们点击批量删除即可,全部删除(逻辑删除)
思路:去Elimentui,Tree树形控件中寻找有关可选节点被选择,就可以返回目前被选中的节点所组成的数组的有关信息,果然找到了如下
那要怎么用呢?我们在Elimentui中继续找有关用法,发现节点过滤有它的用法
this.$refs.tree.getCheckedNodes()
this.$refs指定是当前vue实例里的所有组件(el-button,el-dialog,el-form),
我们拿到哪个组件呢?我们在tree组件里写一个ref="tree",拿到this.$refs.tree的组件
批量删除
监听事件代码如下
//批量删除,拖拽的数据
batchDelete(){
let catIds=[];
let checkNodes=this.$refs.tree.getCheckedNodes();
console.log("被选中的元素",checkNodes);
for (let i=0;i{
this.$http({
url: this.$http.adornUrl(`/product/category/delete`),
method: 'post',
data: this.$http.adornData(catIds,false)
// 服务端传送的数据
}).then(({data}) =>{
this.$message({
type: '批量删除数据成功',
message: '批量删除数据成功!'
});
this.getDataList();
console.log("批量删除数据成功")
})
})
要想让上面配置生效,我们还得在树形控件中,加入如下代码
完成操作,即可成功演示上面代码
使用人人开源管理后台系统来体会前后端分离技术