|
|
@@ -0,0 +1,1177 @@
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <div class="flex flex-wrap">
|
|
|
+ <template v-if="limit == 1">
|
|
|
+ <div
|
|
|
+ class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px]"
|
|
|
+ :class="{ 'rounded-full': type == 'avatar' }"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <div class="w-full h-full relative" v-if="imagesData && imagesData.length > 0 && imagesData[0] != ''">
|
|
|
+ <div class="w-full h-full flex items-center justify-center">
|
|
|
+ <el-image class="w-full h-full" :src="imagesData[0].indexOf('data:image') != -1 ? imagesData[0] : img(imagesData[0])"></el-image>
|
|
|
+ </div>
|
|
|
+ <div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
|
|
|
+ <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(imagesData, 0)" />
|
|
|
+ <icon name="element Delete" color="#fff" size="18px" @click.stop="removeImage" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="w-full h-full flex items-center justify-center flex-col content-wrap" v-else @click="openDialog">
|
|
|
+ <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
|
|
+ <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <template v-else>
|
|
|
+ <div class="flex flex-wrap" ref="imgListRef">
|
|
|
+ <template v-for="(item, index) in imagesData" :key="item + index">
|
|
|
+ <div
|
|
|
+ v-if="item && item != ''"
|
|
|
+ class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
|
|
|
+ :style="style"
|
|
|
+ >
|
|
|
+ <div class="w-full h-full relative">
|
|
|
+ <div class="w-full h-full flex items-center justify-center">
|
|
|
+ <el-image :src="img(item)" fit="contain"></el-image>
|
|
|
+ </div>
|
|
|
+ <div class="absolute z-[1] flex items-center justify-center w-full h-full inset-0 bg-black bg-opacity-60 operation">
|
|
|
+ <icon name="element ZoomIn" color="#fff" size="18px" class="mr-[10px]" @click="previewImage(imagesData, index)" />
|
|
|
+ <icon name="element Delete" color="#fff" size="18px" @click="removeImage(index)" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ <div
|
|
|
+ class="rounded cursor-pointer overflow-hidden relative border border-solid border-color image-wrap mr-[10px] mb-[10px]"
|
|
|
+ :style="style"
|
|
|
+ v-if="imagesData.length < limit"
|
|
|
+ >
|
|
|
+ <div class="w-full h-full flex items-center justify-center flex-col content-wrap" @click="openDialog">
|
|
|
+ <icon name="element Plus" size="20px" color="var(--el-text-color-secondary)" />
|
|
|
+ <div class="leading-none text-xs mt-[10px] text-secondary">{{ imageText || '上传图片' }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </div>
|
|
|
+ <!-- 选择图片 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="dialogVisible"
|
|
|
+ title="选择图片"
|
|
|
+ width="1400"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ class="file-selector-dialog"
|
|
|
+ :before-close="closeDialog"
|
|
|
+ >
|
|
|
+ <div class="dialog-bos">
|
|
|
+ <!-- 工具栏 -->
|
|
|
+ <div class="toolbar">
|
|
|
+ <div class="toolbar-left">
|
|
|
+ <el-upload
|
|
|
+ ref="uploadRef"
|
|
|
+ :action="uploadUrl"
|
|
|
+ :headers="uploadHeaders"
|
|
|
+ :before-upload="beforeUpload"
|
|
|
+ :on-success="onUploadSuccess"
|
|
|
+ :on-error="onUploadError"
|
|
|
+ :show-file-list="false"
|
|
|
+ :accept="getUploadFileAccept()"
|
|
|
+ >
|
|
|
+ <el-button type="primary">
|
|
|
+ <el-icon><Plus /></el-icon>
|
|
|
+ 上传图片
|
|
|
+ </el-button>
|
|
|
+ </el-upload>
|
|
|
+ </div>
|
|
|
+ <div class="toolbar-right">
|
|
|
+ <el-input v-model="queryParams.name" placeholder="请输入图片名称" 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 @click="handleTreeNodeClick({ id: '' })" class="category-item" :class="{ active: queryParams.categoryId == '' }">
|
|
|
+ <el-icon class="category-icon"><Folder /></el-icon>
|
|
|
+ <span>全部图片</span>
|
|
|
+ <el-dropdown trigger="click" @click.stop class="node-actions">
|
|
|
+ <el-icon class="more-icon"><MoreFilled /></el-icon>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item @click="openClassify({}, 'add')" command="addRoot">添加分类</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ <!-- 树形控件 -->
|
|
|
+ <el-tree
|
|
|
+ ref="categoryTreeRef"
|
|
|
+ :data="filteredCategoryTree"
|
|
|
+ :props="treeProps"
|
|
|
+ :expand-on-click-node="false"
|
|
|
+ :current-node-key="queryParams.categoryId"
|
|
|
+ node-key="id"
|
|
|
+ @node-click="handleTreeNodeClick"
|
|
|
+ class="category-tree"
|
|
|
+ >
|
|
|
+ <template #default="{ data }">
|
|
|
+ <div class="tree-node-content">
|
|
|
+ <el-icon class="category-icon"><Folder /></el-icon>
|
|
|
+ <span class="node-label">{{ data.name }}</span>
|
|
|
+ <el-dropdown trigger="click" @click.stop class="node-actions pr-[10px]">
|
|
|
+ <el-icon class="more-icon"><MoreFilled /></el-icon>
|
|
|
+ <template #dropdown>
|
|
|
+ <el-dropdown-menu>
|
|
|
+ <el-dropdown-item @click="openClassify(data, 'add')">添加分类</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="openClassify(data, 'edit')">编辑分类</el-dropdown-item>
|
|
|
+ <el-dropdown-item @click="closeClassify(data)">删除分类</el-dropdown-item>
|
|
|
+ </el-dropdown-menu>
|
|
|
+ </template>
|
|
|
+ </el-dropdown>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-tree>
|
|
|
+ </div>
|
|
|
+ <!-- 右侧文件展示区 -->
|
|
|
+ <div class="content-area">
|
|
|
+ <!-- 网格视图 -->
|
|
|
+ <div v-if="viewMode === 'grid'" class="file-grid">
|
|
|
+ <div
|
|
|
+ v-for="(file, index) in fileList"
|
|
|
+ :key="index"
|
|
|
+ class="file-item"
|
|
|
+ :class="{
|
|
|
+ selected: selectedFiles.some((row: any) => row.id == file.id)
|
|
|
+ }"
|
|
|
+ @click="toggleFileSelection(file)"
|
|
|
+ >
|
|
|
+ <div class="file-wrapper">
|
|
|
+ <el-image :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 class="file-checkbox">
|
|
|
+ <el-checkbox :model-value="selectedFiles.some((row: any) => row.id == file.id)" />
|
|
|
+ </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.stop="
|
|
|
+ previewImage(
|
|
|
+ fileList.map((row: any) => row.url),
|
|
|
+ index
|
|
|
+ )
|
|
|
+ "
|
|
|
+ >预览</el-button
|
|
|
+ >
|
|
|
+ <el-button link size="small" @click.stop="handleRename(file)">重命名</el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 列表视图 -->
|
|
|
+ <div v-else class="file-list">
|
|
|
+ <!-- @selection-change="handleSelectionChange" -->
|
|
|
+ <el-table height="600" v-loading="loading" :data="fileList" style="width: 100%">
|
|
|
+ <!-- <el-table-column type="selection" width="55" /> -->
|
|
|
+ <el-table-column label="预览" width="80">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-image :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>
|
|
|
+ </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="150" fixed="right">
|
|
|
+ <template #default="scope">
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ @click="
|
|
|
+ previewImage(
|
|
|
+ fileList.map((row: any) => row.url),
|
|
|
+ scope.$index
|
|
|
+ )
|
|
|
+ "
|
|
|
+ >预览</el-button
|
|
|
+ >
|
|
|
+ <el-button link @click="handleRename(scope.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="[21, 28, 35, 42]"
|
|
|
+ layout="total, sizes, prev, pager, next, jumper"
|
|
|
+ @size-change="getList"
|
|
|
+ @current-change="getList"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="closeDialog">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmSelection" :disabled="selectedFiles.length > 0 ? false : true">
|
|
|
+ 确认选择({{ selectedFiles.length }})
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 添加编辑分类 -->
|
|
|
+ <el-dialog v-model="classifyDialog.dialog" :title="classifyDialog.title" width="600">
|
|
|
+ <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>
|
|
|
+ <template #footer>
|
|
|
+ <div class="dialog-footer">
|
|
|
+ <el-button @click="classifyDialog.dialog = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="submitCategory" :loading="classifyDialog.loading">确定</el-button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ <!-- 重命名对话框 -->
|
|
|
+ <el-dialog v-model="renameDialogVisible" title="重命名文件" width="400px" append-to-body :close-on-click-modal="false">
|
|
|
+ <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="请输入新文件名" />
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <!-- 图片放大 -->
|
|
|
+ <el-image-viewer
|
|
|
+ :url-list="previewImageList"
|
|
|
+ v-if="imageViewer.show"
|
|
|
+ @close="imageViewer.show = false"
|
|
|
+ :initial-index="imageViewer.index"
|
|
|
+ :zoom-rate="1"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script lang="ts" setup>
|
|
|
+import { img } from '@/utils/common';
|
|
|
+import { listFileInfo, delFileInfo, addFileInfo, updateDownloadCount, updateFileInfo } from '@/api/file/info';
|
|
|
+import { listFileCategoryTree, addFileCategory, updateFileCategory, delFileCategory } from '@/api/file/category';
|
|
|
+import { globalHeaders } from '@/utils/request';
|
|
|
+const props = defineProps({
|
|
|
+ type: {
|
|
|
+ type: String,
|
|
|
+ default: 'image'
|
|
|
+ },
|
|
|
+ modelValue: {
|
|
|
+ type: String || Array,
|
|
|
+ default: ''
|
|
|
+ },
|
|
|
+ width: {
|
|
|
+ type: String,
|
|
|
+ default: '100px'
|
|
|
+ },
|
|
|
+ height: {
|
|
|
+ type: String,
|
|
|
+ default: '100px'
|
|
|
+ },
|
|
|
+ imageText: {
|
|
|
+ type: String
|
|
|
+ },
|
|
|
+ limit: {
|
|
|
+ type: Number,
|
|
|
+ default: 1
|
|
|
+ }
|
|
|
+});
|
|
|
+const imagesData = ref<any>([]);
|
|
|
+const previewImageList = ref<any>([]);
|
|
|
+watch(
|
|
|
+ () => props.modelValue,
|
|
|
+ () => {
|
|
|
+ if (props.limit == 1) {
|
|
|
+ imagesData.value = [props.modelValue];
|
|
|
+ } else {
|
|
|
+ if (Array.isArray(props.modelValue)) {
|
|
|
+ imagesData.value = props.modelValue;
|
|
|
+ } else {
|
|
|
+ imagesData.value = props.modelValue.split(',');
|
|
|
+ imagesData.value = imagesData.value.filter((item: any) => item !== '');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+ { immediate: true }
|
|
|
+);
|
|
|
+
|
|
|
+const emit = defineEmits(['update:modelValue', 'change']);
|
|
|
+
|
|
|
+const dialogVisible = ref<any>(false);
|
|
|
+const viewMode = ref<any>('grid');
|
|
|
+// 图片列表
|
|
|
+const loading = ref<any>(false);
|
|
|
+const fileList = ref([]);
|
|
|
+const total = ref(0);
|
|
|
+const selectedFiles = ref([]);
|
|
|
+
|
|
|
+// 查询参数
|
|
|
+const queryParams = ref<any>({
|
|
|
+ pageNum: 1,
|
|
|
+ pageSize: 20,
|
|
|
+ name: null,
|
|
|
+ categoryId: '',
|
|
|
+ categoryType: 1
|
|
|
+});
|
|
|
+
|
|
|
+// 分类相关数据
|
|
|
+const filteredCategoryTree = ref<any>([]);
|
|
|
+const categoryTreeRef = ref<any>(null);
|
|
|
+const treeProps = {
|
|
|
+ label: 'name',
|
|
|
+ children: 'children'
|
|
|
+};
|
|
|
+const classifyDialog = ref<any>({
|
|
|
+ dialog: false,
|
|
|
+ title: '添加分类',
|
|
|
+ loading: false
|
|
|
+});
|
|
|
+// 分类表单数据
|
|
|
+const categoryFormRef = ref<any>(null);
|
|
|
+const categoryForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ code: '',
|
|
|
+ parentId: null,
|
|
|
+ type: 1,
|
|
|
+ sort: 1,
|
|
|
+ description: '',
|
|
|
+ status: 0
|
|
|
+});
|
|
|
+// 分类表单验证规则
|
|
|
+const categoryRules = ref<any>({
|
|
|
+ name: [
|
|
|
+ { required: true, message: '请输入分类名称', trigger: 'blur' },
|
|
|
+ { min: 2, max: 50, message: '分类名称长度在 2 到 50 个字符', trigger: 'blur' }
|
|
|
+ ]
|
|
|
+});
|
|
|
+
|
|
|
+// 重命名相关数据
|
|
|
+const renameDialogVisible = ref(false);
|
|
|
+const renameForm = ref({
|
|
|
+ id: null,
|
|
|
+ name: '',
|
|
|
+ originalName: '',
|
|
|
+ currentFile: null
|
|
|
+});
|
|
|
+const renameFormRef = ref();
|
|
|
+
|
|
|
+// 上传配置
|
|
|
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + '/resource/oss/upload');
|
|
|
+const uploadHeaders = ref(globalHeaders());
|
|
|
+const uploadFileList = ref([]);
|
|
|
+
|
|
|
+// 打开弹窗
|
|
|
+const openDialog = () => {
|
|
|
+ getCategoryTree();
|
|
|
+ getList();
|
|
|
+ selectedFiles.value = [];
|
|
|
+ dialogVisible.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+//关闭弹窗
|
|
|
+const closeDialog = () => {
|
|
|
+ dialogVisible.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+//确定
|
|
|
+const confirmSelection = () => {
|
|
|
+ if (props.limit == 1) {
|
|
|
+ emit('update:modelValue', selectedFiles.value[0].url);
|
|
|
+ emit('change', selectedFiles.value[0]);
|
|
|
+ } else {
|
|
|
+ const result = selectedFiles.value.map((item: any) => item.url);
|
|
|
+ let resultArray = [];
|
|
|
+ let resultString = '';
|
|
|
+ if (Array.isArray(props.modelValue)) {
|
|
|
+ resultArray = [...result, ...imagesData.value];
|
|
|
+ emit('update:modelValue', resultArray);
|
|
|
+ emit('change', resultArray);
|
|
|
+ } else {
|
|
|
+ resultString = imagesData.value + ',' + result.join(',');
|
|
|
+ emit('update:modelValue', resultString);
|
|
|
+ emit('change', resultString);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ closeDialog();
|
|
|
+};
|
|
|
+
|
|
|
+// 搜索文件
|
|
|
+const handleSearch = () => {
|
|
|
+ queryParams.value.pageNum = 1;
|
|
|
+ getList();
|
|
|
+};
|
|
|
+
|
|
|
+// 获取文件列表
|
|
|
+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 handleSelectionChange = (res: any) => {};
|
|
|
+// 切换文件选择状态
|
|
|
+const toggleFileSelection = (res: any) => {
|
|
|
+ if (props.limit == 1) {
|
|
|
+ selectedFiles.value = [res];
|
|
|
+ } else {
|
|
|
+ // 多选模式,实现切换逻辑
|
|
|
+ const index = selectedFiles.value.findIndex((item: any) => item.id === res.id);
|
|
|
+ // 计算当前总选择数量(已选文件 + 已上传图片)
|
|
|
+ const currentTotalCount = selectedFiles.value.length + imagesData.value.length;
|
|
|
+ if (index > -1) {
|
|
|
+ // 如果已选中,则取消选中(删除)
|
|
|
+ selectedFiles.value.splice(index, 1);
|
|
|
+ } else {
|
|
|
+ // 如果未选中,检查是否超过限制
|
|
|
+ if (currentTotalCount >= props.limit) {
|
|
|
+ ElMessage.warning(`最多只能选择 ${props.limit} 张图片`);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 未超过限制,添加选中
|
|
|
+ selectedFiles.value.push(res);
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 获取图片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 '';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取分类树
|
|
|
+const getCategoryTree = () => {
|
|
|
+ listFileCategoryTree().then((res) => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ if (res.data.length > 0) {
|
|
|
+ res.data.forEach((item: any) => {
|
|
|
+ if (item.type == 1 && item.code == 'IMAGE') {
|
|
|
+ filteredCategoryTree.value = item.children;
|
|
|
+ }
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+// 添加分类
|
|
|
+const openClassify = (res: any, type: any) => {
|
|
|
+ console.log(res, 'res');
|
|
|
+ if (type == 'add') {
|
|
|
+ classifyDialog.value.title = '添加分类';
|
|
|
+ categoryForm.value.name = '';
|
|
|
+ categoryForm.value.id = null;
|
|
|
+ if (res.id) {
|
|
|
+ categoryForm.value.parentId = res.id;
|
|
|
+ } else {
|
|
|
+ categoryForm.value.parentId = 1;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ classifyDialog.value.title = '编辑分类';
|
|
|
+ categoryForm.value.name = res.name;
|
|
|
+ categoryForm.value.id = res.id;
|
|
|
+ categoryForm.value.parentId = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ classifyDialog.value.dialog = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 新增编辑分类
|
|
|
+const submitCategory = async () => {
|
|
|
+ try {
|
|
|
+ await categoryFormRef.value?.validate();
|
|
|
+ classifyDialog.value.loading = true;
|
|
|
+
|
|
|
+ const categoryData = {
|
|
|
+ ...categoryForm.value,
|
|
|
+ tenantId: '000000'
|
|
|
+ };
|
|
|
+
|
|
|
+ if (categoryForm.value.id) {
|
|
|
+ await updateFileCategory(categoryData);
|
|
|
+ } else {
|
|
|
+ await addFileCategory(categoryData);
|
|
|
+ }
|
|
|
+
|
|
|
+ ElMessage.success(categoryForm.value.id ? '更新成功' : '添加成功');
|
|
|
+ classifyDialog.value.loading = false;
|
|
|
+ getCategoryTree();
|
|
|
+ } catch (error) {
|
|
|
+ console.error('保存分类失败:', error);
|
|
|
+ ElMessage.error('保存分类失败');
|
|
|
+ } finally {
|
|
|
+ classifyDialog.value.loading = false;
|
|
|
+ classifyDialog.value.dialog = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 删除分类
|
|
|
+const closeClassify = async (data: any) => {
|
|
|
+ if (data.children && data.children.length > 0) {
|
|
|
+ ElMessage.warning('该分类下有子分类,请先删除子分类');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ await ElMessageBox.confirm(`确定要删除分类"${data.name}"吗?`, '提示', {
|
|
|
+ confirmButtonText: '确定',
|
|
|
+ cancelButtonText: '取消',
|
|
|
+ type: 'warning'
|
|
|
+ } as any);
|
|
|
+ await delFileCategory(data.id);
|
|
|
+ ElMessage.success('删除成功');
|
|
|
+ getCategoryTree();
|
|
|
+ } catch (error) {
|
|
|
+ if (error !== 'cancel') {
|
|
|
+ console.error('删除分类失败:', error);
|
|
|
+ ElMessage.error('删除分类失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 树节点点击事件
|
|
|
+const handleTreeNodeClick = (res: any) => {
|
|
|
+ queryParams.value.categoryId = res.id;
|
|
|
+ if (res.id == '') {
|
|
|
+ categoryTreeRef.value.setCurrentKey(null);
|
|
|
+ }
|
|
|
+ handleSearch();
|
|
|
+};
|
|
|
+
|
|
|
+// 图片放大
|
|
|
+const imageViewer = reactive({
|
|
|
+ show: false,
|
|
|
+ index: 0
|
|
|
+});
|
|
|
+
|
|
|
+const previewImage = (list: any, index: any) => {
|
|
|
+ previewImageList.value = list;
|
|
|
+ imageViewer.index = index ? index : 0;
|
|
|
+ imageViewer.show = true;
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 删除图片
|
|
|
+ * @param index
|
|
|
+ */
|
|
|
+const removeImage = (index?: any) => {
|
|
|
+ if (props.limit == 1) {
|
|
|
+ emit('update:modelValue', '');
|
|
|
+ emit('change', '');
|
|
|
+ } else {
|
|
|
+ const list = [...imagesData.value];
|
|
|
+ list.splice(index, 1);
|
|
|
+ if (Array.isArray(props.modelValue)) {
|
|
|
+ emit('update:modelValue', list);
|
|
|
+ emit('change', list);
|
|
|
+ } else {
|
|
|
+ emit('update:modelValue', list.join(','));
|
|
|
+ emit('change', list.join(','));
|
|
|
+ }
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 重命名文件
|
|
|
+const handleRename = (file: any) => {
|
|
|
+ renameForm.value = {
|
|
|
+ id: file.id,
|
|
|
+ name: '',
|
|
|
+ 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;
|
|
|
+
|
|
|
+ // 刷新文件列表
|
|
|
+ handleSearch();
|
|
|
+
|
|
|
+ console.log('重命名文件:', {
|
|
|
+ id: renameForm.value.id,
|
|
|
+ oldName: renameForm.value.originalName,
|
|
|
+ newName: renameForm.value.name.trim()
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('重命名失败:', error);
|
|
|
+ ElMessage.error('重命名失败');
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化文件大小
|
|
|
+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 beforeUpload = (file) => {
|
|
|
+ // 获取准确的MIME类型
|
|
|
+ const actualMimeType = getFileMimeType(file);
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase();
|
|
|
+
|
|
|
+ let isValidType = false;
|
|
|
+ let fileTypeText = '';
|
|
|
+
|
|
|
+ isValidType = actualMimeType.startsWith('image/') || ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'].includes(extension);
|
|
|
+ fileTypeText = '图片';
|
|
|
+
|
|
|
+ if (!isValidType) {
|
|
|
+ ElMessage.error(`只能上传${fileTypeText}文件! 检测到的文件类型: ${actualMimeType}`);
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查文件大小 (50MB)
|
|
|
+ const isLtSize = file.size / 1024 / 1024 < 50;
|
|
|
+ if (!isLtSize) {
|
|
|
+ ElMessage.error('上传文件大小不能超过 50MB!');
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 非图片文件或非图片分类,直接上传
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+// 上传成功
|
|
|
+const onUploadSuccess = (response, file) => {
|
|
|
+ if (response.code === 200) {
|
|
|
+ // 获取文件MIME类型和扩展名
|
|
|
+ const mimeType = getFileMimeType(file);
|
|
|
+ const fileName = file.name || '';
|
|
|
+ const extension = fileName.split('.').pop()?.toLowerCase() || '';
|
|
|
+ const datas = {
|
|
|
+ categoryId: queryParams.value.categoryId,
|
|
|
+ categoryType: 1,
|
|
|
+ description: '',
|
|
|
+ downloadCount: 0,
|
|
|
+ extension: extension,
|
|
|
+ isPublic: 1,
|
|
|
+ name: file.name,
|
|
|
+ originalName: file.name,
|
|
|
+ ossId: response.data?.ossId || '',
|
|
|
+ path: response.data?.url || '',
|
|
|
+ size: file.size,
|
|
|
+ status: 0,
|
|
|
+ type: mimeType,
|
|
|
+ uploadStatus: 1,
|
|
|
+ url: response.data?.url || '',
|
|
|
+ viewCount: 0
|
|
|
+ };
|
|
|
+ addFileInfo(datas).then((res) => {
|
|
|
+ if (res.code == 200) {
|
|
|
+ ElMessage.success('文件上传成功');
|
|
|
+ handleSearch();
|
|
|
+ }
|
|
|
+ });
|
|
|
+ } else {
|
|
|
+ ElMessage.error('上传失败:' + response.msg);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 上传失败
|
|
|
+const onUploadError = (error) => {
|
|
|
+ console.error('上传失败:', error);
|
|
|
+ ElMessage.error('上传失败');
|
|
|
+};
|
|
|
+
|
|
|
+// 获取上传文件接受类型
|
|
|
+const getUploadFileAccept = () => {
|
|
|
+ return '.jpg,.jpeg,.png,.gif,.bmp,.webp';
|
|
|
+};
|
|
|
+
|
|
|
+// 获取文件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 style = computed(() => {
|
|
|
+ return {
|
|
|
+ width: props.width,
|
|
|
+ height: props.height
|
|
|
+ };
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+.image-wrap {
|
|
|
+ .operation {
|
|
|
+ display: none;
|
|
|
+ }
|
|
|
+
|
|
|
+ &:hover {
|
|
|
+ .operation {
|
|
|
+ display: flex;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.border-color {
|
|
|
+ border-color: #e5e7eb;
|
|
|
+}
|
|
|
+
|
|
|
+.dialog-bos {
|
|
|
+ height: 750px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ padding: 10px;
|
|
|
+ border-radius: 0 0 8px 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ margin-left: 0px !important;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ .toolbar {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .content-wrapper {
|
|
|
+ display: flex;
|
|
|
+ gap: 20px;
|
|
|
+ height: 0;
|
|
|
+ flex: 1;
|
|
|
+ .sidebar {
|
|
|
+ width: 280px;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ overflow: auto;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ padding: 10px;
|
|
|
+ .category-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ padding: 0px 15px;
|
|
|
+ border-radius: 6px;
|
|
|
+ cursor: pointer;
|
|
|
+ margin-bottom: 8px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+ height: 40px;
|
|
|
+ .category-icon {
|
|
|
+ margin-right: 10px;
|
|
|
+ font-size: 18px;
|
|
|
+ }
|
|
|
+ &.active {
|
|
|
+ background: #409eff;
|
|
|
+ color: white;
|
|
|
+ .more-icon {
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ &:hover {
|
|
|
+ color: white;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ /* 树形控件样式 */
|
|
|
+ .category-tree {
|
|
|
+ background: transparent;
|
|
|
+ :deep(.el-tree-node__content) {
|
|
|
+ height: 40px;
|
|
|
+ border-radius: 6px;
|
|
|
+ margin-bottom: 4px;
|
|
|
+ background: #f5f7fa;
|
|
|
+ transition: background-color 0.3s;
|
|
|
+ }
|
|
|
+ :deep(.el-tree-node__content:hover) {
|
|
|
+ background: #ebeef5;
|
|
|
+ }
|
|
|
+ :deep(.el-tree-node.is-current > .el-tree-node__content) {
|
|
|
+ background: #409eff;
|
|
|
+ color: white;
|
|
|
+ .more-icon {
|
|
|
+ color: rgba(255, 255, 255, 0.8);
|
|
|
+ &:hover {
|
|
|
+ color: white;
|
|
|
+ background: rgba(255, 255, 255, 0.2);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ :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;
|
|
|
+ .category-icon {
|
|
|
+ margin-right: 8px;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+ .node-label {
|
|
|
+ flex: 1;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .node-actions {
|
|
|
+ margin-left: auto;
|
|
|
+ .more-icon {
|
|
|
+ font-size: 16px;
|
|
|
+ color: #909399;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: color 0.3s;
|
|
|
+ border-radius: 2px;
|
|
|
+ &:hover {
|
|
|
+ color: #409eff;
|
|
|
+ background: rgba(64, 158, 255, 0.1);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .content-area {
|
|
|
+ flex: 1;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
+ overflow: hidden;
|
|
|
+ .file-grid {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 15px 10px;
|
|
|
+ padding: 10px;
|
|
|
+ flex: 1;
|
|
|
+ overflow-y: auto;
|
|
|
+ min-height: 0;
|
|
|
+ .file-item {
|
|
|
+ flex: 0 0 calc((100% - 30px) / 4);
|
|
|
+ overflow: hidden;
|
|
|
+ position: relative;
|
|
|
+ cursor: pointer;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
|
|
+ transition:
|
|
|
+ transform 0.3s ease,
|
|
|
+ box-shadow 0.3s ease;
|
|
|
+ height: 223px;
|
|
|
+ &:hover {
|
|
|
+ transform: translateY(-5px);
|
|
|
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
|
|
|
+ }
|
|
|
+ &.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-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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .file-checkbox {
|
|
|
+ position: absolute;
|
|
|
+ top: 8px;
|
|
|
+ right: 8px;
|
|
|
+ z-index: 10;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .file-info {
|
|
|
+ padding: 5px 5px 10px 5px;
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ .pagination {
|
|
|
+ display: flex;
|
|
|
+ justify-content: center;
|
|
|
+ padding: 10px;
|
|
|
+ background: white;
|
|
|
+ flex-shrink: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+</style>
|