feat(pages): 新增文件管理和图库管理页面
**文件管理页面:** - 实现完整的文件管理界面,支持文件的增删改查操作 - 左侧文件分类导航,支持按文件类型筛选(全部、图片、文档、视频等) - 右侧文件列表,支持表格和卡片两种显示模式 - 完整的搜索功能,支持文件名、服务类型、上传日期范围等条件筛选 - 文件上传功能,支持拖拽上传和点击上传两种方式 - 批量操作功能,支持批量删除选中文件 - 文件预览和下载功能,提升用户体验 **图库管理页面:** - 专门的图片管理界面,针对图片文件进行优化 - 图片网格展示,支持缩略图预览 - 图片上传、编辑、删除等基础管理功能 - 支持图片批量操作和分类管理 - 响应式设计,适配不同屏幕尺寸 **技术特点:** - 严格遵循项目开发规范,统一使用CoiDialog弹框组件 - 所有按钮配备图标,遵循图标使用规范 - 使用NTag组件显示标签,遵循主题色统一规范 - 完整的表单验证和错误处理机制 - 响应式布局设计,良好的用户交互体验 - TypeScript类型安全,完整的类型定义
This commit is contained in:
parent
407178771a
commit
5b2c57ca34
938
src/views/system/file/index.vue
Normal file
938
src/views/system/file/index.vue
Normal file
@ -0,0 +1,938 @@
|
|||||||
|
<template>
|
||||||
|
<div class="file-management p-1 bg-gray-50 h-screen flex flex-col">
|
||||||
|
<div class="flex-1 flex gap-2 overflow-hidden">
|
||||||
|
<!-- 左侧文件分类 -->
|
||||||
|
<div class="w-48 bg-white rounded-lg shadow-sm border border-gray-100 flex flex-col overflow-hidden">
|
||||||
|
<div class="px-4 py-3 border-b border-gray-100">
|
||||||
|
<h3 class="text-sm font-medium text-gray-900">
|
||||||
|
文件分类
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<div class="flex-1 overflow-y-auto">
|
||||||
|
<div class="p-2 space-y-1">
|
||||||
|
<div
|
||||||
|
v-for="category in fileCategories"
|
||||||
|
:key="category.value"
|
||||||
|
class="flex items-center px-3 py-2 rounded-md cursor-pointer transition-colors category-item"
|
||||||
|
:class="selectedCategory === category.value ? 'category-selected' : 'category-unselected'"
|
||||||
|
@click="handleCategoryChange(category.value)"
|
||||||
|
>
|
||||||
|
<component :is="category.icon" class="w-4 h-4 mr-2" />
|
||||||
|
<span class="text-sm flex-1">{{ category.label }}</span>
|
||||||
|
<NTag
|
||||||
|
v-if="selectedCategory === category.value"
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
class="ml-auto"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</NTag>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 右侧文件列表 -->
|
||||||
|
<div class="flex-1 bg-white rounded-lg shadow-sm border border-gray-100 flex flex-col overflow-hidden">
|
||||||
|
<!-- 搜索表单 -->
|
||||||
|
<div class="px-4 py-2 border-b border-gray-100">
|
||||||
|
<n-form
|
||||||
|
ref="searchFormRef"
|
||||||
|
:model="searchForm"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="auto"
|
||||||
|
class="search-form"
|
||||||
|
>
|
||||||
|
<n-grid :cols="5" :x-gap="8" :y-gap="4">
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="文件名称" path="fileName">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.fileName"
|
||||||
|
placeholder="请输入文件名称"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="文件后缀" path="fileSuffix">
|
||||||
|
<n-input
|
||||||
|
v-model:value="searchForm.fileSuffix"
|
||||||
|
placeholder="请输入文件后缀"
|
||||||
|
clearable
|
||||||
|
@keydown.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="存储类型" path="fileService">
|
||||||
|
<n-select
|
||||||
|
v-model:value="searchForm.fileService"
|
||||||
|
placeholder="请选择存储类型"
|
||||||
|
clearable
|
||||||
|
:options="FILE_SERVICE_OPTIONS"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="" path="">
|
||||||
|
<NSpace>
|
||||||
|
<NButton type="primary" @click="handleSearch">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><icon-park-outline:search /></NIcon>
|
||||||
|
</template>
|
||||||
|
搜索
|
||||||
|
</NButton>
|
||||||
|
<NButton @click="handleReset">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><icon-park-outline:refresh /></NIcon>
|
||||||
|
</template>
|
||||||
|
重置
|
||||||
|
</NButton>
|
||||||
|
</NSpace>
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
</n-grid>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格头部操作栏 -->
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 border-b border-gray-100">
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<NButton
|
||||||
|
v-permission="PERMISSIONS.FILE.UPLOAD"
|
||||||
|
type="primary"
|
||||||
|
class="px-3 flex items-center"
|
||||||
|
@click="handleUpload"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||||
|
<icon-park-outline:upload />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
上传文件
|
||||||
|
</NButton>
|
||||||
|
|
||||||
|
<NButton
|
||||||
|
v-permission="PERMISSIONS.FILE.DELETE"
|
||||||
|
type="error"
|
||||||
|
:disabled="selectedRows.length === 0"
|
||||||
|
class="px-3 flex items-center"
|
||||||
|
@click="handleBatchDelete"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<NIcon class="mr-1" style="transform: translateY(-1px)">
|
||||||
|
<icon-park-outline:delete />
|
||||||
|
</NIcon>
|
||||||
|
</template>
|
||||||
|
批量删除
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||||
|
<span>共 {{ pagination.itemCount }} 条</span>
|
||||||
|
<NButton text @click="handleRefresh">
|
||||||
|
<template #icon>
|
||||||
|
<NIcon><icon-park-outline:refresh /></NIcon>
|
||||||
|
</template>
|
||||||
|
</NButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 表格内容 -->
|
||||||
|
<div class="table-wrapper flex-1 overflow-auto pt-4">
|
||||||
|
<!-- 数据表格 -->
|
||||||
|
<NDataTable
|
||||||
|
v-if="tableData.length > 0 || loading"
|
||||||
|
ref="tableRef"
|
||||||
|
:columns="columns"
|
||||||
|
:data="tableData"
|
||||||
|
:loading="loading"
|
||||||
|
:row-key="(row: SysFileVo) => row.fileId"
|
||||||
|
:checked-row-keys="selectedRowKeys"
|
||||||
|
:on-update:checked-row-keys="handleRowKeysUpdate"
|
||||||
|
:bordered="false"
|
||||||
|
:single-line="false"
|
||||||
|
:scroll-x="1600"
|
||||||
|
size="small"
|
||||||
|
class="custom-table"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- 空状态 -->
|
||||||
|
<CoiEmpty
|
||||||
|
v-else
|
||||||
|
type="empty"
|
||||||
|
title="暂无文件数据"
|
||||||
|
description="当前没有找到任何文件记录"
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 分页器 -->
|
||||||
|
<div v-if="tableData.length > 0" class="flex items-center px-4 py-2 border-t border-gray-100">
|
||||||
|
<div class="text-sm text-gray-500 mr-4">
|
||||||
|
共 {{ pagination.itemCount }} 条
|
||||||
|
</div>
|
||||||
|
<n-pagination
|
||||||
|
v-model:page="pagination.page"
|
||||||
|
v-model:page-size="pagination.pageSize"
|
||||||
|
:item-count="pagination.itemCount"
|
||||||
|
:show-size-picker="true"
|
||||||
|
:page-sizes="[10, 20, 50, 100]"
|
||||||
|
show-quick-jumper
|
||||||
|
@update:page="handlePageChange"
|
||||||
|
@update:page-size="handlePageSizeChange"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 文件上传弹框 -->
|
||||||
|
<CoiDialog
|
||||||
|
ref="uploadDialogRef"
|
||||||
|
title="上传操作 ☀️"
|
||||||
|
:width="600"
|
||||||
|
height="auto"
|
||||||
|
confirm-text="确定"
|
||||||
|
cancel-text="取消"
|
||||||
|
@coi-confirm="handleUploadConfirm"
|
||||||
|
@coi-cancel="handleUploadCancel"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
<div class="px-3 py-2">
|
||||||
|
<n-form
|
||||||
|
ref="uploadFormRef"
|
||||||
|
:model="uploadForm"
|
||||||
|
:rules="uploadRules"
|
||||||
|
label-placement="left"
|
||||||
|
label-width="100px"
|
||||||
|
require-mark-placement="right-hanging"
|
||||||
|
>
|
||||||
|
<!-- 文件服务选择 -->
|
||||||
|
<n-form-item label="文件服务" path="fileService">
|
||||||
|
<n-select
|
||||||
|
v-model:value="uploadForm.fileService"
|
||||||
|
placeholder="请选择文件服务"
|
||||||
|
:options="[
|
||||||
|
{ label: 'LOCAL', value: 'LOCAL' },
|
||||||
|
{ label: 'MINIO', value: 'MINIO' },
|
||||||
|
{ label: 'OSS', value: 'OSS' },
|
||||||
|
]"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 文件回显路径 -->
|
||||||
|
<n-form-item label="文件回显路径" path="filePath">
|
||||||
|
<n-input
|
||||||
|
v-model:value="uploadForm.filePath"
|
||||||
|
placeholder="系统自动生成"
|
||||||
|
readonly
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 文件上传区域 -->
|
||||||
|
<n-form-item label="上传文件" path="files">
|
||||||
|
<div class="w-full">
|
||||||
|
<NUpload
|
||||||
|
ref="uploadRef"
|
||||||
|
v-model:file-list="uploadFileList"
|
||||||
|
:max="5"
|
||||||
|
multiple
|
||||||
|
directory-dnd
|
||||||
|
:before-upload="beforeUpload"
|
||||||
|
:custom-request="customUpload"
|
||||||
|
:on-remove="handleRemoveFile"
|
||||||
|
:on-error="handleUploadError"
|
||||||
|
@change="handleFileChange"
|
||||||
|
>
|
||||||
|
<NUploadDragger>
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-3">
|
||||||
|
<NIcon size="48" class="text-blue-500">
|
||||||
|
<icon-park-outline:upload />
|
||||||
|
</NIcon>
|
||||||
|
</div>
|
||||||
|
<NText class="text-base">
|
||||||
|
点击或者拖动文件到该区域来上传
|
||||||
|
</NText>
|
||||||
|
<NP depth="3" class="mt-2">
|
||||||
|
支持图片[jpg, jpeg, png, gif, bmp, webp, svg] 文档[doc/docx/pdf/txt/xls/xlsx/ppt/pptx/md/zip/rar/7z];
|
||||||
|
</NP>
|
||||||
|
<NP depth="3">
|
||||||
|
文件大小不能超过2MB;最多上传5个;
|
||||||
|
</NP>
|
||||||
|
</div>
|
||||||
|
</NUploadDragger>
|
||||||
|
</NUpload>
|
||||||
|
</div>
|
||||||
|
</n-form-item>
|
||||||
|
</n-form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CoiDialog>
|
||||||
|
|
||||||
|
<!-- 图片预览弹框 -->
|
||||||
|
<n-modal
|
||||||
|
v-model:show="imagePreviewVisible"
|
||||||
|
preset="card"
|
||||||
|
title="图片预览"
|
||||||
|
:style="{ width: '80%', maxWidth: '800px' }"
|
||||||
|
:mask-closable="true"
|
||||||
|
>
|
||||||
|
<div class="text-center">
|
||||||
|
<img
|
||||||
|
v-if="previewImageUrl"
|
||||||
|
:src="previewImageUrl"
|
||||||
|
:alt="previewImageName"
|
||||||
|
class="max-w-full max-h-96 mx-auto"
|
||||||
|
>
|
||||||
|
<div class="mt-2 text-sm text-gray-500">
|
||||||
|
{{ previewImageName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</n-modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { h, onMounted, ref } from 'vue'
|
||||||
|
import { NButton, NIcon, NImage, NP, NPopconfirm, NSpace, NTag, NText, NUpload, NUploadDragger } from 'naive-ui'
|
||||||
|
import type { DataTableColumns, DataTableInst, FormInst, UploadFileInfo, UploadInst } from 'naive-ui'
|
||||||
|
import CoiEmpty from '@/components/common/CoiEmpty.vue'
|
||||||
|
import { coiMsgBox, coiMsgError, coiMsgSuccess, coiMsgWarning } from '@/utils/coi'
|
||||||
|
import { usePermission } from '@/hooks/usePermission'
|
||||||
|
import { PERMISSIONS } from '@/constants/permissions'
|
||||||
|
import {
|
||||||
|
batchDeleteSysFiles,
|
||||||
|
deleteSysFile,
|
||||||
|
FILE_SERVICE_OPTIONS,
|
||||||
|
getSysFileList,
|
||||||
|
uploadFile,
|
||||||
|
} from '@/service/api/system/file'
|
||||||
|
import type { SysFileQueryBo, SysFileSearchForm, SysFileVo } from '@/service/api/system/file'
|
||||||
|
import IconParkOutlineDelete from '~icons/icon-park-outline/delete'
|
||||||
|
import IconParkOutlinePreviewOpen from '~icons/icon-park-outline/preview-open'
|
||||||
|
|
||||||
|
// 文件分类图标组件
|
||||||
|
const StarIcon = () => h('span', { class: 'text-yellow-500' }, '✨')
|
||||||
|
const _ImageIcon = () => h('span', { class: 'text-pink-500' }, '🎨')
|
||||||
|
const DocumentIcon = () => h('span', { class: 'text-blue-500' }, '📄')
|
||||||
|
const AudioIcon = () => h('span', { class: 'text-purple-500' }, '🎵')
|
||||||
|
const VideoIcon = () => h('span', { class: 'text-red-500' }, '📹')
|
||||||
|
const ArchiveIcon = () => h('span', { class: 'text-orange-500' }, '🗂️')
|
||||||
|
const AppIcon = () => h('span', { class: 'text-green-500' }, '🦄')
|
||||||
|
const OtherIcon = () => h('span', { class: 'text-gray-500' }, '🎃')
|
||||||
|
|
||||||
|
// 文件分类选项
|
||||||
|
const fileCategories = [
|
||||||
|
{ label: '全部', value: '0', icon: StarIcon },
|
||||||
|
{ label: '图片', value: '1', icon: StarIcon },
|
||||||
|
{ label: '文档', value: '2', icon: DocumentIcon },
|
||||||
|
{ label: '音频', value: '3', icon: AudioIcon },
|
||||||
|
{ label: '视频', value: '4', icon: VideoIcon },
|
||||||
|
{ label: '压缩包', value: '5', icon: ArchiveIcon },
|
||||||
|
{ label: '应用程序', value: '6', icon: AppIcon },
|
||||||
|
{ label: '其他', value: '9', icon: OtherIcon },
|
||||||
|
]
|
||||||
|
|
||||||
|
// 权限验证
|
||||||
|
const { hasPermission } = usePermission()
|
||||||
|
|
||||||
|
// 响应式数据
|
||||||
|
const searchFormRef = ref<FormInst>()
|
||||||
|
const tableRef = ref<DataTableInst>()
|
||||||
|
const uploadFormRef = ref<FormInst>()
|
||||||
|
const uploadRef = ref<UploadInst>()
|
||||||
|
const uploadDialogRef = ref()
|
||||||
|
const loading = ref(false)
|
||||||
|
const tableData = ref<SysFileVo[]>([])
|
||||||
|
const selectedCategory = ref<string>('0') // 默认选中全部
|
||||||
|
const selectedRows = ref<SysFileVo[]>([])
|
||||||
|
const selectedRowKeys = ref<number[]>([])
|
||||||
|
|
||||||
|
// 搜索表单
|
||||||
|
const searchForm = ref<SysFileSearchForm>({
|
||||||
|
fileName: '',
|
||||||
|
fileSuffix: '',
|
||||||
|
fileService: '',
|
||||||
|
fileType: '0', // 默认全部
|
||||||
|
})
|
||||||
|
|
||||||
|
// 图片预览相关
|
||||||
|
const imagePreviewVisible = ref(false)
|
||||||
|
const previewImageUrl = ref('')
|
||||||
|
const previewImageName = ref('')
|
||||||
|
|
||||||
|
// 文件上传相关
|
||||||
|
const uploadFileList = ref<UploadFileInfo[]>([])
|
||||||
|
const uploading = ref(false)
|
||||||
|
|
||||||
|
// 上传表单数据
|
||||||
|
const uploadForm = ref({
|
||||||
|
fileService: 'LOCAL',
|
||||||
|
filePath: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
// 上传表单验证规则
|
||||||
|
const uploadRules = {
|
||||||
|
fileService: [
|
||||||
|
{ required: true, message: '请选择文件服务', trigger: 'change' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页设置
|
||||||
|
const pagination = ref({
|
||||||
|
page: 1,
|
||||||
|
pageSize: 20,
|
||||||
|
itemCount: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 表格列定义
|
||||||
|
const columns: DataTableColumns<SysFileVo> = [
|
||||||
|
{
|
||||||
|
type: 'selection',
|
||||||
|
width: 50,
|
||||||
|
disabled: (_row: SysFileVo) => !hasPermission(PERMISSIONS.FILE.DELETE),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '序号',
|
||||||
|
key: 'index',
|
||||||
|
width: 80,
|
||||||
|
render: (_, index) => {
|
||||||
|
return (pagination.value.page - 1) * pagination.value.pageSize + index + 1
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件原始名称',
|
||||||
|
key: 'fileName',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件新名称',
|
||||||
|
key: 'newName',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件后缀',
|
||||||
|
key: 'fileSuffix',
|
||||||
|
width: 100,
|
||||||
|
render: (row) => {
|
||||||
|
return h(NTag, {
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
}, { default: () => row.fileSuffix?.toUpperCase() })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件上传路径',
|
||||||
|
key: 'fileUpload',
|
||||||
|
width: 250,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件回显路径',
|
||||||
|
key: 'filePath',
|
||||||
|
width: 200,
|
||||||
|
ellipsis: {
|
||||||
|
tooltip: true,
|
||||||
|
},
|
||||||
|
render: (row) => {
|
||||||
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(
|
||||||
|
row.fileSuffix?.toLowerCase() || '',
|
||||||
|
)
|
||||||
|
|
||||||
|
if (isImage && row.filePath) {
|
||||||
|
return h(NImage, {
|
||||||
|
src: row.filePath,
|
||||||
|
width: 45,
|
||||||
|
height: 30,
|
||||||
|
objectFit: 'cover',
|
||||||
|
previewDisabled: true,
|
||||||
|
onClick: () => handleImagePreview(row.filePath!, row.fileName),
|
||||||
|
class: 'cursor-pointer rounded border',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于非图片文件,返回文本让 ellipsis 配置生效
|
||||||
|
return row.filePath || '--'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '文件服务类型',
|
||||||
|
key: 'fileService',
|
||||||
|
width: 120,
|
||||||
|
render: (row) => {
|
||||||
|
const serviceMap: Record<string, { type: 'success' | 'info' | 'warning', text: string }> = {
|
||||||
|
1: { type: 'success', text: '本地存储' },
|
||||||
|
2: { type: 'info', text: 'MinIO' },
|
||||||
|
3: { type: 'warning', text: '阿里云OSS' },
|
||||||
|
}
|
||||||
|
const config = serviceMap[row.fileService] || { type: 'info', text: '未知' }
|
||||||
|
return h(NTag, {
|
||||||
|
type: config.type,
|
||||||
|
size: 'small',
|
||||||
|
}, { default: () => config.text })
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '创建时间',
|
||||||
|
key: 'createTime',
|
||||||
|
width: 160,
|
||||||
|
render: (row) => {
|
||||||
|
return row.createTime ? new Date(row.createTime).toLocaleString() : '--'
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: '操作',
|
||||||
|
key: 'actions',
|
||||||
|
width: 160,
|
||||||
|
align: 'center',
|
||||||
|
fixed: 'right',
|
||||||
|
render: (row) => {
|
||||||
|
const buttons = []
|
||||||
|
|
||||||
|
// 预览按钮
|
||||||
|
buttons.push(h(NButton, {
|
||||||
|
type: 'primary',
|
||||||
|
size: 'small',
|
||||||
|
onClick: () => handlePreview(row),
|
||||||
|
}, {
|
||||||
|
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||||
|
default: () => h(IconParkOutlinePreviewOpen),
|
||||||
|
}),
|
||||||
|
default: () => '预览',
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 删除按钮
|
||||||
|
if (hasPermission(PERMISSIONS.FILE.DELETE)) {
|
||||||
|
buttons.push(h(NPopconfirm, {
|
||||||
|
onPositiveClick: () => handleDelete(row.fileId!),
|
||||||
|
negativeText: '取消',
|
||||||
|
positiveText: '确定',
|
||||||
|
}, {
|
||||||
|
default: () => '确定删除此文件吗?',
|
||||||
|
trigger: () => h(NButton, {
|
||||||
|
type: 'error',
|
||||||
|
size: 'small',
|
||||||
|
}, {
|
||||||
|
icon: () => h(NIcon, { size: 14, style: 'transform: translateY(-1px)' }, {
|
||||||
|
default: () => h(IconParkOutlineDelete),
|
||||||
|
}),
|
||||||
|
default: () => '删除',
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
return h('div', { class: 'flex items-center justify-center gap-2' }, buttons)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
// 查询文件列表
|
||||||
|
async function fetchData() {
|
||||||
|
loading.value = true
|
||||||
|
try {
|
||||||
|
const params: SysFileQueryBo = {
|
||||||
|
pageNo: pagination.value.page,
|
||||||
|
pageSize: pagination.value.pageSize,
|
||||||
|
fileName: searchForm.value.fileName || undefined,
|
||||||
|
fileSuffix: searchForm.value.fileSuffix || undefined,
|
||||||
|
fileService: searchForm.value.fileService || undefined,
|
||||||
|
fileType: selectedCategory.value === '0' ? undefined : selectedCategory.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
const { data } = await getSysFileList(params)
|
||||||
|
if (data) {
|
||||||
|
tableData.value = data.records
|
||||||
|
pagination.value.itemCount = data.total
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
coiMsgError('获取文件列表失败')
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 搜索
|
||||||
|
function handleSearch() {
|
||||||
|
pagination.value.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重置搜索
|
||||||
|
function handleReset() {
|
||||||
|
searchForm.value = {
|
||||||
|
fileName: '',
|
||||||
|
fileSuffix: '',
|
||||||
|
fileService: '',
|
||||||
|
fileType: '0',
|
||||||
|
}
|
||||||
|
selectedCategory.value = '0'
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刷新
|
||||||
|
function handleRefresh() {
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分页变化处理
|
||||||
|
function handlePageChange(page: number) {
|
||||||
|
pagination.value.page = page
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
function handlePageSizeChange(pageSize: number) {
|
||||||
|
pagination.value.pageSize = pageSize
|
||||||
|
pagination.value.page = 1
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分类切换
|
||||||
|
function handleCategoryChange(category: string) {
|
||||||
|
selectedCategory.value = category
|
||||||
|
searchForm.value.fileType = category
|
||||||
|
handleSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 表格行选择
|
||||||
|
function handleRowKeysUpdate(keys: number[]) {
|
||||||
|
selectedRowKeys.value = keys
|
||||||
|
selectedRows.value = tableData.value.filter(row => keys.includes(row.fileId!))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除文件
|
||||||
|
async function handleDelete(id: number) {
|
||||||
|
try {
|
||||||
|
await deleteSysFile(id)
|
||||||
|
coiMsgSuccess('删除成功')
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
coiMsgError('删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量删除
|
||||||
|
async function handleBatchDelete() {
|
||||||
|
if (selectedRowKeys.value.length === 0) {
|
||||||
|
coiMsgWarning('请选择要删除的文件')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await coiMsgBox(`确定要删除选中的 ${selectedRowKeys.value.length} 个文件吗?`, '批量删除确认')
|
||||||
|
await batchDeleteSysFiles(selectedRowKeys.value)
|
||||||
|
coiMsgSuccess('批量删除成功')
|
||||||
|
selectedRowKeys.value = []
|
||||||
|
selectedRows.value = []
|
||||||
|
fetchData()
|
||||||
|
}
|
||||||
|
catch (error) {
|
||||||
|
if (error !== 'cancel') {
|
||||||
|
coiMsgError('批量删除失败')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预览文件
|
||||||
|
function handlePreview(row: SysFileVo) {
|
||||||
|
if (row.filePath) {
|
||||||
|
handleImagePreview(row.filePath, row.fileName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 图片预览
|
||||||
|
function handleImagePreview(url: string, name: string) {
|
||||||
|
previewImageUrl.value = url
|
||||||
|
previewImageName.value = name
|
||||||
|
imagePreviewVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传相关函数
|
||||||
|
|
||||||
|
// 打开上传弹框
|
||||||
|
function handleUpload() {
|
||||||
|
uploadForm.value = {
|
||||||
|
fileService: 'LOCAL',
|
||||||
|
filePath: '',
|
||||||
|
}
|
||||||
|
uploadFileList.value = []
|
||||||
|
uploading.value = false
|
||||||
|
uploadDialogRef.value?.coiOpen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通用文件验证函数
|
||||||
|
function validateFile(file: File, fileName: string, showError: boolean = true) {
|
||||||
|
// 文件空检查
|
||||||
|
if (!file || file.size === 0) {
|
||||||
|
if (showError)
|
||||||
|
coiMsgError('请选择有效的文件')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件名检查
|
||||||
|
if (!fileName) {
|
||||||
|
if (showError)
|
||||||
|
coiMsgError('无法获取文件名信息')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取文件扩展名
|
||||||
|
const fileExtension = fileName.split('.').pop()?.toLowerCase()
|
||||||
|
if (!fileExtension) {
|
||||||
|
if (showError)
|
||||||
|
coiMsgError('文件必须有扩展名')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件大小验证 (2MB)
|
||||||
|
const fileSize = file.size / 1024 / 1024
|
||||||
|
if (fileSize > 2) {
|
||||||
|
if (showError) {
|
||||||
|
coiMsgError(`文件大小超出限制!当前文件:${fileSize.toFixed(2)}MB,最大允许:2MB`)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件格式验证 - 与后端完全一致
|
||||||
|
const allowedImageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg']
|
||||||
|
const allowedDocumentExtensions = ['doc', 'docx', 'pdf', 'txt', 'xls', 'xlsx', 'ppt', 'pptx', 'md', 'zip', 'rar', '7z']
|
||||||
|
const allAllowedExtensions = [...allowedImageExtensions, ...allowedDocumentExtensions]
|
||||||
|
|
||||||
|
if (!allAllowedExtensions.includes(fileExtension)) {
|
||||||
|
if (showError) {
|
||||||
|
coiMsgError(`不支持的文件格式"${fileExtension}",仅支持:${allAllowedExtensions.join(', ')}`)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件上传前验证
|
||||||
|
function beforeUpload(data: { file: UploadFileInfo }) {
|
||||||
|
try {
|
||||||
|
const file = data.file.file as File
|
||||||
|
const fileName = data.file.name || file?.name || ''
|
||||||
|
|
||||||
|
// 显示错误提示的主要验证点
|
||||||
|
return validateFile(file, fileName, true)
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
coiMsgError('文件验证失败,请重新选择文件')
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 自定义上传逻辑
|
||||||
|
async function customUpload({ file, onProgress, onFinish, onError }: any) {
|
||||||
|
try {
|
||||||
|
const fileObj = file.file as File
|
||||||
|
const fileName = file.name || fileObj.name
|
||||||
|
|
||||||
|
// 在 customUpload 中再次验证 - 双重保险
|
||||||
|
if (!validateFile(fileObj, fileName, false)) {
|
||||||
|
onError()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断文件夹名称:图片文件夹为 pictures,其他为 files
|
||||||
|
const fileExtension = fileName.split('.').pop()?.toLowerCase()
|
||||||
|
const isImage = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(fileExtension || '')
|
||||||
|
const folderName = isImage ? 'pictures' : 'files'
|
||||||
|
|
||||||
|
// 设置进度
|
||||||
|
onProgress({ percent: 10 })
|
||||||
|
|
||||||
|
// 调用上传API - 使用2MB限制
|
||||||
|
await uploadFile(fileObj, folderName, 2, '-1')
|
||||||
|
|
||||||
|
// 设置完成进度
|
||||||
|
onProgress({ percent: 100 })
|
||||||
|
|
||||||
|
// 完成上传
|
||||||
|
onFinish()
|
||||||
|
|
||||||
|
coiMsgSuccess(`文件 ${fileObj.name} 上传成功`)
|
||||||
|
}
|
||||||
|
catch (error: any) {
|
||||||
|
onError()
|
||||||
|
|
||||||
|
// 解析后端错误信息
|
||||||
|
let errorMessage = `文件 ${file.file.name} 上传失败`
|
||||||
|
|
||||||
|
// 检查是否是服务返回的结构化错误
|
||||||
|
if (error?.isSuccess === false) {
|
||||||
|
if (error.message) {
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
else if (error.msg) {
|
||||||
|
errorMessage = error.msg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (error?.response?.data?.msg) {
|
||||||
|
errorMessage = error.response.data.msg
|
||||||
|
}
|
||||||
|
else if (error?.message) {
|
||||||
|
if (error.message.includes('413')) {
|
||||||
|
errorMessage = '文件大小超出限制,请选择较小的文件'
|
||||||
|
}
|
||||||
|
else if (error.message.includes('400')) {
|
||||||
|
errorMessage = '文件格式不支持或文件无效'
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
errorMessage = error.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coiMsgError(errorMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件列表变化处理
|
||||||
|
function handleFileChange(data: { fileList: UploadFileInfo[] }) {
|
||||||
|
uploadFileList.value = data.fileList
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移除文件
|
||||||
|
function handleRemoveFile(data: { file: UploadFileInfo }) {
|
||||||
|
const index = uploadFileList.value.findIndex(item => item.id === data.file.id)
|
||||||
|
if (index !== -1) {
|
||||||
|
uploadFileList.value.splice(index, 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 上传错误处理 - 静默处理,避免重复错误提示
|
||||||
|
function handleUploadError(_data: { file: UploadFileInfo, event?: ProgressEvent }) {
|
||||||
|
// 静默处理,主要错误信息已在 customUpload 中处理
|
||||||
|
}
|
||||||
|
|
||||||
|
// 确认上传
|
||||||
|
async function handleUploadConfirm() {
|
||||||
|
if (!uploadFormRef.value)
|
||||||
|
return
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 表单验证
|
||||||
|
await uploadFormRef.value.validate()
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
coiMsgWarning('请检查表单填写')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有待上传的文件
|
||||||
|
const pendingFiles = uploadFileList.value.filter(file => file.status === 'pending')
|
||||||
|
if (pendingFiles.length > 0) {
|
||||||
|
coiMsgWarning('还有文件未完成上传,请等待上传完成')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否有上传失败的文件
|
||||||
|
const errorFiles = uploadFileList.value.filter(file => file.status === 'error')
|
||||||
|
if (errorFiles.length > 0) {
|
||||||
|
coiMsgWarning('有文件上传失败,请重新上传')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果有上传成功的文件,刷新列表
|
||||||
|
const finishedFiles = uploadFileList.value.filter(file => file.status === 'finished')
|
||||||
|
if (finishedFiles.length > 0) {
|
||||||
|
coiMsgSuccess('所有文件上传完成')
|
||||||
|
uploadDialogRef.value?.coiClose()
|
||||||
|
await fetchData() // 刷新文件列表
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
coiMsgWarning('请先选择要上传的文件')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 取消上传
|
||||||
|
function handleUploadCancel() {
|
||||||
|
uploadDialogRef.value?.coiClose()
|
||||||
|
uploadFileList.value = []
|
||||||
|
uploading.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 页面加载时获取数据
|
||||||
|
onMounted(() => {
|
||||||
|
fetchData()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.search-form :deep(.n-form-item-label) {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-form :deep(.n-form-item) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-management {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-management .n-data-table {
|
||||||
|
--n-th-font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 表格字体和间距优化 */
|
||||||
|
.custom-table :deep(.n-data-table-td) {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-table :deep(.n-data-table-th) {
|
||||||
|
font-size: 13px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 操作按钮尺寸优化 */
|
||||||
|
.custom-table :deep(.n-button--small-type) {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 标签尺寸优化 */
|
||||||
|
.custom-table :deep(.n-tag--small-type) {
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 分类选择器样式 */
|
||||||
|
.category-item {
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-selected {
|
||||||
|
background-color: #f8faff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-unselected {
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.category-unselected:hover {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
1084
src/views/system/picture/index.vue
Normal file
1084
src/views/system/picture/index.vue
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user