- 谷粒商城-分布式基础篇【环境准备】
- 谷粒商城-分布式基础【业务编写】
- 谷粒商城-分布式高级篇【业务编写】持续更新
- 谷粒商城-分布式高级篇-ElasticSearch
- 谷粒商城-分布式高级篇-分布式锁与缓存
- 项目托管于gitee
一、三级分类
此处三级分类最起码得启动renren-fast
、nacos
、gateway
、product
pms_category表说明
代表商品的分类
cat_id
:分类id,cat代表分类,bigint(20)name
:分类名称parent_cid
:在哪个父目录下cat_level
:分类层级show_status
:是否显示,用于逻辑删除sort
:同层级同父目录下显示顺序ico图标
,product_unit商品计量单位,InnoDB表
,自增大小1437,utf编码,动态行格式
# 导入数据,在对应的数据库下执行资料里的 `pms_catelog.sql` 文件
# /Users/hgw/Documents/Data/Project/谷粒商城/1.分布式基础篇/docs/代码/sql/pms_catelog.sql
1.1、业务编写 (查询、递归树形结构获取)
第一步、编写Controller层
在分类Controller层加上一个三级分类的业务
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查处所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
public R list(){
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
//......
}
第二步、编写Service层
CategoryService
接口:
/**
* 商品三级分类
*
* @author hgw
* @email hgw6721@163.com
* @date 2022-03-07 13:28:36
*/
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
}
CategoryServiceImpl
实现类 :
Stream 的 map()方法: 转换流数据返回, 当前流的泛型变为返回值的类型,
Stream 的 peek()方法: 修饰流数据, 无返回值
1.2、配置路由网关 与 路径重写 (实现三级分类查询操作)
启动 renren-fast
、nacos
、product
还有前端项目 renren-fast-vue
1.2.1、创建 菜单目录
创建一个一级目 :
商品系统
添加的这个菜单其实是添加到了guli-admin.sys_menu
表里
(新增了memu_id=31 parent_id=0 name=商品系统 icon=editor )
在 商品系统 下创建一个菜单:
分类维护
guli-admin.sys_menu
表又多了一行,父id是刚才的商品系统id
1.2.2、菜单路由
在左侧点击【商品系统-分类维护】,希望在此展示3级分类。可以看到
- url是
http://localhost:8001/#/product-category
- 填写的菜单路由是 product/category
- 对应的视图是 src/view/modules/product/category.vue
再如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue
所以要自定义我们的product/category视图的话,就是创建 mudules/product/category.vue
输入vue快捷生成模板,然后去https://element.eleme.cn/#/zh-CN/component/tree. 看如何使用多级目录
创建
mudules/product/category.vue
<template>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
children: "children",
label: "label",
},
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then((data) => {
console.log("成功获取到菜单数据", data);
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.2.3、网关配置
第一步、修改Api接口请求地址
第一步、在
/static/config/index.js
文件中修改Api接口请求地址指向网关端口:88
在登录管理后台的时候,我们会发现,他要求localhost:8080/renrenfast/product/category/list/tree
这个url, 但是报错404找不到,此处就解决登录页验证码不显示的问题。
他要给8080发请求读取数据,但是数据是在10000端口上,如果找到了这个请求改端口那改起来很麻烦。
- 方法1: 是改vue项目里的全局配置,
- 方法2: 是搭建个网关,让网关路由到10000(即将vue项目里的请求都给网关,网关经过url处理后,去nacos里找到管理后台的微服务,就可以找到对应的端口了,这样我们就无需管理端口,统一交给网关管理端口接口)
// api接口请求地址
window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api';
// 意思是说本vue项目中要请求的资源url都发给88/api,那么我们就让网关端口为88,然后匹配到/api请求即可,
// 网关可以通过过滤器处理url后指定给某个微服务
// renren-fast服务已经注册到了nacos中
问题:他要去nacos中查找api服务,但是nacos里有的是fast服务,就通过网关过滤器把api改成fast服务
所以让fast注册到服务注册中心,这样请求88网关转发到8080fast
第二步、将fast注册到服务注册中心
第二步、将fast注册到服务注册中心,这样请求88网关转发到8080fast
-
在fast里加入注册中心的依赖
<!--SpringCloud-nacos 注册中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId> <version>2.1.0.RELEASE</version> </dependency> <!--SpringCloud-nacos 配置中心--> <dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId> <version>2.1.0.RELEASE</version> </dependency>
-
在renren-fast项目中
src/main/resources/application.yml
添加nacos配置spring: application: name: renren-fast # 意思是把renren-fast项目也注册到nacos中(后面不再强调了),这样网关才能转发给 cloud: nacos: discovery: server-addr: 127.0.0.1:8848 # nacos
-
然后在fast启动类上加上注解
@EnableDiscoveryClient
,重启@EnableDiscoveryClient @SpringBootApplication public class RenrenApplication { public static void main(String[] args) { SpringApplication.run(RenrenApplication.class, args); } }
然后在nacos的服务列表里看到了renren-fast
问题解决:
-
如果报错gson依赖,就导入google的gson依赖
-
如果一直获取不到nacos信息, 则在
resources
路径下创建一个 bootstrap.propertiesspring.application.name=renren-fast spring.cloud.nacos.config.server-addr=127.0.0.1:8848
第三步、添加网关
第三步、配置**gateway(网关)**模块中的
application.yml
文件, 添加网关
- id: admin_route
uri: lb://renren-fast # 路由给renren-fast (lb)负载均衡
predicates: # 什么情况下路由给它
- Path=/api/** # 默认前端项目都带上api前缀,就是我们前面题的localhost:88/api
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment} # 把/api/* 改变成 /renren-fast/*fast找
- lb代表负载均衡
修改过vue的api之后, 此时验证码请求的是 http://localhost:88/api/captcha.jpg?uuid=72b9da67-0130-4d1d-8dda-6bfe4b5f7935
也就是说, 他请求网关, 路由到了renren-fast , 然后去nacos里找fast.
找到后拼接成了: http://renren-fast:8080/api/captcha.jpg
但是正确的是: localhost:8080/renren-fast/captcha.jpg
所以要利用网关带路径重写, 参考https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#the-rewritepath-gatewayfilter-factory
照猫画虎,在网关里写了如上,把api换成renren-fast,
登录,还是报错:(出现了跨域的问题,就是说vue项目是8001端口,却要跳转到88端口,为了安全性,不可以)
:8001/#/login:1 Access to XMLHttpRequest at ‘http://localhost:88/api/sys/login’ from origin ‘http://localhost:8001’ has been blocked by CORS policy: Response to preflight request doesn’t pass access control check: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.
从8001访问88,引发CORS跨域请求,浏览器会拒绝跨域请求。具体来说当前页面是8001端口,但是要跳转88端口,这是不可以的(post请求json可以)
问题描述:已拦截跨源请求:同源策略禁止8001端口页面读取位于 http://localhost:88/api/sys/login 的远程资源。(原因:CORS 头缺少 ‘Access-Control-Allow-Origin’)。
问题分析:这是一种跨域问题。访问的域名或端口和原来请求的域名端口一旦不同,请求就会被限制
第四步、网关统一配置跨域
第四步、网关统一配置跨域
解决方法:在网关中定义“GulimallCorsConfiguration
”类,该类用来做过滤,允许所有的请求跨域。
package com.hgw.gulimall.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
/**
* Data time:2022/3/14 21:17
* StudentID:2019112118
* Author:hgw
* Description: 配置跨域,该类用来做过滤,允许所有的请求跨域。
*/
@Configuration
public class GulimallCorsConfiguration {
@Bean // 添加过滤器
public CorsWebFilter corsWebFilter() {
// 基于url跨域,选择reactive包下的
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
// 配置跨域信息
CorsConfiguration configuration = new CorsConfiguration();
// 允许跨域的头 *:表示所有
configuration.addAllowedHeader("*");
// 允许跨域的请求方式
configuration.addAllowedMethod("*");
// 允许跨域的请求来源
configuration.addAllowedOrigin("*");
// 是否允许携带cookie跨域
configuration.setAllowCredentials(true);
// `/**` :任意url都要进行跨域配置
source.registerCorsConfiguration("/**",configuration);
return new CorsWebFilter(source);
}
}
再次访问:http://localhost:8001/#/login
已拦截跨源请求:同源策略禁止读取位于 http://localhost:88/api/sys/login 的远程资源。
(原因:不允许有多个 ‘Access-Control-Allow-Origin’ CORS 头)
renren-fast/captcha.jpg?uuid=69c79f02-d15b-478a-8465-a07fd09001e6
出现了多个请求,并且也存在多个跨源请求。因为在renren-fast项目下有过滤器 .
为了解决这个问题,需要修改renren-fast项目,注释掉“io.renren.config.CorsConfig”类。然后再次进行访问。
第五步、Product 请求路径重写
之前解决了登陆验证码的问题,
/api/
请求重写成了/renren-fast
, 但是vue项目中或者你自己写的数据库中有些是以/product
为前缀的, 它要请求 product微服务, 这里也会让它请求renren-fast 显然是不合适的.
- 解决办法是把请求在网关中以更小的范围先拦截一下,剩下的请求再交给renren-fast
在显示商品系统/分类信息的时候,出现了404异常,请求的http://localhost:88/api/product/category/list/tree不存在
这是因为网关上所做的路径映射不正确,映射后的路径为http://localhost:8001/renren-fast/product/category/list/tree
但是只有通过http://localhost:10000/product/category/list/tree路径才能够正常访问,所以会报404异常。
- 原本请求:
http://localhost:88/api/product/category/list/tree
- 映射请求:
http://localhost:8001/renren-fast/product/category/list/tree
- 真实请求:
http://localhost:10000/product/category/list/tree
1.2.3.5.1、将 gulimall-product
加入到注册中心nacos中
- 首先将
gulimall-product
加入到注册中心nacos中
修改: 在product项目的application.yml
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://124.222.223.222:3306/gulimall_pms?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
application:
name: gulimall-product
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
# 设置表主键自增
id-type: auto
server:
port: 10000
如果要使用nacos配置中心,可以这么做
-
在nacos中新建命名空间,用命名空间隔离项目,(可以在其中新建gulimall-product.yml)
-
在product项目中新建bootstrap.properties并配置
spring.application.name=gulimall-product spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=502fa214-0e44-47d4-91c4-2d4589720c76
为了让product注册到主类上加上注解@EnableDiscoveryClient
1.2.3.5.2、定义路由规则, 进行路径重写
- 定义路由规则, 进行路径重写
修改 gulimall-gateway 的 application.yml
文件, 在后面加上以下路由规则
- id: product_route
uri: lb://gulimall-product # 注册中心的服务
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment} # 将/api/替换为空
此时 访问 localhost:88/api/product/category/list/tree invalid token,非法令牌,后台管理系统中没有登录,所以没有带令牌
原因:先匹配的先路由,fast和product路由重叠,fast要求登录
修正:在路由规则的顺序上,将精确的路由规则放置到模糊的路由规则的前面,否则的话,精确的路由规则将不会被匹配到,类似于异常体系中try catch子句中异常的处理顺序。
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product # 注册中心的服务
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast # 路由给renren-fast (lb)负载均衡
predicates: # 什么情况下路由给它
- Path=/api/** # 默认前端项目都带上api前缀,就是我们前面题的localhost:88/api
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment} # 把/api/* 改变成 /renren-fast/*fast找
此时请求已可请求到数据!
补充: 跨域问题
跨域概括
- 跨域: 指的是浏览器不能执行其他网站的脚本. 它是由浏览器的同源策略造成的, 是浏览器对js施加的安全措施. (ajax可以)
- 同源策略: 是指
协议、域名、端口
都要相同, 其中有一个不同都会产生跨域
URL | 说明 | 是否允许通信 |
---|---|---|
http://www.a.com/a.js http://www.a.com/b.js | 同一域名下 | 允许 |
http://www.a.com/lab/a.js http://www.a.com/script/b.js | 同一域名下不同文件夹 | 允许 |
http://www.a.com:8000/a.js http://www.a.com/b.js | 同一域名,不同端口 | 不允许 |
http://www.a.com/a.js https://www.a.com/b.js | 同一域名,不同协议 | 不允许 |
http://www.a.com/a.js http://70.32.92.74/b.js | 域名和域名对应ip | 不允许 |
http://www.a.com/a.js http://script.a.com/b.js | 主域相同,子域不同 | 不允许 |
http://www.a.com/a.js http://a.com/b.js | 同一域名,不同二级域名(同上) | 不允许(cookie这种情况下也不允许访问) |
http://www.cnblogs.com/a.js http://www.a.com/b.js | 不同域名 | 不允许 |
跨域流程
跨域流程
这个跨域请求的实现是通过预检请求实现的, 发送一个OPSTIONS
探路, 收到响应允许跨域后再发送真实请求
什么意思呢?
- 跨域是要请求的、新的端口那个服务器限制的, 不是浏览器限制的
跨域请求流程: 非简单请求(PUT、DELETE)等,需要先发送预检请求
跨域的解决方案
跨域的解决方案
- 方法一: 使用Nginx部署为同一域
- 方法二: 让服务器告诉预检请求能跨域
-
方法一: 使用Nginx部署为同一域
设置Nginx包含admin 和 gateway. 都先请求nginx, 这样端口就统一了
-
方法二: 配置当次请求允许跨域
在响应头中添加:参考:https://blog.csdn.net/qq_38128179/article/details/84956552Access-Control-Allow-Origin
: 支持哪些来源的请求跨域Access-Control-Allow-Method
: 支持那些方法跨域Access-Control-Allow-Credentials
:跨域请求默认不包含cookie,设置为true可以包含cookieAccess-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 :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效
1.2.4、三级分类-查询-树形展示三级分类数据
接着修改前端category.vue,这里改的是点击分类维护后的右侧显示
- data解构,加上{},取出我们想要的数据
//方法集合
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
},
- 此时有了3级结构,但是没有数据,在category.vue的模板中,数据是menus,而还有一个props。这是element-ui的规则,
<template>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</template>
而在data中
defaultProps: {
children: "children",
label: "name"
}
整个代码 :
<template>
<el-tree
:data="menus"
:props="defaultProps"
@node-click="handleNodeClick"
></el-tree>
</template>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
menus: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
handleNodeClick(data) {
console.log(data);
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.3、三级分类 增删改操作
1.3.1、三级分类 [删除]
1.3.1.1、实现页面效果
这里采用ElementUI 的自定义节点内容 的
scoped slot
方式来实现 ElementUI组件
1.3.1.1.1、[效果一]: 实现增加、删除的效果, 点击节点的时候不展开或者收缩节点
<template>
<el-tree :data="menus" :props="defaultProps" :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)">
Append
</el-button>
<el-button type="text" size="mini" @click="() => remove(node, data)">
Delete
</el-button>
</span>
</span>
</el-tree>
</template>
export default {
append(data) {
console.log("append", data);
},
remove(node, data) {
console.log("remove", node, data);
},
},
}
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
expand-on-click-node | 是否在点击节点的时候展开或者收缩节点, 默认值为 true, 如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。 | boolean | — | true |
:expand-on-click-node="false"
: 即设置为在点击节点的时候不展开或者收缩节点
1.3.1.1.2、[效果二]: 实现在规定的地方显示 增删按钮
- 没有子节点的时候才显示 Delete按钮
- 解决:
v-if="node.level <= 2"
- 解决:
- 只有一级菜单和二级菜单才显示 Append按钮
- 解决:
v-if="node.childNodes.length == 0"
- 解决:
<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
>
1.3.1.1.3、[效果三]: 实现多选
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
>
//......
</el-tree>
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
show-checkbox | 节点是否可被选择 | boolean | — | false |
node-key | 每个树节点用来作为唯一标识的属性,整棵树应该是唯一的 | String | — | — |
1.3.1.2、逻辑删除
这里使用MyBatis-Plus的逻辑删除 官网使用方法
逻辑删除是为了方便数据恢复和保护数据本身价值等等的一种方案,但实际就是删除。在表中应当编写一个字段标记是否被删除. 在进行删除的时候并不是执行delete
命令, 而是执行update
命令 , 如下 :
update user set deleted=1 where id = 1 and deleted=0
-
1、配置全局的逻辑删除规则(可省略)
-
2、配置逻辑删除的组件Bean(mybatis-plus3之后可省略)
-
3、实体类字段上加上@TableLogic注解
第一步、配置
application.yml
全局的逻辑删除规则
mybatis-plus:
mapper-locations: classpath:/mapper/**/*.xml
global-config:
db-config:
# 设置表主键自增
id-type: auto
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
第二步、给
product.entity
路径下的 CategoryEntity类的 showStatus属性加上注解
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
表中
1
显示的是 删除0
显示的是 不删除
和全局配置是反的, 这里通过 @TableLogic(value = "1",delval = "0")
配置自己的规则 !
String value() default ""
: 默认逻辑未删除值 (该值可无、会自动获取全局配置)String delval() default ""
: 默认逻辑删除值 (该值可无、会自动获取全局配置)
故前面配置 application.yml
全局的逻辑删除规则并没有做效, 而是 1(未删除), 0(删除)
第三步、修改Controller层
/**
* 删除
* @RequestBody: 获取请求体,必须发送POST请求
* SpringMVC自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
// @RequiresPermissions("product:category:delete")
public R delete(@RequestBody Long[] catIds){
// 1、检查当前删除的菜单,是否被别的地方引用
// categoryService.removeByIds(Arrays.asList(catIds));
categoryService.removeMenuByIds(Arrays.asList(catIds));
return R.ok();
}
第四步、修改Service层
接口 :
public interface CategoryService extends IService<CategoryEntity> {
PageUtils queryPage(Map<String, Object> params);
List<CategoryEntity> listWithTree();
// 加上删除方法
void removeMenuByIds(List<Long> asList);
}
实现类 :
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1、检查当前删除的菜单,是否被别的地方引用
// 逻辑删除
baseMapper.deleteBatchIds(asList);
}
这里留下了一个待完成事项: 等以后业务来完成
//TODO 注释内容
- todo默认不区分大小写,todo、Todo、ToDO、TODO都是可以的。也可以修改为区分。
- todo后面必须要使用一个空格隔开注释内容。
- 我们在某个地方加上了todo注释之后,我们可以通过任务列表快速定位到某个todo注释位置
1.3.1.3、删除效果细化
效果一: 实现逻辑删除功能
remove(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({
message: "菜单删除成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message("取消删除");
});
console.log("remove", node, data);
},
- 点击
delete
按钮弹出提示框确定
: 向 /product/category/delete 发出post请求, 并带着请求体 data.catId(当前菜单的id)- then :
- 则删除成功, 弹出提示框.
- 并刷新出新的菜单.
:default-expanded-keys="expandedKey"
修改动态绑定expandedKey数组的值为当前删除菜单的母菜单id, 从而实现删除后默认展开删除的菜单
- catch :
- 取消删除, 弹出提示框
- then :
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
default-expanded-keys | 默认展开的节点的 key 的数组 | array | — | — |
全部代码附上:
<template>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<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>
<script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';
export default {
//import引入的组件需要注入到对象中才能使用
components: {},
props: {},
data() {
//这里存放数据
return {
menus: [],
expandedKey: [],
defaultProps: {
children: "children",
label: "name",
},
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
},
remove(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({
message: "菜单删除成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message("取消删除");
});
console.log("remove", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.3.2、三级分类[新增]
需求一: 点击append
按钮之后, 弹出一个对话框输入子分类的信息
在 <template>
中添加一个弹框组件 :
<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>
- 动态绑定一个变量
category
, 里面存放着 name、parentCid、catLevel、showStatus、sort属性 dialogVisible
: 对话框是否显示- false : 对话框不显示
- true : 对话框显示
addCategory
: 点击确定则触发这个时间, 保存事件
export default {
data() {
//这里存放数据
return {
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
},
dialogVisible: false, // 对话框是否显示
menus: [], // 用来存放数据
expandedKey: [], // 默认展开的节点的 key 的数组
defaultProps: {
children: "children",
label: "name",
},
};
},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
},
// 添加三级分类的方法
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({
message: "菜单保存成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
};
- 点击
append
按钮,dialogVisible
值修改为true(即对话框可见), 此时并计算出category.parentCid、category.catLevel的值, 其他属性使用默认值- 点击
确定
按钮, 则执行 addCategory()方法- 向 /product/category/save发出post请求, 请求体为: category
- 提示菜单保存成功
- 刷新出新的菜单
- 设置需要默认展开的菜单
- 向 /product/category/save发出post请求, 请求体为: category
- 点击
后端逆向工程生成了save接口方法
@RequestMapping("/save")
// @RequiresPermissions("product:commentreplay:save")
public R save(@RequestBody CommentReplayEntity commentReplay){
commentReplayService.save(commentReplay);
return R.ok();
}
1.3.3、三级分类[修改]
需求一: 点击update
按钮之后, 弹出一个对话框修改分类的信息
- 通过
submitData
方法进行判断 ,此对话框供修改 和 增加分类使用- dialogType: “”, 对话框的方法
- add: 则增加;
- edit: 则修改;
- title: “”, 提示框的标题
- dialogType: “”, 对话框的方法
<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>
<el-form :model="category">
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<el-form :model="category">
<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>
- 点击
edit
修改按钮title
属性修改为 “修改分类” ,dialogType
属性修改为 “edit", 则对话框执行的是 修改方法- 发送请求获取当前节点最新的数据 (因为防止多人同时操控后台, 脏读现象. 这里采用重新发送请求获取当前节点最新数据)
- 将当前节点的最新数据, 赋值给
category
,即要回显的数据
- 通过
submitData
方法判断执行哪个操作- add : 增加分类
- edit : 修改分类
- 修改三级分类的方法
- { catId, name, icon, productUnit } : 获取我们要回显的数据
- 带着回显的数据向
/product/category/update
发送post请求 - 菜单修复成功
- 弹出提示框
- 关闭对话框
- 刷新出新的菜单
- 设置需要默认展开的菜单
export default {
data() {
//这里存放数据
return {
title: "", //提示框的标题
dialogType: "", //对话框的方法 add: 则增加; edit: 则修改
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
dialogVisible: false, // 对话框是否显示
menus: [], // 用来存放数据
expandedKey: [], // 默认展开的节点的 key 的数组
defaultProps: {
children: "children",
label: "name",
},
};
},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.title = "添加分类";
this.dialogType = "add";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.showStatus = 1;
this.category.sort = "";
},
edit(data) {
console.log("要修改的数据", data);
this.title = "修改分类";
this.dialogType = "edit";
this.dialogVisible = true;
// 发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
// 请求成功
console.log("要回显的数据", data);
this.category.catId = data.data.catId;
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.showStatus = data.data.showStatus;
this.category.sort = data.data.sort;
});
},
submitData(data) {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
// 添加三级分类的方法
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({
message: "菜单保存成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改三级分类的方法
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var data = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(data, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
};
完整代码:
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
>
<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 type="text" size="mini" @click="() => edit(data)"
>edit</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>
<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>
<el-form :model="category">
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<el-form :model="category">
<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: {},
props: {},
data() {
//这里存放数据
return {
title: "", //提示框的标题
dialogType: "", //对话框的方法 add: 则增加; edit: 则修改
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
dialogVisible: false, // 对话框是否显示
menus: [], // 用来存放数据
expandedKey: [], // 默认展开的节点的 key 的数组
defaultProps: {
children: "children",
label: "name",
},
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
append(data) {
console.log("append", data);
this.title = "添加分类";
this.dialogType = "add";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.showStatus = 1;
this.category.sort = "";
},
edit(data) {
console.log("要修改的数据", data);
this.title = "修改分类";
this.dialogType = "edit";
this.dialogVisible = true;
// 发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
// 请求成功
console.log("要回显的数据", data);
this.category.catId = data.data.catId;
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.showStatus = data.data.showStatus;
this.category.sort = data.data.sort;
});
},
submitData(data) {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
// 添加三级分类的方法
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({
message: "菜单保存成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改三级分类的方法
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var data = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(data, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(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({
message: "菜单删除成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message("取消删除");
});
console.log("remove", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.3.4、三级分类[修改-拖拽效果]
需求: 通过拖拽节点改变节点顺序以及节点之间关系的业务
1.3.4.1、拖拽页面的效果
拖拽页面的效果
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
draggable | 是否开启拖拽节点的功能 | boolean | — | false |
allow-drop | 拖拽时判定目标节点能否被放置。type 参数有三种情况:‘prev’、‘inner’ 和 ‘next’,分别表示放置在目标节点前、插入至目标节点和放置在目标节点后 | Function(draggingNode, dropNode, type) | — | — |
Function(draggingNode, dropNode, type)
draggingNode
: 可拖拽节点dropNode
: 目标节点type
: 拖拽目标节点的哪些位置prev
: 目标节点前inner
: 插入至目标节点next
: 目标节点后
给组件加上 draggable
属性, 并绑定allowDrop()
方法
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
draggable
:allow-drop="allowDrop"
>
// ....此处省略代码
</el-tree>
- 拖拽时判定目标节点能否被放置, 调用
allowDrop(draggingNode, dropNode, type)
方法- 被拖动的当前节点以及所在的父节点总层数不能大于3
- 调用
countNodeLevel(node)
方法求出被拖动的当前节点总层数(也就是叶子最大结点的层数)maxLevel
:属性用来存放最大叶子结点的层数- 通过递归遍历求出大于
maxLevel
的叶子结点层数并赋值给maxLevel
并返回
- 求出当前正在拖拽节点的深度(也就是把它看成一棵树有几层)
当前节点总层数 - 当前节点的层级 +1
, 比如说手机通讯的层级是2, 下面有1个节点(也就是节点的层次是3),拖拽到层级为2, 则 (3-2+1)=2)
- 进行两种情况判断
- 插入到目标节点里面 :
deep + dropNode.level <= 3
- 插入到目标节点的前后 :
deep + dropNode.level - 1 <= 3
- 插入到目标节点里面 :
- 调用
- 被拖动的当前节点以及所在的父节点总层数不能大于3
export default {
data() {
//这里存放数据
return {
updateNodes: [],
maxLevel: 1, // 当前节点子节点的最大深度
};
},
//方法集合
methods: {
// 判断能否拖动
allowDrop(draggingNode, dropNode, type) {
// 1、被拖动的当前节点以及所在的父节点总层数不能大于3
// 被拖动的当前节点总层数(也就是叶子结点的层数)
this.countNodeLevel(draggingNode.data);
// 当前正在拖拽的节点 + 父节点所在深度不大于3即可
//求出当前正在拖拽节点的深度 ((当前节点的深度 - 当前节点的层级 +1) , 比如说手机通讯的层级是2, 下面有1个节点,拖拽到层级为2, 则 (3-2+1)=2)
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
if (type == "inner") {
// 插入到目标节点里面
return deep + dropNode.level <= 3;
} else {
// 插入到目标节点的前后
return deep + dropNode.level - 1 <= 3;
}
},
countNodeLevel(node) {
// 找出所有子节点,求出最大深度(也就是当前节点的叶子结点的层级)
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
};
</script>
1.3.4.2、拖拽数据收集
拖追移动后数据收集:
-
拖拽分类的 :
-
拖拽分类的id :
catId
-
拖拽分类的层级:
catLevel
-
拖拽分类父节点的id :
parentCid
-
拖拽分类的排序 :
sort
-
-
拖拽分类子节点的 :
- 拖拽分类子节点id:
catId
- 拖拽分类子节点的层级:
catLevel
- 拖拽分类子节点id:
-
拖拽分类后兄弟节点的 :
- 拖拽分类后兄弟节点的id :
catId
- 拖拽分类后兄弟节点的排序:
sort
- 拖拽分类后兄弟节点的id :
事件名称 | 说明 | 回调参数 |
---|---|---|
node-drop | 拖拽成功完成时触发的事件 | 共四个参数,依次为: 被拖拽节点对应的 Node、 结束拖拽时最后进入的节点、 被拖拽节点的放置位置(before、after、inner)、 event |
代码解说
- 求出拖拽分类父节点的id :
parentCid
- 如果他是放在了前后/后面,
- 则
parentCid
就为拖追后最后进入的节点的 父节点的id siblings
存放拖追后最后进入的节点的父节点的所有子类(这里是拖拽后的所有子类)
- 则
- 如果他是放在了里面,
- 则
parentCid
就为 最后进入的节点的catId siblings
存放拖追后最后进入的节点的所有子类 (这里是拖拽后的所有子类)
- 则
- 如果他是放在了前后/后面,
- 当前拖拽节点的最新顺序(将当前页面的顺序遍历出来保存, 在数据库中更改新的顺序)
- 遍历拖拽分类父节点的所有子类
- 如果遍历的是当前正在拖拽的节点
- 如果当前正在拖拽的节点层级 和 拖拽前节点层级 不同
- 当前节点的层级发生变化,
catLevel
修改为拖拽后节点层级 - 修改其子节点的层级: 调用
updateChildNodeLevel
方法进行递归遍历更新子节点层级
- 当前节点的层级发生变化,
- 根据catID 更改 父节点parentCid,排序sort
- 如果当前正在拖拽的节点层级 和 拖拽前节点层级 不同
- 兄弟节点则只需要(根据catId更改排序sort)
- 如果遍历的是当前正在拖拽的节点
- 遍历拖拽分类父节点的所有子类
export default {
data() {
//这里存放数据
return {
updateNodes: [],
maxLevel: 1, // 当前节点子节点的最大深度
};
},
// 方法集合
methods: {
// 拖拽数据收集
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
// 1、当前节点最新的父节点id
let pCid = 0;
let siblings = null;
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 包装回显类
// 2、当前拖拽节点的最新顺序(将当前页面的顺序遍历出来保存, 在数据库中更改新的顺序)
for (let i = 0; i < siblings.length; i++) {
// 如果遍历的是当前正在拖拽的节点
if (siblings[i].data.catId == draggingNode.data.catId) {
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
// 当前节点的层级发生变化
catLevel = siblings[i].level;
// 修改其子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
// (根据catID 更改 父节点parentCid,排序sort)
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
// 兄弟节点则只需要(根据catId更改排序sort)
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
// 3、当前拖拽节点的最新层级
console.log("updateNodes", this.updateNodes);
},
// 更新子节点层级方法
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
allowDrop(draggingNode, dropNode, type) {
// 1、被拖动的当前节点以及所在的父节点总层数不能大于3
// 被拖动的当前节点总层数(也就是叶子结点的层数)
this.countNodeLevel(draggingNode.data);
// 当前正在拖拽的节点 + 父节点所在深度不大于3即可
//求出当前正在拖拽节点的深度 ((当前节点的深度 - 当前节点的层级 +1) , 比如说手机通讯的层级是2, 下面有1个节点,拖拽到层级为2, 则 (3-2+1)=2)
let deep = this.maxLevel - draggingNode.data.catLevel + 1;
if (type == "inner") {
// 插入到目标节点里面
return deep + dropNode.level <= 3;
} else {
// 插入到目标节点的前后
return deep + dropNode.level - 1 <= 3;
}
},
countNodeLevel(node) {
// 找出所有子节点,求出最大深度(也就是当前节点的叶子结点的层级)
if (node.children != null && node.children.length > 0) {
for (let i = 0; i < node.children.length; i++) {
if (node.children[i].catLevel > this.maxLevel) {
this.maxLevel = node.children[i].catLevel;
}
this.countNodeLevel(node.children[i]);
}
}
},
};
</script>
1.3.4.3、拖拽功能完成
在后端编写一个批量修改方法
第一步、Controller层编写
在 product模块下的CategoryController
中加入批量修改方法:
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 批量修改
*/
@RequestMapping("/update/sort")
// @RequiresPermissions("product:category:update")
public R updateSort(@RequestBody CategoryEntity[] category){
// 调用逆向工程生成的批量修改方法
categoryService.updateBatchById(Arrays.asList(category.clone()));
return R.ok();
}
}
- 底层使用逆向工程生成的方法
测试成功:
第二步、前端修改
<script>
export default {
data() {
//这里存放数据
return {
updateNodes: [],
maxLevel: 1, // 当前节点子节点的最大深度
title: "", //提示框的标题
dialogType: "", //对话框的方法 add: 则增加; edit: 则修改
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
};
},
//方法集合
methods: {
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
// 本次一共要修改拖拽类的 父Id、sort排序、自己以及子节点的层级; 兄弟分类的排序sort
// 1、当前节点最新的父节点id
let pCid = 0;
let siblings = null; // 子节点
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
// 包装回显类
// 2、当前拖拽节点的最新顺序(将当前页面的顺序遍历出来保存, 在数据库中更改新的顺序)
// 3、当前拖拽节点及其子节点的最新层级
for (let i = 0; i < siblings.length; i++) {
// 如果遍历的是当前正在拖拽的节点
if (siblings[i].data.catId == draggingNode.data.catId) {
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
// 当前节点的层级发生变化
catLevel = siblings[i].level;
// 修改其子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
// (根据catID 更改 父节点parentCid,排序sort)
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
// 兄弟节点则只需要(根据catId更改排序sort)
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
console.log("updateNodes", this.updateNodes);
// 向后端接口发出请求, 保存至数据库
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [pCid];
// 初始化数据
(this.updateNodes = []), (this.maxLevel = 0);
});
},
};
</script>
- 向后端接口发出请求, 保存至数据库
- 保存成功之后,弹出提示框
- 刷新出新的菜单
- 设置需要默认展开的菜单
- 初始化数据
1.3.4.4、批量拖拽效果[优化]
效果一 : 实现按钮开启是否拖拽功能
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
>
</el-switch>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
//.....
</el-tree>
<template>
draggable: false, // 是否开启拖拽功能
加入组件, 绑定 draggable
属性, 该属性并和el-tree组件的draggable
属性动态绑定
效果二: 实现按钮点击保存才提交至数据库保存
- 加上新组件 :
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
- 在变量区申请一个变量数组
- 因为原本
pCid
是在handleDrop
方法中定义的一个局部变量, 并在handleDrop
方法中获取到父节点id的时候给存入this.pCid.push(pCid);
data() {
//这里存放数据
return {
pCid: [],
}
}
handleDrop(draggingNode, dropNode, dropType, ev) {
// 此处省略 当前节点最新的父节点id 代码
// ...
this.pCid.push(pCid);
}
- 将发送请求重构成 批量拖拽保存功能方法 batchSave(), 点击
保存
按钮才触发
// 批量拖拽保存功能
batchSave() {
// 向后端接口发出请求, 保存至数据库
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = this.pCid;
// 初始化数据
(this.updateNodes = []), (this.maxLevel = 0), (this.pCid = 0);
});
},
- 重写 拖拽时判定目标节点能否被放置 方法,
- 不再使用从数据库中读取的数据做比较, 因为批量拖拽存在数据不一致性
/ 拖拽时判定目标节点能否被放置
allowDrop(draggingNode, dropNode, type) {
// 1、被拖动的当前节点以及所在的父节点总层数不能大于3
// 被拖动的当前节点总层数(也就是叶子结点的层数)
this.countNodeLevel(draggingNode);
// 当前正在拖拽的节点 + 父节点所在深度不大于3即可
//求出当前正在拖拽节点的层级 ((当前节点的深度 - 当前节点的层级 +1) , 比如说手机通讯的层级是2, 下面有1个节点,拖拽到层级为2, 则 (3-2+1)=2)
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
if (type == "inner") {
// 插入到目标节点里面
return deep + dropNode.level <= 3;
} else {
// 插入到目标节点的前后
return deep + dropNode.level - 1 <= 3;
}
},
完整代码附上:
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽"
>
</el-switch>
<el-button v-if="draggable" @click="batchSave">批量保存</el-button>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
show-checkbox
node-key="catId"
:default-expanded-keys="expandedKey"
:draggable="draggable"
:allow-drop="allowDrop"
@node-drop="handleDrop"
>
<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 type="text" size="mini" @click="() => edit(data)"
>edit</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>
<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>
<el-form :model="category">
<el-form-item label="图标">
<el-input v-model="category.icon" autocomplete="off"></el-input>
</el-form-item>
</el-form>
<el-form :model="category">
<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: {},
props: {},
data() {
//这里存放数据
return {
draggable: false, // 是否开启拖拽功能
updateNodes: [],
pCid: [],
maxLevel: 1, // 当前节点子节点的最大深度
title: "", //提示框的标题
dialogType: "", //对话框的方法 add: 则增加; edit: 则修改
category: {
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: "",
productUnit: "",
},
dialogVisible: false, // 对话框是否显示
menus: [], // 用来存放数据
expandedKey: [], // 默认展开的节点的 key 的数组
defaultProps: {
children: "children",
label: "name",
},
};
},
//计算属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//方法集合
methods: {
// 批量拖拽保存功能
batchSave() {
// 向后端接口发出请求, 保存至数据库
this.$http({
url: this.$http.adornUrl("/product/category/update/sort"),
method: "post",
data: this.$http.adornData(this.updateNodes, false),
}).then(({ data }) => {
this.$message({
message: "菜单顺序修改成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = this.pCid;
// 初始化数据
(this.updateNodes = []), (this.maxLevel = 0), (this.pCid = 0);
});
},
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then(({ data }) => {
console.log("成功获取到菜单数据", data.data);
this.menus = data.data;
});
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log("handleDrop: ", draggingNode, dropNode, dropType);
// 本次一共要修改拖拽类的 父Id、sort排序、自己以及子节点的层级; 兄弟分类的排序sort
// 1、当前节点最新的父节点id
let pCid = 0;
let siblings = null; // 子节点
if (dropType == "before" || dropType == "after") {
pCid =
dropNode.parent.data.catId == undefined
? 0
: dropNode.parent.data.catId;
siblings = dropNode.parent.childNodes;
} else {
pCid = dropNode.data.catId;
siblings = dropNode.childNodes;
}
this.pCid.push(pCid);
// 包装回显类
// 2、当前拖拽节点的最新顺序(将当前页面的顺序遍历出来保存, 在数据库中更改新的顺序)
// 3、当前拖拽节点及其子节点的最新层级
for (let i = 0; i < siblings.length; i++) {
// 如果遍历的是当前正在拖拽的节点
if (siblings[i].data.catId == draggingNode.data.catId) {
let catLevel = draggingNode.level;
if (siblings[i].level != draggingNode.level) {
// 当前节点的层级发生变化
catLevel = siblings[i].level;
// 修改其子节点的层级
this.updateChildNodeLevel(siblings[i]);
}
// (根据catID 更改 父节点parentCid,排序sort)
this.updateNodes.push({
catId: siblings[i].data.catId,
sort: i,
parentCid: pCid,
catLevel: catLevel,
});
} else {
// 兄弟节点则只需要(根据catId更改排序sort)
this.updateNodes.push({ catId: siblings[i].data.catId, sort: i });
}
}
console.log("updateNodes", this.updateNodes);
},
// 更新子节点层级方法
updateChildNodeLevel(node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNode = node.childNodes[i].data;
this.updateNodes.push({
catId: cNode.catId,
catLevel: node.childNodes[i].level,
});
this.updateChildNodeLevel(node.childNodes[i]);
}
}
},
// 拖拽时判定目标节点能否被放置
allowDrop(draggingNode, dropNode, type) {
// 1、被拖动的当前节点以及所在的父节点总层数不能大于3
// 被拖动的当前节点总层数(也就是叶子结点的层数)
this.countNodeLevel(draggingNode);
// 当前正在拖拽的节点 + 父节点所在深度不大于3即可
//求出当前正在拖拽节点的层级 ((当前节点的深度 - 当前节点的层级 +1) , 比如说手机通讯的层级是2, 下面有1个节点,拖拽到层级为2, 则 (3-2+1)=2)
let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;
if (type == "inner") {
// 插入到目标节点里面
return deep + dropNode.level <= 3;
} else {
// 插入到目标节点的前后
return deep + dropNode.level - 1 <= 3;
}
},
countNodeLevel(node) {
// 找出所有子节点,求出最大深度(也就是当前节点的叶子结点的层级)
if (node.childNodes != null && node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
if (node.childNodes[i].level > this.maxLevel) {
this.maxLevel = node.childNodes[i].level;
}
this.countNodeLevel(node.childNodes[i]);
}
}
},
append(data) {
console.log("append", data);
this.title = "添加分类";
this.dialogType = "add";
this.dialogVisible = true;
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.catId = null;
this.category.name = null;
this.category.icon = "";
this.category.productUnit = "";
this.category.showStatus = 1;
this.category.sort = "";
},
edit(data) {
console.log("要修改的数据", data);
this.title = "修改分类";
this.dialogType = "edit";
this.dialogVisible = true;
// 发送请求获取当前节点最新的数据
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: "get",
}).then(({ data }) => {
// 请求成功
console.log("要回显的数据", data);
this.category.catId = data.data.catId;
this.category.name = data.data.name;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
this.category.parentCid = data.data.parentCid;
this.category.catLevel = data.data.catLevel;
this.category.showStatus = data.data.showStatus;
this.category.sort = data.data.sort;
});
},
submitData(data) {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
// 添加三级分类的方法
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({
message: "菜单保存成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
// 修改三级分类的方法
editCategory() {
var { catId, name, icon, productUnit } = this.category;
var data = {
catId: catId,
name: name,
icon: icon,
productUnit: productUnit,
};
this.$http({
url: this.$http.adornUrl("/product/category/update"),
method: "post",
data: this.$http.adornData(data, false),
}).then(({ data }) => {
this.$message({
message: "菜单修改成功",
type: "success",
});
// 关闭提示框
this.dialogVisible = false;
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [this.category.parentCid];
});
},
remove(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({
message: "菜单删除成功",
type: "success",
});
// 刷新出新的菜单
this.getMenus();
// 设置需要默认展开的菜单
this.expandedKey = [node.parent.data.catId];
});
})
.catch(() => {
this.$message("取消删除");
});
console.log("remove", node, data);
},
},
//生命周期 - 创建完成(可以访问当前this实例)
created() {
this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.3.5、三级分类 [批量删除]
前端效果: 批量删除按钮
// <el-button /...>批量保存</el-button>
<el-button type="danger" @click="batchDelete">批量删除</el-button>
// <el-tree /...>
方法名 | 说明 | 参数 |
---|---|---|
getCheckedNodes | 若节点可被选择(即 show-checkbox 为 true ),则返回目前被选中的节点所组成的数组 | (leafOnly, includeHalfChecked) 接收两个 boolean 类型的参数,1. 是否只是叶子节点,默认值为 false 2. 是否包含半选节点,默认值为 false |
-
增加个批量删除的按钮 组件
<el-button type="danger" @click="batchDelete">批量删除</el-button>
-
给
el-tree
组件加上属性, 通过它可以获得选中分类的数据<el-tree // ... ref="menuTree" >
-
编写批量删除功能
// 批量删除功能 batchDelete() { let catIds = []; let checkedNodes = this.$refs.menuTree.getCheckedNodes(); console.log("被选中的元素", checkedNodes); for (let i = 0; i < checkedNodes.length; i++) { catIds.push(checkedNodes[i].catId); } this.$confirm(`是否删除当前[${catIds}]菜单?`, "提示", { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning", }) .then(() => { this.$http({ url: this.$http.adornUrl("/product/category/delete"), method: "post", data: this.$http.adornData(catIds, false), }).then(({ data }) => { this.$message({ message: "菜单批量删除成功", type: "success", }); this.getMenus(); }); }) .catch(() => { this.$message("取消删除"); }); },
-
使用后端提供的批量删除接口
二、品牌管理
使用 pms_brand 表 :
2.1、使用逆向工程的前后端代码
1、菜单管理->新增菜单
- 把生成的前端代码复制到前端工程下
将逆向工程生成的
brand.vue
、brand-add-or-update.vue
文件复制到前端项目:/renren-fast-vue/src/views/modules/product
下
本机放置路径: /Users/hgw/Documents/Data/Project/GuliMALL/逆向生成代码/gulimall-product/main/resources/src/views/modules/product
下
- 没有新增删除按钮: 修改权限,Ctrl+Shift+F查找
isAuth
,全部返回为true
修改src/utils/index.js
路径下的 isAuth
方法
/**
* 是否有权限
* @param {*} key
*/
export function isAuth (key) {
// return JSON.parse(sessionStorage.getItem('permissions') || '[]').indexOf(key) !== -1 || false
return true;
}
- 查看效果
2.2、效果优化-快速显示开关
这里因为EsLint规则太严格了, 一直报错. 这里并没有错. 对
build/webpack.base.conf.js
下文件进行修改, 注释掉createLintingRule
方法, 并重启项目
const createLintingRule = () => ({
// test: /\.(js|vue)$/,
// loader: 'eslint-loader',
// enforce: 'pre',
// include: [resolve('src'), resolve('test')],
// options: {
// formatter: require('eslint-friendly-formatter'),
// emitWarning: !config.dev.showEslintErrorsInOverlay
// }
})
2.2.1、前端修改
需求一: 在品牌管理页面 显示状态处加上一个开关按钮, 管控该品牌是否显示
在列表中添加自定义列:中间加<template></template>
标签。可以通过 Scoped slot
可以获取到 row, column, $index 和 store(table 内部的状态管理)的数据
<el-table-column prop="showStatus" header-align="center" align="center" label="显示状态">
<template slot-scope="scope">
<el-switch v-model="scope.row.showStatus" active-color="#13ce66" inactive-color="#ff4949">
</el-switch>
</template>
</el-table-column>
需求二: 在新增/修改对话框中 显示状态改成 开关按钮, 管控该品牌是否显示
修改src/views/modules/product/brand-add-or-update.vue
文件
<el-form-item label="显示状态" prop="showStatus">
<el-switch
v-model="dataForm.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</el-form-item>
2.2.2、修改开关状态,发送修改请求
事件名称 | 说明 | 回调参数 |
---|---|---|
change | switch 状态发生变化时的回调函数 | 新状态的值 |
参数 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
active-text | switch 打开时的文字描述 | string | — | — |
inactive-text | switch 关闭时的文字描述 | string | — | — |
<el-table-column
prop="showStatus"
header-align="center"
align="center"
label="显示状态"
>
<template slot-scope="scope">
<el-switch
v-model="scope.row.showStatus"
active-color="#13ce66"
inactive-color="#ff4949"
@change="updateBrandStatus(scope.row)"
:active-value="1"
:inactive-value="0"
>
</el-switch>
</template>
</el-table-column>
- 将 switch 打开时的文字描述绑定成 1
- 将 switch 关闭时的文字描述绑定成 0
组件change绑定方法 updateBrandStatus(), 并传入整行的数据scope.row
.
scope.row 包括一下信息
- brandId : 品牌id
- descript : 介绍
- firstLetter : 检索首字母
- logo : 品牌logo地址
- name : 品牌名称
- showStatus : 显示状态
- true : 显示
- false : 不限时
- sort : 排序
// 显示现状按钮触发事件
updateBrandStatus(data) {
console.log("最新信息", data);
let { brandId, showStatus } = data; // 从data中解构出brandId,showStatus
// 发送请求修改状态
this.$http({
url: this.$http.adornUrl("/product/brand/update"),
method: "post",
data: this.$http.adornData(
{ brandId, showStatus }, // 因为数据库中showStatus是int类型的, 这里通过一个三元运算符转换
false
),
}).then(({ data }) => {
this.$message({
type: "success",
message: "状态更新成功",
});
});
},
2.3、文件上传技术
和传统的单体应用不同,这里我们选择将数据上传到分布式文件服务器上。
这里我们选择将图片放置到阿里云上,使用对象存储。
介绍
阿里云对象存储服务(Object Storage Service,简称OSS),是阿里云对外提供的海量、安全、低成本、高可靠的云存储服务。您可以通过本文档提供的简单的REST接口,在任何时间、任何地点、任何互联网设备上进行上传和下载数据。基于OSS,您可以搭建出各种多媒体分享网站、网盘、个人和企业数据备份等基于大规模数据的服务。
中文 | 英文 | 说明 |
---|---|---|
存储空间 | Bucket | 存储空间是您用于存储对象(Object)的容器, 所有的对象都必须隶属于某个存储空间。 |
对象/文件 | Object | 对象是 OSS 存储数据的基本单元,也被称为 OSS的文件。对象由元信息(Object Meta) 、用户数据(Data)和文件名(Key)组成。 对象由存储空间内部唯一的Key来标识。 |
地域 | Region | 地域表示 OSS 的数据中心所在物理位置。 您可以根据费用、请求来源等综合选择数据存储 的地域。详情请查看OSS已经开通的Region。 |
访问域名 | Endpoint | Endpoint 表示OSS对外服务的访问域名。 OSS以HTTP RESTful API的形式对外提供服务, 当访问不同地域的时候,需要不同的域名。通过 内网和外网访问同一个地域所需要的域名也是 不同的。具体的内容请参见各个Region对应的Endpoint。 |
访问密钥 | AccessKey | AccessKey,简称 AK,指的是访问身份验证中 用到的AccessKeyId 和AccessKeySecret。OSS通过 使用AccessKeyId 和AccessKeySecret对称加密的方法 来验证某个请求的发送者身份。AccessKeyId用于标识 用户,AccessKeySecret是用户用于加密签名字符串 和OSS用来验证签名字符串的密钥,其中AccessKeySecret 必须保密。 |
2.3.1、开通阿里云OSS对象存储服务,创建新的Bucket
1、开通阿里云OSS对象存储服务,创建新的Bucket
2.3.2、oos整合测试
2.3.1、导入依赖
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.5.0</version>
</dependency>
2.3.2、获取Endpoint
、AccessKey ID
、AccessKey Secret
Endpoint
获取:
新建成功后得到==AccessKey ID
、AccessKey Secret
==
(这里不提供截图)
2.3.2.2、对子账户分配权限,管理OSS对象存储服务
(这里不提供截图)
2.3.3.3、测试上传
@Test
public void testUpload() throws IOException {
// 指定Endpoint
String endpoint = "你的Endpoint";
// 阿里云账号子用户
String accessKeyId = "你的accessKeyId";
String accessKeySecret = "你的accessKeySecret";
// 创建OSSClient实例
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
// 上传文件流
FileInputStream inputStream = new FileInputStream("/Users/hgw/Downloads/login.png");
ossClient.putObject("gulimall-hly", "login.png", inputStream);
// 关闭OSSClient
ossClient.shutdown();
inputStream.close();
System.out.println("上传成功");
}
2.3.3、对象存储测试
第一步、引入oss-starter依赖 (在 gulimall-common 模块中导入第三方依赖)
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
</dependency>
第二步、配置
Endpoint
、AccessKey ID
、AccessKey Secret
等信息
修改 gulimall-product 模块下 application.yml
文件
spring:
alicloud:
access-key: 你的access-key
secret-key: 你的secret-key
oss:
endpoint: 你的endpoint
第三步、使用OSSClient 进行相关操作
/**
* 1、引入oss-starter
* 2、配置key、endpoint相关信息
* 3、使用OSSClient 进行相关操作
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class GulimallProductApplicationTests {
@Autowired
OSSClient ossClient;
@Test
public void testUpload3() throws IOException {
// 上传文件流
FileInputStream inputStream = new FileInputStream("/Users/hgw/Downloads/1615260734059578.jpeg");
ossClient.putObject("gulimall-hly", "dog.png", inputStream);
// 关闭OSSClient
ossClient.shutdown();
inputStream.close();
System.out.println("上传成功");
}
2.3.4、建立第三方工程 (gulimall-third-party
)
2.3.4.1、新建一个module gulimall-third-party
随后对其进行, 降版本处理
第一步、引入依赖
<!--SpringCloud-nacos 注册中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--SpringCloud-nacos 配置中心-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2.1.0.RELEASE</version>
</dependency>
<!--存储对象依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alicloud-oss</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
第二步、注册到注册中心
- nacos新建命名空间
third-party
- 项目创建
application.yml
用来配置nacos信息
spring:
cloud:
nacos:
config:
server-addr: 127.0.0.1:8848
application:
name: gulimall-third-party
server:
port: 30000
- 在主启动类上加上
@EnableDiscoveryClient
注解
第三步、加入控制中心并配置oss.yml
-
nacos在
third-party
命名空间下创建oss.yml
,配置oss信息spring: cloud: alicloud: access-key: 你的 secret-key: 你的 oss: endpoint: 你的 bucket: gulimall-hly
-
本地项目新建
bootstrap.properties
, 配置注册中心信息spring.application.name=gulimall-third-party spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=104f67d0-dfb8-46e6-aec5-09efe9e7eae0 spring.cloud.nacos.config.ext-config[0].data-id=oss.yml spring.cloud.nacos.config.ext-config[0].group=DEFAULT_GROUP spring.cloud.nacos.config.ext-config[0].refresh=true
-
测试成功文件上传成功 !
2.3.4.2、OSS获取服务端签名
第四步、编写一个Controller请求
package com.hgw.gulimall.thirdparty.controller;
import com.aliyun.oss.OSS;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.aliyun.oss.model.MatchMode;
import com.aliyun.oss.model.PolicyConditions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Data time:2022/3/18 10:40
* StudentID:2019112118
* Author:hgw
* Description:
*/
@RestController
public class OssController {
@Autowired
OSS ossClient;
@Value("${spring.cloud.alicloud.oss.endpoint}")
String endpoint;
@Value("${spring.cloud.alicloud.oss.bucket}")
String bucket;
@Value("${spring.cloud.alicloud.access-key}")
String accessId;
@Value("${spring.cloud.alicloud.secret-key}")
String accessKey;
@RequestMapping("/oss/policy")
public Map<String, String> policy() {
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format; // 用户上传文件时指定的前缀。
Map<String, String> respMap=null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap= new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}
测试成功!
对其返回值进行封装成R对象
@RequestMapping("/oss/policy")
public R policy() {
String host = "https://" + bucket + "." + endpoint; // host的格式为 bucketname.endpoint
String format = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
String dir = format; // 用户上传文件时指定的前缀。
Map<String, String> respMap=null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);
String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes("utf-8");
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);
respMap= new LinkedHashMap<String, String>();
respMap.put("accessid", accessId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return R.ok().put("data",respMap);
}
2.3.4.3、配置网关
第五步、配置网关
spring:
cloud:
gateway:
routes:
- id: product_route
uri: lb://gulimall-product # 注册中心的服务
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?<segment>.*),/$\{segment}
- id: third_party_route
uri: lb://gulimall-third-party
predicates: # 什么情况下路由给它
- Path=/api/thirdparty/**
filters:
- RewritePath=/api/thirdparty/(?<segment>.*),/$\{segment}
- id: admin_route
uri: lb://renren-fast # 路由给renren-fast (lb)负载均衡
predicates: # 什么情况下路由给它
- Path=/api/** # 默认前端项目都带上api前缀,就是我们前面题的localhost:88/api
filters:
- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment} # 把/api/* 改变成 /renren-fast/*fast找
测试连接: http://localhost:88/api/thirdparty/oss/policy
/api/thirdparty/
情况下路由给注册中心注册的third_party_route服务- 将
/api/thirdparty/oss/policy
替换成/oss/policy
2.3.5、前端联调, 实现文件上传功能
需求: 实现在 新增/修改对话框中 品牌logo地址位置处 通过点击或者拖拽上传文件
文件上传组件在/renren-fast-vue/src/components
中, 将资料中的upload文件夹复制到该路径下
-
修改
src/components/upload/
路径下singleUpload.vue和multiUpload.vue 文件里组件中el-upload
中的action
属性,替换成自己的Bucket域名action="http:gulimall-hly.oss-cn-hangzhou.aliyuncs.com"
-
修改
src/components/upload/
路径下policy.js文件中http请求的地址 :url: http.adornUrl("/thirdparty/oss/policy"),
-
把单个文件上传组件应用到
brand-add-or-update.vue
//在<script>标签中导入组件 import singleUpload from "@/components/upload/singleUpload" //在export default中声明要用到的组件 export default { components: { singleUpload }, } // 用新的组件替换原来的输入框 <el-form-item label="品牌logo地址" prop="logo"> <!-- <el-input v-model="dataForm.logo" placeholder="品牌logo地址"></el-input> --> <single-upload v-model="dataForm.logo"></single-upload> </el-form-item>
2.3.6、解决跨域问题
在OSS中将Bucket设置为可以跨于访问
创建新规则
配置成功后, 点击图片上传, 进行测试
测试成功!
2.3.7、效果优化-显示图片
新增品牌,发现在品牌logo下面显示的是地址。应该显示图片。
在品牌logo下添加图片标签
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
<template slot-scope="scope">
<el-image
style="width: 100px; height: 80px"
:src="scope.row.logo"
fit="center"
></el-image>
</template>
</el-table-column>
解决 :
最后还是选择了使用原生的img组件
<el-table-column
prop="logo"
header-align="center"
align="center"
label="品牌logo地址"
>
<template slot-scope="scope">
<img :src="scope.row.logo" style="width: 100px; height: 80px" />
</template>
</el-table-column>
2.4、表单校验
2.4.1、前端表单校验
需求 :
- 首字母只能为a-z或者A-Z的一个字母
- 排序必须是大于等于0的一个整数
el-Form 组件提供了表单验证的功能,只需要通过 rules
属性传入约定的验证规则,并将 Form-Item 的 prop
属性设置为需校验的字段名即可。校验规则参见 async-validator
- 给
showStatus
、sort
属性设置默认值:
showStatus: 1,
sort: 0,
- 排序加上.number表示要接受一个数字
//排序加上.number表示要接受一个数字
<el-form-item label="排序" prop="sort">
<el-input v-model.number="dataForm.sort" placeholder="排序"></el-input>
</el-form-item>
- 自定义校验器
// 首字母校验guises
firstLetter: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("首字母必须填写"));
} else if (!/^[a-zA-Z]$/.test(value)) {
callback(new Error("首字母必须a-z或者A-Z之间"));
} else {
callback();
}
},
trigger: "blur",
},
],
sort: [
{
validator: (rule, value, callback) => {
if (value == "") {
callback(new Error("排序字段必须填写"));
} else if (!Number.isInteger(value) || value < 0) {
callback(new Error("排序必须是一个大于等于0的整数"));
} else {
callback();
}
},
trigger: "blur",
},
],
2.4.2、后端校验 JSR303数据校验
2.4.2.1、基本校验实现
第一步、给需要校验的数据(Bean) 添加校验注解
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@TableId
private Long brandId;
/**
* 品牌名
* @NotBlank 字段不能一个或多个空格,则不能为空且不能有空格
* @NotEmpty 不能为空
*/
@NotBlank(message = "品牌名必须提交")
private String name;
/**
* 品牌logo地址
*/
@NotEmpty
@URL(message = "logo必须是一个合法的url地址")
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母")
private String firstLetter;
/**
* 排序
*/
@NotNull
@Min(value = 0, message = "排序必须大于等于0")
private Integer sort;
}
第二步、在需要校验的方法上添加
@Valid
注解,并返回提示信息
- 给校验的bean后紧跟着一个BindingResult,就可以获取到校验的结果
/**
* 保存
*/
@RequestMapping("/save")
// @RequiresPermissions("product:brand:save")
public R save(@Valid @RequestBody BrandEntity brand, BindingResult result){
// 是否有校验错误
if (result.hasErrors()){
Map<String,String> map = new HashedMap();
// 1、获取校验的错误结果
result.getFieldErrors().forEach((item)->{
//FieldError 获取到错误提示
String message = item.getDefaultMessage();
// 获取错误的属性的名字
String field = item.getField();
map.put(field,message);
});
return R.error(400,"提交的数据不合法").put("data", map);
} else {
brandService.save(brand);
}
return R.ok();
}
测试结果正常:
2.4.2.2、统一异常处理
因为其他模块也会存在校验问题, 这样太过于繁琐, 这里校验异常抛出去做统一处理.
2.4.2.2.1、系统错误码
-
错误码定义规则为 5 个数字
-
前两位表示业务场景, 最后三位表示错误码.
- 例如: 100001
- 10 : 通用
- 001 : 系统未知异常
- 例如: 100001
-
维护错误码后需要维护错误描述, 将他们定义为枚举形式
错误码列表:
- 10 : 通用
- 11 : 商品
- 12 : 订单
- 13 : 购物车
- 14 : 物流
为了定义这些错误状态码,我们可以单独定义一个常量类,用来存储这些错误状态码。
-
在common中新建
BizCodeEnume
用来存储状态码package com.hgw.common.exception; /** * 1. 错误码定义规则为 5 个数字 * 2. 前两位表示业务场景, 最后三位表示错误码. * + 例如: 100001 * + 10 : 通用 * + 001 : 系统未知异常 * * 3. 维护错误码后需要维护错误描述, 将他们定义为枚举形式 * * 错误码列表: * * + 10 : 通用 * + 11 : 商品 * + 12 : 订单 * + 13 : 购物车 * + 14 : 物流 */ public enum BizCodeEnume { UNKNOW_EXEPTION(10000,"系统未知异常"), VALID_EXCEPTION( 10001,"参数格式校验失败"); private int code; private String msg; BizCodeEnume(int code, String msg) { this.code = code; this.msg = msg; } public int getCode() { return code; } public String getMsg() { return msg; } }
2.4.2.2.2、集中处理所有异常类
- 在
/product/exception/路径下面新建类
GulimallExceptionControllerAdvice`,用来集中处理所有异常
/**
* Description: 集中处理所有异常
* @ControllerAdvice 统一处理异常
* basePackages = "com/hgw/gulimall/product/controller": 接收了由本模块controller层抛过来的异常
* @RestControllerAdvice = @ResponseBody + @ControllerAdvice
*/
@Slf4j
@RestControllerAdvice(basePackages = "com.hgw.gulimall.product.controller")
public class GulimallExceptionControllerAdvice {
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public R handleVaildException(MethodArgumentNotValidException e){
log.error("数据校验出现问题{},异常类型{}",e.getMessage(),e.getClass());
BindingResult result = e.getBindingResult();
Map<String,String> errorMap = new HashedMap();
result.getFieldErrors().forEach(fieldError -> {
errorMap.put(fieldError.getField(),fieldError.getDefaultMessage());
});
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg()).put("data",errorMap);
}
@ExceptionHandler(value = Throwable.class)
public R handleException(Throwable throwable) {
return R.error(BizCodeEnume.VALID_EXCEPTION.getCode(),BizCodeEnume.VALID_EXCEPTION.getMsg());
}
}
2.4.2.3、分组校验
-
在common中新建valid包,里面新建两个空接口
AddGroup
,UpdateGroup
用来分组 -
给校验注解,标注上groups,指定什么情况下才需要进行校验. 没有标注分组就不会被校验
如下代码:- UpdateGroup 分组下必须指定brandId
- AddGroup 分组下不能指定brandId
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class}) @Null(message = "新增不能指定id", groups = {AddGroup.class}) @TableId private Long brandId;
-
业务方法参数上使用@Validated注解,并在value中给出group接口,标记当前校验是哪个组
/** * 保存 */ @RequestMapping("/save") public R save(@Validated({AddGroup.class}) @RequestBody BrandEntity brand){ brandService.save(brand); return R.ok(); } /** * 修改 */ @RequestMapping("/update") public R update(@Validated({UpdateGroup.class}) @RequestBody BrandEntity brand){ brandService.updateById(brand); return R.ok(); }
- 默认情况下,在分组校验情况下,没有指定指定分组的校验注解,将不会生效,它只会在不分组的情况下生效
BrandEntity
类完整代码如下 :
@Data
@TableName("pms_brand")
public class BrandEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 品牌id
*/
@NotNull(message = "修改必须指定品牌id",groups = {UpdateGroup.class})
@Null(message = "新增不能指定id", groups = {AddGroup.class})
@TableId
private Long brandId;
/**
* 品牌名
* @NotBlank 字段不能一个或多个空格,则不能为空且不能有空格
* @NotEmpty 不能为空
*/
@NotBlank(message = "品牌名必须提交", groups = {AddGroup.class,UpdateGroup.class})
private String name;
/**
* 品牌logo地址
*/
@NotBlank(groups = {AddGroup.class})
@URL(message = "logo必须是一个合法的url地址", groups = {AddGroup.class,UpdateGroup.class})
private String logo;
/**
* 介绍
*/
private String descript;
/**
* 显示状态[0-不显示;1-显示]
*/
private Integer showStatus;
/**
* 检索首字母
*/
@NotEmpty(groups = {AddGroup.class})
@Pattern(regexp = "^[a-zA-Z]$",message = "检索首字母必须是一个字母", groups = {AddGroup.class,UpdateGroup.class})
private String firstLetter;
/**
* 排序
*/
@NotNull(groups = {AddGroup.class})
@Min(value = 0, message = "排序必须大于等于0", groups = {AddGroup.class,UpdateGroup.class})
private Integer sort;
}
2.4.2.4、自定义校验
比如说 显示状态 是Integer属性, 我们可以用正则表达式来描述我们的一些校验, 但有些校验正则表达式实现不了, 故需要自定义校验 .
- 编写一个自定义校验注解
ListValue
- 新建配置文件
ValidationMessages.properties
保存注解信息 - 编写一个自定义校验器
ListValueConstraintValidator
- 关联自定义的校验器和自定义的校验注解
(可以指定多个不同的校验器,适配不同类型的校验)
- 校验注解:
- 在JSR303规范中校验注解必须满足拥有以下前三个属性
- message : 校验出错后, 出错消息从哪儿取
- groups : 校验得支持分组校验得功能
- payload : 自定义一些负载信息
- 得有以下元注解信息
- @Target : 指定注解可以标注的位置
- @Retention : 校验注解的运行时机, 这里指定在运行时获得
- @Constraint : 指定校验器校验, 可以指定多个
- 在JSR303规范中校验注解必须满足拥有以下前三个属性
@Documented
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
public @interface ListValue {
String message() default "{com.hgw.gulimall.product.valid.ListValue.message}";
Class<?>[] groups() default { };
Class<? extends Payload>[] payload() default { };
int[] vals() default { };
}
- 配置文件:
com.hgw.gulimall.product.valid.ListValue.message=必须提交指定的值
- 自定义校验器:
public class ListValueConstraintValidator implements ConstraintValidator<ListValue,Integer> {
private Set<Integer> set = new HashSet<>();
// 初始化方法(这里将ListValue注解的详细信息给我们, 比如: vals={0,1}
@Override
public void initialize(ListValue注解的详细信息给我们 constraintAnnotation) {
int[] vals = constraintAnnotation.vals();
for (int val : vals) {
set.add(val);
}
}
/**
* 判断是否校验成功
* @param value 需要校验的值
* @param context 上下文环境信息
* @return
*/
@Override
public boolean isValid(Integer value, ConstraintValidatorContext context) {
return set.contains(value);
}
}
- 关联校验器和校验注解:在校验注解的
@Constraint
注解上关联校验器
@Constraint(validatedBy = { ListValueConstraintValidator.class })
@Constraint(validatedBy = { ListValueConstraintValidator.class, 校验器... })
可以关联多个校验器, 不同情况下使用不同的校验器
- 校验注解添加到showStatus上,进行测试
@ListValue(vals={0,1}, groups = {AddGroup.class})
private Integer showStatus;
2.4.2.5、测试补漏
品牌管理的首页 是否显示按钮的修改 和 修改对话框中的修改使用的是一套校验, 这是不对的. 再次进行补漏
-
在common模块valid包下创建一个
UpdateStatusGroup
接口用于分类 -
指定showStatus的校验规则分组
/** * 显示状态[0-不显示;1-显示] */ @NotNull(groups = {AddGroup.class, UpdateStatusGroup.class}) @ListValue(vals={0,1}, groups = {AddGroup.class, UpdateStatusGroup.class}) private Integer showStatus;
-
编写一个单独修改显示状态的Controller
/** * 单独修改显示状态 */ @RequestMapping("/update/status") // @RequiresPermissions("product:brand:update") public R updateStatus(@Validated({UpdateStatusGroup.class}) @RequestBody BrandEntity brand){ brandService.updateById(brand); return R.ok(); }
-
修改显示现状按钮触发事件
updateBrandStatus
方法的http请求uri// 显示现状按钮触发事件 updateBrandStatus(data) { console.log("最新信息", data); let { brandId, showStatus } = data; // 从data中解构出brandId,showStatus // 发送请求修改状态 this.$http({ url: this.$http.adornUrl("/product/brand/update/status"), method: "post", data: this.$http.adornData( { brandId, showStatus }, // 因为数据库中showStatus是int类型的, 这里通过一个三元运算符转换 false ), }).then(({ data }) => { this.$message({ type: "success", message: "状态更新成功", }); }); },
2.5、实现分页-引入插件
发现自动生成的分页条不好使,原因是没有引入mybatis-plus的分页插件。新建配置类,引入如下配置
com/hgw/gulimall/product/config/
路径下创建 MyBatisConfig.java类, 编写MyBatisPlus分页配置
@Configuration
@EnableTransactionManagement // 开启事务
@MapperScan("com.hgw.gulimall.product.dao")
public class MyBatisConfig {
// 最新版
@Bean
public PaginationInterceptor mybatisPlusInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
// 设置请求的页面大于最大页后操作, true调回到首页, false继续请求, 默认false
paginationInterceptor.setOverflow(true);
// 设置最大单页限制数量, 默认 500条, -1不受限制
paginationInterceptor.setLimit(1000);
return paginationInterceptor;
}
}
2.6、模糊查询
修改product.service.impl.BrandServiceImpl
类的queryPage方法
@Service("brandService")
public class BrandServiceImpl extends ServiceImpl<BrandDao, BrandEntity> implements BrandService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
// 1、获取key
String key = (String) params.get("key");
QueryWrapper<BrandEntity> queryWrapper = new QueryWrapper<>();
if (!StringUtils.isEmpty(key)) {
queryWrapper.eq("brand_id", key).or().like("name",key);
}
IPage<BrandEntity> page = this.page(
new Query<BrandEntity>().getPage(params),
queryWrapper
);
return new PageUtils(page);
}
}