初始化
This commit is contained in:
@@ -0,0 +1,361 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user