mirror of
https://gitee.com/czh-dev/upload-hub
synced 2026-05-06 20:32:48 +08:00
488 lines
11 KiB
Vue
488 lines
11 KiB
Vue
<template>
|
||
<div class="gallery-container" @scroll="handleScroll">
|
||
<div class="gallery-header">
|
||
<label for="storage-select">存储类型:</label>
|
||
<select id="storage-select" v-model="selectedStorageType" @change="fetchFiles(true)">
|
||
<option v-for="option in storageOptions" :key="option.value" :value="option.value">
|
||
{{ option.label }}
|
||
</option>
|
||
</select>
|
||
<label for="file-name-input"> 文件名:</label>
|
||
<el-input v-model="selectedFileName" placeholder="请输入文件名" class="file-name-input" style="width: 150px;"
|
||
@change="fetchFiles(true)"></el-input>
|
||
</div>
|
||
<div v-if="filePage.length === 0 && !loading" class="empty-tips">
|
||
🖼️ 暂无已上传的文件
|
||
</div>
|
||
<div v-else-if="loading && filePage.length === 0" class="loading-tips">
|
||
加载中...
|
||
</div>
|
||
<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>
|
||
<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>
|
||
<div class="copy-button" @click.stop="copyUrl(file.accessUrl)" title="复制文件URL">📋</div>
|
||
</div>
|
||
<div class="file-meta">
|
||
<div class="filename">{{ file.fileName }}</div>
|
||
<div class="file-size-status">
|
||
<div class="file-size">{{ formatSize(file.totalSize) }}</div>
|
||
<div class="status-indicator">{{ formattedDate(file.createTime) }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div v-if="loading && filePage.length > 0" class="loading-more">加载更多...</div>
|
||
|
||
<!-- 预览弹窗 -->
|
||
<div v-if="showImagePreview" class="image-preview-modal" @click="closeImagePreview">
|
||
<div class="image-preview-content" @click.stop>
|
||
<img :src="selectedImageUrl" alt="预览图片" class="full-image" />
|
||
<div class="close-button" @click="closeImagePreview">✖</div>
|
||
</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>
|
||
<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>
|
||
<iframe v-show="!loadingPdf" :src="selectedPdfUrl" class="full-pdf" frameborder="0" @load="onPdfLoad"></iframe>
|
||
<div class="close-button" @click="closePdfPreview">✖</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { pageFiles } from '@/utils/api';
|
||
|
||
export default {
|
||
name: 'FileGallery',
|
||
props: {
|
||
storageOptions: {
|
||
type: Array,
|
||
default: () => [
|
||
{ label: 'Local', value: 'local' },
|
||
{ label: 'MinIO', value: 'minio' },
|
||
{ label: 'OSS', value: 'oss' }
|
||
]
|
||
}
|
||
},
|
||
data() {
|
||
return {
|
||
selectedStorageType: 'local',
|
||
selectedFileName: '',
|
||
currentPage: 1,
|
||
pageSize: 10,
|
||
loading: false,
|
||
hasMore: true,
|
||
filePage: [],
|
||
showImagePreview: false,
|
||
selectedImageUrl: '',
|
||
showVideoPreview: false,
|
||
selectedVideoUrl: '',
|
||
showPdfPreview: false,
|
||
selectedPdfUrl: '',
|
||
loadingPdf: false
|
||
};
|
||
},
|
||
methods: {
|
||
async fetchFiles(reset = false) {
|
||
if (this.loading || (!reset && !this.hasMore)) return;
|
||
this.loading = true;
|
||
if (reset) {
|
||
this.currentPage = 1;
|
||
this.filePage = [];
|
||
this.hasMore = true;
|
||
}
|
||
try {
|
||
const response = await pageFiles({
|
||
page: this.currentPage,
|
||
pageSize: this.pageSize,
|
||
storageType: this.selectedStorageType,
|
||
fileName: this.selectedFileName
|
||
});
|
||
const files = response.data.records || [];
|
||
this.filePage = reset ? files : this.filePage.concat(files);
|
||
this.hasMore = files.length === this.pageSize;
|
||
if (this.hasMore) this.currentPage++;
|
||
} catch (error) {
|
||
console.error('获取文件列表失败:', error);
|
||
this.$message.error('加载文件列表失败');
|
||
} finally {
|
||
this.loading = false;
|
||
}
|
||
},
|
||
handleScroll(event) {
|
||
const container = event.target;
|
||
const isBottom = container.scrollTop + container.clientHeight >= container.scrollHeight - 10;
|
||
if (isBottom && !this.loading && this.hasMore) this.fetchFiles();
|
||
},
|
||
isImage(file) {
|
||
return file.contentType.startsWith('image/');
|
||
},
|
||
isVideo(file) {
|
||
return file.contentType.startsWith('video/');
|
||
},
|
||
isPdf(file) {
|
||
return file.contentType === 'application/pdf';
|
||
},
|
||
formatSize(bytes) {
|
||
if (bytes === 0) return '0 B';
|
||
const k = 1024;
|
||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||
},
|
||
formattedDate(date) {
|
||
const d = new Date(date);
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||
},
|
||
openImagePreview(file) {
|
||
this.selectedImageUrl = file.accessUrl;
|
||
this.showImagePreview = true;
|
||
},
|
||
closeImagePreview() {
|
||
this.showImagePreview = false;
|
||
this.selectedImageUrl = '';
|
||
},
|
||
openVideoPreview(file) {
|
||
this.selectedVideoUrl = file.accessUrl;
|
||
this.showVideoPreview = true;
|
||
},
|
||
closeVideoPreview() {
|
||
this.showVideoPreview = false;
|
||
this.selectedVideoUrl = '';
|
||
},
|
||
openPdfPreview(file) {
|
||
this.selectedPdfUrl = `${file.accessUrl}?response-content-disposition=inline`;
|
||
this.showPdfPreview = true;
|
||
this.loadingPdf = true;
|
||
},
|
||
onPdfLoad() {
|
||
this.loadingPdf = false;
|
||
},
|
||
closePdfPreview() {
|
||
this.showPdfPreview = false;
|
||
this.selectedPdfUrl = '';
|
||
this.loadingPdf = false;
|
||
},
|
||
async copyUrl(url) {
|
||
try {
|
||
await navigator.clipboard.writeText(url);
|
||
this.$message({ message: '文件URL已复制到剪贴板', type: 'success', duration: 2000 });
|
||
} catch (error) {
|
||
console.error('复制URL失败:', error);
|
||
this.$message.error('复制URL失败');
|
||
}
|
||
}
|
||
},
|
||
mounted() {
|
||
this.fetchFiles(true);
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style scoped>
|
||
/* eslint-disable */
|
||
.gallery-container {
|
||
padding: 1rem 2rem 2rem;
|
||
height: 400px;
|
||
background: #fcfdff;
|
||
border-top: 1px solid #f0f7ff;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.gallery-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 10px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.gallery-header label {
|
||
font-size: 14px;
|
||
color: #333;
|
||
}
|
||
|
||
.gallery-header select {
|
||
padding: 6px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
background: #fff;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.empty-tips,
|
||
.loading-tips {
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 1.2rem;
|
||
padding: 4rem 0;
|
||
}
|
||
|
||
.file-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||
gap: 1rem;
|
||
}
|
||
|
||
.loading-more {
|
||
text-align: center;
|
||
color: #666;
|
||
font-size: 14px;
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.file-card {
|
||
background: #fff;
|
||
border-radius: 6px;
|
||
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.1);
|
||
overflow: hidden;
|
||
transition: transform 0.2s, box-shadow 0.2s;
|
||
}
|
||
|
||
.file-card:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 3px 8px rgba(25, 118, 210, 0.2);
|
||
}
|
||
|
||
.copy-button {
|
||
position: absolute;
|
||
top: 8px;
|
||
right: 8px;
|
||
width: 24px;
|
||
height: 24px;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
opacity: 0;
|
||
transition: opacity 0.2s;
|
||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.file-card:hover .copy-button {
|
||
opacity: 1;
|
||
}
|
||
|
||
.preview-wrapper {
|
||
position: relative;
|
||
padding-top: 100%;
|
||
background: #f8fafd;
|
||
}
|
||
|
||
.preview-image {
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-bottom: 1px solid #eee;
|
||
}
|
||
|
||
.file-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 2rem;
|
||
opacity: 0.6;
|
||
}
|
||
|
||
.file-meta {
|
||
padding: 0.8rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.file-size-status {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
}
|
||
|
||
.filename {
|
||
font-weight: 500;
|
||
color: #333;
|
||
font-size: 0.9rem;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
}
|
||
|
||
.file-size {
|
||
font-size: 0.7rem;
|
||
color: #666;
|
||
margin: 0.2rem 0;
|
||
}
|
||
|
||
.status-indicator {
|
||
padding: 0.15rem 0.4rem;
|
||
border-radius: 3px;
|
||
font-size: 0.65rem;
|
||
background: #e8f5e9;
|
||
color: #2e7d32;
|
||
}
|
||
|
||
.image-preview-modal,
|
||
.video-preview-modal,
|
||
.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;
|
||
}
|
||
|
||
.image-preview-content,
|
||
.video-preview-content,
|
||
.pdf-preview-content {
|
||
position: relative;
|
||
width: 90vw;
|
||
height: 90vh;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
}
|
||
|
||
.full-image,
|
||
.full-video {
|
||
max-width: 100%;
|
||
max-height: 100%;
|
||
width: auto;
|
||
height: auto;
|
||
object-fit: contain;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
}
|
||
|
||
.full-pdf {
|
||
width: 100%;
|
||
height: 100%;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||
background: #fff;
|
||
}
|
||
|
||
.close-button {
|
||
position: absolute;
|
||
top: 10px;
|
||
right: 10px;
|
||
width: 30px;
|
||
height: 30px;
|
||
background: rgba(255, 255, 255, 0.9);
|
||
color: #333;
|
||
border-radius: 50%;
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
cursor: pointer;
|
||
font-size: 18px;
|
||
}
|
||
|
||
.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;
|
||
margin-top: -105px;
|
||
}
|
||
|
||
.pdf-preview {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 100%;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.pdf-icon {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
font-size: 2rem;
|
||
opacity: 0.8;
|
||
margin-top: -105px;
|
||
}
|
||
|
||
.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);
|
||
}
|
||
}
|
||
</style> |