1422 lines
41 KiB
Vue
1422 lines
41 KiB
Vue
<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>
|