springboot_vue知识点

代码放到了仓库

1.搭建

1.vue

npm install -g @vue@cli
vue create yourproject#手动选择babel和router,3
npm run serve
npm install element-plus#安装
#main.js里面全局使用
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
const app = createApp(App)
app.use(ElementPlus, { size: "small" })
app.mount('#app')
#main.js导入样式,清除控件自带
import '@/assets/global.css'
body {
    margin: 0;
    padding: 0;
    overflow: hidden;
}
/*把所有的元素变成盒状模型*/
* {
    /*外边距不会额外占用1px的像素*/
    box-sizing: border-box;
}

然后在App.vue里面使用el-container配置页面布局

<el-container>
  <el-header style="background-color: #4c535a">
    
  </el-header>
</el-container>

<el-container>
  <el-aside style="overflow: hidden; min-height: 100vh; background-color: #545c64; width: 250px">
    
  </el-aside>
  <el-main>
    
  </el-main>
</el-container>

左侧的menu绑定路由

#1.首先在 el-menu 标签里绑定 default-active 为路由的形式::default-active="$route.path" router
<el-menu :default-active="$route.path" router background-color="#545c64" text-color="#fff" active-text-color="#ffd04b">
#2.然后将 <el-menu-item> 标签里的index属性值设置成对应的路由
<el-menu-item index="/admin">管理员信息</el-menu-item>
#3.在 router/index.js 里添加对应路由配置
{path: '/admin',name: 'AdminView',component: AdminView},
#4.去掉menu小滚轮
<style>
.el-menu{
  border-right: none !important;
}
</style>

el-table用:data="tableData",el-table-column用prop="name"绑定表单数据。

2.springboot

创建数据库和表,然后创建spring工程,依赖选择web就可以,然后在pom里面添加依赖:
这里遇到Property 'sqlSessionFactory' or 'sqlSessionTemplate' are required错误,好像是因为springboot3不支持mybatis-spring-boot-starter 2.x 及以下版本,所以就去https://mvnrepository.com/搜索最新的MyBatis Spring Boot Starter ,这里我用了3.0.2。

<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>3.0.2</version>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.26</version>
		</dependency>
		<dependency>
			<groupId>tk.mybatis</groupId>
			<artifactId>mapper</artifactId>
			<version>4.1.5</version>
		</dependency>
	</dependencies>
	<repositories>
		<!-- 由于未正式发版,所以在Maven仓库里还搜不到,需要额外配置一个远程仓库 -->
		<repository>
			<id>ossrh</id>
			<name>OSS Snapshot repository</name>
			<url>https://oss.sonatype.org/content/repositories/snapshots/</url>
			<releases>
				<enabled>false</enabled>
			</releases>
			<snapshots>
				<enabled>true</enabled>
			</snapshots>
		</repository>
	</repositories>

在application.yml中添加配置

server:
  port: 8181
# 数据库配置
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root   #你本地的数据库用户名
    password: xxx #你本地的数据库密码
    url: jdbc:mysql://localhost:3306/knowledges?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true&useSSL=false&serverTimezone=GMT%2b8&allowPublicKeyRetrieval=true
# 配置mybatis实体和xml映射
mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.hckj.springboot.entity

跨域问题可以在controller上加个注解:@CrossOrigin

2.前后端请求和响应的封装

1.请求封装

前端请求用到了axios,所以先安装npm i axios -S,然后在src/utils/request.js里面封装前端请求的格式:
请求基地址,响应时间,请求头,拿到后端返回的result(response.data),以后就可以用import request from '@/utils/request'使用request去请求了。

import axois from 'axios';
//1.创建一个axios对象
const request=axois.create({baseURL:'http://localhost:8181',timeout:5000});
//2.request拦截器:请求发送前对请求做一些处理,比如统一加token,对请求参数统一加密
request.interceptors.request.use(config=>{
    config.headers['Content-Type']='application/json;charset=utf-8';
    //config.headers['token']=user.token;//设置请求头
    return config
},
    error=>{
    return Promise.reject(error)
    })
//3.response拦截器:接口响应后统一处理结果
request.interceptors.response.use(response=>{
    let res=response.data;
    if (typeof res==='string'){
        res=res?JSON.parse(res):res
    }
    return res;
},
    error => {
        console.log('err'+error)
        return Promise.reject(error)
    })
//4.导出配置好的request
export default request

2.响应封装

在common/Result.java里面封装响应,包括code,msg,data并定义常用的success和error响应:

package com.hckj.springboot.common;

public class Result {
    private static final String SUCCESS="0";
    private static final String ERROR="-1";
    private String code;
    private String msg;
    private Object data;
    public static Result success(){
        Result result=new Result();
        result.setCode(SUCCESS);
        return result;
    }
    public static Result success(Object data){
        Result result=new Result();
        result.setCode(SUCCESS);
        result.setData(data);
        return result;
    }
    public static Result error(String msg){
        Result result=new Result();
        result.setCode(ERROR);
        result.setMsg(msg);
        return  result;
    }
    //get和set方法

这样,后端在给前端数据时都是Result类型,并调用里面的success和error方法。

3.增删改查

1.查询

将全部查询和按条件查询写到一个接口里,因为进来要全部查询,所以函数要挂载到onMounted上;条件查询,所以要给查询按钮绑定点击事件;然后在xml里面通过sql语句按条件查询和全部查询。
后端

#1.参数和数据库表都创建实体类
public class Params {
    private String name;
    private  String phone;
	//get,set方法
}
@Table(name="admin")//这里必须是双引号
public class Admin {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    @Column(name = "name")
    private String name;
    @Column(name = "password")
    private String password;
    @Column(name = "sex")
    private String sex;
    @Column(name = "age")
    private Integer age;
    @Column(name = "phone")
    private String phone;
	//get,set方法
}
#2.dao接口和xml
@Repository
public interface AdminDao extends Mapper<Admin> {
    List<Admin> findBySearch(@Param("params") Params params);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.hckj.springboot.dao.AdminDao">
    <select id="findBySearch" resultType="com.hckj.springboot.entity.Admin">
        select * from admin
        <where>
            <if test="params != null and params.name != null and params.name != ''">
                and name like concat('%', #{ params.name }, '%')
            </if>
            <if test="params != null and params.phone != null and params.phone != ''">
                and phone like concat('%', #{ params.phone }, '%')
            </if>
        </where>
    </select>
</mapper>
# 3.service类
@Service
public class AdminService {
    @Autowired
    private AdminDao adminDao;
    public List<Admin> findBySearch(Params params) {
        return adminDao.findBySearch(params);
    }

}
#4.controller,通过封装的result返回前端数据
@GetMapping("/search")
    public Result findBySearch(Params params){
        List<Admin> list = adminService.findBySearch(params);
        return Result.success(list);

前端,这里初始化ref变量的时候注意是列表[]还是对象{},赋值和取值的时候记得加.value,然后变量和方法都要return。

<template>
  <div class="about">
    <div>
      <el-input v-model="searchparams.name" style="width: 200px" placeholder="请输入姓名"></el-input>
      <el-input v-model="searchparams.phone" style="width: 200px; margin-left: 5px" placeholder="请输入电话"></el-input>
      <el-button type="warning" style="margin-left: 10px" @click="findBySearch()">查询</el-button>
      <el-button type="primary" style="margin-left: 10px" >新增</el-button>
    </div>
    <div>
      <el-table :data="tableData" style="width: 100%; margin: 15px 0px">
        <el-table-column prop="name" label="姓名" width="180"></el-table-column>
        <el-table-column prop="sex" label="姓别" width="180"></el-table-column>
        <el-table-column prop="age" label="年龄"></el-table-column>
        <el-table-column prop="phone" label="电话"></el-table-column>
        <el-table-column label="操作">
          <el-button type="primary">编辑</el-button>
          <el-button type="danger">删除</el-button>
        </el-table-column>
      </el-table>
    </div>
  </div>
</template>
<script>
import {ref,onMounted} from "vue";
import request from '@/utils/request'
export default {
  setup(){
    const tableData=ref([]);
    const searchparams=ref({
      name:"",
      phone:"",
    });
    const findBySearch=()=>{
      request.get("/search",{params:searchparams.value}).then((res)=>{
        if(res.code==="0"){
          tableData.value=res.data;
        }
      })
    };
    onMounted(()=>{
      findBySearch();
    });
    return{
      tableData,
      searchparams,
      findBySearch,
    }
  }
}
</script>

2.分页

后端
1.首先pom添加依赖:

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.7</version>
</dependency>

2.application.yml里面写分页配置

#配置分页
pagehelper:
  helper-dialect: mysql
  reasonable: true
  support-methods-arguments: true
  params: count=countSql

3.修改service和controler层

//1.service层里面首先开启分页查询,然后返回时将数据类型变为PageInfo
 public PageInfo<Admin>  findBySearch(Params params) {
        // 开启分页查询
        PageHelper.startPage(params.getPageNum(), params.getPageSize());
        // 接下来的查询会自动按照当前开启的分页设置来查询
        List<Admin> list = adminDao.findBySearch(params);
        return PageInfo.of(list);
    }
//2.controller层里面调用service层时返回的数据类型改为PageInfo即可
PageInfo<Admin> list = adminService.findBySearch(params);

4.前端
在vue组件里面添加el-pagination组件,然后在script里面配置参数:

<el-pagination
          @size-change="handleSizeChange"
          @current-change="handleCurrentChange"
          :current-page="searchparams.pageNum"
          :page-sizes="[5, 10, 15, 20]"
          :page-size="searchparams.pageSize"
          layout="total, sizes, prev, pager, next, jumper"
          :total="total">
      </el-pagination>
//这里面涉及到方法2个,参数3个;其中两个参数放到searchparams里面传给后端,total不用传给后端,后端会返回过来数据,然后赋值给total,最后将参数和方法return
const searchparams=ref({
      name:"",
      phone:"",
      pageNum: 1,
      pageSize: 5
    });
    const total =ref( 0);
    const findBySearch=()=>{
      request.get("/search",{params:searchparams.value}).then((res)=>{
        if(res.code==="0"){
          tableData.value=res.data.list;
          total.value = res.data.total;
        }
      })
    };
    function  handleSizeChange(pageSize){
      searchparams.value.pageSize=pageSize;
      findBySearch();
    }
    function  handleCurrentChange(pageNum){
      searchparams.value.pageNum=pageNum;
      findBySearch();
    }

3.新增和编辑

1.首先给新增和编辑添加click事件,然后使用el-dialog填写表单信息,编辑的时候使用v-slot绑定就可以拿到这条数据信息,这两个前端的区分就是form数据。

<el-button type="primary" style="margin-left: 10px" @click="add()">新增</el-button>
<el-table-column label="操作" v-slot="scope">
    <el-button type="primary" @click="edit(scope.row)">编辑</el-button>
    <el-button type="danger">删除</el-button>
</el-table-column>
const form=ref({});
const add=()=>{
      form.value={};
      dialogFormVisible.value=true;
    };
const edit=(obj)=>{
  form.value=obj;
  dialogFormVisible.value=true;
}

2.然后就是form表单,这里使用了el-dialog和el-form,取消的话就关闭,确定的话就向后端发送数据进行请求。

<el-dialog title="用户信息" v-model="dialogFormVisible" >
        <el-form :model="form">
          <el-form-item label="姓名" label-width="15%">
            <el-input v-model="form.name" autocomplete="off" style="width:90%"></el-input>
          </el-form-item>
          <el-form-item label="性别" label-width="15%">
            <el-radio v-model="form.sex" label="男"></el-radio>
            <el-radio v-model="form.sex" label="女"></el-radio>
          </el-form-item>
          <el-form-item label="年龄" label-width="15%">
            <el-input v-model="form.age" autocomplete="off" style="width: 90%"></el-input>
          </el-form-item>
          <el-form-item label="电话" label-width="15%">
            <el-input v-model="form.phone" autocomplete="off" style="width: 90%"></el-input>
          </el-form-item>
        </el-form>
        <div slot="footer" class="dialog-footer">
          <el-button @click="dialogFormVisible = false">取 消</el-button>
          <el-button type="primary" @click="submit()">确 定</el-button>
        </div>
      </el-dialog>
      const submit=()=>{
      request.post('addedit',form.value).then((res)=>{
        if (res.code==="0"){
          dialogFormVisible.value=false;
          findBySearch();
        }
      })
    }

3.后端拿到数据根据id判断时新增还是编辑,然后通过controller和service层完成操作。

@PostMapping("/addedit")
    public Result save(@RequestBody Admin admin){
        if (admin.getId()==null){//新增
            adminService.add(admin);
        }else{//编辑
            adminService.update(admin);
        }
        return Result.success();
    }
    public void add(Admin admin){
        if (admin.getPassword() == null) {
            admin.setPassword("123456");
        }
        adminDao.insertSelective(admin);//通过掉包实现插入数据,不用再去操作dao层
    }
    public void update(Admin admin) {
        adminDao.updateByPrimaryKeySelective(admin);//同上
    }

5.error:java.lang.NoSuchMethodException: tk.mybatis.mapper.provider.SpecialProvider.()
解决方法:mapperscan包从tk中导入 import tk.mybatis.spring.annotation.MapperScan;

4.删除

1.删除按钮使用popconfirm进行二次确认:

<el-popconfirm title="确定删除吗?" @confirm="del(scope.row.id)">
   <template #reference>
    <el-button slot="reference" type="danger" style="margin-left: 5px">删除</el-button>
   </template>>
 </el-popconfirm>

2.当confirm确认时,就向后端发送删除请求:

const del=(id)=> {
      request.delete("/del/" + id).then((res)=> {
        if (res.code === '0') {
          findBySearch();
        }
      })
    }

3.后端处理

@DeleteMapping("/del/{id}")
    public Result delete(@PathVariable Integer id){
        adminService.delete(id);
        return Result.success();
    }
public void delete(Integer id) {
        adminDao.deleteByPrimaryKey(id);
    }

4.跨域和自定义异常

1.跨域问题,后端common里面加一个CorsConfig.java

package com.hckj.springboot.common;

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 {

    @Bean
    public CorsFilter corsFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
        corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
        corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
        source.registerCorsConfiguration("/**", corsConfiguration); // 4 对接口配置跨域设置
        return new CorsFilter(source);
    }
}

2.自定义异常捕获,在exception里面先建GlobalException:

@ControllerAdvice(basePackages="com.hckj.springboot.controller")
public class GlobalExceptionHandler {

    private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);

    //统一异常处理@ExceptionHandler,主要用于Exception
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public Result error(HttpServletRequest request, Exception e){
        log.error("异常信息:",e);
        return Result.error("系统异常");
    }

    @ExceptionHandler(CustomException.class)
    @ResponseBody
    public Result customError(HttpServletRequest request, CustomException e){
        return Result.error(e.getMsg());
    }
}

然后相同目录下新建CustomException自定义异常msg:

public class CustomException extends RuntimeException {
    private String msg;

    public CustomException(String msg) {
        this.msg = msg;
    }

  get和set方法
}

5.JWT鉴权

1.首先用户登录之后将后台返回的用户信息保存到浏览器的localstorage中:

localStorage.setItem("user", JSON.stringify(res.data));

2.在页面右上角拿到localstorage的user数据,显示username,退出登陆时删除localstorage里面的user信息:

localStorage.setItem("user", JSON.stringify(res.data));
<el-dropdown style="float: right; height: 60px; line-height: 60px">
 <span class="el-dropdown-link" style="color: white; font-size: 16px">{{ user.name }}<el-icon class="el-icon--right"><arrow-down /></el-icon></span>
  <template #dropdown>
    <el-dropdown-item>
      <div @click="logout">退出登录</div>
    </el-dropdown-item>
  </template>
</el-dropdown>
const logout=()=>{
      localStorage.removeItem("user");
      router.push("/login")
    };

3.任何人都可以通过路由访问主页等信息,不安全,所以在前端做一个路由守卫,如果localstorage里面没有user的信息就只能去注册和登录页面:

router.beforeEach((to ,from, next) => {
  if (to.path ==='/login'|| to.path==='/register') {
    next();
  }
  const user = localStorage.getItem("user");
  if (!user && to.path !== '/login' && to.path !== '/register'){
    return next("/login");
  }
  next();
})

4.这样就只有localstorage里面有user:“xxx”数据才可以,但是这个数据可以伪造,所以就用到了jwt:在用户登录后,后台给前台发送一个凭证(token),前台请求的时候需要带上这个凭证(token),才可以访问接口,如果没有凭证或者凭证跟后台创建的不一致,则说明该用户不合法。

1.配置pom

添加依赖

<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>4.4.0</version>
</dependency>
<dependency>
	<groupId>cn.hutool</groupId>
	<artifactId>hutool-all</artifactId>
	<version>5.8.22</version>
</dependency>

2.拦截前端请求的拦截器

给后台接口加上统一的前缀/api,然后我们统一拦截该前缀开头的接口,所以在common/WebConfig.java配置一个拦截器。

@Configuration
public class WebConfig implements  WebMvcConfigurer {
    @Override
    public void configurePathMatch(PathMatchConfigurer configurer) {
        // 指定controller统一的接口前缀
        configurer.addPathPrefix("/api", clazz -> clazz.isAnnotationPresent(RestController.class));
    }
}

记得给前端请求的拦截器request封装里面,baseUrl也加个 /api 前缀。

3.生成token并验证token

在common/JwtTokenUtils.java里面genToken利用用户的id和密码生成一个有效期2小时的Token,getCurrentUser根据token解码到id,然后查找用户是否存在:

@Component
public class JwtTokenUtils {

    private static AdminService staticAdminService;
    private static final Logger log = LoggerFactory.getLogger(JwtTokenUtils.class);
    @Resource
    private AdminService adminService;

    @PostConstruct
    public void setUserService() {
        staticAdminService = adminService;
    }

    /**
     * 生成token
     */
    public static String genToken(String adminId, String sign) {
        return JWT.create().withAudience(adminId) // 将 user id 保存到 token 里面,作为载荷
                .withExpiresAt(DateUtil.offsetHour(new Date(), 2)) // 2小时后token过期
                .sign(Algorithm.HMAC256(sign)); // 以 password 作为 token 的密钥
    }

    /**
     * 获取当前登录的用户信息
     */
    public static Admin getCurrentUser() {
        String token = null;
        try {

            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            token = request.getHeader("token");
            if (StrUtil.isBlank(token)) {
                token = request.getParameter("token");
            }
            if (StrUtil.isBlank(token)) {
                log.error("获取当前登录的token失败, token: {}", token);
                return null;
            }
            // 解析token,获取用户的id
            String adminId = JWT.decode(token).getAudience().get(0);
            return staticAdminService.findById(Integer.valueOf(adminId));
        } catch (Exception e) {
            log.error("获取当前登录的管理员信息失败, token={}", token,  e);
            return null;
        }
    }
}

在service里面添加一个利用id找用户

public Admin findById(Integer id) {
        return adminDao.selectByPrimaryKey(id);
    }

4.登录后生成token

在登录的service层里面,当用户登陆成功后,利用上面的函数生成token:

String token = JwtTokenUtils.genToken(user.getId().toString(), user.getPassword());
user.setToken(token);//这里给admin实体添加一个token

这里给用户实体类添加一个暂时的token属性,然后setget方法:

@Transient//不需要被持久化或序列化的临时数据或敏感数据
private String token;

5.前端获取token然后每次请求时header带着token

因为登录后返回的用户信息保存在了localstorage里面,所以在request.js封装的request请求里面从localstorage里面拿到token,然后放到请求头里面:

const user = localStorage.getItem("user");
if (user) {
    config.headers['token'] = JSON.parse(user).token;
}

这样的话如果登录了并拿到了token,2小时之内向后端请求的话header会带有token去给后端验证。

6.后端jwt拦截器

在common/JwtInterceptor.java里面拦截http请求,验证token:

@Component
public class JwtInterceptor implements HandlerInterceptor {

    private static final Logger log = LoggerFactory.getLogger(JwtInterceptor.class);

    @Resource
    private AdminService adminService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        // 1. 从http请求的header中获取token
        String token = request.getHeader("token");
        if (StrUtil.isBlank(token)) {
            // 如果没拿到,我再去参数里面拿一波试试  /api/admin?token=xxxxx
            token = request.getParameter("token");
        }
        // 2. 开始执行认证
        if (StrUtil.isBlank(token)) {
            throw new CustomException("无token,请重新登录");
        }
        // 获取 token 中的userId
        String userId;
        Admin admin;
        try {
            userId = JWT.decode(token).getAudience().get(0);
            // 根据token中的userid查询数据库
            admin = adminService.findById(Integer.parseInt(userId));
        } catch (Exception e) {
            String errMsg = "token验证失败,请重新登录";
            log.error(errMsg + ", token=" + token, e);
            throw new CustomException(errMsg);
        }
        if (admin == null) {
            throw new CustomException("用户不存在,请重新登录");
        }
        try {
            // 用户密码加签验证 token
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(admin.getPassword())).build();
            jwtVerifier.verify(token); // 验证token
        } catch (JWTVerificationException e) {
            throw new CustomException("token验证失败,请重新登录");
        }
        return true;
    }
}

7.使用jwt拦截器拦截前端请求

将上面的拦截功能在common/webConfig里面使用拦截,过滤掉登录注册等白名单路由:

@Resource
private JwtInterceptor jwtInterceptor;

// 加自定义拦截器JwtInterceptor,设置拦截规则
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(jwtInterceptor).addPathPatterns("/api/**")
            .excludePathPatterns("/api/login")
            .excludePathPatterns("/api/register");
}

6.文件的上传下载

1.上传

1.后端FileController.java里面写文件上传的控制器,这里用到了hutool这个依赖去将上传的文件写入到服务器的指定位置,然后将文件名里面的时间戳返回到前端,前端拿到时间戳再和表单里的其他信息一起保存,时间戳保存到img字段。

private static final String filePath = System.getProperty("user.dir") + "/file/";
@PostMapping("/upload")
public Result upload(MultipartFile file) {
    synchronized (FileController.class) {
        String flag = System.currentTimeMillis() + "";
        String fileName = file.getOriginalFilename();
        try {
            if (!FileUtil.isDirectory(filePath)) {
                FileUtil.mkdir(filePath);
            }
            // 文件存储形式:时间戳-文件名
            FileUtil.writeBytes(file.getBytes(), filePath + flag + "-" + fileName);
            System.out.println(fileName + "--上传成功");
            Thread.sleep(1L);
        } catch (Exception e) {
            System.err.println(fileName + "--文件上传失败");
        }
        return Result.success(flag);
    }
}

2.因为文件上传没有走http请求,所以没有header的token,这里有两种方式,一种是在后端的webconfig拦截器里面放行,另一种是给加上token,这里用第一种:.excludePathPatterns("/api/files/**")
3.前端写上传文件的el-upload和拿后端给的时间戳:

<el-form-item label="图书封面" label-width="15%">
 <el-upload action="http://localhost:8181/api/files/upload" :on-success="successUpload">
    <el-button  type="primary">点击上传</el-button>
  </el-upload>
</el-form-item>
function successUpload(res){
  form.value.img=res.data;
}

2.下载

1.FileController.java里面写文件下载的get请求。

@GetMapping("/{flag}")
    public void avatarPath(@PathVariable String flag, HttpServletResponse response) {
        if (!FileUtil.isDirectory(filePath)) {
            FileUtil.mkdir(filePath);
        }
        OutputStream os;
        List<String> fileNames = FileUtil.listFileNames(filePath);
        String avatar = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse("");
        try {
            if (StrUtil.isNotEmpty(avatar)) {
                response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(avatar, "UTF-8"));
                response.setContentType("application/octet-stream");
                byte[] bytes = FileUtil.readBytes(filePath + avatar);
                os = response.getOutputStream();
                os.write(bytes);
                os.flush();
                os.close();
            }
        } catch (Exception e) {
            System.out.println("文件下载失败");
        }
    }

2.下载到前端页面进行显示

<el-table-column label="图书封面">
  <template v-slot="scope">
    <el-image
        style="width: 70px; height: 70px; border-radius: 50%"
        :src="'http://localhost:8181/api/files/' + scope.row.img"
        :preview-src-list="['http://localhost:8181/api/files/' + scope.row.img]">
    </el-image>
  </template>
</el-table-column>

3.点击下载按钮,通过浏览器下载到本地

 <el-button type="primary" @click="down(scope.row.img)">下载</el-button>
 const down=(flag)=>{
      window.location.href = `http://localhost:8181/api/files/${flag}`;
    };

7.批量删除

1.首先是在table里面在条数据前面加一个勾选框,然后每次点选都有触发事件:

<el-table :data="tableData" style="width: 100%" ref="table" @selection-change="handleSelectionChange" :row-key="getRowKeys">
        <el-table-column ref="table" type="selection" width="55" align="center" :reserve-selection="true"></el-table-column>
</el-table>
const multipleSelection = ref([]);
const handleSelectionChange = (val) => {
 multipleSelection.value = val;
};
const getRowKeys = (row) => {
  return row.id;
};

2.批量删除的二次确认按钮,并触发后端请求事件

<el-popconfirm title="确定删除这些数据吗?" @confirm="delBatch()">
 <template #reference>
    <el-button slot="reference" type="danger" style="margin-left: 5px">批量删除</el-button>
  </template>>
</el-popconfirm>
import { ElMessage } from 'element-plus';
const delBatch = () => {
	if (multipleSelection.value.length === 0) {
	  ElMessage.warning("请勾选您要删除的项");
	  return;
	}
	request.put("/type/delBatch", multipleSelection.value).then(res => {
	  if (res.code === '0') {
	    ElMessage.success("批量删除成功");
	    findBySearch(); // 请确保你的 `findBySearch` 方法在这个作用域中是可用的
	  } else {
	    ElMessage.error(res.msg);
	  }
	});
	};

3.后端在controller层里面利用for循环调用del:

@PutMapping("/delBatch")
    public Result delBatch(@RequestBody List<Type> list) {
        for (Type type : list) {
            typeService.delete(type.getId());
        }
        return Result.success();
    }

8.数据库导入导出excel文件

1.导出

1.首先前端有一个导出按钮,然后点击之后带着token像后端发送请求,因为不是走request,所以拼接上token(或者在后端放行)。

<el-button type="success" style="margin-left: 10px" @click="exp()">导出报表</el-button>
const exp=()=>{
  const user = JSON.parse(localStorage.getItem("user"));
  if (user) {
    const token = user.token;
    window.location.href = `http://localhost:8181/api/type/export?token=${token}`;
  }
};

2.后端

@GetMapping("/export")
public Result export(HttpServletResponse response) throws IOException {
     // 思考:
     // 要一行一行的组装数据,塞到一个list里面
     // 每一行数据,其实就对应数据库表中的一行数据,也就是对应Java的一个实体类Type
     // 我们怎么知道它某一列就是对应某个表头呢?? 需要映射数据,我们需要一个Map<key,value>,把这个map塞到list里
     // 1. 从数据库中查询出所有数据
     List<Type> all = typeService.findAll();

     if (CollectionUtil.isEmpty(all)) {
         throw new CustomException("未找到数据");
     }

     // 2. 定义一个 List,存储处理之后的数据,用于塞到 list 里
     List<Map<String, Object>> list = new ArrayList<>(all.size());

     // 3. 定义Map<key,value> 出来,遍历每一条数据,然后封装到 Map<key,value> 里,把这个 map 塞到 list 里
     for (Type type : all) {
         Map<String, Object> row = new HashMap<>();
         row.put("图书类别名称", type.getName());
         row.put("图书类别描述", type.getDescription());

         list.add(row);
     }

     // 4. 创建一个 ExcelWriter,把 list 数据用这个writer写出来(生成出来)
     ExcelWriter wr = ExcelUtil.getWriter(true);
     wr.write(list, true);

     // 5. 把这个 excel 下载下来
     response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=utf-8");
     response.setHeader("Content-Disposition","attachment;filename=type.xlsx");
     ServletOutputStream out = response.getOutputStream();
     wr.flush(out, true);
     wr.close();
     IoUtil.close(System.out);

     return Result.success();
 }

2.导入

1.首先是前端的导入按钮,这里让它post访问后端的接口,因为没有带token并没有使用request封装,所以在后端拦截器里面给他放行。

<el-upload action="http://localhost:8181/api/type/upload" style="display: inline-block; margin-left: 10px" :show-file-list="false" :on-success="successUpload">
 <el-button size="small" type="primary">批量导入</el-button>
</el-upload>
const successUpload=(res)=>{
 if (res.code==='0'){
    ElMessage.success("批量导入成功");
  }else{
    ElMessage.error(res.msg);
  }
}
.excludePathPatterns("/api/type/upload")

2.后端在controller里面读取excel并将数据写入数据库

@PostMapping("/upload")
    public Result upload(MultipartFile file) throws IOException {
        List<Type> infoList = ExcelUtil.getReader(file.getInputStream()).readAll(Type.class);
        if (!CollectionUtil.isEmpty(infoList)) {
            for (Type type : infoList) {
                try {
                    typeService.add(type);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return Result.success();
    }

3.这里需要注意:excel里面的表头要和数据库里面的表头对应,所以在实体类里面添加@Alias("分类名称")注解,即列的别名或描述信息。

@Column(name = "name")
@Alias("图书类别名称")
private String name;
@Column(name = "description")
@Alias("图书类别描述")
private String description;

9.模块关联

这里用图书和图书类别为实例,需要给图书表里面添加字段typeId,用来关联类别表里面的id,然后记得给图书Book实体类添加这个字段映射,然后在图书列表里面也显示这一列。

@Column(name="typeId")
private Integer typeId;

然后前端遍历type表,将type信息放到下拉选框里,让用户选择,并显示在book信息列。

<el-table-column prop="typeId" label="图书分类"></el-table-column>//1.table添加这一列显示
<el-form-item label="图书分类" label-width="15%">//2.form表单的下拉选择,这里遍历了typeObjs列表,然后将用户选的id放到form.typeId。
 <el-select v-model="form.typeId" placeholder="请选择" style="width: 90%">
    <el-option v-for="item in typeObjs" :key="item.id" :label="item.name" :value="item.id"></el-option>
  </el-select>
</el-form-item>
//3.拿到typeid的功能要放在onmounted里面,最后这个列表和方法都要return
 const typeObjs=ref([]);
    const findTypes=()=>{
 request.get("/type").then((res)=>{
    if(res.code==='0'){
      typeObjs.value=res.data;
    }else{
      ElMessage.error(res.msg);
    }
  })
}
onMounted(()=>{
  findTypes();
});

后端就在type控制层里面拿到type表里的所有信息。

@GetMapping
public Result findAll() {
    return Result.success(typeService.findAll());
}

此时,book信息就会显示图书类别这一列,并在form里面有下拉框遍历了type让用户选,但是用户选择后拿到的typeid,这是int类型的数据,所以还需要根据这个id去type表里面拿到对应的name,显示到前端。这里有两种方法,一种是在service层,将拿到的图书列表信息的typeid在type表里面通过id 查到那么,返回给前端;另一种方式是在mapper层通过关联两张表拿到type.name。这里要注意,因为book表里面只有typeid这个字段,但是没有typename这个字段,所以需要在实体类里面添加@Transient注解,然后在前端table显示时prop字段用typename

@Transient
private String typeName;
<el-table-column prop="typeName" label="图书分类"></el-table-column>

这里两种方式都演示以下。

1.service映射

@Resource
private TypeDao typeDao;

public PageInfo<Book> findBySearch(Params params) {
    // 开启分页查询
    PageHelper.startPage(params.getPageNum(), params.getPageSize());
    // 接下来的查询会自动按照当前开启的分页设置来查询
    List<Book> list = bookDao.findBySearch(params);
    if (CollectionUtil.isEmpty(list)) {
        return PageInfo.of(new ArrayList<>());
    }
    for (Book book : list) {
        if (ObjectUtil.isNotEmpty(book.getTypeId())) {
            Type type = typeDao.selectByPrimaryKey(book.getTypeId());
            if (ObjectUtil.isNotEmpty(type)) {
                book.setTypeName(type.getName());
            }
        }
    }
    return PageInfo.of(list);
}

2.mapper关联

select book.*,type.name as typeName from book left join type on book.typeId=type.id

10.角色管理

这里的一个简便方法就是,首先拿到localstorage里面的user,然后用if语句判断用户的role 是否是你想要的角色,就可以隐藏显示menu控件等。

v-if="user.role === 'ROLE_ADMIN'">
const user=ref(localStorage.getItem("user") ? JSON.parse(localStorage.getItem("user")) : {})
return{user,}

11.审批功能

这个功能是在一个模块里面完成,一个角色负责申请(add),另一个角色负责审批(update/edit)。
这里面主要是有两个dialog-form;然后就是当学生打开dialog时,自动拿到他的id(form.value.userId =user.value.id;),放到表单一起提交到后台,然后后台在显示列表时,加一个条件就是id和学生限制,这样每个学生就只能看到自己的申请记录:

if ("ROLE_STUDENT".equals(user.getRole())) {
   params.setUserId(user.getId());
}
<select id="findBySearch" resultType="com.hckj.springboot.entity.Audit">
    select audit.*, admin.name as userName from audit left join admin on audit.userId = admin.id
    <where>
        <if test="params != null and params.name != null and params.name != ''">
            and audit.name like concat('%', #{ params.name }, '%')
        </if>
        <if test="params != null and params.userId != null">
            and audit.userId = #{ params.userId }
        </if>
    </where>
</select>

12.预约功能

这个功能涉及到两个模块,一个模块负责酒店信息列表和预约功能,一个模块负责显示预约列表。所以有两个表和实体类,hotel信息和reserve信息。reserve信息涉及将id转换为name,这个主要就是现在entity里面用transient注解,然后在mapper或者service层过滤。

@Column(name = "hotelId")
private Integer hotelId;
@Column(name = "userId")
private Integer userId;
@Transient
private String hotelName;
@Transient
private String userName;

13.AOP日志管理

日志管理的前端和后端对数据库的增删差都和前面没差,主要就是要实现AOP切面管理。

1.依赖

首先导入要用的依赖

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.自定义注解

在common/AutoLog.java里面自定义一个注解,这个注解将被用在controller的方法上。

package com.hckj.springboot.common;
import java.lang.annotation.*;
@Target(ElementType.METHOD)//指定注解可以应用的目标元素,这里是 ElementType.METHOD,表示该注解可以用于方法。
@Retention(RetentionPolicy.RUNTIME)//指定注解的生命周期,RetentionPolicy.RUNTIME 表示该注解会在运行时保留,这允许在运行时通过反射来访问注解信息
@Documented//指定了注解 AutoLog 包含在生成的 Javadoc 文档中
public @interface AutoLog {
    String value() default "";
}

3.AOP切面处理

在common/LogAspect.java里面将使用控制器方法前后需要做的动作定义好。

@Component
@Aspect // 表示 LogAspect 类是一个切面类,用于定义横切关注点(cross-cutting concerns),在这里是用于日志记录。
public class LogAspect {
    @Resource
    private LogService logService;
    @Around("@annotation(autoLog)")//使用 @Around 注解指定在目标方法执行前和执行后都会执行的通知。@annotation(autoLog) 表示这个通知会织入那些被标记了@AutoLog 注解的方法。
    public Object doAround(ProceedingJoinPoint joinPoint,AutoLog autoLog)throws Throwable{//joinPoint 是Spring AOP提供的一个接口,用于访问被通知方法的信息。
        String name = autoLog.value();//在注解里定义了value()
        String time = DateUtil.now();// 操作时间(当前时间)
        String username = ""; 操作人
        Admin user = JwtTokenUtils.getCurrentUser();
        if (ObjectUtil.isNotNull(user)) {
            username = user.getName();
        }
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();//通过RequestContextHolder获取当前请求的上下文信息,然后,执行了被通知方法,获取了方法的返回结果 Result。
        String ip = request.getRemoteAddr();// 操作人IP
        //前面是切面前执行
        Result result = (Result) joinPoint.proceed();// 执行具体的接口(开始去执行注解的方法的内容)
        //后面是切面后执行
        Object data = result.getData();
        if (data instanceof Admin) {//登录操作,没有从token中拿到name,所以接口执行完了再那name。
            Admin admin = (Admin) data;
            username = admin.getName();
        }
        Log log = new Log(null, name, time, username, ip);//去往日志表里写一条日志记录,admin实体类要有构造方法
        logService.add(log);
        return result;

    };
}

4.在controller的方法里面使用自定义的注解

@AutoLog("登录")
@AutoLog("酒店预订")

14.图形验证码

首先是前端随机生成一个key,然后发送到后端,后端用着key生成一个value(验证码数据)和图片,然后把图片发送到前端,让后登录按钮点击后会再次带上这个key,后台会根据key找value,看和前端发过来的数字是否一致。

1.依赖

<dependency>
    <groupId>com.github.whvcse</groupId>
    <artifactId>easy-captcha</artifactId>
    <version>1.6.2</version>
</dependency>

2.定义Mapper映射格式

因为涉及到key,value所以在common/CaptureConfig.java里面定义一个captureconfig类,他的格式就是map映射的格式

@Component
public class CaptureConfig {
    public static Map<String ,String > CAPTURE_MAP=new HashMap<>();
}

3.生成验证码的控制器

在controller/CaptureController.java里面根据key生成value和验证码图片

@CrossOrigin
@RestController
@RequestMapping
public class CaptureController {

    @RequestMapping("/captcha")
    public void captcha(@RequestParam String key, HttpServletRequest request, HttpServletResponse response) throws Exception {
        // 指定验证码的长宽以及字符的个数
        SpecCaptcha captcha = new SpecCaptcha(135, 33, 5);
        captcha.setCharType(Captcha.TYPE_NUM_AND_UPPER);
        // 首先把验证码在后台保存一份,但是不能保存在session,可以存在redis,也可以存在后台的某个Map里面
        CaptureConfig.CAPTURE_MAP.put(key, captcha.text().toLowerCase());
        CaptchaUtil.out(captcha, request, response);

        // 算术类型
//        ArithmeticCaptcha captcha = new ArithmeticCaptcha(135, 33);
//        captcha.setLen(4);  // 几位数运算,默认是两位
//        captcha.getArithmeticString();  // 获取运算的公式:3+2=?
//        captcha.text();  // 获取运算的结果:5
//        CaptureConfig.CAPTURE_MAP.put(key, captcha.text().toLowerCase());
//        CaptchaUtil.out(captcha, request, response);
    }
}

4.登陆页面的key和验证码请求

这里提前做两件事儿,首先是在admin实体类里面添加临时数据

 @Transient
private String verCode;

2.访问captcha控制器没有token,所以需要在webconfig里面放行:.excludePathPatterns("/api/captcha")
3.现在就可以开始在前端生成key,发送给后端captcha_controller生成验证码图像,然后登录时给请求地址里添加key

<el-form-item>
 <div style="display: flex; justify-content: center; align-items: center;">
    <el-input v-model="admin.verCode" prefix-icon="el-icon-user" style="width: 60%;" placeholder="请输入验证码"></el-input>
    <img :src="captchaUrl" @click="clickImg()" style="cursor: pointer; width:140px; height:33px" />
  </div>
</el-form-item>
 const admin=ref({name:'',password:'',verCode: '',});
 const key=ref("");
 const captchaUrl=ref("");
 const clickImg = () => {
      key.value = Math.random();
      captchaUrl.value = `http://localhost:8181/api/captcha?key=${key.value}`;
    };
onMounted(()=>{
    key.value=Math.random();
  captchaUrl.value = 'http://localhost:8181/api/captcha?key=' + key.value;
});

5.后端登录的验证

现在需要拿到请求路径力的key,然后根据map映射拿到原本的captcha和用户提交的form表单里的captcha进行验证。

 @PostMapping("/login")
 @AutoLog("登录")
  public Result login(@RequestBody Admin admin,@RequestParam String key, HttpServletRequest request){
      if (!admin.getVerCode().toLowerCase().equals(CaptureConfig.CAPTURE_MAP.get(key))) {
          // 如果不相等,说明验证不通过
          CaptchaUtil.clear(request);
          return Result.error("验证码不正确");
      }
      Admin loginUser=adminService.login(admin);
      return Result.success(loginUser);
  }

15.Echarts

可以去echarts官网进行学习,首先下载导入

npm install echarts
import * as echarts from 'echarts';

然后利用官网文档作图,这里需要注意的时图的初始化initECharts和后台数据的处理。

1.饼状图

bie图的数据格式是[{value:xxx,name:xxx},{}],所以后端传递的数据要处理成这种格式:

@Select("select book.*, type.name as typeName from book left join type on book.typeId = type.id")
List<Book> findAll();
public List<Book> findAll(){
        return bookDao.findAll();
    }
@GetMapping("/echarts/bie")
public Result bie() {
    // 查询出所有图书
    List<Book> list = bookService.findAll();
    Map<String, Long> collect = list.stream()
            .filter(x -> ObjectUtil.isNotEmpty(x.getTypeName()))
            .collect(Collectors.groupingBy(Book::getTypeName, Collectors.counting()));
    // 最后返回给前端的数据结构
    List<Map<String, Object>> mapList = new ArrayList<>();
    if (CollectionUtil.isNotEmpty(collect)) {
        for (String key : collect.keySet()) {
            Map<String, Object> map = new HashMap<>();
            map.put("name", key);
            map.put("value", collect.get(key));
            mapList.add(map);
        }
    }
    return Result.success(mapList);
}

前端的话就是给一个div表明位置,然后准备初始化数据,并都放在initecharts,最后挂载到onmounted上,再return。

<div id="bie" style="width: 100%; height: 400px"></div>
const initBie=(data)=>{
  var chartDom = document.getElementById('bie');
  let myChart = echarts.init(chartDom);
    const option = {
      title: {
        text: '图书统计(饼图)',
        subtext: '统计维度:图书分类',
        left: 'center'
      },
      tooltip: {
        trigger: 'item'
      },
      legend: {
        orient: 'vertical',
        left: 'left'
      },
      series: [
        {
          name: 'Access From',
          type: 'pie',
          radius: '50%',
          data: data,
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)'
            }
          }
        }
      ]
    };
    option && myChart.setOption(option);
};
const initEcharts=()=>{
  request.get("/book/echarts/bie").then(res => {
    if (res.code === '0') {
      // 开始去渲染饼图数据啦
      initBie(res.data)
    }
  })
};
 onMounted(()=>{
  initEcharts();
  });

2.折线图和柱状图

这两个图的数据格式是一样的

@GetMapping("/echarts/bar")
public Result bar() {
    // 查询出所有图书
    List<Book> list = bookService.findAll();
    Map<String, Long> collect = list.stream()
            .filter(x -> ObjectUtil.isNotEmpty(x.getTypeName()))
            .collect(Collectors.groupingBy(Book::getTypeName, Collectors.counting()));

    List<String> xAxis = new ArrayList<>();
    List<Long> yAxis = new ArrayList<>();
    if (CollectionUtil.isNotEmpty(collect)) {
        for (String key : collect.keySet()) {
            xAxis.add(key);
            yAxis.add(collect.get(key));
        }
    }
    Map<String, Object> map = new HashMap<>();
    map.put("xAxis", xAxis);
    map.put("yAxis", yAxis);
    return Result.success(map);
}

前端同上

const initBie=(data)=>{
  var chartDom = document.getElementById('bie');
  let myChart = echarts.init(chartDom);
    const option = {
      title: {
        text: '图书统计(饼图)',
        subtext: '统计维度:图书分类',
        left: 'center'
      },
      tooltip: {
        trigger: 'item'
      },
      legend: {
        orient: 'vertical',
        left: 'left'
      },
      series: [
        {
          name: 'Access From',
          type: 'pie',
          radius: '50%',
          data: data,
          emphasis: {
            itemStyle: {
              shadowBlur: 10,
              shadowOffsetX: 0,
              shadowColor: 'rgba(0, 0, 0, 0.5)'
            }
          }
        }
      ]
    };
    option && myChart.setOption(option);
};
const initBar=(xAxis, yAxis)=>{
  let chartDom = document.getElementById('bar');
  let myChart = echarts.init(chartDom);
  let option;

  option = {
    title: {
      text: '图书统计(柱状图)',
      subtext: '统计维度:图书分类',
      left: 'center'
    },
    xAxis: {
      type: 'category',
      data: xAxis
    },
    yAxis: {
      type: 'value'
    },
    series: [
      {
        data: yAxis,
        type: 'bar',
        showBackground: true,
        backgroundStyle: {
          color: 'rgba(180, 180, 180, 0.2)'
        }
      }
    ]
  };

  option && myChart.setOption(option);
};
const initEcharts=()=>{
  request.get("/book/echarts/bar").then(res => {
    if (res.code === '0') {
      // 开始去渲染柱状图数据啦
      initBar(res.data.xAxis, res.data.yAxis)
      // 开始去渲染折线图数据啦
      initLine(res.data.xAxis, res.data.yAxis)
    }
  })
};

16.富文本

1.首先下载并导入wangeditor,前端export之前初始化富文本:

npm i wangeditor --save
import E from 'wangeditor'
let editor
function initWangEditor(content) {	setTimeout(() => {
  if (!editor) {
    editor = new E('#editor')
    editor.config.placeholder = '请输入内容'
    editor.config.uploadFileName = 'file'
    editor.config.uploadImgServer = 'http://localhost:8181/api/files/wang/upload'
    editor.create()
  }
  editor.txt.html(content)
}, 0)
}

2.后端这里就是添加一列content,然后实体类也添加,然后一个富文本编辑器的文件上传功能,因为这里会有图片之类的文件

/**
 * wang-editor编辑器文件上传接口
 */
@PostMapping("/wang/upload")
public Map<String, Object> wangEditorUpload(MultipartFile file) {
    String flag = System.currentTimeMillis() + "";
    String fileName = file.getOriginalFilename();
    try {
        // 文件存储形式:时间戳-文件名
        FileUtil.writeBytes(file.getBytes(), filePath + flag + "-" + fileName);
        System.out.println(fileName + "--上传成功");
        Thread.sleep(1L);
    } catch (Exception e) {
        System.err.println(fileName + "--文件上传失败");
    }
    Map<String, Object> resMap = new HashMap<>();
    // wangEditor上传图片成功后, 需要返回的参数
    resMap.put("errno", 0);
    resMap.put("data", CollUtil.newArrayList(Dict.create().set("url", "http://localhost:8080/api/files/" + flag)));
    return resMap;
}

3.首先是在el-table里面添加一列按钮,列表是图书介绍,按钮显示点击查看。

 <el-table-column label="图书介绍">
  <template v-slot="scope">
    <el-button type="success" @click="viewEditor(scope.row.content)">点击查看</el-button>
  </template>
</el-table-column>

4.当点击查看时就显示一个dialogue,里面是图书介绍的html的渲染结果:

<el-dialog title="图书介绍" v-model="editorVisible" width="50%">
  <div v-html="this.viewData" class="w-e-text"></div>
</el-dialog>
const viewData=ref('');
const editorVisible=ref(false);
const viewEditor=(data)=> {
  viewData.value = data;
  editorVisible.value = true;
};

5.然后就是给add和eddit时的对话框添加富文本编辑器(id="editor"),提交form之前先给form里面添加content内容。

<el-form-item label="图书介绍" label-width="15%">
  <div id="editor" style="width: 90%"></div>
</el-form-item>
const add=()=>{
 form.value={};
  initWangEditor("");
  dialogFormVisible.value=true;
};
const edit=(obj)=>{
  form.value=obj;
  initWangEditor(obj.content ? form.value.content : "");
  dialogFormVisible.value=true;
}
const submit=()=>{
  form.value.content = editor.txt.html();
  request.post('book/addedit',form.value).then((res)=>{
    if (res.code==="0"){
      dialogFormVisible.value=false;
      findBySearch();
    }
  })
}

效果

在这里插入图片描述

17.工具

1.拖拉拽

低无代码项目经常用到的拖拉拽,这里主要是使用了组件vueDragglePlus:
官网:https://alfred-skyblue.gitee.io/vue-draggable-plus/
github:https://github.com/Alfred-Skyblue/vue-draggable-plus

2.Enum

public enum RoleEnum {
    // 管理员
    ADMIN,
    HOTEL,
    USER,
}
public enum StatusEnum {
    CHECKING("待审核"),
    CHECK_OK("审核通过"),
    CHECK_NO("审核不通过"),
    ;
    public String status;

    StatusEnum(String status) {
        this.status = status;
    }
}
public enum ResultCodeEnum {
    SUCCESS("200", "成功"),
    TIME_CHECK_ERROR("5007", "时间参数有误");
    
    public String code;
    public String msg;

    ResultCodeEnum(String code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

3.日期

1.获取今天周几

private String getTodayWeek(){
        LocalDate today=LocalDate.now();
        DayOfWeek dayOfWeek=today.getDayOfWeek();
        return dayOfWeek.getDisplayName(TextStyle.FULL_STANDALONE, Locale.CHINA);
    }

2.获取天数

private Long getDayNum(String inTime,String outTime) throws ParseException{
        DateFormat dft=new SimpleDateFormat("yyyy-MM-dd");
        Date star=dft.parse(inTime);//开始时间
        Date endDay=dft.parse(outTime);//结束时间
        int result = star.compareTo(endDay);
        if (result >= 0) {
            throw new CustomException(ResultCodeEnum.TIME_CHECK_ERROR);
        }
        Long starTime=star.getTime();
        Long endTime=endDay.getTime();
        long num=endTime-starTime;//时间戳相差的毫秒数
        return num/24/60/60/1000;
    }
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是小z呀

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值