mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-05-06 20:32:48 +08:00
feat:增强文件预览功能
- 新增视频文件预览 - 新增Pdf文件预览
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user