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

2329 lines
59 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="light"
class="folder-sidebar"
:collapsed="sidebarCollapsed"
collapsible
@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="pulse">
<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 ? '未找到匹配的文件夹' : '暂无文件夹'" />
<a-list v-if="!loading && folderList.length > 0" class="folder-list">
<!-- 文件夹列表 -->
<a-list-item
v-for="folder in folderList"
:key="folder.id"
:class="['folder-list-item', { 'active': currentFolderId === folder.id }]"
@click="handleFolderClick(folder.id)"
:tooltip="sidebarCollapsed ? folder.name : ''"
>
<div class="folder-icon-wrapper">
<IconFolder class="folder-icon" :style="{ color: folderColor }" />
</div>
<span v-if="!sidebarCollapsed">{{ folder.name }}</span>
<!-- 文件夹操作按钮 -->
<div v-if="!sidebarCollapsed" class="folder-actions">
<a-button
type="text"
shape="circle"
size="small"
@click.stop="handleRenameFolder(folder, folder.id, folder.name)"
tooltip="重命名"
class="action-btn"
>
<icon-edit />
</a-button>
<a-button
type="text"
shape="circle"
size="small"
@click.stop="handleDeleteFolder(folder)"
tooltip="删除"
status="danger"
class="action-btn"
>
<icon-delete />
</a-button>
</div>
</a-list-item>
</a-list>
</div>
<!-- 侧边栏底部分页控件 -->
<div class="sidebar-footer" v-if="!sidebarCollapsed && folderList.length > 0">
<div class="pagination-info">
<a-typography-text type="secondary" size="small">
共 {{ totalFolders }} 个文件夹
</a-typography-text>
</div>
<div class="pagination-controls">
<a-pagination
:current="currentPage"
:page-size="pageSize"
:total="totalFolders"
:show-size-changer="true"
:page-size-options="['10', '20', '50', '100']"
@change="handlePageChange"
@showSizeChange="handlePageSizeChange"
size="small"
show-total
/>
</div>
</div>
</a-layout-sider>
<a-layout>
<a-layout-header class="file-header">
<div class="breadcrumbs">
<a-breadcrumb>
<a-breadcrumb-item>知识库</a-breadcrumb-item>
<a-breadcrumb-item>{{ currentFolderName }}</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">
<a-descriptions :title="`文件列表 (${fileList.length})`" v-if="currentFolderId" />
<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">文件名</a-col>
<a-col :span="4" class="table-column type-column">类型</a-col>
<a-col :span="3" class="table-column size-column">大小</a-col>
<a-col :span="5" class="table-column time-column">修改时间</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">{{ formatFileSize(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>
<!-- 空状态 -->
<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-select
v-model="folderForm.parentId"
placeholder="请选择父级目录"
>
<a-option value="0">根目录</a-option>
<a-option
v-for="folder in folderList"
:key="folder.id"
:value="folder.id"
v-if="!folderForm.id || folder.id !== folderForm.id"
>
{{ folder.name }}
</a-option>
</a-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"
: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-layout>
</template>
<script setup>
// 导入核心依赖
import { ref, reactive, onMounted, computed, watch, nextTick } 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,
previewFileApi
} from '@/apis/bussiness/bussiness.js'
// 状态管理
const folderList = ref([]);
const fileList = ref([]);
const currentFolderId = ref('');
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 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 = '#165DFF';
const refreshing = ref(false);
const folderSubmitting = ref(false);
const uploading = ref(false);
const allowedFileTypes = '.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.zip,.txt';
const allowedFileTypesText = 'PDF, Word, Excel, PPT, 压缩文件, 文本文件';
const maxFileSize = 100 * 1024 * 1024; // 100MB
const maxFileSizeText = '100MB';
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 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: pageSize.value,
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) {
const processedFolders = folderRes.data.rows.map(folder => ({
id: String(folder.folderId),
name: folder.folderName,
parentId: String(folder.parentId || 0)
}));
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或数据为空');
}
} 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 searchKeyword = ref('');
const searchTimeout = ref(null);
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 loadFiles = async (folderId) => {
try {
loading.value = true;
const res = await getFilesApi({
folderId: folderId,
page: fileCurrentPage.value,
pageSize: filePageSize.value
});
// 根据后端返回的数据结构处理
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;
if (folderId === '0') {
currentFolderName.value = '根目录';
} else {
const folder = folderList.value.find(f => f.id === folderId);
currentFolderName.value = folder ? folder.name : '未知文件夹';
}
} catch (error) {
console.error('加载文件失败:', error);
Message.error('服务开小差,请稍后再试');
fileList.value = [];
totalFiles.value = 0;
} finally {
loading.value = false;
}
};
// 文件夹点击事件
const handleFolderClick = (folderId) => {
const id = String(folderId);
if (currentFolderId.value !== id) {
fileCurrentPage.value = 1;
}
currentFolderId.value = id;
};
// 重命名对话框状态
const renameModalVisible = ref(false);
const renameForm = reactive({
folderId: '',
currentName: '',
newName: '',
isRoot: false
});
// 文件夹重命名处理函数
const handleRenameFolder = (folder, folderId, currentName) => {
console.log('handleRenameFolder 被调用:', { folder, folderId, currentName });
// 验证参数
if (!folderId) {
console.error('folderId 为空');
Message.error('文件夹ID不能为空');
return;
}
if (!currentName) {
console.error('currentName 为空');
Message.error('当前文件夹名称不能为空');
return;
}
// 先显示一个简单的提示,确认函数被调用
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.value = newName.trim();
} else {
Message.success('文件夹重命名成功');
// 如果重命名的是当前选中的文件夹,更新显示名称
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 {
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 {
await createFolderApi({
name: folderForm.name,
parentId: folderForm.parentId
});
Message.success('文件夹创建成功');
}
folderDialogVisible.value = false;
initData();
} 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);
// 处理新选择的文件
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));
// 确保文件对象有正确的属性
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对象
};
// 只处理新选择的文件,避免重复添加
if (!fileListTemp.value.find(f => f.uid === fileObj.uid)) {
console.log('这是新文件,开始验证...');
// 验证文件
const isValid = validateFile(fileObj);
console.log('文件验证结果:', isValid);
if (isValid) {
fileListTemp.value.push(fileObj);
console.log('✅ 成功添加文件到列表:', fileObj.name);
} else {
console.log('❌ 文件验证失败:', fileObj.name, '错误:', fileObj.error);
}
} else {
console.log('文件已存在,跳过:', fileObj.name);
}
});
// 清理已移除的文件
const beforeCleanCount = fileListTemp.value.length;
fileListTemp.value = fileListTemp.value.filter(file =>
fileList.some(f => f.uid === file.uid)
);
const afterCleanCount = fileListTemp.value.length;
if (beforeCleanCount !== afterCleanCount) {
console.log(`清理了 ${beforeCleanCount - afterCleanCount} 个已移除的文件`);
}
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'
};
return colorMap[extension.toLowerCase()] || '#8c8c8c';
};
// 移除文件
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'
);
console.log('提交上传 - 所有文件:', fileListTemp.value);
console.log('提交上传 - 有效文件:', validFiles);
if (validFiles.length === 0) {
Message.warning('请选择有效的文件');
return;
}
// 验证文件夹ID
if (!uploadForm.folderId) {
Message.warning('请选择目标文件夹');
return;
}
uploading.value = true;
let successCount = 0;
let totalFiles = validFiles.length;
try {
for (const fileItem of validFiles) {
// 获取原始File对象
const realFile = fileItem.originFileObj || fileItem;
if (!realFile) {
fileItem.error = '文件数据无效';
continue;
}
fileItem.status = 'uploading';
fileItem.percent = 0;
// 创建取消令牌
const source = axios.CancelToken.source();
cancelTokens.value[fileItem.uid] = source;
try {
// 调用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 === 0 && result.success) {
fileItem.status = 'success';
fileItem.percent = 100;
successCount++;
} else {
fileItem.status = 'error';
fileItem.error = result.msg || '上传失败';
}
} catch (error) {
if (!axios.isCancel(error)) {
fileItem.status = 'error';
fileItem.error = error.message || '上传失败';
}
}
}
// 显示结果
if (successCount === totalFiles) {
Message.success('所有文件上传成功');
resetUpload();
// 刷新当前文件夹文件列表
if (currentFolderId.value === uploadForm.folderId) {
loadFiles(currentFolderId.value);
}
} else if (successCount >= 0) {
Message.warning(`${successCount}/${totalFiles} 个文件上传成功`);
resetUpload();
} else {
Message.error('所有文件上传失败');
}
} catch (error) {
console.error('上传过程出错:', error);
Message.error('上传过程出错');
} finally {
uploading.value = false;
}
};
// 重置上传表单
const resetUpload = () => {
// 取消所有正在进行的上传
Object.values(cancelTokens.value).forEach(source => {
source.cancel('上传已取消');
});
uploadDialogVisible.value = false;
uploadForm.folderId = currentFolderId.value || '';
fileListTemp.value = [];
cancelTokens.value = {};
// 清空上传组件
if (uploadRef.value) {
uploadRef.value.reset();
}
};
// 预览文件
const handlePreview = async (file) => {
try {
const blob = await previewFileApi(file.fileId);
const url = URL.createObjectURL(blob);
// 根据文件类型决定预览方式
const ext = getFileExtension(file.fileName || file.name).toLowerCase();
if (['jpg', 'jpeg', 'png', 'gif', 'bmp'].includes(ext)) {
// 图片预览
const img = new Image();
img.src = url;
Modal.info({
title: '图片预览',
content: img,
width: '80%'
});
} else if (ext === 'pdf') {
// PDF预览在新窗口打开
window.open(url, '_blank');
} else {
// 其他类型提示下载
Message.info(`该文件类型不支持预览,将为您下载文件: ${file.fileName || file.name}`);
handleDownload(file);
}
} catch (error) {
console.error('预览失败:', error);
Message.error('预览文件失败');
}
};
// 下载文件
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) => {
Modal.warning({
title: '功能提示',
content: '后端暂不支持文件重命名功能,如需重命名请先删除文件再重新上传。',
okText: '知道了'
});
};
// 删除文件夹
const handleDeleteFolder = (folder) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除文件夹「${folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
onOk: async () => {
try {
const result = await deleteFolderApi(folder.id);
if (result.code === 200) {
Message.success('文件夹删除成功');
// 如果删除的是当前选中的文件夹,切换到根目录
if (currentFolderId.value === folder.id) {
currentFolderId.value = '0';
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`;
};
const fileTypeText = (type) => {
const types = {
pdf: 'PDF文档',
doc: 'Word文档',
docx: 'Word文档',
xls: 'Excel表格',
xlsx: 'Excel表格',
ppt: 'PPT演示',
pptx: 'PPT演示',
zip: '压缩文件',
txt: '文本文件',
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 = () => {
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);
}
});
// 初始化加载
onMounted(() => {
initData();
});
</script>
<style scoped>
.knowledge-container {
height: 100vh;
background-color: var(--color-bg-2);
}
/* 侧边栏样式 */
.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: linear-gradient(180deg, #ffffff 0%, #fafbfc 100%);
}
.sidebar-header {
padding: 20px 16px;
border-bottom: 1px solid var(--color-border);
background: linear-gradient(135deg, #f8fafc 0%, #e2e8f0 100%);
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;
height: calc(100vh - 280px); /* 为底部分页控件留出空间 */
overflow-y: auto;
scrollbar-width: thin;
background: rgba(255, 255, 255, 0.6);
}
.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;
align-items: center;
border: 1px solid transparent;
&:hover {
background: linear-gradient(135deg, #f1f5f9 0%, #e2e8f0 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-actions {
display: none;
gap: 6px;
margin-left: 12px;
opacity: 0;
transition: opacity 0.3s ease;
}
.folder-list-item:hover .folder-actions {
display: flex;
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);
}
}
.folder-list-item span {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
/* 顶部导航样式 */
.file-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 24px;
background: var(--color-bg-2);
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: auto;
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
background: var(--color-bg-2);
}
.file-card {
border-radius: 8px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
min-height: 300px;
display: flex;
flex-direction: column;
}
/* 表格容器 */
.file-grid-container {
flex: 1;
width: 100%;
margin-top: 16px;
border-radius: 8px;
border: 1px solid var(--color-border);
overflow: auto;
background-color: var(--color-bg-1);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
}
/* 表头行样式 */
.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: #165DFF;
background-color: #E8F3FF;
}
.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: #165DFF;
}
.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: #165DFF;
}
}
.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;
}
}
/* 空状态样式 */
.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: #fafafa;
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: #165DFF;
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: #0E42D2;
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, .file-pagination {
margin-top: 16px;
text-align: right;
padding: 0 16px 16px;
}
.file-pagination {
border-top: 1px solid var(--color-border);
}
/* 侧边栏底部分页样式 */
.sidebar-footer {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(180deg, #f8fafc 0%, #e2e8f0 100%);
border-top: 1px solid var(--color-border);
padding: 20px 16px;
z-index: 10;
backdrop-filter: blur(10px);
box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.05);
}
.pagination-info {
margin-bottom: 16px;
text-align: center;
padding: 8px 12px;
background: rgba(255, 255, 255, 0.8);
border-radius: 8px;
border: 1px solid var(--color-border);
}
.pagination-controls {
display: flex;
justify-content: center;
:deep(.arco-pagination) {
.arco-pagination-item {
border-radius: 6px;
transition: all 0.2s ease;
&:hover {
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
}
}
}
/* 确保文件夹内容区域不被底部分页遮挡 */
.folder-content {
/* 高度已调整无需额外padding */
}
/* 动画效果 */
:deep(.arco-icon-refresh.spin) {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* 骨架屏样式 */
.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 {
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(255, 255, 255, 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, #f0f9ff 0%, #e0f2fe 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(255, 255, 255, 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-2);
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%); }
}
</style>