谷粒商城——商品服务-三级分类
1.商品服务-三级分类
1.1三级分类介绍
1.2查询三级分类查询-递归树型结构数据获取
1.2.1导入数据pms_catelog.sql到数据表pms_category
1.2.2一次性查出所有分类及子分类
1.2.2.1修改CategoryController.java
/*** 查出所有分类以及子分类,以树形结构组装起来*/@RequestMapping("/list/tree")public R list(){List<CategoryEntity> entities = categoryService.listWithTree();return R.ok().put("data", entities);}
1.2.2.2CategoryEntity
新增子分类属性
@TableField(exist = false) //表示数据库表中不存在private List<CategoryEntity> children;
1.2.2.3实现接口CategoryService.java的listwithTree()
@Overridepublic List<CategoryEntity> listWithTree() {//1、查出所有分类。baseMapper来自于继承的ServiceImpl<>类,跟CategoryDao一样用法List<CategoryEntity> entities = baseMapper.selectList(null);//2、递归组装多级分类的树形结构。先过滤得到一级分类,再加工递归设置一级分类的子孙分类,再排序,再收集List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> categoryEntity.getParentCid() == 0).map((menu)->{// 设置一级分类的子分类menu.setChildren(getChildren(menu, entities));return menu;}).sorted((menu1, menu2) -> {//排序,sort是实体类的排序属性,值越小优先级越高,要判断非空防止空指针异常return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList());return level1Menus;}//递归查找所有菜单的子菜单private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all){List<CategoryEntity> children = all.stream().filter(CategoryEntity -> CategoryEntity.getParentCid().equals(root.getCatId())).map(categoryEntity -> {//1、递归查找子菜单categoryEntity.setChildren(getChildren(categoryEntity, all));return categoryEntity;}).sorted((menu1, menu2) -> {//2、菜单排序、判空处理空指针异常return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());}).collect(Collectors.toList());return children;}
1.2.2.4启动product服务测试结果
打开F12访问:localhost:10000/product/category/list/tree
1.3后台页面管理三级分类
1.启动后台管理系统renren-fast
2.启动前端renren-fast-vue;终端输入命令:npm run dev
1.3.1新增目录-商品系统
系统管理中的菜单管理:点击新增按钮
点击确定后刷新页面可见
数据库中可查到
1.3.2新增菜单-分类维护
1.3.3前端展示三级分类
需求:在左侧点击【商品系统-分类维护】,希望在此展示3级分类。可以看到
url是http://localhost:8001/#/product-category
填写的菜单路由是product/category
对应的视图是src/view/modules/product/category.vue (renren-fast-vue文件)
1.3.3.1创建product/category
视图
1.创建
src/views/mudules/product/category.vue
2.在
category.vue中
创建vue模板 (输入vue加回车,可快速生成模板。)3.elementui看如何使用多级目录
Element组件网址
进入Element官网,进入组件,找到Tree树形控件
模仿用法写入vue
<!-- -->
<template>
<el-tree :data="data" :props="defaultProps" @node-click="handleNodeClick"></el-tree>
</template><script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';export default {
//import引入的组件需要注入到对象中才能使用
components: {},
data() {return {data: [],defaultProps: {children: 'children',label: 'label'}};},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概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {//创建完成时,就调用getMenus函数this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>
1.3.3.2测试
localhost:8001/#/product-category
F12刷新页面
发现404请求端口问题:
他是给8080端口发的请求,而我们的商品服务在10000端口。我们以后还会同时发向更多的端口,所以需要配置网关,前端只向网关发送请求,然后由网关自己路由到相应端口。
1.3.3.3查看在哪定义的请求路径
复制 http://localhost:8080/renren-fasthttp://localhost:8080/renren-fast
ctrl + shift + f 全局搜索
1.3.3.4修改请求到网关
请求地址修改为
刷新测试
刷新,发现验证码出不来。
验证码请求路径问题:
分析原因:前端给网关发验证码请求,但是验证码请求在renren-fast服务里,所以要想使验证码好使,需要把renren-fast服务注册到服务中心,并且由网关进行路由
1.3.3.5renren-fast注册到nacos
renren-fast修改pom文件依赖gulimall-common
修改renren-fast配置文件application.yml
spring:application:name: renren-fastcloud:nacos:discovery:server-addr: 127.0.0.1:8848
启动类添加注解 @EnableDiscoveryClient
1.3.3.6配置网关
需求: http://localhost:88/api/xxx 转发--> http://renren-fast:8080/renren-fast/xxx
例如:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/renren-fast/captcha.jpg
修改gulimall-gateway配置文件application.yml
- id: admin_routeuri: lb://renren-fast #负载均衡predicates:-Path=/api/** #断言
##前端项目,/api
## http://localhost:88/api/captcha.jpg
## http://renren-fast:8080/api/captcha.jpg
测试验证发现请求地址为:http://renren-fast:88/api/captcha.jpg
再次修改gulimall-gateway配置文件application.yml,添加下列配置
spring:cloud:nacos:discovery:server-addr: 127.0.0.1:8848gateway:routes:
# 路由id,自定义,只要唯一即可- id: admin_route
# uri路由的目标地址。lb就是负载均衡,后面跟服务名称。uri: lb://renren-fast#断言工厂的Path,请求路径必须符合指定规则predicates:- Path=/api/** # 把所有api开头的请求都转发给renren-fast#局部过滤器。回顾默认过滤器default-filters是与routes同级filters:
#路径重写。逗号左边是原路径,右边是重写后的路径- RewritePath=/api/(?<segment>.*),/renren-fast/$\{segment}# 默认规则, 请求过来:http://localhost:88/api/captcha.jpg 转发--> http://renren-fast:8080/renren-fast/captcha.jpg
刷新测试,出现跨域错误
1.4解决跨域问题
1.4.1跨域和同源策略
跨域:指的是浏览器不能执行其他网站的脚本,它是由浏览器的同源策略造成的,是浏览器对javascript施加的安全限制。
同源策略
:是指协议,域名,端口都要相同,其中有一个不同都会产生跨域问题
1.4.2跨域流程
预检请求:options
1.4.3解决跨域方法1:使用nginx反向代理为同一域
1.4.4解决跨域方法2:配置当前请求允许跨域
CORS:CORS 是一个 W3C 标准,全称是“跨域资源共享”(Cross-origin resource sharing)。它允许浏览器向跨域的服务器,发出XMLHttpRequest请求,从而克服了 AJAX 只能同源使用的限制。
Access-Control-Allow-Origin : 支持哪些来源的请求跨域
Access-Control-Allow-Method : 支持那些方法跨域
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 :表明该响应的有效时间为多少秒。在有效时间内,浏览器无须为同一请求再次发起预检请求。请注意,浏览器自身维护了一个最大有效时间,如果该首部字段的值超过了最大有效时间,将失效。
在网关中统一配置
在gulimall-gateway的gulimall.gateway.config包下中新建配置类
//这个包别导错了,有一个很像的。
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Configuration
public class GulimallCorsConfiguration{@Beanpublic CorsWebFilter corsWebFilter(){UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();CorsConfiguration corsConfiguration= new CorsConfiguration();//1、配置跨域// 允许跨域的请求头corsConfiguration.addAllowedHeader("*");// 允许跨域的请求方式corsConfiguration.addAllowedMethod("*");// 允许跨域的请求来源corsConfiguration.addAllowedOriginPattern("*");
//注释的这句会报错。因为当allowCredentials为真时,allowedorigin不能包含特殊值"*",因为不能在"访问-控制-起源“响应头中设置该值。//corsConfiguration.addAllowedOrigin("*");//这句会报错// 是否允许携带cookie跨域corsConfiguration.setAllowCredentials(true);// 任意url都要进行跨域配置,两个*号就是可以匹配包含0到多个/的路径source.registerCorsConfiguration("/**",corsConfiguration);return new CorsWebFilter(source);}
}
启动测试
发现有多个值错误
注释掉renren-fast中的跨域,不然会有一些重复的规则导致跨域失败:
io.renren/config/CorsConfig
重启测试成功
1.5前端树形展示三级分类数据
1.5.1网关配置product路由
需求:
localhost:88/api/product/xx -> localhost:10000/product/xx
网关路由配置
# 精确的路由要放在/api的admin_route上面- id: product_routeuri: lb://gulimall-product #路由的目标地址predicates: # 路由断言。也就是判断请求是否符合路由规则的条件。- Path=/api/product/** # 路径断言。这个是按照路径匹配,只要以/api/product/开头就符合要求filters: #局部过滤器- RewritePath=/api/(?<segment>.*),/$\{segment} #重写路径,/api/xx过滤成/xx
1.5.2将product服务注册并配置到nacos
1.新建bootstrap.properties
2.nacos新建命名空间
3.修改application.yml注册到注册中心
4.启动类添加注解@EnableDiscoveryClient开启服务注册发现功能
启动测试数据显示如下
1.5.3修改前端组件category.vue
需求:将显示的数据展示到页面上
想要的数据在data.data里,需要将其解构出来
结构代码修改如下
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: {},
data() {return {menus: [],defaultProps: {children: 'children', //子节点label: 'name' //name属性作为标签的值,展示出来}};},methods: {handleNodeClick(data) {console.log(data);},getMenus(){this.$http({url: this.$http.adornUrl('/product/category/list/tree'),method: 'get'}).then(({data})=>{console.log("成功了获取到菜单数据....", data.data)this.menus = data.data;})}},
//监听属性 类似于data概念
computed: {},
//监控data中的数据变化
watch: {},
//生命周期 - 创建完成(可以访问当前this实例)
created() {this.getMenus();
},
//生命周期 - 挂载完成(可以访问DOM元素)
mounted() {},
beforeCreate() {}, //生命周期 - 创建之前
beforeMount() {}, //生命周期 - 挂载之前
beforeUpdate() {}, //生命周期 - 更新之前
updated() {}, //生命周期 - 更新之后
beforeDestroy() {}, //生命周期 - 销毁之前
destroyed() {}, //生命周期 - 销毁完成
activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
}
</script>
<style scoped></style>
启动product测试
http://localhost:10000/product/category/list/tree
1.6逻辑删除三级分类
1.6.1分类的新增和删除
需求:
- 在每一个菜单后面添加append, delete
- 点击按钮时,不进行菜单的打开合并,仅点击箭头时展示子分类:expand-on-click-node="false"
- 当没有子菜单时,才可以显示delete按钮;当为一级、二级菜单时,才显示append按钮。使用v-if判断是否显示
- <el-tree>添加多选框show-checkbox
- 设置node-key=""标识每一个节点的不同
<!-- -->
<template><el-tree:data="menus"show-checkbox:props="defaultProps"@node-click="handleNodeClick":expand-on-click-node="false"node-key="catId"><span class="custom-tree-node" slot-scope="{ node, data }"><span>{{ node.label }}</span><span><el-buttontype="text"v-if="node.level <= 2"size="mini"@click="() => append(data)">Append</el-button><el-buttontype="text"v-if="node.childNodes.length == 0"size="mini"@click="() => remove(node, data)">Delete</el-button></span></span></el-tree>
</template><script>
//这里可以导入其他文件(比如:组件,工具js,第三方插件js,json文件,图片文件等等)
//例如:import 《组件名称》 from '《组件路径》';export default {//import引入的组件需要注入到对象中才能使用components: {},data() {return {menus: [],defaultProps: {children: "children", //子节点label: "name", //name属性作为标签的值,展示出来},};},methods: {handleNodeClick(data) {},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) {console.log("remove", node, data);},},//监听属性 类似于data概念computed: {},//监控data中的数据变化watch: {},//生命周期 - 创建完成(可以访问当前this实例)created() {this.getMenus();},//生命周期 - 挂载完成(可以访问DOM元素)mounted() {},beforeCreate() {}, //生命周期 - 创建之前beforeMount() {}, //生命周期 - 挂载之前beforeUpdate() {}, //生命周期 - 更新之前updated() {}, //生命周期 - 更新之后beforeDestroy() {}, //生命周期 - 销毁之前destroyed() {}, //生命周期 - 销毁完成activated() {}, //如果页面有keep-alive缓存功能,这个函数会触发
};
</script>
<style scoped>
</style>
1.6.2后端:逻辑删除
实体类成员变量加上
@TableLogic(value = "0", delval = "1")
注解
CategoryEntity注解逻辑删除:
/*** 是否显示[0-不显示,1显示]*本项目使用的是category表的show_status字段,逻辑删除值正好相反 *状态为1表示未删除,状态为0表示删除。*/@TableLogic(value = "1",delval = "0")private Integer showStatus;
修改CategoryController.java
/**
删除
@RequestBody:获取请求体,必须发送POST请求
*SpringMvc自动将请求体的数据(json),转为对应的对象
*/
@RequestMapping("/delete")
public R delete(@RequestBody Long[] catIds){
//1、检查当前删除的菜单,是否被别的地方引用
categoryService.removeMenuByIds(Arrays.asList(catId));
return R.ok();
}
实现类CategoryServicelmpl.java实现CategoryService.java接口方法(Ctrl+N快速检索)
@Override
public void removeMenuByIds(List<Long>asList){
//TODO 1、检查当前删除的菜单,是否被别的地方引用
//逻辑删除
baseMapper.deleteBatchIds(asList);
}
修改日志级别
logging:level:com.xmh.guliamll.product: debug
在测试工具发送POST请求
http://localhost:88/api/product/category/delete
查看控制台打印语句,发现是update操作
1.6.3前端:逻辑删除
需求:
- 编写前端remove方法,实现向后端发送请求
- 点击delete弹出提示框,是否删除这个节点: elementui中MessageBox 弹框中的确认消息添加到删除之前
- 删除成功后有消息提示: elementui中Message 消息提示
- 删除后刷新页面后,分类应该保持之前展开状态: el-tree组件的default-expanded-keys属性,默认展开。 每次删除之后,把删除菜单的父菜单的id值赋给默认展开值即可。
1.6.3.1创建请求代码块快捷命令
文件->首选项->用户片段->新建全局代码片段,文件名vue.code-snippets(snippets译为代码片段,片段)
"http-get请求": {"prefix": "httpget","body":["this.\\$http({","url: this.\\$http.adornUrl(''),","method:'get',","params:this.\\$http.adornParams({})","}).then(({data})=>{","})"],"description":"httpGET请求"},"http-post请求":{"prefix":"httppost","body":["this.\\$http({","url:this.\\$http.adornUrl(''),","method:'post',","data: this.\\$http.adornData(data, false)","}).then(({data})=>{ })"],"description":"httpPOST请求"}
//在<el-tree>中设置默认展开属性,绑定给expandedKey
:default-expanded-keys="expandedKey"//data中添加属性,删除后给它赋值父节点id,令树形控件刷新后展开
expandedKey: [],//完整的remove方法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();//设置需要默认展开的菜单。删除后给它赋值父节点id,令树形控件刷新后展开this.expandedKey = [node.parent.data.catId]});}).catch(() => {});},
1.7新增三级分类
1.7.1前端:新增
需求:
- 点击append新增弹出对话框,输入分类名称
- 确定/取消后,关闭对话框
- 确定后发送post请求,成功后刷新前端展示页面
创建对话框组件el-dialog:
对话框标签el-dialog放在el-tree标签上下都是可以的,主要是visible.sync属性控制对话框的显示和隐藏
<!--对话框组件--><el-dialog title="提示" :visible.sync="dialogVisible" width="30%"><el-form :model="categroy"><el-form-item label="分类名称"><el-input v-model="categroy.name" autocomplete="off"></el-input></el-form-item></el-form><span slot="footer" class="dialog-footer"><el-button @click="dialogVisible = false">取 消</el-button><el-button type="primary" @click="addCategory">确 定</el-button></span></el-dialog>
//data中新增数据
//按照数据库格式声明的数据。注意category属性用来接收输入框的参数,需要赋值默认属性,包括父id,层级、展示状态为1,排序值是0categroy: { name: "", parentCid: 0, catLevel: 0, showStatus: 1, sort: 0 },
//判断是否显示对话框dialogVisible: false,//修改append方法,新增addCategory方法
//点击append后,计算category属性,显示对话框append(data) {console.log("append", data);this.dialogVisible = true;this.categroy.parentCid = data.catId;this.categroy.catLevel = data.catLevel * 1 + 1;},//点击确定后,发送post请求
//成功后显示添加成功,展开刚才的菜单addCategory() {console.log("提交的数据", this.categroy);this.$http({url: this.$http.adornUrl("/product/category/save"),method: "post",data: this.$http.adornData(this.categroy, false),}).then(({ data }) => {this.$message({message: "添加成功",type: "success",});//刷新出新的菜单this.getMenus();//设置需要默认展开的菜单this.expandedKey = [this.categroy.parentCid];this.dialogVisible = false;});
1.8修改三级分类
1.8.1后端:修改
后端,修改“回显方法”结果对象的键为"data"
product模块的CategoryController
1.8.2前端:修改
需求:
- 新增Edit按钮:复制之前的append
- updata方法是由id进行更新的,所以
data
中的category
中新增catId
- 增加、修改的同时修改图标和计量单位,所以data的
category
新增inco,productUnit
- 新建
edit
方法,用来绑定Edit按钮。新建editCategory
方法,用来绑定对话框的确定按钮- 复用对话框(新增、修改)
- 新建方法
submitData
,与对话框的确定按钮进行绑定,在方法中判断,如果dialogType==add
调用addCategory(),如果dialogType==edit
调用editCategory()- data数据中新增
title
,绑定对话框的title,用来做提示信息。判断dialogType
的值,动态提示信息- 修改回显必须发请求,而非直接从实参中获取。防止多个人同时操作,对话框中的回显的信息应该是由数据库中读出来的:点击Edit按钮,发送httpget请求。
- 成功之后发送提示消息,展开刚才的菜单
- 编辑之后,再点击添加,发现会回显刚才编辑的信息。所以在
append
方法中重置回显的信息
新增修改共享表单对话框,所以还要修改添加方法,第一步初始化数据
提交修改表单时,不能像新增一样把携带初始化值的category直接提交上去
方法一:局部更新,只解构出表单里的属性封装成对象,再提交(推荐)
方法二:全量更新,回显时,把其他数据库字段也赋值
<!--编辑按钮--><el-button type="text" size="mini" @click="() => edit(data)">Edit</el-button><!--可复用的对话框--><el-dialog :title="title" :visible.sync="dialogVisible" width="30%"><el-form :model="categroy"><el-form-item label="分类名称"><el-input v-model="categroy.name" autocomplete="off"></el-input></el-form-item><el-form-item label="图标"><el-input v-model="categroy.inco" autocomplete="off"></el-input></el-form-item><el-form-item label="计量单位"><el-inputv-model="categroy.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>
//data, 新增了title、dialogType。 categroy中新增了inco、productUnit、catIddata() {return {title: "",dialogType: "",categroy: {name: "",parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,inco: "",productUnit: "",catId: null,},dialogVisible: false,menus: [],expandedKey: [],defaultProps: {children: "children", //子节点label: "name", //name属性作为标签的值,展示出来},};//方法//绑定对话框的确定按钮,根据dialogType判断调用哪个函数submitData() {if (this.dialogType == "add") {this.addCategory();}if (this.dialogType == "edit") {this.editCategory();}},//绑定Edit按钮,设置dialogType、title,从后台读取数据,展示到对话框内edit(data) {this.dialogType = "edit";this.title = "修改菜单";this.dialogVisible = true;this.$http({url: this.$http.adornUrl(`/product/category/info/${data.catId}`),method: "get",}).then(({ data }) => {console.log(data);this.categroy.catId = data.data.catId;this.categroy.name = data.data.name;this.categroy.inco = data.data.inco;this.categroy.productUnit = data.data.productUnit;});},//绑定对话框的确定按钮,向后台发送更新请求,传过去想要修改的字段editCategory() {var { catId, name, inco, productUnit } = this.categroy;this.$http({url: this.$http.adornUrl("/product/category/update"),method: "post",data: this.$http.adornData({ catId, name, inco, productUnit }, false),}).then(({ data }) => {this.$message({message: "修改成功",type: "success",});//刷新出新的菜单this.getMenus();//设置需要默认展开的菜单this.expandedKey = [this.categroy.parentCid];this.dialogVisible = false;});},//点击append按钮,清空编辑之后的回显数据append(data) {this.dialogType = "add";this.title = "添加菜单";console.log("append", data);this.dialogVisible = true;this.categroy.parentCid = data.catId;this.categroy.catLevel = data.catLevel * 1 + 1;this.categroy.name = "",this.categroy.inco = "",this.categroy.productUnit = ""},
1.9修改层级关系,实现拖拽效果
1.9.1前端:拖拽
实现逻辑:
1.在
<el-tree>
中加入属性draggable
表示节点可拖拽2.在<el-tree>中加入属性:allow-drop="allowDrop",拖拽时判定目标节点能否被放置。
3.allowDrop有三个参数draggingNode表示拖拽的节点,dropNode表示拖拽到哪个节点,type表示拖拽的类型’prev’、‘inner’ 和 ‘next’,表示拖拽到目标节点之前、里面、之后
注意:函数实现判断,拖拽后必须保持数型的三层结构。
- 节点的深度 = 最深深度 - 当前深度 + 1
- 当拖拽节点拖拽到目标节点的内部,要满足: 拖拽节点的深度 + 目标节点的深度 <= 3
- 当拖拽节点拖拽的目标节点的两侧,要满足: 拖拽节点的深度 + 目标节点的父节点的深度 <= 3
<!--el-tree中添加属性-->draggable:allow-drop="allowDrop"
// data中新增属性,用来记录当前节点的最大深度
maxLevel: 1,//新增方法allowDrop(draggingNode, dropNode, type) {console.log("allowDrag:", draggingNode, dropNode, type);//节点的最大深度this.countNodeLevel(draggingNode.data);console.log("level:", this.maxLevel);//当前节点的深度let deep = (this.maxLevel - draggingNode.data.catLevel) + 1;console.log(deep)if (type == "inner"){return (deep + dropNode.level) <= 3;}else{return (deep + dropNode.parent.level) <= 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]);}}},
拖拽后的数据收集
- 在<el-tree>中加入属性@node-drop="handleDrop",表示拖拽事件结束后触发事件handleDrop,handleDrop共四个参数,draggingNode:被拖拽节点对应的 Node; dropNode:结束拖拽时最后进入的节点; dropType:被拖拽节点的放置位置(before、after、inner);ev:event
- 拖拽可能影响的节点的数据:parentCid、catLevel、sort
- data中新增updateNodes,把所有要修改的节点都传进来。
- 要修改的数据:拖拽节点的parentCid、catLevel、sort
- 要修改的数据:新的兄弟节点的sort (把新的节点收集起来,然后重新排序)
- 要修改的数据:子节点的catLevel
//el-tree中新增属性,绑定handleDrop,表示拖拽完触发
@node-drop="handleDrop"//data 中新增数据,用来记录需要更新的节点(拖拽的节点(parentCid、catLevel、sort),拖拽后的兄弟节点(sort),拖拽节点的子节点(catLevel))
updateNodes: [],//新增方法handleDrop(draggingNode, dropNode, dropType, ev) {console.log("handleDrop: ", draggingNode, dropNode, dropType);//1、当前节点最新父节点的idlet pCid = 0;//拖拽后的兄弟节点,分两种情况,一种是拖拽到两侧,一种是拖拽到内部let sibings = null;if (dropType == "before" || dropType == "after") {pCid = dropNode.parent.data.catId == undefined ? 0: dropNode.parent.data.catId;sibings = dropNode.parent.childNodes;} else {pCid = dropNode.data.catId;sibings = dropNode.childNodes;}//2、当前拖拽节点的最新顺序//遍历所有的兄弟节点,如果是拖拽节点,传入(catId,sort,parentCid,catLevel),如果是兄弟节点传入(catId,sort)for (let i = 0; i < sibings.length; i++) {if (sibings[i].data.catId == draggingNode.data.catId){//如果遍历的是当前正在拖拽的节点let catLevel = draggingNode.level;if (sibings[i].level != draggingNode.level){//当前节点的层级发生变化catLevel = sibings[i].level;//修改他子节点的层级this.updateChildNodeLevel(sibings[i]);}this.updateNodes.push({catId:sibings[i].data.catId, sort: i, parentCid: pCid, catLevel:catLevel});}else{this.updateNodes.push({catId:sibings[i].data.catId, sort: i});}}//每次拖拽后把数据清空,否则要修改的节点将会越拖越多this.updateNodes = [],this.maxLevel = 1,}// 修改拖拽节点的子节点的层级
updateChildNodeLevel(node){if (node.childNodes.length > 0){for (let i = 0; i < node.childNodes.length; i++){//遍历子节点,传入(catId,catLevel)var cNode = node.childNodes[i].data;this.updateNodes.push({catId:cNode.catId,catLevel:node.childNodes[i].level});//处理子节点的子节点this.updateChildNodeLevel(node.childNodes[i]);}}},
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];});
1.9.2批量拖拽功能
- 添加开关,控制拖拽功能是否开启
- 每次拖拽都要和数据库交互,不合理。批量拖拽过后,一次性保存
<!--添加拖拽开关和批量保存按钮--><el-switchv-model="draggable"active-text="开启拖拽"inactive-text="关闭拖拽"></el-switch><el-button v-if="draggable" size="small" round @click="batchSave">批量保存</el-button>
//data中新增数据pCid:[], //批量保存过后要展开的菜单iddraggable: false, //绑定拖拽开关是否打开//修改了一些方法,修复bug,修改过的方法都贴在下面了//点击批量保存按钮,发送请求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 = [];},//handleDrop(draggingNode, dropNode, dropType, ev) {console.log("handleDrop: ", draggingNode, dropNode, dropType);//1、当前节点最新父节点的idlet pCid = 0;let sibings = null;if (dropType == "before" || dropType == "after") {pCid =dropNode.parent.data.catId == undefined? 0: dropNode.parent.data.catId;sibings = dropNode.parent.childNodes;} else {pCid = dropNode.data.catId;sibings = dropNode.childNodes;}//2、当前拖拽节点的最新顺序for (let i = 0; i < sibings.length; i++) {if (sibings[i].data.catId == draggingNode.data.catId) {//如果遍历的是当前正在拖拽的节点let catLevel = draggingNode.level;if (sibings[i].level != draggingNode.level) {//当前节点的层级发生变化catLevel = sibings[i].level;//修改他子节点的层级this.updateChildNodeLevel(sibings[i]);}this.updateNodes.push({catId: sibings[i].data.catId,sort: i,parentCid: pCid,catLevel: catLevel,});} else {this.updateNodes.push({ catId: sibings[i].data.catId, sort: i });}}this.pCid.push(pCid);console.log(this.pCid)//3、当前拖拽节点的最新层级//console.log("updateNodes", this.updateNodes)//拖拽之后重新置1this.maxLevel = 1;},// 修改拖拽判断逻辑allowDrop(draggingNode, dropNode, type) {console.log("allowDrag:", draggingNode, dropNode, type);this.maxLevel = draggingNode.level;//节点的最大深度this.countNodeLevel(draggingNode);console.log("maxLevel:", this.maxLevel);//当前节点的深度let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;console.log("level",deep);if (type == "inner") {return deep + dropNode.level <= 3;} else {return deep + dropNode.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.maxLevel) {this.maxLevel = node.childNodes[i].level;}this.countNodeLevel(node.childNodes[i]);}}},
1.9.3后端:拖拽
思路:
- 在后端编写批量修改的方法
update/sort
- 前端发送post请求,把要修改的数据发送过来
- 提示信息,展开拖拽节点的父节点
CategoryController批量修改功能
//批量修改,参数要传数组,不能传list@RequestMapping("/update/sort")public R updateSort(@RequestBody CategoryEntity[] category){categoryService.updateBatchById(Arrays.asList(category));return R.ok();}
测试批量修改功能
http://localhost:88/api/product/category/update/sort
1.10批量删除分类
1.10.1后端:批量删除
1.10.2前端:批量删除
1.新增删除按钮
<el-button type="danger" size="small" @click="batchDelete" round>批量删除</el-button><!--eltree中新增属性,用作组件的唯一标示-->
ref="menuTree"
2.批量删除方法
batchDelete(){let catIds = [];let catNames = [];let checkedNodes = this.$refs.menuTree.getCheckedNodes();for (let i = 0; i < checkedNodes.length; i++){catIds.push(checkedNodes[i].catId);catNames.push(checkedNodes[i].name);}this.$confirm(`是否批量删除【${catNames}】菜单?`, "提示", {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(()=>{});},
1.11前端category.vue最终代码
<template><div><el-switch v-model="draggable" active-text="开启拖拽" inactive-text="关闭拖拽"></el-switch><el-button v-if="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-checkboxnode-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-buttonv-if="node.level <=2"type="text"size="mini"@click="() => append(data)">新增</el-button><el-button type="text" size="mini" @click="edit(data)">编辑</el-button><el-buttonv-if="node.childNodes.length==0"type="text"size="mini"@click="() => remove(node, data)">删除</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: [],maxLevel: 0,title: "",dialogType: "", //edit,addcategory: {name: "",parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,productUnit: "",icon: "",catId: null},dialogVisible: false,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;});},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(() => {});},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;});},handleDrop(draggingNode, dropNode, dropType, ev) {console.log("handleDrop: ", draggingNode, dropNode, dropType);//1、当前节点最新的父节点idlet 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、当前拖拽节点的最新顺序,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]);}this.updateNodes.push({catId: siblings[i].data.catId,sort: i,parentCid: pCid,catLevel: catLevel});} else {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//1)、被拖动的当前节点总层数console.log("allowDrop:", draggingNode, dropNode, type);//this.countNodeLevel(draggingNode);//当前正在拖动的节点+父节点所在的深度不大于3即可let deep = Math.abs(this.maxLevel - draggingNode.level) + 1;console.log("深度:", deep);// this.maxLevelif (type == "inner") {// console.log(// `this.maxLevel:${this.maxLevel};draggingNode.data.catLevel:${draggingNode.data.catLevel};dropNode.level:${dropNode.level}`// );return deep + dropNode.level <= 3;} else {return deep + dropNode.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.maxLevel) {this.maxLevel = node.childNodes[i].level;}this.countNodeLevel(node.childNodes[i]);}}},edit(data) {console.log("要修改的数据", data);this.dialogType = "edit";this.title = "修改分类";this.dialogVisible = true;//发送请求获取当前节点最新的数据this.$http({url: this.$http.adornUrl(`/product/category/info/${data.catId}`),method: "get"}).then(({ data }) => {//请求成功console.log("要回显的数据", data);this.category.name = data.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;this.category.catLevel = data.data.catLevel;this.category.sort = data.data.sort;this.category.showStatus = data.data.showStatus;/** * parentCid: 0,catLevel: 0,showStatus: 1,sort: 0,*/});},append(data) {console.log("append", data);this.dialogType = "add";this.title = "添加分类";this.dialogVisible = true;this.category.parentCid = data.catId;this.category.catLevel = data.catLevel * 1 + 1;this.category.catId = null;this.category.name = "";this.category.icon = "";this.category.productUnit = "";this.category.sort = 0;this.category.showStatus = 1;},submitData() {if (this.dialogType == "add") {this.addCategory();}if (this.dialogType == "edit") {this.editCategory();}},//提交修改三级分类数据editCategory() {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({message: "菜单修改成功",type: "success"});//关闭对话框this.dialogVisible = false;//刷新出新的菜单this.getMenus();//设置需要默认展开的菜单this.expandedKey = [this.category.parentCid];});},//提交添加三级分类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];});},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(() => {});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.商品服务-三级分类 1.1三级分类介绍 1.2查询三级分类查询-递归树型结构数据获取 1.2.1导入数据pms_catelog.sql到数据表pms_category 1.2.2一次性查出所有分类及子分类 1.2.2.1修改CategoryController.java /*** 查出所有分类以及子分类,以树形结构组装起来*/R…...

视觉语言模型 (VLMs):跨模态智能的探索
文章目录 一. VLMs 的重要性与挑战:连接视觉与语言的桥梁 🌉二. VLMs 的核心训练范式:四种主流策略 🗺️1. 对比训练 (Contrastive Training):拉近正例,推远负例 ⚖️2. 掩码方法 (Masking):重构…...

HarmonyOS NEXT:华为分享-碰一碰开发分享
随着科技的不断进步,智能手机和智能设备之间的互联互通变得越来越重要。华为作为科技行业的领军企业,一直致力于为用户提供更加便捷、高效的使用体验。HarmonyOS NEXT系统的推出,特别是其中的“碰一碰”功能,为用户带来了前所未有…...

宝塔Linux+docker部署nginx出现403 Forbidden
本文主要讲述了宝塔docker部署nginx出现403 Forbidden的原因,以及成功部署前端的方法步骤。 目录 1、问题描述2、问题检测2.1 检测监听端口是否异常2.2 检测Docker容器是否异常2.2.1 打开宝塔Linux的软件商店,找到Docker管理器,查看前端容器是…...

软件测试丨Redis 的数据同步策略以及数据一致性保证
Redis 以其键值存储的方式,为开发者提供了数据快速存取的能力。它不仅支持丰富的数据结构,如字符串、哈希、列表、集合等,而且提供了高效的数据同步与一致性保障机制。正因为如此,Redis 被广泛应用于缓存、消息队列、实时数据分析…...

C语言-运算符
1. 按位与运算符(&) 按位与运算符对两个整数的每一位执行“与”操作。只有当两个相应位都为 1 时,结果才为 1 ;否则为 0。 // 示例 int a 5; // 二进制: 0101 int b 3; // 二进制: 0011 int result a & b; …...

困境如雾路难寻,心若清明步自轻---2024年创作回顾
文章目录 前言博客创作回顾第一次被催更第一次获得证书周榜几篇博客互动最多的最满意的引发思考的 写博契机 碎碎念时也运也部分经验 尾 前言 今年三月份,我已写下一篇《近一年多个人总结》,当时还没开始写博客。四月份写博后,就顺手将那篇总…...

表格标签基本使用
表格主要用于显示、展示数据,因为它可以让数据显示的非常的规整,可读性非常好。特别是后台展示数据的时候,能够熟练运用表格就显得很重要。一个清爽简约的表格能够把繁杂的教据表现得很有条理。 1.<table></table>是用于定义表格…...

【学术会议论文投稿】深度解码:机器学习与深度学习的界限与交融
目录 一、定义与起源:历史长河中的两条轨迹 二、原理差异:从浅层到深层的跨越 三、代码解析:实战中的机器学习与深度学习 机器学习示例:线性回归 深度学习示例:卷积神经网络(CNN) 四、应用差异:各自领…...

使用printmap()函数来打印地图
使用PrintMap()函数可以将地图布局发送到打印机.默认情况下,任务会发送到地图文档保存的默认打印机,但也可以通过自定义一个特定的打印机来执行打印任务 操作方法 1.打开目标地图 2.打开python窗口 3.导入arcpy.mapping模块 import arcpy.mapping as mapping 4.引用活动地…...

MyBatis Plus 的 InnerInterceptor:更轻量级的 SQL 拦截器
在 Spring Boot 项目中使用 MyBatis Plus 时,你可能会遇到 InnerInterceptor 这个概念。 InnerInterceptor 是 MyBatis Plus 提供的一种轻量级 SQL 拦截器,它与传统的 MyBatis 拦截器(Interceptor)有所不同,具有更简单…...

Java复习第四天
一、代码题 1.相同的树 (1)题目 给你两棵二叉树的根节点p和q,编写一个函数来检验这两棵树是否相同。 如果两个树在结构上相同,并且节点具有相同的值,则认为它们是相同的。 示例 1: 输入:p[1,2,3],q[1,2,3] 输出:true示例 2: 输…...

docker 安装 mysql 详解
在平常的开发工作中,我们经常需要用到 mysql 数据库。那么在docker容器中,应该怎么安装mysql数据库呢。简单来说,第一步:拉取镜像;第二步:创建挂载目录并设置 my.conf;第三步:启动容…...

本地Ubuntu轻松部署高效性能监控平台SigNoz与远程使用教程
文章目录 前言1.关于SigNoz2.本地部署SigNoz3.SigNoz简单使用4. 安装内网穿透5.配置SigNoz公网地址6. 配置固定公网地址 前言 本文介绍如何在Ubuntu系统上使用 Docker 快速部署一款强大的应用性能监控工具SigNoz,并结合cpolar内网穿透工具轻松实现异地远程使用。 …...

防火墙的会话并发数、端口数量及其关系
防火墙的会话并发数、端口数量及其关系: 会话并发数:会话并发数,也称为并发连接数,是指防火墙能够同时处理的点对点连接的最大数目。这个参数直接影响到防火墙在高流量环境下的表现,特别是对于需要处理大量并发…...

随机变量的变量替换——归一化流和直方图规定化的数学基础
变量替换是一种在统计学和数学中广泛应用的技术,它通过定义新的变量来简化问题,使得原本复杂的随机变量变得更加容易分析。 变量替换的公式,用于将一个随机变量 X X X 的概率密度函数 f X f_X fX 转换为其经过函数 g g g 变换后的随机变…...

Java春招面试指南前言
在当今竞争激烈的就业市场中,对于即将踏入职场的Java开发者而言,春招是一次宝贵的机会。本博客专栏旨在为大家提供一份全面且实用的Java春招面试指南,助力大家顺利通过面试,开启职业生涯的新篇章。 无论你是初出茅庐的应届生&…...

【技术洞察】2024科技绘卷:浪潮、突破、未来
涌动与突破 2024年,科技的浪潮汹涌澎湃,人工智能、量子计算、脑机接口等前沿技术如同璀璨星辰,方便了大家的日常生活,也照亮了人类未来的道路。这一年,科技的突破与创新不断刷新着人们对未来的想象。那么回顾2024年的科…...

为AI聊天工具添加一个知识系统 之54 为事务处理 设计 基于DDD的一个 AI操作系统 来处理维度
本文要点 要点 Architecture程序 它被设计为一个双面神结构的控制器,它的两侧一侧编译执行另一侧 解释执行,自已则是一个 翻译器--通过提供两个不同取向之间 的 结构映射的显示器(带 图形用户接口GUI和命令行接口CLI 两种 接口)…...

【数据结构】二分查找
🚩 WRITE IN FRONT 🚩 🔎 介绍:"謓泽"正在路上朝着"攻城狮"方向"前进四" 🔎🏅 荣誉:2021|2022年度博客之星物联网与嵌入式开发TOP5|TOP4、2021|2222年获评…...

读书笔记《网络是怎样连接的》
目录 第一章1.1 生成http请求消息输入网址URL解析URLURL中省略文件名的情况http的基本思路生成HTTP请求消息发送请求后收到响应 1.2 向DNS服务器查询Web服务器的IP地址IP地址的基本知识域名和IP地址并用的理由Socket库提供查询IP地址的功能通过解析器向 DNS 服务器发出查询解析…...

Java 设计模式一
Java 设计模式是软件开发中的一类解决方案,旨在解决常见的设计问题,提升代码的可维护性、可复用性和扩展性。它们通常基于一些经验和最佳实践,提供了解决问题的标准化方法。以下是常见的 Java 设计模式及其概述: 1. 创建型模式 (…...

SOME/IP服务接口
本系列文章将分享我在学习 SOME/IP 过程中积累的一些感悟,并结合 SOME/IP 的理论知识进行讲解。主要内容是对相关知识的梳理,并结合实际代码展示 SOME/IP 的使用,旨在自我复习并与大家交流。文中引用了一些例图,但由于未能找到原作…...

Java 生成 PDF 文档 如此简单
嘿,朋友!在 Java 里实现 PDF 文档生成那可真是个挺有意思的事儿,今儿个就来好好唠唠这个。咱有不少好用的库可以选择,下面就给你详细讲讲其中两个超实用的库,一个是 iText,另一个是 Apache PDFBox。 用 iTe…...

深入探究 YOLOv5:从优势到模型导出全方位解析
一、引言 在计算机视觉领域,目标检测是一项至关重要的任务,它在自动驾驶、安防监控、工业检测等众多领域都有着广泛的应用。而 YOLO(You Only Look Once)系列作为目标检测算法中的佼佼者,一直备受关注。其中ÿ…...

【PoCL】运行 LLVM 中 pass 优化过程详解
PoCL 项目中调用 LLVM 的 Pass 对编译过程的优化至关重要。本博文以PoCL 开源项目源码为例,详细说明【PoCL】运行 LLVM 中 pass 优化过程 目录 0. 个人简介 && 授权须知1. pocl_llvm_run_pocl_passes 函数作用2. 禁止 “小网格 small grid” 工作组(workGroup)特化的…...

如何将使用unsloth微调的模型部署到ollama?
目录 一、将模型保存为gguf格式 二、下载llama.cpp 三、生成 llama-quantize 可执行文件 四、使用llama-quantize 五、训练模型 六、将模型部署到ollama 一、将模型保存为gguf格式 在你的训练代码 trainer.train() 之后添加: model.save_pretrained_gguf(&q…...

【测试】UI自动化测试
长期更新,建议关注收藏点赞! 目录 概论WEB环境搭建Selenium APPAppium 概论 使用工具和代码执行用例。 什么样的项目需要自动化? 需要回归测试、自动化的功能模块需求变更不频繁、项目周期长(功能测试时长:UI自动化测…...

SSM开发(二) MyBatis两种SQL配置方式及其对比
目录 一、MyBatis两种SQL配置方式 二、使用XML映射文件配置SQL语句 三、使用注解配置SQL语句 四、两种方式对比 总结 1、注解 2、XML配置 五、MyBatis多数据源的两种配置方式 参考 一、MyBatis两种SQL配置方式 MyBatis 提供了两种方式来配置SQL语句:注解&a…...

【Redis】在ubuntu上安装Redis
文章目录 提权搜索软件包安装修改配置文件ip保护模式配置密码 重新启动服务器使用 redis 自带的客户端来连接服务器 提权 先切换到 root 用户,su 命令切换到 root. 搜索软件包 使用 apt 命令来搜索 redis 相关的软件包 apt search redis 安装 使用 apt 命令安装 redisapt …...