Files
system-file/web-vue/src/views/files/index.vue
2026-04-02 23:35:48 +08:00

905 lines
27 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="app-layout">
<div class="main-wrapper">
<!-- 左侧菜单 -->
<FileSidebar
:active-menu="activeMenu"
:storage-percent="storagePercent"
:used-storage="usedStorage"
:total-storage="totalStorage"
@select="handleMenuSelect"
/>
<!-- 右侧内容 -->
<div class="content-area">
<!-- 工具栏 -->
<FileToolbar
v-model:viewMode="viewMode"
v-model:searchKeyword="searchKeyword"
:menu-type="activeMenu"
:show-back="folderStack.length > 0"
:folder-stack="folderStack"
@search="loadFiles"
@refresh="loadFiles"
@upload="handleOpenUpload"
@newFolder="folderVisible = true"
@emptyTrash="handleEmptyTrash"
@goBack="handleBreadcrumbClick"
/>
<!-- 批量操作工具栏 -->
<div v-if="activeMenu === 'my-files' || activeMenu === 'shared-to-me' || activeMenu === 'my-share' || activeMenu === 'trash'" class="batch-toolbar">
<el-button-group>
<el-button v-if="activeMenu !== 'trash'" @click="handleBatchDownload" :disabled="selectedFiles.length === 0">
<el-icon><Download /></el-icon>
<span style="margin-left: 4px">批量下载</span>
</el-button>
<el-button v-if="activeMenu === 'my-share'" type="warning" @click="handleBatchCancelShare" :disabled="selectedFiles.length === 0">
<el-icon><CloseBold /></el-icon>
<span style="margin-left: 4px">批量取消</span>
</el-button>
<el-button v-if="activeMenu === 'trash'" type="primary" @click="handleBatchRestore" :disabled="selectedFiles.length === 0">
<el-icon><RefreshLeft /></el-icon>
<span style="margin-left: 4px">批量还原</span>
</el-button>
<el-button v-if="activeMenu === 'my-files'" type="success" @click="handleBatchMove" :disabled="selectedFiles.length === 0">
<el-icon><FolderOpened /></el-icon>
<span style="margin-left: 4px">批量移动</span>
</el-button>
<el-button v-if="activeMenu === 'my-files'" type="danger" @click="handleBatchDelete" :disabled="selectedFiles.length === 0">
<el-icon><Delete /></el-icon>
<span style="margin-left: 4px">批量删除</span>
</el-button>
</el-button-group>
</div>
<!-- 文件列表 -->
<div class="file-content">
<FileTable
v-if="viewMode === 'table'"
:files="paginatedFiles"
:loading="loading"
:menu-type="activeMenu"
:in-folder="currentFolderId !== null"
@preview="handlePreview"
@rename="handleRename"
@share="handleShare"
@download="handleDownload"
@delete="handleDelete"
@restore="handleRestore"
@delete-permanent="handleDeletePermanently"
@cancel-share="handleCancelShare"
@selection-change="handleSelectionChange"
@row-dblclick="handleRowDblClick"
/>
<div v-else class="grid-view">
<div v-for="file in paginatedFiles" :key="file.id" class="file-card" @dblclick="handleRowDblClick(file)">
<div class="file-card-main">
<div class="file-card-icon">
<FileIcon :icon="getFileIconType(file)" :color="getFileIconColor(file)" :size="36" />
</div>
<el-tooltip :content="file.name" placement="top" :show-after="300">
<div class="file-card-name">{{ file.name }}</div>
</el-tooltip>
</div>
<div class="file-card-meta">
<span class="file-card-date">{{ formatDate(file.createTime) }}</span>
<span class="file-card-size">{{ file.type === 'folder' ? '-' : formatSize(file.size) }}</span>
</div>
<div class="file-card-actions">
<template v-if="activeMenu === 'trash'">
<el-button link @click.stop="handleRestore(file)"><el-icon><RefreshLeft /></el-icon><span>还原</span></el-button>
<el-button link type="danger" @click.stop="handleDeletePermanently(file)"><el-icon><Delete /></el-icon><span>删除</span></el-button>
</template>
<template v-else-if="activeMenu === 'my-files'">
<el-button link @click.stop="handlePreview(file)" v-if="canPreview(file)"><el-icon><View /></el-icon><span>预览</span></el-button>
<el-button link @click.stop="handleRename(file)"><el-icon><Edit /></el-icon><span>重命名</span></el-button>
<el-button link @click.stop="handleShare(file)"><el-icon><Share /></el-icon><span>共享</span></el-button>
<el-button link @click.stop="handleDownload(file)" v-if="file.type !== 'folder'"><el-icon><Download /></el-icon><span>下载</span></el-button>
<el-button link type="danger" @click.stop="handleDelete(file)"><el-icon><Delete /></el-icon><span>删除</span></el-button>
</template>
<template v-else-if="activeMenu === 'my-share'">
<el-button link @click.stop="handlePreview(file)" v-if="canPreview(file)"><el-icon><View /></el-icon><span>预览</span></el-button>
<el-button link @click.stop="handleDownload(file)" v-if="file.type !== 'folder'"><el-icon><Download /></el-icon><span>下载</span></el-button>
<el-button link type="warning" @click.stop="handleCancelShare(file)" v-if="!currentFolderId"><el-icon><CloseBold /></el-icon><span>取消共享</span></el-button>
</template>
<template v-else-if="activeMenu === 'shared-to-me'">
<el-button link @click.stop="handlePreview(file)" v-if="canPreview(file)"><el-icon><View /></el-icon><span>预览</span></el-button>
<el-button link @click.stop="handleDownload(file)" v-if="file.type !== 'folder'"><el-icon><Download /></el-icon><span>下载</span></el-button>
</template>
</div>
</div>
</div>
</div>
<!-- 分页 -->
<div class="pagination-wrapper">
<el-pagination
v-model:current-page="currentPage"
v-model:page-size="pageSize"
:total="files.length"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next, jumper"
/>
</div>
</div>
</div>
<!-- 弹窗组件 -->
<UploadDialog
v-model="uploadVisible"
:uploading="uploading"
:remaining-storage="remainingStorage"
@upload="handleUpload"
/>
<FolderDialog
v-model="folderVisible"
@create="handleCreateFolder"
/>
<ShareDialog
v-model="shareVisible"
:file="currentShareFile"
@share="handleConfirmShare"
/>
<PreviewDialog
v-model="previewVisible"
:preview-file="previewFile"
:preview-url="previewUrl"
:preview-content="previewContent"
/>
<RenameDialog
v-model="renameVisible"
:file="currentRenameFile"
@rename="handleConfirmRename"
/>
<BatchMoveDialog
v-model="batchMoveVisible"
:folders="movableFolders"
:can-go-parent="folderStack.length > 0"
:parent-folder-name="folderStack.length > 0 ? folderStack[folderStack.length - 1].name : '返回上一级'"
@confirm="handleConfirmBatchMove"
/>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import { RefreshLeft, Delete, View, Share, Download, CloseBold, Edit, FolderOpened, Close } from '@element-plus/icons-vue'
import FileSidebar from '@/components/FileSidebar.vue'
import FileToolbar from '@/components/FileToolbar.vue'
import FileTable from '@/components/FileTable.vue'
import FileIcon from '@/components/FileIcon.vue'
import UploadDialog from '@/components/UploadDialog.vue'
import FolderDialog from '@/components/FolderDialog.vue'
import ShareDialog from '@/components/ShareDialog.vue'
import PreviewDialog from '@/components/PreviewDialog.vue'
import RenameDialog from '@/components/RenameDialog.vue'
import BatchMoveDialog from '@/components/BatchMoveDialog.vue'
import {
getFiles, getTrashFiles, getSharedByMe, getSharedByMeFolderFiles, getSharedToMe, getSharedFolderFiles,
uploadFiles, downloadFile, deleteFile, restoreFile,
deletePermanently, emptyTrash, createFolder,
shareFileApi, cancelShare, getFilePreview, renameFile, moveFile, batchDownload, getMovableFolders,
batchCancelShare, batchRestore
} from '@/api/file'
import { getCurrentUser } from '@/api/auth'
import { useUserStore } from '@/store/user'
const userStore = useUserStore()
// 状态
const activeMenu = ref('my-files')
const loading = ref(false)
const files = ref([])
const viewMode = ref('table')
const searchKeyword = ref('')
const currentPage = ref(1)
const pageSize = ref(20)
const currentFolderId = ref(null)
const folderStack = ref([])
// 弹窗
const uploadVisible = ref(false)
const uploading = ref(false)
const handleOpenUpload = () => {
if (remainingStorage.value <= 0) {
ElMessage.warning('存储空间已满,无法上传文件')
return
}
uploadVisible.value = true
}
const folderVisible = ref(false)
const shareVisible = ref(false)
const currentShareFile = ref(null)
const previewVisible = ref(false)
const previewFile = ref(null)
const previewUrl = ref('')
const previewContent = ref('')
const renameVisible = ref(false)
const currentRenameFile = ref(null)
const selectedFiles = ref([])
const batchMoveVisible = ref(false)
const movableFolders = ref([])
// 存储 —— 真实数据
const storagePercent = computed(() => {
const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024
const used = userStore.storageUsed || 0
if (limit <= 0) return 0
return Math.min(Math.round((used / limit) * 100), 100)
})
const usedStorage = computed(() => {
const used = userStore.storageUsed || 0
return used >= 1024 * 1024 * 1024
? (used / (1024 * 1024 * 1024)).toFixed(2) + ' GB'
: (used / (1024 * 1024)).toFixed(2) + ' MB'
})
const totalStorage = computed(() => {
const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024
return (limit / (1024 * 1024 * 1024)).toFixed(0) + ' GB'
})
// 剩余存储空间(字节)
const remainingStorage = computed(() => {
const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024
const used = userStore.storageUsed || 0
return Math.max(0, limit - used)
})
// 刷新存储数据(从后端精确重算)
const refreshStorage = async () => {
try {
const data = await getCurrentUser()
if (data) {
userStore.setUser({
storageUsed: data.storageUsed ?? 0,
storageLimit: data.storageLimit ?? 20 * 1024 * 1024 * 1024
})
}
} catch (e) {
// 静默失败,不影响页面正常使用
}
}
// 辅助函数
const formatSize = (size) => {
if (!size) return '0 B'
const units = ['B', 'KB', 'MB', 'GB']
let i = 0
while (size >= 1024 && i < units.length - 1) { size /= 1024; i++ }
return size.toFixed(2) + ' ' + units[i]
}
const formatDate = (date) => {
if (!date) return '-'
return new Date(date).toLocaleDateString('zh-CN')
}
const canPreview = (file) => {
if (file.type === 'folder') return false
const ext = file.name.split('.').pop()?.toLowerCase()
// 支持预览的文件类型图片、PDF、文本、Office注意doc 旧格式不支持)
return ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
'pdf',
'txt', 'md', 'markdown', 'json', 'xml', 'log', 'js', 'ts', 'vue', 'java', 'py', 'css', 'html', 'htm', 'yaml', 'yml', 'sql',
'docx', 'xlsx', 'xls', 'pptx', 'ppt', 'ofd'].includes(ext)
}
// 分页
const paginatedFiles = computed(() => {
const start = (currentPage.value - 1) * pageSize.value
return files.value.slice(start, start + pageSize.value)
})
// 加载文件
const loadFiles = async () => {
loading.value = true
currentPage.value = 1
try {
let data
switch (activeMenu.value) {
case 'trash': data = await getTrashFiles(currentFolderId.value ? { folderId: currentFolderId.value } : {}); break
case 'my-share':
if (currentFolderId.value) {
data = await getSharedByMeFolderFiles(currentFolderId.value)
} else {
data = await getSharedByMe()
}
break
case 'shared-to-me':
if (currentFolderId.value) {
data = await getSharedFolderFiles(currentFolderId.value)
} else {
data = await getSharedToMe()
}
break
default: data = await getFiles({ folderId: currentFolderId.value, keyword: searchKeyword.value })
}
files.value = data || []
} catch (e) {
console.error(e)
} finally {
loading.value = false
}
}
const handleMenuSelect = (index) => {
activeMenu.value = index
searchKeyword.value = ''
currentFolderId.value = null
folderStack.value = []
loadFiles()
}
// 进入目录
const enterFolder = (folder) => {
// 先把当前目录信息压入栈(用于返回)
folderStack.value.push({
id: folder.id,
name: folder.name
})
// 进入新目录
currentFolderId.value = folder.id
loadFiles()
}
// 返回上级
const goBack = () => {
if (!folderStack.value.length) return
folderStack.value.pop()
// 获取上一个目录的ID
const lastFolder = folderStack.value[folderStack.value.length - 1]
currentFolderId.value = lastFolder?.id || null
loadFiles()
}
// 面包屑点击
const handleBreadcrumbClick = (index) => {
if (index === -1) {
// 点击根目录
folderStack.value = []
currentFolderId.value = null
} else {
// 点击某个目录,截取到该位置之后
folderStack.value = folderStack.value.slice(0, index + 1)
// 当前目录设为点击的目录
currentFolderId.value = folderStack.value[index]?.id || null
}
loadFiles()
}
// 获取当前路径名称
const getCurrentPathName = () => {
if (!folderStack.value.length) return '/'
return folderStack.value.map(f => f.name).join(' / ')
}
const handleUpload = async (fileList) => {
uploading.value = true
try {
await uploadFiles(fileList, currentFolderId.value)
ElMessage.success('上传成功')
uploadVisible.value = false
loadFiles()
refreshStorage()
} catch (e) {
ElMessage.error('上传失败')
} finally {
uploading.value = false
}
}
const handleCreateFolder = async (name) => {
try {
await createFolder({ name, parentId: currentFolderId.value })
ElMessage.success('创建成功')
loadFiles()
} catch (e) {
ElMessage.error('创建失败')
}
}
const handlePreview = async (file) => {
previewFile.value = file
previewUrl.value = ''
previewContent.value = ''
previewVisible.value = true // 先打开弹窗
try {
const blob = await getFilePreview(file.id)
const ext = file.name.split('.').pop()?.toLowerCase()
// 需要用 blob URL 的格式图片、PDF、Office
const blobUrlFormats = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg', 'bmp', 'ico',
'pdf', 'docx', 'xlsx', 'xls', 'pptx', 'ppt', 'ofd']
if (blobUrlFormats.includes(ext)) {
// 图片、PDF、Office 文件使用 blob URL
previewUrl.value = URL.createObjectURL(blob)
} else {
// 文本文件直接读取内容
previewContent.value = await blob.text()
console.log('Text content loaded:', previewContent.value?.substring(0, 100))
}
} catch (e) {
console.error('Preview error:', e)
ElMessage.error('预览失败')
previewVisible.value = false
}
}
const handleRename = (file) => {
currentRenameFile.value = file
renameVisible.value = true
}
const handleConfirmRename = async ({ id, name }) => {
try {
await renameFile(id, name)
ElMessage.success('重命名成功')
loadFiles()
} catch (e) {
ElMessage.error('重命名失败')
}
}
const handleDownload = async (file) => {
try {
const blob = await downloadFile(file.id)
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = file.name
a.click()
URL.revokeObjectURL(url)
} catch (e) {
ElMessage.error('下载失败')
}
}
const handleDelete = async (file) => {
await ElMessageBox.confirm("确定删除此文件或目录吗?", '提示', { type: 'warning' })
try {
await deleteFile(file.id)
ElMessage.success('已移至回收站')
loadFiles()
} catch (e) {
// 显示后端返回的错误信息
const errorMsg = e.response?.data?.message || e.message || '删除失败'
ElMessage.error(errorMsg)
}
}
const handleRestore = async (file) => {
try {
await restoreFile(file.id)
ElMessage.success('已还原')
loadFiles()
} catch (e) {
ElMessage.error('还原失败')
}
}
const handleDeletePermanently = async (file) => {
await ElMessageBox.confirm("确定彻底删除此文件或目录吗?", '警告', { type: 'error' })
try {
await deletePermanently(file.id)
ElMessage.success('已彻底删除')
loadFiles()
refreshStorage()
} catch (e) {
ElMessage.error('删除失败')
}
}
const handleEmptyTrash = async () => {
await ElMessageBox.confirm('确定清空回收站吗?', '警告', { type: 'error' })
try {
await emptyTrash()
ElMessage.success('已清空回收站')
loadFiles()
refreshStorage()
} catch (e) {
ElMessage.error('操作失败')
}
}
const handleShare = (file) => {
currentShareFile.value = file
shareVisible.value = true
}
const handleConfirmShare = async ({ users, permission }) => {
if (!users.length) { ElMessage.warning('请选择共享用户'); return }
try {
for (const userId of users) {
await shareFileApi(currentShareFile.value.id, { userId, permission })
}
ElMessage.success('共享成功')
loadFiles()
} catch (e) {
ElMessage.error('共享失败')
}
}
const handleCancelShare = async (file) => {
await ElMessageBox.confirm("确定取消共享吗?", '提示', { type: 'warning' })
try {
await cancelShare(file.id)
ElMessage.success('已取消共享')
loadFiles()
} catch (e) {
ElMessage.error('操作失败')
}
}
const handleSelectionChange = (selection) => {
selectedFiles.value = selection
}
const handleBatchDownload = async () => {
const filesToDownload = selectedFiles.value.filter(f => f.type !== 'folder')
if (filesToDownload.length === 0 && selectedFiles.value.length > 0) {
ElMessage.warning('请选择要下载的文件')
return
}
if (selectedFiles.value.length === 0) {
ElMessage.warning('请选择要下载的文件')
return
}
try {
ElMessage.info('正在打包下载...')
const ids = selectedFiles.value.map(f => f.id)
const blob = await batchDownload(ids)
// 检查返回的是否是错误信息JSON 格式)
if (blob.type && blob.type.includes('application/json')) {
const text = await blob.text()
const error = JSON.parse(text)
ElMessage.error(error.message || '下载失败')
return
}
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `download_${new Date().getTime()}.zip`
a.click()
URL.revokeObjectURL(url)
selectedFiles.value = []
} catch (e) {
// 处理 HTTP 错误响应
if (e.response && e.response.data) {
// 如果是 blob 类型的错误响应
if (e.response.data instanceof Blob) {
try {
const text = await e.response.data.text()
const error = JSON.parse(text)
ElMessage.error(error.message || '下载失败')
} catch {
ElMessage.error('下载失败')
}
} else {
ElMessage.error(e.response.data.message || '下载失败')
}
} else {
ElMessage.error('下载失败')
}
}
}
const handleBatchMove = async () => {
if (selectedFiles.value.length === 0) {
ElMessage.warning('请选择要移动的文件')
return
}
try {
const selectedIds = selectedFiles.value.map(f => f.id)
const data = await getMovableFolders(selectedIds, currentFolderId.value)
movableFolders.value = data || []
batchMoveVisible.value = true
} catch (e) {
ElMessage.error('获取目录列表失败')
}
}
const handleConfirmBatchMove = async (targetFolderId) => {
// Handle target folder ID
let finalFolderId = null
if (targetFolderId === 'root' || targetFolderId === '' || targetFolderId === null || targetFolderId === undefined) {
finalFolderId = null
} else {
finalFolderId = targetFolderId
}
let successCount = 0
let failCount = 0
for (const file of selectedFiles.value) {
try {
await moveFile(file.id, finalFolderId)
successCount++
} catch (e) {
failCount++
console.error('移动失败:', file.name, e)
}
}
if (successCount > 0) {
ElMessage.success(`成功移动 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`)
} else if (failCount > 0) {
ElMessage.error('移动失败')
}
selectedFiles.value = []
loadFiles()
}
const handleBatchDelete = async () => {
if (selectedFiles.value.length === 0) {
ElMessage.warning('请选择要删除的文件')
return
}
await ElMessageBox.confirm(`确定删除选中的 ${selectedFiles.value.length} 个文件吗?`, '提示', { type: 'warning' })
let successCount = 0
let failCount = 0
const errors = []
for (const file of selectedFiles.value) {
try {
await deleteFile(file.id)
successCount++
} catch (e) {
failCount++
const errorMsg = e.response?.data?.message || e.message || '删除失败'
if (!errors.includes(errorMsg)) {
errors.push(errorMsg)
}
}
}
if (successCount > 0) {
ElMessage.success(`成功删除 ${successCount} 个文件${failCount > 0 ? `${failCount} 个失败` : ''}`)
} else if (failCount > 0) {
ElMessage.error(errors[0] || '删除失败')
}
selectedFiles.value = []
loadFiles()
}
const handleRowDblClick = (row) => {
if (row.type === 'folder') {
enterFolder(row)
} else if (canPreview(row)) {
handlePreview(row)
}
}
const handleBatchCancelShare = async () => {
if (selectedFiles.value.length === 0) {
ElMessage.warning('请选择要取消共享的文件')
return
}
await ElMessageBox.confirm(`确定取消选中 ${selectedFiles.value.length} 个文件的共享吗?`, '提示', { type: 'warning' })
try {
const ids = selectedFiles.value.map(f => f.id)
const res = await batchCancelShare(ids)
ElMessage.success('批量取消共享成功')
selectedFiles.value = []
loadFiles()
} catch (e) {
ElMessage.error('操作失败')
}
}
const handleBatchRestore = async () => {
if (selectedFiles.value.length === 0) {
ElMessage.warning('请选择要还原的文件')
return
}
await ElMessageBox.confirm(`确定还原选中的 ${selectedFiles.value.length} 个文件吗?`, '提示', { type: 'info' })
try {
const ids = selectedFiles.value.map(f => f.id)
const res = await batchRestore(ids)
ElMessage.success('批量还原成功')
selectedFiles.value = []
loadFiles()
refreshStorage()
} catch (e) {
ElMessage.error('操作失败')
}
}
onMounted(async () => {
await refreshStorage()
loadFiles()
})
// 文件图标类型
const getFileIconType = (file) => {
if (file.type === 'folder') return 'folder'
if (file.type === 'image') return 'image'
if (file.type === 'video') return 'video'
if (file.type === 'audio') return 'audio'
const ext = file.name.split('.').pop()?.toLowerCase()
if (ext === 'pdf') return 'pdf'
if (['docx', 'doc'].includes(ext)) return 'word'
if (['xlsx', 'xls'].includes(ext)) return 'excel'
if (['pptx', 'ppt'].includes(ext)) return 'ppt'
if (['js', 'ts', 'vue', 'jsx', 'tsx', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml'].includes(ext)) return 'code'
if (['txt', 'md', 'markdown', 'log', 'sql', 'yaml', 'yml', 'sh', 'bat', 'py', 'java', 'c', 'cpp', 'h', 'go', 'rs'].includes(ext)) return 'text'
if (ext === 'ofd') return 'pdf'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return 'zip'
return 'document'
}
// 文件图标颜色
const getFileIconColor = (file) => {
if (file.type === 'folder') return '#f7b32b'
if (file.type === 'image') return '#67c23a'
if (file.type === 'video') return '#409eff'
if (file.type === 'audio') return '#e6a23c'
const ext = file.name.split('.').pop()?.toLowerCase()
if (ext === 'pdf') return '#f56c6c'
if (['docx', 'doc'].includes(ext)) return '#2b579a'
if (['xlsx', 'xls'].includes(ext)) return '#217346'
if (['pptx', 'ppt'].includes(ext)) return '#d24726'
if (['js', 'ts', 'vue', 'jsx', 'tsx', 'html', 'css', 'scss', 'sass', 'less', 'json', 'xml'].includes(ext)) return '#9c27b0'
if (['txt', 'md', 'markdown', 'log', 'yaml', 'yml', 'sh', 'bat'].includes(ext)) return '#606266'
if (ext === 'sql') return '#1976d2'
if (ext === 'py') return '#3776ab'
if (ext === 'java') return '#f89820'
if (ext === 'ofd') return '#f56c6c'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(ext)) return '#8d6e63'
return '#909399'
}
</script>
<style scoped>
.app-layout {
display: flex;
flex: 1;
overflow: hidden;
background: #f5f7fa;
width: 100%;
}
.main-wrapper {
display: flex;
flex: 1;
overflow: hidden;
}
.content-area {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
.batch-toolbar {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 16px;
padding: 12px 16px;
border-bottom: 1px solid #e4e7ed;
background: #fff;
}
.batch-count {
font-size: 14px;
color: #606266;
}
.batch-actions {
display: flex;
gap: 12px;
}
.file-content {
flex: 1;
overflow: auto;
background: #fff;
}
.grid-view {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 16px;
padding: 16px;
align-content: start;
}
.file-card {
display: flex;
flex-direction: column;
padding: 16px;
border: 1px solid #e4e7ed;
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
background: #fff;
min-height: 140px;
}
.file-card:hover {
border-color: #409eff;
box-shadow: 0 2px 12px rgba(64, 158, 255, 0.15);
}
.file-card-main {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
flex: 1;
}
.file-card-icon {
flex-shrink: 0;
}
.file-card-name {
font-size: 13px;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-card-meta {
display: flex;
justify-content: space-between;
font-size: 11px;
color: #909399;
margin-bottom: 8px;
padding-bottom: 8px;
border-bottom: 1px solid #f0f0f0;
}
.file-card-actions {
display: flex;
gap: 4px;
justify-content: center;
overflow: hidden;
}
.file-card-actions .el-button {
font-size: 12px;
padding: 4px 8px;
min-width: 0;
}
.file-card-actions .el-button + .el-button {
margin-left: 0;
}
.file-card-actions .el-button span {
display: inline;
}
.pagination-wrapper {
padding: 10px 16px;
background: #fff;
border-top: 1px solid #e4e7ed;
display: flex;
justify-content: flex-end;
}
</style>