此次实现是基于SpringBoot、vue3、elementplus。
目录
1、上传文件前端组件
<el-form-item label="上传文件">
<el-upload
ref="uploadRef"
:action="uploadUrl"
:before-upload="beforeUpload"
:on-success="handleSuccess"
:on-error="handleError"
:limit="1"
:show-file-list="false"
:on-change="handleFileChange"
>
<template #trigger>
<el-button type="primary">点击选择PDF文件</el-button>
</template>
<template #tip>
<div class="el-upload__tip">
PDF格式,小于2M。
</div>
</template>
</el-upload>
</el-form-item>
1.1
ref="uploadRef"上传组件的引用
在上传成功后,清空文件列表上次上传的文件时用到
1.2
:action上传的url地址,
组件里面:
uploadUrl: this.$uploadUrl,//上传文件的url,在环境变量里面配置,生产环境和开发环境的地址不一样
main.js:
app.config.globalProperties.$uploadUrl = import.meta.env.VITE_UPLOAD_URL;
.env.development:
VITE_UPLOAD_URL=http://localhost:8083/api/files/upload
#后端文件上传接口
.env.production:
VITE_UPLOAD_URL=/apis/api/files/up
1.3
:before-upload上传前的处理方法,主要用来验证文件格式和文件大小限制
// 在上传之前进行的检查,包括文件大小和格式,返回值:false阻止上传,true放行
beforeUpload() {
console.log(this.fileList[0])//输出查看文件的类型和文件大小如何定义的
const isPDF = this.fileList[0].raw.type === 'application/pdf';
const isLessThan2M = this.fileList[0].size / 1024 / 1024 < 2;
if (!isPDF) {
messageTip('请上传PDF格式的文件', 'error');
return false;
}
if (!isLessThan2M) {
messageTip('文件大小不能超过2M', 'error');
return false;
}
return true;
},
1.4
:on-success 上传成功的处理方法
// 处理上传成功的回调
handleSuccess(response, file) {
messageTip('文件上传成功', 'success')
//当文件上传成功后,清空选择的文件,以便下次上传
this.$refs.uploadRef.clearFiles();
if (this.dialogQua) {
//资质名称
this.QQualification.qualificationName = response.data.filename;
this.QQualification.qualificationStoragePath = response.data.uploadFile;
} else if (this.dialogEditChar) {
this.QChargeItem.proofMaterialPath = response.data.uploadFile;
}
},
1.5
:on-error上传失败的处理方法
// 处理上传失败的回调
handleError(error, file) {
messageTip('文件上传失败', 'error');
// 可以在这里对上传失败的原因进行分析和处理,比如记录错误日志等
},
1.6
:limit限制文件的上传数量
:limit="1" 单个文件上传
1.7
:show-file-list是否已经上传的文件列表
:show-file-list="false" 不在组件下面显示已经上传的文件列表
1.8
:on-change文件状态改变时的钩子,添加文件、上传成功和上传失败时都会被调用
//选择框改变时,拿到要上传的文件列表
handleFileChange(file, fileList) {
this.fileList = fileList;
},
2 、文件上传后端处理
2.1controller
//处理文件上传
@PostMapping(value = "/upload")
public R upload(@RequestParam(value = "file") MultipartFile file) {
//真正的文件上传方法
String uploadFile = fileUploadDownloadUtils.uploadFile(file);
//uploadFile:D://pointProject/uploads/2024/11/22/43979372.pdf
//获取文件原来的名称,不包含扩展名
String originalFilename = file.getOriginalFilename();
int indexOf = originalFilename.lastIndexOf(".");
String filename =originalFilename.substring(0,indexOf);
Map<String, String> map = new HashMap<>();
map.put("filename", filename);
map.put("uploadFile", uploadFile);
return R.OK(map);
}
2.2fileUploadDownloadUtils
//自定义一个文件上传路径
private static final String UPLOAD_DIR = "D:/pointProject/uploads";
// 真正的文件上传方法
public String uploadFile(MultipartFile file) {
if (file == null || file.isEmpty()) {
throw new IllegalArgumentException("上传文件不能为空");
}
try {
//获取文件上传的全路径,单独写一个方法
String filePath = generateFilePath(file);
int indexOf = filePath.lastIndexOf("/");
//上传位置的目录字符串
String fileDir = filePath.substring(0,indexOf );
//文件的全路径,保存文件使用
Path path = Paths.get(filePath);
//文件存放目录的路径,创建目录使用
Path dir=Paths.get(fileDir);
//如果文件目录不存在,就创建文件目录
if (!Files.exists(dir)) {
try {
Files.createDirectories(dir);
System.out.println("目录 " + fileDir + " 不存在,已成功创建。");
} catch (IOException e) {
System.out.println("创建目录 " + fileDir + " 时出错:" + e.getMessage());
}
} else {
System.out.println("目录 " + fileDir + " 已经存在。");
}
//保存上传的文件
file.transferTo(path);
return filePath;
} catch (IOException e) {
throw new RuntimeException("文件上传失败", e);
}
}
// 生成文件存储路径,格式为:年/月/日/时间戳+原来文件扩展名
private String generateFilePath(MultipartFile file) {
LocalDateTime now = LocalDateTime.now();
//日期格式
DateTimeFormatter yearFormatter = DateTimeFormatter.ofPattern("yyyy");
DateTimeFormatter monthFormatter = DateTimeFormatter.ofPattern("MM");
DateTimeFormatter dayFormatter = DateTimeFormatter.ofPattern("dd");
//获取文件名
String originalFileName = file.getOriginalFilename();
//获取文件扩展名
String fileExtension = StringUtils.getFilenameExtension(originalFileName);
//获取时间戳,用来生成唯一的文件名
long timestamp = new Date().getTime();
return UPLOAD_DIR + "/" + now.format(yearFormatter) + "/" +
now.format(monthFormatter) + "/" + now.format(dayFormatter) +
"/" + timestamp + "." + fileExtension;
}
3、PDF文件预览前端组件
3.1定义一个用于展示预览的抽屉
<!-- PDF预览抽屉-->
<el-drawer v-model="drawer" size="45%" :show-close="false">
<template #header>
<h4>PDF预览</h4>
<el-button @click="downloadPDF" type="primary">下载</el-button>
</template>
<template #default>
<!-- 这个div用来挂载需要预览的PDF-->
<div id="pdfContainer"></div>
</template>
<template #footer>
<div style="flex: auto">
<el-button @click="cancelClick">关闭</el-button>
</div>
</template>
</el-drawer>
3.2定义一个调用的组件
<el-table-column label="查看资质" width="140">
<template #default="scope">
<div v-if="scope.row.qualificationStoragePath">
<el-button @click="PDFView(scope.row.qualificationStoragePath)"> 点击预览</el-button>
</div>
<div v-else>-</div>
</template>
</el-table-column>
3.3PDF预览方法
//PDF预览
PDFView(path) {
//显示抽屉
this.drawer = true;
this.pdfPath = path;//为下载提供路径
if (this.drawer) {
if (this.pdfPath) {
axios.get(`/api/files/download?path=${path}`, {responseType: "arraybuffer"})
.then(resp => {
//将后端返回的数据转换成一个前端的pdfjs能够使用的url
const url = window.URL.createObjectURL(new Blob([resp.data], {type: 'application/pdf'}));
// 使用pdfjsLib的getDocument方法创建一个加载PDF文件的任务
const loadingTask = pdfjsLib.getDocument(url);
// 当加载任务成功完成后,会得到一个PDF对象(pdf),这里通过promise.then来处理加载成功的情况
loadingTask.promise.then(pdf => {
//先加载所有页面,防止页面顺序乱了,
//定义一个数组,用来存储全部页面
let pagePromises = [];
// 获取PDF文件的总页数
let pageNum = pdf.numPages;
// 循环遍历每一页,从第一页(pageNumber = 1)开始,直到总页数(pageNum)
for (let pageNumber = 1; pageNumber <= pageNum; pageNumber++) {
pagePromises.push(pdf.getPage(pageNumber));
}
//返回所有页面
return Promise.all(pagePromises);
}).then(pages => {//开始渲染处理
//循环全部页面
pages.forEach(page => {
const scale = 1; // 可调整缩放比例
// 根据设置的缩放比例获取页面的视图参数,用于后续在canvas上正确显示页面内容
const viewport = page.getViewport({scale});
// 创建一个用于渲染PDF页面的canvas元素
const canvas = document.createElement('canvas');
// 获取canvas的2D绘图上下文,用于在canvas上绘制内容
const context = canvas.getContext('2d');
// 设置canvas的固定高度和宽度,这里统一设置,不再根据页面视图大小动态设置
canvas.height = 800;
canvas.width = 600;
// 创建一个渲染上下文对象,包含了要在哪个canvas上下文进行渲染以及页面的视图参数
const renderContext = {
canvasContext: context,
viewport: viewport
};
// 使用页面的render方法,将页面内容按照渲染上下文的设置渲染到canvas上
page.render(renderContext);
// 获取用于放置所有渲染后的PDF页面的容器元素,id为'pdfContainer'
const pdfContainer = document.getElementById('pdfContainer');
// 将渲染好的canvas元素添加到pdfContainer中,这样就可以在页面上显示出来了
pdfContainer.appendChild(canvas);
});
});
}).catch(error => {
console.log(error)
})
}
}
},
3.4关闭抽屉时清空内容
//关闭抽屉
cancelClick() {
this.drawer = false;
//清空PDF
const container = document.getElementById('pdfContainer');
container.innerHTML = '';
},
4、前端下载PDF
//下载pdf
downloadPDF() {
if (this.drawer) {
let path = this.pdfPath;
axios.get(`/api/files/download?path=${path}`, {responseType: "arraybuffer"})
.then(resp => {
//获取文件名
let fileName = path.substring(path.lastIndexOf("/") + 1);
// 同时指定该Blob对象的MIME类型为'application/octet-stream',表示它是一个字节流形式的数据,可用于传输各种二进制文件
const blob = new Blob([resp.data], {type: 'application/octet-stream'});
// 在文档对象模型(DOM)中创建一个<a>标签元素,通常用于创建超链接
const link = document.createElement('a');
// 设置<a>标签的'download'属性,拼接后再加上".pdf"组成的字符串
// 这样设置后,当用户点击这个链接进行下载时,下载的文件名将以此字符串命名
link.setAttribute('download', fileName);
// 通过window.URL.createObjectURL方法为创建的Blob对象生成一个临时的可访问的URL地址,并将这个地址设置为<a>标签的'href'属性
// 使得该<a>标签指向这个由Blob对象生成的临时URL,以便后续能够通过点击这个链接来访问和下载Blob对象所代表的文件内容
link.href = window.URL.createObjectURL(blob);
// 将创建好的<a>标签元素添加到文档的<body>部分,使其成为页面DOM结构中的一部分
// 这样用户在浏览器页面中就能看到这个下载链接(虽然可能看不到具体的样式,具体样式可根据后续的CSS设置来展现)
document.body.appendChild(link);
// 模拟用户点击刚刚添加到页面中的<a>标签,触发下载操作
// 当点击这个链接时,浏览器会根据之前设置的'download'属性值来命名下载的文件,并从通过'href'属性指定的临时URL地址获取文件内容进行下载
link.click();
messageTip("下载成功", "success");
}).catch(err => {
messageTip(err, "error")
})
}
},
5、后端提供的下载接口
无论前端是预览还是下载,对于后端来说都是下载接口。用同一个controller解决。
//处理文件下载 这里前端传过来完整的路径,也可以根据ID从数据库中查询
@GetMapping("/download")
public ResponseEntity<FileSystemResource> download(@RequestParam("path") String path) {
File file = new File(path);
if (!file.exists()) {
//没找到文件
return ResponseEntity.notFound().build();
}
String fileName = path.substring(path.lastIndexOf("/") + 1);
//设置返回的头
HttpHeaders headers = new HttpHeaders();
headers.add("Cache-Control", "no-cache, no-store, must-revalidate");
headers.add("Pragma", "no-cache");
headers.add("Expires", "0");
headers.add("Content-Disposition", "attachment; filename=" + fileName);
//构建文件资源
FileSystemResource resource = new FileSystemResource(file);
return ResponseEntity.ok()
.headers(headers)
.contentLength(file.length())
.contentType(MediaType.APPLICATION_OCTET_STREAM)
.body(resource);
}
6、定时删除冗余的文件
6.1清理方法
// 定时清理冗余文件方法(这里在DataTask定时任务中调用此方法)
public void cleanRedundantFiles() {
//查询相关数据库表中的文件路径,集中到一个集合中,文件路径集合1
//查询出来有空串,null,过滤一下
//解决一下文件路径斜杠不一致的问题
//查询数据库中表1中包含的文件路径
List<String> charPaths=qChargeItemsDao.findAllPath().stream().filter(path->{
return StringUtils.hasText(path);
}).map(path->{
return path.replace("/",File.separator);
}).collect(Collectors.toList());
//查询数据库中表2中包含的文件路径
List<String> qualificationPaths=qQualificationsDao.findAllPath().stream().filter(path->{
return StringUtils.hasText(path);
}).map(path->{
return path.replace("/",File.separator);
}).collect(Collectors.toList());
//合并
List<String> allDbPaths = new ArrayList<>();
allDbPaths.addAll(charPaths);
allDbPaths.addAll(qualificationPaths);
//编写一个递归遍历指定文件夹下的所有文件路径,并返回一个文件路径的集合,文件路径集合2
File fileDir = new File(UPLOAD_DIR);
List<String> allFilesPaths = getAllFilesPaths(fileDir);
//遍历集合2,如果不在集合1中就删除
// 遍历所有文件路径,如果不在数据库记录中,则删除该文件
for (String filePath : allFilesPaths) {
if (!allDbPaths.contains(filePath)) {
if (!deleteFile(filePath)) {
// 如果文件删除失败,可以在这里进行日志记录等操作
System.out.println("无法删除文件:" + filePath);
}
}
}
}
6.2删除文件的工具方法
//删除文件的工具方法
public static boolean deleteFile(String filePath) {
File file = new File(filePath);
if (file.exists()) {
log.info("文件路径:{}=>不在数据库中,已删除。",filePath);
return file.delete();
}
return false;
}
6.3递归获取指定文件目录下的所有文件的工具方法
//递归获取指定文件目录下的所有文件的工具方法
private List<String> getAllFilesPaths(File directory) {
List<String> filePaths = new ArrayList<>();
File[] files = directory.listFiles();
if (files!= null) {
for (File file : files) {
if (file.isDirectory()) {
// 递归调用,获取子文件夹中的文件路径
filePaths.addAll(getAllFilesPaths(file));
} else {
filePaths.add(file.getAbsolutePath());
}
}
}
return filePaths;
}
6.4定时任务类中编写方法
/**
* 每天晚上2点,定时清理数据库中没记录的冗余文件
*/
@Scheduled(cron = "0 0 2 * * *")
public void cleanRedundantFilesScheduled() {
fileUploadDownloadUtils.cleanRedundantFiles();
}