商务模块的文件夹结构优化成树形层级结构

This commit is contained in:
chabai 2025-08-11 10:24:17 +08:00
parent 0401a28037
commit 40ae745dfb
1 changed files with 495 additions and 85 deletions

View File

@ -37,7 +37,7 @@
<div class="folder-content">
<!-- 加载状态 -->
<a-skeleton :loading="loading && folderList.length === 0" :rows="5" v-if="loading" animation="pulse">
<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>
@ -53,51 +53,53 @@
</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-main-info">
<div class="folder-icon-wrapper">
<IconFolder class="folder-icon" :style="{ color: folderColor }" />
</div>
<span v-if="!sidebarCollapsed" class="folder-name">{{ folder.name }}</span>
</div>
<!-- 树形文件夹结构 -->
<div v-if="!loading && folderList.length > 0" class="folder-tree-container">
<!-- 第二行文件夹操作按钮 -->
<div v-if="!sidebarCollapsed" class="folder-actions-row">
<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"
shape="circle"
size="small"
@click.stop="handleRenameFolder(folder, folder.id, folder.name)"
@click="handleRenameCurrentFolder"
tooltip="重命名"
class="action-btn"
>
<icon-edit />
<template #icon><icon-edit /></template>
重命名
</a-button>
<a-button
type="text"
shape="circle"
size="small"
@click.stop="handleDeleteFolder(folder)"
@click="handleDeleteCurrentFolder"
tooltip="删除"
status="danger"
class="action-btn"
>
<icon-delete />
<template #icon><icon-delete /></template>
删除
</a-button>
</div>
</a-list-item>
</a-list>
</a-space>
</div>
</div>
</div>
<!-- 侧边栏底部分页控件 -->
@ -108,7 +110,8 @@
</a-typography-text>
</div>
<div class="pagination-controls">
<!-- 隐藏分页控件因为现在获取所有文件夹 -->
<!-- <div class="pagination-controls">
<a-pagination
:current="currentPage"
:page-size="pageSize"
@ -120,7 +123,7 @@
size="small"
show-total
/>
</div>
</div> -->
</div>
</a-layout-sider>
@ -128,8 +131,14 @@
<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-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"
@ -369,20 +378,18 @@
<a-input v-model="folderForm.name" placeholder="输入文件夹名称" max-length="50" />
</a-form-item>
<a-form-item label="父级目录" field="parentId" :rules="folderRules.parentId">
<a-select
<a-tree-select
v-model="folderForm.parentId"
placeholder="请选择父级目录"
:data="folderTreeSelectData"
:field-names="{ key: 'id', title: 'name', children: 'children' }"
allow-clear
:tree-props="{ showLine: true }"
>
<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>
<template #title="{ node }">
<span>{{ node?.title || node?.name }}</span>
</template>
</a-tree-select>
</a-form-item>
</a-form>
</a-modal>
@ -592,7 +599,8 @@ import {
const folderList = ref([]);
const fileList = ref([]);
const currentFolderId = ref('');
const currentFolderName = ref('');
// currentFolderName使
// const currentFolderName = ref('');
const loading = ref(false);
const folderDialogVisible = ref(false);
const uploadDialogVisible = ref(false);
@ -664,6 +672,111 @@ 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, // Treekey
title: folder.name, // Treetitle
children: [], // Treechildren
//
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', // Treekey
title: '根目录', // Treetitle
children: [], // Treechildren
//
id: '0',
name: '根目录'
};
return [rootOption, ...folderTreeData.value];
});
//
const searchKeyword = ref(''); //
const fileSearchKeyword = ref(''); //
@ -682,7 +795,7 @@ const initData = async () => {
const apiParams = {
page: currentPage.value,
pageSize: pageSize.value,
pageSize: 1000, //
folderName: searchKeyword.value.trim() || undefined
};
@ -701,11 +814,43 @@ const initData = async () => {
//
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)
}));
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;
@ -720,6 +865,8 @@ const initData = async () => {
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);
@ -784,6 +931,7 @@ const handleSearchClear = () => {
}
currentPage.value = 1;
console.log('重置页码为:', currentPage.value);
//
initData();
};
@ -854,13 +1002,6 @@ const loadFiles = async (folderId) => {
}
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('服务开小差,请稍后再试');
@ -873,14 +1014,141 @@ const loadFiles = async (folderId) => {
//
const handleFolderClick = (folderId) => {
const id = String(folderId);
if (currentFolderId.value !== id) {
fileCurrentPage.value = 1;
//
fileSearchKeyword.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 = '';
}
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';
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;
loadFiles(targetFolder.id);
}
}
currentFolderId.value = id;
};
//
@ -901,22 +1169,42 @@ const renameFileForm = reactive({
});
//
const handleRenameFolder = (folder, folderId, currentName) => {
console.log('handleRenameFolder 被调用:', { folder, folderId, currentName });
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) {
console.error('folderId 为空');
Message.error('文件夹ID不能为空');
return;
}
if (!currentName) {
console.error('currentName 为空');
Message.error('当前文件夹名称不能为空');
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('重命名功能被触发');
@ -961,13 +1249,14 @@ const confirmRename = async () => {
if (result && result.code === 200) {
if (isRoot) {
Message.success('根目录重命名成功');
currentFolderName.value = newName.trim();
// currentFolderName使
// currentFolderName.value = newName.trim();
} else {
Message.success('文件夹重命名成功');
//
if (currentFolderId.value === folderId) {
currentFolderName.value = newName.trim();
}
// currentFolderName使
// if (currentFolderId.value === folderId) {
// currentFolderName.value = newName.trim();
// }
}
initData(); //
@ -1015,6 +1304,10 @@ const handleFilePageSizeChange = (current, size) => {
const refreshData = async () => {
refreshing.value = true;
try {
//
searchKeyword.value = '';
currentPage.value = 1;
await initData();
if (currentFolderId.value) {
await loadFiles(currentFolderId.value);
@ -1035,14 +1328,22 @@ const submitFolderForm = async () => {
await updateFolderApi(folderForm.id, folderForm.name);
Message.success('文件夹重命名成功');
} else {
await createFolderApi({
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;
initData();
} catch (error) {
console.error('文件夹操作失败:', error);
Message.error(folderForm.id ? '重命名失败' : '创建失败');
@ -1669,7 +1970,7 @@ const showAudioPreview = (url, fileName) => {
marginBottom: '20px',
color: '#165DFF'
}
}, '🎵'),
}, '<EFBFBD><EFBFBD>'),
//
h('audio', {
@ -1883,16 +2184,17 @@ const confirmRenameFile = async () => {
const handleDeleteFolder = (folder) => {
Modal.confirm({
title: '确认删除',
content: `确定要删除文件夹「${folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
content: `确定要删除文件夹「${folder.title || folder.name}」吗?删除后无法恢复,文件夹内的所有文件也将被删除。`,
onOk: async () => {
try {
const result = await deleteFolderApi(folder.id);
const result = await deleteFolderApi(folder.key || folder.id);
if (result.code === 200) {
Message.success('文件夹删除成功');
//
if (currentFolderId.value === folder.id) {
if (currentFolderId.value === (folder.key || folder.id)) {
currentFolderId.value = '0';
currentFolderName.value = '根目录';
// currentFolderName使
// currentFolderName.value = '';
fileList.value = [];
totalFiles.value = 0;
}
@ -2067,6 +2369,8 @@ onMounted(() => {
background-color: var(--color-bg-2);
}
/* 侧边栏样式 */
.folder-sidebar {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
@ -3208,4 +3512,110 @@ onMounted(() => {
}
}
}
/* 树形文件夹结构 */
.folder-tree-container {
padding: 8px;
background: var(--color-bg-2);
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>