feat:增强文件预览功能

- 新增视频文件预览
- 新增Pdf文件预览
This commit is contained in:
czhqwer
2025-03-21 01:33:58 +08:00
parent 8a9373867f
commit eafbc82b5e
4 changed files with 331 additions and 8 deletions

View File

@@ -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<byte[]> 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();
}

View File

@@ -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;
}
}

View File

@@ -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",

View File

@@ -65,8 +65,18 @@
<div v-else class="file-grid">
<div v-for="file in filePage" :key="file.id" class="file-card">
<div class="preview-wrapper">
<!-- 图片预览 -->
<img v-if="isImage(file)" :src="file.accessUrl" :alt="file.fileName" class="preview-image"
@click="openImagePreview(file)" loading="lazy" />
<!-- 视频预览 -->
<div v-else-if="isVideo(file)" class="video-preview" @click="openVideoPreview(file)">
<div class="play-button"></div>
</div>
<!-- PDF 预览 -->
<div v-else-if="isPdf(file)" class="pdf-preview" @click="openPdfPreview(file)">
<div class="pdf-icon">📜</div>
</div>
<!-- 其他文件类型 -->
<div v-else class="file-icon">
📄
</div>
@@ -125,6 +135,34 @@
</div>
</div>
<!-- 视频预览模态框 -->
<div v-if="showVideoPreview" class="video-preview-modal" @click="closeVideoPreview">
<div class="video-preview-content" @click.stop>
<video :src="selectedVideoUrl" controls class="full-video" autoplay></video>
<div class="close-button" @click="closeVideoPreview"></div>
</div>
</div>
<!-- PDF 预览模态框 -->
<div v-if="showPdfPreview" class="pdf-preview-modal" @click="closePdfPreview">
<div class="pdf-preview-content" @click.stop>
<!-- 加载动画 -->
<div v-if="loadingPdf" class="loading-overlay">
<div class="loading-spinner"></div>
<p>加载 PDF ...</p>
</div>
<!-- PDF 内容 -->
<iframe
v-show="!loadingPdf"
:src="selectedPdfUrl"
class="full-pdf"
frameborder="0"
@load="onPdfLoad"
></iframe>
<div class="close-button" @click="closePdfPreview"></div>
</div>
</div>
<!-- 存储配置弹窗 -->
<div v-if="showStorageConfig" class="storage-config-modal" @click="closeStorageConfigOnOutside">
<div class="storage-config-content" @click.stop>
@@ -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;