springboot_vue知识点

代码放到了仓库。

springboot_vue知识点

  • 1.搭建
    • 1.vue
    • 2.springboot
  • 2.前后端请求和响应的封装
    • 1.请求封装
    • 2.响应封装
  • 3.增删改查
    • 1.查询
    • 2.分页
    • 3.新增和编辑
    • 4.删除
  • 4.跨域和自定义异常
  • 5.JWT鉴权
    • 1.配置pom
    • 2.拦截前端请求的拦截器
    • 3.生成token并验证token
    • 4.登录后生成token
    • 5.前端获取token然后每次请求时header带着token
    • 6.后端jwt拦截器
    • 7.使用jwt拦截器拦截前端请求
  • 6.文件的上传下载
    • 1.上传
    • 2.下载
  • 7.批量删除
  • 8.数据库导入导出excel文件
    • 1.导出
    • 2.导入
  • 9.模块关联
    • 1.service映射
    • 2.mapper关联
  • 10.角色管理
  • 11.审批功能
  • 12.预约功能
  • 13.AOP日志管理
    • 1.依赖
    • 2.自定义注解
    • 3.AOP切面处理
    • 4.在controller的方法里面使用自定义的注解
  • 14.图形验证码
    • 1.依赖
    • 2.定义Mapper映射格式
    • 3.生成验证码的控制器
    • 4.登陆页面的key和验证码请求
    • 5.后端登录的验证
  • 15.Echarts
    • 1.饼状图
    • 2.折线图和柱状图
  • 16.富文本
  • 效果

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();
    }
  })
}

效果

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/180498.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

centos7 系统keepalived 定时执行脚本

安装keepalived yum install -y keepalived 修改配置文件 配置文件路径 /etc/keepalived 配置文件内容 global_defs {router_id localhost.localdomain # 访问到主机&#xff0c;本机的hostname&#xff0c;需要修改 }vrrp_script chk_http_port {script "/etc/kee…

微信小程序使用腾讯地图实现地点搜索并且随着地图的滑动加载滑动到区域的地点,本文地点使用医院关键词作为搜索地点

实现效果如下 1.页面加载时&#xff0c;根据getLocation方法获取用户当前经纬度获取20条医院位置信息 2.页面滑动时&#xff0c;根据滑动到的经纬度再次获取20条医院位置信息 获取到的医院位置信息 实现方法如下 1.在.wxml中添加触发滑动的方法bindregiοnchange“onMapRegio…

数组对象判重最佳实践

数组对象判重最佳实践 赶紧挣钱&#xff0c;回家过年… 1.问题回顾 deviceSelectedRow(row) {this.ElectricalPartList.push(row)}&#xff0c;在此方法中&#xff0c;ElectricalPartList需要多次push进去数据&#xff0c;但是row可能存在重复&#xff0c;如何判重呢&#xff…

thingsboard3.6的mailConfigTemplateController错误

1、bug内容 使用3.6版本的tb代码进行打包生成boot的jar包,在启动的时候会报错mailConfigTemplateController bean初始化找不到文件路径。 Error creating bean with name mailConfigTemplateController defined in URL [jar:file:/D:/yuxinwei/AE/thingsboard/thingsboard-3…

PMP对项目工程师有用吗?

一、什么是项目工程师&#xff1f; 项目工程师是指在各个领域负责技术操作、设计、管理以及评估能力的人员。他们通常担当项目的实施和执行角色&#xff0c;在开发或控制类项目中发挥重要作用。有时&#xff0c;项目工程师的称号还可以用来表示在某个领域取得专业资格的人员。…

前端js语音朗读文本

<!DOCTYPE html> <html lang"zh"><head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>语音朗读</title></head><body>&l…

写出漂亮代码的45个小技巧

不知道大家有没有经历过维护一个已经离职的人的代码的痛苦&#xff0c;一个方法写老长&#xff0c;还有很多的if else &#xff0c;根本无法阅读&#xff0c;更不知道代码背后的含义&#xff0c;最重要的是没有人可以问&#xff0c;此时只能心里默默地问候这个留坑的兄弟。。 …

网络运维与网络安全 学习笔记2023.11.23

网络运维与网络安全 学习笔记 第二十四天 今日目标 VRRP负载均衡、BFD原理与配置、BFD典型应用 DHCP工作原理、全局模式DHCP VRRP负载均衡 VRRP单组缺陷 每网段存在一个VRRP组&#xff0c;缺点如下&#xff1a; 主网关数据转发压力大 备份网关不转发任何数据 网络设备利用…

【KMP算法】学习总结

说明&#xff1a; 文章内容为对KMP算法的总结&#xff0c;以及力扣例题&#xff1b;文章内容为个人的学习总结&#xff0c;如有错误&#xff0c;欢迎指正。 文章目录 1. KMP算法1.1 算法步骤1.2 关于指针回退问题 2 . LeetCode例题 1. KMP算法 1.1 算法步骤 KMP算法通常用于…

INFLOW:用于检测隐藏服务器的反向网络流水印

文章信息 论文题目&#xff1a;INFLOW: Inverse Network Flow Watermarking for Detecting Hidden Servers 期刊&#xff08;会议&#xff09;&#xff1a;IEEE INFOCOM 2018 - IEEE Conference on Computer Communications 级别&#xff1a;CCF A 文章链接&#xff1a;https:…

ajax请求方式处理

1、前置准备 1.1、SpringBoot项目下&#xff1a;写一个controller RestController public class TestController {RequestMapping("/yyy")public void test(HttpServletRequest request, HttpServletResponse response){String yang request.getParameter("y…

示波器探头讲解及案例分享

示波器探头讲解 示波器探头 分为X1、X10档&#xff1a; X1档&#xff0c;表示被测量的信号没有经过衰减进入示波器 X10档&#xff0c;表示被测量的信号衰减10倍进入示波器&#xff08;当示波器也设置为10X档&#xff0c;直接读数即可&#xff0c;但是当示波器设置为1X档&…

Java【XML 配置文件解析】

前言 最近考试周忙得要死&#xff0c;但我却不紧不慢&#xff0c;还有三天复习时间&#xff0c;考试科目几乎都还没学呢。今天更新一个算是工具类-XML文件的解析&#xff0c;感觉还是挺有用的&#xff0c;之后可以融进自己的项目里。 XML 配置文件解析 0、导入依赖 有点像我…

Spark---基于Standalone模式提交任务

Standalone模式两种提交任务方式 一、Standalone-client提交任务方式 1、提交命令 ./spark-submit --master spark://mynode1:7077 --class org.apache.spark.examples.SparkPi ../examples/jars/spark-examples_2.11-2.3.1.jar 100 或者 ./spark-submit --master spark…

Python爬取京东商品销售数据进行数据分析示例代码,以口红为例

文章目录 一、准备工作驱动安装模块使用与介绍 二、流程解析三、完整代码四、效果展示关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资…

idea手动导入maven包

当maven仓库中没有包时&#xff0c;我们需要手动导入jar到maven项目中 1.这里的maven设置成你自己安装的maven 2.查看pom.xml文件中maven&#xff0c;以下面为例 <dependency><groupId>com.jdd.pay</groupId><artifactId>mapi-sdk-v3</artifactId&…

数字人直播系统开发要注意的陷阱

数字人做为元宇宙的底层基座&#xff0c;BAT都在跑步进场&#xff0c;目前具有前瞻性的公司都在布局数字人产业。数字人可以应用于很多业务场景&#xff0c;对今年来说&#xff0c;无疑数字人直播系统是最火的。像去年数字人直播SAAS系统定制开发的话没有个百把万是下不来的。但…

Python教程73:Pandas中一维数组Series学习

创建一维数据类型Series dataNone 要转化为Series的数据(也可用dict直接设置行索引) 若是标量则必须设置索引,该值会重复,来匹配索引的长度 indexNone 设置行索引 dtypeNone 设置数据类型(使用numpy数据类型) nameNone 设置Series的name属性 copyFalse 不复制 (当data为ndarray…

云端导览,数字互动 | 拓世法宝AI数字人一体机助力全新旅游时代

《中国旅行消费趋势洞察白皮书&#xff08;2023版&#xff09;》显示&#xff0c;消费者旅行习惯已从“到此一游”变为“深度在地”&#xff0c;更强调在旅游中充实自我、学习新知识。 &#xff08;《中国旅行消费趋势洞察白皮书&#xff08;2023版》截图&#xff09; 从这些资…

Navicat 技术指引 | 适用于 GaussDB 的用户权限设置

Navicat Premium&#xff08;16.2.8 Windows版或以上&#xff09; 已支持对 GaussDB 主备版的管理和开发功能。它不仅具备轻松、便捷的可视化数据查看和编辑功能&#xff0c;还提供强大的高阶功能&#xff08;如模型、结构同步、协同合作、数据迁移等&#xff09;&#xff0c;这…