Industrial-image-management.../src/views/bussiness-data/bussiness.vue

3790 lines
98 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>
<a-layout class="knowledge-container">
<!-- 侧边栏 -->
<a-layout-sider
width="260"
:collapsed-width="80"
theme="dark"
class="folder-sidebar"
:collapsed="sidebarCollapsed"
@collapse="handleSidebarCollapse"
@expand="handleSidebarExpand"
>
<div class="sidebar-header">
<a-space direction="vertical" size="medium" v-if="!sidebarCollapsed">
<a-button
type="primary"
size="large"
long
@click="handleCreateFolder"
class="create-folder-btn"
>
<template #icon><icon-plus /></template>
新建文件夹
</a-button>
<a-input-search
placeholder="搜索文件夹..."
allow-clear
v-model="searchKeyword"
@search="handleFolderSearch"
@input="handleSearchInput"
@clear="handleSearchClear"
class="search-input"
/>
</a-space>
</div>
<div class="folder-content">
<!-- 加载状态 -->
<a-skeleton :loading="loading && folderList.length === 0" :rows="5" v-if="loading" :animation="true">
<template #skeleton>
<div class="skeleton-item flex items-center px-4 py-3" v-for="i in 5" :key="i">
<div class="w-6 h-6 rounded bg-gray-200 mr-3"></div>
<div class="flex-1 h-4 bg-gray-200 rounded"></div>
</div>
</template>
</a-skeleton>
<!-- 搜索结果提示 -->
<div v-if="searchKeyword && !loading" class="search-result-tip">
<a-typography-text type="secondary">
搜索 "{{ searchKeyword }}" 的结果:共 {{ totalFolders }} 个文件夹
</a-typography-text>
</div>
<a-empty v-if="!loading && folderList.length === 0" :description="searchKeyword ? '未找到匹配的文件夹' : '暂无文件夹'" />
<!-- 树形文件夹结构 -->
<div v-if="!loading && folderList.length > 0" class="folder-tree-container">
<a-tree
:data="folderTreeData"
:selected-keys="currentFolderId ? [currentFolderId] : []"
:field-names="{ key: 'key', title: 'title', children: 'children' }"
:show-line="!sidebarCollapsed"
:block-node="true"
:default-expand-all="true"
@select="handleFolderSelect"
@dblclick="handleFolderDoubleClick"
class="folder-tree"
:class="{ 'collapsed': sidebarCollapsed }"
/>
<!-- 文件夹操作按钮 -->
<div v-if="currentFolderId && currentFolderId !== '0'" class="folder-actions-bar" style="padding: 8px; border-top: 1px solid #e5e6eb; margin-top: 8px;">
<a-space>
<a-button
type="text"
size="small"
@click="handleRenameCurrentFolder"
tooltip="重命名"
>
<template #icon><icon-edit /></template>
重命名
</a-button>
<a-button
type="text"
size="small"
@click="handleDeleteCurrentFolder"
tooltip="删除"
status="danger"
>
<template #icon><icon-delete /></template>
删除
</a-button>
</a-space>
</div>
</div>
</div>
</a-layout-sider>
<a-layout>
<a-layout-header class="file-header">
<div class="breadcrumbs">
<a-breadcrumb>
<a-breadcrumb-item
v-for="(item, index) in breadcrumbPath"
:key="index"
:class="{ 'clickable': index < breadcrumbPath.length - 1 }"
@click="handleBreadcrumbClick(index)"
>
{{ item }}
</a-breadcrumb-item>
</a-breadcrumb>
<a-button
type="text"
shape="circle"
@click="refreshData"
:loading="refreshing"
tooltip="刷新数据"
>
<template #icon>
<icon-refresh :spin="refreshing" />
</template>
</a-button>
</div>
<a-space>
<a-button type="outline" @click="handleUploadFile">
<template #icon><icon-upload /></template>
上传文件
</a-button>
<a-button type="primary" @click="handleCreateFolder">
<template #icon><icon-plus /></template>
新建文件夹
</a-button>
</a-space>
</a-layout-header>
<a-layout-content class="file-content">
<a-card :bordered="false" class="file-card">
<!-- 文件列表标题和搜索框在同一行 -->
<div v-if="currentFolderId" class="file-header-container">
<div class="file-title">
<span class="file-list-title">文件列表 ({{ fileList.length }})</span>
</div>
<div class="file-search-container">
<a-input-search
v-model="fileSearchKeyword"
placeholder="搜索文件名..."
class="file-search-input"
@search="handleFileSearch"
@input="handleFileSearchInput"
@clear="handleFileSearchClear"
allow-clear
/>
</div>
</div>
<a-divider size="small" v-if="currentFolderId" />
<template v-if="!currentFolderId">
<div class="initial-state">
<icon-folder-add class="initial-icon" />
<div class="initial-text">请从左侧选择一个文件夹</div>
</div>
</template>
<!-- 文件列表加载状态 -->
<a-skeleton
:loading="loading && currentFolderId"
:rows="8"
v-if="loading && currentFolderId"
animation="pulse"
>
<template #skeleton>
<a-row class="table-data-row" v-for="i in 8" :key="i">
<!-- 文件名列 -->
<a-col :span="10" class="table-column name-column">
<div class="file-main">
<div class="w-8 h-8 rounded bg-gray-200 mr-3"></div>
<div class="file-name-wrap">
<div class="h-5 bg-gray-200 rounded w-1/2 mb-1"></div>
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
</div>
</div>
</a-col>
<!-- 类型列 -->
<a-col :span="4" class="table-column type-column">
<div class="h-4 bg-gray-200 rounded w-1/3"></div>
</a-col>
<!-- 大小列 -->
<a-col :span="3" class="table-column size-column">
<div class="h-4 bg-gray-200 rounded w-1/2"></div>
</a-col>
<!-- 时间列 -->
<a-col :span="5" class="table-column time-column">
<div class="h-4 bg-gray-200 rounded w-2/3"></div>
</a-col>
<!-- 操作列 -->
<a-col :span="2" class="table-column action-column">
<div class="flex gap-2">
<div class="w-6 h-6 rounded bg-gray-200"></div>
<div class="w-6 h-6 rounded bg-gray-200"></div>
<div class="w-6 h-6 rounded bg-gray-200"></div>
<div class="w-6 h-6 rounded bg-gray-200"></div>
</div>
</a-col>
</a-row>
</template>
</a-skeleton>
<!-- 文件表格 -->
<div class="file-grid-container" v-if="currentFolderId && !loading">
<!-- 表头行 -->
<a-row class="table-header-row">
<a-col :span="10" class="table-column name-column">
<div class="sortable-header" @click="handleSortChange('fileName')">
<span>文件名</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_name' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_name' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="4" class="table-column type-column">
<div class="sortable-header" @click="handleSortChange('fileType')">
<span>类型</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_type' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_type' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="3" class="table-column size-column">
<div class="sortable-header" @click="handleSortChange('fileSize')">
<span>大小</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'file_size' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'file_size' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="5" class="table-column time-column">
<div class="sortable-header" @click="handleSortChange('uploadTime')">
<span>修改时间</span>
<div class="sort-indicator">
<div class="sort-arrow up" :class="{ active: sortField === 'upload_time' && sortOrder === 'asc' }"></div>
<div class="sort-arrow down" :class="{ active: sortField === 'upload_time' && sortOrder === 'desc' }"></div>
</div>
</div>
</a-col>
<a-col :span="2" class="table-column action-column">操作</a-col>
</a-row>
<!-- 数据行 -->
<a-row
v-for="file in fileList"
:key="file.fileId"
class="table-data-row"
>
<!-- 文件名列 -->
<a-col :span="10" class="table-column name-column">
<div class="file-main">
<icon-file :style="{ color: fileColor(getFileExtension(file.fileName || file.name)) }" class="file-icon-large" />
<div class="file-name-wrap">
<a-typography-title :heading="6" class="file-name">{{ file.fileName || file.name }}</a-typography-title>
<div class="file-name-small">{{ file.fileName || file.name }}</div>
</div>
</div>
</a-col>
<!-- 类型列 -->
<a-col :span="4" class="table-column type-column">
<div class="cell-content">{{ fileTypeText(getFileExtension(file.fileName || file.name)) }}</div>
</a-col>
<!-- 大小列 -->
<a-col :span="3" class="table-column size-column">
<div class="cell-content">{{ formatFileListSize(file.fileSize || file.size) }}</div>
</a-col>
<!-- 时间列 -->
<a-col :span="5" class="table-column time-column">
<div class="cell-content">{{ formatUploadTime(file.uploadTime || file.uploadTime) }}</div>
</a-col>
<!-- 操作列 -->
<a-col :span="2" class="table-column action-column">
<div class="file-actions">
<a-button
type="text"
shape="circle"
size="small"
tooltip="预览"
@click="handlePreview(file)"
>
<icon-eye />
</a-button>
<a-button
type="text"
shape="circle"
size="small"
tooltip="下载"
@click="handleDownload(file)"
>
<icon-download />
</a-button>
<a-button
type="text"
shape="circle"
size="small"
tooltip="重命名"
@click="handleEditFile(file)"
>
<icon-edit />
</a-button>
<a-button
type="text"
shape="circle"
size="small"
tooltip="删除"
@click="handleDelete(file)"
class="action-btn delete-btn"
>
<icon-delete />
</a-button>
</div>
</a-col>
</a-row>
</div>
<!-- 文件分页 -->
<div v-if="currentFolderId && !loading && totalFiles > 0" class="pagination-container">
<a-pagination
:total="totalFiles"
:current="fileCurrentPage"
:page-size="filePageSize"
:show-total="true"
:show-page-size="true"
:page-size-options="[10, 20, 50, 100]"
:show-jumper="true"
:hide-on-single-page="false"
size="default"
@change="handleFilePageChange"
@page-size-change="handleFilePageSizeChange"
/>
</div>
<!-- 空状态 -->
<a-empty
v-if="!loading && currentFolderId && fileList.length === 0"
description="暂无文件"
class="empty-state"
>
<template #image><icon-file /></template>
<template #actions>
<a-button type="primary" @click="handleUploadFile">
<template #icon><icon-upload /></template>
上传文件
</a-button>
</template>
</a-empty>
</a-card>
</a-layout-content>
</a-layout>
<!-- 新建/编辑文件夹对话框 -->
<a-modal
v-model:visible="folderDialogVisible"
:title="folderForm.id ? '编辑文件夹' : '新建文件夹'"
width="520px"
@ok="submitFolderForm"
@cancel="folderDialogVisible = false"
:confirm-loading="folderSubmitting"
>
<a-form
:model="folderForm"
ref="folderFormRef"
layout="vertical"
:validate-trigger="['change', 'blur']"
>
<a-form-item label="文件夹名称" field="name" :rules="folderRules.name">
<a-input v-model="folderForm.name" placeholder="输入文件夹名称" max-length="50" />
</a-form-item>
<a-form-item label="父级目录" field="parentId" :rules="folderRules.parentId">
<a-tree-select
v-model="folderForm.parentId"
placeholder="请选择父级目录"
:data="folderTreeSelectData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
allow-clear
:tree-props="{ showLine: true }"
>
<template #title="{ node }">
<span>{{ node?.title || node?.name }}</span>
</template>
</a-tree-select>
</a-form-item>
</a-form>
</a-modal>
<!-- 上传文件对话框 -->
<a-modal
v-model:visible="uploadDialogVisible"
title="上传文件"
width="620px"
:mask-closable="false"
@ok="handleUploadSubmit"
@cancel="resetUpload"
:confirm-loading="uploading"
:ok-disabled="!canUpload"
>
<a-form :model="uploadForm" ref="uploadFormRef" layout="vertical">
<!-- 选择文件 -->
<a-form-item
label="选择文件"
:validate-status="!hasFiles ? 'error' : ''"
:help="!hasFiles ? '请选择需要上传的文件' : ''"
>
<div class="upload-container">
<!-- 上传按钮 -->
<a-upload
ref="uploadRef"
:key="uploadDialogVisible ? 'upload-open' : 'upload-closed'"
:auto-upload="false"
:show-file-list="false"
@change="handleFileChange"
:accept="allowedFileTypes"
multiple
>
<a-button type="primary" class="upload-btn">
<icon-upload />
点击选择文件
</a-button>
</a-upload>
<!-- 文件类型提示 -->
<div class="upload-hint">
支持 {{ allowedFileTypesText }} 等格式,单个文件不超过 {{ maxFileSizeText }}
</div>
</div>
<!-- 文件列表 -->
<div class="upload-file-list" v-if="fileListTemp.length > 0">
<div
class="upload-file-item"
v-for="file in fileListTemp"
:key="file.uid"
:class="{ 'file-error': file.error }"
>
<div class="file-info">
<icon-file
:style="{ color: fileColor(getFileExtension(file.name)) }"
class="file-icon"
/>
<div class="file-details">
<div class="file-name">{{ file.name }}</div>
<div class="file-meta">
{{ formatFileSize(file.size) }}
<span v-if="file.error" class="error-text">{{ file.error }}</span>
</div>
</div>
</div>
<!-- 进度条 -->
<div class="file-progress" v-if="file.status === 'uploading'">
<a-progress
:percent="file.percent || 0"
size="small"
:status="file.percent === 100 ? 'success' : 'processing'"
/>
</div>
<!-- 操作按钮 -->
<div class="file-actions">
<a-button
v-if="file.status !== 'uploading'"
type="text"
shape="circle"
size="small"
@click="removeFile(file)"
class="remove-btn"
>
<icon-delete />
</a-button>
<a-button
v-else
type="text"
shape="circle"
size="small"
@click="cancelUpload(file)"
class="cancel-btn"
>
<icon-stop />
</a-button>
</div>
</div>
</div>
</a-form-item>
<!-- 目标文件夹选择 -->
<a-form-item
label="上传至目录"
field="folderId"
:rules="[{ required: true, message: '请选择目标文件夹' }]"
>
<a-select
v-model="uploadForm.folderId"
placeholder="请选择目标文件夹"
allow-clear
>
<a-option value="0">根目录</a-option>
<a-option
v-for="folder in folderList"
:key="folder.id"
:value="folder.id"
>
{{ folder.name }}
</a-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<!-- 重命名文件夹对话框 -->
<a-modal
v-model:visible="renameModalVisible"
:title="renameForm.isRoot ? '重命名根目录' : '重命名文件夹'"
width="520px"
@ok="confirmRename"
@cancel="renameModalVisible = false"
>
<a-form layout="vertical">
<a-form-item label="文件夹名称">
<a-input
v-model="renameForm.newName"
placeholder="请输入新的文件夹名称"
max-length="50"
@keyup.enter="confirmRename"
/>
</a-form-item>
</a-form>
</a-modal>
<!-- 重命名文件对话框 -->
<a-modal
v-model:visible="renameFileModalVisible"
title="重命名文件"
width="520px"
@ok="confirmRenameFile"
@cancel="renameFileModalVisible = false"
>
<a-form layout="vertical">
<a-form-item label="新文件名">
<a-input
v-model="renameFileForm.newName"
placeholder="请输入新的文件名称(不含扩展名)"
max-length="100"
@keyup.enter="confirmRenameFile"
/>
</a-form-item>
</a-form>
</a-modal>
</a-layout>
</template>
<script setup>
// 导入核心依赖
import { ref, reactive, onMounted, computed, watch, nextTick, h } from 'vue';
import {
IconFolder,
IconFile,
IconPlus,
IconUpload,
IconMenuFold,
IconMenuUnfold,
IconEye,
IconDownload,
IconDelete,
IconRefresh,
IconEdit,
IconFolderAdd,
IconStop
} from '@arco-design/web-vue/es/icon';
import { Message, Modal } from '@arco-design/web-vue';
import axios from 'axios';
// 导入API
import {
getFolderListApi,
getFilesApi,
createFolderApi,
updateFolderApi,
deleteFolderApi,
deleteFileApi,
downloadFileApi,
uploadFileApi,
updateFileNameApi,
renameFileApi,
previewFileApi
} from '@/apis/bussiness'
// 状态管理
const folderList = ref([]);
const fileList = ref([]);
const currentFolderId = ref('');
// 移除currentFolderName现在使用面包屑导航
// const currentFolderName = ref('');
const loading = ref(false);
const folderDialogVisible = ref(false);
const uploadDialogVisible = ref(false);
// 分页
const currentPage = ref(1);
const pageSize = ref(10);
const totalFolders = ref(0);
const fileCurrentPage = ref(1);
const filePageSize = ref(10);
const totalFiles = ref(0);
// 排序状态
const sortField = ref('');
const sortOrder = ref('');
// 排序字段映射(前端显示名 -> 后端字段名)
const sortFieldMap = {
'fileName': 'file_name',
'fileType': 'file_type',
'fileSize': 'file_size',
'uploadTime': 'upload_time'
};
// 表单数据
const folderForm = reactive({
id: '',
name: '',
parentId: '0'
});
const folderRules = {
name: [
{ required: true, message: '请输入文件夹名称' },
{ maxLength: 50, message: '文件夹名称不能超过50个字符' }
],
parentId: [
{ required: true, message: '请选择父级目录' }
]
};
// 上传相关状态
const uploadForm = reactive({
folderId: ''
});
const fileListTemp = ref([]);
const folderFormRef = ref(null);
const uploadFormRef = ref(null);
const uploadRef = ref(null);
const folderColor = 'var(--color-primary)';
const refreshing = ref(false);
const folderSubmitting = ref(false);
const uploading = ref(false);
const allowedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt,.jpg,.jpeg,.png,.gif,.bmp,.webp';
const allowedFileTypesText = 'PDF, Word, Excel, PPT, 压缩文件, 文本文件, 图片文件';
const maxFileSize = 1000 * 1024 * 1024; // 1000MB
const maxFileSizeText = '1000MB';
const cancelTokens = ref({});
// 计算属性:是否有文件可上传
const hasFiles = computed(() => {
console.log('=== hasFiles计算属性执行 ===');
console.log('原始fileListTemp:', fileListTemp.value);
console.log('fileListTemp长度:', fileListTemp.value.length);
const validFiles = fileListTemp.value.filter(file => {
const isValid = !file.error && file.status !== 'removed' && file.status !== 'canceled';
console.log(`文件 ${file.name}: error=${file.error}, status=${file.status}, isValid=${isValid}`);
return isValid;
});
console.log('过滤后的有效文件:', validFiles);
console.log('有效文件数量:', validFiles.length);
console.log('hasFiles结果:', validFiles.length > 0);
return validFiles.length > 0;
});
// 计算属性:是否可以上传
const canUpload = computed(() => {
return hasFiles.value && !uploading.value && uploadForm.folderId;
});
// 计算属性:将平铺的文件夹数据转换为树形结构
const folderTreeData = computed(() => {
console.log('=== folderTreeData计算属性执行 ===');
console.log('folderList.value:', folderList.value);
console.log('folderList.value.length:', folderList.value?.length);
if (!folderList.value || folderList.value.length === 0) {
console.log('folderList为空返回空数组');
return [];
}
// 创建文件夹映射表
const folderMap = new Map();
const rootFolders = [];
console.log('=== 开始创建文件夹映射 ===');
// 首先创建所有文件夹的映射
folderList.value.forEach((folder, index) => {
console.log(`处理第${index + 1}个文件夹:`, folder);
// 确保文件夹数据完整
if (folder && folder.id && folder.name) {
const node = {
key: folder.id, // Tree组件需要的key字段
title: folder.name, // Tree组件需要的title字段
children: [], // Tree组件需要的children字段
// 保留原始字段用于其他功能
id: folder.id,
name: folder.name,
parentId: folder.parentId
};
folderMap.set(folder.id, node);
console.log(`✅ 成功添加文件夹到映射: ${folder.name} (ID: ${folder.id})`);
} else {
console.warn('❌ 跳过不完整的文件夹数据:', folder);
}
});
console.log('=== 开始构建树形结构 ===');
console.log('文件夹映射表大小:', folderMap.size);
// 构建树形结构
folderList.value.forEach((folder, index) => {
console.log(`构建第${index + 1}个文件夹的树形结构:`, folder);
// 确保文件夹数据完整
if (!folder || !folder.id || !folder.name) {
console.warn('❌ 跳过不完整的文件夹数据:', folder);
return;
}
const node = folderMap.get(folder.id);
if (!node) {
console.warn('❌ 找不到文件夹节点:', folder.id);
return;
}
console.log(`处理文件夹: ${folder.name} (ID: ${folder.id}, ParentID: ${folder.parentId})`);
if (folder.parentId === '0' || folder.parentId === 0) {
// 根文件夹
rootFolders.push(node);
console.log(`✅ 添加为根文件夹: ${folder.name}`);
} else {
// 子文件夹
const parent = folderMap.get(folder.parentId);
if (parent) {
parent.children.push(node);
console.log(`✅ 添加为子文件夹: ${folder.name} -> ${parent.name}`);
} else {
// 如果找不到父文件夹,当作根文件夹处理
console.warn('⚠️ 找不到父文件夹,将文件夹作为根文件夹:', folder.name, folder.parentId);
rootFolders.push(node);
}
}
});
console.log('=== 树形结构构建完成 ===');
console.log('根文件夹数量:', rootFolders.length);
console.log('构建的树形结构:', rootFolders);
// 验证树形结构中的节点数据
rootFolders.forEach((root, index) => {
console.log(`根文件夹${index + 1}:`, {
id: root.id,
name: root.name,
childrenCount: root.children?.length || 0
});
});
return rootFolders;
});
// 计算属性:树形选择器数据(包含根目录选项)
const folderTreeSelectData = computed(() => {
const rootOption = {
key: '0', // Tree组件需要的key字段
title: '根目录', // Tree组件需要的title字段
children: [], // Tree组件需要的children字段
// 保留原始字段用于其他功能
id: '0',
name: '根目录'
};
return [rootOption, ...folderTreeData.value];
});
// 搜索相关
const searchKeyword = ref(''); // 文件夹搜索关键词
const fileSearchKeyword = ref(''); // 文件搜索关键词
const searchTimeout = ref(null);
// 初始化文件夹数据
const initData = async () => {
try {
loading.value = true;
console.log('=== 开始初始化数据 ===');
console.log('搜索关键词:', searchKeyword.value);
console.log('搜索关键词类型:', typeof searchKeyword.value);
console.log('搜索关键词长度:', searchKeyword.value?.length);
console.log('当前页码:', currentPage.value);
console.log('页面大小:', pageSize.value);
const apiParams = {
page: currentPage.value,
pageSize: 1000, // 修改为足够大的值,确保获取所有文件夹
folderName: searchKeyword.value.trim() || undefined
};
console.log('API参数:', apiParams);
const folderRes = await getFolderListApi(apiParams);
console.log('=== API响应详情 ===');
console.log('完整响应:', folderRes);
console.log('响应状态码:', folderRes.code);
console.log('响应数据:', folderRes.data);
console.log('rows数据:', folderRes.data?.rows);
console.log('rows数据类型:', typeof folderRes.data?.rows);
console.log('rows数据长度:', folderRes.data?.rows?.length);
console.log('total数据:', folderRes.data?.total);
// 根据后端返回的数据结构处理
if (folderRes.code === 200 && folderRes.data) {
console.log('=== 开始处理数据 ===');
console.log('folderRes.data:', folderRes.data);
console.log('folderRes.data.rows:', folderRes.data.rows);
console.log('folderRes.data.rows类型:', typeof folderRes.data.rows);
console.log('folderRes.data.rows长度:', folderRes.data.rows?.length);
// 检查数据结构
if (!folderRes.data.rows || !Array.isArray(folderRes.data.rows)) {
console.error('API返回的数据结构不正确rows字段不存在或不是数组');
console.log('可用的字段:', Object.keys(folderRes.data));
folderList.value = [];
totalFolders.value = 0;
return;
}
const processedFolders = folderRes.data.rows.map((folder, index) => {
console.log(`处理第${index + 1}个原始文件夹数据:`, folder);
console.log(`原始数据字段:`, Object.keys(folder));
console.log(`folderId:`, folder.folderId);
console.log(`folderName:`, folder.folderName);
console.log(`parentId:`, folder.parentId);
// 确保所有必需字段都存在
if (!folder.folderId || !folder.folderName) {
console.warn('❌ 跳过不完整的文件夹数据:', folder);
return null;
}
const processedFolder = {
id: String(folder.folderId),
name: String(folder.folderName),
parentId: String(folder.parentId || 0)
};
console.log(`✅ 处理后的文件夹数据:`, processedFolder);
return processedFolder;
}).filter(Boolean); // 过滤掉null值
folderList.value = processedFolders;
totalFolders.value = Number(folderRes.data.total) || 0;
console.log('=== 处理后的数据 ===');
console.log('处理后的文件夹列表:', folderList.value);
console.log('文件夹列表长度:', folderList.value.length);
console.log('总文件夹数:', totalFolders.value);
console.log('当前folderList.value:', folderList.value);
} else {
folderList.value = [];
totalFolders.value = 0;
console.log('API响应异常清空列表');
console.log('响应码不是200或数据为空');
console.log('folderRes.code:', folderRes.code);
console.log('folderRes.data:', folderRes.data);
}
} catch (error) {
console.error('初始化文件夹数据失败:', error);
console.error('错误详情:', error.response?.data);
Message.error('加载文件夹失败,请重试');
folderList.value = [];
totalFolders.value = 0;
} finally {
loading.value = false;
console.log('=== 初始化完成 ===');
console.log('最终folderList.value:', folderList.value);
console.log('最终loading.value:', loading.value);
}
};
// 分页事件处理
const handlePageChange = (page) => {
currentPage.value = page;
initData();
};
const handlePageSizeChange = (current, size) => {
pageSize.value = size;
currentPage.value = 1;
initData();
};
const handleFolderSearch = () => {
console.log('=== 执行搜索 ===');
console.log('搜索关键词:', searchKeyword.value);
// 重置到第一页并搜索
currentPage.value = 1;
console.log('重置页码为:', currentPage.value);
initData();
};
const handleSearchInput = (value) => {
console.log('=== 搜索输入 ===');
console.log('输入值:', value);
searchKeyword.value = value;
console.log('设置搜索关键词为:', searchKeyword.value);
// 防抖搜索300ms后自动搜索
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
console.log('清除之前的搜索定时器');
}
searchTimeout.value = setTimeout(() => {
console.log('=== 防抖搜索执行 ===');
currentPage.value = 1;
console.log('重置页码为:', currentPage.value);
initData();
}, 300);
};
const handleSearchClear = () => {
console.log('=== 清除搜索 ===');
searchKeyword.value = '';
console.log('清空搜索关键词');
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
console.log('清除搜索定时器');
}
currentPage.value = 1;
console.log('重置页码为:', currentPage.value);
// 清除搜索后立即刷新数据,显示所有文件夹
initData();
};
// 文件搜索相关函数
const handleFileSearch = () => {
console.log('=== 执行文件搜索 ===');
console.log('文件搜索关键词:', fileSearchKeyword.value);
// 重置到第一页并搜索
fileCurrentPage.value = 1;
// 搜索时重置排序状态
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
const handleFileSearchInput = (value) => {
console.log('=== 文件搜索输入 ===');
console.log('输入值:', value);
fileSearchKeyword.value = value;
console.log('设置文件搜索关键词为:', fileSearchKeyword.value);
// 防抖搜索300ms后自动搜索
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
console.log('清除之前的文件搜索定时器');
}
searchTimeout.value = setTimeout(() => {
console.log('=== 防抖文件搜索执行 ===');
fileCurrentPage.value = 1;
// 搜索时重置排序状态
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
}, 300);
};
const handleFileSearchClear = () => {
console.log('=== 清除文件搜索 ===');
fileSearchKeyword.value = '';
console.log('清空文件搜索关键词');
if (searchTimeout.value) {
clearTimeout(searchTimeout.value);
console.log('清除文件搜索定时器');
}
fileCurrentPage.value = 1;
// 清除搜索时重置排序状态
sortField.value = '';
sortOrder.value = '';
console.log('重置文件页码为:', fileCurrentPage.value);
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
const loadFiles = async (folderId) => {
try {
loading.value = true;
const apiParams = {
folderId: folderId,
page: fileCurrentPage.value,
pageSize: filePageSize.value,
fileName: fileSearchKeyword.value || undefined
};
// 添加排序参数
if (sortField.value && sortOrder.value) {
apiParams.sortField = sortField.value;
apiParams.sortOrder = sortOrder.value;
}
const res = await getFilesApi(apiParams);
// 根据后端返回的数据结构处理
if (res.code === 200 && res.data) {
fileList.value = res.data.rows || [];
totalFiles.value = res.data.total || 0;
} else {
fileList.value = [];
totalFiles.value = 0;
}
currentFolderId.value = folderId;
} catch (error) {
console.error('加载文件失败:', error);
Message.error('服务开小差,请稍后再试');
fileList.value = [];
totalFiles.value = 0;
} finally {
loading.value = false;
}
};
// 排序处理函数
const handleSortChange = (field) => {
const backendField = sortFieldMap[field];
if (!backendField) return;
// 切换排序方向
if (sortField.value === backendField) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc';
} else {
// 新字段,默认降序
sortField.value = backendField;
sortOrder.value = 'desc';
}
// 重新加载文件列表
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
// 文件夹点击事件
// const handleFolderClick = (folderId) => {
// const id = String(folderId);
// if (currentFolderId.value !== id) {
// fileCurrentPage.value = 1;
// // 切换文件夹时清空文件搜索关键词
// fileSearchKeyword.value = '';
// }
// currentFolderId.value = id;
// };
// 树形文件夹选择事件
const handleFolderSelect = (selectedKeys, info) => {
if (selectedKeys.length > 0) {
const folderId = selectedKeys[0];
if (currentFolderId.value !== folderId) {
fileCurrentPage.value = 1;
// 切换文件夹时清空文件搜索关键词和排序状态
fileSearchKeyword.value = '';
sortField.value = '';
sortOrder.value = '';
}
currentFolderId.value = folderId;
loadFiles(folderId);
}
};
// 文件夹双击处理
const handleFolderDoubleClick = (info) => {
console.log('文件夹双击:', info);
const { node } = info;
if (!node) return;
// 显示操作选项
Modal.confirm({
title: '文件夹操作',
content: `请选择对文件夹"${node.title}"的操作`,
okText: '重命名',
cancelText: '删除',
onOk: () => handleRenameFolder(node),
onCancel: () => handleDeleteFolder(node)
});
};
// 重命名当前选中的文件夹
const handleRenameCurrentFolder = () => {
if (!currentFolderId.value || currentFolderId.value === '0') {
Message.warning('请先选择一个文件夹');
return;
}
// 从folderList中找到当前选中的文件夹
const currentFolder = folderList.value.find(folder => folder.id === currentFolderId.value);
if (!currentFolder) {
Message.error('找不到当前文件夹信息');
return;
}
// 构造node对象
const node = {
key: currentFolder.id,
title: currentFolder.name,
id: currentFolder.id,
name: currentFolder.name
};
handleRenameFolder(node);
};
// 删除当前选中的文件夹
const handleDeleteCurrentFolder = () => {
if (!currentFolderId.value || currentFolderId.value === '0') {
Message.warning('请先选择一个文件夹');
return;
}
// 从folderList中找到当前选中的文件夹
const currentFolder = folderList.value.find(folder => folder.id === currentFolderId.value);
if (!currentFolder) {
Message.error('找不到当前文件夹信息');
return;
}
// 构造node对象
const node = {
key: currentFolder.id,
title: currentFolder.name,
id: currentFolder.id,
name: currentFolder.name
};
handleDeleteFolder(node);
};
// 计算属性:面包屑导航路径
const breadcrumbPath = computed(() => {
if (!currentFolderId.value || currentFolderId.value === '0') {
return ['知识库', '根目录'];
}
const path = ['知识库'];
let currentId = currentFolderId.value;
// 从当前文件夹向上查找父级路径
while (currentId && currentId !== '0') {
const folder = folderList.value.find(f => f.id === currentId);
if (folder) {
path.unshift(folder.name);
currentId = folder.parentId;
} else {
break;
}
}
return path;
});
// 面包屑点击事件处理
const handleBreadcrumbClick = (index) => {
if (index === 0) {
// 点击"知识库",回到根目录
currentFolderId.value = '0';
// 重置排序状态
sortField.value = '';
sortOrder.value = '';
loadFiles('0');
} else {
// 点击其他路径项需要找到对应的文件夹ID
const targetPath = breadcrumbPath.value.slice(0, index + 1);
const targetFolderName = targetPath[targetPath.length - 1];
// 查找对应的文件夹
const targetFolder = folderList.value.find(folder => folder.name === targetFolderName);
if (targetFolder) {
currentFolderId.value = targetFolder.id;
// 重置排序状态
sortField.value = '';
sortOrder.value = '';
loadFiles(targetFolder.id);
}
}
};
// 重命名对话框状态
const renameModalVisible = ref(false);
const renameForm = reactive({
folderId: '',
currentName: '',
newName: '',
isRoot: false
});
// 重命名文件对话框状态
const renameFileModalVisible = ref(false);
const renameFileForm = reactive({
fileId: '',
newName: '',
fileExtension: ''
});
// 文件夹重命名处理函数
const handleRenameFolder = (folder) => {
console.log('重命名文件夹:', folder);
if (!folder) {
Message.error('文件夹信息不能为空');
return;
}
const folderId = folder.key || folder.id;
const currentName = folder.title || folder.name;
if (!folderId) {
Message.error('文件夹ID不能为空');
return;
}
if (!currentName) {
Message.error('文件夹名称不能为空');
return;
}
if (!currentName) {
console.error('❌ currentName 为空');
console.error('尝试从folder对象获取名称...');
const fallbackName = folder?.title || folder?.name;
console.error('fallbackName:', fallbackName);
if (!fallbackName) {
Message.error('当前文件夹名称不能为空');
return;
} else {
console.log('✅ 使用fallbackName:', fallbackName);
currentName = fallbackName;
}
}
// 先显示一个简单的提示,确认函数被调用
Message.info('重命名功能被触发');
// 设置重命名对话框数据
renameForm.folderId = folderId;
renameForm.currentName = currentName;
renameForm.newName = currentName;
renameForm.isRoot = folderId === '0';
console.log('重命名表单数据已设置:', renameForm);
// 显示重命名对话框
renameModalVisible.value = true;
console.log('重命名对话框已显示');
};
// 确认重命名
const confirmRename = async () => {
const { folderId, newName, currentName, isRoot } = renameForm;
console.log('确认重命名:', { folderId, newName, currentName, isRoot });
if (!newName || newName.trim() === '') {
Message.warning('文件夹名称不能为空');
return;
}
if (newName.trim() === currentName) {
renameModalVisible.value = false;
return;
}
try {
console.log('开始调用重命名API...');
console.log('API参数:', { folderId, newName: newName.trim() });
// 调用重命名API
const result = await updateFolderApi(folderId, newName.trim());
console.log('重命名API响应:', result);
// 检查API响应
if (result && result.code === 200) {
if (isRoot) {
Message.success('根目录重命名成功');
// 移除对currentFolderName的设置现在使用面包屑导航
// currentFolderName.value = newName.trim();
} else {
Message.success('文件夹重命名成功');
// 移除对currentFolderName的设置现在使用面包屑导航
// if (currentFolderId.value === folderId) {
// currentFolderName.value = newName.trim();
// }
}
initData(); // 刷新文件夹列表
renameModalVisible.value = false;
} else {
// API返回错误
const errorMsg = result?.msg || '重命名失败,请检查网络连接';
console.error('重命名API返回错误:', result);
Message.error(errorMsg);
}
} catch (error) {
console.error('重命名失败 - 详细错误信息:', error);
console.error('错误响应数据:', error.response?.data);
console.error('错误状态码:', error.response?.status);
// 显示更详细的错误信息
let errorMessage = '重命名失败';
if (error.response?.data?.msg) {
errorMessage = error.response.data.msg;
} else if (error.message) {
errorMessage = `重命名失败: ${error.message}`;
}
Message.error(errorMessage);
}
};
// 文件分页事件
const handleFilePageChange = (page) => {
fileCurrentPage.value = page;
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
const handleFilePageSizeChange = (current, size) => {
filePageSize.value = size;
fileCurrentPage.value = 1;
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
};
// 刷新数据
const refreshData = async () => {
refreshing.value = true;
try {
// 强制清空搜索关键词,确保显示所有文件夹
searchKeyword.value = '';
currentPage.value = 1;
// 刷新时重置排序状态
sortField.value = '';
sortOrder.value = '';
await initData();
if (currentFolderId.value) {
await loadFiles(currentFolderId.value);
}
Message.success('数据已刷新');
} catch (error) {
Message.error('刷新失败');
} finally {
refreshing.value = false;
}
};
// 新建/编辑文件夹提交
const submitFolderForm = async () => {
folderSubmitting.value = true;
try {
if (folderForm.id) {
await updateFolderApi(folderForm.id, folderForm.name);
Message.success('文件夹重命名成功');
} else {
const result = await createFolderApi({
name: folderForm.name,
parentId: folderForm.parentId
});
Message.success('文件夹创建成功');
// 新建文件夹后,刷新数据并自动选中新建的文件夹
await initData();
// 如果有返回新建文件夹的ID自动选中它
if (result.data && result.data.folderId) {
currentFolderId.value = String(result.data.folderId);
loadFiles(currentFolderId.value);
}
}
folderDialogVisible.value = false;
} catch (error) {
console.error('文件夹操作失败:', error);
Message.error(folderForm.id ? '重命名失败' : '创建失败');
} finally {
folderSubmitting.value = false;
}
};
// 格式化上传时间
const formatUploadTime = (timeStr) => {
if (!timeStr) return '未知时间';
const date = new Date(timeStr);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false
});
};
// 文件变化处理
const handleFileChange = (info) => {
console.log('=== 文件变化事件 ===');
console.log('完整info对象:', info);
// 安全检查:确保 info 存在且是数组
if (!info || !Array.isArray(info)) {
console.log('❌ info 不存在或不是数组,跳过处理');
return;
}
const fileList = info;
console.log('文件列表:', fileList);
console.log('文件列表长度:', fileList.length);
// 检查是否是组件内部状态触发的(可能是之前的状态)
if (fileList.length === 0) {
console.log('⚠️ 文件列表为空,可能是组件内部状态,跳过处理');
return;
}
// 检查是否是对话框刚打开时的触发(可能是之前的状态)
if (!uploadDialogVisible.value) {
console.log('⚠️ 对话框未显示,可能是之前的状态触发,跳过处理');
return;
}
// 获取当前已存在的文件UID列表用于去重
const existingUids = fileListTemp.value.map(f => f.uid);
console.log('已存在的文件UID:', existingUids);
// 获取当前文件夹中已存在的文件名列表,用于检查重复
const currentFolderFiles = fileList.value || [];
const existingFileNames = currentFolderFiles.map(f => f.fileName || f.name);
console.log('当前文件夹中的文件:', existingFileNames);
// 强制重置上传组件状态
if (uploadRef.value) {
try {
uploadRef.value.reset();
console.log('已强制重置上传组件');
} catch (error) {
console.log('重置上传组件时出错:', error);
}
}
// 处理新选择的文件 - 支持多文件选择,同时避免重复
fileList.forEach((file, index) => {
console.log(`处理第${index + 1}个文件:`, file);
console.log('文件名称:', file.name);
console.log('文件大小:', file.size);
console.log('文件UID:', file.uid);
console.log('文件对象结构:', Object.keys(file));
// 检查文件是否已存在(去重)
if (existingUids.includes(file.uid)) {
console.log('⚠️ 文件已存在,跳过:', file.name);
return;
}
// 确保文件对象有正确的属性
const fileObj = {
uid: file.uid,
name: file.name,
size: file.size || file.file?.size || 0,
type: file.type || file.file?.type || '',
status: 'ready',
error: '',
originFileObj: file.file || file // 保存原始File对象
};
console.log('开始验证新文件...');
// 检查文件是否已存在于当前文件夹中
if (existingFileNames.includes(fileObj.name)) {
fileObj.error = '文件已存在于当前文件夹中';
console.log('⚠️ 文件已存在于文件夹中:', fileObj.name);
// 显示友好的提示信息
Message.warning(`文件 "${fileObj.name}" 已存在于当前文件夹中,已跳过`);
return;
}
// 验证文件
const isValid = validateFile(fileObj);
console.log('文件验证结果:', isValid);
if (isValid) {
// 支持多文件:添加到列表
fileListTemp.value.push(fileObj);
console.log('✅ 成功添加文件到列表:', fileObj.name);
} else {
console.log('❌ 文件验证失败:', fileObj.name, '错误:', fileObj.error);
}
});
console.log('=== 当前文件列表状态 ===');
console.log('fileListTemp长度:', fileListTemp.value.length);
console.log('fileListTemp内容:', fileListTemp.value);
console.log('hasFiles计算结果:', hasFiles.value);
};
// 文件验证
const validateFile = (file) => {
console.log('=== 开始验证文件 ===');
console.log('验证文件:', file.name);
console.log('文件大小:', file.size);
// 清除之前的错误
file.error = '';
// 验证文件类型
const ext = getFileExtension(file.name).toLowerCase();
console.log('文件扩展名:', ext);
const allowedExts = allowedFileTypes
.split(',')
.map(type => type.toLowerCase().replace(/^\./, ''));
console.log('允许的扩展名:', allowedExts);
console.log('扩展名是否匹配:', allowedExts.includes(ext));
if (!allowedExts.includes(ext)) {
file.error = `不支持的文件类型,支持: ${allowedFileTypesText}`;
console.log('❌ 文件类型验证失败:', file.error);
return false;
}
// 验证文件大小
console.log('文件大小验证:', file.size, '<=', maxFileSize);
if (file.size > maxFileSize) {
file.error = `文件过大,最大支持 ${maxFileSizeText}`;
console.log('❌ 文件大小验证失败:', file.error);
return false;
}
console.log('✅ 文件验证通过');
return true;
};
// 获取文件扩展名
const getFileExtension = (fileName) => {
const lastDotIndex = fileName.lastIndexOf('.');
return lastDotIndex > 0 ? fileName.slice(lastDotIndex + 1) : '';
};
// 获取文件图标颜色
const fileColor = (extension) => {
const colorMap = {
pdf: '#ff4d4f',
doc: '#1890ff',
docx: '#1890ff',
xls: '#52c41a',
xlsx: '#52c41a',
ppt: '#faad14',
pptx: '#faad14',
zip: '#722ed1',
txt: '#8c8c8c',
// 图片格式颜色
jpg: '#52c41a',
jpeg: '#52c41a',
png: '#1890ff',
gif: '#faad14',
bmp: '#722ed1',
webp: '#13c2c2'
};
return colorMap[extension.toLowerCase()] || 'var(--color-text-3)';
};
// 移除文件
const removeFile = (file) => {
fileListTemp.value = fileListTemp.value.filter(f => f.uid !== file.uid);
// 如果是正在上传的文件,取消请求
if (file.status === 'uploading' && cancelTokens.value[file.uid]) {
cancelTokens.value[file.uid].cancel('上传已取消');
delete cancelTokens.value[file.uid];
}
};
// 取消上传
const cancelUpload = (file) => {
if (cancelTokens.value[file.uid]) {
cancelTokens.value[file.uid].cancel('上传已取消');
file.status = 'canceled';
}
};
// 提交上传
const handleUploadSubmit = async () => {
// 过滤有效文件
const validFiles = fileListTemp.value.filter(file =>
!file.error && file.status !== 'removed' && file.status !== 'canceled'
);
if (validFiles.length === 0) {
Message.warning('请选择有效的文件');
return;
}
// 验证文件夹ID
if (!uploadForm.folderId) {
Message.warning('请选择目标文件夹');
return;
}
uploading.value = true;
let hasError = false;
let hasFileExists = false;
for (const fileItem of validFiles) {
// 获取原始File对象
const realFile = fileItem.originFileObj || fileItem;
if (!realFile) {
hasError = true;
continue;
}
fileItem.status = 'uploading';
fileItem.percent = 0;
// 创建取消令牌
const source = axios.CancelToken.source();
cancelTokens.value[fileItem.uid] = source;
// 调用API
const result = await uploadFileApi(
realFile,
Number(uploadForm.folderId),
(progressEvent) => {
if (progressEvent.lengthComputable) {
fileItem.percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
}
},
source.token
);
// 检查上传结果
if (result.code === 200) {
fileItem.status = 'success';
fileItem.percent = 100;
} else if (result.code === 400 && result.msg && result.msg.includes('已存在')) {
// 文件已存在的情况
fileItem.status = 'error';
fileItem.error = '文件已存在';
hasFileExists = true;
} else {
fileItem.status = 'error';
fileItem.error = result.msg || '上传失败';
hasError = true;
}
}
// 根据结果显示相应的消息
if (hasFileExists && !hasError) {
Message.warning('文件已存在');
} else if (hasError) {
Message.error('上传失败');
} else {
Message.success('上传成功');
// 刷新当前文件夹文件列表
if (currentFolderId.value === uploadForm.folderId) {
loadFiles(currentFolderId.value);
}
}
resetUpload();
};
// 重置上传表单
const resetUpload = () => {
console.log('=== 重置上传表单 ===');
// 取消所有正在进行的上传
Object.values(cancelTokens.value).forEach(source => {
source.cancel('上传已取消');
});
// 重置所有状态
uploadDialogVisible.value = false;
uploadForm.folderId = currentFolderId.value || '';
fileListTemp.value = [];
cancelTokens.value = {};
uploading.value = false;
// 清空上传组件
if (uploadRef.value) {
uploadRef.value.reset();
console.log('已重置上传组件');
}
console.log('上传表单重置完成');
};
// 预览文件
const handlePreview = async (file) => {
try {
console.log('开始预览文件:', file);
Message.loading('正在加载预览...', 0); // 显示加载提示
const blob = await previewFileApi(file.fileId);
Message.clear(); // 清除加载提示
if (!blob) {
Message.error('无法获取文件数据');
return;
}
const url = URL.createObjectURL(blob);
const fileName = file.fileName || file.name;
const ext = getFileExtension(fileName).toLowerCase();
console.log('文件扩展名:', ext);
// 根据文件类型决定预览方式
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(ext)) {
// 图片预览 - 使用更好的图片预览组件
showImagePreview(url, fileName);
} else if (ext === 'pdf') {
// PDF预览在新窗口打开
window.open(url, '_blank');
Message.success('PDF文件已在新窗口打开');
} else if (['txt', 'md', 'json', 'xml', 'csv', 'log'].includes(ext)) {
// 文本文件预览
showTextPreview(blob, fileName);
} else if (['mp4', 'avi', 'mov', 'wmv', 'flv'].includes(ext)) {
// 视频预览
showVideoPreview(url, fileName);
} else if (['mp3', 'wav', 'flac', 'aac'].includes(ext)) {
// 音频预览
showAudioPreview(url, fileName);
} else if (['doc', 'docx', 'xls', 'xlsx', 'ppt', 'pptx'].includes(ext)) {
// Office文档预览提示
Modal.confirm({
title: '文件预览',
content: `${fileName} 是Office文档格式您可以选择`,
okText: '下载查看',
cancelText: '取消',
onOk: () => {
handleDownload(file);
}
});
} else {
// 其他类型询问是否下载
Modal.confirm({
title: '文件预览',
content: `文件类型 ${ext.toUpperCase()} 暂不支持在线预览,是否下载查看?`,
okText: '下载',
cancelText: '取消',
onOk: () => {
handleDownload(file);
}
});
}
// 延迟释放URL确保预览组件有足够时间加载
setTimeout(() => {
URL.revokeObjectURL(url);
}, 10000);
} catch (error) {
Message.clear(); // 清除加载提示
console.error('预览失败:', error);
// 更详细的错误处理
if (error.response?.status === 404) {
Message.error('文件不存在或已被删除');
} else if (error.response?.status === 403) {
Message.error('没有权限访问该文件');
} else if (error.response?.status === 500) {
Message.error('服务器内部错误,请稍后重试');
} else {
Message.error('预览文件失败,请检查网络连接');
}
}
};
// 显示图片预览
const showImagePreview = (url, fileName) => {
Modal.info({
title: '图片预览',
content: h('div', {
style: {
textAlign: 'center',
padding: '20px'
}
}, [
h('img', {
src: url,
style: {
maxWidth: '100%',
maxHeight: '70vh',
objectFit: 'contain'
}
})
]),
width: '80%',
footer: null,
closable: true
});
};
// 显示文本预览
const showTextPreview = async (blob, fileName) => {
try {
const text = await blob.text();
Modal.info({
title: '文本预览',
content: h('div', {
style: {
textAlign: 'center',
padding: '20px'
}
}, [
h('div', {
style: {
maxWidth: '100%',
maxHeight: '70vh',
overflow: 'auto',
backgroundColor: 'var(--color-fill-1)',
border: '1px solid var(--color-border)',
borderRadius: '8px',
padding: '20px',
fontFamily: "'Consolas', 'Monaco', 'Courier New', monospace",
fontSize: '14px',
lineHeight: '1.6',
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
color: 'var(--color-text-1)',
textAlign: 'left'
}
}, text)
]),
width: '80%',
footer: null,
closable: true
});
} catch (error) {
console.error('文本预览失败:', error);
Message.error('无法读取文本内容');
}
};
// 显示视频预览
const showVideoPreview = (url, fileName) => {
const container = h('div', {
class: 'preview-container',
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
boxSizing: 'border-box',
background: '#f8fafc !important'
}
}, [
// 视频容器
h('div', {
style: {
width: '100%',
maxWidth: '1000px',
backgroundColor: '#ffffff',
borderRadius: '8px',
padding: '20px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0',
display: 'flex',
flexDirection: 'column',
alignItems: 'center'
}
}, [
h('video', {
src: url,
controls: true,
style: {
width: '100%',
maxHeight: '70vh',
borderRadius: '8px',
background: '#000',
boxShadow: '0 4px 12px rgba(0,0,0,0.15)',
border: '1px solid #e2e8f0'
}
})
]),
// 文件信息栏
h('div', {
style: {
marginTop: '16px',
padding: '12px 16px',
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0',
width: '100%',
maxWidth: '1000px',
display: 'flex',
alignItems: 'center',
gap: '16px',
fontSize: '14px',
color: '#2c3e50'
}
}, [
h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
h('span', { style: { fontSize: '16px' } }, '🎬'),
fileName
]),
h('span', { style: { color: '#666', fontSize: '13px' } }, '|'),
h('span', { style: { color: '#666', fontSize: '13px' } }, '视频文件'),
h('span', { style: { color: '#666', fontSize: '13px' } }, '|'),
h('span', { style: { color: '#666', fontSize: '13px' } }, new Date().toLocaleString('zh-CN'))
])
]);
Modal.info({
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '16px',
fontWeight: '600',
color: '#2c3e50'
}
}, [
h('span', { style: { fontSize: '18px' } }, '🎬'),
'视频预览'
]),
content: container,
width: '90%',
style: {
maxWidth: '1200px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
border: '1px solid #e2e8f0',
zIndex: 10000
},
mask: true,
maskClosable: true,
maskStyle: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100vw',
height: '100vh',
zIndex: 9999
},
footer: null,
closable: true,
okText: null,
cancelText: null
});
};
// 显示音频预览
const showAudioPreview = (url, fileName) => {
const container = h('div', {
class: 'preview-container',
style: {
width: '100%',
height: '100%',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
padding: '20px',
boxSizing: 'border-box',
background: '#f8fafc !important'
}
}, [
// 音频容器
h('div', {
style: {
width: '100%',
maxWidth: '500px',
backgroundColor: '#ffffff',
borderRadius: '8px',
padding: '32px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0',
textAlign: 'center'
}
}, [
// 音频图标
h('div', {
style: {
fontSize: '48px',
marginBottom: '20px',
color: '#165DFF'
}
}, '<27><>'),
// 音频播放器
h('audio', {
src: url,
controls: true,
style: {
width: '100%',
height: '40px',
borderRadius: '6px',
marginBottom: '16px'
}
}),
// 文件名
h('div', {
style: {
fontSize: '14px',
color: '#2c3e50',
wordBreak: 'break-all',
textAlign: 'center',
padding: '8px 12px',
backgroundColor: '#f8fafc',
borderRadius: '6px',
fontWeight: '500',
border: '1px solid #e2e8f0'
}
}, fileName)
]),
// 文件信息栏
h('div', {
style: {
marginTop: '16px',
padding: '12px 16px',
backgroundColor: '#ffffff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
border: '1px solid #e2e8f0',
width: '100%',
maxWidth: '500px',
display: 'flex',
alignItems: 'center',
gap: '16px',
fontSize: '14px',
color: '#2c3e50'
}
}, [
h('span', { style: { display: 'flex', alignItems: 'center', gap: '6px' } }, [
h('span', { style: { fontSize: '16px' } }, '🎵'),
fileName
]),
h('span', { style: { color: '#666', fontSize: '13px' } }, '|'),
h('span', { style: { color: '#666', fontSize: '13px' } }, '音频文件'),
h('span', { style: { color: '#666', fontSize: '13px' } }, '|'),
h('span', { style: { color: '#666', fontSize: '13px' } }, new Date().toLocaleString('zh-CN'))
])
]);
Modal.info({
title: h('div', {
style: {
display: 'flex',
alignItems: 'center',
gap: '8px',
fontSize: '16px',
fontWeight: '600',
color: '#2c3e50'
}
}, [
h('span', { style: { fontSize: '18px' } }, '🎵'),
'音频预览'
]),
content: container,
width: '70%',
style: {
maxWidth: '800px',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
borderRadius: '8px',
overflow: 'hidden',
boxShadow: '0 8px 24px rgba(0,0,0,0.15)',
border: '1px solid #e2e8f0',
zIndex: 10000
},
mask: true,
maskClosable: true,
maskStyle: {
backgroundColor: 'rgba(0, 0, 0, 0.6)',
position: 'fixed',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100vw',
height: '100vh',
zIndex: 9999
},
footer: null,
closable: true,
okText: null,
cancelText: null
});
};
// 下载文件
const handleDownload = async (file) => {
try {
const blob = await downloadFileApi(file.fileId);
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = file.fileName || file.name;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
Message.success('开始下载');
} catch (error) {
console.error('下载失败:', error);
Message.error('下载文件失败');
}
};
// 重命名文件
const handleEditFile = (file) => {
console.log('=== 重命名文件函数被调用 ===');
console.log('重命名文件 - 文件对象:', file);
console.log('文件对象的所有属性:', Object.keys(file));
console.log('文件属性详情:', {
fileId: file.fileId,
fileName: file.fileName,
name: file.name,
originalName: file.originalName,
displayName: file.displayName,
title: file.title
});
// 尝试多种可能的文件名字段
let fileName = '';
if (file.fileName) {
fileName = file.fileName;
console.log('使用 fileName 字段:', fileName);
} else if (file.name) {
fileName = file.name;
console.log('使用 name 字段:', fileName);
} else if (file.originalName) {
fileName = file.originalName;
console.log('使用 originalName 字段:', fileName);
} else if (file.displayName) {
fileName = file.displayName;
console.log('使用 displayName 字段:', fileName);
} else if (file.title) {
fileName = file.title;
console.log('使用 title 字段:', fileName);
}
console.log('最终获取到的文件名:', fileName);
if (!fileName) {
console.error('无法获取文件名,文件对象:', file);
Message.error('无法获取文件名,请检查文件数据');
return;
}
const fileExtension = getFileExtension(fileName);
const fileNameWithoutExtension = fileExtension ? fileName.substring(0, fileName.lastIndexOf('.')) : fileName;
console.log('处理后的文件名信息:', {
originalName: fileName,
extension: fileExtension,
nameWithoutExtension: fileNameWithoutExtension
});
// 设置重命名表单数据
renameFileForm.fileId = file.fileId;
renameFileForm.newName = fileNameWithoutExtension;
renameFileForm.fileExtension = fileExtension;
console.log('设置的重命名表单数据:', renameFileForm);
// 显示重命名弹窗
renameFileModalVisible.value = true;
};
// 确认重命名文件
const confirmRenameFile = async () => {
if (!renameFileForm.newName.trim()) {
Message.warning('请输入文件名称');
return;
}
// 使用用户输入的文件名,不添加任何扩展名
const newFileName = renameFileForm.newName.trim();
try {
await renameFileApi(renameFileForm.fileId, newFileName);
Message.success('文件重命名成功');
renameFileModalVisible.value = false;
// 刷新当前文件夹的文件列表
if (currentFolderId.value) {
loadFiles(currentFolderId.value);
}
} catch (error) {
console.error('重命名文件失败:', error);
Message.error('重命名失败');
}
};
// 删除文件夹
const handleDeleteFolder = (folder) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除文件夹「${folder.title || folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
onOk: async () => {
try {
const result = await deleteFolderApi(folder.key || folder.id);
if (result.code === 200) {
Message.success('文件夹删除成功');
// 如果删除的是当前选中的文件夹,切换到根目录
if (currentFolderId.value === (folder.key || folder.id)) {
currentFolderId.value = '0';
// 移除对currentFolderName的设置现在使用面包屑导航
// currentFolderName.value = '根目录';
fileList.value = [];
totalFiles.value = 0;
}
// 刷新文件夹列表
initData();
} else {
Message.error(result.msg || '删除失败');
}
} catch (error) {
console.error('删除文件夹失败:', error);
Message.error('删除失败');
}
}
});
};
// 删除文件
const handleDelete = (file) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除 ${file.fileName || file.name} 吗?`,
onOk: async () => {
try {
const result = await deleteFileApi(file.fileId);
if (result.code === 200) {
Message.success('删除成功');
loadFiles(currentFolderId.value);
} else {
Message.error(result.msg || '删除失败');
}
} catch (error) {
console.error('删除失败:', error);
Message.error('删除失败');
}
}
});
};
// 格式化文件大小
const formatFileSize = (fileSize) => {
const size = Number(fileSize);
if (isNaN(size) || size < 0) return '未知';
if (size < 1024) return `${size} B`;
if (size < 1024 * 1024) return `${(size / 1024).toFixed(1)} KB`;
if (size < 1024 * 1024 * 1024) return `${(size / (1024 * 1024)).toFixed(1)} MB`;
return `${(size / (1024 * 1024 * 1024)).toFixed(1)} GB`;
};
// 专门用于文件列表的格式化函数假设后端返回的是KB单位
const formatFileListSize = (fileSize) => {
const size = Number(fileSize);
if (isNaN(size) || size < 0) return '未知';
// 假设后端返回的是KB单位
if (size < 1024) {
return `${size} KB`;
} else if (size < 1024 * 1024) {
return `${(size / 1024).toFixed(1)} MB`;
} else {
return `${(size / (1024 * 1024)).toFixed(1)} GB`;
}
};
const fileTypeText = (type) => {
const types = {
pdf: 'PDF文档',
doc: 'Word文档',
docx: 'Word文档',
xls: 'Excel表格',
xlsx: 'Excel表格',
ppt: 'PPT演示',
pptx: 'PPT演示',
zip: '压缩文件',
txt: '文本文件',
// 图片格式
jpg: 'JPG图片',
jpeg: 'JPEG图片',
png: 'PNG图片',
gif: 'GIF图片',
bmp: 'BMP图片',
webp: 'WebP图片',
unknown: '未知类型'
};
return types[type] || type;
};
// 侧边栏控制
const sidebarCollapsed = ref(false);
// 打开新建文件夹对话框
const handleCreateFolder = () => {
folderForm.id = '';
folderForm.name = '';
folderForm.parentId = currentFolderId.value || '0';
folderDialogVisible.value = true;
};
// 打开上传文件对话框
const handleUploadFile = () => {
// 清空文件列表,避免显示之前上传的文件
fileListTemp.value = [];
// 重置上传组件状态
if (uploadRef.value) {
try {
uploadRef.value.reset();
// 强制清空组件的内部文件列表
if (uploadRef.value.fileList) {
uploadRef.value.fileList = [];
}
if (uploadRef.value.fileListTemp) {
uploadRef.value.fileListTemp = [];
}
} catch (error) {
console.log('重置上传组件时出错:', error);
}
}
uploadForm.folderId = currentFolderId.value || '';
uploadDialogVisible.value = true;
};
// 侧边栏控制函数
const handleSidebarCollapse = (collapsed) => {
sidebarCollapsed.value = collapsed;
};
const handleSidebarExpand = (collapsed) => {
sidebarCollapsed.value = collapsed;
};
// 监听文件夹ID变化自动加载文件
watch(currentFolderId, (newId) => {
if (newId) {
loadFiles(newId);
}
});
// 监听上传对话框显示状态,确保文件列表清空
watch(uploadDialogVisible, (visible) => {
if (visible) {
console.log('=== 上传对话框已显示,确保文件列表清空 ===');
// 立即清空文件列表
fileListTemp.value = [];
console.log('✅ 已清空文件列表');
// 强制重置上传组件
if (uploadRef.value) {
try {
uploadRef.value.reset();
// 强制清空组件的内部文件列表
if (uploadRef.value.fileList) {
uploadRef.value.fileList = [];
}
if (uploadRef.value.fileListTemp) {
uploadRef.value.fileListTemp = [];
}
console.log('✅ 已重置上传组件');
} catch (error) {
console.log('❌ 重置上传组件时出错:', error);
}
}
// 延迟再次清空,确保处理完所有可能的触发
setTimeout(() => {
fileListTemp.value = [];
console.log('✅ 延迟清空文件列表');
}, 100);
}
});
// 初始化加载
onMounted(() => {
initData();
});
</script>
<style scoped>
.knowledge-container {
height: 100vh;
background-color: var(--color-bg-1);
}
/* 侧边栏样式 */
.folder-sidebar {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
border-right: 1px solid var(--color-border);
transition: all 0.3s ease;
overflow: hidden;
position: relative;
background: var(--color-bg-1);
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 20px 16px;
border-bottom: 1px solid var(--color-border);
background: var(--color-bg-1);
position: relative;
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 1px;
background: linear-gradient(90deg, transparent 0%, var(--color-border) 50%, transparent 100%);
}
}
.folder-content {
padding: 16px 0;
flex: 1;
overflow-y: auto;
scrollbar-width: thin;
background: var(--color-bg-1);
display: flex;
flex-direction: column;
min-height: 0;
max-height: calc(100vh - 200px);
}
.folder-content::-webkit-scrollbar {
width: 8px;
}
.folder-content::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: 4px;
}
.folder-content::-webkit-scrollbar-thumb {
background: linear-gradient(180deg, var(--color-primary-light-2) 0%, var(--color-primary) 100%);
border-radius: 4px;
border: 1px solid rgba(255, 255, 255, 0.2);
&:hover {
background: linear-gradient(180deg, var(--color-primary) 0%, var(--color-primary-dark-1) 100%);
}
}
.folder-list {
border: none;
background: transparent;
padding: 0 16px;
}
/* 文件夹列表项样式 */
.folder-list-item {
padding: 12px 16px;
margin-bottom: 6px;
border-radius: 8px;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
border: 1px solid transparent;
&:hover {
background: linear-gradient(135deg, var(--color-fill-2) 0%, var(--color-fill-3) 100%);
border-color: var(--color-primary-light-2);
transform: translateX(2px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
}
&.active {
background: linear-gradient(135deg, var(--color-primary-light-1) 0%, var(--color-primary-light-2) 100%);
color: var(--color-primary);
font-weight: 500;
border-color: var(--color-primary);
box-shadow: 0 2px 12px rgba(var(--color-primary-6), 0.2);
}
}
/* 文件夹主要信息样式 */
.folder-main-info {
display: flex;
align-items: center;
width: 100%;
margin-bottom: 8px;
}
.folder-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-left: 12px;
font-size: 14px;
font-weight: 500;
}
/* 文件夹操作按钮样式 */
.folder-actions-row {
display: flex;
gap: 6px;
width: 100%;
justify-content: flex-end;
opacity: 0;
transition: opacity 0.3s ease;
}
.folder-list-item:hover .folder-actions-row {
opacity: 1;
}
.folder-actions .action-btn {
width: 28px;
height: 28px;
color: var(--color-text-3);
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
color: var(--color-primary);
background: var(--color-fill-3);
transform: scale(1.1);
}
}
/* 删除旧的span样式因为现在使用.folder-name */
/* 顶部导航样式 */
.file-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
background: var(--color-bg-1);
border-bottom: 1px solid var(--color-border);
height: 64px;
}
.breadcrumbs {
display: flex;
align-items: center;
gap: 8px;
}
/* 文件内容区域样式 */
.file-content {
flex: 1;
display: flex;
flex-direction: column;
padding: 24px;
overflow: hidden;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
background: var(--color-bg-1);
min-height: 0;
max-height: calc(100vh - 120px);
position: relative;
}
.file-card {
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
min-height: 300px;
display: flex;
flex-direction: column;
position: relative;
height: 100%;
overflow: hidden;
padding-bottom: 80px; /* 为分页器留出空间 */
}
/* 表格容器 */
.file-grid-container {
flex: 1;
width: 100%;
margin-top: 16px;
border-radius: 8px;
border: 1px solid var(--color-border);
overflow-y: auto;
background-color: var(--color-bg-1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
margin-bottom: 0;
min-height: 300px;
max-height: calc(100vh - 380px); /* 调整高度为分页器留出空间 */
}
/* 表头行样式 */
.table-header-row {
padding: 0 16px;
height: 48px;
line-height: 48px;
background-color: var(--color-fill-1);
border-bottom: 1px solid var(--color-border);
font-size: 13px;
color: var(--color-text-3);
font-weight: 500;
}
/* 数据行样式 */
.table-data-row {
display: flex;
padding: 0 16px;
height: 64px;
align-items: center;
border-bottom: 1px solid var(--color-border);
transition: all 0.25s ease;
cursor: pointer;
background-color: var(--color-bg-1);
&:last-child {
border-bottom: none;
}
&:hover {
background-color: rgba(22, 93, 255, 0.1);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.05);
}
}
/* 通用列样式 */
.table-column {
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
white-space: nowrap;
padding: 0 8px;
}
.cell-content {
display: inline-block;
width: 100%;
text-align: center;
justify-content: center;
align-items: center;
}
.name-column {
padding: 0 14px;
justify-content: flex-start !important;
}
.file-info {
display: flex;
align-items: center;
width: 100%;
}
.file-icon {
font-size: 20px;
margin-right: 12px;
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.folder-icon {
color: var(--color-primary);
background-color: var(--color-primary-light-1);
}
.file-name {
font-size: 14px;
font-weight: 500;
max-width: 220px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
transition: color 0.2s ease;
}
.table-data-row:hover .file-name {
color: var(--color-primary);
}
.type-column, .size-column, .time-column {
color: var(--color-text-3);
font-size: 14px;
justify-content: center;
align-items: center;
padding: 4px;
}
.action-column {
justify-content: center;
}
.file-main {
display: flex;
align-items: center;
width: 100%;
}
.file-icon-large {
font-size: 24px;
margin-right: 12px;
flex-shrink: 0;
}
.file-name-wrap {
flex: 1;
overflow: hidden;
min-width: 0;
display: flex;
justify-content: center;
}
.file-name {
margin: 0;
font-size: 14px;
color: var(--color-text-1);
transition: color 0.2s;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.table-data-row:hover & {
color: var(--color-primary);
}
}
.file-name-small {
font-size: 14px;
color: var(--color-text-4);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
}
/* 操作按钮区域 */
.file-actions {
display: flex;
gap: 4px;
justify-content: center;
}
.action-btn {
width: 24px;
height: 24px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.2s ease;
color: var(--color-text-3);
&:hover {
background: var(--color-fill-3);
color: var(--color-primary);
}
}
.delete-btn {
&:hover {
color: var(--color-danger);
background-color: rgba(255, 77, 77, 0.05);
}
}
/* 响应式调整 */
@media (max-width: 1200px) {
.name-column {
flex: 0 0 35% !important;
max-width: 35% !important;
}
.time-column {
flex: 0 0 20% !important;
max-width: 20% !important;
}
}
@media (max-width: 992px) {
.name-column {
flex: 0 0 45% !important;
max-width: 45% !important;
}
.type-column {
flex: 0 0 20% !important;
max-width: 20% !important;
}
.time-column {
display: none;
}
.action-column {
flex: 0 0 35% !important;
max-width: 35% !important;
}
.file-actions {
justify-content: flex-end;
}
}
@media (max-width: 768px) {
.size-column, .time-column {
display: none;
}
.type-column {
flex: 0 0 25% !important;
max-width: 25% !important;
}
.name-column {
flex: 0 0 45% !important;
max-width: 45% !important;
}
.action-column {
flex: 0 0 30% !important;
max-width: 30% !important;
}
.file-content {
padding: 12px;
}
.file-grid-container {
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
}
@media (max-width: 576px) {
.type-column {
display: none;
}
.name-column {
flex: 0 0 60% !important;
max-width: 60% !important;
}
.action-column {
flex: 0 0 40% !important;
max-width: 40% !important;
}
.file-header {
padding: 0 12px;
flex-wrap: wrap;
}
.breadcrumbs {
margin-bottom: 8px;
width: 100%;
}
.file-card {
min-height: auto;
}
}
/* 浏览器缩放调整 */
@media (max-height: 800px) {
.folder-content {
max-height: calc(100vh - 180px);
}
.file-content {
max-height: calc(100vh - 100px);
}
.file-grid-container {
max-height: calc(100vh - 280px);
}
}
@media (max-height: 600px) {
.folder-content {
max-height: calc(100vh - 160px);
}
.file-content {
max-height: calc(100vh - 80px);
}
.file-grid-container {
max-height: calc(100vh - 260px);
}
}
/* 空状态样式 */
.initial-state, .empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 64px 0;
color: var(--color-text-3);
background-color: var(--color-fill-1);
border-radius: 8px;
text-align: center;
}
.initial-icon {
font-size: 48px;
margin-bottom: 16px;
color: var(--color-text-4);
}
:deep(.empty-state .arco-btn) {
margin-top: 16px;
padding: 8px 16px;
background-color: var(--color-primary);
color: white;
border-radius: 4px;
border: none;
cursor: pointer;
transition: all 0.2s ease;
font-weight: 500;
}
:deep(.empty-state .arco-btn:hover) {
background-color: var(--color-primary-dark-1);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
:deep(.empty-state .arco-btn:active) {
transform: translateY(0);
}
/* 上传区域样式 */
.upload-area {
display: flex;
justify-content: center;
align-items: center;
width: 100%;
min-height: 140px;
border: 1px dashed var(--color-border);
border-radius: 4px;
background-color: var(--color-fill-1);
transition: all 0.2s;
cursor: pointer;
&:hover {
border-color: rgb(var(--primary-6));
background-color: var(--color-primary-light-1);
}
}
.upload-icon {
text-align: center;
}
.upload-text {
margin-top: 8px;
color: var(--color-text-3);
}
.upload-hint {
margin-top: 8px;
font-size: 12px;
color: var(--color-text-4);
}
/* 上传相关样式 */
.upload-file-list {
margin-top: 16px;
border-radius: 4px;
border: 1px solid var(--color-border);
overflow: hidden;
}
.upload-file-item {
display: flex;
align-items: center;
padding: 12px;
border-bottom: 1px solid var(--color-border);
&:last-child {
border-bottom: none;
}
&:hover {
background-color: var(--color-fill-1);
}
}
.file-info {
display: flex;
align-items: center;
flex: 1;
min-width: 0;
}
.file-icon {
font-size: 20px;
margin-right: 12px;
flex-shrink: 0;
}
.file-name {
flex: 1;
min-width: 0;
}
.name-text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 14px;
}
.file-meta {
font-size: 12px;
color: var(--color-text-4);
margin-top: 4px;
}
.file-error {
color: var(--color-danger);
margin-left: 8px;
}
.file-progress {
flex: 1;
margin: 0 16px;
}
.file-actions {
flex-shrink: 0;
}
/* 分页样式 */
.pagination-container {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: var(--color-bg-1);
padding: 16px 24px;
border-top: 1px solid var(--color-border);
display: flex;
justify-content: flex-end;
align-items: center;
z-index: 10;
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.06);
margin-top: 0;
.arco-pagination {
margin: 0;
.arco-pagination-item {
border-radius: 6px;
margin: 0 4px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
&.arco-pagination-item-active {
background: var(--color-primary);
border-color: var(--color-primary);
color: white;
}
}
.arco-pagination-prev,
.arco-pagination-next {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
}
.arco-pagination-size-changer {
margin-left: 16px;
}
.arco-pagination-jumper {
margin-left: 16px;
}
.arco-pagination-total {
color: var(--color-text-2);
font-size: 14px;
}
}
}
/* 动画效果 */
:deep(.arco-icon-refresh.spin) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 预览弹窗动画效果 */
@keyframes float {
0%, 100% { transform: translateY(0px) rotate(0deg); }
25% { transform: translateY(-10px) rotate(1deg); }
50% { transform: translateY(-5px) rotate(-1deg); }
75% { transform: translateY(-15px) rotate(0.5deg); }
}
@keyframes pulse {
0%, 100% { transform: scale(1); opacity: 1; }
50% { transform: scale(1.05); opacity: 0.8; }
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateY(-30px) scale(0.9);
}
to {
opacity: 1;
transform: translateY(0) scale(1);
}
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
/* 预览弹窗样式增强 */
:deep(.arco-modal) {
animation: slideIn 0.2s ease-out;
z-index: 10000 !important;
}
/* 确保遮罩覆盖整个屏幕 */
:deep(.arco-modal-mask) {
animation: fadeIn 0.2s ease;
background-color: rgba(0, 0, 0, 0.6) !important;
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100vw !important;
height: 100vh !important;
z-index: 9999 !important;
margin: 0 !important;
padding: 0 !important;
border: none !important;
outline: none !important;
box-sizing: border-box !important;
}
:deep(.arco-modal-content) {
border-radius: 8px !important;
overflow: hidden;
box-shadow: 0 8px 24px rgba(0,0,0,0.15) !important;
border: 1px solid #e2e8f0 !important;
background: #ffffff !important;
}
:deep(.arco-modal-body) {
background: #f8fafc !important;
padding: 0 !important;
}
/* 强制覆盖所有可能的背景样式 */
:deep(.arco-modal-body > div) {
background: #f8fafc !important;
}
:deep(.arco-modal-body > div > div) {
background: #f8fafc !important;
}
/* 确保预览弹窗内容区域使用正确的背景 */
:deep(.arco-modal-body .preview-container) {
background: #f8fafc !important;
}
/* 强制覆盖所有预览弹窗的背景 */
:deep(.arco-modal-body) {
background: #f8fafc !important;
}
:deep(.arco-modal-body > *) {
background: #f8fafc !important;
}
/* 确保没有渐变背景 */
:deep(.arco-modal-body *[style*="linear-gradient"]) {
background: #f8fafc !important;
}
:deep(.arco-modal-header) {
background: #ffffff;
border-bottom: 1px solid #e2e8f0;
padding: 16px 20px;
}
:deep(.arco-modal-title) {
font-weight: 600;
color: #2c3e50;
font-size: 16px;
}
:deep(.arco-modal-close) {
background: #f8fafc;
border-radius: 50%;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s ease;
border: 1px solid #e2e8f0;
&:hover {
background: var(--color-fill-2);
color: var(--color-primary);
}
}
/* 按钮悬停效果增强 */
:deep(.arco-btn) {
transition: all 0.2s ease;
&:hover {
border-color: var(--color-primary);
color: var(--color-primary);
}
&:active {
transform: translateY(0);
}
}
/* 骨架屏样式 */
.skeleton-item {
height: 36px;
margin: 4px 16px;
border-radius: 4px;
background-color: var(--color-fill-2);
}
.file-skeleton-item {
height: 60px;
margin: 8px 0;
border-radius: 4px;
background-color: var(--color-fill-2);
}
/** 文件夹样式 **/
.folder-icon-wrapper {
margin-right: 12px;
display: flex;
align-items: center;
width: 24px;
height: 24px;
border-radius: 6px;
background: linear-gradient(135deg, var(--color-primary-light-1) 0%, var(--color-primary-light-2) 100%);
justify-content: center;
transition: all 0.3s ease;
}
.folder-icon {
font-size: 14px;
color: var(--color-primary);
transition: all 0.3s ease;
}
.folder-list-item:hover .folder-icon-wrapper {
transform: scale(1.1);
box-shadow: 0 2px 8px rgba(var(--color-primary-6), 0.2);
}
.folder-list-item.active .folder-icon-wrapper {
background: linear-gradient(135deg, var(--color-primary) 0%, var(--color-primary-dark-1) 100%);
box-shadow: 0 2px 8px rgba(var(--color-primary-6), 0.3);
}
.folder-list-item.active .folder-icon {
color: white;
}
/* 文件夹操作按钮样式 */
.folder-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s;
}
.folder-list-item:hover .folder-actions {
opacity: 1;
}
.action-btn {
width: 24px;
height: 24px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
}
.action-btn:hover {
background-color: var(--color-fill-3);
}
/* 确保在折叠状态下不显示操作按钮 */
:deep(.folder-sidebar.collapsed) .folder-actions-row {
display: none;
}
/* 新建文件夹按钮美化 */
.create-folder-btn {
background: linear-gradient(135deg, #1890ff 0%, #40a9ff 100%);
border: none;
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
transition: all 0.3s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(24, 144, 255, 0.4);
}
&:active {
transform: translateY(0);
}
}
/* 搜索输入框美化 */
.search-input {
:deep(.arco-input-wrapper) {
border-radius: 8px;
border: 1px solid var(--color-border);
transition: all 0.3s ease;
background: rgba(var(--color-bg-1-rgb), 0.9);
&:hover {
border-color: var(--color-primary-light-2);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
&:focus-within {
border-color: var(--color-primary);
box-shadow: 0 0 0 3px rgba(var(--color-primary-6), 0.1);
}
}
}
/* 搜索结果提示样式 */
.search-result-tip {
padding: 12px 16px;
margin: 12px 16px;
background: linear-gradient(135deg, var(--color-primary-light-1) 0%, var(--color-primary-light-2) 100%);
border-radius: 8px;
border-left: 4px solid var(--color-primary);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(45deg, transparent 30%, rgba(var(--color-bg-1-rgb), 0.1) 50%, transparent 70%);
animation: shimmer 2s infinite;
}
}
/* 上传文件相关样式 */
.upload-container {
display: flex;
flex-direction: column;
gap: 12px;
}
.upload-btn {
align-self: flex-start;
border-radius: 8px;
font-weight: 500;
transition: all 0.3s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(24, 144, 255, 0.3);
}
}
.upload-hint {
color: var(--color-text-3);
font-size: 12px;
line-height: 1.4;
padding: 8px 12px;
background: var(--color-fill-2);
border-radius: 6px;
border-left: 3px solid var(--color-primary-light-3);
}
.upload-file-list {
margin-top: 16px;
border: 1px solid var(--color-border);
border-radius: 8px;
background: var(--color-bg-1);
max-height: 300px;
overflow-y: auto;
}
.upload-file-item {
display: flex;
align-items: center;
padding: 12px 16px;
border-bottom: 1px solid var(--color-border);
transition: all 0.2s ease;
&:last-child {
border-bottom: none;
}
&:hover {
background: var(--color-fill-1);
}
&.file-error {
background: rgba(255, 77, 79, 0.05);
border-left: 3px solid #ff4d4f;
}
}
.file-info {
display: flex;
align-items: center;
flex: 1;
gap: 12px;
min-width: 0;
}
.file-icon {
font-size: 20px;
flex-shrink: 0;
}
.file-details {
flex: 1;
min-width: 0;
}
.file-name {
font-weight: 500;
color: var(--color-text-1);
margin-bottom: 4px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.file-meta {
font-size: 12px;
color: var(--color-text-3);
display: flex;
align-items: center;
gap: 8px;
}
.error-text {
color: #ff4d4f;
font-weight: 500;
}
.file-progress {
margin: 0 16px;
min-width: 120px;
}
.file-actions {
display: flex;
gap: 4px;
}
.remove-btn {
color: var(--color-text-3);
&:hover {
color: #ff4d4f;
background: rgba(255, 77, 79, 0.1);
}
}
.cancel-btn {
color: var(--color-text-3);
&:hover {
color: #faad14;
background: rgba(250, 173, 20, 0.1);
}
}
@keyframes shimmer {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* 文件头部容器样式 */
.file-header-container {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding: 16px 0;
}
.file-title {
display: flex;
align-items: center;
}
.file-list-title {
font-size: 16px;
font-weight: 600;
color: var(--color-text-1);
margin: 0;
}
/* 文件搜索样式 */
.file-search-container {
display: flex;
align-items: center;
gap: 12px;
}
.file-search-input {
max-width: 300px;
transition: all 0.3s ease;
&:focus-within {
box-shadow: 0 0 0 2px rgba(22, 93, 255, 0.1);
}
}
/* 可排序表头样式 */
.sortable-header {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
padding: 4px 8px;
border-radius: 4px;
transition: all 0.2s ease;
user-select: none;
}
.sortable-header:hover {
background: var(--color-fill-2);
color: var(--color-primary);
}
.sort-indicator {
display: flex;
flex-direction: column;
gap: 1px;
margin-left: 4px;
}
.sort-arrow {
width: 0;
height: 0;
border-left: 3px solid transparent;
border-right: 3px solid transparent;
transition: all 0.2s ease;
}
.sort-arrow.up {
border-bottom: 3px solid var(--color-text-4);
}
.sort-arrow.down {
border-top: 3px solid var(--color-text-4);
}
.sort-arrow.active {
border-bottom-color: var(--color-primary);
border-top-color: var(--color-primary);
}
.sortable-header:hover .sort-arrow.up {
border-bottom-color: var(--color-primary);
}
.sortable-header:hover .sort-arrow.down {
border-top-color: var(--color-primary);
}
/* 树形文件夹结构 */
.folder-tree-container {
padding: 8px;
background: var(--color-bg-1);
border-radius: 6px;
margin: 8px;
overflow: hidden;
}
.tree-node-content {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 4px 0;
}
.folder-name {
flex: 1;
margin-left: 4px;
font-size: 14px;
color: var(--color-text-1);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.folder-actions {
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 0.2s ease;
}
.tree-node-content:hover .folder-actions {
opacity: 1;
}
.folder-actions .action-btn {
width: 20px;
height: 20px;
color: var(--color-text-3);
border-radius: 4px;
transition: all 0.2s ease;
&:hover {
color: var(--color-primary);
background: var(--color-fill-3);
transform: scale(1.1);
}
}
.folder-tree {
:deep(.arco-tree-node-content) {
padding: 4px 8px;
border-radius: 4px;
transition: background-color 0.2s ease;
&:hover {
background-color: var(--color-fill-2);
}
}
:deep(.arco-tree-node-selected .arco-tree-node-content) {
background-color: var(--color-primary-light-1);
color: var(--color-primary);
}
:deep(.arco-tree-node-indent) {
padding-left: 8px;
}
&.collapsed {
:deep(.arco-tree-node-content) {
padding: 8px 4px;
justify-content: center;
}
:deep(.arco-tree-node-title) {
display: none;
}
:deep(.arco-tree-node-switcher) {
display: none;
}
:deep(.arco-tree-node-indent) {
display: none;
}
}
}
/* 面包屑导航样式 */
.breadcrumbs {
.clickable {
cursor: pointer;
color: var(--color-primary);
transition: color 0.2s ease;
&:hover {
color: var(--color-primary-light-1);
text-decoration: underline;
}
}
}
</style>