platform-vue/src/views/system/fileManager/index.vue
2026-04-09 16:26:35 +08:00

1422 lines
41 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="file-manager-container">
<el-card>
<template #header>
<div class="card-header">
<span class="title">
<el-icon>
<Folder />
</el-icon>
文件管理系统
</span>
<el-button :icon="Refresh" @click="refushData"> 刷新 </el-button>
</div>
</template>
<el-row :gutter="20">
<!-- 左侧分组列表 -->
<el-col :span="6">
<el-card shadow="hover" class="group-card" style="min-height: 600px">
<template #header>
<div class="card-title">
<div style="display: flex; align-items: center; gap: 8px">
<el-icon>
<Grid />
</el-icon>
<span>文件分组</span>
</div>
<el-button type="primary" size="small" @click="handleCreateCategory">新建分组</el-button>
</div>
</template>
<el-input v-model="groupSearchQuery" placeholder="搜索分组..." :prefix-icon="Search" clearable
class="search-input" />
<el-scrollbar class="group-list" v-loading="loading">
<div v-for="group in filteredGroups" :key="group.id" :class="[
'group-item',
{ active: selectedGroup?.id === group.id },
]" @click="selectGroup(group)">
<div class="group-info">
<div class="group-name">
<div style="display: flex; align-items: center; gap: 8px">
<el-icon class="group-icon">
<FolderOpened />
</el-icon>
{{ group.name }}
</div>
</div>
<div class="group-count">{{ group.total }} 个文件</div>
</div>
<div class="group-actions" v-if="group.id !== 0">
<div class="action-buttons" @click.stop>
<el-button type="primary" link size="small" :icon="Edit" @click="handleRenameCategory(group)"
title="重命名" />
<el-button type="danger" link size="small" :icon="Delete" @click="deleteFileCateData(group.id)"
title="删除" />
</div>
</div>
</div>
<el-empty v-if="filteredGroups.length === 0" description="暂无分组数据" />
</el-scrollbar>
</el-card>
</el-col>
<!-- 右侧:文件内容 -->
<el-col :span="18">
<el-card style="min-height: 600px" shadow="hover" class="file-content-card">
<template #header>
<div class="card-title">
<div style="display: flex; align-items: center; gap: 8px">
<el-icon>
<Document />
</el-icon>
<span>文件列表</span>
</div>
<el-button type="primary" :icon="Upload" @click="handleUpload">
上传文件
</el-button>
</div>
</template>
<div v-if="!selectedGroup" class="empty-state">
<el-empty description="请先选择一个分组" />
</div>
<div v-else class="file-content">
<!-- 工具栏 -->
<div class="toolbar">
<el-input v-model="fileSearchQuery" placeholder="搜索文件..." :prefix-icon="Search" clearable
style="width: 300px" @clear="handleFileSearch" @keyup.enter="handleFileSearch" />
<div class="actions">
<el-link type="warning" :underline="false" @click="handleBatchMove"
:disabled="selectedFileIds.length === 0">
<el-icon :size="16">
<FolderOpened />
</el-icon>
批量移动 ({{ selectedFileIds.length }})
</el-link>
<el-link type="danger" :underline="false" @click="handleBatchDelete"
:disabled="selectedFileIds.length === 0">
<el-icon :size="16">
<Delete />
</el-icon>
批量删除 ({{ selectedFileIds.length }})
</el-link>
<el-link type="danger" :underline="false" @click="handleBatchDeletePermanently"
:disabled="selectedFileIds.length === 0">
<el-icon :size="16">
<DeleteFilled />
</el-icon>
批量永久删除 ({{ selectedFileIds.length }})
</el-link>
<el-link type="info" :underline="false" @click="clearSelection"
:disabled="selectedFileIds.length === 0">
取消选择
</el-link>
</div>
</div>
<!-- 文件卡片网格 -->
<div class="file-grid-container">
<div class="file-grid-header" v-if="files.length > 0">
<button class="allbtns" @click="toggleSelectAll">全选/反选</button>
</div>
<div v-loading="loading" class="file-grid" v-if="files.length > 0">
<el-card v-for="file in files" :key="file.id" shadow="hover"
:class="['file-card', { 'file-card-selected': selectedFileIds.includes(file.id) }]"
@click="toggleFileSelection(file)">
<div class="file-checkbox">
<el-checkbox :model-value="selectedFileIds.includes(file.id)" />
</div>
<div class="file-icon">
<div v-if="isImage(file)">
<img :src="getFileUrl(file.url)" alt="file" class="preview-image" />
</div>
<div v-else-if="isVideo(file)">
<video :src="getFileUrl(file.url)" alt="file" />
</div>
<div v-else-if="isDocument(file)">
<el-icon :size="48" color="#3973ff">
<Document />
</el-icon>
</div>
<div v-else>
<el-icon :size="48" color="#909399">
<Files />
</el-icon>
</div>
</div>
<div class="file-name" :title="file.name">
{{ file.name }}
</div>
<div class="file-info">
<span class="file-size">{{
formatFileSize(file.size)
}}</span>
</div>
<div class="file-actions">
<el-button v-if="isImage(file)" type="primary" link @click.stop="handleImagePreview(file)"
title="预览">
<i class="fa-solid fa-eye"></i>
</el-button>
<el-button type="primary" link @click.stop="handleMoveClick(file)" title="移动">
<i class="fa-solid fa-arrows-up-down-left-right"></i>
</el-button>
<el-button type="primary" link @click.stop="handleDownload(file)" title="下载">
<i class="fa-solid fa-download"></i>
</el-button>
<el-button type="danger" link @click.stop="handleDelete(file)" title="删除">
<i class="fa-regular fa-trash-can"></i>
</el-button>
<el-button type="danger" link @click.stop="handlePermanentDelete(file)" title="彻底删除">
<i class="fa-solid fa-trash"></i>
</el-button>
</div>
</el-card>
</div>
<el-empty v-if="!loading && files.length === 0" description="该分组暂无文件" />
</div>
<!-- 分页组件 -->
<div class="pagination-container">
<el-pagination v-model:current-page="currentPage" v-model:page-size="pageSize"
:page-sizes="[12, 24, 48, 96]" :total="total" layout="total, sizes, prev, pager, next, jumper"
@size-change="handleSizeChange" @current-change="handlePageChange" />
</div>
</div>
</el-card>
</el-col>
</el-row>
</el-card>
<!-- 重命名分组对话框 -->
<el-dialog v-model="showRenameDialog" title="重命名分组" width="400px" @close="handleRenameDialogClose">
<el-form ref="renameFormRef" :model="renameForm" :rules="renameFormRules" label-width="80px">
<el-form-item label="分组名称" prop="name">
<el-input v-model="renameForm.name" placeholder="请输入分组名称" maxlength="20" show-word-limit />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showRenameDialog = false">取消</el-button>
<el-button type="primary" @click="handleRenameConfirm" :loading="renaming">
确定
</el-button>
</template>
</el-dialog>
<!-- 上传文件对话框 -->
<UploadFile v-model="showUploadDialog" :category-id="selectedGroup?.id" @success="handleUploadSuccess" />
<!-- 图片预览对话框 -->
<el-dialog v-model="showImagePreview" title="图片预览" width="90%" :before-close="handlePreviewClose"
class="image-preview-dialog" @opened="handlePreviewOpened">
<template #header>
<div class="preview-header">
<span>图片预览</span>
<div class="preview-toolbar">
<el-button type="primary" :icon="ZoomOut" circle size="small" @click="zoomOut"
:disabled="imageScale <= 0.5" />
<span class="zoom-text">{{ Math.round(imageScale * 100) }}%</span>
<el-button type="primary" :icon="ZoomIn" circle size="small" @click="zoomIn" :disabled="imageScale >= 3" />
<el-button type="primary" :icon="RefreshLeft" circle size="small" @click="resetZoom" />
<el-button type="primary" :icon="FullScreen" circle size="small" @click="toggleFullscreen" />
</div>
</div>
</template>
<div class="preview-container" ref="previewContainerRef" @wheel="handleWheel">
<div class="preview-image-wrapper" :style="{
transform: `scale(${imageScale}) translate(${imagePosition.x}px, ${imagePosition.y}px)`,
transformOrigin: 'center center',
cursor: imageScale > 1 ? 'move' : 'default',
}" @mousedown="handleMouseDown">
<img :src="previewImageUrl" alt="预览图片" class="preview-image-full" draggable="false" @load="handleImageLoad" />
</div>
<div class="preview-info" v-if="previewFile">
<div class="preview-name">{{ previewFile.name }}</div>
<div class="preview-size">{{ formatFileSize(previewFile.size) }}</div>
</div>
</div>
</el-dialog>
<!-- 移动文件对话框 -->
<MoveFile v-model="showMoveDialog" :file-id="selectedFile?.id" :current-cate-id="selectedFile?.groupId"
:cate-list="groups" @moved="handleMoveSuccess" />
<!-- 重命名文件分组对话框 -->
<RenameCategory ref="renameCategoryRef" v-model="showRenameCategoryDialog" :category-id="currentRenameCategoryId"
:category-name="currentRenameCategoryName" @success="handleRenameCategorySuccess"
@close="handleRenameCategoryClose" />
<!-- 创建文件分组对话框 -->
<CreateCategory ref="createCategoryRef" v-model="showCreateCategoryDialog" @success="handleCreateCategorySuccess"
@close="handleCreateCategoryClose" />
<!-- 批量移动文件对话框 -->
<el-dialog v-model="showBatchMoveDialog" title="批量移动文件" width="400px">
<el-form label-width="80px">
<el-form-item label="目标分组">
<el-select v-model="batchMoveTargetCate" placeholder="请选择目标分组">
<el-option :value="0" label="未分类" />
<el-option v-for="group in groups" :key="group.id" :value="group.id" :label="group.name" />
</el-select>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="showBatchMoveDialog = false">取消</el-button>
<el-button type="primary" @click="confirmBatchMove" :loading="batchMoveLoading">
确定
</el-button>
</template>
</el-dialog>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, onMounted } from "vue";
import { ElMessage, ElMessageBox } from "element-plus";
import { getFileUrl as getFileUrlUtil } from "@/utils/url";
import {
Folder,
Grid,
Search,
Check,
FolderOpened,
Document,
Upload,
Refresh,
Picture,
VideoPlay,
Files,
Download,
Delete,
DeleteFilled,
Edit,
ZoomIn,
ZoomOut,
RefreshLeft,
FullScreen,
} from "@element-plus/icons-vue";
import type { FormInstance, FormRules } from "element-plus";
import {
getUserCate,
getCateFiles,
createFileCate,
renameFileCate,
deleteFileCate,
deleteFilePermanently,
deleteFile,
batchDeleteFiles,
batchDeleteFilesPermanently,
batchMoveFiles,
} from "@/api/file";
import UploadFile from "./components/uploadFile.vue";
import MoveFile from "./components/moveFile.vue";
import RenameCategory from "./components/renameCategory.vue";
import CreateCategory from "./components/createCategory.vue";
// 分组相关
interface Group {
id: number;
name: string;
fileCount: number;
description?: string;
}
const groups = ref<Group[]>([]);
const groupSearchQuery = ref("");
const selectedGroup = ref<Group | null>(null);
// 文件相关
interface FileItem {
id: number;
name: string;
size: number;
type: string;
url: string;
createTime: string;
groupId: number;
}
const files = ref<FileItem[]>([]);
const fileSearchQuery = ref("");
const loading = ref(false);
// 分页相关
const currentPage = ref(1);
const pageSize = ref(20);
const total = ref(0);
// 上传对话框
const showUploadDialog = ref(false);
// 移动文件对话框
const showMoveDialog = ref(false);
const selectedFile = ref<FileItem | null>(null);
// 批量选择
const selectedFileIds = ref<number[]>([]);
// 批量移动对话框
const showBatchMoveDialog = ref(false);
const batchMoveTargetCate = ref(0);
const batchMoveLoading = ref(false);
// 图片预览
const showImagePreview = ref(false);
const previewImageUrl = ref("");
const previewFile = ref<FileItem | null>(null);
const previewContainerRef = ref<HTMLElement | null>(null);
// 图片缩放和位置
const imageScale = ref(1);
const imagePosition = ref({ x: 0, y: 0 });
const isDragging = ref(false);
const dragStart = ref({ x: 0, y: 0 });
const isFullscreen = ref(false);
// 重命名文件分组对话框
const showRenameCategoryDialog = ref(false);
const renameCategoryRef = ref<any>(null);
const currentRenameCategoryId = ref<number | null>(null);
const currentRenameCategoryName = ref<string>("");
// 创建文件分组对话框
const showCreateCategoryDialog = ref(false);
const createCategoryRef = ref<any>(null);
//获取用户分类
const getUserCateData = async () => {
try {
const res = await getUserCate();
if (res.code === 200) {
// 为每个分类添加文件数量初始为0后续可以通过接口获取
groups.value = res.data.map((item: any) => ({
id: item.id,
name: item.name,
total: item.total || 0,
description: item.description,
}));
// 默认选择"未分类"组
selectGroup(uncategorizedGroup.value);
}
} catch (error) {
console.error("获取分类失败:", error);
ElMessage.error("获取分类失败");
}
};
//图片拼接接口地址
const getFileUrl = (url: string) => {
return getFileUrlUtil(url);
};
// 未分类分组(固定,不可编辑)
const uncategorizedGroup = ref<Group>({
id: 0,
name: "未分类",
total: 0,
description: "未分类的文件",
});
// 过滤后的分组列表(未分类组置顶)
const filteredGroups = computed(() => {
let result = [uncategorizedGroup.value, ...groups.value];
if (groupSearchQuery.value) {
const query = groupSearchQuery.value.toLowerCase();
result = result.filter((group) => group.name.toLowerCase().includes(query));
}
return result;
});
// 搜索文件(触发重新加载)
const handleFileSearch = () => {
currentPage.value = 1; // 搜索时重置到第一页
loadFiles();
};
// 选择分组
const selectGroup = (group: Group) => {
selectedGroup.value = group;
currentPage.value = 1; // 切换分组时重置到第一页
loadFiles();
};
// 打开创建分组对话框
const handleCreateCategory = () => {
showCreateCategoryDialog.value = true;
// 如果组件有 open 方法,调用它
if (createCategoryRef.value && createCategoryRef.value.open) {
createCategoryRef.value.open();
}
};
// 创建分组成功回调
const handleCreateCategorySuccess = () => {
getUserCateData();
};
// 关闭创建分组对话框
const handleCreateCategoryClose = () => {
showCreateCategoryDialog.value = false;
};
//重命名文件分组
const renameFileCateData = async (id: number, data: any) => {
const res = await renameFileCate(id, data);
if (res.code === 200) {
getUserCateData();
}
};
// 打开重命名对话框
const handleRenameCategory = (group: any) => {
if (!group || !group.id) {
ElMessage.error("分组信息不完整");
return;
}
currentRenameCategoryId.value = group.id;
currentRenameCategoryName.value = group.name || "";
showRenameCategoryDialog.value = true;
// 如果组件有 open 方法,调用它
if (renameCategoryRef.value && renameCategoryRef.value.open) {
renameCategoryRef.value.open(group.id, group.name);
}
};
// 重命名成功回调
const handleRenameCategorySuccess = () => {
getUserCateData();
// 如果当前选中的是重命名的分组,更新选中分组信息
if (
selectedGroup.value &&
selectedGroup.value.id === currentRenameCategoryId.value
) {
const updatedGroup = groups.value.find(
(g: any) => g.id === currentRenameCategoryId.value,
);
if (updatedGroup) {
selectGroup(updatedGroup);
}
}
};
// 关闭重命名对话框
const handleRenameCategoryClose = () => {
showRenameCategoryDialog.value = false;
currentRenameCategoryId.value = null;
currentRenameCategoryName.value = "";
};
//删除文件分组
const deleteFileCateData = async (id: number) => {
ElMessageBox.confirm("确定删除该文件分组吗?", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(async () => {
const res = await deleteFileCate(id);
if (res.code === 200) {
ElMessage.success("删除成功");
getUserCateData();
} else {
ElMessage.error(res.msg || "删除失败");
}
});
};
// 加载文件列表
const loadFiles = async () => {
if (!selectedGroup.value) {
files.value = [];
total.value = 0;
return;
}
loading.value = true;
try {
// 如果是未分类组id === 0传递 0 作为分类ID
const cateId = selectedGroup.value.id === 0 ? 0 : selectedGroup.value.id;
const res = await getCateFiles(
cateId,
currentPage.value,
pageSize.value,
fileSearchQuery.value,
);
if (res.code === 200 && res.data) {
// 更新总数
total.value = res.data.total || 0;
// 更新分组的文件数量
if (selectedGroup.value.id === 0) {
// 更新未分类组的文件数量
uncategorizedGroup.value.total = res.data.total || 0;
} else {
// 更新普通分组的文件数量
const group = groups.value.find(
(g) => g.id === selectedGroup.value!.id,
);
if (group) {
group.total = res.data.total || 0;
}
}
// 处理文件数据将type数字转换为MIME类型
files.value = (res.data.list || []).map((file: any) => {
// 根据文件扩展名判断MIME类型
const fileName = file.name || "";
const ext = fileName.split(".").pop()?.toLowerCase() || "";
let mimeType = "application/octet-stream";
// 图片类型
if (["jpg", "jpeg", "png", "gif", "bmp", "webp"].includes(ext)) {
mimeType = `image/${ext === "jpg" ? "jpeg" : ext}`;
}
// 视频类型
else if (["mp4", "webm", "mov", "avi"].includes(ext)) {
mimeType = `video/${ext}`;
}
// 音频类型
else if (["mp3", "wav", "ogg"].includes(ext)) {
mimeType = `audio/${ext}`;
}
// PDF
else if (ext === "pdf") {
mimeType = "application/pdf";
}
// Word文档
else if (["doc", "docx"].includes(ext)) {
mimeType = "application/msword";
}
// Excel
else if (["xls", "xlsx"].includes(ext)) {
mimeType = "application/vnd.ms-excel";
}
// PowerPoint
else if (["ppt", "pptx"].includes(ext)) {
mimeType = "application/vnd.ms-powerpoint";
}
// 文本文件
else if (ext === "txt") {
mimeType = "text/plain";
}
return {
id: file.id,
name: file.name,
size: file.size || 0,
type: mimeType,
url: file.url || file.src || "",
createTime: file.createTime || file.create_time || "",
groupId: file.groupId || file.cate || selectedGroup.value!.id,
};
});
} else {
files.value = [];
total.value = 0;
ElMessage.warning(res.msg || "获取文件列表失败");
}
} catch (error) {
console.error("加载文件列表失败:", error);
ElMessage.error("加载文件列表失败");
files.value = [];
total.value = 0;
} finally {
loading.value = false;
}
};
//刷新数据
const refushData = () => {
getUserCateData();
loadFiles();
};
// 分页大小改变
const handleSizeChange = (size: number) => {
pageSize.value = size;
currentPage.value = 1; // 改变每页数量时重置到第一页
loadFiles();
};
// 页码改变
const handlePageChange = (page: number) => {
currentPage.value = page;
loadFiles();
};
// 判断文件类型
const isImage = (file: FileItem) => {
return file.type.startsWith("image/");
};
const isVideo = (file: FileItem) => {
return file.type.startsWith("video/");
};
const isDocument = (file: FileItem) => {
return (
file.type.includes("pdf") ||
file.type.includes("msword") ||
file.type.includes("ms-excel") ||
file.type.includes("ms-powerpoint") ||
file.type.includes("text") ||
file.type.includes("document")
);
};
// 格式化文件大小
const formatFileSize = (bytes: number): string => {
if (bytes === 0) return "0 B";
const k = 1024;
const sizes = ["B", "KB", "MB", "GB", "TB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + " " + sizes[i];
};
// 格式化日期
const formatDate = (dateStr: string): string => {
const date = new Date(dateStr);
return date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "2-digit",
day: "2-digit",
});
};
// 文件操作
const handleFileClick = (file: FileItem) => {
console.log("点击文件:", file);
// TODO: 实现文件预览或打开
};
// 图片预览
const handleImagePreview = (file: FileItem) => {
previewFile.value = file;
previewImageUrl.value = getFileUrl(file.url);
showImagePreview.value = true;
resetZoom();
};
// 预览对话框打开后
const handlePreviewOpened = () => {
resetZoom();
};
// 图片加载完成
const handleImageLoad = () => {
resetZoom();
};
// 关闭预览
const handlePreviewClose = () => {
showImagePreview.value = false;
previewImageUrl.value = "";
previewFile.value = null;
resetZoom();
if (isFullscreen.value) {
exitFullscreen();
}
};
// 放大
const zoomIn = () => {
if (imageScale.value < 3) {
imageScale.value = Math.min(imageScale.value + 0.25, 3);
}
};
// 缩小
const zoomOut = () => {
if (imageScale.value > 0.5) {
imageScale.value = Math.max(imageScale.value - 0.25, 0.5);
}
};
// 重置缩放
const resetZoom = () => {
imageScale.value = 1;
imagePosition.value = { x: 0, y: 0 };
};
// 鼠标滚轮缩放
const handleWheel = (e: WheelEvent) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -0.1 : 0.1;
const newScale = Math.max(0.5, Math.min(3, imageScale.value + delta));
imageScale.value = newScale;
};
// 鼠标按下开始拖拽
const handleMouseDown = (e: MouseEvent) => {
if (imageScale.value > 1) {
isDragging.value = true;
dragStart.value = {
x: e.clientX - imagePosition.value.x,
y: e.clientY - imagePosition.value.y,
};
document.addEventListener("mousemove", handleMouseMove);
document.addEventListener("mouseup", handleMouseUp);
}
};
// 鼠标移动拖拽
const handleMouseMove = (e: MouseEvent) => {
if (isDragging.value) {
imagePosition.value = {
x: e.clientX - dragStart.value.x,
y: e.clientY - dragStart.value.y,
};
}
};
// 鼠标释放结束拖拽
const handleMouseUp = () => {
isDragging.value = false;
document.removeEventListener("mousemove", handleMouseMove);
document.removeEventListener("mouseup", handleMouseUp);
};
// 全屏切换
const toggleFullscreen = () => {
if (!isFullscreen.value) {
enterFullscreen();
} else {
exitFullscreen();
}
};
// 进入全屏
const enterFullscreen = () => {
const element = previewContainerRef.value;
if (element) {
if (element.requestFullscreen) {
element.requestFullscreen();
} else if ((element as any).webkitRequestFullscreen) {
(element as any).webkitRequestFullscreen();
} else if ((element as any).mozRequestFullScreen) {
(element as any).mozRequestFullScreen();
} else if ((element as any).msRequestFullscreen) {
(element as any).msRequestFullscreen();
}
isFullscreen.value = true;
}
};
// 退出全屏
const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if ((document as any).webkitExitFullscreen) {
(document as any).webkitExitFullscreen();
} else if ((document as any).mozCancelFullScreen) {
(document as any).mozCancelFullScreen();
} else if ((document as any).msExitFullscreen) {
(document as any).msExitFullscreen();
}
isFullscreen.value = false;
};
const handleDownload = (file: FileItem) => {
console.log("下载文件:", file);
ElMessage.success(`开始下载: ${file.name}`);
// TODO: 实现文件下载
};
// 点击移动按钮
const handleMoveClick = (file: FileItem) => {
selectedFile.value = file;
showMoveDialog.value = true;
};
// 移动成功回调
const handleMoveSuccess = () => {
// 关闭对话框
showMoveDialog.value = false;
selectedFile.value = null;
// 刷新文件列表
loadFiles();
getUserCateData();
};
function handleDelete(row) {
ElMessageBox.confirm("确定要删除文件吗?此操作不可恢复。", "提示", {
confirmButtonText: "确定",
cancelButtonText: "取消",
type: "warning",
}).then(() => {
deleteFile(row.id).then((res) => {
const resp =
res && typeof res.code !== "undefined"
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success("删除成功");
loadFiles();
} else {
ElMessage.error((resp && resp.message) || "删除失败");
}
});
});
}
// 彻底删除文件
function handlePermanentDelete(row) {
ElMessageBox.confirm("确定要彻底删除文件吗?此操作不可恢复,服务器上的文件也会被删除!", "警告", {
confirmButtonText: "确定删除",
cancelButtonText: "取消",
type: "error",
}).then(() => {
deleteFilePermanently(row.id).then((res) => {
const resp =
res && typeof res.code !== "undefined"
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success("彻底删除成功");
loadFiles();
} else {
ElMessage.error((resp && resp.message) || "彻底删除失败");
}
});
});
}
const handleUpload = () => {
if (!selectedGroup.value) {
ElMessage.warning("请先选择一个分组");
return;
}
showUploadDialog.value = true;
};
// 上传成功回调
const handleUploadSuccess = () => {
// 刷新文件列表
loadFiles();
};
// 切换文件选择
const toggleFileSelection = (file: FileItem) => {
const index = selectedFileIds.value.indexOf(file.id);
if (index > -1) {
selectedFileIds.value.splice(index, 1);
} else {
selectedFileIds.value.push(file.id);
}
};
// 全选/反选切换
const toggleSelectAll = () => {
const allFileIds = files.value.map(file => file.id);
const isAllSelected = allFileIds.length === selectedFileIds.value.length && allFileIds.length > 0;
if (isAllSelected) {
selectedFileIds.value = [];
} else {
selectedFileIds.value = allFileIds;
}
};
// 清除选择
const clearSelection = () => {
selectedFileIds.value = [];
};
// 批量移动
const handleBatchMove = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要移动的文件');
return;
}
batchMoveTargetCate.value = 0;
showBatchMoveDialog.value = true;
};
// 确认批量移动
const confirmBatchMove = async () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要移动的文件');
return;
}
if (batchMoveTargetCate.value === null || batchMoveTargetCate.value === undefined) {
ElMessage.warning('请选择目标分组');
return;
}
batchMoveLoading.value = true;
try {
const res = await batchMoveFiles(selectedFileIds.value, batchMoveTargetCate.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量移动成功');
showBatchMoveDialog.value = false;
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量移动失败');
}
} catch (error) {
console.error('批量移动失败:', error);
ElMessage.error('批量移动失败');
} finally {
batchMoveLoading.value = false;
}
};
// 批量删除
const handleBatchDelete = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要删除的文件');
return;
}
ElMessageBox.confirm(
`确定要删除选中的 ${selectedFileIds.value.length} 个文件吗?`,
'提示',
{
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning',
}
).then(async () => {
try {
const res = await batchDeleteFiles(selectedFileIds.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量删除成功');
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量删除失败');
}
} catch (error) {
console.error('批量删除失败:', error);
ElMessage.error('批量删除失败');
}
});
};
// 批量永久删除
const handleBatchDeletePermanently = () => {
if (selectedFileIds.value.length === 0) {
ElMessage.warning('请先选择要永久删除的文件');
return;
}
ElMessageBox.confirm(
`确定要永久删除选中的 ${selectedFileIds.value.length} 个文件吗?此操作不可恢复,服务器上的文件也会被删除!`,
'危险操作',
{
confirmButtonText: '确定永久删除',
cancelButtonText: '取消',
type: 'error',
}
).then(async () => {
try {
const res = await batchDeleteFilesPermanently(selectedFileIds.value);
const resp =
res && typeof res.code !== 'undefined'
? res
: res && res.data
? res.data
: res;
if (resp.code === 200) {
ElMessage.success('批量永久删除成功');
clearSelection();
loadFiles();
getUserCateData();
} else {
ElMessage.error((resp && resp.message) || '批量永久删除失败');
}
} catch (error) {
console.error('批量永久删除失败:', error);
ElMessage.error('批量永久删除失败');
}
});
};
// 初始化
onMounted(() => {
getUserCateData();
});
</script>
<style lang="less" scoped>
.file-manager-container {
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
.title {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: var(--el-text-color-primary);
}
.subtitle {
font-size: 13px;
color: var(--el-text-color-placeholder);
}
}
.group-card,
.file-content-card {
// height: 100%;
.card-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
font-size: 16px;
font-weight: 600;
.selected-group-name {
font-size: 14px;
color: var(--el-color-primary);
font-weight: normal;
margin-left: 8px;
}
}
}
.search-input {
margin-bottom: 16px;
}
.group-list {
margin-top: 16px;
.group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 16px;
margin-bottom: 8px;
// background: #f5f7fa;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s;
&:hover {
// background: #e6f0ff;
transform: translateX(4px);
}
&.active {
background: var(--el-color-primary-light-9);
border-left: 3px solid var(--el-color-primary);
.group-name {
color: var(--el-color-primary);
font-weight: 600;
}
}
.group-info {
flex: 1;
.group-name {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 500;
color: var(--el-text-color-primary);
margin-bottom: 4px;
.group-icon {
font-size: 18px;
}
}
.group-count {
font-size: 12px;
color: #909399;
}
}
.check-icon {
color: var(--el-color-primary);
font-size: 20px;
}
}
}
.file-content {
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 16px;
border-bottom: 1px solid #ebeef5;
.actions {
display: flex;
gap: 8px;
}
}
.file-grid-container {
margin-bottom: 16px;
min-height: 400px;
}
.file-grid-header {
margin-bottom: 12px;
display: flex;
justify-content: flex-start;
.allbtns {
color: #0081ff;
&:hover {
color: #0088ff;
}
}
}
.file-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-auto-rows: auto;
align-items: start;
gap: 16px;
padding: 8px;
min-height: 400px;
.file-card {
cursor: pointer;
transition: all 0.3s;
// height: 100%;
display: flex;
flex-direction: column;
position: relative;
&:hover {
transform: translateY(-4px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
&.file-card-selected {
border: 2px solid var(--el-color-primary);
box-shadow: 0 0 0 2px rgba(var(--el-color-primary-rgb), 0.2);
}
.file-checkbox {
position: absolute;
top: 8px;
left: 8px;
z-index: 10;
}
:deep(.el-card__body) {
padding: 16px;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.file-icon {
margin-bottom: 12px;
height: 120px;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
.preview-image {
max-width: 100%;
max-height: 120px;
object-fit: contain;
cursor: pointer;
border-radius: 4px;
transition: transform 0.3s;
&:hover {
transform: scale(1.05);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
}
}
video {
max-width: 100%;
max-height: 120px;
object-fit: contain;
}
}
.file-name {
font-size: 14px;
font-weight: 500;
color: #303133;
margin-bottom: 8px;
width: 100%;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
flex-shrink: 0;
}
.file-info {
display: flex;
flex-direction: column;
gap: 4px;
font-size: 12px;
color: #909399;
margin-bottom: 12px;
width: 100%;
flex-shrink: 0;
.file-size,
.file-date {
text-align: center;
}
}
.file-actions {
display: flex;
gap: 8px;
width: 100%;
justify-content: center;
margin-top: auto;
flex-shrink: 0;
}
}
}
}
.empty-state {
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
}
.pagination-container {
display: flex;
justify-content: center;
padding: 20px 0;
margin-top: 20px;
border-top: 1px solid #ebeef5;
}
}
// 图片预览对话框
:deep(.image-preview-dialog) {
.preview-header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
.preview-toolbar {
display: flex;
align-items: center;
gap: 12px;
.zoom-text {
min-width: 50px;
text-align: center;
font-size: 14px;
color: #606266;
}
}
}
.el-dialog__body {
padding: 20px;
overflow: hidden;
}
}
// 图片预览容器
.preview-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
max-height: 80vh;
overflow: hidden;
position: relative;
// background: #f5f7fa;
border-radius: 8px;
.preview-image-wrapper {
display: inline-block;
transition: transform 0.1s ease-out;
cursor: move;
user-select: none;
.preview-image-full {
max-width: 100%;
max-height: 80vh;
object-fit: contain;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
display: block;
}
}
.preview-info {
margin-top: 20px;
text-align: center;
color: #606266;
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
background: rgba(255, 255, 255, 0.9);
padding: 12px 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
.preview-name {
font-size: 16px;
font-weight: 500;
color: #303133;
margin-bottom: 4px;
}
.preview-size {
font-size: 14px;
color: #909399;
}
}
}
</style>