初始化

This commit is contained in:
zg
2026-07-15 16:13:27 +08:00
parent 6d6f559793
commit c4023c1623
276 changed files with 42622 additions and 0 deletions

View File

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