028-044 前端基础
讲了一些前端开发的基础知识,不看也不太影响项目开发。我选择了跳过。
045 三级分类-查询-递归树形结构数据获取
1 分类数据初始化
在数据库gulimall_pms中执行脚本pms_catelog.sql。
百度网盘:提取码2239
2 组装树形结构数据
项目:gulimall-product
Controller层 -?CategoryController类:
/**
* 查出所有分类,以树形结构组装起来
* @return
*/
@RequestMapping("/list/tree")
public R queryListTree() {
List<CategoryEntity> entities = categoryService.queryListTree();
return R.ok().put("data", entities);
}
Service层 -?CategoryService接口:
List<CategoryEntity> queryListTree();
Service层 -?CategoryServiceImpl类:
@Override
public List<CategoryEntity> queryListTree() {
// 1 查出所有分类
List<CategoryEntity> entities = baseMapper.selectList(null);
// 2 组装成树形结构
List<CategoryEntity> level1Menus = entities.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == 0;
}).map(menu -> {
menu.setChildren(getChildren(menu, entities));
return menu;
}).sorted((menu1, menu2) -> {
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return entities;
}
/**
* 递归查找所有分类的子分类
*
* @param root
* @param all
* @return
*/
private List<CategoryEntity> getChildren(CategoryEntity root, List<CategoryEntity> all) {
List<CategoryEntity> children = all.stream().filter(categoryEntity -> {
return categoryEntity.getParentCid() == root.getCatId();
}).map(categoryEntity -> {
categoryEntity.setChildren(getChildren(categoryEntity, all));
return categoryEntity;
}).sorted((menu1, menu2) -> {
return (menu1.getSort() == null ? 0 : menu1.getSort()) - (menu2.getSort() == null ? 0 : menu2.getSort());
}).collect(Collectors.toList());
return children;
}
Entity层 -?CategoryEntity类:
/**
* 子分类
*/
@TableField(exist = false)
private List<CategoryEntity> children;
3 测试
启动gulimall-gateway服务和gulimall-product服务。访问
http://localhost:12000/product/category/list/tree
发现页面返回了分类数据:
并且分类数据通过children属性组装成了树形结构:
4 使用Postman进行测试
Postman官网
使用postman,可以模拟发送http请求。很好理解。
045-048?三级分类-分类数据展示
启动前端项目renren-fast-vue,登录后台管理系统。
使用VS?Code打开前端项目renren-fast-vue,在终端执行命令:npm run dev
访问地址:http://localhost:8001
用户名/密码:admin/admin
1 添加分类维护页面
打开系统管理-菜单管理页面,新增目录:商品系统。
?然后新增菜单:分类维护。
刷新网页,左侧菜单栏出现 商品系统/分类维护。
2?获取分类数据并展示到页面
在上一节中创建的分类维护菜单的路由是product/category。
在renren-fast-vue项目的src/views/modules/路径下创建目录product,以后将商品服务的所有.vue文件都放在这个目录下。在product目录下创建category.vue,对应分类维护菜单的路由。
.vue文件的基本结构是:
<template>
</template>
<script>
export default {
}
</script>
<style>
</style>
我们现在要做的是,在进入分类维护页面时,获取分类数据并展示到页面上:
<template>?</template>
<script>
export default {
methods: {
getMenus() {
this.$http({
url: this.$http.adornUrl("/product/category/list/tree"),
method: "get",
}).then((data) => {
console.log("成功获取到分类数据:", data);
});
},
},
created() {
this.getMenus();
},
};
</script>
<style>
</style>
刷新页面,发现浏览器发送的请求url:?http://localhost:8080/renren-fast/product/category/list/tree报404:
这是因为gulimall-product服务的端口号是12000,正确的请求url应该是:
http://localhost:12000/product/category/list/tree
解决方法是,我们将所有的http请求都发送给网关,由网关路由根据请求的服务路由到不同的地址。
修改static/config/index.js中的api接口请求地址为:'http://localhost:88/api'
修改完成后,”人人快速开发平台”会自动退回到登录页面,并且验证码显示异常。这是因为原来发送给renren-fast服务获取验证码的请求现在被发送给了网关。
2.1 网关的默认路由规则
我们默认将发送到网关的所有请求路由到renren-fast服务(8080端口)。
将renren-fast服务注册到nacos注册中心。
renren-fast不再(像视频中那样)依赖gulimall-common,向renren-fast的pom.xml文件中添加依赖:
<!-- Nacos:服务注册/发现 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-loadbalancer</artifactId>
<version>3.0.3</version>
</dependency>
<!-- Nacos:配置管理 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2021.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
<version>3.0.3</version>
</dependency>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2021.1</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
在配置文件src/main/resourses/application.yml中添加配置:
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
spring.application.name=renren-fast
给src/main/java/io/renren/RenrenApplication类添加类注解:
@EnableDiscoverClient
重启renren-fast服务。(在这里我遇到了一个spring?boot的版本问题,改一下pom.xml里面spirng?boot的版本号就好了)
配置网关:gulimall-gateway/src/main/resources/application.yml文件
spring.cloud.gateway.routes:
- id: admin_route
uri: lb://renren-fast #lb:负载均衡
predicates:
- Path=/api/** #前端项目发送的请求带有/api 前缀
filters:
- RewritePath=/api/?(?<segment>.*), /renren-fast/$\{segment} #将/api 前缀改写成/renren-fast
重启gulimall-gateway服务。
重新打开前端页面:http://localhost:8001
验证码正常显示,但登录仍然失败。这是由于发生了跨域问题。
我做到这个地方的时候,验证码一直出不来,后来听弹幕大神的,将renren-fast/src/main/java/io/renren/config/CorsConfig.java中的.allowedOrigins("*")改成了.allowedOriginPatterns("*"),重启renren-fast服务后,验证码终于显示成功。
?2.2 跨域问题
跨域指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器对JavaScript施加的安全限制。
同源策略是指协议、域名、端口号都要相同。
跨源资源共享(CORS)详解
非简单请求的跨域处理流程:
解决方案:进行配置,允许本次请求跨域。
在gulimall-gateway中创建目录src/main/java/com/atguigu/gulimall/gateway/config/,在该路径下创建CorsConfiguration类:
package com.atguigu.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;
@Configuration
public class GulimallCorsConfiguration {
@Bean
public CorsWebFilter corsWebFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.addAllowedOriginPattern("*");
// 允许携带cookie进行跨域
corsConfiguration.setAllowCredentials(true);
source.registerCorsConfiguration("/**", corsConfiguration);
return new CorsWebFilter(source);
}
}
重启renren-fast服务和gulimall-gateway服务。
重新打开前端页面:http://localhost:8001
再次尝试登录。
进行到这里,发现预检请求响应正常,真实请求响应仍然失败。
这是因为在脚手架工程renren-fast中已经配置了允许跨域。
解决方法是将renren-fast中src/main/java/io/renren/config/CorsConfig类中的addCorsMappings方法注释掉。
重启renren-fast服务,再次尝试登录。
2.3 发送给product服务的请求的路由规则
在gulimall-gateway项目的配置文件中配置gulimall-product服务的路由规则:
spring.cloud.gateway.routes:
- id: product_route
uri: lb://gulimall-product
predicates:
- Path=/api/product/**
filters:
- RewritePath=/api/?(?<segment>.*), /$\{segment} #去掉/api 前缀
这里注意要把名为product_route的路由规则放到admin_route规则之前,因为满足product_route规则的请求全都满足admin_route规则。product_route规则放到后面会被覆盖掉。
重启gulimall-gateway服务。进入商品系统-分类维护页面。发现页面成功返回了分类数据。
?3 使用ElementUI快速开发前端
使用npm安装ElementUI:在终端执行命令
npm i element-ui
安装完成后,可以在ElementUI官网选择想要的组件拷贝到自己的前端项目中。
这里我选择使用Tree树形控件。
编辑前端文件category.vue:
<template>
? <el-tree
? ? :data="menus"
? ? :props="defaultProps"
? ? :expand-on-click-node="false"
? ? show-checkbox
? ? node-key="catId"
? >
? ? <span class="custom-tree-node" slot-scope="{ node, data }">
? ? ? <span>{{ node.label }}</span>
? ? ? <span>
? ? ? ? <el-button
? ? ? ? ? v-if="node.level <= 2"
? ? ? ? ? type="text"
? ? ? ? ? size="mini"
? ? ? ? ? @click="() => append(data)"
? ? ? ? >
? ? ? ? ? Append
? ? ? ? </el-button>
? ? ? ? <el-button
? ? ? ? ? v-if="node.childNodes.length == 0"
? ? ? ? ? type="text"
? ? ? ? ? size="mini"
? ? ? ? ? @click="() => remove(node, data)"
? ? ? ? >
? ? ? ? ? Delete
? ? ? ? </el-button>
? ? ? </span>
? ? </span>
? </el-tree>
</template>
<script>
/* eslint-disable */
export default {
? data() {
? ? return {
? ? ? menus: [],
? ? ? defaultProps: {
? ? ? ? children: "children",
? ? ? ? label: "name",
? ? ? },
? ? };
? },
? methods: {
? ? getMenus() {
? ? ? this.dataListLoading = true;
? ? ? 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);
? ? },
? },
? created() {
? ? this.getMenus();
? },
};
</script>
<style>
</style>
页面展示效果:
049-051?三级分类-删除
删除分类调用的方法:
1 使用postman测试删除
想要删除cat_id=1432的数据:
2?配置mybatis-plus逻辑删除
2.1 全局配置
2.2?标识字段配置
(如果@TableLogic注解后面有括号,就用括号里的配置,否则使用全局配置)
重启gulimall-product服务。?
2.3 测试逻辑删除
想要删除cat_id=1431的数据:
删除结果:
3 页面删除
3.1 http请求模板
因为发送http请求的代码以后会多次使用,所以把它抽取成模板。
文件-首选项-用户片段,新建文件vue.code-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请求"
? ? },
保存。以后在项目中输入“httpget”或“httppost”即可输出请求模板。
3.2 发送delete请求
在前端文件category.vue中补充remove方法:
3.3 页面优化
此时在前端页面上点击删除某个分类,已经能够成功删除。但是删除功能仍然对用户很不友好:
- 删除分类前应弹框询问用户是否确定删除。
- 删除成功后,页面应提示删除成功。
- 删除完成后,已展开的分类不应该折叠起来。
使用ElementUI提供的模板MessageBox弹框、Message消息提示、Tree树形控件中的default-expanded-keys属性,可以分别解决上面的三个问题。
4 完整代码
<template>
? <el-tree
? ? :data="menus"
? ? :props="defaultProps"
? ? :expand-on-click-node="false"
? ? show-checkbox
? ? node-key="catId"
? ? :default-expanded-keys="expandKey"
? >
? ? <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>
/* eslint-disable */
export default {
? data() {
? ? return {
? ? ? menus: [],
? ? ? expandKey: [],
? ? ? defaultProps: {
? ? ? ? children: "children",
? ? ? ? label: "name",
? ? ? },
? ? };
? },
? methods: {
? ? getMenus() {
? ? ? this.dataListLoading = true;
? ? ? 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.expandKey = [node.parent.data.catId];
? ? ? ? });
? ? ? });
? ? ? // console.log("remove", node, data);
? ? },
? },
? created() {
? ? this.getMenus();
? },
};
</script>
<style>
</style>
052 三级分类-添加
添加操作:点击append,弹出一个对话框,在对话框编辑新增分类,点确定后将新增分类添加到数据库。
添加分类调用的方法:
post请求url:http://localhost:88/api/product/category/save
1 设置主键自增
如果一开始没有设置主键自增,主键会是一个很长的随机值,即使后面设置了主键自增,主键也会从当前随机值开始计数。解决方法是在数据库中执行sql语句:
ALTER TABLE #table_name# AUTO_INCREMENT=#number#;
2 页面添加
使用ElementUI提供的Dialog对话框可实现弹窗效果。
代码见 058 三级分类-完整前端代码。
053-057 三级分类-修改
修改分类调用的方法:(注意此处有代码改动)
1 页面修改
复用页面添加弹出框可完成修改操作。
代码见 058 三级分类-完整前端代码。
2 拖拽节点改变层级关系
是比较复杂的前端业务,使用ElementUI提供的可拖拽节点组件可实现效果。
不实现了。交给前端同学。
3 批量删除
不实现了。实现后的页面太丑了,又不会设计UI。
058 三级分类-完整前端代码
<template>
<div>
<el-tree
:data="menus"
:props="defaultProps"
:expand-on-click-node="false"
node-key="catId"
:default-expanded-keys="expandKey"
draggable
:allow-drop="allowDrop"
>
<span class="custom-tree-node" slot-scope="{ node, data }">
<span>{{ node.label }}</span>
<span>
<el-button type="text" size="mini" @click="() => edit(data)">
    编辑
</el-button>
<el-button
v-if="node.level <= 2"
type="text"
size="mini"
@click="() => append(data)"
>
添加
</el-button>
<el-button
v-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%">
<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>
/* eslint-disable */
export default {
data() {
return {
menus: [],
expandKey: [],
dialogVisible: false,
category: {
catId: null,
name: "",
parentCid: 0,
catLevel: 0,
showStatus: 1,
sort: 0,
icon: "",
productUnit: "",
},
dialogType: "", // add,edit
title: "",
defaultProps: {
children: "children",
label: "name",
},
};
},
methods: {
getMenus() {
//this.dataListLoading = true;
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.dialogType = "add";
this.title = "添加分类";
this.dialogVisible = true;
this.category.catId = null;
this.category.name = "";
this.category.parentCid = data.catId;
this.category.catLevel = data.catLevel * 1 + 1;
this.category.showStatus = 1;
this.category.sort = 0;
this.category.icon = "";
this.category.productUnit = "";
},
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.expandKey = [this.category.parentCid];
});
},
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.catId = data.data.catId;
this.category.name = data.data.name;
this.category.parentCid = data.data.parentCid;
//this.category.catLevel = dada.data.catLevel;
//this.category.showStatus = data.data.showStatus;
//this.category.sort = data.data.sort;
this.category.icon = data.data.icon;
this.category.productUnit = data.data.productUnit;
});
},
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.expandKey = [this.category.parentCid];
});
},
submitData() {
if (this.dialogType == "add") {
this.addCategory();
}
if (this.dialogType == "edit") {
this.editCategory();
}
},
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.expandKey = [node.parent.data.catId];
});
});
console.log("remove", node, data);
},
allowDrop(draggingNode, dropNode, type) {
return false;
},
},
created() {
this.getMenus();
},
};
</script>
<style>
</style>
|