|
|
@@ -0,0 +1,3046 @@
|
|
|
+<template>
|
|
|
+ <div class="file-manager" :class="{ 'select-mode': selectMode }">
|
|
|
+ <!-- 顶部分类导航 - 选择模式下根据 fileType 自动隐藏 -->
|
|
|
+ <div class="top-categories" v-if="!selectMode || filteredTopCategories.length > 1">
|
|
|
+ <div class="category-tabs">
|
|
|
+ <!-- 全部文件标签 - 选择模式下如果指定了文件类型则隐藏 -->
|
|
|
+ <div v-if="!selectMode || !fileType" class="category-tab" :class="{ active: !currentTopCategory }" @click="switchTopCategory(null)">
|
|
|
+ <el-icon>
|
|
|
+ <Folder />
|
|
|
+ </el-icon>
|
|
|
+ <span>全部文件</span>
|
|
|
+ </div>
|
|
|
+ <!-- 具体分类标签 -->
|
|
|
+ <div
|
|
|
+ v-for="topCategory in filteredTopCategories"
|
|
|
+ :key="topCategory.id"
|
|
|
+ class="category-tab"
|
|
|
+ :class="{ active: currentTopCategory?.id === topCategory.id }"
|
|
|
+ @click="switchTopCategory(topCategory)"
|
|
|
+ >
|
|
|
+ <el-icon v-if="topCategory.icon">
|
|
|
+ <component :is="topCategory.icon" />
|
|
|
+ </el-icon>
|
|
|
+ <el-icon v-else>
|
|
|
+ <Folder />
|
|
|
+ </el-icon>
|
|
|
+ <span>{{ topCategory.name }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 主要内容区域 -->
|
|
|
+ <div class="main-container">
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-button type="primary" @click="openUploadDialog">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ {{ getUploadButtonText() }}
|
|
|
+ </el-button>
|
|
|
+ <el-button @click="handleBatchDelete" :disabled="selectedFiles.length === 0"> 删除{{ getFileTypeText() }} </el-button>
|
|
|
+ <el-dropdown @command="handleMoveToCategory" :disabled="selectedFiles.length === 0">
|
|
|
+ <el-button>
|
|
|
+ {{ getFileTypeText() }}移至<el-icon class="el-icon--right"><ArrowDown /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item v-for="category in categoryList" :key="category.id" :command="category.id">
|
|
|
+ {{ category.name }}
|
|
|
+ </el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ <span v-if="selectedFiles.length > 0" class="selected-info"> 已选择 {{ selectedFiles.length }} 个{{ getFileTypeText() }} </span>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar-right">
|
|
|
+ <el-input v-model="searchKeyword" :placeholder="`请输入${getFileTypeText()}名`" style="width: 200px" clearable @input="handleSearch">
|
|
|
+ <template #prefix>
|
|
|
+ <el-icon><Search /></el-icon>
|
|
|
+ </template>
|
|
|
+ </el-input>
|
|
|
+ <div class="view-toggle">
|
|
|
+ <el-button :type="viewMode === 'grid' ? 'primary' : 'default'" size="small" @click="viewMode = 'grid'">
|
|
|
+ <el-icon><Grid /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ <el-button :type="viewMode === 'list' ? 'primary' : 'default'" size="small" @click="viewMode = 'list'">
|
|
|
+ <el-icon><List /></el-icon>
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 内容区域 -->
|
|
|
+ <div class="content-wrapper">
|
|
|
+ <!-- 左侧分类导航 -->
|
|
|
+ <div class="sidebar">
|
|
|
+ <div class="sidebar-header">
|
|
|
+ <h3>添加分类</h3>
|
|
|
+ <el-button link icon="Plus" @click="handleAddCategory">+</el-button>
|
|
|
+ </div>
|
|
|
+ <div class="category-list">
|
|
|
+ <!-- 全部文件 -->
|
|
|
+ <div class="category-item all-files" :class="{ active: currentCategory?.id === null }" @click="handleShowAllFiles">
|
|
|
+ <el-icon class="category-icon"><Folder /></el-icon>
|
|
|
+ <span>全部{{ getFileTypeText() }}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 树形控件 -->
|
|
|
+ <el-tree
|
|
|
+ ref="categoryTreeRef"
|
|
|
+ :data="filteredCategoryTree"
|
|
|
+ :props="treeProps"
|
|
|
+ :expand-on-click-node="false"
|
|
|
+ :highlight-current="true"
|
|
|
+ node-key="id"
|
|
|
+ @node-click="handleTreeNodeClick"
|
|
|
+ class="category-tree"
|
|
|
+ >
|
|
|
+ <template #default="{ node, data }">
|
|
|
+ <div class="tree-node-content">
|
|
|
+ <!-- 根据是否有子分类显示不同图标:有子分类显示文件夹,无子分类显示文件 -->
|
|
|
+ <el-icon class="category-icon">
|
|
|
+ <Folder v-if="data.children && data.children.length > 0" />
|
|
|
+ <Document v-else />
|
|
|
+ </el-icon>
|
|
|
+ <span class="node-label">{{ data.name }}</span>
|
|
|
+ <el-dropdown @command="handleCategoryAction" trigger="click" @click.stop class="node-actions">
|
|
|
+ <el-icon class="more-icon"><MoreFilled /></el-icon>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item :command="{ action: 'addChild', data: data }">添加子分类</el-dropdown-item>
|
|
|
+ <el-dropdown-item :command="{ action: 'edit', data: data }">编辑</el-dropdown-item>
|
|
|
+ <el-dropdown-item :command="{ action: 'delete', data: data }">删除</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 右侧文件展示区 -->
|
|
|
+ <div class="content-area">
|
|
|
+ <!-- 网格视图 -->
|
|
|
+ <div v-if="viewMode === 'grid'" class="file-grid">
|
|
|
+ <div
|
|
|
+ v-for="file in fileList"
|
|
|
+ :key="file.id"
|
|
|
+ class="file-item"
|
|
|
+ :class="{ selected: selectedFiles.includes(file.id) }"
|
|
|
+ @click="toggleFileSelection(file.id)"
|
|
|
+ >
|
|
|
+ <div class="file-wrapper">
|
|
|
+ <el-image v-if="isImage(file)" :src="getImageUrl(file)" fit="cover" class="file-thumbnail" :preview-disabled="true" lazy>
|
|
|
+ <template #error>
|
|
|
+ <div class="file-error">
|
|
|
+ <el-icon size="24" color="#c0c4cc">
|
|
|
+ <Picture />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #placeholder>
|
|
|
+ <div class="file-loading">
|
|
|
+ <el-icon size="24" color="#409eff">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
+ <div v-else-if="isVideo(file)" class="video-thumbnail-wrapper full">
|
|
|
+ <video
|
|
|
+ :src="getVideoUrl(file)"
|
|
|
+ class="file-thumbnail video-thumbnail"
|
|
|
+ muted
|
|
|
+ preload="metadata"
|
|
|
+ @loadedmetadata="onVideoLoadedMetadata"
|
|
|
+ @error="onVideoError"
|
|
|
+ ></video>
|
|
|
+ <div class="video-play-overlay">
|
|
|
+ <el-icon size="32" color="#fff">
|
|
|
+ <VideoPlay />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-else class="file-icon-wrapper">
|
|
|
+ <el-icon class="file-icon" :color="getFileIconColor(file)">
|
|
|
+ <component :is="getFileIcon(file)" />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ <div class="file-checkbox">
|
|
|
+ <el-checkbox :model-value="selectedFiles.includes(file.id)" @change="(val) => toggleFileSelection(file.id, val)" @click.stop />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="file-info">
|
|
|
+ <div class="file-name">{{ file.name || file.originalName }}</div>
|
|
|
+ <div class="file-actions">
|
|
|
+ <el-button link size="small" @click="handlePreview(file)">预览</el-button>
|
|
|
+ <el-button link size="small" @click="handleRename(file)">重命名</el-button>
|
|
|
+ <el-button link size="small" class="delete-btn" @click="handleDelete(file)"> 删除 </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 列表视图 -->
|
|
|
+ <div v-else class="file-list">
|
|
|
+ <el-table v-loading="loading" :data="fileList" style="width: 100%" @selection-change="handleSelectionChange">
|
|
|
+ <el-table-column type="selection" width="55" />
|
|
|
+ <el-table-column label="预览" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-image
|
|
|
+ v-if="isImage(row)"
|
|
|
+ :src="getImageUrl(row)"
|
|
|
+ style="width: 50px; height: 50px; border-radius: 4px"
|
|
|
+ fit="cover"
|
|
|
+ :preview-disabled="true"
|
|
|
+ lazy
|
|
|
+ >
|
|
|
+ <template #error>
|
|
|
+ <div class="list-image-error">
|
|
|
+ <el-icon size="20" color="#c0c4cc">
|
|
|
+ <Picture />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template #placeholder>
|
|
|
+ <div class="list-image-loading">
|
|
|
+ <el-icon size="20" color="#409eff">
|
|
|
+ <Loading />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-image>
|
|
|
+ <div v-else-if="isVideo(row)" class="video-thumbnail-wrapper">
|
|
|
+ <video
|
|
|
+ :src="getVideoUrl(row)"
|
|
|
+ style="width: 50px; height: 50px; border-radius: 4px; object-fit: cover"
|
|
|
+ muted
|
|
|
+ preload="metadata"
|
|
|
+ @loadedmetadata="onVideoLoadedMetadata"
|
|
|
+ @error="onVideoError"
|
|
|
+ class="video-thumbnail"
|
|
|
+ ></video>
|
|
|
+ <div class="video-play-overlay">
|
|
|
+ <el-icon size="16" color="#fff">
|
|
|
+ <VideoPlay />
|
|
|
+ </el-icon>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <el-icon v-else size="30" :color="getFileIconColor(row)">
|
|
|
+ <component :is="getFileIcon(row)" />
|
|
|
+ </el-icon>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="文件名" prop="name" min-width="200">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ row.name || row.originalName }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="大小" width="100">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatFileSize(row.size) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="类型" prop="type" width="120" />
|
|
|
+ <el-table-column label="上传时间" width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ {{ formatTime(row.createTime) }}
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ <el-table-column label="操作" width="200" fixed="right">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link @click="handlePreview(row)">预览</el-button>
|
|
|
+ <el-button link @click="handleRename(row)">重命名</el-button>
|
|
|
+ <el-button link class="delete-btn" @click="handleDelete(row)"> 删除 </el-button>
|
|
|
+ </template>
|
|
|
+ </el-table-column>
|
|
|
+ </el-table>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div class="pagination">
|
|
|
+ <el-pagination
|
|
|
+ v-model:current-page="queryParams.pageNum"
|
|
|
+ v-model:page-size="queryParams.pageSize"
|
|
|
+ :total="total"
|
|
|
+ :page-sizes="[12, 24, 48, 96]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="getList"
|
|
|
+ @current-change="getList"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 上传对话框 -->
|
|
|
+ <el-dialog v-model="showUploadDialog" :title="`上传${getFileTypeText()}`" width="600px">
|
|
|
+ <el-form ref="uploadFormRef" :model="uploadForm" label-width="100px">
|
|
|
+ <!-- 文件分类选择 -->
|
|
|
+ <el-form-item label="文件分类" prop="categoryId">
|
|
|
+ <el-tree-select
|
|
|
+ v-model="uploadForm.categoryId"
|
|
|
+ :data="uploadCategoryTree"
|
|
|
+ :props="{ value: 'id', label: 'name', children: 'children' }"
|
|
|
+ placeholder="请选择文件分类(可选)"
|
|
|
+ style="width: 100%"
|
|
|
+ clearable
|
|
|
+ check-strictly
|
|
|
+ @change="onCategoryChange"
|
|
|
+ />
|
|
|
+ <div class="form-tip" v-if="selectedCategoryType">当前分类只能上传:{{ getFileTypeByCategory(selectedCategoryType) }}</div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 文件上传 -->
|
|
|
+ <el-form-item label="文件上传" required>
|
|
|
+ <el-upload
|
|
|
+ ref="uploadRef"
|
|
|
+ :action="uploadUrl"
|
|
|
+ :headers="uploadHeaders"
|
|
|
+ :before-upload="beforeUpload"
|
|
|
+ :on-success="onUploadSuccess"
|
|
|
+ :on-error="onUploadError"
|
|
|
+ :file-list="uploadFileList"
|
|
|
+ multiple
|
|
|
+ drag
|
|
|
+ :accept="getUploadFileAccept()"
|
|
|
+ >
|
|
|
+ <el-icon class="el-icon--upload"><UploadFilled /></el-icon>
|
|
|
+ <div class="el-upload__text">将文件拖到此处,或<em>点击上传</em></div>
|
|
|
+ <template #tip>
|
|
|
+ <div class="el-upload__tip">
|
|
|
+ <span v-if="uploadForm.categoryId"> 支持 {{ getUploadFileTypeText() }} 格式,单个文件不超过 50MB </span>
|
|
|
+ <span v-else> 支持多文件上传,单个文件不超过 50MB </span>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-upload>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 文件描述 -->
|
|
|
+ <el-form-item label="文件描述" prop="description">
|
|
|
+ <el-input v-model="uploadForm.description" type="textarea" :rows="3" placeholder="请输入文件描述(可选)" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="showUploadDialog = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitUpload" :loading="uploadLoading">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 分类管理对话框 -->
|
|
|
+ <el-dialog v-model="categoryDialogVisible" :title="categoryDialogTitle" width="500px">
|
|
|
+ <el-form ref="categoryFormRef" :model="categoryForm" :rules="categoryRules" label-width="100px">
|
|
|
+ <!-- 分类名称 -->
|
|
|
+ <el-form-item label="分类名称" prop="name" required>
|
|
|
+ <el-input v-model="categoryForm.name" placeholder="请输入分类名称" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 分类编码 -->
|
|
|
+ <el-form-item label="分类编码" prop="code" required>
|
|
|
+ <el-input v-model="categoryForm.code" placeholder="请输入分类编码(英文大写)" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 父级分类 -->
|
|
|
+ <el-form-item label="父级分类" prop="parentId">
|
|
|
+ <el-tree-select
|
|
|
+ v-model="categoryForm.parentId"
|
|
|
+ :data="categoryTree"
|
|
|
+ :props="{ value: 'id', label: 'name', children: 'children' }"
|
|
|
+ placeholder="请选择父级分类(可选,不选则为顶级分类)"
|
|
|
+ clearable
|
|
|
+ check-strictly
|
|
|
+ :disabled="(categoryForm.id && isEditing) || isAddingChildCategory"
|
|
|
+ />
|
|
|
+ <div class="form-tip">
|
|
|
+ <span v-if="categoryForm.id && isEditing">编辑模式下不能修改父级分类</span>
|
|
|
+ <span v-else-if="isAddingChildCategory">正在为选定的分类添加子分类</span>
|
|
|
+ <span v-else>选择父级分类可以创建子分类,不选则创建顶级分类</span>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 分类类型 -->
|
|
|
+ <el-form-item label="分类类型" prop="type" required>
|
|
|
+ <el-select v-model="categoryForm.type" placeholder="请选择分类类型" style="width: 100%">
|
|
|
+ <el-option label="图片文件" :value="1" />
|
|
|
+ <el-option label="视频文件" :value="2" />
|
|
|
+ <el-option label="音频文件" :value="3" />
|
|
|
+ <el-option label="文档文件" :value="4" />
|
|
|
+ <el-option label="其他文件" :value="5" />
|
|
|
+ </el-select>
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 排序 -->
|
|
|
+ <el-form-item label="排序" prop="sort">
|
|
|
+ <el-input-number v-model="categoryForm.sort" :min="0" :max="999" placeholder="排序值" />
|
|
|
+ </el-form-item>
|
|
|
+
|
|
|
+ <!-- 描述 -->
|
|
|
+ <el-form-item label="描述" prop="description">
|
|
|
+ <el-input v-model="categoryForm.description" type="textarea" :rows="3" placeholder="请输入分类描述(可选)" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="categoryDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitCategory" :loading="categoryLoading">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 重命名对话框 -->
|
|
|
+ <el-dialog v-model="renameDialogVisible" title="重命名文件" width="400px">
|
|
|
+ <el-form ref="renameFormRef" :model="renameForm" label-width="80px">
|
|
|
+ <el-form-item label="原文件名">
|
|
|
+ <el-input v-model="renameForm.originalName" readonly />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item
|
|
|
+ label="新文件名"
|
|
|
+ prop="name"
|
|
|
+ :rules="[
|
|
|
+ { required: true, message: '请输入新文件名', trigger: 'blur' },
|
|
|
+ { min: 1, max: 100, message: '文件名长度在 1 到 100 个字符', trigger: 'blur' }
|
|
|
+ ]"
|
|
|
+ >
|
|
|
+ <el-input v-model="renameForm.name" placeholder="请输入新文件名" @keyup.enter="submitRename" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="renameDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitRename">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 图片裁剪组件 -->
|
|
|
+ <ImageCropper
|
|
|
+ v-model="cropDialogVisible"
|
|
|
+ :file="currentCropFile"
|
|
|
+ :image-url="cropImageUrl"
|
|
|
+ @confirm="handleCropConfirm"
|
|
|
+ @cancel="handleCropCancel"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup name="FileInfo" lang="ts">
|
|
|
+import { ref, onMounted, getCurrentInstance, watch, h, computed, nextTick } from 'vue';
|
|
|
+import { listFileInfo, delFileInfo, addFileInfo, updateDownloadCount, updateFileInfo } from '@/api/file/info';
|
|
|
+import { listFileCategoryTree, addFileCategory, updateFileCategory, delFileCategory } from '@/api/file/category';
|
|
|
+import {
|
|
|
+ Plus,
|
|
|
+ Search,
|
|
|
+ UploadFilled,
|
|
|
+ Document,
|
|
|
+ VideoPlay,
|
|
|
+ Microphone,
|
|
|
+ Picture,
|
|
|
+ FolderOpened,
|
|
|
+ Edit,
|
|
|
+ MoreFilled,
|
|
|
+ Back,
|
|
|
+ ArrowDown,
|
|
|
+ Grid,
|
|
|
+ List,
|
|
|
+ Folder,
|
|
|
+ Loading
|
|
|
+} from '@element-plus/icons-vue';
|
|
|
+import ImageCropper from '@/components/ImageCropper/index.vue';
|
|
|
+import { globalHeaders } from '@/utils/request';
|
|
|
+import { getToken } from '@/utils/auth';
|
|
|
+import { ElMessage, ElMessageBox } from 'element-plus';
|
|
|
+import type { FormRules } from 'element-plus';
|
|
|
+
|
|
|
+const { proxy } = getCurrentInstance();
|
|
|
+
|
|
|
+// 定义 Props - 支持选择模式
|
|
|
+interface Props {
|
|
|
+ selectMode?: boolean; // 是否为选择模式
|
|
|
+ fileType?: string; // 文件类型过滤('image', 'video', 'audio', 'document')
|
|
|
+ multiple?: boolean; // 是否支持多选
|
|
|
+}
|
|
|
+
|
|
|
+const props = withDefaults(defineProps<Props>(), {
|
|
|
+ selectMode: false,
|
|
|
+ fileType: '',
|
|
|
+ multiple: false
|
|
|
+});
|
|
|
+
|
|
|
+// 定义 Emits
|
|
|
+const emit = defineEmits<{
|
|
|
+ 'file-selected': [file: any];
|
|
|
+ 'files-selected': [files: any[]];
|
|
|
+}>();
|
|
|
+
|
|
|
+// 计算属性 - 用于模板访问 props
|
|
|
+const selectMode = computed(() => props.selectMode);
|
|
|
+const multiple = computed(() => props.multiple);
|
|
|
+const fileType = computed(() => props.fileType);
|
|
|
+
|
|
|
+// 响应式数据
|
|
|
+const loading = ref(false);
|
|
|
+const fileList = ref([]);
|
|
|
+const total = ref(0);
|
|
|
+const searchKeyword = ref('');
|
|
|
+const selectedFiles = ref([]);
|
|
|
+const showUploadDialog = ref(false);
|
|
|
+const uploadFileList = ref([]);
|
|
|
+const uploadLoading = ref(false);
|
|
|
+const currentTopCategory = ref(null);
|
|
|
+const viewMode = ref('grid');
|
|
|
+
|
|
|
+// 分类相关数据
|
|
|
+const categoryTree = ref([]);
|
|
|
+const categoryList = ref([]);
|
|
|
+const topCategories = ref([]);
|
|
|
+const currentCategory = ref(null);
|
|
|
+const expandedKeys = ref([]);
|
|
|
+const categoryTreeRef = ref();
|
|
|
+
|
|
|
+// 分类管理对话框
|
|
|
+const categoryDialogVisible = ref(false);
|
|
|
+const categoryDialogTitle = ref('添加分类');
|
|
|
+const categoryLoading = ref(false);
|
|
|
+const categoryFormRef = ref();
|
|
|
+const isEditing = ref(false);
|
|
|
+const isAddingChildCategory = ref(false);
|
|
|
+
|
|
|
+// 上传表单
|
|
|
+const uploadFormRef = ref();
|
|
|
+const selectedCategoryType = ref(null);
|
|
|
+
|
|
|
+// 重命名相关数据
|
|
|
+const renameDialogVisible = ref(false);
|
|
|
+const renameForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ originalName: '',
|
|
|
+ currentFile: null
|
|
|
+});
|
|
|
+const renameFormRef = ref();
|
|
|
+
|
|
|
+// 图片裁剪相关数据
|
|
|
+const cropDialogVisible = ref(false);
|
|
|
+const currentCropFile = ref(null);
|
|
|
+const cropImageUrl = ref('');
|
|
|
+
|
|
|
+// 上传配置
|
|
|
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
|
|
|
+const uploadHeaders = ref(globalHeaders());
|
|
|
+
|
|
|
+// 查询参数
|
|
|
+const queryParams = ref({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ name: null,
|
|
|
+ categoryId: null,
|
|
|
+ categoryType: null
|
|
|
+});
|
|
|
+
|
|
|
+// 上传表单数据
|
|
|
+const uploadForm = ref({
|
|
|
+ categoryId: null,
|
|
|
+ categoryType: null,
|
|
|
+ categoryName: null,
|
|
|
+ description: ''
|
|
|
+});
|
|
|
+
|
|
|
+// 分类表单数据
|
|
|
+const categoryForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ code: '',
|
|
|
+ parentId: null,
|
|
|
+ type: 1,
|
|
|
+ sort: 0,
|
|
|
+ description: '',
|
|
|
+ status: 0
|
|
|
+});
|
|
|
+
|
|
|
+// 分类表单验证规则
|
|
|
+const categoryRules = ref<FormRules>({
|
|
|
+ name: [
|
|
|
+ { required: true, message: '请输入分类名称', trigger: 'blur' },
|
|
|
+ { min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ code: [
|
|
|
+ { required: true, message: '请输入分类编码', trigger: 'blur' },
|
|
|
+ { pattern: /^[A-Z_]+$/, message: '分类编码只能包含大写字母和下划线', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ type: [{ required: true, message: '请选择分类类型', trigger: 'change' }]
|
|
|
+});
|
|
|
+
|
|
|
+// 树形控件配置
|
|
|
+const treeProps = {
|
|
|
+ label: 'name',
|
|
|
+ children: 'children'
|
|
|
+};
|
|
|
+
|
|
|
+// 获取文件列表
|
|
|
+const getList = async () => {
|
|
|
+ try {
|
|
|
+ loading.value = true;
|
|
|
+ const response = (await listFileInfo(queryParams.value)) as any;
|
|
|
+ const data = response?.data ?? response;
|
|
|
+ if (data && data.rows) {
|
|
|
+ fileList.value = data.rows as any[];
|
|
|
+ total.value = data.total || 0;
|
|
|
+ } else {
|
|
|
+ fileList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取文件列表失败:', error);
|
|
|
+ ElMessage.error('获取文件列表失败');
|
|
|
+ fileList.value = [];
|
|
|
+ total.value = 0;
|
|
|
+ } finally {
|
|
|
+ loading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 搜索文件
|
|
|
+const handleSearch = () => {
|
|
|
+ queryParams.value.name = searchKeyword.value || null;
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+// 多选处理
|
|
|
+const handleSelectionChange = (selection) => {
|
|
|
+ selectedFiles.value = selection.map((item) => item.id);
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分类树
|
|
|
+const getCategoryTree = async () => {
|
|
|
+ try {
|
|
|
+ const response = await listFileCategoryTree();
|
|
|
+ categoryTree.value = response.data || [];
|
|
|
+ categoryList.value = flattenCategories(categoryTree.value);
|
|
|
+
|
|
|
+ console.log('getCategoryTree - response.data:', response.data);
|
|
|
+ console.log('getCategoryTree - categoryTree.value:', categoryTree.value);
|
|
|
+
|
|
|
+ // 获取顶级分类(parent_id为0的分类)
|
|
|
+ topCategories.value = categoryTree.value.filter((category) => category.parentId === 0 || category.parentId === null);
|
|
|
+ console.log('getCategoryTree - topCategories.value:', topCategories.value);
|
|
|
+
|
|
|
+ // 默认不选择任何顶级分类,显示所有文件
|
|
|
+ if (!currentTopCategory.value) {
|
|
|
+ currentTopCategory.value = null;
|
|
|
+ queryParams.value.categoryType = null;
|
|
|
+ console.log('getCategoryTree - set currentTopCategory to null (show all files)');
|
|
|
+ }
|
|
|
+
|
|
|
+ return response;
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取分类树失败:', error);
|
|
|
+ ElMessage.error('获取分类树失败');
|
|
|
+ throw error;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 扁平化分类列表
|
|
|
+const flattenCategories = (categories, result = []) => {
|
|
|
+ categories.forEach((category) => {
|
|
|
+ result.push(category);
|
|
|
+ if (category.children && category.children.length > 0) {
|
|
|
+ flattenCategories(category.children, result);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ return result;
|
|
|
+};
|
|
|
+
|
|
|
+// 分类点击事件
|
|
|
+const handleCategoryClick = (data) => {
|
|
|
+ currentCategory.value = data;
|
|
|
+ queryParams.value.categoryId = data.id;
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+// 树节点点击事件
|
|
|
+const handleTreeNodeClick = (data, node, treeComponent) => {
|
|
|
+ handleCategoryClick(data);
|
|
|
+};
|
|
|
+
|
|
|
+// 显示全部文件
|
|
|
+const handleShowAllFiles = () => {
|
|
|
+ currentCategory.value = null;
|
|
|
+ queryParams.value.categoryId = null;
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+// 切换分类展开状态
|
|
|
+const toggleExpand = (categoryId) => {
|
|
|
+ const index = expandedKeys.value.indexOf(categoryId);
|
|
|
+ if (index > -1) {
|
|
|
+ // 如果已展开,则收起
|
|
|
+ expandedKeys.value.splice(index, 1);
|
|
|
+ } else {
|
|
|
+ // 如果未展开,则展开
|
|
|
+ expandedKeys.value.push(categoryId);
|
|
|
+ }
|
|
|
+ console.log('toggleExpand - categoryId:', categoryId, 'expandedKeys:', expandedKeys.value);
|
|
|
+};
|
|
|
+
|
|
|
+// 文件类型判断
|
|
|
+const isImage = (file) => {
|
|
|
+ // 首先检查MIME类型
|
|
|
+ if (file.type?.includes('image')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件扩展名
|
|
|
+ const extension = file.extension?.toLowerCase();
|
|
|
+ if (extension && ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(extension)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从文件名中提取扩展名作为备选方案
|
|
|
+ const fileName = file.name || file.originalName || '';
|
|
|
+ const fileExt = fileName.split('.').pop()?.toLowerCase();
|
|
|
+ if (fileExt && ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg', 'ico'].includes(fileExt)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+// 视频文件类型判断
|
|
|
+const isVideo = (file) => {
|
|
|
+ // 首先检查MIME类型
|
|
|
+ if (file.type?.includes('video')) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件扩展名
|
|
|
+ const extension = file.extension?.toLowerCase();
|
|
|
+ if (extension && ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp'].includes(extension)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从文件名中提取扩展名作为备选方案
|
|
|
+ const fileName = file.name || file.originalName || '';
|
|
|
+ const fileExt = fileName.split('.').pop()?.toLowerCase();
|
|
|
+ if (fileExt && ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm', 'm4v', '3gp'].includes(fileExt)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取图片URL
|
|
|
+const getImageUrl = (file) => {
|
|
|
+ // 优先使用url字段
|
|
|
+ if (file.url) {
|
|
|
+ // 如果是完整的URL,直接返回
|
|
|
+ if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
|
|
|
+ return file.url;
|
|
|
+ }
|
|
|
+ // 如果是相对路径,添加基础URL
|
|
|
+ if (file.url.startsWith('/')) {
|
|
|
+ return import.meta.env.VITE_APP_BASE_API + file.url;
|
|
|
+ }
|
|
|
+ return file.url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 备选方案:使用path字段
|
|
|
+ if (file.path) {
|
|
|
+ if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
|
|
|
+ return file.path;
|
|
|
+ }
|
|
|
+ if (file.path.startsWith('/')) {
|
|
|
+ return import.meta.env.VITE_APP_BASE_API + file.path;
|
|
|
+ }
|
|
|
+ return file.path;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果都没有,返回空字符串,触发错误处理
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取视频URL
|
|
|
+const getVideoUrl = (file) => {
|
|
|
+ // 优先使用url字段
|
|
|
+ if (file.url) {
|
|
|
+ // 如果是完整的URL,直接返回
|
|
|
+ if (file.url.startsWith('http://') || file.url.startsWith('https://')) {
|
|
|
+ return file.url;
|
|
|
+ }
|
|
|
+ // 如果是相对路径,添加基础URL
|
|
|
+ if (file.url.startsWith('/')) {
|
|
|
+ return import.meta.env.VITE_APP_BASE_API + file.url;
|
|
|
+ }
|
|
|
+ return file.url;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 备选方案:使用path字段
|
|
|
+ if (file.path) {
|
|
|
+ if (file.path.startsWith('http://') || file.path.startsWith('https://')) {
|
|
|
+ return file.path;
|
|
|
+ }
|
|
|
+ if (file.path.startsWith('/')) {
|
|
|
+ return import.meta.env.VITE_APP_BASE_API + file.path;
|
|
|
+ }
|
|
|
+ return file.path;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果都没有,返回空字符串,触发错误处理
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+const getFileIcon = (file) => {
|
|
|
+ if (file.type?.includes('video')) return VideoPlay;
|
|
|
+ if (file.type?.includes('audio')) return Microphone;
|
|
|
+ if (file.type?.includes('pdf')) return Document;
|
|
|
+ if (file.type?.includes('word') || file.extension === 'doc' || file.extension === 'docx') return Edit;
|
|
|
+ if (file.type?.includes('zip') || file.extension === 'zip') return FolderOpened;
|
|
|
+ return Document;
|
|
|
+};
|
|
|
+
|
|
|
+const getFileIconColor = (file) => {
|
|
|
+ if (file.type?.includes('video')) return '#409eff';
|
|
|
+ if (file.type?.includes('audio')) return '#67c23a';
|
|
|
+ if (file.type?.includes('pdf')) return '#f56c6c';
|
|
|
+ return '#909399';
|
|
|
+};
|
|
|
+
|
|
|
+// 视频缩略图相关事件处理
|
|
|
+const onVideoLoadedMetadata = (event) => {
|
|
|
+ const video = event.target;
|
|
|
+ // 跳转到视频的第一秒或10%位置来获取有意义的缩略图
|
|
|
+ video.currentTime = Math.min(1, video.duration * 0.1);
|
|
|
+};
|
|
|
+
|
|
|
+const onVideoError = (event) => {
|
|
|
+ console.warn('视频加载失败:', event.target.src);
|
|
|
+ // 可以在这里添加错误处理逻辑,比如显示默认图标
|
|
|
+};
|
|
|
+
|
|
|
+// 预览文件
|
|
|
+const handlePreview = (file) => {
|
|
|
+ if (file.type?.includes('image')) {
|
|
|
+ // 使用弹窗预览图片
|
|
|
+ handlePreviewImage(file);
|
|
|
+ } else if (file.type?.includes('video')) {
|
|
|
+ handlePlayVideo(file);
|
|
|
+ } else if (file.type?.includes('audio')) {
|
|
|
+ handlePlayAudio(file);
|
|
|
+ } else if (file.type?.includes('pdf')) {
|
|
|
+ handlePreviewPdf(file);
|
|
|
+ } else if (file.type?.includes('text')) {
|
|
|
+ handlePreviewText(file);
|
|
|
+ } else {
|
|
|
+ // 其他文件类型使用弹窗预览
|
|
|
+ handlePreviewOther(file);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 下载文件
|
|
|
+const handleDownload = async (file) => {
|
|
|
+ console.log('下载文件:', file);
|
|
|
+
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('文件URL不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取文件URL
|
|
|
+ let fileUrl = file.url || file.path;
|
|
|
+ let fileName = file.name || file.originalName || 'download';
|
|
|
+
|
|
|
+ // 确保URL是完整的
|
|
|
+ if (fileUrl && !fileUrl.startsWith('http')) {
|
|
|
+ // 如果是相对路径,添加基础URL
|
|
|
+ fileUrl = import.meta.env.VITE_APP_BASE_API + fileUrl;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 确保文件名有正确的扩展名
|
|
|
+ const extension = file.extension || fileName.split('.').pop()?.toLowerCase();
|
|
|
+ if (extension && !fileName.toLowerCase().endsWith('.' + extension)) {
|
|
|
+ fileName = fileName + '.' + extension;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('文件URL:', fileUrl);
|
|
|
+ console.log('文件名:', fileName);
|
|
|
+ console.log('文件类型:', file.type);
|
|
|
+ console.log('文件扩展名:', extension);
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 如果URL是OSS ID(纯数字),需要先获取文件信息
|
|
|
+ if (/^\d+$/.test(fileUrl)) {
|
|
|
+ console.log('检测到OSS ID,获取文件信息');
|
|
|
+ try {
|
|
|
+ const { listByIds } = await import('@/api/system/oss');
|
|
|
+ const res = await listByIds(fileUrl);
|
|
|
+ if (res.data && res.data.length > 0) {
|
|
|
+ fileUrl = res.data[0].url;
|
|
|
+ fileName = res.data[0].originalName || fileName;
|
|
|
+ console.log('获取到OSS文件信息:', fileUrl, fileName);
|
|
|
+ } else {
|
|
|
+ ElMessage.error('未找到对应的文件信息');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取OSS文件信息失败:', error);
|
|
|
+ ElMessage.error('获取文件信息失败');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用fetch下载文件
|
|
|
+ const response = await fetch(fileUrl, {
|
|
|
+ method: 'GET',
|
|
|
+ headers: {
|
|
|
+ 'Authorization': 'Bearer ' + getToken()
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const blob = await response.blob();
|
|
|
+
|
|
|
+ // 根据文件类型设置正确的MIME类型
|
|
|
+ let mimeType = file.type || 'application/octet-stream';
|
|
|
+
|
|
|
+ // 如果文件类型不正确,根据扩展名推断
|
|
|
+ if (!mimeType || mimeType === 'application/octet-stream') {
|
|
|
+ switch (extension) {
|
|
|
+ case 'jpg':
|
|
|
+ case 'jpeg':
|
|
|
+ mimeType = 'image/jpeg';
|
|
|
+ break;
|
|
|
+ case 'png':
|
|
|
+ mimeType = 'image/png';
|
|
|
+ break;
|
|
|
+ case 'gif':
|
|
|
+ mimeType = 'image/gif';
|
|
|
+ break;
|
|
|
+ case 'bmp':
|
|
|
+ mimeType = 'image/bmp';
|
|
|
+ break;
|
|
|
+ case 'webp':
|
|
|
+ mimeType = 'image/webp';
|
|
|
+ break;
|
|
|
+ case 'mp4':
|
|
|
+ mimeType = 'video/mp4';
|
|
|
+ break;
|
|
|
+ case 'avi':
|
|
|
+ mimeType = 'video/x-msvideo';
|
|
|
+ break;
|
|
|
+ case 'mov':
|
|
|
+ mimeType = 'video/quicktime';
|
|
|
+ break;
|
|
|
+ case 'wmv':
|
|
|
+ mimeType = 'video/x-ms-wmv';
|
|
|
+ break;
|
|
|
+ case 'flv':
|
|
|
+ mimeType = 'video/x-flv';
|
|
|
+ break;
|
|
|
+ case 'mkv':
|
|
|
+ mimeType = 'video/x-matroska';
|
|
|
+ break;
|
|
|
+ case 'mp3':
|
|
|
+ mimeType = 'audio/mpeg';
|
|
|
+ break;
|
|
|
+ case 'wav':
|
|
|
+ mimeType = 'audio/wav';
|
|
|
+ break;
|
|
|
+ case 'flac':
|
|
|
+ mimeType = 'audio/flac';
|
|
|
+ break;
|
|
|
+ case 'aac':
|
|
|
+ mimeType = 'audio/aac';
|
|
|
+ break;
|
|
|
+ case 'ogg':
|
|
|
+ mimeType = 'audio/ogg';
|
|
|
+ break;
|
|
|
+ case 'pdf':
|
|
|
+ mimeType = 'application/pdf';
|
|
|
+ break;
|
|
|
+ case 'doc':
|
|
|
+ mimeType = 'application/msword';
|
|
|
+ break;
|
|
|
+ case 'docx':
|
|
|
+ mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document';
|
|
|
+ break;
|
|
|
+ case 'xls':
|
|
|
+ mimeType = 'application/vnd.ms-excel';
|
|
|
+ break;
|
|
|
+ case 'xlsx':
|
|
|
+ mimeType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
|
+ break;
|
|
|
+ case 'ppt':
|
|
|
+ mimeType = 'application/vnd.ms-powerpoint';
|
|
|
+ break;
|
|
|
+ case 'pptx':
|
|
|
+ mimeType = 'application/vnd.openxmlformats-officedocument.presentationml.presentation';
|
|
|
+ break;
|
|
|
+ case 'txt':
|
|
|
+ mimeType = 'text/plain';
|
|
|
+ break;
|
|
|
+ case 'zip':
|
|
|
+ mimeType = 'application/zip';
|
|
|
+ break;
|
|
|
+ case 'rar':
|
|
|
+ mimeType = 'application/vnd.rar';
|
|
|
+ break;
|
|
|
+ case '7z':
|
|
|
+ mimeType = 'application/x-7z-compressed';
|
|
|
+ break;
|
|
|
+ default:
|
|
|
+ mimeType = 'application/octet-stream';
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('使用的MIME类型:', mimeType);
|
|
|
+ console.log('最终文件名:', fileName);
|
|
|
+
|
|
|
+ // 创建带有正确MIME类型的blob
|
|
|
+ const correctBlob = new Blob([blob], { type: mimeType });
|
|
|
+
|
|
|
+ // 创建下载链接
|
|
|
+ const url = window.URL.createObjectURL(correctBlob);
|
|
|
+ const link = document.createElement('a');
|
|
|
+ link.href = url;
|
|
|
+ link.download = fileName;
|
|
|
+ link.style.display = 'none';
|
|
|
+
|
|
|
+ // 添加到DOM并触发下载
|
|
|
+ document.body.appendChild(link);
|
|
|
+ link.click();
|
|
|
+
|
|
|
+ // 清理
|
|
|
+ document.body.removeChild(link);
|
|
|
+ window.URL.revokeObjectURL(url);
|
|
|
+
|
|
|
+ ElMessage.success('文件下载已开始');
|
|
|
+
|
|
|
+ // 更新下载次数
|
|
|
+ updateDownloadCount(file.id)
|
|
|
+ .then(() => {
|
|
|
+ console.log('下载次数已更新');
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error('更新下载次数失败:', error);
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('下载文件失败:', error);
|
|
|
+ const msg = error instanceof Error ? error.message : String(error);
|
|
|
+ ElMessage.error('下载文件失败: ' + msg);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 重命名文件
|
|
|
+const handleRename = (file) => {
|
|
|
+ console.log('重命名文件:', file);
|
|
|
+ renameForm.value = {
|
|
|
+ id: file.id,
|
|
|
+ name: file.name || file.originalName || '',
|
|
|
+ originalName: file.name || file.originalName || '',
|
|
|
+ currentFile: file // 保存完整的文件信息
|
|
|
+ };
|
|
|
+ renameDialogVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 提交重命名
|
|
|
+const submitRename = async () => {
|
|
|
+ try {
|
|
|
+ await renameFormRef.value?.validate();
|
|
|
+
|
|
|
+ if (!renameForm.value.name.trim()) {
|
|
|
+ ElMessage.error('请输入新文件名');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (renameForm.value.name === renameForm.value.originalName) {
|
|
|
+ ElMessage.warning('新文件名与原文件名相同');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 调用重命名API
|
|
|
+ const file = renameForm.value.currentFile;
|
|
|
+ await updateFileInfo({
|
|
|
+ id: renameForm.value.id,
|
|
|
+ name: renameForm.value.name.trim(),
|
|
|
+ originalName: file.originalName,
|
|
|
+ path: file.path,
|
|
|
+ url: file.url,
|
|
|
+ size: file.size,
|
|
|
+ type: file.type,
|
|
|
+ extension: file.extension,
|
|
|
+ categoryId: file.categoryId,
|
|
|
+ description: file.description
|
|
|
+ });
|
|
|
+
|
|
|
+ ElMessage.success('重命名成功');
|
|
|
+ renameDialogVisible.value = false;
|
|
|
+
|
|
|
+ // 刷新文件列表
|
|
|
+ getList();
|
|
|
+
|
|
|
+ console.log('重命名文件:', {
|
|
|
+ id: renameForm.value.id,
|
|
|
+ oldName: renameForm.value.originalName,
|
|
|
+ newName: renameForm.value.name.trim()
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重命名失败:', error);
|
|
|
+ ElMessage.error('重命名失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 删除文件
|
|
|
+const handleDelete = async (file) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除文件"${file.name || file.originalName}"吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+
|
|
|
+ await delFileInfo(file.id);
|
|
|
+ ElMessage.success('删除成功');
|
|
|
+ getList();
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除失败:', error);
|
|
|
+ ElMessage.error('删除失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 批量删除
|
|
|
+const handleBatchDelete = async () => {
|
|
|
+ if (selectedFiles.value.length === 0) {
|
|
|
+ ElMessage.warning('请选择要删除的文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除选中的 ${selectedFiles.value.length} 个文件吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+
|
|
|
+ const promises = selectedFiles.value.map((id) => delFileInfo(id));
|
|
|
+ await Promise.all(promises);
|
|
|
+ ElMessage.success('批量删除成功');
|
|
|
+ selectedFiles.value = [];
|
|
|
+ getList();
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('批量删除失败:', error);
|
|
|
+ ElMessage.error('批量删除失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 移动到分类
|
|
|
+const handleMoveToCategory = async (categoryId) => {
|
|
|
+ if (selectedFiles.value.length === 0) {
|
|
|
+ ElMessage.warning('请选择要移动的文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(
|
|
|
+ `确定要将选中的 ${selectedFiles.value.length} 个文件移动到分类"${categoryList.value.find((cat) => cat.id === categoryId)?.name}"吗?`,
|
|
|
+ '提示',
|
|
|
+ {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ }
|
|
|
+ );
|
|
|
+
|
|
|
+ const promises = selectedFiles.value.map((id) => updateFileInfoCategory(id, categoryId));
|
|
|
+ await Promise.all(promises);
|
|
|
+ ElMessage.success('批量移动成功');
|
|
|
+ selectedFiles.value = [];
|
|
|
+ getList();
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('批量移动失败:', error);
|
|
|
+ ElMessage.error('批量移动失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 更新文件分类
|
|
|
+const updateFileInfoCategory = async (fileId, categoryId) => {
|
|
|
+ const fileInfo = {
|
|
|
+ id: fileId,
|
|
|
+ categoryId: categoryId
|
|
|
+ };
|
|
|
+ // 这里需要调用更新文件信息的API,暂时注释掉
|
|
|
+ // await updateFileInfo(fileInfo);
|
|
|
+ console.log('更新文件分类:', fileId, categoryId);
|
|
|
+};
|
|
|
+
|
|
|
+// 添加分类
|
|
|
+const handleAddCategory = () => {
|
|
|
+ categoryDialogVisible.value = true;
|
|
|
+ categoryDialogTitle.value = '添加分类';
|
|
|
+ categoryForm.value = {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ code: '',
|
|
|
+ parentId: null,
|
|
|
+ type: 1,
|
|
|
+ sort: 0,
|
|
|
+ description: '',
|
|
|
+ status: 0
|
|
|
+ };
|
|
|
+ isEditing.value = false;
|
|
|
+ isAddingChildCategory.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 添加子分类
|
|
|
+const handleAddChildCategory = (parentCategory) => {
|
|
|
+ console.log('添加子分类,父分类:', parentCategory);
|
|
|
+ categoryDialogVisible.value = true;
|
|
|
+ categoryDialogTitle.value = `添加子分类 - ${parentCategory.name}`;
|
|
|
+ categoryForm.value = {
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ code: '',
|
|
|
+ parentId: parentCategory.id, // 设置父分类ID
|
|
|
+ type: parentCategory.type, // 继承父分类的类型
|
|
|
+ sort: 0,
|
|
|
+ description: '',
|
|
|
+ status: 0
|
|
|
+ };
|
|
|
+ isEditing.value = false;
|
|
|
+ isAddingChildCategory.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 打开上传对话框
|
|
|
+const openUploadDialog = () => {
|
|
|
+ showUploadDialog.value = true;
|
|
|
+ // 自动填充当前主分类的类型和名称
|
|
|
+ const defaultCategoryType = currentTopCategory.value ? currentTopCategory.value.type : null;
|
|
|
+ const defaultCategoryName = currentTopCategory.value ? currentTopCategory.value.name : null;
|
|
|
+
|
|
|
+ uploadForm.value = {
|
|
|
+ categoryId: null,
|
|
|
+ categoryType: defaultCategoryType,
|
|
|
+ categoryName: defaultCategoryName,
|
|
|
+ description: ''
|
|
|
+ };
|
|
|
+ uploadFileList.value = [];
|
|
|
+ selectedCategoryType.value = defaultCategoryType;
|
|
|
+};
|
|
|
+
|
|
|
+// 编辑分类
|
|
|
+const handleEditCategory = (data) => {
|
|
|
+ categoryDialogVisible.value = true;
|
|
|
+ categoryDialogTitle.value = '编辑分类';
|
|
|
+ categoryForm.value = { ...data };
|
|
|
+ isEditing.value = true;
|
|
|
+ isAddingChildCategory.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 删除分类
|
|
|
+const handleDeleteCategory = async (data) => {
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除分类"${data.name}"吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ });
|
|
|
+
|
|
|
+ await delFileCategory(data.id);
|
|
|
+ ElMessage.success('删除成功');
|
|
|
+ getCategoryTree();
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除分类失败:', error);
|
|
|
+ ElMessage.error('删除分类失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 分类操作处理
|
|
|
+const handleCategoryAction = (command) => {
|
|
|
+ if (command.action === 'addChild') {
|
|
|
+ handleAddChildCategory(command.data);
|
|
|
+ } else if (command.action === 'edit') {
|
|
|
+ handleEditCategory(command.data);
|
|
|
+ } else if (command.action === 'delete') {
|
|
|
+ // 检查是否有子分类
|
|
|
+ if (command.data.children && command.data.children.length > 0) {
|
|
|
+ ElMessage.warning('该分类下有子分类,请先删除子分类');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ handleDeleteCategory(command.data);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 提交分类
|
|
|
+const submitCategory = async () => {
|
|
|
+ try {
|
|
|
+ await categoryFormRef.value?.validate();
|
|
|
+ categoryLoading.value = true;
|
|
|
+
|
|
|
+ const categoryData = {
|
|
|
+ ...categoryForm.value,
|
|
|
+ tenantId: '000000'
|
|
|
+ };
|
|
|
+
|
|
|
+ if (categoryForm.value.id) {
|
|
|
+ await updateFileCategory(categoryData);
|
|
|
+ } else {
|
|
|
+ await addFileCategory(categoryData);
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success(categoryForm.value.id ? '更新成功' : '添加成功');
|
|
|
+ categoryDialogVisible.value = false;
|
|
|
+ getCategoryTree();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存分类失败:', error);
|
|
|
+ ElMessage.error('保存分类失败');
|
|
|
+ } finally {
|
|
|
+ categoryLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 上传前检查
|
|
|
+const beforeUpload = (file) => {
|
|
|
+ console.log('开始上传文件:', file.name, '原始MIME类型:', file.type);
|
|
|
+
|
|
|
+ // 获取准确的MIME类型
|
|
|
+ const actualMimeType = getFileMimeType(file);
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
+
|
|
|
+ console.log('检测到的MIME类型:', actualMimeType, '文件扩展名:', extension);
|
|
|
+
|
|
|
+ // 如果是图片文件,且当前分类是图片分类,则打开裁剪对话框
|
|
|
+ const currentCategoryType = uploadForm.value.categoryType || getCategoryType(uploadForm.value.categoryId);
|
|
|
+ const isImageFile = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(extension);
|
|
|
+
|
|
|
+ if (isImageFile && (currentCategoryType === 1 || !currentCategoryType)) {
|
|
|
+ // 延迟打开裁剪对话框,让上传组件先完成初始化
|
|
|
+ setTimeout(() => {
|
|
|
+ openCropDialog(file);
|
|
|
+ }, 100);
|
|
|
+
|
|
|
+ // 返回false阻止自动上传,等待裁剪完成后手动上传
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果选择了分类或有分类类型,检查文件类型
|
|
|
+ const selectedCategoryType = uploadForm.value.categoryType || getCategoryType(uploadForm.value.categoryId);
|
|
|
+
|
|
|
+ if (selectedCategoryType) {
|
|
|
+ let isValidType = false;
|
|
|
+ let fileTypeText = '';
|
|
|
+
|
|
|
+ if (selectedCategoryType === 1) {
|
|
|
+ // 图片分类
|
|
|
+ isValidType = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
|
|
|
+ fileTypeText = '图片';
|
|
|
+ } else if (selectedCategoryType === 2) {
|
|
|
+ // 视频分类
|
|
|
+ isValidType = actualMimeType.startsWith('video/') || ['mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv', 'webm'].includes(extension);
|
|
|
+ fileTypeText = '视频';
|
|
|
+ } else if (selectedCategoryType === 3) {
|
|
|
+ // 音频分类
|
|
|
+ isValidType = actualMimeType.startsWith('audio/') || ['mp3', 'wav', 'flac', 'aac', 'ogg', 'm4a'].includes(extension);
|
|
|
+ fileTypeText = '音频';
|
|
|
+ } else if (selectedCategoryType === 4) {
|
|
|
+ // 文档分类
|
|
|
+ isValidType =
|
|
|
+ actualMimeType.includes('pdf') ||
|
|
|
+ actualMimeType.includes('document') ||
|
|
|
+ actualMimeType.includes('word') ||
|
|
|
+ actualMimeType.includes('excel') ||
|
|
|
+ actualMimeType.includes('powerpoint') ||
|
|
|
+ actualMimeType.includes('text') ||
|
|
|
+ actualMimeType === 'application/msword' ||
|
|
|
+ actualMimeType === 'application/vnd.ms-excel' ||
|
|
|
+ actualMimeType === 'application/vnd.ms-powerpoint' ||
|
|
|
+ ['pdf', 'doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx', 'txt'].includes(extension);
|
|
|
+ fileTypeText = '文档';
|
|
|
+ } else {
|
|
|
+ isValidType = true;
|
|
|
+ fileTypeText = '文件';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!isValidType) {
|
|
|
+ ElMessage.error(`当前分类只能上传${fileTypeText}文件! 检测到的文件类型: ${actualMimeType}`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('文件类型验证通过:', fileTypeText, '类型');
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小 (50MB)
|
|
|
+ const isLtSize = file.size / 1024 / 1024 < 50;
|
|
|
+ if (!isLtSize) {
|
|
|
+ ElMessage.error('上传文件大小不能超过 50MB!');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('文件大小验证通过:', (file.size / 1024 / 1024).toFixed(2), 'MB');
|
|
|
+
|
|
|
+ // 非图片文件或非图片分类,直接上传
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取文件MIME类型
|
|
|
+const getFileMimeType = (file) => {
|
|
|
+ // 优先使用浏览器检测的MIME类型
|
|
|
+ if (file.type) {
|
|
|
+ return file.type;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果浏览器无法检测,根据文件扩展名推断
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
+
|
|
|
+ const mimeTypeMap = {
|
|
|
+ // 图片类型
|
|
|
+ 'jpg': 'image/jpeg',
|
|
|
+ 'jpeg': 'image/jpeg',
|
|
|
+ 'png': 'image/png',
|
|
|
+ 'gif': 'image/gif',
|
|
|
+ 'bmp': 'image/bmp',
|
|
|
+ 'webp': 'image/webp',
|
|
|
+ 'svg': 'image/svg+xml',
|
|
|
+
|
|
|
+ // 视频类型
|
|
|
+ 'mp4': 'video/mp4',
|
|
|
+ 'avi': 'video/x-msvideo',
|
|
|
+ 'mov': 'video/quicktime',
|
|
|
+ 'wmv': 'video/x-ms-wmv',
|
|
|
+ 'flv': 'video/x-flv',
|
|
|
+ 'mkv': 'video/x-matroska',
|
|
|
+ 'webm': 'video/webm',
|
|
|
+
|
|
|
+ // 音频类型
|
|
|
+ 'mp3': 'audio/mpeg',
|
|
|
+ 'wav': 'audio/wav',
|
|
|
+ 'flac': 'audio/flac',
|
|
|
+ 'aac': 'audio/aac',
|
|
|
+ 'ogg': 'audio/ogg',
|
|
|
+ 'm4a': 'audio/mp4',
|
|
|
+
|
|
|
+ // 文档类型
|
|
|
+ 'pdf': 'application/pdf',
|
|
|
+ 'doc': 'application/msword',
|
|
|
+ 'docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
|
+ 'xls': 'application/vnd.ms-excel',
|
|
|
+ 'xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
|
+ 'ppt': 'application/vnd.ms-powerpoint',
|
|
|
+ 'pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
|
+ 'txt': 'text/plain',
|
|
|
+
|
|
|
+ // 压缩文件
|
|
|
+ 'zip': 'application/zip',
|
|
|
+ 'rar': 'application/vnd.rar',
|
|
|
+ '7z': 'application/x-7z-compressed',
|
|
|
+ 'tar': 'application/x-tar',
|
|
|
+ 'gz': 'application/gzip',
|
|
|
+
|
|
|
+ // 其他常见类型
|
|
|
+ 'json': 'application/json',
|
|
|
+ 'xml': 'application/xml',
|
|
|
+ 'csv': 'text/csv',
|
|
|
+ 'html': 'text/html',
|
|
|
+ 'css': 'text/css',
|
|
|
+ 'js': 'application/javascript'
|
|
|
+ };
|
|
|
+
|
|
|
+ return mimeTypeMap[extension] || 'application/octet-stream';
|
|
|
+};
|
|
|
+
|
|
|
+// 上传成功
|
|
|
+const onUploadSuccess = (response, file) => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ ElMessage.success('文件上传成功');
|
|
|
+
|
|
|
+ // 获取文件MIME类型和扩展名
|
|
|
+ const mimeType = getFileMimeType(file);
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
|
|
+
|
|
|
+ uploadFileList.value.push({
|
|
|
+ name: file.name,
|
|
|
+ url: response.data?.url || '',
|
|
|
+ size: file.size,
|
|
|
+ type: mimeType,
|
|
|
+ extension: extension,
|
|
|
+ originalName: file.name,
|
|
|
+ ossId: response.data?.ossId || ''
|
|
|
+ });
|
|
|
+
|
|
|
+ console.log('文件上传成功,MIME类型:', mimeType, '扩展名:', extension);
|
|
|
+ } else {
|
|
|
+ ElMessage.error('上传失败:' + response.msg);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 上传失败
|
|
|
+const onUploadError = (error) => {
|
|
|
+ console.error('上传失败:', error);
|
|
|
+ ElMessage.error('上传失败');
|
|
|
+};
|
|
|
+
|
|
|
+// 提交上传
|
|
|
+const submitUpload = async () => {
|
|
|
+ if (uploadFileList.value.length === 0) {
|
|
|
+ ElMessage.error('请先上传文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 验证分类信息,如果没有分类信息,提示用户选择分类
|
|
|
+ if (!uploadForm.value.categoryType && !uploadForm.value.categoryId) {
|
|
|
+ ElMessage.error('请先选择文件分类(点击顶部分类标签)或在上传对话框中选择具体分类');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ uploadLoading.value = true;
|
|
|
+
|
|
|
+ // 批量保存文件信息
|
|
|
+ const promises = uploadFileList.value.map((file) => {
|
|
|
+ const fileInfo = {
|
|
|
+ name: file.name,
|
|
|
+ originalName: file.originalName,
|
|
|
+ path: file.url,
|
|
|
+ url: file.url,
|
|
|
+ size: file.size,
|
|
|
+ type: file.type,
|
|
|
+ extension: file.extension,
|
|
|
+ categoryId: uploadForm.value.categoryId,
|
|
|
+ categoryType: uploadForm.value.categoryType,
|
|
|
+ description: uploadForm.value.description || '',
|
|
|
+ uploadStatus: 1,
|
|
|
+ isPublic: 1,
|
|
|
+ status: 0,
|
|
|
+ downloadCount: 0,
|
|
|
+ viewCount: 0
|
|
|
+ };
|
|
|
+
|
|
|
+ console.log('保存文件信息到后端:', {
|
|
|
+ 文件名: fileInfo.name,
|
|
|
+ 原始文件名: fileInfo.originalName,
|
|
|
+ MIME类型: fileInfo.type,
|
|
|
+ 文件扩展名: fileInfo.extension,
|
|
|
+ 文件大小: (fileInfo.size / 1024 / 1024).toFixed(2) + 'MB',
|
|
|
+ 文件主分类: fileInfo.categoryType,
|
|
|
+ 分类ID: fileInfo.categoryId,
|
|
|
+ 文件URL: fileInfo.url
|
|
|
+ });
|
|
|
+
|
|
|
+ return addFileInfo(fileInfo);
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.all(promises);
|
|
|
+
|
|
|
+ ElMessage.success('文件上传成功');
|
|
|
+ showUploadDialog.value = false;
|
|
|
+ uploadFileList.value = [];
|
|
|
+ uploadForm.value = {
|
|
|
+ categoryId: null,
|
|
|
+ categoryType: null,
|
|
|
+ categoryName: null,
|
|
|
+ description: ''
|
|
|
+ };
|
|
|
+ getList();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存文件信息失败:', error);
|
|
|
+ ElMessage.error('保存文件信息失败');
|
|
|
+ } finally {
|
|
|
+ uploadLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分类类型
|
|
|
+const getCategoryType = (categoryId) => {
|
|
|
+ const category = categoryList.value.find((cat) => cat.id === categoryId);
|
|
|
+ return category ? category.type : null;
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分类文件类型文本
|
|
|
+const getFileTypeByCategory = (categoryType) => {
|
|
|
+ if (categoryType === 1) return 'jpg/png/gif/bmp/webp';
|
|
|
+ if (categoryType === 2) return 'mp4/avi/mov/wmv/flv/mkv';
|
|
|
+ if (categoryType === 3) return 'mp3/wav/flac/aac/ogg';
|
|
|
+ if (categoryType === 4) return 'pdf/doc/docx/xls/xlsx/ppt/pptx/txt';
|
|
|
+ if (categoryType === 5) return '所有文件';
|
|
|
+ return '所有文件';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取上传文件接受类型
|
|
|
+const getUploadFileAccept = () => {
|
|
|
+ if (uploadForm.value.categoryId) {
|
|
|
+ const categoryType = getCategoryType(uploadForm.value.categoryId);
|
|
|
+ if (categoryType === 1) return '.jpg,.jpeg,.png,.gif,.bmp,.webp';
|
|
|
+ if (categoryType === 2) return '.mp4,.avi,.mov,.wmv,.flv,.mkv';
|
|
|
+ if (categoryType === 3) return '.mp3,.wav,.flac,.aac,.ogg';
|
|
|
+ if (categoryType === 4) return '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt';
|
|
|
+ if (categoryType === 5) return ''; // 其他文件类型允许所有文件格式
|
|
|
+ }
|
|
|
+ return '';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取上传文件类型文本
|
|
|
+const getUploadFileTypeText = () => {
|
|
|
+ if (uploadForm.value.categoryId) {
|
|
|
+ const categoryType = getCategoryType(uploadForm.value.categoryId);
|
|
|
+ return getFileTypeByCategory(categoryType);
|
|
|
+ }
|
|
|
+ return '所有文件';
|
|
|
+};
|
|
|
+
|
|
|
+// 类别变化时更新上传表单
|
|
|
+const onCategoryChange = () => {
|
|
|
+ selectedCategoryType.value = getCategoryType(uploadForm.value.categoryId);
|
|
|
+};
|
|
|
+
|
|
|
+// 打开裁剪对话框
|
|
|
+const openCropDialog = (file) => {
|
|
|
+ console.log('准备打开裁剪对话框,文件:', file.name, '大小:', (file.size / 1024 / 1024).toFixed(2), 'MB');
|
|
|
+ currentCropFile.value = file;
|
|
|
+
|
|
|
+ // 清除可能的表单验证状态
|
|
|
+ nextTick(() => {
|
|
|
+ // 清除上传表单的验证状态
|
|
|
+ uploadFormRef.value?.clearValidate();
|
|
|
+ // 清除分类表单的验证状态
|
|
|
+ categoryFormRef.value?.clearValidate();
|
|
|
+ // 清除重命名表单的验证状态
|
|
|
+ renameFormRef.value?.clearValidate();
|
|
|
+ });
|
|
|
+
|
|
|
+ // 读取文件为base64
|
|
|
+ const reader = new FileReader();
|
|
|
+ reader.onload = (e) => {
|
|
|
+ const result = e.target?.result as string;
|
|
|
+ if (result) {
|
|
|
+ console.log('图片已转换为base64,长度:', result.length);
|
|
|
+ cropImageUrl.value = result;
|
|
|
+
|
|
|
+ // 延迟打开对话框,确保图片已加载
|
|
|
+ setTimeout(() => {
|
|
|
+ cropDialogVisible.value = true;
|
|
|
+ }, 100);
|
|
|
+ }
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.onerror = () => {
|
|
|
+ ElMessage.error('读取图片文件失败');
|
|
|
+ };
|
|
|
+
|
|
|
+ reader.readAsDataURL(file);
|
|
|
+};
|
|
|
+
|
|
|
+// 处理裁剪确认
|
|
|
+const handleCropConfirm = async (croppedFile) => {
|
|
|
+ try {
|
|
|
+ console.log('裁剪完成,新文件大小:', (croppedFile.size / 1024 / 1024).toFixed(2), 'MB');
|
|
|
+
|
|
|
+ // 上传裁剪后的文件
|
|
|
+ await uploadCroppedFile(croppedFile);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理裁剪文件失败:', error);
|
|
|
+ const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
+ ElMessage.error('处理失败: ' + errorMessage);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 处理裁剪取消
|
|
|
+const handleCropCancel = () => {
|
|
|
+ currentCropFile.value = null;
|
|
|
+ cropImageUrl.value = '';
|
|
|
+
|
|
|
+ // 关闭对话框后清除表单验证状态
|
|
|
+ nextTick(() => {
|
|
|
+ uploadFormRef.value?.clearValidate();
|
|
|
+ categoryFormRef.value?.clearValidate();
|
|
|
+ renameFormRef.value?.clearValidate();
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 上传裁剪后的文件
|
|
|
+const uploadCroppedFile = async (file) => {
|
|
|
+ try {
|
|
|
+ // 创建FormData
|
|
|
+ const formData = new FormData();
|
|
|
+ formData.append('file', file);
|
|
|
+
|
|
|
+ // 上传文件
|
|
|
+ const response = await fetch(uploadUrl.value, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: uploadHeaders.value,
|
|
|
+ body: formData
|
|
|
+ });
|
|
|
+
|
|
|
+ const result = await response.json();
|
|
|
+
|
|
|
+ if (result.code === 200) {
|
|
|
+ ElMessage.success('裁剪并上传成功');
|
|
|
+
|
|
|
+ // 添加到上传文件列表
|
|
|
+ const mimeType = getFileMimeType(file);
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
|
|
+
|
|
|
+ uploadFileList.value.push({
|
|
|
+ name: file.name,
|
|
|
+ url: result.data?.url || '',
|
|
|
+ size: file.size,
|
|
|
+ type: mimeType,
|
|
|
+ extension: extension,
|
|
|
+ originalName: file.name,
|
|
|
+ ossId: result.data?.ossId || ''
|
|
|
+ });
|
|
|
+
|
|
|
+ // 清理裁剪状态
|
|
|
+ handleCropCancel();
|
|
|
+ } else {
|
|
|
+ throw new Error(result.msg || '上传失败');
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('上传裁剪文件失败:', error);
|
|
|
+ const errorMessage = error instanceof Error ? error.message : String(error);
|
|
|
+ ElMessage.error('上传失败: ' + errorMessage);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化文件大小
|
|
|
+const formatFileSize = (size) => {
|
|
|
+ if (!size) return '0 B';
|
|
|
+ const units = ['B', 'KB', 'MB', 'GB'];
|
|
|
+ let index = 0;
|
|
|
+ let fileSize = size;
|
|
|
+ while (fileSize >= 1024 && index < units.length - 1) {
|
|
|
+ fileSize /= 1024;
|
|
|
+ index++;
|
|
|
+ }
|
|
|
+ return fileSize.toFixed(2) + ' ' + units[index];
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化时间
|
|
|
+const formatTime = (time) => {
|
|
|
+ if (!time) return '';
|
|
|
+ return new Date(time).toLocaleString();
|
|
|
+};
|
|
|
+
|
|
|
+// 预览图片
|
|
|
+const handlePreviewImage = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('图片文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用ElMessageBox创建图片预览对话框
|
|
|
+ const imageUrl = file.url || file.path;
|
|
|
+ ElMessageBox({
|
|
|
+ title: `预览图片 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center;' }, [
|
|
|
+ h('img', {
|
|
|
+ src: imageUrl,
|
|
|
+ style: 'max-width: 100%; max-height: 500px; height: auto; border-radius: 4px;',
|
|
|
+ onError: () => {
|
|
|
+ ElMessage.error('图片加载失败');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ ]),
|
|
|
+ showCancelButton: false,
|
|
|
+ confirmButtonText: '关闭',
|
|
|
+ customClass: 'image-preview-dialog'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 播放视频
|
|
|
+const handlePlayVideo = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('视频文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建视频播放对话框
|
|
|
+ const videoUrl = file.url || file.path;
|
|
|
+ ElMessageBox({
|
|
|
+ title: `播放视频 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center;' }, [
|
|
|
+ h('video', {
|
|
|
+ src: videoUrl,
|
|
|
+ controls: true,
|
|
|
+ autoplay: true,
|
|
|
+ style: 'width: 100%; max-width: 800px; height: auto;'
|
|
|
+ })
|
|
|
+ ]),
|
|
|
+ showCancelButton: false,
|
|
|
+ confirmButtonText: '关闭',
|
|
|
+ customClass: 'video-player-dialog'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 播放音频
|
|
|
+const handlePlayAudio = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('音频文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('开始播放音频文件:', file);
|
|
|
+
|
|
|
+ // 获取完整的音频URL
|
|
|
+ let audioUrl = file.url || file.path;
|
|
|
+
|
|
|
+ // 处理URL,确保是完整路径
|
|
|
+ if (audioUrl) {
|
|
|
+ // 如果是OSS ID(纯数字),需要特殊处理
|
|
|
+ if (/^\d+$/.test(audioUrl)) {
|
|
|
+ // 直接打开新窗口播放
|
|
|
+ window.open(import.meta.env.VITE_APP_BASE_API + '/resource/oss/download/' + audioUrl, '_blank');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理相对路径
|
|
|
+ if (!audioUrl.startsWith('http')) {
|
|
|
+ // 如果是相对路径,添加基础URL
|
|
|
+ audioUrl = import.meta.env.VITE_APP_BASE_API + (audioUrl.startsWith('/') ? audioUrl : '/' + audioUrl);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('处理后的音频URL:', audioUrl);
|
|
|
+
|
|
|
+ // 创建一个临时的audio元素测试URL是否可访问
|
|
|
+ const testAudio = new Audio();
|
|
|
+ testAudio.src = audioUrl;
|
|
|
+ testAudio.oncanplay = () => {
|
|
|
+ console.log('音频可以播放,URL有效');
|
|
|
+ // URL有效,创建音频播放对话框
|
|
|
+ showAudioDialog(file, audioUrl);
|
|
|
+ };
|
|
|
+
|
|
|
+ testAudio.onerror = (e) => {
|
|
|
+ console.error('音频URL测试失败:', e);
|
|
|
+ // 尝试直接在新窗口打开
|
|
|
+ const confirmed = window.confirm(`音频预览加载失败,是否在新窗口打开?`);
|
|
|
+ if (confirmed) {
|
|
|
+ window.open(audioUrl, '_blank');
|
|
|
+ }
|
|
|
+ };
|
|
|
+};
|
|
|
+
|
|
|
+// 显示音频播放对话框
|
|
|
+const showAudioDialog = (file, audioUrl) => {
|
|
|
+ // 创建音频播放对话框
|
|
|
+ ElMessageBox({
|
|
|
+ title: `播放音频 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center;' }, [
|
|
|
+ h('div', { class: 'audio-container' }, [
|
|
|
+ h('audio', {
|
|
|
+ src: audioUrl,
|
|
|
+ controls: true,
|
|
|
+ autoplay: false,
|
|
|
+ preload: 'auto',
|
|
|
+ style: 'width: 100%; max-width: 500px;',
|
|
|
+ ref: 'audioPlayer',
|
|
|
+ id: `audio-player-${file.id || Date.now()}`,
|
|
|
+ onError: (e) => {
|
|
|
+ console.error('音频加载失败:', e);
|
|
|
+ ElMessage.error('音频加载失败,请检查文件格式或网络连接');
|
|
|
+ }
|
|
|
+ }),
|
|
|
+ h('div', { class: 'audio-info' }, [
|
|
|
+ h('p', { style: 'margin-top: 15px; color: #606266;' }, `文件名: ${file.name || file.originalName}`),
|
|
|
+ h('p', { style: 'color: #909399; font-size: 12px;' }, `格式: ${file.type || '音频文件'}`),
|
|
|
+ h('p', { style: 'color: #909399; font-size: 12px;' }, `大小: ${formatFileSize(file.size)}`)
|
|
|
+ ])
|
|
|
+ ])
|
|
|
+ ]),
|
|
|
+ showCancelButton: true,
|
|
|
+ confirmButtonText: '在新窗口打开',
|
|
|
+ cancelButtonText: '关闭',
|
|
|
+ customClass: 'audio-player-dialog',
|
|
|
+ beforeClose: (action, instance, done) => {
|
|
|
+ // 关闭对话框前停止音频播放
|
|
|
+ try {
|
|
|
+ const audioId = document.getElementById(`audio-player-${file.id || Date.now()}`);
|
|
|
+ if (audioId && audioId instanceof HTMLAudioElement) {
|
|
|
+ audioId.pause();
|
|
|
+ }
|
|
|
+
|
|
|
+ const audioElements = document.querySelectorAll('.audio-player-dialog audio');
|
|
|
+ if (audioElements && audioElements.length > 0) {
|
|
|
+ audioElements.forEach((audio) => {
|
|
|
+ if (audio instanceof HTMLAudioElement) {
|
|
|
+ audio.pause();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ } catch (err) {
|
|
|
+ console.warn('停止音频播放失败:', err);
|
|
|
+ }
|
|
|
+ done();
|
|
|
+ }
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ // 用户点击"在新窗口打开"按钮
|
|
|
+ window.open(audioUrl, '_blank');
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 用户点击"关闭"按钮,不做任何操作
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 预览PDF
|
|
|
+const handlePreviewPdf = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('PDF文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const pdfUrl = file.url || file.path;
|
|
|
+ // 使用弹窗预览PDF
|
|
|
+ ElMessageBox({
|
|
|
+ title: `预览PDF - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center;' }, [
|
|
|
+ h('iframe', {
|
|
|
+ src: pdfUrl,
|
|
|
+ style: 'width: 100%; height: 600px; border: none; border-radius: 4px;',
|
|
|
+ onError: () => {
|
|
|
+ ElMessage.error('PDF加载失败');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ ]),
|
|
|
+ showCancelButton: false,
|
|
|
+ confirmButtonText: '关闭',
|
|
|
+ customClass: 'pdf-preview-dialog'
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 预览其他文件类型
|
|
|
+const handlePreviewOther = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const fileUrl = file.url || file.path;
|
|
|
+ const fileExtension = file.extension?.toLowerCase() || '';
|
|
|
+
|
|
|
+ // 判断文件类型并使用相应的预览方式
|
|
|
+ if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(fileExtension)) {
|
|
|
+ // Office文档使用iframe预览
|
|
|
+ ElMessageBox({
|
|
|
+ title: `预览文档 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center;' }, [
|
|
|
+ h('iframe', {
|
|
|
+ src: `https://view.officeapps.live.com/op/embed.aspx?src=${encodeURIComponent(fileUrl)}`,
|
|
|
+ style: 'width: 100%; height: 600px; border: none; border-radius: 4px;',
|
|
|
+ onError: () => {
|
|
|
+ ElMessage.error('文档预览失败,请尝试下载查看');
|
|
|
+ }
|
|
|
+ })
|
|
|
+ ]),
|
|
|
+ showCancelButton: false,
|
|
|
+ confirmButtonText: '关闭',
|
|
|
+ customClass: 'document-preview-dialog'
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ // 其他文件类型显示文件信息和下载提示
|
|
|
+ ElMessageBox({
|
|
|
+ title: `文件信息 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'text-align: center; padding: 20px;' }, [
|
|
|
+ h('div', { style: 'margin-bottom: 15px;' }, [h('i', { class: 'el-icon-document', style: 'font-size: 48px; color: #909399;' })]),
|
|
|
+ h('p', { style: 'margin: 10px 0; font-size: 16px; font-weight: 500;' }, file.name || file.originalName),
|
|
|
+ h('p', { style: 'margin: 5px 0; color: #666;' }, `文件类型: ${file.type || '未知'}`),
|
|
|
+ h('p', { style: 'margin: 5px 0; color: #666;' }, `文件大小: ${formatFileSize(file.size)}`),
|
|
|
+ h('p', { style: 'margin: 15px 0; color: #999;' }, '此文件类型无法直接预览,您可以下载后查看')
|
|
|
+ ]),
|
|
|
+ showCancelButton: true,
|
|
|
+ confirmButtonText: '下载文件',
|
|
|
+ cancelButtonText: '关闭',
|
|
|
+ customClass: 'file-info-dialog'
|
|
|
+ })
|
|
|
+ .then(() => {
|
|
|
+ // 点击下载按钮时执行下载
|
|
|
+ handleDownload(file);
|
|
|
+ })
|
|
|
+ .catch(() => {
|
|
|
+ // 用户取消,不做任何操作
|
|
|
+ });
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 预览文本文件
|
|
|
+const handlePreviewText = (file) => {
|
|
|
+ if (!file.url && !file.path) {
|
|
|
+ ElMessage.error('文本文件地址不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 下载并读取文本内容
|
|
|
+ fetch(file.url || file.path)
|
|
|
+ .then((response) => response.text())
|
|
|
+ .then((text) => {
|
|
|
+ ElMessageBox({
|
|
|
+ title: `预览文本 - ${file.name || file.originalName}`,
|
|
|
+ message: h('div', { style: 'max-height: 400px; overflow-y: auto;' }, [
|
|
|
+ h('pre', { style: 'white-space: pre-wrap; word-wrap: break-word;' }, text)
|
|
|
+ ]),
|
|
|
+ showCancelButton: false,
|
|
|
+ confirmButtonText: '关闭',
|
|
|
+ customClass: 'text-preview-dialog'
|
|
|
+ });
|
|
|
+ })
|
|
|
+ .catch((error) => {
|
|
|
+ console.error('读取文本文件失败:', error);
|
|
|
+ ElMessage.error('读取文本文件失败');
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 切换顶级分类
|
|
|
+const switchTopCategory = (topCategory) => {
|
|
|
+ console.log('switchTopCategory - switching to:', topCategory);
|
|
|
+ currentTopCategory.value = topCategory;
|
|
|
+ currentCategory.value = null;
|
|
|
+ selectedFiles.value = [];
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ queryParams.value.categoryId = null;
|
|
|
+
|
|
|
+ // 设置categoryType为顶部分类的ID值
|
|
|
+ if (topCategory && topCategory.type) {
|
|
|
+ // 按类型过滤(1=图片,2=视频,3=音频,4=文档)
|
|
|
+ queryParams.value.categoryType = topCategory.type;
|
|
|
+ console.log('switchTopCategory - set categoryType to type:', topCategory.type);
|
|
|
+ } else {
|
|
|
+ queryParams.value.categoryType = null;
|
|
|
+ console.log('switchTopCategory - cleared categoryType (show all files)');
|
|
|
+ }
|
|
|
+
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+// 获取上传按钮文本
|
|
|
+const getUploadButtonText = () => {
|
|
|
+ if (!currentTopCategory.value) return '上传文件';
|
|
|
+
|
|
|
+ switch (currentTopCategory.value.type) {
|
|
|
+ case 1:
|
|
|
+ return '上传图片';
|
|
|
+ case 2:
|
|
|
+ return '上传视频';
|
|
|
+ case 3:
|
|
|
+ return '上传音频';
|
|
|
+ case 4:
|
|
|
+ return '上传文档';
|
|
|
+ default:
|
|
|
+ return '上传文件';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取文件类型文本
|
|
|
+const getFileTypeText = () => {
|
|
|
+ if (!currentTopCategory.value) return '文件';
|
|
|
+
|
|
|
+ switch (currentTopCategory.value.type) {
|
|
|
+ case 1:
|
|
|
+ return '图片';
|
|
|
+ case 2:
|
|
|
+ return '视频';
|
|
|
+ case 3:
|
|
|
+ return '音频';
|
|
|
+ case 4:
|
|
|
+ return '文档';
|
|
|
+ default:
|
|
|
+ return '文件';
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 切换文件选择状态
|
|
|
+const toggleFileSelection = (id, checked = null) => {
|
|
|
+ // 选择模式下的特殊处理
|
|
|
+ if (props.selectMode) {
|
|
|
+ const file = fileList.value.find((f) => f.id === id);
|
|
|
+ if (!file) return;
|
|
|
+
|
|
|
+ if (props.multiple) {
|
|
|
+ // 多选模式
|
|
|
+ if (checked !== null) {
|
|
|
+ if (checked) {
|
|
|
+ if (!selectedFiles.value.includes(id)) {
|
|
|
+ selectedFiles.value.push(id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ selectedFiles.value = selectedFiles.value.filter((fileId) => fileId !== id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ const index = selectedFiles.value.indexOf(id);
|
|
|
+ if (index > -1) {
|
|
|
+ selectedFiles.value.splice(index, 1);
|
|
|
+ } else {
|
|
|
+ selectedFiles.value.push(id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 发射多选事件
|
|
|
+ const selectedFileObjects = fileList.value.filter((f) => selectedFiles.value.includes(f.id));
|
|
|
+ emit('files-selected', selectedFileObjects);
|
|
|
+ } else {
|
|
|
+ // 单选模式 - 只保留当前选中的文件
|
|
|
+ selectedFiles.value = [id];
|
|
|
+ emit('file-selected', file);
|
|
|
+ }
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非选择模式下的原有逻辑
|
|
|
+ if (checked !== null) {
|
|
|
+ // 如果传入了checked参数,使用它
|
|
|
+ if (checked) {
|
|
|
+ selectedFiles.value.push(id);
|
|
|
+ } else {
|
|
|
+ selectedFiles.value = selectedFiles.value.filter((fileId) => fileId !== id);
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 如果没有传入checked参数,切换状态
|
|
|
+ const index = selectedFiles.value.indexOf(id);
|
|
|
+ if (index > -1) {
|
|
|
+ selectedFiles.value.splice(index, 1);
|
|
|
+ } else {
|
|
|
+ selectedFiles.value.push(id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 根据 fileType 过滤顶级分类(选择模式下使用)
|
|
|
+const filteredTopCategories = computed(() => {
|
|
|
+ // 非选择模式或没有指定 fileType,返回所有分类
|
|
|
+ if (!props.selectMode || !props.fileType) {
|
|
|
+ return topCategories.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 根据 fileType 映射到对应的分类类型
|
|
|
+ let targetType = null;
|
|
|
+ switch (props.fileType.toLowerCase()) {
|
|
|
+ case 'image':
|
|
|
+ targetType = 1;
|
|
|
+ break;
|
|
|
+ case 'video':
|
|
|
+ targetType = 2;
|
|
|
+ break;
|
|
|
+ case 'audio':
|
|
|
+ targetType = 3;
|
|
|
+ break;
|
|
|
+ case 'document':
|
|
|
+ targetType = 4;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果没有匹配的类型,返回所有分类
|
|
|
+ if (!targetType) {
|
|
|
+ return topCategories.value;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 只返回匹配的分类
|
|
|
+ return topCategories.value.filter((cat) => cat.type === targetType);
|
|
|
+});
|
|
|
+
|
|
|
+// 根据当前顶级分类过滤子分类
|
|
|
+const filteredCategoryTree = computed(() => {
|
|
|
+ console.log('filteredCategoryTree computed - currentTopCategory:', currentTopCategory.value);
|
|
|
+ console.log('filteredCategoryTree computed - categoryTree:', categoryTree.value);
|
|
|
+
|
|
|
+ if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
|
|
|
+ console.log('filteredCategoryTree computed - returning empty array');
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 从树形结构中获取当前顶级分类的子分类
|
|
|
+ const getSubCategories = (parentId) => {
|
|
|
+ // 在顶级分类中查找对应的分类
|
|
|
+ const parentCategory = categoryTree.value.find((category) => category.id === parentId);
|
|
|
+ if (parentCategory && parentCategory.children) {
|
|
|
+ console.log('getSubCategories - parentId:', parentId, 'subCategories:', parentCategory.children);
|
|
|
+ return parentCategory.children;
|
|
|
+ }
|
|
|
+ console.log('getSubCategories - parentId:', parentId, 'subCategories: not found');
|
|
|
+ return [];
|
|
|
+ };
|
|
|
+
|
|
|
+ const result = getSubCategories(currentTopCategory.value.id);
|
|
|
+ console.log('filteredCategoryTree computed - result:', result);
|
|
|
+ return result;
|
|
|
+});
|
|
|
+
|
|
|
+// 上传对话框中的分类选项
|
|
|
+const uploadCategoryOptions = computed(() => {
|
|
|
+ if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取当前顶级分类的子分类
|
|
|
+ const parentCategory = categoryTree.value.find((category) => category.id === currentTopCategory.value.id);
|
|
|
+ if (parentCategory && parentCategory.children) {
|
|
|
+ return parentCategory.children;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [];
|
|
|
+});
|
|
|
+
|
|
|
+// 上传对话框中的树形分类数据(仅允许选择叶子节点:非叶子设置为 disabled)
|
|
|
+const uploadCategoryTree = computed(() => {
|
|
|
+ if (!currentTopCategory.value || !categoryTree.value || categoryTree.value.length === 0) {
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ const mapWithDisabled = (nodes) => {
|
|
|
+ if (!Array.isArray(nodes)) return [];
|
|
|
+ return nodes.map((node) => {
|
|
|
+ const hasChildren = Array.isArray(node.children) && node.children.length > 0;
|
|
|
+ const mappedChildren = hasChildren ? mapWithDisabled(node.children) : undefined;
|
|
|
+ return {
|
|
|
+ ...node,
|
|
|
+ children: mappedChildren,
|
|
|
+ disabled: hasChildren
|
|
|
+ };
|
|
|
+ });
|
|
|
+ };
|
|
|
+ const parentCategory = categoryTree.value.find((category) => category.id === currentTopCategory.value.id);
|
|
|
+ const children = parentCategory && Array.isArray(parentCategory.children) ? parentCategory.children : [];
|
|
|
+ return mapWithDisabled(children);
|
|
|
+});
|
|
|
+
|
|
|
+// 监听分类变化
|
|
|
+watch(currentCategory, () => {
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ selectedFiles.value = [];
|
|
|
+ getList();
|
|
|
+});
|
|
|
+
|
|
|
+// 监听 fileType 变化,自动切换到对应分类(选择模式下)
|
|
|
+watch(
|
|
|
+ () => props.fileType,
|
|
|
+ async (newType) => {
|
|
|
+ if (newType && props.selectMode) {
|
|
|
+ // 等待分类树加载完成
|
|
|
+ if (topCategories.value.length === 0) {
|
|
|
+ await getCategoryTree();
|
|
|
+ }
|
|
|
+
|
|
|
+ let categoryType = null;
|
|
|
+ switch (newType.toLowerCase()) {
|
|
|
+ case 'image':
|
|
|
+ categoryType = 1;
|
|
|
+ break;
|
|
|
+ case 'video':
|
|
|
+ categoryType = 2;
|
|
|
+ break;
|
|
|
+ case 'audio':
|
|
|
+ categoryType = 3;
|
|
|
+ break;
|
|
|
+ case 'document':
|
|
|
+ categoryType = 4;
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (categoryType) {
|
|
|
+ queryParams.value.categoryType = categoryType;
|
|
|
+ // 找到对应的顶级分类并自动选中
|
|
|
+ const topCategory = topCategories.value.find((cat) => cat.type === categoryType);
|
|
|
+ if (topCategory) {
|
|
|
+ currentTopCategory.value = topCategory;
|
|
|
+ }
|
|
|
+ getList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+// 清除选择状态(供外部调用)
|
|
|
+const clearSelection = () => {
|
|
|
+ selectedFiles.value = [];
|
|
|
+};
|
|
|
+
|
|
|
+// 暴露方法给父组件
|
|
|
+defineExpose({
|
|
|
+ clearSelection
|
|
|
+});
|
|
|
+
|
|
|
+// 页面初始化
|
|
|
+onMounted(async () => {
|
|
|
+ await getCategoryTree();
|
|
|
+ getList();
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.file-manager {
|
|
|
+ padding: 20px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ min-height: calc(100vh - 120px);
|
|
|
+}
|
|
|
+
|
|
|
+/* 选择模式下的样式调整 */
|
|
|
+.file-manager.select-mode {
|
|
|
+ padding: 0;
|
|
|
+ background: transparent;
|
|
|
+ min-height: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.file-manager.select-mode .top-categories {
|
|
|
+ border-radius: 0;
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.file-manager.select-mode .main-container {
|
|
|
+ margin-left: 0;
|
|
|
+ padding: 15px;
|
|
|
+ box-shadow: none;
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.file-manager.select-mode .toolbar {
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 选择信息样式 */
|
|
|
+.selected-info {
|
|
|
+ color: #409eff;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ padding-left: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 顶部分类导航 */
|
|
|
+.top-categories {
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 0;
|
|
|
+ padding: 0 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tabs {
|
|
|
+ display: flex;
|
|
|
+ gap: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tab {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 16px 24px;
|
|
|
+ cursor: pointer;
|
|
|
+ border-bottom: 3px solid transparent;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tab:hover {
|
|
|
+ color: #409eff;
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tab.active {
|
|
|
+ color: #409eff;
|
|
|
+ border-bottom-color: #409eff;
|
|
|
+ background: #f0f9ff;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tab .el-icon {
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 主容器 */
|
|
|
+.main-container {
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-left: 0px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.content-wrapper {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: 20px;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+ padding: 16px 20px;
|
|
|
+ background: white;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-left {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.view-toggle {
|
|
|
+ display: flex;
|
|
|
+ gap: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-left h2 {
|
|
|
+ margin: 0;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.toolbar-right {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.main-content {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ height: calc(100vh - 280px);
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar {
|
|
|
+ width: 280px;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px 20px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ background: #fafafa;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-header h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.sidebar-header h3 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 16px;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.category-list {
|
|
|
+ padding: 10px;
|
|
|
+ overflow-y: auto;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.category-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 10px 15px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.category-item:hover {
|
|
|
+ background: #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.category-item.active {
|
|
|
+ background: #409eff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.category-item.active:hover {
|
|
|
+ background: #66b1ff;
|
|
|
+}
|
|
|
+
|
|
|
+.category-icon {
|
|
|
+ margin-right: 10px;
|
|
|
+ font-size: 18px;
|
|
|
+}
|
|
|
+
|
|
|
+.category-actions {
|
|
|
+ margin-left: auto;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.expand-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: transform 0.3s;
|
|
|
+ margin-right: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.expand-icon:hover {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.expand-icon.expanded {
|
|
|
+ transform: rotate(90deg);
|
|
|
+}
|
|
|
+
|
|
|
+.indent {
|
|
|
+ width: 20px;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.child-category {
|
|
|
+ margin-left: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.grandchild-category {
|
|
|
+ margin-left: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.more-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.more-icon:hover {
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 树形控件样式 */
|
|
|
+.category-tree {
|
|
|
+ margin-top: 10px;
|
|
|
+ background: transparent;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tree :deep(.el-tree-node__content) {
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tree :deep(.el-tree-node__content:hover) {
|
|
|
+ background: #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
|
|
+ background: #409eff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.category-tree :deep(.el-tree-node.is-current > .el-tree-node__content:hover) {
|
|
|
+ background: #66b1ff;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ padding: 0 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content .category-icon {
|
|
|
+ margin-right: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content .node-label {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content .node-actions {
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content .more-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.3s;
|
|
|
+ padding: 2px 5px;
|
|
|
+ border-radius: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.tree-node-content .more-icon:hover {
|
|
|
+ color: #409eff;
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.content-area {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.content-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 16px 20px;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-bottom: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.breadcrumb .clickable {
|
|
|
+ cursor: pointer;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.breadcrumb .clickable:hover {
|
|
|
+ text-decoration: underline;
|
|
|
+}
|
|
|
+
|
|
|
+.header-actions {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-count {
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.content {
|
|
|
+ flex: 1;
|
|
|
+ background: white;
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: hidden;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 20px;
|
|
|
+ background: white;
|
|
|
+ flex-shrink: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.form-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ margin-top: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table {
|
|
|
+ border: none;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table th {
|
|
|
+ background: #fafafa;
|
|
|
+ color: #606266;
|
|
|
+ font-weight: 500;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table td {
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.el-table tr:hover td {
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.el-upload-dragger {
|
|
|
+ width: 100%;
|
|
|
+ height: 180px;
|
|
|
+ background: #fafafa;
|
|
|
+ border: 2px dashed #d9d9d9;
|
|
|
+ border-radius: 6px;
|
|
|
+ box-sizing: border-box;
|
|
|
+ text-align: center;
|
|
|
+ cursor: pointer;
|
|
|
+ position: relative;
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.el-upload-dragger:hover {
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.el-icon--upload {
|
|
|
+ font-size: 67px;
|
|
|
+ color: #c0c4cc;
|
|
|
+ margin: 40px 0 16px;
|
|
|
+ line-height: 50px;
|
|
|
+}
|
|
|
+
|
|
|
+.el-upload__text {
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.el-upload__text em {
|
|
|
+ color: #409eff;
|
|
|
+ font-style: normal;
|
|
|
+}
|
|
|
+
|
|
|
+.el-upload__tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #606266;
|
|
|
+ margin-top: 7px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-grid {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
|
+ gap: 15px;
|
|
|
+ padding: 15px;
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.file-item {
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 8px;
|
|
|
+ overflow: hidden;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ transition:
|
|
|
+ transform 0.3s ease,
|
|
|
+ box-shadow 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.file-item:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.file-item.selected {
|
|
|
+ border: 2px solid #409eff;
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
+}
|
|
|
+
|
|
|
+.file-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ height: 150px; /* Fixed height for grid view */
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.file-thumbnail {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.file-icon-wrapper {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f7fa;
|
|
|
+}
|
|
|
+
|
|
|
+.file-icon {
|
|
|
+ font-size: 48px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-checkbox {
|
|
|
+ position: absolute;
|
|
|
+ top: 8px;
|
|
|
+ right: 8px;
|
|
|
+ z-index: 10;
|
|
|
+}
|
|
|
+
|
|
|
+.file-checkbox :deep(.el-checkbox__inner) {
|
|
|
+ border: 2px solid #fff;
|
|
|
+ background: transparent;
|
|
|
+ border-radius: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.file-checkbox :deep(.el-checkbox.is-checked .el-checkbox__inner) {
|
|
|
+ background: #409eff;
|
|
|
+ border-color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+.file-info {
|
|
|
+ padding: 10px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-top: 1px solid #ebeef5;
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-name {
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ margin-bottom: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-actions {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-around;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-error {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #f5f7fa;
|
|
|
+ color: #c0c4cc;
|
|
|
+}
|
|
|
+
|
|
|
+.file-loading {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+ background: #f0f9ff;
|
|
|
+ color: #409eff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 列表视图中的图片预览样式 */
|
|
|
+.list-image-error {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.list-image-loading {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ background: #f0f9ff;
|
|
|
+ border-radius: 4px;
|
|
|
+ border: 1px solid #b3d8ff;
|
|
|
+}
|
|
|
+
|
|
|
+/* 视频缩略图样式 */
|
|
|
+.video-thumbnail-wrapper {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ overflow: hidden;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-thumbnail-wrapper.full {
|
|
|
+ width: 100%;
|
|
|
+ height: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.video-thumbnail {
|
|
|
+ display: block;
|
|
|
+ object-fit: cover;
|
|
|
+ background: #000;
|
|
|
+}
|
|
|
+
|
|
|
+.video-play-overlay {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ background: rgba(0, 0, 0, 0.6);
|
|
|
+ border-radius: 50%;
|
|
|
+ width: 40px;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ align-items: center;
|
|
|
+ pointer-events: none;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.video-thumbnail-wrapper:hover .video-play-overlay {
|
|
|
+ background: rgba(0, 0, 0, 0.8);
|
|
|
+ transform: translate(-50%, -50%) scale(1.1);
|
|
|
+}
|
|
|
+
|
|
|
+/* 列表视图中的视频缩略图样式 */
|
|
|
+.video-thumbnail-wrapper:not(.full) {
|
|
|
+ width: 50px;
|
|
|
+ height: 50px;
|
|
|
+}
|
|
|
+
|
|
|
+.video-thumbnail-wrapper:not(.full) .video-play-overlay {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.file-list {
|
|
|
+ padding: 15px;
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-height: 0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 删除按钮样式 - 浅灰色 */
|
|
|
+.delete-btn {
|
|
|
+ color: #909399 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.delete-btn:hover {
|
|
|
+ color: #606266 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图片预览对话框样式 */
|
|
|
+.image-preview-dialog {
|
|
|
+ max-width: 80vw;
|
|
|
+}
|
|
|
+
|
|
|
+.image-preview-dialog .el-message-box__content {
|
|
|
+ padding: 20px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.image-preview-dialog .el-message-box__message {
|
|
|
+ margin: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 音频播放器对话框样式 */
|
|
|
+.audio-player-dialog {
|
|
|
+ max-width: 600px;
|
|
|
+ min-width: 400px;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog .el-message-box__content {
|
|
|
+ padding: 30px 20px !important;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog .el-message-box__message {
|
|
|
+ margin: 0 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog .audio-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ padding: 15px;
|
|
|
+ background: #f8f9fa;
|
|
|
+ border-radius: 10px;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog audio {
|
|
|
+ width: 100%;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: #f0f9ff;
|
|
|
+ box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
|
|
+ padding: 10px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog .audio-info {
|
|
|
+ width: 100%;
|
|
|
+ text-align: left;
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 10px;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+}
|
|
|
+
|
|
|
+.audio-player-dialog .el-message-box__btns {
|
|
|
+ padding: 15px 20px !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 选中指示器 */
|
|
|
+.select-indicator {
|
|
|
+ position: absolute;
|
|
|
+ top: 8px;
|
|
|
+ right: 8px;
|
|
|
+ z-index: 10;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 50%;
|
|
|
+ padding: 4px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-footer {
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+
|
|
|
+/* 防止对话框影响主页面元素样式 */
|
|
|
+.file-selector :deep(.el-input.is-error),
|
|
|
+.file-selector :deep(.el-input.is-invalid) {
|
|
|
+ border-color: #dcdfe6 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.file-selector :deep(.el-input.is-error:hover),
|
|
|
+.file-selector :deep(.el-input.is-invalid:hover) {
|
|
|
+ border-color: #c0c4cc !important;
|
|
|
+}
|
|
|
+
|
|
|
+.file-selector :deep(.el-input.is-error:focus),
|
|
|
+.file-selector :deep(.el-input.is-invalid:focus) {
|
|
|
+ border-color: #409eff !important;
|
|
|
+}
|
|
|
+
|
|
|
+/* 确保分页组件样式正常 */
|
|
|
+.file-selector :deep(.el-pagination) {
|
|
|
+ border: none !important;
|
|
|
+}
|
|
|
+
|
|
|
+.file-selector :deep(.el-pagination .el-input) {
|
|
|
+ border-color: #dcdfe6 !important;
|
|
|
+}
|
|
|
+
|
|
|
+.file-selector :deep(.el-pagination .el-input:hover) {
|
|
|
+ border-color: #c0c4cc !important;
|
|
|
+}
|
|
|
+
|
|
|
+.file-selector :deep(.el-pagination .el-input:focus) {
|
|
|
+ border-color: #409eff !important;
|
|
|
+}
|
|
|
+</style>
|