Files
mokeegateway/mokee-gateway-web/backup-on-demand/views-20260612-2236/admin/file/FileList.vue
2026-07-15 16:13:27 +08:00

362 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="file-page">
<!-- 顶部操作栏 -->
<div class="page-toolbar">
<div class="toolbar-left">
<el-select v-model="filterType" placeholder="文件类型" clearable style="width:140px" @change="fetchData">
<el-option label="头像" value="avatar" />
<el-option label="图标" value="icon" />
<el-option label="文档" value="doc" />
<el-option label="其他" value="other" />
</el-select>
</div>
<div class="toolbar-right">
<el-button type="primary" @click="uploadVisible = true" v-permission="'system:file:upload'">
<el-icon><Upload /></el-icon>上传文件
</el-button>
<el-button @click="fetchData" :icon="RefreshRight">刷新</el-button>
</div>
</div>
<!-- 上传弹窗 -->
<el-dialog v-model="uploadVisible" title="上传文件" width="460px" destroy-on-close>
<el-form :model="uploadForm" label-width="80px">
<el-form-item label="文件类型">
<el-select v-model="uploadForm.fileType" style="width:100%">
<el-option label="头像" value="avatar" />
<el-option label="图标" value="icon" />
<el-option label="文档" value="doc" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="选择文件">
<div class="upload-area" @click="triggerUpload" v-if="!uploadForm.file">
<el-icon :size="32" color="#94a3b8"><UploadFilled /></el-icon>
<p>点击选择文件</p>
</div>
<div class="upload-file" v-else>
<el-icon><Document /></el-icon>
<span>{{ uploadForm.file.name }}</span>
<span class="upload-size">{{ formatSize(uploadForm.file.size) }}</span>
<el-button size="small" text type="danger" @click="uploadForm.file = null">
<el-icon><Close /></el-icon>
</el-button>
</div>
<input ref="fileInputRef" type="file" style="display:none" @change="onFileSelected" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="uploadVisible = false">取消</el-button>
<el-button type="primary" :loading="uploading" :disabled="!uploadForm.file" @click="doUpload">
开始上传
</el-button>
</template>
</el-dialog>
<!-- 文件网格 -->
<div class="file-grid" v-loading="loading">
<div
v-for="item in fileList"
:key="item.id"
class="file-card"
@click="preview(item)"
>
<!-- 预览区域 -->
<div class="file-preview">
<template v-if="isImage(item.mimeType)">
<img
:src="getFileUrl(item.id)"
:alt="item.fileName"
class="file-thumb"
loading="lazy"
/>
</template>
<template v-else-if="item.fileType === 'doc'">
<el-icon :size="48" color="#6366f1"><Document /></el-icon>
</template>
<template v-else>
<el-icon :size="48" color="#94a3b8"><FolderOpened /></el-icon>
</template>
</div>
<!-- 信息 -->
<div class="file-info">
<div class="file-name" :title="item.fileName">{{ item.fileName }}</div>
<div class="file-meta">
<el-tag size="small" type="info">{{ item.fileType || 'other' }}</el-tag>
<span class="file-size">{{ formatSize(item.fileSize) }}</span>
</div>
</div>
<!-- 操作 -->
<div class="file-actions" @click.stop>
<el-button size="small" text @click="preview(item)">
<el-icon><View /></el-icon>
</el-button>
<el-popconfirm title="确定删除此文件?" @confirm="handleDelete(item.id)">
<template #reference>
<el-button size="small" text type="danger">
<el-icon><Delete /></el-icon>
</el-button>
</template>
</el-popconfirm>
</div>
</div>
</div>
<el-empty v-if="!loading && fileList.length === 0" description="暂无文件" />
<!-- 分页 -->
<div class="page-footer" v-if="total > 0">
<el-pagination
v-model:current-page="page"
:page-size="pageSize"
:total="total"
layout="prev, pager, next"
@current-change="fetchData"
small
/>
</div>
<!-- 预览弹窗 -->
<el-dialog v-model="previewVisible" title="文件预览" width="720px" destroy-on-close>
<div class="preview-body">
<template v-if="previewItem && isImage(previewItem.mimeType)">
<img :src="getFileUrl(previewItem.id)" style="max-width:100%;max-height:60vh;display:block;margin:0 auto" />
</template>
<template v-else>
<el-empty description="非图片文件,无法预览" />
</template>
<div class="preview-meta" v-if="previewItem">
<div class="meta-line"><span class="meta-label">文件名</span>{{ previewItem.fileName }}</div>
<div class="meta-line"><span class="meta-label">类型</span>{{ previewItem.mimeType || '-' }}</div>
<div class="meta-line"><span class="meta-label">大小</span>{{ formatSize(previewItem.fileSize) }}</div>
<div class="meta-line"><span class="meta-label">上传时间</span>{{ previewItem.createdAt }}</div>
</div>
</div>
</el-dialog>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import { RefreshRight, Document, FolderOpened, View, Delete, Upload, UploadFilled, Close } from '@element-plus/icons-vue'
import request from '@/utils/request'
interface FileItem {
id: string
fileName: string
fileType: string
fileSize: number
mimeType: string
md5: string
createdAt: string
}
const loading = ref(false)
const fileList = ref<FileItem[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const filterType = ref('')
const previewVisible = ref(false)
const previewItem = ref<FileItem | null>(null)
const gatewayUrl = import.meta.env.VITE_GATEWAY_URL || ''
// ===== 上传 =====
const uploadVisible = ref(false)
const uploading = ref(false)
const fileInputRef = ref<HTMLInputElement>()
const uploadForm = reactive({ fileType: 'other', file: null as File | null })
function triggerUpload() {
fileInputRef.value?.click()
}
function onFileSelected(e: Event) {
const file = (e.target as HTMLInputElement).files?.[0]
if (file) uploadForm.file = file
}
async function doUpload() {
if (!uploadForm.file) return
uploading.value = true
try {
const fd = new FormData()
fd.append('file', uploadForm.file)
fd.append('fileType', uploadForm.fileType)
await request.post('/zgapi/v1/admin/file/upload', fd)
ElMessage.success('上传成功')
uploadVisible.value = false
uploadForm.file = null
fetchData()
} catch { /* ignore */ } finally {
uploading.value = false
}
}
function getFileUrl(id: string) {
return `${gatewayUrl}/zgapi/v1/admin/file/${id}/view`
}
function isImage(mimeType: string | null | undefined) {
return mimeType ? mimeType.startsWith('image/') : false
}
function formatSize(bytes: number | null) {
if (!bytes) return '0 B'
if (bytes < 1024) return bytes + ' B'
if (bytes < 1048576) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / 1048576).toFixed(1) + ' MB'
}
async function fetchData() {
loading.value = true
try {
const res = await request.post('/zgapi/v1/admin/file/page', {
page: page.value,
pageSize: pageSize.value,
fileType: filterType.value || undefined,
})
fileList.value = res.data?.records || []
total.value = res.data?.total || 0
} catch { /* 拦截器已提示 */ } finally {
loading.value = false
}
}
function preview(item: FileItem) {
previewItem.value = item
previewVisible.value = true
}
async function handleDelete(id: string) {
try {
await request.delete(`/zgapi/v1/admin/file/${id}`)
ElMessage.success('已删除')
if (fileList.value.length === 1 && page.value > 1) page.value--
fetchData()
} catch { /* ignore */ }
}
onMounted(() => fetchData())
</script>
<style scoped>
.file-page { max-width: 1100px; }
/* 工具栏 */
.page-toolbar { display:flex; justify-content:space-between; align-items:center; margin-bottom:20px; }
.toolbar-left { display:flex; gap:12px; }
.page-footer { display:flex; justify-content:center; margin-top:24px; }
/* 文件网格 */
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
}
.file-card {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-lg);
overflow: hidden;
cursor: pointer;
transition: all var(--transition-fast);
}
.file-card:hover {
box-shadow: var(--shadow-md);
border-color: var(--color-primary);
}
/* 预览区 */
.file-preview {
width: 100%;
height: 160px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-page);
overflow: hidden;
}
.file-thumb {
width: 100%;
height: 100%;
object-fit: cover;
}
/* 信息区 */
.file-info {
padding: 12px;
}
.file-name {
font-size: 13px;
font-weight: 500;
color: var(--text-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-bottom: 8px;
}
.file-meta {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
}
.file-size {
font-size: 12px;
color: var(--text-muted);
}
/* 操作 */
.file-actions {
display: flex;
justify-content: flex-end;
padding: 0 8px 8px;
gap: 4px;
}
/* 上传弹窗 */
.upload-area {
width: 100%; height: 120px;
border: 2px dashed var(--border-color);
border-radius: var(--radius-md);
display: flex; flex-direction: column;
align-items: center; justify-content: center;
cursor: pointer; transition: all var(--transition-fast);
color: var(--text-muted); font-size: 13px; gap: 8px;
}
.upload-area:hover { border-color: var(--color-primary); background: var(--color-primary-bg); }
.upload-file {
display: flex; align-items: center; gap: 10px;
padding: 12px; background: var(--bg-page); border-radius: var(--radius-md);
font-size: 13px;
}
.upload-size { color: var(--text-muted); font-size: 12px; margin-left: auto; }
/* 移动端 */
@media (max-width: 768px) {
.file-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 10px; }
.file-preview { height: 120px; }
.file-name { font-size: 12px; }
.page-toolbar { flex-direction: column; gap: 10px; }
}
/* 预览弹窗 */
.preview-meta {
margin-top: 20px;
padding: 16px;
background: var(--bg-page);
border-radius: var(--radius-md);
}
.meta-line {
font-size: 13px;
color: var(--text-secondary);
line-height: 2;
}
.meta-label {
color: var(--text-muted);
}
</style>