2026-04-02 12:06:51 +08:00
|
|
|
|
<template>
|
2026-04-01 22:39:11 +08:00
|
|
|
|
<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"
|
2026-04-02 12:06:51 +08:00
|
|
|
|
:folder-stack="folderStack"
|
2026-04-01 22:39:11 +08:00
|
|
|
|
@search="loadFiles"
|
|
|
|
|
|
@refresh="loadFiles"
|
2026-04-02 12:06:51 +08:00
|
|
|
|
@upload="handleOpenUpload"
|
2026-04-01 22:39:11 +08:00
|
|
|
|
@newFolder="folderVisible = true"
|
|
|
|
|
|
@emptyTrash="handleEmptyTrash"
|
2026-04-02 12:06:51 +08:00
|
|
|
|
@goBack="handleBreadcrumbClick"
|
2026-04-01 22:39:11 +08:00
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 批量操作工具栏 -->
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<div v-if="activeMenu === 'my-files' || activeMenu === 'shared-to-me' || activeMenu === 'my-share' || activeMenu === 'trash'" class="batch-toolbar">
|
2026-04-01 23:03:55 +08:00
|
|
|
|
<el-button-group>
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<el-button v-if="activeMenu !== 'trash'" @click="handleBatchDownload" :disabled="selectedFiles.length === 0">
|
2026-04-01 22:39:11 +08:00
|
|
|
|
<el-icon><Download /></el-icon>
|
|
|
|
|
|
<span style="margin-left: 4px">批量下载</span>
|
|
|
|
|
|
</el-button>
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<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">
|
2026-04-01 22:39:11 +08:00
|
|
|
|
<el-icon><FolderOpened /></el-icon>
|
|
|
|
|
|
<span style="margin-left: 4px">批量移动</span>
|
|
|
|
|
|
</el-button>
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<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>
|
2026-04-01 23:03:55 +08:00
|
|
|
|
</el-button-group>
|
2026-04-01 22:39:11 +08:00
|
|
|
|
</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">
|
2026-04-02 12:06:51 +08:00
|
|
|
|
<FileIcon :icon="getFileIconType(file)" :color="getFileIconColor(file)" :size="36" />
|
2026-04-01 22:39:11 +08:00
|
|
|
|
</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"
|
2026-04-02 12:06:51 +08:00
|
|
|
|
:remaining-storage="remainingStorage"
|
2026-04-01 22:39:11 +08:00
|
|
|
|
@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"
|
|
|
|
|
|
/>
|
2026-04-01 23:50:49 +08:00
|
|
|
|
<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"
|
|
|
|
|
|
/>
|
2026-04-01 22:39:11 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
|
|
|
|
|
import { ref, computed, onMounted } from 'vue'
|
|
|
|
|
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
2026-04-02 12:06:51 +08:00
|
|
|
|
import { RefreshLeft, Delete, View, Share, Download, CloseBold, Edit, FolderOpened, Close } from '@element-plus/icons-vue'
|
2026-04-01 22:39:11 +08:00
|
|
|
|
import FileSidebar from '@/components/FileSidebar.vue'
|
|
|
|
|
|
import FileToolbar from '@/components/FileToolbar.vue'
|
|
|
|
|
|
import FileTable from '@/components/FileTable.vue'
|
2026-04-02 12:06:51 +08:00
|
|
|
|
import FileIcon from '@/components/FileIcon.vue'
|
2026-04-01 22:39:11 +08:00
|
|
|
|
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'
|
2026-04-01 23:50:49 +08:00
|
|
|
|
import BatchMoveDialog from '@/components/BatchMoveDialog.vue'
|
2026-04-01 22:39:11 +08:00
|
|
|
|
import {
|
|
|
|
|
|
getFiles, getTrashFiles, getSharedByMe, getSharedByMeFolderFiles, getSharedToMe, getSharedFolderFiles,
|
|
|
|
|
|
uploadFiles, downloadFile, deleteFile, restoreFile,
|
|
|
|
|
|
deletePermanently, emptyTrash, createFolder,
|
2026-04-02 12:06:51 +08:00
|
|
|
|
shareFileApi, cancelShare, getFilePreview, renameFile, moveFile, batchDownload, getMovableFolders,
|
|
|
|
|
|
batchCancelShare, batchRestore
|
2026-04-01 22:39:11 +08:00
|
|
|
|
} 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)
|
2026-04-02 12:06:51 +08:00
|
|
|
|
|
|
|
|
|
|
const handleOpenUpload = () => {
|
|
|
|
|
|
if (remainingStorage.value <= 0) {
|
|
|
|
|
|
ElMessage.warning('存储空间已满,无法上传文件')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
uploadVisible.value = true
|
|
|
|
|
|
}
|
2026-04-01 22:39:11 +08:00
|
|
|
|
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([])
|
2026-04-01 23:50:49 +08:00
|
|
|
|
const batchMoveVisible = ref(false)
|
|
|
|
|
|
const movableFolders = ref([])
|
2026-04-01 22:39:11 +08:00
|
|
|
|
|
|
|
|
|
|
// 存储 —— 真实数据
|
|
|
|
|
|
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'
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 剩余存储空间(字节)
|
|
|
|
|
|
const remainingStorage = computed(() => {
|
|
|
|
|
|
const limit = userStore.storageLimit || 20 * 1024 * 1024 * 1024
|
|
|
|
|
|
const used = userStore.storageUsed || 0
|
|
|
|
|
|
return Math.max(0, limit - used)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-04-01 22:39:11 +08:00
|
|
|
|
// 刷新存储数据(从后端精确重算)
|
|
|
|
|
|
const refreshStorage = async () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const res = await getCurrentUser()
|
|
|
|
|
|
const data = res.data
|
|
|
|
|
|
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()
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 支持预览的文件类型:图片、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)
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 分页
|
|
|
|
|
|
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 res
|
|
|
|
|
|
switch (activeMenu.value) {
|
|
|
|
|
|
case 'trash': res = await getTrashFiles(currentFolderId.value ? { folderId: currentFolderId.value } : {}); break
|
|
|
|
|
|
case 'my-share':
|
|
|
|
|
|
if (currentFolderId.value) {
|
|
|
|
|
|
res = await getSharedByMeFolderFiles(currentFolderId.value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res = await getSharedByMe()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
case 'shared-to-me':
|
|
|
|
|
|
if (currentFolderId.value) {
|
|
|
|
|
|
res = await getSharedFolderFiles(currentFolderId.value)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
res = await getSharedToMe()
|
|
|
|
|
|
}
|
|
|
|
|
|
break
|
|
|
|
|
|
default: res = await getFiles({ folderId: currentFolderId.value, keyword: searchKeyword.value })
|
|
|
|
|
|
}
|
|
|
|
|
|
files.value = res.data || []
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
console.error(e)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleMenuSelect = (index) => {
|
|
|
|
|
|
activeMenu.value = index
|
|
|
|
|
|
searchKeyword.value = ''
|
|
|
|
|
|
currentFolderId.value = null
|
|
|
|
|
|
folderStack.value = []
|
|
|
|
|
|
loadFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 进入目录
|
2026-04-01 22:39:11 +08:00
|
|
|
|
const enterFolder = (folder) => {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 先把当前目录信息压入栈(用于返回)
|
|
|
|
|
|
folderStack.value.push({
|
|
|
|
|
|
id: folder.id,
|
|
|
|
|
|
name: folder.name
|
|
|
|
|
|
})
|
|
|
|
|
|
// 进入新目录
|
2026-04-01 22:39:11 +08:00
|
|
|
|
currentFolderId.value = folder.id
|
|
|
|
|
|
loadFiles()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 返回上级
|
|
|
|
|
|
const goBack = () => {
|
|
|
|
|
|
if (!folderStack.value.length) return
|
2026-04-02 12:06:51 +08:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2026-04-01 22:39:11 +08:00
|
|
|
|
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 = ''
|
2026-04-02 12:06:51 +08:00
|
|
|
|
previewVisible.value = true // 先打开弹窗
|
|
|
|
|
|
|
2026-04-01 22:39:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
const blob = await getFilePreview(file.id)
|
|
|
|
|
|
const ext = file.name.split('.').pop()?.toLowerCase()
|
2026-04-02 12:06:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 需要用 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
|
2026-04-01 22:39:11 +08:00
|
|
|
|
previewUrl.value = URL.createObjectURL(blob)
|
|
|
|
|
|
} else {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 文本文件直接读取内容
|
2026-04-01 22:39:11 +08:00
|
|
|
|
previewContent.value = await blob.text()
|
2026-04-02 12:06:51 +08:00
|
|
|
|
console.log('Text content loaded:', previewContent.value?.substring(0, 100))
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
} catch (e) {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
console.error('Preview error:', e)
|
2026-04-01 22:39:11 +08:00
|
|
|
|
ElMessage.error('预览失败')
|
2026-04-02 12:06:51 +08:00
|
|
|
|
previewVisible.value = false
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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) => {
|
2026-04-01 23:50:49 +08:00
|
|
|
|
await ElMessageBox.confirm("确定删除此文件或目录吗?", '提示', { type: 'warning' })
|
2026-04-01 22:39:11 +08:00
|
|
|
|
try {
|
|
|
|
|
|
await deleteFile(file.id)
|
|
|
|
|
|
ElMessage.success('已移至回收站')
|
|
|
|
|
|
loadFiles()
|
|
|
|
|
|
} catch (e) {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 显示后端返回的错误信息
|
|
|
|
|
|
const errorMsg = e.response?.data?.message || e.message || '删除失败'
|
|
|
|
|
|
ElMessage.error(errorMsg)
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRestore = async (file) => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await restoreFile(file.id)
|
|
|
|
|
|
ElMessage.success('已还原')
|
|
|
|
|
|
loadFiles()
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
ElMessage.error('还原失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleDeletePermanently = async (file) => {
|
2026-04-01 23:50:49 +08:00
|
|
|
|
await ElMessageBox.confirm("确定彻底删除此文件或目录吗?", '警告', { type: 'error' })
|
2026-04-01 22:39:11 +08:00
|
|
|
|
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')
|
2026-04-01 23:03:55 +08:00
|
|
|
|
if (filesToDownload.length === 0 && selectedFiles.value.length > 0) {
|
2026-04-01 23:50:49 +08:00
|
|
|
|
ElMessage.warning('请选择要下载的文件')
|
2026-04-01 22:39:11 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 23:03:55 +08:00
|
|
|
|
if (selectedFiles.value.length === 0) {
|
|
|
|
|
|
ElMessage.warning('请选择要下载的文件')
|
|
|
|
|
|
return
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 23:03:55 +08:00
|
|
|
|
try {
|
|
|
|
|
|
ElMessage.info('正在打包下载...')
|
|
|
|
|
|
const ids = selectedFiles.value.map(f => f.id)
|
|
|
|
|
|
const blob = await batchDownload(ids)
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 检查返回的是否是错误信息(JSON 格式)
|
|
|
|
|
|
if (blob.type && blob.type.includes('application/json')) {
|
|
|
|
|
|
const text = await blob.text()
|
|
|
|
|
|
const error = JSON.parse(text)
|
|
|
|
|
|
ElMessage.error(error.message || '下载失败')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 23:03:55 +08:00
|
|
|
|
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) {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// 处理 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('下载失败')
|
|
|
|
|
|
}
|
2026-04-01 23:03:55 +08:00
|
|
|
|
}
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleBatchMove = async () => {
|
|
|
|
|
|
if (selectedFiles.value.length === 0) {
|
|
|
|
|
|
ElMessage.warning('请选择要移动的文件')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const selectedIds = selectedFiles.value.map(f => f.id)
|
2026-04-01 23:50:49 +08:00
|
|
|
|
const res = await getMovableFolders(selectedIds, currentFolderId.value)
|
|
|
|
|
|
movableFolders.value = res.data || []
|
|
|
|
|
|
batchMoveVisible.value = true
|
2026-04-01 22:39:11 +08:00
|
|
|
|
} catch (e) {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
ElMessage.error('获取目录列表失败')
|
2026-04-01 23:50:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleConfirmBatchMove = async (targetFolderId) => {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
// Handle target folder ID
|
2026-04-01 23:50:49 +08:00
|
|
|
|
let finalFolderId = null
|
2026-04-02 12:06:51 +08:00
|
|
|
|
|
|
|
|
|
|
if (targetFolderId === 'root' || targetFolderId === '' || targetFolderId === null || targetFolderId === undefined) {
|
2026-04-01 23:50:49 +08:00
|
|
|
|
finalFolderId = null
|
|
|
|
|
|
} else {
|
|
|
|
|
|
finalFolderId = targetFolderId
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
let successCount = 0
|
|
|
|
|
|
let failCount = 0
|
|
|
|
|
|
|
2026-04-01 23:50:49 +08:00
|
|
|
|
for (const file of selectedFiles.value) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
await moveFile(file.id, finalFolderId)
|
2026-04-02 12:06:51 +08:00
|
|
|
|
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++
|
2026-04-01 23:50:49 +08:00
|
|
|
|
} catch (e) {
|
2026-04-02 12:06:51 +08:00
|
|
|
|
failCount++
|
|
|
|
|
|
const errorMsg = e.response?.data?.message || e.message || '删除失败'
|
|
|
|
|
|
if (!errors.includes(errorMsg)) {
|
|
|
|
|
|
errors.push(errorMsg)
|
|
|
|
|
|
}
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-01 23:50:49 +08:00
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
if (successCount > 0) {
|
|
|
|
|
|
ElMessage.success(`成功删除 ${successCount} 个文件${failCount > 0 ? `,${failCount} 个失败` : ''}`)
|
|
|
|
|
|
} else if (failCount > 0) {
|
|
|
|
|
|
ElMessage.error(errors[0] || '删除失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 23:50:49 +08:00
|
|
|
|
selectedFiles.value = []
|
|
|
|
|
|
loadFiles()
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const handleRowDblClick = (row) => {
|
|
|
|
|
|
if (row.type === 'folder') {
|
|
|
|
|
|
enterFolder(row)
|
2026-04-02 15:02:54 +08:00
|
|
|
|
} else if (canPreview(row)) {
|
|
|
|
|
|
handlePreview(row)
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-02 12:06:51 +08:00
|
|
|
|
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('操作失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-01 22:39:11 +08:00
|
|
|
|
onMounted(async () => {
|
|
|
|
|
|
await refreshStorage()
|
|
|
|
|
|
loadFiles()
|
|
|
|
|
|
})
|
2026-04-02 12:06:51 +08:00
|
|
|
|
|
|
|
|
|
|
// 文件图标类型
|
|
|
|
|
|
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'
|
|
|
|
|
|
}
|
2026-04-01 22:39:11 +08:00
|
|
|
|
</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;
|
2026-04-01 23:03:55 +08:00
|
|
|
|
justify-content: flex-start;
|
|
|
|
|
|
gap: 16px;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
padding: 12px 16px;
|
|
|
|
|
|
border-bottom: 1px solid #e4e7ed;
|
2026-04-01 23:03:55 +08:00
|
|
|
|
background: #fff;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-count {
|
|
|
|
|
|
font-size: 14px;
|
2026-04-01 23:03:55 +08:00
|
|
|
|
color: #606266;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.batch-actions {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-content {
|
|
|
|
|
|
flex: 1;
|
|
|
|
|
|
overflow: auto;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.grid-view {
|
|
|
|
|
|
display: grid;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
|
|
|
|
gap: 16px;
|
|
|
|
|
|
padding: 16px;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
align-content: start;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-card {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
padding: 16px;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
border: 1px solid #e4e7ed;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
|
background: #fff;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
min-height: 140px;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.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;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
gap: 4px;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
justify-content: center;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
overflow: hidden;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-card-actions .el-button {
|
|
|
|
|
|
font-size: 12px;
|
2026-04-02 12:52:40 +08:00
|
|
|
|
padding: 4px 8px;
|
|
|
|
|
|
min-width: 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-card-actions .el-button + .el-button {
|
|
|
|
|
|
margin-left: 0;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-card-actions .el-button span {
|
2026-04-02 12:52:40 +08:00
|
|
|
|
display: inline;
|
2026-04-01 22:39:11 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.pagination-wrapper {
|
|
|
|
|
|
padding: 10px 16px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-top: 1px solid #e4e7ed;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|