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

RFC 124: 文件夹分类系统前端实现

元数据

概述

实现前端对文件夹分类系统的支持,使用虚拟根节点方式展示"我的项目"和"协作项目"两个顶层分类,避免为每个用户创建实际的文件夹记录。

背景

后端已实现基于 folder_category 字段的文件夹分类系统(SMALLINT 类型,1=我的项目,2=协作项目)。前端需要:

  1. 使用虚拟根节点展示两个分类
  2. 在虚拟根下创建文件夹时正确传递 folder_category 参数
  3. 子文件夹自动继承父文件夹的分类(由后端触发器处理)

设计方案

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 - 使用虚拟根 ID
  • client/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];

测试要点

  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. 错误处理

    • 添加创建文件夹失败的友好提示
    • 处理分类不匹配的边界情况

参考文档