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