Learn Springboot & Vue 1 前端布局和分页查询

145 阅读6分钟

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-color text-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:该数据表的id
    • IdType.AUTO:自增的Integer型id
    • IdType.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了半天浪费了一中午,令人感叹

image.png

之后就是前后端连接对上端口要解决跨域问题:

在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()
},

pageNumpageSize更新:

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:"",
}

目前的整体布局:

image.png

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得到正常界面:

image.png

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 = "")来表示默认值为空避免没有传参的问题

分页查询结果如下:

image.png

困死我了,明天还要上课qwq