- 添加基于 VbenAdmin + Vue3 + Element Plus 的前端管理系统 - 包含完整的 UI 组件库和工具链 - 支持多应用架构 (web-ele, backend-mock, playground) - 包含完整的开发规范和配置 - 修复 admin 目录的子模块问题,确保正确提交
1248 lines
33 KiB
Vue
1248 lines
33 KiB
Vue
<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> |