文章目录
1 文件上传功能
需要使用到element-plus中的文件上传功能。
1.1 后台方面
需要设计一个服务器用来接收上传的文件信息。
在controller文件夹创建一个FileController类,用来实现文件的上传与下载功能。除此之外,需要在resources文件夹下创建一个files 文件用来存放上传的文件信息。
package com.example.controller;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.RandomUtil;
import cn.hutool.core.util.StrUtil;
import com.example.common.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URLEncoder;
import java.util.List;
@RestController
@RequestMapping("/files")
public class FileController {
//获取端口
@Value("${server.port}")
private String port;
private static final String ip = "http://localhost";
/**
* 文件上传
* @param file
* @return
* @throws IOException
*/
//采用post接口实现文件上传
@PostMapping("/upload")
// 当泛型中的内容不知道如何写的时候,使用 ? 代替
public Result<?> upload(MultipartFile file) throws IOException {
// file 用来接收前台传过来的file 对象
//1 获取源文件的名称
String originalFilename = file.getOriginalFilename();
// 1.1 由于在进行保存文件的时候,相同的文件名会存在覆盖问题,所以为了解决这个问题,在文件的前面加一个前缀,为了定义文件的唯一标识
// 1.1.1时间戳实现
// long l = System.currentTimeMillis();
// 1.1.2 使用 UID,生成一串不重复的字符串
String flag = IdUtil.fastSimpleUUID();
//2 将文件名存到 files 文件夹下
// 2.1 获取当前项目所在的路径 System.getProperty("user.dir")
// 2.2 获取到files的路径 绝对路径
String rootFilePath = System.getProperty("user.dir") + "/src/main/resources/files/" + flag + '_' + originalFilename;
// 3 使用工具类进行文件存入 originalFilename获取字节流
FileUtil.writeBytes(file.getBytes(),rootFilePath);
// 4 返回结果 url
return Result.success(ip + ":" +port + "/files/" + flag);
}
/**
* 文件下载
* @param flag
* @param response
*/
@GetMapping("/{flag}")
// 获取response 对象,将获取到的所有文件与当前传过来的文件进行对比
public void getFiles(@PathVariable String flag,HttpServletResponse response){
OutputStream os; // 新建一个输出流对象
String basePath = System.getProperty("user.dir") + "/src/main/resources/files/"; // 定于文件上传的根路径
List<String> fileNames = FileUtil.listFileNames(basePath); // 获取所有的文件名称
String fileName = fileNames.stream().filter(name -> name.contains(flag)).findAny().orElse(""); // 找到跟参数一致的文件
try {
if (StrUtil.isNotEmpty(fileName)) { //文件名称存在 下载文件
response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8"));
response.setContentType("application/octet-stream");
// 获取文件路径
byte[] bytes = FileUtil.readBytes(basePath + fileName); // 通过文件的路径读取文件字节流
os = response.getOutputStream(); // 通过输出流返回文件
os.write(bytes);
os.flush();
os.close();
}
} catch (Exception e) {
System.out.println("文件下载失败");
}
}
}
1.2 数据库表的修改
由于在后台取出图片需要使用一个地址,所以要新增一个字段用来储存地址信息。
同时也需要对entity文件夹下的book类进行修改:
//表名记得改
@TableName("book")
// 自动进行生成getter setter 方法
@Data
public class Book {
@TableId(type = IdType.AUTO)
private Integer id;
private String name;
private BigDecimal price;
private String author;
// @JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
//数据库中有下划线的,自动准换为驼峰写法
private Date createTime;
private String cover;
}
1.3 前端方面
需要在页面加上封面信息,所以要分别在table表单中和弹窗中添加封面信息,具体代码如下:
<template>
<div style="flex: 1; padding: 10px">
<div class="content">
<!-- 功能区域———新增按钮 -->
<div style="margin: 10px 0" class="content_left">
<el-button type="primary" @click="add">增加</el-button>
</div>
<!-- 搜索区域 -->
<div style="margin: 10px 0" class="content_right">
<el-input
v-model="search"
placeholder="请输入关键字"
style="width: 200px"
clearable
/>
<el-button type="primary" style="margin-left: 5px" @click="load"
>查询</el-button
>
</div>
</div>
<el-table :data="tableData" border style="width: 100%" stripe>
<!-- 在进行数据显示的时候,给日期加上排序 -->
<el-table-column prop="id" label="ID" sortable />
<el-table-column prop="name" label="书名" />
<el-table-column prop="price" label="单价" />
<el-table-column prop="author" label="作者" />
<el-table-column prop="createTime" label="出版时间" />
<el-table-column label="封面">
<template #default="scope">
<el-image
style="width: 100px; height: 100px"
:src="scope.row.cover"
:preview-src-list="[scope.row.cover]"
preview-teleported="true"
fit="fill"
/>
</template>
</el-table-column>
<el-table-column fixed="right" label="操作" width="150">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-popconfirm
title="确认删除吗?"
@confirm="handleDelete(scope.row.id)"
>
<template #reference>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="margin: 10px 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<el-dialog v-model="dialogVisible" title="提示" width="30%">
<el-form :model="form" label-width="120px">
<el-form-item label="书名" style="width: 80%">
<el-input v-model="form.name" />
</el-form-item>
<el-form-item label="单价" style="width: 80%">
<el-input v-model="form.price" />
</el-form-item>
<el-form-item label="作者" style="width: 80%">
<el-input v-model="form.author" />
</el-form-item>
<el-form-item label="出版时间" style="width: 80%">
<el-date-picker
v-model="form.createTime"
type="date"
clearable
value-format="YYYY-MM-DD"
/>
</el-form-item>
<el-form-item label="封面" style="width: 80%">
<!-- on-success 文件上传成功时的钩子 -->
<el-upload
ref="upload"
action="http://localhost:9090/files/upload"
:on-success="filesUploadSuccess"
>
<el-button type="primary">点击上传</el-button>
</el-upload>
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<!-- 点击之后,将数据保存到后台,显示在页面上面 -->
<el-button type="primary" @click="save"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
同时设置一个钩子函数,当文件成功上传之后触发的函数,通过 ** :on-success=“filesUploadSuccess”** 进行实现。主要进行修改的是添加和编辑功能,在文件上传成功之后要将原先上传记录给删除,其中需要注意的,在进行编辑操作的时候,当点击 操作 按钮的时候,可能存在当前dom结点不存在的问题,所以需要进行使用** this.$nextTick**来解决这个问题。
,
<script>
import request from "@/utils/request";
export default {
name: "Book",
components: {},
data() {
return {
form: {},
dialogVisible: false,
currentPage: 1,
pageSize: 10,
total: 0,
search: "",
// 从后台进行获取数据
tableData: [],
flag: false,
};
},
created() {
this.load();
},
// updated() {
// this.load();
// },
methods: {
// 编辑操作
handleEdit(row) {
// console.log(row);
/** 此时可以拿到当前行的数据,然后将拿到的数据,传入到form中进行显示,为了避免v-model
* 的影响,当在表格中进行修改数据的时候,会导致form中的数据,当点击取消之后,会影响原先的数据
* 所以采用深拷贝的方式进行赋值
*/
this.form = JSON.parse(JSON.stringify(row));
// 打开弹窗
this.dialogVisible = true;
// 解决dom 不存在的问题。
this.$nextTick(() => {
this.$refs["upload"].clearFiles();
});
},
// 改变页面个数触发函数
handleSizeChange() {
this.load();
},
// 改变当前页码
handleCurrentChange() {
this.load();
},
// 当点击添加按钮之后,会显示一个弹窗
add() {
// 打开弹窗
this.dialogVisible = true;
// 同时要进行清空表单里面的内容
this.form = {};
this.$refs["upload"].clearFiles();
},
//查询数据库中的数据,由于需要多次进行使用,所以先进行一下封装
// 此时的方法并没有进行调用,所以可以再页面一进行加载,就调用
load() {
// 在进行后端设计的时候,get代表查询
// get不能直接使用对象进行传参
request
.get("/book", {
params: {
// 需要进行传递参数
pageNum: this.currentPage,
pageSize: this.pageSize,
search: this.search,
},
})
.then((res) => {
// console.log(res);
this.tableData = res.data.records;
// 总条数
this.total = res.data.total;
});
},
// 将数据保存到后台
save() {
// 通过 form表单中id进行判断,如果有id,表示为更新,否则为新增
if (this.form.id) {
// 更新 put
request.put("/book", this.form).then((res) => {
// console.log(res);
if (res.code === "0") {
this.$message({
type: "success",
message: "更新成功",
});
} else {
this.$message({
type: "error",
message: "更新失败",
});
}
// 判断之后,需要进行弹窗的关闭和页面的重新渲染
this.load();
this.dialogVisible = false;
});
} else {
// 新增
// 此时路径不能使用/api/book,要不然会自动拼接
request.post("/book", this.form).then((res) => {
// console.log(res);
//成功之后的提示
if (res.code === "0") {
this.$message({
type: "success",
message: "新增成功",
});
} else {
this.$message({
type: "error",
message: "新增失败",
});
} // 判断之后,需要进行弹窗的关闭和页面的重新渲染
this.load();
this.dialogVisible = false;
});
}
},
// 删除当前数据
handleDelete(id) {
// console.log(id);
// 通过字符串拼接的方式,将id传给后端userMapper.deleteById(id);中进行删除
request.delete("/book/" + id).then((res) => {
if (res.code === "0") {
this.$message({
type: "success",
message: "删除成功",
});
} else {
this.$message({
type: "error",
message: "删除失败",
});
}
this.load(); //重新进行表格数据的渲染
});
},
// 文件上传成功之后的操作
filesUploadSuccess(res) {
// res 接收的是response ,从里面获取url
console.log(res.data);
this.form.cover = res.data;
},
},
};
</script>
1.4 后端跨域问题
虽然在前端已经解决跨域问题,但是在后端进行一下配置。在common文件夹下创建一个CorsConfig类,用来设置跨域方面的信息,具体代码如下:
package com.example.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 {
// 当前跨域请求最大有效时长。这里默认1天
private static final long MAX_AGE = 24 * 60 * 60;
private CorsConfiguration buildConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*"); // 1 设置访问源地址
corsConfiguration.addAllowedHeader("*"); // 2 设置访问源请求头
corsConfiguration.addAllowedMethod("*"); // 3 设置访问源请求方法
corsConfiguration.setMaxAge(MAX_AGE);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", buildConfig()); // 4 对接口配置跨域设置
return new CorsFilter(source);
}
}
2 富文本编辑器
2.1 使用方法
官网
在项目中进行安装依赖:
npm install @wangeditor/editor --save
2.2 在项目中的具体应用。
2.2.1 创建news表
2.2.2 创建实体类News
在entity文件夹下创建News类:
//表名记得改
@TableName("news")
// 自动进行生成getter setter 方法
@Data
public class News {
@TableId(type = IdType.AUTO)
private Integer id;
private String title;
private String content;
private String author;
@JsonFormat(pattern = "yyyy-MM-dd",timezone = "GMT+8")
private Date time;
}
2.2.2 创建接口NewsMapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.example.entity.News;
public interface NewsMapper extends BaseMapper<News> {
}
2.2.3 创建Newscontroller
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.example.common.Result;
import com.example.entity.News;
import com.example.mapper.NewsMapper;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@RestController
@RequestMapping("/news")
public class NewsController {
@Resource
NewsMapper newsMapper;
// 新增
@PostMapping
// Result<?>表示接收任何类型的数据
public Result<?> save(@RequestBody News news){
// 插入数据库,在里面进行使用
newsMapper.insert(news);
return Result.success();
}
// 查询,使用的是分页查询
// 在进行网页查询的时候,需要必须传入三个参数pageNum pageSize search
@GetMapping
// 当前页pageNum 默认从 1 开始,,每页条数 pageSize 默认从 10 开始,,查询关键字 默认为空
public Result<?> findPage(@RequestParam(defaultValue = "1") Integer pageNum,@RequestParam(defaultValue = "10") Integer pageSize,@RequestParam(defaultValue = "") String search){
LambdaQueryWrapper<News> wrapper = Wrappers.<News>lambdaQuery();
if(StrUtil.isNotBlank(search)){
wrapper.like(News::getTitle,search);
}
Page<News> newsPage = newsMapper.selectPage(new Page<>(pageNum,pageSize),wrapper);
return Result.success(newsPage);
}
// 更新 使用PutMapping接口
@PutMapping
// Result<?>表示接收任何类型的数据
public Result<?> update(@RequestBody News news){
// 根据id进行更新
newsMapper.updateById(news);
return Result.success();
}
// 删除 使用DeleteMapping接口
@DeleteMapping("/{id}")
// "/{id}" 占位符是方式 需要通过PathVariable注解进行获取参数
public Result<?> delete(@PathVariable Long id){
// 根据id进行删除
newsMapper.deleteById(id);
return Result.success();
}
}
2.2.4 创建News.vue组件
<template>
<div style="flex: 1; padding: 10px">
<div class="content">
<!-- 功能区域———新增按钮 -->
<div style="margin: 10px 0" class="content_left">
<el-button type="primary" @click="add">增加</el-button>
</div>
<!-- 搜索区域 -->
<div style="margin: 10px 0" class="content_right">
<el-input
v-model="search"
placeholder="请输入关键字"
style="width: 200px"
clearable
/>
<el-button type="primary" style="margin-left: 5px" @click="load"
>查询</el-button
>
</div>
</div>
<el-table :data="tableData" border style="width: 100%" stripe>
<!-- 在进行数据显示的时候,给日期加上排序 -->
<el-table-column prop="id" label="ID" sortable />
<el-table-column prop="title" label="标题" />
<el-table-column prop="author" label="作者" />
<el-table-column prop="time" label="发表时间" />
<el-table-column fixed="right" label="操作" width="150">
<template #default="scope">
<el-button size="small" @click="handleEdit(scope.row)"
>编辑</el-button
>
<el-popconfirm
title="确认删除吗?"
@confirm="handleDelete(scope.row.id)"
>
<template #reference>
<el-button type="danger" size="small">删除</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div style="margin: 10px 0">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:page-sizes="[5, 10, 20]"
layout="total, sizes, prev, pager, next, jumper"
:total="total"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
<!-- 对话弹出框 -->
<el-dialog v-model="dialogVisible" title="提示" width="50%">
<!-- 定义一个表单,将要进行添加的信息显示在此位置 -->
<!-- <span>这是一段信息</span> -->
<!-- 此时需要进行绑定一个变量form -->
<el-form :model="form" label-width="120px">
<el-form-item label="标题" style="width: 50%">
<el-input v-model="form.title" />
</el-form-item>
<!-- <el-form-item label="内容" style="width: 80%">
<el-input v-model="form.content" />
</el-form-item> -->
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="dialogVisible = false">取消</el-button>
<!-- 点击之后,将数据保存到后台,显示在页面上面 -->
<el-button type="primary" @click="save"> 确定 </el-button>
</span>
</template>
</el-dialog>
</div>
</div>
</template>
2.2.5 创建 /news 路由
{
// 头部加上侧边栏
path: '/',
name: 'Layout',
// 重定向功能,当访问 / 时,会自动的访问 /home 的页面
redirect: '/user',
component: Layout,
// 进行路由的嵌套 主体区域
children:[
{
path: '/user',
name: 'User',
component: ()=>import("@/views/User"),
},
{
path: '/person',
name: 'Person',
component: ()=>import("@/views/Person"),
},
{
path: '/book',
name: 'Book',
component: ()=>import("@/views/Book"),
},
{
path: '/news',
name: 'News',
component: ()=>import("@/views/News"),
}
]
},
2.2.6 在侧边栏进行加入
<template>
<div>
<!-- el-menu 存在一个属性 router ,
功能是,当进行点击里面的内容时,会自动进行跳转
会根据index中的路由内容进行跳转
-->
<!-- default-active="user" 进行控制高亮 -->
<el-menu
:default-active="path"
class="el-menu-vertical-demo"
router
style="width: 200px; min-height: calc(100vh - 50px)"
>
<el-sub-menu index="1">
<template #title>
<span>系统管理</span>
</template>
<el-menu-item index="/user">用户管理</el-menu-item>
</el-sub-menu>
<!-- index 中的值与路由名称相对应 -->
<el-menu-item index="/book">图书管理</el-menu-item>
<el-menu-item index="/news">新闻管理</el-menu-item>
<!-- <el-menu-item index="date" :route="{ path: '/' }">数据管理</el-menu-item> -->
</el-menu>
</div>
</template>
2.2.7 富文本编辑器的使用
在进行安装依赖的时候,由于安装的是wangEditor5版本,与wangEditor4版本使用有很大的不同,同时由于是使用vue3进行项目的编写,所以除了安装wangEditor5版本依赖之外,还需要安装vue3的依赖,代码如下:
yarn add @wangeditor/editor
# 或者 npm install @wangeditor/editor --save
yarn add @wangeditor/editor-for-vue@next
# 或者 npm install @wangeditor/editor-for-vue@next --save
在项目中的使用:
<div>
<Toolbar
style="border-bottom: 1px solid #ccc"
:editor="editorRef"
/>
<Editor
style="height: 500px; overflow-y: hidden"
v-model="valueHtml"
@onCreated="handleCreated"
/>
</div>
<script>
import request from "@/utils/request";
import { Editor, Toolbar } from "@wangeditor/editor-for-vue";
import "@wangeditor/editor/dist/css/style.css"; // 引入 css
import { shallowRef } from "vue";
export default {
name: "News",
components: {
Editor,
Toolbar,
},
data() {
return {
form: {},
editorRef: shallowRef(),
valueHtml: "",
};
},
methods(){
save() {
// 当进行保存的时候,获取到富文本框中的内容,然后将获取的内容保存到form 中的content中
// console.log(this.valueHtml);
this.form.content = this.valueHtml;
// 通过 form表单中id进行判断,如果有id,表示为更新,否则为新增
if (this.form.id) {
// 更新 put
request.put("/news", this.form).then((res) => {
// console.log(res);
if (res.code === "0") {
this.$message({
type: "success",
message: "更新成功",
});
} else {
this.$message({
type: "error",
message: "更新失败",
});
}
// 判断之后,需要进行弹窗的关闭和页面的重新渲染
this.load();
this.dialogVisible = false;
});
}
}
2.2.8 本地图片上传功能
官网图片上传的步骤:图片上传
上传是两个需要注意的点:
(1) 服务端地址,必填,否则上传图片会报错,里面的地址就是在后端进行设置的地址,里面存在一个属性
fieldName,里面的值要跟后端的参数一样。
data(){
return {
editorConfig: {
MENU_CONF: {
// 图片上传
uploadImage: {
server: "http://localhost:9090/files/editor/upload",
// 需要与后台的接收的名字相同,要不然会报 500 的错误
// 设置上传参数名称
fieldName: "file",
},
},
},
}
}
(2) 服务端 response body 格式要求如下:
/**
* 富文本编译器的 文件上传
* @param file
* @return
* @throws IOException
*/
//采用post接口实现文件上传
@PostMapping("/editor/upload")
public JSON editorUpload(MultipartFile file) throws IOException {
// file 用来接收前台传过来的file 对象
//1 获取源文件的名称
String originalFilename = file.getOriginalFilename();
String flag = IdUtil.fastSimpleUUID();
//2 将文件名存到 files 文件夹下
// 2.1 获取当前项目所在的路径 System.getProperty("user.dir")
// 2.2 获取到files的路径 绝对路径
String rootFilePath = System.getProperty("user.dir") + "/src/main/resources/files/" + flag + '_' + originalFilename;
// 3 使用工具类进行文件存入 originalFilename获取字节流
FileUtil.writeBytes(file.getBytes(),rootFilePath);
// 4 返回结果 url
String url = ip + ":" +port + "/files/" + flag;
// 5 此时需要自定义一个对象 json
JSONObject json = new JSONObject();
json.set("errno",0);
JSONObject data = new JSONObject();
json.set("data",data);
data.set("url",url);
return json;
}