You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
7.7 KiB
7.7 KiB
RFC 124: 文件夹分类系统前端实现
元数据
- 状态: 已实现
- 创建日期: 2026-01-21
- 实现日期: 2026-01-21
- 关联后端 RFC: 123-folder-category-enum.md
概述
实现前端对文件夹分类系统的支持,使用虚拟根节点方式展示"我的项目"和"协作项目"两个顶层分类,避免为每个用户创建实际的文件夹记录。
背景
后端已实现基于 folder_category 字段的文件夹分类系统(SMALLINT 类型,1=我的项目,2=协作项目)。前端需要:
- 使用虚拟根节点展示两个分类
- 在虚拟根下创建文件夹时正确传递
folder_category参数 - 子文件夹自动继承父文件夹的分类(由后端触发器处理)
设计方案
1. 类型定义 (client/src/types/folder.ts)
// 文件夹分类常量(对应后端 SMALLINT)
export const FolderCategory = {
MY_PROJECTS: 1,
COLLABORATIVE_PROJECTS: 2,
} as const;
export type FolderCategory = typeof FolderCategory[keyof typeof FolderCategory];
// 虚拟根节点定义
export interface VirtualRoot {
id: string;
name: string;
category: FolderCategory;
icon: string;
color: string;
}
export const VIRTUAL_ROOTS: Record<'mine' | 'collab', VirtualRoot> = {
mine: {
id: 'virtual-mine',
name: '我的项目',
category: FolderCategory.MY_PROJECTS,
icon: 'folder',
color: '#3B82F6',
},
collab: {
id: 'virtual-collab',
name: '协作项目',
category: FolderCategory.COLLABORATIVE_PROJECTS,
icon: 'users',
color: '#10B981',
},
};
2. 工具函数
// 判断是否为虚拟根
export const isVirtualRoot = (id: string): boolean => {
return id === 'virtual-mine' || id === 'virtual-collab';
};
// 获取虚拟根对应的分类
export const getVirtualRootCategory = (id: string): FolderCategory | null => {
if (id === 'virtual-mine') return FolderCategory.MY_PROJECTS;
if (id === 'virtual-collab') return FolderCategory.COLLABORATIVE_PROJECTS;
return null;
};
// 根据 ID 获取虚拟根对象
export const getVirtualRootById = (id: string): VirtualRoot | null => {
if (id === 'virtual-mine') return VIRTUAL_ROOTS.mine;
if (id === 'virtual-collab') return VIRTUAL_ROOTS.collab;
return null;
};
3. API 层更新 (client/src/services/api/folders.ts)
// 支持 folderCategory 查询参数
async getAll(params?: {
parentId?: string | null;
folderCategory?: FolderCategory;
page?: number;
pageSize?: number;
}): Promise<FolderListResponse>
4. Hooks 更新 (client/src/hooks/api/useFolders.ts)
// 缓存键包含 folderCategory
export const folderKeys = {
list: (parentId?: string | null, folderCategory?: FolderCategory, page?: number, pageSize?: number) =>
[...folderKeys.lists(), { parentId, folderCategory, page, pageSize }] as const,
};
// useFolders 支持 folderCategory 参数
export function useFolders(params?: {
parentId?: string | null;
folderCategory?: FolderCategory;
page?: number;
pageSize?: number;
})
5. 创建文件夹逻辑 (CreateFolderModal.tsx)
// 判断创建场景
const isCreatingRootFolder = createFolderParentInfo && isVirtualRoot(createFolderParentInfo.id);
const isSubfolder = !!createFolderParentInfo && !isCreatingRootFolder;
// 创建文件夹时的参数处理
if (createFolderParentInfo) {
if (isVirtualRoot(createFolderParentInfo.id)) {
// 在虚拟根下创建:parentFolderId = undefined, 指定 folderCategory
parentFolderId = undefined;
folderCategory = getVirtualRootCategory(createFolderParentInfo.id) || undefined;
} else {
// 在真实文件夹下创建:指定 parentFolderId,不需要 folderCategory
parentFolderId = createFolderParentInfo.id;
}
}
6. 页面导航更新 (ProjectsPage.tsx)
// 使用虚拟根 ID 替代硬编码的 UUID
import { VIRTUAL_ROOTS, isVirtualRoot } from '@/types/folder';
const [activeRootId, setActiveRootId] = useState<string>(VIRTUAL_ROOTS.mine.id);
const [currentFolderId, setCurrentFolderId] = useState<string>(VIRTUAL_ROOTS.mine.id);
// 创建文件夹时传递虚拟根 ID
const handleCreateFolder = () => {
if (isVirtualRoot(currentFolderId)) {
setCreateFolderModalOpen(true, { id: currentFolderId, name: '' });
} else {
setCreateFolderModalOpen(true, { id: currentFolderId, name: currentFolder?.name || '' });
}
};
关键实现细节
虚拟根 ID 转换规则
| 场景 | parentFolderId | folderCategory | 说明 |
|---|---|---|---|
| 在"我的项目"下创建 | undefined |
1 |
创建根文件夹 |
| 在"协作项目"下创建 | undefined |
2 |
创建根文件夹 |
| 在真实文件夹下创建 | 父文件夹 ID | undefined |
创建子文件夹,自动继承分类 |
API 调用示例
// 查询"我的项目"下的根文件夹
folderApi.getAll({
parentId: null,
folderCategory: FolderCategory.MY_PROJECTS
});
// 创建"我的项目"下的根文件夹
folderApi.create({
name: '新文件夹',
parentFolderId: undefined,
folderCategory: FolderCategory.MY_PROJECTS,
});
// 创建子文件夹(自动继承分类)
folderApi.create({
name: '子文件夹',
parentFolderId: 'parent-folder-uuid',
// 不需要传 folderCategory
});
文件变更清单
新增文件
client/src/types/folder.ts- 文件夹类型定义和工具函数
修改文件
client/src/services/api/folders.ts- 添加folderCategory参数支持client/src/hooks/api/useFolders.ts- 更新缓存键和 Hook 参数client/src/pages/ProjectsPage.tsx- 使用虚拟根 IDclient/src/components/features/project/CreateFolderModal.tsx- 虚拟根 ID 转换逻辑client/src/types/project-view.ts- 废弃旧常量,添加TreeNode类型和treeNodeColorClasses
修复问题
- 添加缺失的
TreeNode接口定义 - 添加缺失的
treeNodeColorClasses颜色映射常量 - 清除 Vite 缓存以解决热更新问题
TypeScript 配置兼容性
由于项目使用 erasableSyntaxOnly: true 配置,不能使用 enum,改用常量对象 + 类型别名:
// ❌ 不兼容
export enum FolderCategory {
MY_PROJECTS = 1,
COLLABORATIVE_PROJECTS = 2,
}
// ✅ 兼容
export const FolderCategory = {
MY_PROJECTS: 1,
COLLABORATIVE_PROJECTS: 2,
} as const;
export type FolderCategory = typeof FolderCategory[keyof typeof FolderCategory];
测试要点
-
虚拟根显示
- 页面加载时默认显示"我的项目"
- 可以切换到"协作项目"
-
创建根文件夹
- 在"我的项目"下创建文件夹,
folder_category = 1 - 在"协作项目"下创建文件夹,
folder_category = 2
- 在"我的项目"下创建文件夹,
-
创建子文件夹
- 在根文件夹下创建子文件夹,不传
folder_category - 验证子文件夹自动继承父文件夹的分类
- 在根文件夹下创建子文件夹,不传
-
面包屑导航
- 虚拟根应显示在面包屑顶层
- 点击面包屑可以返回虚拟根视图
-
搜索功能
- 搜索时应限定在当前虚拟根下
- 搜索结果应正确显示所属分类
后续优化
-
Mock 数据更新
- 更新
client/src/mocks/folders.ts,添加folderCategory字段 - 确保 Mock 数据与真实 API 结构一致
- 更新
-
面包屑增强
- 在面包屑中显示虚拟根层级
- 优化面包屑的视觉样式
-
错误处理
- 添加创建文件夹失败的友好提示
- 处理分类不匹配的边界情况