代码放到了仓库。
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();
}
})
}