Note
这只是一篇学习笔记而非技术向博客
Background
- 学校没有教过Java
- 这学期选了一门课要求使用Spring框架做一个web
- 然而老师到现在都还在那讲Bean
- 而且是一点前端都不教啊
- 后端现在还好随便写写,前端只能说一窍不通
- 所以最近在学习vue和springboot,权且应付期末的项目检查
- 学习的视频
Development Tools
-
Frontend
- WebStorm 2022.1
- vue 2.6.14
- element ui 2.15.10
- vue router 3.5.1
- 大概率还会使用axios,我暂且蒙在鼓里
-
Backend
- IDEA 2022.2.3
- spring boot 2.7.5
- mybatis plus 3.5.2
- maven 3.8.6
- mysql 8.0.30
Process
前端整体布局
直接复制element ui的布局容器:
<el-container style="height: 500px; border: 1px solid #eee">
<el-aside width="200px" style="background-color: rgb(238, 241, 246)">
<el-menu :default-openeds="['1', '3']">
<el-submenu index="1">
<template slot="title"><i class="el-icon-message"></i>导航一</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="1-1">选项1</el-menu-item>
<el-menu-item index="1-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="1-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="1-4">
<template slot="title">选项4</template>
<el-menu-item index="1-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="2">
<template slot="title"><i class="el-icon-menu"></i>导航二</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="2-1">选项1</el-menu-item>
<el-menu-item index="2-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="2-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="2-4">
<template slot="title">选项4</template>
<el-menu-item index="2-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
<el-submenu index="3">
<template slot="title"><i class="el-icon-setting"></i>导航三</template>
<el-menu-item-group>
<template slot="title">分组一</template>
<el-menu-item index="3-1">选项1</el-menu-item>
<el-menu-item index="3-2">选项2</el-menu-item>
</el-menu-item-group>
<el-menu-item-group title="分组2">
<el-menu-item index="3-3">选项3</el-menu-item>
</el-menu-item-group>
<el-submenu index="3-4">
<template slot="title">选项4</template>
<el-menu-item index="3-4-1">选项4-1</el-menu-item>
</el-submenu>
</el-submenu>
</el-menu>
</el-aside>
<el-container>
<el-header style="text-align: right; font-size: 12px">
<el-dropdown>
<i class="el-icon-setting" style="margin-right: 15px"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<span>王小虎</span>
</el-header>
<el-main>
<el-table :data="tableData">
<el-table-column prop="date" label="日期" width="140">
</el-table-column>
<el-table-column prop="name" label="姓名" width="120">
</el-table-column>
<el-table-column prop="address" label="地址">
</el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
为了页面美观,最好将一些标签的style设置为height: 100%
如在el-menu中,添加style="height: 100%";可以将菜单栏完全铺平界面
el-menu
<el-menu :default-openeds="[]" style="height: 100%; overflow-x: hidden"
background-color="rgb(48,65,86)"
text-color="#fff"
active-text-color="#409EFF"
:collapse-transition="false"
:collapse="isCollapse"
>
:default-openeds表示初始跳转该界面时,指定左边打开的导航栏;我将其设置为空overflow-x: hidden:导航栏在伸缩时可以将溢出的边框hidden掉background-colortext-color还行active-text-color:点击该导航栏时文本的颜色:collapse-transition="false":是否启动过度动画;自己测了测设置成true的话文本的收缩不够及时,还是false快一点:collapse="isCollapse":启动碰撞效果- 接下来就是编辑一个折叠按钮,通过点击这个按钮来实现伸缩折叠
<div style="flex: 1;font-size: 25px">
<span :class="collapseBtnClass" style="cursor: pointer" @click="collapse"></span>
</div>
- 定义了一个collapseBtnClass的类和collapse方法
- 将collapseBtnClass定义为icon
- 点击这个icon后调用collapse方法
- 可以使用bool类型的自反简单实现折叠
return {
tableData: Array(10).fill(item),
collapseBtnClass:"el-icon-s-fold",
isCollapse:false,
sideWidth:200,
logoTextShow:true,
}
methods:{
collapse(){ //点击收缩按钮触发
this.isCollapse = !this.isCollapse;
if(this.isCollapse){
this.sideWidth = 64 ;
this.collapseBtnClass = 'el-icon-s-unfold';
this.logoTextShow=false;
}else {
this.sideWidth = 200;
this.collapseBtnClass = 'el-icon-s-fold';
this.logoTextShow=true;
}
}
}
- 折叠后需要修改width的长度,不妨直接将其设置为一个
sideWidth变量直接在方法中控制 - 关于这个长度要自己去f12康康各个布局有多长
<el-aside :width="sideWidth + 'px'" style="background-color: rgb(238, 241, 246);height: 100%">
折叠伸缩导航文本
<div style="height: 60px; line-height: 60px; text-align: center">
<img src="../assets/logo.png" alt="" style="width: 20px; position: relative;top: 5px;margin-right: 5px">
<b style="color: white" v-show="logoTextShow">后台管理系统</b>
</div>
- 将
v-show设置为一个logoTextShow的变量 - 因为一开始是伸缩状态,所以return中该变量值为true
- 在click collapse方法后,将该变量取反即可
其他的一些组件和style随便写写能见人就行
后端
后端在写分页和登录界面之前都没什么好写的,我也看不懂Springboot源码,不会鞭辟入里,现在也没时间给我剖析设计哲学,所以就不按图索骥了🥴🥴🥴
连接数据库 + 设置前后端分离端口
直接在properties里面设置即可 数据库名是url那行的vue
# database connection
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/vue?serverTimezone=GMT%2b8
spring.datasource.username=root
spring.datasource.password=123456
# revise port
server.port=9090
# check mybatis XML files
mybatis-plus.mapper-locations=classpath:mapper/*.xml
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
User
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("sys_user")
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
@TableField(value = "username")
private String username;
@TableField(value = "password")
private String password;
@TableField(value = "nickname")
private String nickname;
@TableField(value = "email")
private String email;
@TableField(value = "phone")
private String phone;
@TableField(value = "address")
private String address;
}
@TableName:数据表名@TableField:数据字段名@TableId:该数据表的idIdType.AUTO:自增的Integer型idIdType.ASSIGN_ID:自增的String型id
UserMapper
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
- 暂时没有什么复杂的数据库操作,直接mybatis plus一步到位
UserService
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public List<User> findAll(){
return userMapper.selectList(null);
}
public int save(User user){
if(user.getId()==null){
return userMapper.insert(user);
}else{
return userMapper.updateById(user);
}
}
public int deleteById(Integer id){
return userMapper.deleteById(id);
}
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
// insert & update
@PostMapping
public Integer save(@RequestBody User user){
return userService.save(user);
}
// select users list
@GetMapping("/")
public List<User> findAll(){
return userService.findAll();
}
// delete from sys_user by id
@DeleteMapping("/{id}")
public Integer delete(@PathVariable Integer id){
return userService.deleteById(id);
}
}
@RequestMapping起到一个分流的作用@RequestBody将接收到的数据转换为User@DeleteMapping中使用了一个PathVariable来接收id
用户的分页查询
在查询过程中,如果有如password这样的字段不想显示给前端,可以在字段名上加注解@JsonIgnore
如:
@TableField(value = "password")
@JsonIgnore
private String password;
简单实现一个分页查询需要向数据库提供Limit的两个参数:pageNum, pageSize
@Select("select * from sys_user LIMIT #{pageNum}, #{pageSize}")
List<User> selectPage(Integer pageNum, Integer pageSize);
在controller层调用如下:
@GetMapping("/page")
public List<User> findPage(@RequestParam Integer pageNum,@RequestParam Integer pageSize){
pageNum = (pageNum-1) * pageSize;
return userMapper.selectPage(pageNum, pageSize);
}
这边@RequestParam注解一开始写成了RequestBody,debug了半天浪费了一中午,令人感叹
之后就是前后端连接对上端口要解决跨域问题:
在config层下新建一个CorsConfig类
直接复制下面的代码就行
package com.example.springweb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
分页顺便写一个能够select count的Map
//分页查询
//@RequestParam接收 pageNum=1 & pageSize = 10
@GetMapping("/page")
public Map<String,Object> findPage(@RequestParam Integer pageNum,@RequestParam Integer pageSize){
pageNum = (pageNum-1) * pageSize;
List<User> data = userMapper.selectByPage(pageNum,pageSize);
Integer total = userMapper.selectTotal();
Map<String , Object> res = new HashMap<>();
res.put("data",data);
res.put("total",total);
return res;
}
前端fetch到后端端口的JSON数据:
created(){
fetch( "http://localhost:9090/user/page?pageNum="+this.pageNum+"&pageSize="+this.pageSize).then(res => res.json())
.then( res => {
this.tableData = res.data
this.total = res.total
console.log(res)
})
},
此时更新页码后没有将当前page的各个数据传给后端,需要重新写一个fetch
考虑到代码复用,因此将fetch封装进一个load函数
load(){
fetch( "http://localhost:9090/user/page?pageNum="+this.pageNum+"&pageSize="+this.pageSize).then(res => res.json())
.then( res => {
this.tableData = res.data
this.total = res.total
console.log(res)
})
},
初始化:
created(){
this.load()
},
pageNum和pageSize更新:
handleSizeChange(pageSize){
console.log(pageSize)
this.pageSize = pageSize
this.load()
},
handleCurrentChange(pageNum){
console.log(pageNum)
this.pageNum = pageNum
this.load()
}
分页组件:
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page="pageNum"
:page-sizes="[5,10,20]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
script return值:
return {
tableData: [],
total:0,
pageNum : 1,
pageSize : 10,
collapseBtnClass:"el-icon-s-fold",
isCollapse:false,
sideWidth:200,
logoTextShow:true,
username:"",
}
目前的整体布局:
Mybatis-plus
之后的分页查询就是用Mybatis plus
在config层下配置分页MybatisPlusConfig
package com.example.springweb.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("com.example.springweb.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
这里加了@MapperScan注解之后就不需要在mapper层再写@Mapper注解了
在properties下设置:
# check mybatis XML files
mybatis-plus.mapper-locations=classpath:mapper/*.xml
# output sql sessions
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
Swagger Config
在pom中添加依赖:
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
配置SwaggerConfig类:
package com.example.springweb.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
public class SwaggerConfig {
/**
* 创建API应用
* apiInfo() 增加API相关信息
* 通过select()函数返回一个ApiSelectorBuilder实例,用来控制哪些接口暴露给Swagger来展现,
* 本例采用指定扫描的包路径来定义指定要建立API的目录。
*
* @return
*/
@Bean
public Docket restApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("标准接口")
.apiInfo(apiInfo("Spring Boot中使用Swagger2构建RESTful APIs", "1.0"))
.useDefaultResponseMessages(true)
.forCodeGeneration(false)
.select()
.apis(RequestHandlerSelectors.basePackage("com.example.springweb.controller"))
.paths(PathSelectors.any())
.build();
}
/**
* 创建该API的基本信息(这些基本信息会展现在文档页面中)
* 访问地址:http://ip:port/swagger-ui.html
*
* @return
*/
private ApiInfo apiInfo(String title, String version) {
return new ApiInfoBuilder()
.title(title)
.description("这是swaggerUI的description")
.termsOfServiceUrl("www.example.com")
.contact(new Contact("contact email", "url", "email"))
.version(version)
.build();
}
}
这里有个小bug
spring boot版本太高无法正常显示swagger-ui.html界面
需要在properties中添加配置如下:
# enable swagger2
spring.mvc.pathmatch.matching-strategy=ant_path_matcher
访问http://localhost:9090/swagger-ui.html得到正常界面:
mybatis-plus分页查询
//mybatis-plus 分页查询
@GetMapping("/page")
@ResponseBody
public IPage<User> findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestParam(required = false) String username,
@RequestParam(required = false) String email,
@RequestParam(required = false) String address){
IPage<User> page = new Page<>(pageNum, pageSize);
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
if (username != null){
queryWrapper.like("username",username);
}
if (email != null) {
queryWrapper.like("email",email);
}
if (address != null){
queryWrapper.like("address",address);
}
IPage<User> userPage = userService.page(page,queryWrapper);
return userPage;
}
- 如果不添加
(required = false)的话,没有传参进去就会panic - 同样也可以写成
@RequestParam(defaultValue = "")来表示默认值为空避免没有传参的问题
分页查询结果如下:
困死我了,明天还要上课qwq