Files
wwjcloud/admin/apps/web-ele/src/views/common/file/index.vue
万物街 dc6e9baec0 feat: 添加完整的前端管理系统 (VbenAdmin)
- 添加基于 VbenAdmin + Vue3 + Element Plus 的前端管理系统
- 包含完整的 UI 组件库和工具链
- 支持多应用架构 (web-ele, backend-mock, playground)
- 包含完整的开发规范和配置
- 修复 admin 目录的子模块问题,确保正确提交
2025-08-23 13:24:04 +08:00

1248 lines
33 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>
<Page
description="管理系统文件上传、存储和分类"
title="文件管理"
>
<!-- 搜索表单 -->
<el-card>
<el-form
ref="searchFormRef"
:model="searchForm"
:inline="true"
label-width="80px"
>
<el-form-item label="文件名称" prop="filename">
<el-input
v-model="searchForm.filename"
placeholder="请输入文件名称"
clearable
style="width: 200px"
/>
</el-form-item>
<el-form-item label="文件类型" prop="type">
<el-select
v-model="searchForm.type"
placeholder="请选择文件类型"
clearable
style="width: 150px"
>
<el-option label="图片" value="image" />
<el-option label="视频" value="video" />
<el-option label="音频" value="audio" />
<el-option label="文档" value="document" />
<el-option label="压缩包" value="archive" />
<el-option label="其他" value="other" />
</el-select>
</el-form-item>
<el-form-item label="存储驱动" prop="driver">
<el-select
v-model="searchForm.driver"
placeholder="请选择存储驱动"
clearable
style="width: 150px"
>
<el-option label="本地存储" value="local" />
<el-option label="阿里云OSS" value="oss" />
<el-option label="腾讯云COS" value="cos" />
<el-option label="七牛云" value="qiniu" />
<el-option label="又拍云" value="upyun" />
</el-select>
</el-form-item>
<el-form-item label="分类" prop="categoryId">
<el-tree-select
v-model="searchForm.categoryId"
:data="categoryTree"
placeholder="请选择分类"
clearable
check-strictly
:render-after-expand="false"
style="width: 200px"
/>
</el-form-item>
<el-form-item label="上传时间" prop="dateRange">
<el-date-picker
v-model="searchForm.dateRange"
type="daterange"
range-separator=""
start-placeholder="开始日期"
end-placeholder="结束日期"
format="YYYY-MM-DD"
value-format="YYYY-MM-DD"
style="width: 240px"
/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="handleSearch">
<Icon icon="ep:search" class="mr-1" />
搜索
</el-button>
<el-button @click="handleResetSearch">
<Icon icon="ep:refresh" class="mr-1" />
重置
</el-button>
</el-form-item>
</el-form>
</el-card>
</div>
<!-- 操作栏 -->
<div class="action-bar">
<div class="action-left">
<el-button type="primary" @click="handleUpload">
<Icon icon="ep:upload" class="mr-1" />
上传文件
</el-button>
<el-button @click="handleCreateCategory">
<Icon icon="ep:folder-add" class="mr-1" />
新建分类
</el-button>
<el-button
type="danger"
:disabled="!selectedFiles.length"
@click="handleBatchDelete"
>
<Icon icon="ep:delete" class="mr-1" />
批量删除
</el-button>
<el-button
:disabled="!selectedFiles.length"
@click="handleBatchMove"
>
<Icon icon="ep:folder" class="mr-1" />
批量移动
</el-button>
</div>
<div class="action-right">
<el-radio-group v-model="viewMode" size="small">
<el-radio-button label="grid">
<Icon icon="ep:grid" />
</el-radio-button>
<el-radio-button label="list">
<Icon icon="ep:list" />
</el-radio-button>
</el-radio-group>
</div>
</div>
<!-- 文件列表 -->
<div class="file-content">
<el-row :gutter="20">
<!-- 分类树 -->
<el-col :span="6">
<el-card class="category-card">
<template #header>
<div class="card-header">
<span>文件分类</span>
<el-button type="primary" text @click="handleCreateCategory">
<Icon icon="ep:plus" />
</el-button>
</div>
</template>
<el-tree
ref="categoryTreeRef"
:data="categoryTree"
:props="{ label: 'name', children: 'children' }"
node-key="id"
:current-node-key="currentCategoryId"
:expand-on-click-node="false"
@node-click="handleCategoryClick"
>
<template #default="{ node, data }">
<div class="category-node">
<Icon icon="ep:folder" class="mr-1" />
<span>{{ data.name }}</span>
<span class="file-count">({{ data.fileCount || 0 }})</span>
<div class="node-actions">
<el-button
type="primary"
text
size="small"
@click.stop="handleEditCategory(data)"
>
<Icon icon="ep:edit" />
</el-button>
<el-button
type="danger"
text
size="small"
@click.stop="handleDeleteCategory(data)"
>
<Icon icon="ep:delete" />
</el-button>
</div>
</div>
</template>
</el-tree>
</el-card>
</el-col>
<!-- 文件区域 -->
<el-col :span="18">
<el-card class="file-card">
<!-- 网格视图 -->
<div v-if="viewMode === 'grid'" class="file-grid" v-loading="loading">
<div
v-for="file in fileList"
:key="file.id"
class="file-item"
:class="{ selected: selectedFiles.includes(file.id) }"
@click="handleFileSelect(file)"
>
<div class="file-checkbox">
<el-checkbox
:model-value="selectedFiles.includes(file.id)"
@change="handleFileCheck(file.id, $event)"
@click.stop
/>
</div>
<div class="file-preview">
<el-image
v-if="isImage(file.type)"
:src="file.url"
:preview-src-list="[file.url]"
fit="cover"
class="file-image"
/>
<div v-else class="file-icon">
<Icon :icon="getFileIcon(file.type)" size="48" />
</div>
</div>
<div class="file-info">
<div class="file-name" :title="file.originalName">
{{ file.originalName }}
</div>
<div class="file-meta">
<span class="file-size">{{ formatFileSize(file.size) }}</span>
<span class="file-time">{{ formatTime(file.createdAt) }}</span>
</div>
</div>
<div class="file-actions">
<el-dropdown trigger="click" @click.stop>
<el-button type="primary" text>
<Icon icon="ep:more" />
</el-button>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="handlePreview(file)">
<Icon icon="ep:view" class="mr-1" />
预览
</el-dropdown-item>
<el-dropdown-item @click="handleDownload(file)">
<Icon icon="ep:download" class="mr-1" />
下载
</el-dropdown-item>
<el-dropdown-item @click="handleCopyUrl(file)">
<Icon icon="ep:copy-document" class="mr-1" />
复制链接
</el-dropdown-item>
<el-dropdown-item @click="handleEditFile(file)">
<Icon icon="ep:edit" class="mr-1" />
编辑
</el-dropdown-item>
<el-dropdown-item @click="handleMoveFile(file)">
<Icon icon="ep:folder" class="mr-1" />
移动
</el-dropdown-item>
<el-dropdown-item divided @click="handleDeleteFile(file)">
<Icon icon="ep:delete" class="mr-1" />
删除
</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</div>
<!-- 列表视图 -->
<div v-else class="file-list">
<el-table
:data="fileList"
v-loading="loading"
@selection-change="handleSelectionChange"
>
<el-table-column type="selection" width="55" />
<el-table-column label="文件名" min-width="200">
<template #default="{ row }">
<div class="file-name-cell">
<div class="file-icon-small">
<Icon :icon="getFileIcon(row.type)" size="20" />
</div>
<div class="file-info-text">
<div class="file-name">{{ row.originalName }}</div>
<div class="file-path">{{ row.path }}</div>
</div>
</div>
</template>
</el-table-column>
<el-table-column label="类型" width="100">
<template #default="{ row }">
<el-tag size="small">{{ getFileTypeLabel(row.type) }}</el-tag>
</template>
</el-table-column>
<el-table-column label="大小" width="100">
<template #default="{ row }">
{{ formatFileSize(row.size) }}
</template>
</el-table-column>
<el-table-column label="存储驱动" width="100">
<template #default="{ row }">
<el-tag type="info" size="small">{{ row.driver }}</el-tag>
</template>
</el-table-column>
<el-table-column label="上传时间" width="180">
<template #default="{ row }">
{{ formatTime(row.createdAt) }}
</template>
</el-table-column>
<el-table-column label="操作" width="200" fixed="right">
<template #default="{ row }">
<el-button type="primary" text @click="handlePreview(row)">
预览
</el-button>
<el-button type="primary" text @click="handleDownload(row)">
下载
</el-button>
<el-button type="primary" text @click="handleEditFile(row)">
编辑
</el-button>
<el-button type="danger" text @click="handleDeleteFile(row)">
删除
</el-button>
</template>
</el-table-column>
</el-table>
</div>
<!-- 分页 -->
<div class="pagination-container">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.size"
:total="pagination.total"
:page-sizes="[20, 50, 100, 200]"
layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
/>
</div>
</el-card>
</el-col>
</el-row>
</div>
<!-- 上传对话框 -->
<el-dialog
v-model="uploadDialogVisible"
title="上传文件"
width="600px"
:close-on-click-modal="false"
>
<el-upload
ref="uploadRef"
:action="uploadAction"
:headers="uploadHeaders"
:data="uploadData"
:multiple="true"
:drag="true"
:auto-upload="false"
:on-change="handleFileChange"
:before-upload="handleBeforeUpload"
:on-success="handleUploadSuccess"
:on-error="handleUploadError"
class="upload-area"
>
<Icon icon="ep:upload-filled" size="48" class="upload-icon" />
<div class="upload-text">
<p>将文件拖到此处<em>点击上传</em></p>
<p class="upload-tip">支持多文件上传单个文件不超过 {{ maxFileSize }}MB</p>
</div>
</el-upload>
<div class="upload-options">
<el-form :model="uploadForm" label-width="80px">
<el-form-item label="上传分类">
<el-tree-select
v-model="uploadForm.categoryId"
:data="categoryTree"
placeholder="请选择分类"
clearable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="存储驱动">
<el-select v-model="uploadForm.driver" placeholder="请选择存储驱动">
<el-option label="本地存储" value="local" />
<el-option label="阿里云OSS" value="oss" />
<el-option label="腾讯云COS" value="cos" />
<el-option label="七牛云" value="qiniu" />
</el-select>
</el-form-item>
</el-form>
</div>
<template #footer>
<el-button @click="uploadDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleStartUpload" :loading="uploading">
开始上传
</el-button>
</template>
</el-dialog>
<!-- 分类对话框 -->
<el-dialog
v-model="categoryDialogVisible"
:title="categoryForm.id ? '编辑分类' : '新建分类'"
width="500px"
:close-on-click-modal="false"
>
<el-form
ref="categoryFormRef"
:model="categoryForm"
:rules="categoryRules"
label-width="80px"
>
<el-form-item label="上级分类" prop="parentId">
<el-tree-select
v-model="categoryForm.parentId"
:data="categoryTree"
placeholder="请选择上级分类(不选则为顶级分类)"
clearable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="分类名称" prop="name">
<el-input
v-model="categoryForm.name"
placeholder="请输入分类名称"
clearable
/>
</el-form-item>
<el-form-item label="排序" prop="sort">
<el-input-number
v-model="categoryForm.sort"
:min="0"
:max="9999"
placeholder="排序值"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="categoryForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="categoryDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveCategory" :loading="saveLoading">
确定
</el-button>
</template>
</el-dialog>
<!-- 文件编辑对话框 -->
<el-dialog
v-model="editFileDialogVisible"
title="编辑文件"
width="500px"
:close-on-click-modal="false"
>
<el-form
ref="editFileFormRef"
:model="editFileForm"
:rules="editFileRules"
label-width="80px"
>
<el-form-item label="文件名称" prop="originalName">
<el-input
v-model="editFileForm.originalName"
placeholder="请输入文件名称"
clearable
/>
</el-form-item>
<el-form-item label="所属分类" prop="categoryId">
<el-tree-select
v-model="editFileForm.categoryId"
:data="categoryTree"
placeholder="请选择分类"
clearable
check-strictly
:render-after-expand="false"
/>
</el-form-item>
<el-form-item label="备注" prop="remark">
<el-input
v-model="editFileForm.remark"
type="textarea"
:rows="3"
placeholder="请输入备注"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="editFileDialogVisible = false">取消</el-button>
<el-button type="primary" @click="handleSaveFile" :loading="saveLoading">
确定
</el-button>
</template>
</el-dialog>
</Page>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted, computed } from 'vue';
import { ElMessage, ElMessageBox, type FormInstance, type UploadInstance } from 'element-plus';
import { Icon } from '@iconify/vue';
import { Page } from '@vben/common-ui';
import {
getFileListApi,
getFileCategoriesApi,
createFileCategoryApi,
updateFileCategoryApi,
deleteFileCategoryApi,
updateFileApi,
deleteFileApi,
batchDeleteFilesApi,
moveFileApi,
batchMoveFilesApi,
type FileItem,
type FileCategory,
type FileListParams,
type CreateFileCategoryParams,
type UpdateFileCategoryParams,
type UpdateFileParams,
} from '#/api/upload';
import { useUserStore } from '#/store/modules/user';
// 响应式数据
const loading = ref(false);
const saveLoading = ref(false);
const uploading = ref(false);
const viewMode = ref<'grid' | 'list'>('grid');
const currentCategoryId = ref<number | null>(null);
const selectedFiles = ref<number[]>([]);
const fileList = ref<FileItem[]>([]);
const categoryTree = ref<FileCategory[]>([]);
const uploadDialogVisible = ref(false);
const categoryDialogVisible = ref(false);
const editFileDialogVisible = ref(false);
const searchFormRef = ref<FormInstance>();
const categoryFormRef = ref<FormInstance>();
const editFileFormRef = ref<FormInstance>();
const uploadRef = ref<UploadInstance>();
const categoryTreeRef = ref();
const userStore = useUserStore();
// 搜索表单
const searchForm = reactive({
filename: '',
type: '',
driver: '',
categoryId: null as number | null,
dateRange: [] as string[],
});
// 分页
const pagination = reactive({
page: 1,
size: 20,
total: 0,
});
// 上传表单
const uploadForm = reactive({
categoryId: null as number | null,
driver: 'local',
});
// 分类表单
const categoryForm = reactive({
id: null as number | null,
parentId: null as number | null,
name: '',
sort: 0,
remark: '',
});
// 文件编辑表单
const editFileForm = reactive({
id: null as number | null,
originalName: '',
categoryId: null as number | null,
remark: '',
});
// 表单验证规则
const categoryRules = {
name: [
{ required: true, message: '请输入分类名称', trigger: 'blur' },
{ min: 1, max: 50, message: '分类名称长度为 1-50 个字符', trigger: 'blur' },
],
};
const editFileRules = {
originalName: [
{ required: true, message: '请输入文件名称', trigger: 'blur' },
{ min: 1, max: 255, message: '文件名称长度为 1-255 个字符', trigger: 'blur' },
],
};
// 计算属性
const uploadAction = computed(() => '/api/upload/file');
const uploadHeaders = computed(() => ({
Authorization: `Bearer ${userStore.token}`,
}));
const uploadData = computed(() => ({
categoryId: uploadForm.categoryId,
driver: uploadForm.driver,
}));
const maxFileSize = computed(() => 100); // MB
// 方法
const loadFileList = async () => {
loading.value = true;
try {
const params: FileListParams = {
page: pagination.page,
size: pagination.size,
filename: searchForm.filename || undefined,
type: searchForm.type || undefined,
driver: searchForm.driver || undefined,
categoryId: searchForm.categoryId || currentCategoryId.value || undefined,
startDate: searchForm.dateRange[0] || undefined,
endDate: searchForm.dateRange[1] || undefined,
};
const result = await getFileListApi(params);
fileList.value = result.list;
pagination.total = result.total;
} catch (error) {
ElMessage.error('加载文件列表失败');
} finally {
loading.value = false;
}
};
const loadCategories = async () => {
try {
categoryTree.value = await getFileCategoriesApi();
} catch (error) {
ElMessage.error('加载分类列表失败');
}
};
const handleSearch = () => {
pagination.page = 1;
loadFileList();
};
const handleResetSearch = () => {
searchFormRef.value?.resetFields();
pagination.page = 1;
loadFileList();
};
const handleSizeChange = (size: number) => {
pagination.size = size;
pagination.page = 1;
loadFileList();
};
const handleCurrentChange = (page: number) => {
pagination.page = page;
loadFileList();
};
const handleCategoryClick = (data: FileCategory) => {
currentCategoryId.value = data.id;
pagination.page = 1;
loadFileList();
};
const handleFileSelect = (file: FileItem) => {
const index = selectedFiles.value.indexOf(file.id);
if (index > -1) {
selectedFiles.value.splice(index, 1);
} else {
selectedFiles.value.push(file.id);
}
};
const handleFileCheck = (fileId: number, checked: boolean) => {
if (checked) {
if (!selectedFiles.value.includes(fileId)) {
selectedFiles.value.push(fileId);
}
} else {
const index = selectedFiles.value.indexOf(fileId);
if (index > -1) {
selectedFiles.value.splice(index, 1);
}
}
};
const handleSelectionChange = (selection: FileItem[]) => {
selectedFiles.value = selection.map(item => item.id);
};
const handleUpload = () => {
uploadDialogVisible.value = true;
};
const handleFileChange = (file: any, fileList: any[]) => {
// 文件选择变化处理
};
const handleBeforeUpload = (file: File) => {
const isValidSize = file.size / 1024 / 1024 < maxFileSize.value;
if (!isValidSize) {
ElMessage.error(`文件大小不能超过 ${maxFileSize.value}MB`);
return false;
}
return true;
};
const handleStartUpload = () => {
uploadRef.value?.submit();
};
const handleUploadSuccess = (response: any, file: any) => {
ElMessage.success('文件上传成功');
loadFileList();
};
const handleUploadError = (error: any) => {
ElMessage.error('文件上传失败');
};
const handleCreateCategory = () => {
Object.assign(categoryForm, {
id: null,
parentId: currentCategoryId.value,
name: '',
sort: 0,
remark: '',
});
categoryDialogVisible.value = true;
};
const handleEditCategory = (category: FileCategory) => {
Object.assign(categoryForm, {
id: category.id,
parentId: category.parentId,
name: category.name,
sort: category.sort,
remark: category.remark,
});
categoryDialogVisible.value = true;
};
const handleSaveCategory = async () => {
if (!categoryFormRef.value) return;
try {
await categoryFormRef.value.validate();
saveLoading.value = true;
if (categoryForm.id) {
const updateData: UpdateFileCategoryParams = {
name: categoryForm.name,
parentId: categoryForm.parentId,
sort: categoryForm.sort,
remark: categoryForm.remark,
};
await updateFileCategoryApi(categoryForm.id, updateData);
ElMessage.success('分类更新成功');
} else {
const createData: CreateFileCategoryParams = {
name: categoryForm.name,
parentId: categoryForm.parentId,
sort: categoryForm.sort,
remark: categoryForm.remark,
};
await createFileCategoryApi(createData);
ElMessage.success('分类创建成功');
}
categoryDialogVisible.value = false;
await loadCategories();
} catch (error) {
ElMessage.error('保存失败');
} finally {
saveLoading.value = false;
}
};
const handleDeleteCategory = async (category: FileCategory) => {
try {
await ElMessageBox.confirm(
`确定要删除分类 "${category.name}" 吗?删除后该分类下的文件将移动到未分类。`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await deleteFileCategoryApi(category.id);
ElMessage.success('分类删除成功');
await loadCategories();
if (currentCategoryId.value === category.id) {
currentCategoryId.value = null;
loadFileList();
}
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
}
}
};
const handlePreview = (file: FileItem) => {
if (isImage(file.type)) {
// 图片预览
window.open(file.url, '_blank');
} else {
// 其他文件类型预览或下载
handleDownload(file);
}
};
const handleDownload = (file: FileItem) => {
const link = document.createElement('a');
link.href = file.url;
link.download = file.originalName;
link.target = '_blank';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
const handleCopyUrl = async (file: FileItem) => {
try {
await navigator.clipboard.writeText(file.url);
ElMessage.success('链接已复制到剪贴板');
} catch (error) {
ElMessage.error('复制失败');
}
};
const handleEditFile = (file: FileItem) => {
Object.assign(editFileForm, {
id: file.id,
originalName: file.originalName,
categoryId: file.categoryId,
remark: file.remark,
});
editFileDialogVisible.value = true;
};
const handleSaveFile = async () => {
if (!editFileFormRef.value) return;
try {
await editFileFormRef.value.validate();
saveLoading.value = true;
const updateData: UpdateFileParams = {
originalName: editFileForm.originalName,
categoryId: editFileForm.categoryId,
remark: editFileForm.remark,
};
await updateFileApi(editFileForm.id!, updateData);
ElMessage.success('文件更新成功');
editFileDialogVisible.value = false;
loadFileList();
} catch (error) {
ElMessage.error('保存失败');
} finally {
saveLoading.value = false;
}
};
const handleDeleteFile = async (file: FileItem) => {
try {
await ElMessageBox.confirm(
`确定要删除文件 "${file.originalName}" 吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await deleteFileApi(file.id);
ElMessage.success('文件删除成功');
loadFileList();
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('删除失败');
}
}
};
const handleMoveFile = async (file: FileItem) => {
// 实现文件移动逻辑
ElMessage.info('移动功能开发中');
};
const handleBatchDelete = async () => {
if (!selectedFiles.value.length) return;
try {
await ElMessageBox.confirm(
`确定要删除选中的 ${selectedFiles.value.length} 个文件吗?`,
'确认删除',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
);
await batchDeleteFilesApi(selectedFiles.value);
ElMessage.success('批量删除成功');
selectedFiles.value = [];
loadFileList();
} catch (error) {
if (error !== 'cancel') {
ElMessage.error('批量删除失败');
}
}
};
const handleBatchMove = async () => {
if (!selectedFiles.value.length) return;
ElMessage.info('批量移动功能开发中');
};
// 工具方法
const isImage = (type: string) => {
return type === 'image' || type.startsWith('image/');
};
const getFileIcon = (type: string) => {
const iconMap: Record<string, string> = {
image: 'ep:picture',
video: 'ep:video-play',
audio: 'ep:headphones',
document: 'ep:document',
archive: 'ep:box',
pdf: 'ep:document',
excel: 'ep:document',
word: 'ep:document',
ppt: 'ep:document',
};
return iconMap[type] || 'ep:document';
};
const getFileTypeLabel = (type: string) => {
const labelMap: Record<string, string> = {
image: '图片',
video: '视频',
audio: '音频',
document: '文档',
archive: '压缩包',
pdf: 'PDF',
excel: 'Excel',
word: 'Word',
ppt: 'PPT',
};
return labelMap[type] || '其他';
};
const formatFileSize = (size: number) => {
if (size < 1024) {
return `${size} B`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} KB`;
} else if (size < 1024 * 1024 * 1024) {
return `${(size / 1024 / 1024).toFixed(1)} MB`;
} else {
return `${(size / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
};
const formatTime = (time: string) => {
return new Date(time).toLocaleString('zh-CN');
};
// 生命周期
onMounted(() => {
loadFileList();
loadCategories();
});
</script>
<style scoped>
.file-management-page {
padding: 20px;
}
.search-form {
margin-bottom: 20px;
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
padding: 16px;
background: #fff;
border-radius: 8px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
.action-left {
display: flex;
gap: 12px;
}
.action-right {
display: flex;
align-items: center;
}
.category-card {
height: calc(100vh - 300px);
overflow-y: auto;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.category-node {
display: flex;
align-items: center;
width: 100%;
padding: 4px 0;
}
.file-count {
margin-left: auto;
color: #999;
font-size: 12px;
}
.node-actions {
margin-left: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.category-node:hover .node-actions {
opacity: 1;
}
.file-card {
height: calc(100vh - 300px);
overflow: hidden;
display: flex;
flex-direction: column;
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
flex: 1;
overflow-y: auto;
}
.file-item {
position: relative;
border: 2px solid transparent;
border-radius: 8px;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
cursor: pointer;
transition: all 0.2s;
}
.file-item:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
transform: translateY(-2px);
}
.file-item.selected {
border-color: var(--el-color-primary);
}
.file-checkbox {
position: absolute;
top: 8px;
left: 8px;
z-index: 2;
}
.file-preview {
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background: #f5f7fa;
border-radius: 6px 6px 0 0;
}
.file-image {
width: 100%;
height: 100%;
border-radius: 6px 6px 0 0;
}
.file-icon {
color: #999;
}
.file-info {
padding: 12px;
}
.file-name {
font-size: 14px;
font-weight: 500;
color: #333;
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
display: flex;
justify-content: space-between;
font-size: 12px;
color: #999;
}
.file-actions {
position: absolute;
top: 8px;
right: 8px;
opacity: 0;
transition: opacity 0.2s;
}
.file-item:hover .file-actions {
opacity: 1;
}
.file-list {
flex: 1;
overflow: hidden;
}
.file-name-cell {
display: flex;
align-items: center;
gap: 8px;
}
.file-icon-small {
flex-shrink: 0;
}
.file-info-text {
flex: 1;
min-width: 0;
}
.file-path {
font-size: 12px;
color: #999;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.pagination-container {
display: flex;
justify-content: center;
padding: 20px;
border-top: 1px solid #ebeef5;
}
.upload-area {
margin-bottom: 20px;
}
:deep(.el-upload-dragger) {
width: 100%;
height: 200px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.upload-icon {
color: #c0c4cc;
margin-bottom: 16px;
}
.upload-text {
text-align: center;
}
.upload-text p {
margin: 0;
color: #606266;
}
.upload-text em {
color: var(--el-color-primary);
font-style: normal;
}
.upload-tip {
font-size: 12px;
color: #999;
margin-top: 8px;
}
.upload-options {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ebeef5;
}
</style>