From eafbc82b5ecc723311f8586f0a872efb98d41302 Mon Sep 17 00:00:00 2001 From: czhqwer Date: Fri, 21 Mar 2025 01:33:58 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=A2=9E=E5=BC=BA=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E9=A2=84=E8=A7=88=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增视频文件预览 - 新增Pdf文件预览 --- .../cn/czh/controller/FileController.java | 33 ++- .../main/java/cn/czh/utils/FileTypeUtil.java | 59 +++++ upload-file-frontend/package.json | 4 + upload-file-frontend/src/views/FileUpload.vue | 243 +++++++++++++++++- 4 files changed, 331 insertions(+), 8 deletions(-) diff --git a/upload-file-backend/src/main/java/cn/czh/controller/FileController.java b/upload-file-backend/src/main/java/cn/czh/controller/FileController.java index 78b438c..7692c42 100644 --- a/upload-file-backend/src/main/java/cn/czh/controller/FileController.java +++ b/upload-file-backend/src/main/java/cn/czh/controller/FileController.java @@ -4,8 +4,9 @@ import cn.czh.base.Result; import cn.czh.entity.StorageConfig; import cn.czh.service.IFileService; import cn.czh.service.IStorageConfigService; +import cn.czh.utils.FileTypeUtil; +import org.springframework.core.io.ByteArrayResource; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -18,6 +19,7 @@ import javax.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.nio.file.Paths; @RestController @RequestMapping("/file") @@ -29,7 +31,7 @@ public class FileController { private IFileService fileService; @GetMapping("/preview/{storageType}/**") - public ResponseEntity previewFile( + public ResponseEntity previewFile( @PathVariable String storageType, HttpServletRequest request) throws IOException { // 从请求中提取剩余路径作为 objectKey @@ -39,16 +41,37 @@ public class FileController { if (StorageConfig.LOCAL.equals(storageType)) { StorageConfig storageConfig = storageConfigService.getStorageConfigByType(storageType); - String filePath = storageConfig.getBucket() + "\\" + objectKey; + // 使用 Paths.get 拼接路径,避免反斜杠问题 + String filePath = Paths.get(storageConfig.getBucket(), objectKey).toString(); File file = new File(filePath); + + // 检查文件是否存在 if (!file.exists()) { return ResponseEntity.notFound().build(); } + + // 读取文件内容 byte[] fileBytes = Files.readAllBytes(file.toPath()); + ByteArrayResource resource = new ByteArrayResource(fileBytes); + + // 设置响应头 HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); - return new ResponseEntity<>(fileBytes, headers, HttpStatus.OK); + String fileName = file.getName().toLowerCase(); + String fileSuffix = FileTypeUtil.getFileSuffix(fileName).toLowerCase(); + + // 根据文件类型设置 Content-Type + MediaType contentType = FileTypeUtil.determineContentType(fileSuffix); + headers.setContentType(contentType); + + // 设置 Content-Disposition 为 inline,浏览器直接预览 + headers.add(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + file.getName() + "\""); + headers.setContentLength(fileBytes.length); + + return ResponseEntity.ok() + .headers(headers) + .body(resource); } + return ResponseEntity.badRequest().build(); } diff --git a/upload-file-backend/src/main/java/cn/czh/utils/FileTypeUtil.java b/upload-file-backend/src/main/java/cn/czh/utils/FileTypeUtil.java index 21d187f..f654ef0 100644 --- a/upload-file-backend/src/main/java/cn/czh/utils/FileTypeUtil.java +++ b/upload-file-backend/src/main/java/cn/czh/utils/FileTypeUtil.java @@ -1,5 +1,7 @@ package cn.czh.utils; +import org.springframework.http.MediaType; + /** * 文件类型的工具类 */ @@ -158,4 +160,61 @@ public class FileTypeUtil { } return false; } + + /** + * 根据文件后缀确定 Content-Type + */ + public static MediaType determineContentType(String fileSuffix) { + // 使用 FileTypeUtil 判断文件类型 + if (FileTypeUtil.isImgType(fileSuffix)) { + switch (fileSuffix) { + case "jpg": + case "jpeg": + return MediaType.IMAGE_JPEG; + case "png": + return MediaType.IMAGE_PNG; + case "gif": + return MediaType.IMAGE_GIF; + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } else if (FileTypeUtil.bVideoFileType("example." + fileSuffix)) { + switch (fileSuffix) { + case "mp4": + return MediaType.valueOf("video/mp4"); + case "avi": + return MediaType.valueOf("video/x-msvideo"); + case "wmv": + return MediaType.valueOf("video/x-ms-wmv"); + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } else if (FileTypeUtil.bAudioFileType("example." + fileSuffix)) { + switch (fileSuffix) { + case "mp3": + return MediaType.valueOf("audio/mpeg"); + case "wav": + return MediaType.valueOf("audio/wav"); + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } else if (FileTypeUtil.bLogFileType("example." + fileSuffix)) { + switch (fileSuffix) { + case "txt": + return MediaType.TEXT_PLAIN; + case "log": + return MediaType.valueOf("text/plain"); + case "doc": + case "docx": + return MediaType.valueOf("application/msword"); + default: + return MediaType.APPLICATION_OCTET_STREAM; + } + } else if ("pdf".equals(fileSuffix)) { + return MediaType.APPLICATION_PDF; + } + + // 默认类型 + return MediaType.APPLICATION_OCTET_STREAM; + } } diff --git a/upload-file-frontend/package.json b/upload-file-frontend/package.json index d8e25dc..bcbda62 100644 --- a/upload-file-frontend/package.json +++ b/upload-file-frontend/package.json @@ -12,6 +12,7 @@ "axios-extra": "^0.0.8", "core-js": "^3.8.3", "element-ui": "^2.15.14", + "pdfjs-dist": "^5.0.375", "promise-queue-plus": "^1.2.2", "spark-md5": "^3.0.2", "vue": "^2.6.14", @@ -22,6 +23,9 @@ "devDependencies": { "@babel/core": "^7.12.16", "@babel/eslint-parser": "^7.12.16", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/preset-env": "^7.26.9", "@vue/cli-plugin-babel": "~5.0.0", "@vue/cli-plugin-eslint": "~5.0.0", "@vue/cli-plugin-router": "~5.0.0", diff --git a/upload-file-frontend/src/views/FileUpload.vue b/upload-file-frontend/src/views/FileUpload.vue index 47b08cb..1f00925 100644 --- a/upload-file-frontend/src/views/FileUpload.vue +++ b/upload-file-frontend/src/views/FileUpload.vue @@ -65,8 +65,18 @@
+ + +
+
+
+ +
+
📜
+
+
📄
@@ -125,6 +135,34 @@
+ +
+
+ +
+
+
+ + +
+
+ +
+
+

加载 PDF 中...

+
+ + +
+
+
+
@@ -220,7 +258,12 @@ export default { secretKey: '', bucket: '' }, - loadingConfig: false + loadingConfig: false, + showVideoPreview: false, + selectedVideoUrl: '', + showPdfPreview: false, + selectedPdfUrl: '', + loadingPdf: false, }; }, computed: { @@ -238,6 +281,40 @@ export default { } }, methods: { + // 判断是否为 PDF + isPdf(file) { + return file.contentType === 'application/pdf'; + }, + // 打开 PDF 预览 + openPdfPreview(file) { + this.selectedPdfUrl = `${file.accessUrl}?response-content-disposition=inline`; + this.showPdfPreview = true; + this.loadingPdf = true; + }, + // PDF 加载完成 + onPdfLoad() { + this.loadingPdf = false; // 加载完成隐藏动画 + }, + // 关闭 PDF 预览 + closePdfPreview() { + this.showPdfPreview = false; + this.selectedPdfUrl = ''; + this.loadingPdf = false; + }, + // 判断是否为视频 + isVideo(file) { + return file.contentType.startsWith('video/'); + }, + // 打开视频预览 + openVideoPreview(file) { + this.selectedVideoUrl = file.accessUrl; + this.showVideoPreview = true; + }, + // 关闭视频预览 + closeVideoPreview() { + this.showVideoPreview = false; + this.selectedVideoUrl = ''; + }, switchTab(tabValue) { this.activeTab = tabValue; if (tabValue === 'gallery') { @@ -292,8 +369,6 @@ export default { }, // 获取文件列表 async fetchFiles(reset = false) { - console.log('fetchFiles called with reset:', reset, this.loading, this.hasMore); // 调试日志 - if (this.loading || (!reset && !this.hasMore)) return; this.loading = true; @@ -482,6 +557,7 @@ export default { font-size: 18px; font-weight: bold; transition: background 0.2s; + z-index: 9999; } .close-button:hover { @@ -875,6 +951,167 @@ export default { color: #333; } + +/* 视频预览相关样式 */ +.video-preview { + position: relative; + width: 100%; + height: 100%; + cursor: pointer; +} + +.play-button { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + background: rgba(0, 0, 0, 0.5); + color: white; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 20px; + opacity: 0.8; + transition: opacity 0.2s; + margin-top: -105px; +} + +.video-preview:hover .play-button { + opacity: 1; +} + +/* 视频预览模态框 */ +.video-preview-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 200; +} + +.video-preview-content { + position: relative; + width: 90vw; + height: 90vh; + display: flex; + justify-content: center; + align-items: center; +} + +.full-video { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); +} + +/* 加载动画样式 */ +.loading-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.5); + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + z-index: 201; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 4px solid #f0f7ff; + border-top: 4px solid #1976d2; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +.loading-overlay p { + margin-top: 10px; + color: #fff; + font-size: 14px; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +/* PDF 预览相关样式 */ +.pdf-preview { + position: relative; + width: 100%; + height: 100%; + cursor: pointer; +} + +.pdf-icon { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + width: 40px; + height: 40px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + opacity: 0.8; + transition: opacity 0.2s; + margin-top: -105px; +} + +/* PDF 预览模态框 */ +.pdf-preview-modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: rgba(0, 0, 0, 0.8); + display: flex; + justify-content: center; + align-items: center; + z-index: 200; +} + +.pdf-preview-content { + position: relative; + width: 90vw; /* 设置为视口的90%宽度 */ + height: 90vh; /* 设置为视口的90%高度 */ + background: #fff; + border-radius: 8px; + display: flex; + justify-content: center; + align-items: center; +} + +.full-pdf { + width: 100%; + height: 100%; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + background: #fff; +} + /* 关于弹窗样式 */ .about-modal { position: fixed;