# RFC 124: 文件夹分类系统前端实现 ## 元数据 - **状态**: 已实现 - **创建日期**: 2026-01-21 - **实现日期**: 2026-01-21 - **关联后端 RFC**: [123-folder-category-enum.md](../../server/rfcs/123-folder-category-enum.md) ## 概述 实现前端对文件夹分类系统的支持,使用虚拟根节点方式展示"我的项目"和"协作项目"两个顶层分类,避免为每个用户创建实际的文件夹记录。 ## 背景 后端已实现基于 `folder_category` 字段的文件夹分类系统(SMALLINT 类型,1=我的项目,2=协作项目)。前端需要: 1. 使用虚拟根节点展示两个分类 2. 在虚拟根下创建文件夹时正确传递 `folder_category` 参数 3. 子文件夹自动继承父文件夹的分类(由后端触发器处理) ## 设计方案 ### 1. 类型定义 (`client/src/types/folder.ts`) ```typescript // 文件夹分类常量(对应后端 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. 工具函数 ```typescript // 判断是否为虚拟根 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`) ```typescript // 支持 folderCategory 查询参数 async getAll(params?: { parentId?: string | null; folderCategory?: FolderCategory; page?: number; pageSize?: number; }): Promise ``` ### 4. Hooks 更新 (`client/src/hooks/api/useFolders.ts`) ```typescript // 缓存键包含 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`) ```typescript // 判断创建场景 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`) ```typescript // 使用虚拟根 ID 替代硬编码的 UUID import { VIRTUAL_ROOTS, isVirtualRoot } from '@/types/folder'; const [activeRootId, setActiveRootId] = useState(VIRTUAL_ROOTS.mine.id); const [currentFolderId, setCurrentFolderId] = useState(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 调用示例 ```typescript // 查询"我的项目"下的根文件夹 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` - 使用虚拟根 ID - `client/src/components/features/project/CreateFolderModal.tsx` - 虚拟根 ID 转换逻辑 - `client/src/types/project-view.ts` - 废弃旧常量,添加 `TreeNode` 类型和 `treeNodeColorClasses` ### 修复问题 - 添加缺失的 `TreeNode` 接口定义 - 添加缺失的 `treeNodeColorClasses` 颜色映射常量 - 清除 Vite 缓存以解决热更新问题 ## TypeScript 配置兼容性 由于项目使用 `erasableSyntaxOnly: true` 配置,不能使用 `enum`,改用常量对象 + 类型别名: ```typescript // ❌ 不兼容 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]; ``` ## 测试要点 1. **虚拟根显示** - 页面加载时默认显示"我的项目" - 可以切换到"协作项目" 2. **创建根文件夹** - 在"我的项目"下创建文件夹,`folder_category = 1` - 在"协作项目"下创建文件夹,`folder_category = 2` 3. **创建子文件夹** - 在根文件夹下创建子文件夹,不传 `folder_category` - 验证子文件夹自动继承父文件夹的分类 4. **面包屑导航** - 虚拟根应显示在面包屑顶层 - 点击面包屑可以返回虚拟根视图 5. **搜索功能** - 搜索时应限定在当前虚拟根下 - 搜索结果应正确显示所属分类 ## 后续优化 1. **Mock 数据更新** - 更新 `client/src/mocks/folders.ts`,添加 `folderCategory` 字段 - 确保 Mock 数据与真实 API 结构一致 2. **面包屑增强** - 在面包屑中显示虚拟根层级 - 优化面包屑的视觉样式 3. **错误处理** - 添加创建文件夹失败的友好提示 - 处理分类不匹配的边界情况 ## 参考文档 - [后端 RFC 123: 文件夹分类枚举字段](../../server/rfcs/123-folder-category-enum.md) - [后端部署指南](../../../server/DEPLOY_FOLDER_CATEGORY.md) - [文件夹服务设计](../../requirements/backend/04-services/project/folder-service.md)