商品服务-API-三级分类
三级分类增删改查
整个项目采用springboot 2.1.8.RELEASE版本
准备工作:
- 自动生成vue模板功能(新建vue组件,输入vue按回车,会自动生成vue模板)
- 模板里面49行,这一段生成了之后会报错,建议删除 。"//@import url($3); 引入公共css类"。
https://www.cnblogs.com/songjilong/p/12635448.html
- element ui 官网地址
https://element.eleme.cn/#/zh-CN/component/tree#scoped-slot
- VScode格式化HTML代码保持标签属性不换行
标签属性不换行方便阅读标签内容
文件-首选项-设置-Vetur-在setting.json中编辑
{
"workbench.colorTheme": "Default Dark+",
"editor.suggestSelection": "first",
"vsintellicode.modify.editor.suggestSelection": "automaticallyOverrodeDefaultValue",
"java.requirements.JDK11Warning": false,
"java.configuration.checkProjectSettingsExclusions": false,
"java.project.importHint": false,
"extensions.autoUpdate": false,
"vetur.completion.scaffoldSnippetSources": {
"workspace": "💼",
"user": "🗒️",
"vetur": "✌"
},
"vetur.format.defaultFormatter.html": "js-beautify-html",
"vetur.format.defaultFormatterOptions": {
"js-beautify-html": {
"wrap_line_length": 220,
"wrap_attributes": "auto",
"end_with_newline": false
}
}
}
1. 配置网关路由与路径重写
1.1 前端编写vue组件。
新建组件所在位置:
src/views/modules/product/category.vue
创建组件(即调用前端请求打开该页面)即调用方法给后台发请求获取数据并在控制台打印
methods: {
getMenus(){
this.dataListLoading = true,
this.$http({
//获取所有三级分类数据,进行url拼接后发送请求。
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
}).then(({data}) => {
console.log("成功获取数据",data);
})
},
},
//组件创建完成即调用方法给后台发请求
created() {
this.getMenus();
},
保存测试
- 发现请求地址和我们想的不一样,这是因为前端请求没有统一交给网关进行管理。

1.2 解决请求地址问题
解决方案
- 将请求地址前部分统一改为网关地址。这样无论发送什么请求,都会先经过网关进行处理。
- 在
static/config/index.js中修改网关地址前缀。
/**
* 开发环境
*/
;(function () {
window.SITE_CONFIG = {}
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'
// cdn地址 = 域名 + 版本号
window.SITE_CONFIG['domain'] = './' // 域名
window.SITE_CONFIG['version'] = '' // 版本号(年月日时分)
window.SITE_CONFIG['cdnUrl'] = window.SITE_CONFIG.domain + window.SITE_CONFIG.version
})()

- 更改请求路径后刷新,发现页面验证码请求不到

1.3 解决验证码不显示问题
问题所在:
- 前端将请求都交给网关来处理了,如果网关没配置,那么请求验证码的请求也会发给网关服务。
- 网关服务没有处理验证码的controller。需要将请求转发给renren-fast这个服务才行。
将验证码请求交给网关管理。网关需要先发现renren-fast服务。
将renren-fast服务(8080端口)注册在nacos
- 引入common依赖(包含nacos依赖)
<dependency>
<groupId>com.atguigu.gulimall</groupId>
<artifactId>gulimall-common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
- 编写配置文件,配置应用名和服务发现地址
spring:
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
- 开启服务发现功能
@EnableDiscoveryClient
@SpringBootApplication
public class RenrenApplication {
public static void main(String[] args) {
SpringApplication.run(RenrenApplication.class, args);
}
}
- 登录nacos查看,服务已开启

- 在网关服务中配置路由规则
- 前端请求都是/api开头的。都转给renren-fast服务
- 目前请求:http://localhost:88/api/captcha.jpg
- 实际需要:http://localhost:8080/renren-fast/captcha.jpg
- 所以需要进行路径映射
- spring-cloud-gateway配置路由映射规则官网连接
spring:
cloud:
gateway:
routes:
- id: admin_router
# lb 表示负载均衡
uri: lb://renren-fast
# 前端请求都是/api开头的。都转给renren-fast服务
predicates:
- Path=/api/**
# 目前请求:http://localhost:88/api/captcha.jpg
# 实际需要:http://localhost:8080/renren-fast/captcha.jpg
filters:
- RewritePath=/api/(?<segment>.*), /renren-fast/$\{segment}
- 重启服务,刷新页面 。此时,能收到验证码了。

- 输入账号密码,验证码,点击登录,发现跨域问题。
- 从源 'http://localhost:8001' 通过异步(XMLHttpRequest )访问到 'http://localhost:88/api/sys/login' 已被 CORS 策略阻止:对预检请求的响应未通过访问控制检查:请求的资源上不存在“Access-Control-Allow-Origin”请求头

1.4 解决跨域问题
什么是跨域:

解决方案:
- 使用nginx代理。浏览器将所有请求都交给nginx处理。开发过程不方便使用该方法。
- 配置当次请求允许跨域
添加响应头
- Access-Control-Allow-Origin:支持哪些来源的请求跨域.
- Access-Control-Allow-Methods:支持哪些方法跨域
- Access-Control-Allow-Credentials:跨域请求默认不包含cookie,设置为true可以包含cookie.
- Access-Control-Expose-Headers:跨域请求暴露的字段
- CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。
- Access-Control-Max-Age:表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将不会生效。
具体实现:
在网关服务写一个filter类,所有请求都会先经过该过滤器。添加响应头后放行。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.cors.reactive.CorsWebFilter;
@Configuration
public class MyCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter(){
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration configuration = new CorsConfiguration();
//1.配置跨域
configuration.addAllowedHeader("*");
configuration.addAllowedMethod("*");
configuration.addAllowedOrigin("*");
configuration.setAllowCredentials(true);
source.registerCorsConfiguration("/**",configuration);
return new CorsWebFilter(source);
}
}
重启测试,跨域问题解决。
- 请求报错是因为renren-fast项目设置的也有跨域配置,将其注释即可


2. 树形展示三级分类数据(重要)
2.1 在网关服务配置product相关路由
filters少写个s。让我找了1个小时的错.....- 网关配置是按照配置文件上下顺序来确定优先级的。所以请求拦截
/api/product/**要在/api/**前面。
spring:
cloud:
gateway:
routes:
- id: product_router
# 去服务注册中心找该服务
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
# 目前请求:http://localhost:88/api/product/category/list/tree
# 实际需要:http://localhost:10001/product/category/list/tree
# /api舍弃,剩余部分不变。
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_router
# lb 表示负载均衡
uri: lb://renren-fast
# 前端请求都是/api开头的。都转给renren-fast服务
predicates:
- Path=/api/**
# 目前请求:http://localhost:88/api/captcha.jpg
# 实际需要:http://localhost:8080/renren-fast/captcha.jpg
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}
2.2 product服务(10001端口)在nacos注册
- 编写配置文件(服务注册地址和服务名),添加服务发现注解。
spring:
application:
name: gulimall-product
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
刷新页面
- 启动renren-fast、product、gateway服务

2.3 后端逻辑实现
- 逆向工程生成了一个获取所有数据的方法。不符合要求(不是以三级分类格式返回的)。
- 递归树形结构数据获取全部数据。
controller层,新增一个方法
/**
* 查出所有分类以及子分类,以树形列表进行组装
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities=categoryService.listWithTree();
return R.ok().put("page", entities);
}
entity层
- 新增一个属性用来存储该分类下的所有子分类(children)
- 用来存放该分类的子分类。不存在数据库表,需要加注解标注。之后使用VO,现在先使用和数据库字段相匹配的entiy。
// 用来存放该分类的子分类。不存在数据库表,需要加注解标注
@TableField(exist =false)
private List<CategoryEntity> children;
service层
- 通过递归获取该分类下的所有子分类
- 先找出一级分类,再根据一级分类找出它的子分类(二级分类)。
- 再使用递归方式,找到二级分类的子分类(三级分类)。
- 使用java8新特性,函数式编程https://www.cnblogs.com/cxnph/articles/15730101.html
Mybatis-Plus为我们封装好了一系列方法,继承baseMapper即可直接使用
Long类型 如果超过128 用== 比较的结果是false 用equals 比较是true- 因为Long是包装类,不是基本数据类型long。用equals比较。重写了equals方法,实际比较的还是内容值。
- https://blog.csdn.net/zhsh5395/article/details/80622757
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {}
public interface CategoryDao extends BaseMapper<CategoryEntity> {}
CategoryDao.xml文件不用编写任何sql语句(单表简单查询使用Mybatis-Plus为我们封装好了一系列方法即可)
-----------------------------------------------------------------------------------------------------------------
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类。baseMapper就是CategoryDao。参数是null即查找所有分类列表。
List<CategoryEntity> entities = baseMapper.selectList(null);
//2.组装父子树形结构
//2.1 找出所有一级分类(getParentCid==0的分类数据)
List<CategoryEntity> level1Menus = entities.stream().filter(e -> e.getParentCid() == 0)
.map(memu->{
//2.2将该分类的子菜单添加到children字段。映射并返回
memu.setChildren(getChildrens(memu,entities));
return memu;
})
.sorted((menu1,menu2)->
(menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort()))
.collect(Collectors.toList()); //将处理好的流收集为一个List集合。
//2.2 找到每个分类的子分类
return level1Menus;
}
//递归方法(传递当前实体和全部实体集合)
private List<CategoryEntity> getChildrens(CategoryEntity root,List<CategoryEntity> all){
List<CategoryEntity> children = all.stream().filter(e -> {
//找到父id是一级分类(上面传递的是一级分类)的分类(即二级分类)
//return e.getParentCid() == root.getCatId(); //这是一个坑,用==比较会丢失数据
return Objects.equals(e.getParentCid(), root.getCatId());
}).map(e -> {
//递归查找三级分类,并将其设置给二级分类的children。
e.setChildren(getChildrens(e, all));
return e;
}).sorted((menu1, menu2) ->
(menu1.getSort()==null?0:menu1.getSort()) - (menu2.getSort()==null?0:menu2.getSort())
).collect(Collectors.toList());
return children;
}
测试发送请求

2.4 前端展示
<template>
<!-- 展示menus数据 -->
<el-tree :data="menus" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template>
<script>
export default {
components: {},
data() {
//这里存放数据
return {
//menus接收后端数据
menus: [],
defaultProps: {
//哪个属性是子属性
children: "children",
//哪个属性用来展示
label: "name",
},
};
},
//方法集合
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({data}) => {
console.log("成功获取数据", data.page)
// 将后端请求获取的data传递给menus。
this.menus = data.page;
});
},
},
created() {
this.getMenus();
},
};
</script>
这里也出了个错。label写成laber了....
- 只要获取到后端数据,前端可以根据控制台进行调试。
- 后端返回数据是json格式的,返回对象名是data
console.log("成功获取数据", data)
所以这里需要将data.page传给menus参数进行显示。


3. 删除分类需求
3.1 前端展示(添加删除和新增按钮)
- 点击append,调用append(data)方法
- 点击delete,调用remove(node, data)方法
- 一级和二级节点才显示添加按钮 ,三级节点才显示删除按钮(使用v-if判断)
<template>
<!-- 展示menus数据 -->
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<!-- 一级和二级节点才显示添加按钮 -->
<el-button v-if="node.level <=2" type="text" size="mini" @click="() => append(data)">append</el-button>
<!-- 三级节点才显示删除按钮 -->
<el-button v-if="node.childNodes.length==0" type="text" size="mini" @click="() => remove(node, data)">delete</el-button>
</span>
</span>
</el-tree>
</template>
显示效果:

3.2 后端逻辑
controller层 逆向工程自动生成的delete方法
- @RequestBody 获取请求体的内容,springMVC将其转换为所需对象。
- 只有post方法才有请求体。请求体是json格式
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//将前端传递的数组转为集合
categoryService.removeMenusByIds(Arrays.asList(catIds));
return R.ok();
}
使用mybatis-plus实现
逻辑删除
- 使用数据库的某个字段作为标志位。删除该条数据时,只更改该字段的值,页面不进行展示。不会彻底删除该条数据。
- 官网介绍:https://baomidou.com/pages/6b03c5/
mybatis-plus 3.3.0之后
- 可以在配置文件全局配置逻辑删除字段名
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
mybatis-plus 3.3.0之前
- 该项目使用的是3.2.0。在逻辑字段上加@TableLogic注解。
- value表示显示该值。delval表示不显示该值。可自定义
- 只需要在该字段上加上注解即可实现逻辑删除。
@TableLogic(value = "1" ,delval = "0")
private Integer showStatus;
测试逻辑删除
- 设置日志级别。显示数据库操作语句
logging:
level:
com.atguigu.gulimall: debug
- postman发送删除请求。

- 实际执行sql语句

3.3 前后端联调逻辑删除某个或多个菜单
- 前面后端已实现逻辑删除功能。前端需要点击删除按钮,调用删除方法,发送post请求给后端。请求成功后调用方法获取所有数据。刷新页面
前端功能:
<span>
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">append</el-button>
<el-button v-if="node.childNodes.length ==0" type="text" size="mini" @click="() => remove(node, data)">delete</el-button>
<span>
remove(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 }) => {
//刷新页面,重新获取数据
this.getMenus();
console.log('删除成功');
});
},
后端功能:
- 启动reren-fast后端,启动gateway网关服务,启动product商品服务。
- 点击delete

需求一:点击删除按钮后,弹出提示框。确认是否删除该菜单。点击确认,调用post请求,执行逻辑删除。点击取消,什么也不干。
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
//点击确定按钮,发送post请求
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
this.getMenus();
console.log("删除成功");
});
this.$message({
type: "success",
message: "菜单删除成功!",
});
});
},
效果: 删除成功弹出删除成功。

需求二:数据删除成功后,应该依旧展开所在菜单的父节点。而不是刷新整个页面关闭所有子节点。
- elementui组件有介绍
- 删除菜单成功后,先刷新页面,显示全部菜单,然后传递默认展开菜单(已删除菜单的父菜单id)。

<el-tree :default-expanded-keys="expanded">
data() {
return {
menus: [],
//初始默认为空,默认不展开
expanded: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
//点击确定按钮,发送post请求删除
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单,传递参数。
this.expanded=[node.parent.data.catId]
});
3.4 实现批量删除功能
新增一个批量删除按钮,绑定单击事件
<el-button type="danger" size="mini" @click="batchDelete">批量删除</el-button>
单击事件处理
- 定义一个数组,用来存放选中的catId。
- 获取选中的数组元素checkNodes(element ui定义的),将元素catId添加到自定义数组中。
- 弹出是否删除框
- 点击是,给后端发删除请求。传递需要删除的catId数组。
- 后端执行成功,刷新菜单。弹出提示框“菜单批量删除成功!”。
batchDelete() {
let catIds=[];
let checkNodes=this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId);
}
this.$confirm(`是否批量删除菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//点击确定按钮,发送post请求删除
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false),
}).then(({ data }) => {
//刷新出新的菜单
this.getMenus();
});
this.$message({
type: "success",
message: "菜单批量删除成功!",
});
})
.catch(() => {});
},
4. 新增需求
https://element.eleme.cn/#/zh-CN/component/dialog
思路:
- 点击append按钮。弹出一个框,输入要添加的菜单名字。点击确认,菜单添加成功。刷新页面,默认展示添加分类的父分类id。
4.1 显示输入框
点击append按钮,调用单击函数。显示输入框
- 点击append按钮时,双向绑定category对象。
- 计算需要传递给后端的数据[name(用户输入),parentCid(展开的这个id),catLevel(层级,父id+1)]
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">append</el-button>
append(data) {
console.log(data);
this.dialogVisible = true;
this.category.parentCid=data.catId;
this.category.catLevel=data.catLevel*1+1;
},
data() {
return {
dialogVisible: false,
};
},
4.2 输入新增数据
显示输入框,输入值。
- 输入框需要双向绑定data中的一个对象category。用来获取或者给其传递值。
<el-dialog title="提示" :visible.sync="dialogVisible" width="30%">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" 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="addCategory">确 定</el-button>
</span>
</el-dialog>
data() {
return {
dialogVisible: false,
//data中放一个对象,给属性默认值。(需要传递给后端的对象)
category:{name:"",patentCid:0,catLevel:0,showStatus:1,sort:0},
};
},
4.3 点击确认按钮
点击确认按钮,调用单击addCategory方法。给后端发请求。
addCategory() {
console.log("添加成功" ,this.category);
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
});
4.4 后续操作
后端执行成功,弹出添加成功,关闭输入框,刷新页面,默认选择分类框进行展示
addCategory() {
console.log("添加成功" ,this.category);
this.$http({
url: this.$http.adornUrl('/product/category/save'),
method: 'post',
data: this.$http.adornData(this.category, false)
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单保存成功!",
});
//关闭对话框
this.dialogVisible=false;
//刷新菜单
this.getMenus();
//设置默认展示菜单
this.expanded=[this.category.parentCid];
});
5. 修改需求
5.1 前端增加修改按钮和修改框
- 为每级菜单都添加一个修改按钮。点击修改按钮,会弹出一个输入框。回显数据。和添加菜单框可以共用一个。
- 添加框显示添加。修改框显示修改。添加框点确认调用添加方法。修改框点确认调用修改方法。
html:
:title实现双向绑定data中的title数据。实现添加框显示添加。修改框显示修改功能。submitData方法实现添加框点确认调用添加方法,修改框点确认调用修改方法功能。
<span>
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)">append</el-button>
<el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)">delete</el-button>
<el-button type="text" size="mini" @click="() => edit(data)">edit</el-button>
</span>
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<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="submitData">确 定</el-button>
</span>
</el-dialog>
js:
- 发送请求时:`符号可以传递参数。{}使用解构表达式。
- 多人操作时,如果停留在页面时间过长。点击编辑时,需要从数据库实时获取最新的数据。
- 点击确认后,关闭输入框,刷新页面。展示默认菜单(当前菜单的父id)。
data() {
return {
title: "",
dialogType: "", //修改设置为edit,添加设置为add
menus: [], //menus接收后端数据
dialogVisible: false,
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
productUnit: "",
icon: "",
},
expanded: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
//点击添加按钮。
append(data) {
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
//添加框中所有值应该是空的
this.category.catId=null;
this.category.name="";
this.category.icon="";
this.category.productUnit="";
this.category.sort=0;
this.category.showStatus=1;
},
//点击修改按钮。
edit(data) {
this.dialogType = "edit";
this.title = "编辑分类";
this.dialogVisible = true; //弹出编辑框
this.$http({
//多人操作时,保证获取的是节点最新的数据(不能偷懒)
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
console.log("后端返回数据", data);
this.category.name = data.category.name;
this.category.catId = data.category.catId;
this.category.icon = data.category.icon;
this.category.productUnit = data.category.productUnit;
this.category.parentCid=data.category.parentCid
});
},
//弹框选择。
submitData() {
if (this.dialogType = "add") {
this.addCategory();
} if (this.dialogType = "edit") {
this.editCategory();
}
},
//添加点击确认后。
addCategory() {
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单添加成功!",
});
//关闭对话框
this.dialogVisible = false;
//刷新菜单
this.getMenus();
//设置默认展示菜单
this.expanded = [this.category.parentCid];
});
},
//编辑点击确认后。
editCategory() {
var { catId, name, icon, productUnit } = this.category; //解构表达式,解构需要传递给后端的属性
var data={catId, name, icon, productUnit}; //key和value相同可以省略。
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(data, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单修改成功!",
});
//关闭对话框
this.dialogVisible = false;
//刷新菜单
this.getMenus();
//设置默认展示菜单
this.expanded = [this.category.parentCid];
});
},
5.2 拖拽效果实现
时间有限,暂未实现。
https://element.eleme.cn/#/zh-CN/component/tree
6. 前端全部代码
- 至此,除了拖拽功能未实现外。其余代码都在下方。
<template>
<div>
<el-button type="danger" size="mini" @click="batchDelete">批量删除</el-button>
<!-- 展示menus数据 -->
<el-tree :data="menus" :props="defaultProps" :expand-on-click-node="false" show-checkbox node-key="catId" :default-expanded-keys="expanded" ref="menuTree">
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<!-- 一级和二级节点才显示添加按钮 -->
<el-button v-if="node.level <= 2" type="text" size="mini" @click="() => append(data)"><i class="el-icon-circle-plus"></i></el-button>
<!-- 三级节点才显示删除按钮 -->
<el-button v-if="node.childNodes.length == 0" type="text" size="mini" @click="() => remove(node, data)"> <i class="el-icon-delete"></i></el-button>
<el-button type="text" size="mini" @click="() => edit(data)"><i class="el-icon-edit"></i></el-button>
</span>
</span>
</el-tree>
<!-- 显示对话框,修改和添加都会出现该对话框wrh -->
<el-dialog :title="title" :visible.sync="dialogVisible" width="30%" :close-on-click-modal="false">
<el-form :model="category">
<el-form-item label="分类名称">
<el-input v-model="category.name" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
<el-form-item label="计量单位">
<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="submitData">确 定</el-button>
</span>
</el-dialog>
</div>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {
//这里存放数据,wrh
return {
title: "",
dialogType: "", //修改设置为edit,添加设置为add
menus: [], //menus接收后端数据wrh
dialogVisible: false,
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
productUnit: "",
icon: "",
},
expanded: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
//方法集合wrh
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
// 将后端请求获取的data传递给menus。
this.menus = data.page;
});
},
batchDelete() {
let catIds = [];
let checkNodes = this.$refs.menuTree.getCheckedNodes();
for (let i = 0; i < checkNodes.length; i++) {
catIds.push(checkNodes[i].catId);
}
this.$confirm(`是否批量删除菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//点击确定按钮,发送post请求删除
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(catIds, false),
}).then(({ data }) => {
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expanded = [catIds[1].parentCid];
});
this.$message({
type: "success",
message: "菜单批量删除成功!",
});
})
.catch(() => {});
},
append(data) {
this.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
//添加框中所有值应该是空的
this.category.catId = null;
this.category.name = "";
this.category.icon = "";
this.category.productUnit = "";
this.category.sort = 0;
this.category.showStatus = 1;
},
edit(data) {
this.dialogType = "edit";
this.title = "编辑分类";
this.dialogVisible = true; //弹出编辑框
this.$http({
//多人操作时,保证获取的是节点最新的数据(不能偷懒)
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
this.category.name = data.category.name;
this.category.catId = data.category.catId;
this.category.icon = data.category.icon;
this.category.productUnit = data.category.productUnit;
this.category.parentCid = data.category.parentCid;
});
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
} else if (this.dialogType == "edit") {
this.editCategory();
}
},
addCategory() {
this.$http({
url: this.$http.adornUrl("/product/category/save"),
method: "post",
data: this.$http.adornData(this.category, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单新增成功!",
});
//关闭对话框
this.dialogVisible = false;
//刷新菜单
this.getMenus();
//设置默认展示菜单
this.expanded = [this.category.parentCid];
});
},
editCategory() {
var { catId, name, icon, productUnit } = this.category; //解构表达式,解构需要传递给后端的属性
var data = { catId, name, icon, productUnit }; //key和value相同可以省略。
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(data, false),
}).then(({ data }) => {
this.$message({
type: "success",
message: "菜单修改成功!",
});
//关闭对话框
this.dialogVisible = false;
//刷新菜单
this.getMenus();
//设置默认展示菜单
this.expanded = [this.category.parentCid];
});
},
remove(node, data) {
var ids = [data.catId];
this.$confirm(`是否删除[${data.name}]菜单?`, "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
})
.then(() => {
//点击确定按钮,发送post请求删除
this.$http({
url: this.$http.adornUrl("/product/category/delete"),
method: "post",
data: this.$http.adornData(ids, false),
}).then(({ data }) => {
//刷新出新的菜单
this.getMenus();
//设置需要默认展开的菜单
this.expanded = [node.parent.data.catId];
});
this.$message({
type: "success",
message: "菜单删除成功!",
});
})
.catch(() => {});
},
},
//生命周期 - 创建完成(可以访问当前this实例)wrh
//创建组件即调用方法给后台发请求
created() {
this.getMenus();
},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>

浙公网安备 33010602011771号