- 分布式基础篇
- csdn机制问题导致图片丢失,可以查看本人的个人博客:[谷粒商城-P45-P58](https://www.r2coding.vip/articles/2022/05/04/1651673906725.html)
- 1. 三级分类
- 1.1 sql脚本
- 1.2 查出所有分类以及子分类
- 1.2.1 后段代码编写
- 1.2.2前端代码编写
- 1.2.2.1配置网关路由与路径重写
- 1.2.2.2前端代码
- 1.2.3renren-fast相关报错
- javax.validation.constraints不存在
- 1.2.4 请求验证码接口返回200,但是不显示出来
- 1.2.5 跨域问题
- 1.2.5.1 简单请求
- 1.2.6解决跨域
- 1.2.6.1 跨域流程
- 1.2.6.2 方式一: 使用nginx部署为同一域
- 1.2.6.3 方式二: 配置当次请求允许跨域
- 1.3 三级分类-查询-树形结构
- 1.3.1 将 **gulimall-product** 注册到nacos 中 具体配置如下:
- 1.4 前端代码,树形结构的增删改查(P51-P58)
- 1.5 后端代码,树形结构增删改查
三级分类数据脚本
1.2 查出所有分类以及子分类 1.2.1 后段代码编写在gulimall-product的src/main/java/com/rabbitboss/gulimall/product/controller/CategoryController.java`➕以下代码
/**
* 查处所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
//@RequiresPermissions("product:category:list")
public R list(@RequestParam Map<String, Object> params){
// PageUtils page = categoryService.queryPage(params);
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
src/main/java/com/rabbitboss/gulimall/product/service/CategoryService.java
添加以下代码
List<CategoryEntity> listWithTree();
src/main/java/com/rabbitboss/gulimall/product/service/impl/CategoryServiceImpl.java
添加以下代码
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2。组装成父子树状结构
//2.1找到所有的一级分类
List<CategoryEntity> level1Menu = entities.stream().filter(categoryEntity ->
categoryEntity.getParentCid() == 0
).map(menu -> {
//1。找到子分类
menu.setChildren(getChildrens(menu, entities));
return menu;
}).sorted((menu1, menu2) ->
//2。排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return level1Menu;
}
/**
* 递归查找所有菜单的子菜单
*
* @param root
* @param all
* @return
*/
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter((categoryEntity) -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1。找到子分类
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) ->
//2。排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return children;
}
1.2.2前端代码编写
1.2.2.1配置网关路由与路径重写
接着 *** 作后台
localhost:8001 , 点击系统管理,菜单管理,新增
目录
商品系统
一级菜单
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a2SMyzJ5-1651674156447)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-2215375846762372900-191c3030.png)]
1.2.2.2前端代码注意地址栏http://localhost:8001/#/product-category 可以注意到product-category我们的/被替换为了-
比如sys-role具体的视图在renren-fast-vue/views/modules/sys/role.vue
所以要自定义我们的product/category视图的话,就是创建mudules/product/category.vue
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick">el-tree>
template>
<script>
// 这里可以导入其他文件(比如:组件,工具 js,第三方插件 js,json 文件,图片文件等等)
// 例如:import 《组件名称》 from '《组件路径》 ';
export default {
// import 引入的组件需要注入到对象中才能使用
components: {},
props: {},
data () {
return {
data: [],
defaultProps: {
children: 'children',
label: 'label'
}
}
},
// 计算属性 类似于 data 概念
computed: {},
// 监控 data 中的数据变化
watch: {},
// 方法集合
methods: {
handleNodeClick (data) {
console.log(data)
},
getMenus () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
// params: this.$http.adornParams({
// 'page': this.pageIndex,
// 'limit': this.pageSize,
// 'roleName': this.dataForm.roleName
// })
}).then(data => {
console.log('成功获取到菜单数据...', data)
})
}
},
// 生命周期 - 创建完成(可以访问当前this 实例)
created () {
this.getMenus()
},
// 生命周期 - 挂载完成(可以访问 DOM 元素)
mounted () {
},
beforeCreate () {
},
beforeMount () {
}, // 生命周期 - 挂载之前
beforeUpdate () {
}, // 生命周期 - 更新之前
updated () {
}, // 生命周期 - 更新之后
beforeDestroy () {
}, // 生命周期 - 销毁之前
destroyed () {
}, // 生命周期 - 销毁完成
activated () {
} // 如果页面有 keep-alive 缓存功能,这个函数会触发
}
script>
<style scoped>
style>
他要给8080发请求读取数据,但是数据是在10000端口上,如果找到了这个请求改端口那改起来很麻烦。
-
方法1是改vue项目里的全局配置
-
方法2是搭建个网关,让网关路由到10000
Ctrl+Shift+F全局搜索 在static/config/index.js里 window.SITE_CONFIG['baseUrl'] = 'http://localhost:88/api'; 接着让重新登录http://localhost:8001/#/login,验证码是请求88的,所以不显示。而验证码是来源于fast后台的 现在的验证码请求路径为,http://localhost:88/api/captcha.jpg 原始的验证码请求路径:http://localhost:8001/renren-fast/captcha.jpg
-
88是GateWay的端口
那么怎么将88:/api
转发到renren-fast上呢?看下面的步骤
# 1.在renren-fast模块中引入common模块 ➡️ pom.xml中引入common,因为common中集成了Nacos
>
>com.rabbitboss.gulimall >
>gulimall-common>
>0.0.1-SNAPSHOT >
>
# 2.在renren-fast模块的application.yml文件中添加以下内容
application:
name: renren-fast
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
# 3.在renren-fast的启动类上加上注解 @EnableDiscoveryClient
# 4在gulimall-gateway的application.yml添加以下代码
spring:
cloud:
gateway:
routes:
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
在重启renren-fast的时候可能会出错,具体错误请看下面解决过程
1.2.3renren-fast相关报错
renren-fast启动报错:
Your project setup is incompatible with our requirements due to following reasons:
- Spring Boot [2.2.4.RELEASE] is not compatible with this Spring Cloud release train
Action:
Consider applying the following actions:
- Change Spring Boot version to one of the following versions [2.6.x, 2.7.x]
因为在Common模块里面是用的3.1的Nacos,最低支持SpringBoot2.6的版本
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xqp6Z97p-1651674156448)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-2673391811449110828-19d9724b.png)]
所以需要将renren-fast模块里面的pom.xml把SpringBoot版本指定成2.6.6和谷粒商城其他模块的版本保持一致
renren-fast编译报错
javax.validation.constraints不存在
请看另外一篇博客javax.validation.constraints 不存在
启动报错
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2022-04-30 16:25:02.551 ERROR 35690 --- [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPLICATION FAILED TO START *************************** Description: The dependencies of some of the beans in the application context form a cycle: sysLoginController (field private io.renren.modules.sys.service.SysUserService io.renren.modules.sys.controller.SysLoginController.sysUserService) ┌─────┐ | sysUserService (field private io.renren.modules.sys.service.SysRoleService io.renren.modules.sys.service.impl.SysUserServiceImpl.sysRoleService) ↑ ↓ | sysRoleService (field private io.renren.modules.sys.service.SysUserService io.renren.modules.sys.service.impl.SysRoleServiceImpl.sysUserService) └─────┘ Action: Relying upon circular references is discouraged and they are prohibited by default. Update your application to remove the dependency cycle between beans. As a last resort, it may be possible to break the cycle automatically by setting spring.main.allow-circular-references to true.
按照提示在renren-fast项目下的:
src/main/resources/application.yml
加入以下配置
spring.main.allow-circular-references=true
启动报错
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2022-04-30 16:35:10.693 ERROR 40671 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'scheduleJobController': Unsatisfied dependency expressed through field 'scheduleJobService'; nested exception is org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'scheduleJobService': Unsatisfied dependency expressed through field 'scheduler'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'schedulerFactoryBean' defined in class path resource [io/renren/modules/job/config/ScheduleConfig.class]: Invocation of init method failed; nested exception is org.quartz.SchedulerConfigException: DataSource name not set.
特别强调一下:
spring-boot-starter-quartz 2.5.6 之前使用org.quartz.impl.jdbcjobstore.JobStoreTX定义quartz的默认数据源支持,即如下配置:
org.quartz.jobStore.class=org.quartz.impl.jdbcjobstore.JobStoreTX
升级spring boot > 2.5.6的版本后将不再支持此方式进行配置默认数据源,需改为如下配置:
org.quartz.jobStore.class=org.springframework.scheduling.quartz.LocalDataSourceJobStore
因为谷粒商城其他模块是SpringBoot2.6.6的版本,所以应该采取第二种写法
解决步骤
修改renren-fast项目下的:
src/main/java/io/renren/modules/job/config/ScheduleConfig.java
这个文件按照下面的修改:
prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); // 修改成 prop.put("org.quartz.jobStore.class", "org.springframework.scheduling.quartz.LocalDataSourceJobStore");
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o7PsMIlX-1651674156449)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-1206516960012513860-cf207e0c.png)]
1.2.4 请求验证码接口返回200,但是不显示出来启动报错
报错原因:SpringBoot 2.6.x高版本和swagger2版本冲突
Error starting ApplicationContext. To display the conditions report re-run your application with 'debug' enabled. 2022-04-30 15:29:19.138 ERROR 15886 --- [ main] o.s.boot.SpringApplication : Application run failed org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper'; nested exception is java.lang.NullPointerException
解决步骤
- 在renren-fast项目下的:
src/main/resources/application.yml
添加以下配置
spring.mvc.pathmatch.matching-strategy=ant-path-matcher
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lZc4UWuh-1651674156450)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-283096653150069449-8e461ea9.png)]
1.2.5 跨域问题[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bnckr0F7-1651674156452)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-3501653427239191771-31a311e8.png)]
1.2.5.1 简单请求简单请求
某些请求不会触发 CORS 预检请求。本文称这样的请求为“简单请求”,请注意,该术语并不属于 Fetch(其中定义了 CORS)规范。若请求 满足所有下述条件,则该请求可视为“简单请求”:
使用下列方法之一:
GET
HEAD
POST
除了被用户代理自动设置的首部字段(例如 Connection
,User-Agent
)和在 Fetch 规范中定义为 禁用首部名称 的其他首部,允许人为设置的字段为 Fetch 规范定义的 对 CORS 安全的首部字段集合。该集合为:
Accept
Accept-Language
Content-Language
Content-Type
(需要注意额外的限制)
Content-Type
的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
请求中的任意 XMLHttpRequest
对象均没有注册任何事件监听器;XMLHttpRequest
对象可以使用 XMLHttpRequest.upload
属性访问。
请求中没有使用 ReadableStream
对象。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dVAMamFc-1651674156453)(http://www.kaotop.com/file/tupian/20220505/solo-fetchupload-1558048554553530534-464c03a2.png)]
1.2.6.3 方式二: 配置当次请求允许跨域生产环境 的话 推荐使用这种方法解决跨域,原因如下
- 访问静态 域名 可以通过 nginx 直接转到后台管理系统
- 后台登录等其他的增删改查功能,直接通过 nginx 转到 gateway 服务
添加响应头
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:表明该响应的有效时间为多少秒。在有效时间内,浏览器无
须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果
该首部字段的值超过了最大有效时间,将不会生效。
后端跨域配置
1.3 三级分类-查询-树形结构第一步:注释掉 renrenfast 配置的路由规则
注释掉renrenfast 配置的路由规则的类:
src/main/java/io/renren/config/CorsConfig.java
第二步
在gulimall-gateway 模块的
src/main/resources/application.yml
下加入以下代码spring: cloud: gateway: routes: - id: baidu_route uri: https://www.baidu.com predicates: - Query=url,baidu - id: qq_route uri: https://www.qq.com predicates: - Query=url,qq - id: admin_route uri: lb://renren-fast predicates: - Path=/api/** filters: - RewritePath=/api/(?
>.*),/renren-fast/$\{segment} 第三步
配置跨域详细信息
第一种配置方式:「拦截器的方式:CorsFilter 」
/** * @description: 跨域配置 * @author: Jonny * @time: 2022/5/1 8:33 PM */ @Configuration public class GulimallCorsConfiguration { @Bean public CorsWebFilter corsWebFilter(){ UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); // 1.配置跨域 corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedMethod("*"); // corsConfiguration.addAllowedOrigin("*"); corsConfiguration.addAllowedOriginPattern("*"); corsConfiguration.setAllowCredentials(true); source.registerCorsConfiguration("/**",corsConfiguration); return new CorsWebFilter(source); } }
第二种配置方式:「全局配置的方式:WebMvcConfigurer接口的addCorsMappings方法中注册CORS配置」
@EnableWebMvc @Configuration public class MvcConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { //每次调用registry.addMappin可以添加一个跨域配置,需要多个配置可以多次调用registry.addMapping registry.addMapping("/**") .allowedOrigins("*") //放行哪些原始域 .allowedMethods("PUT", "DELETE","POST", "GET") //放行哪些请求方式 .allowedHeaders("header1", "header2", "header3") //放行哪些原始请求头部信息 .exposedHeaders("header1", "header2") //暴露哪些头部信息 .allowCredentials(false) //是否发送 Cookie .maxAge(3600); // Add more mappings... } }
第三种方式:使用
@CrossOrigin
注解
访问http://localhost:8001/#/product-category
报404是因为还没有在 nacos 中并没有把 gulimall-product 服务注册进去
- 修改product 服务的
src/main/resources/application.yml
文件spring: datasource: username: root password: root url: jdbc:mysql://192.168.1.17:3306/gulimall_pms driver-class-name: com.mysql.jdbc.Driver 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
- 在统计目录下创建
bootstrap.properties
配置 nacos 以及绑定命名空间 idspring.application.name=gulimall-product spring.cloud.nacos.config.server-addr=127.0.0.1:8848 spring.cloud.nacos.config.namespace=f0882d03-cf30-47f9-92e5-19a362b937b6
- 在 gulimall-product 模块的启动类加上
@EnableDiscoveryClient
这个注解
前面在解决跨域问题的时候已经加了只要是 api 打头的请求都会转发到 renren-fast模块
- id: admin_route
uri: lb://renren-fast
predicates:
- Path=/api/**
filters:
- RewritePath=/api/(?>.*),/renren-fast/$\{segment}
现在需要做的是 http://localhost:8001/#/product-category
这个请求是要转发到 gulimall-product 模块去的,之前配置的断言已经不符合要求了,需要再添加断言规则
## 在admin_route 断言上面再加入下面的断言规则
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/(?>.*),/$\{segment}
1.4 前端代码,树形结构的增删改查(P51-P58)
<template>
<div>
<el-switch
v-model="draggable"
active-text="开启拖拽"
inactive-text="关闭拖拽">
el-switch>
<el-button v-show="draggable" @click="batchSave">批量保存el-button>
<el-button type="danger" @click="batchDelete">批量删除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"
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)">
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-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: {},
props: {},
data () {
return {
pCid: [],
draggable: false,
updateNodes: [],
maxLeve: 0,
title: '',
dialogType: '', // add,edit
category: {
name: '',
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
catId: null,
icon: '',
productUnit: ''
},
dialogVisible: false,
menus: [],
expandedKey: [],
defaultProps: {
children: 'children',
label: 'name'
}
}
},
// 计算属性 类似于 data 概念
computed: {},
// 监控 data 中的数据变化
watch: {},
// 方法集合
methods: {
getMenus () {
this.dataListLoading = true
this.$http({
url: this.$http.adornUrl('/product/category/list/tree'),
method: 'get'
// params: this.$http.adornParams({
// 'page': this.pageIndex,
// 'limit': this.pageSize,
// 'roleName': this.dataForm.roleName
// })
}).then(({data}) => {
console.log('成功获取到菜单数据...', data.data)
this.menus = data.data
})
},
batchSave () {
this.$http({
url: this.$http.adornUrl('/product/category/update/sort'),
method: 'post',
data: this.$http.adornData(this.updateNodes, false)
}).then(({data}) => {
this.$message({
type: 'success',
message: '菜单等修改成功!'
})
// 刷新菜单
this.getMenus()
// 设置默认展开的菜单
this.expandedKey = this.pCid
this.updateNodes = []
this.maxLeve = 0
// this.pCid = 0
})
},
batchDelete () {
let catIds = []
let nodesName = []
let checkedNodes = this.$refs.menuTree.getCheckedNodes()
console.log('checkedNodes:', checkedNodes)
for (let i = 0; i < checkedNodes.length; i++) {
catIds.push(checkedNodes[i].catId)
nodesName.push(checkedNodes[i].name)
}
this.$confirm(`是否批量删除「${nodesName}」?`, '提示', {
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({
type: 'success',
message: ' 菜单批量删除成功!'
})
this.getMenus()
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
},
handleDrop (draggingNode, dropNode, dropType, ev) {
console.log('handleDrop: ', draggingNode, dropNode, dropType, ev)
// 1.当前节点的父节点 Id
let pCid = 0
let sibling = null
if (dropType === 'before' || dropType === 'after') {
pCid = dropNode.parent.data.catId === undefined ? 0 : dropNode.parent.data.catId
sibling = dropNode.parent.childNodes
} else {
pCid = dropNode.data.catId
sibling = dropNode.childNodes
}
this.pCid.push(pCid)
// 2.当前拖拽的节点的最新顺序
for (let i = 0; i < sibling.length; i++) {
// 如果遍历的是当前拖拽的节点
if (sibling[i].data.catId === draggingNode.data.catId) {
let catLevel = draggingNode.level
// 如果层级发生变化
if (sibling[i].level !== draggingNode.level) {
// 拿到最新层级
catLevel = sibling[i].level
// 修改被拖动元素 子节点的层级
this.updateChildNodeLevel(sibling[i])
}
this.updateNodes.push({catId: sibling[i].data.catId, sort: i, parentCid: pCid, catLevel: catLevel})
} else {
this.updateNodes.push({catId: sibling[i].data.catId, sort: i})
}
}
// 3.当前拖拽的节点的最新层级
console.log(this.updateNodes)
},
updateChildNodeLevel (node) {
if (node.childNodes.length > 0) {
for (let i = 0; i < node.childNodes.length; i++) {
var cNodes = node.childNodes[i].data
this.updateNodes.push({catId: cNodes.catId, catLevel: node.childNodes[i].level})
this.updateChildNodeLevel(node.childNodes[i])
}
}
},
allowDrop (draggingNode, dropNode, type) {
// 1.判断被拖动的节点以及所在的父节点层级不能 > 3
// 1)被拖动的节点的总层数
console.log('当前节点,xxx,节点所在位置', draggingNode, dropNode, type)
// 2)求出当前节点的总层数
this.countNodeLevel(draggingNode)
// let deep = (this.maxLeve - draggingNode.data.catLevel) + 1
let deep = Math.abs(this.maxLeve - draggingNode.level) + 1
console.log('深度', deep)
// 3)当前正在拖动的父节点不大于3 即可
if (type === 'inner') {
return deep + draggingNode.level <= 3
} else {
return deep + draggingNode.parent.level <= 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.maxLeve) {
this.maxLeve = node.childNodes[i].level
}
this.countNodeLevel(node.childNodes[i])
}
}
},
append (data) {
console.log('append', data)
this.dialogType = 'add'
this.title = '添加分类'
this.category.parentCid = data.catId
this.category.catLevel = data.catLevel * 1 + 1
// 清空以及添加默认及需要新增的字段
this.category.catId = null
this.category.name = ''
this.category.showStatus = 1
this.category.productUnit = ''
this.category.icon = ''
this.category.sort = 0
// 显示d出框
this.dialogVisible = true
},
// 添加三级分类
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.expandedKey = [this.category.parentCid]
})
},
edit (data) {
console.log('要修改的数据:', data)
this.dialogType = 'edit'
this.title = '修改分类'
this.dialogVisible = true
// 这样写有这样的一个问题:就是当我需要修改时,我不会刷新页面再新增,
// 比如10分钟之后我再修改,但是在这期间已经有其他人修改了,但是我页面上还是显示的是10分钟之前的数据
// 发送请求获取最新的分类信息
this.$http({
url: this.$http.adornUrl(`/product/category/info/${data.catId}`),
method: 'get',
params: this.$http.adornParams({})
}).then(({data}) => {
// 请求成功回显数据
console.log('要回显的数据:', data)
this.category.name = data.data.name
this.category.catId = data.data.catId
this.category.icon = data.data.icon
this.category.productUnit = data.data.productUnit
this.category.parentCid = data.data.parentCid
})
},
editCategory () {
console.log('提交三级分类的数据:', this.category)
var {catId, name, icon, productUnit} = this.category
this.$http({
url: this.$http.adornUrl('/product/category/update'),
method: 'post',
data: this.$http.adornData({catId, name, icon, productUnit}, false)
}).then(({data}) => {
this.$message({
type: 'success',
message: '菜单修改成功!'
})
// 关闭对话框
this.dialogVisible = false
// 刷新 刷新树状结构
this.getMenus()
// 设置默认展开的菜单
this.expandedKey = [this.category.parentCid]
})
},
submitData () {
if (this.dialogType === 'add') {
this.addCategory()
}
if (this.dialogType === 'edit') {
this.editCategory()
}
},
remove (node, data) {
console.log('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}) => {
// console.log('删除成功')
this.$message({
type: 'success',
message: '菜单删除成功!'
})
// 刷新新的菜单
this.getMenus()
// 设置默认展开的菜单
this.expandedKey = [node.parent.data.catId]
})
}).catch(() => {
this.$message({
type: 'info',
message: '已取消删除'
})
})
}
},
// 生命周期 - 创建完成(可以访问当前this 实例)
created () {
this.getMenus()
},
// 生命周期 - 挂载完成(可以访问 DOM 元素)
mounted () {
},
beforeCreate () {
},
beforeMount () {
}, // 生命周期 - 挂载之前
beforeUpdate () {
}, // 生命周期 - 更新之前
updated () {
}, // 生命周期 - 更新之后
beforeDestroy () {
}, // 生命周期 - 销毁之前
destroyed () {
}, // 生命周期 - 销毁完成
activated () {
} // 如果页面有 keep-alive 缓存功能,这个函数会触发
}
script>
<style scoped>
style>
1.5 后端代码,树形结构增删改查
- controller
@RestController
@RequestMapping("product/category")
public class CategoryController {
@Autowired
private CategoryService categoryService;
/**
* 查处所有分类以及子分类,以树形结构组装起来
*/
@RequestMapping("/list/tree")
//@RequiresPermissions("product:category:list")
public R list(@RequestParam Map<String, Object> params){
// PageUtils page = categoryService.queryPage(params);
List<CategoryEntity> entities = categoryService.listWithTree();
return R.ok().put("data", entities);
}
/**
* 信息
*/
@RequestMapping("/info/{catId}")
//@RequiresPermissions("product:category:info")
public R info(@PathVariable("catId") Long catId){
CategoryEntity category = categoryService.getById(catId);
return R.ok().put("data", category);
}
/**
* 保存
*/
@RequestMapping("/save")
//@RequiresPermissions("product:category:save")
public R save(@RequestBody CategoryEntity category){
categoryService.save(category);
return R.ok();
}
/**
* 修改
*/
@RequestMapping("/update")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity category){
categoryService.updateById(category);
return R.ok();
}
/**
* 批量修改
* @param category
* @return
*/
@RequestMapping("/update/sort")
//@RequiresPermissions("product:category:update")
public R update(@RequestBody CategoryEntity[] category){
categoryService.updateBatchById(Arrays.asList(category));
return R.ok();
}
/**
* 删除
* @RequestBody 获取请求体, 必须发送 POST 请求
* 只有 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);
}
@Service("categoryService")
public class CategoryServiceImpl extends ServiceImpl<CategoryDao, CategoryEntity> implements CategoryService {
@Override
public PageUtils queryPage(Map<String, Object> params) {
IPage<CategoryEntity> page = this.page(
new Query<CategoryEntity>().getPage(params),
new QueryWrapper<CategoryEntity>()
);
return new PageUtils(page);
}
@Override
public List<CategoryEntity> listWithTree() {
//1.查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
//2。组装成父子树状结构
//2.1找到所有的一级分类
List<CategoryEntity> level1Menu = entities.stream().filter(categoryEntity ->
categoryEntity.getParentCid() == 0
).map(menu -> {
//1。找到子分类
menu.setChildren(getChildrens(menu, entities));
return menu;
}).sorted((menu1, menu2) ->
//2。排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return level1Menu;
}
@Override
public void removeMenuByIds(List<Long> asList) {
//TODO 1.检查当前删除的菜单,是否在别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
/**
* 递归查找所有菜单的子菜单
*
* @param root
* @param all
* @return
*/
private List<CategoryEntity> getChildrens(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter((categoryEntity) -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
//1。找到子分类
categoryEntity.setChildren(getChildrens(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) ->
//2。排序
(menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort())
).collect(Collectors.toList());
return children;
}
}
- entity
@Data
@TableName("pms_category")
public class CategoryEntity implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 分类id
*/
@TableId
private Long catId;
/**
* 分类名称
*/
private String name;
/**
* 父分类id
*/
private Long parentCid;
/**
* 层级
*/
private Integer catLevel;
/**
* 是否显示[0-不显示,1显示]
*/
@TableLogic(value = "1",delval = "0")
private Integer showStatus;
/**
* 排序
*/
private Integer sort;
/**
* 图标地址
*/
private String icon;
/**
* 计量单位
*/
private String productUnit;
/**
* 商品数量
*/
private Integer productCount;
/**
* 菜单子分类
*/
@TableField(exist = false)
private List<CategoryEntity> children;
}
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)