# 文件夹选择器实现方案 ## 概述 为创建项目弹窗添加文件夹选择功能,允许用户在创建项目时选择目标文件夹。实现了可复用的文件夹树组件,支持多种使用场景。 ## 实现日期 2026-01-14 ## 需求背景 - 用户在创建项目时需要选择保存位置 - 默认保存在"我的项目"文件夹 - 支持选择"我的项目"下的任意子文件夹 - 交互方式参考素材库的智能搜索框(Popover + 树形结构) - 只显示文件夹,不显示项目 ## 技术方案 ### 架构设计 采用组件化设计,将功能拆分为三个层次: 1. **FolderTree(基础层)**:通用的文件夹树组件 2. **FolderSelector(业务层)**:文件夹选择器组件 3. **CreateProjectModal(应用层)**:集成文件夹选择器 ### 组件结构 ``` FolderTree.tsx # 通用文件夹树组件(可复用) ├── FolderTreeNode # 树节点子组件 └── highlightText # 搜索高亮函数 FolderSelector.tsx # 文件夹选择器(业务组件) ├── Popover # 弹出层容器 ├── Button # 触发按钮 └── FolderTree # 文件夹树 CreateProjectModal.tsx # 创建项目弹窗(应用组件) └── FolderSelector # 文件夹选择器 ``` ## 核心功能 ### 1. FolderTree 组件 **文件路径**:`client/src/components/features/project/FolderTree.tsx` **功能特性**: - ✅ 支持树形展示文件夹和项目 - ✅ 可配置是否显示项目节点(`showProjects` 参数) - ✅ 支持搜索关键词高亮(`searchQuery` 参数) - ✅ 支持展开/收起状态管理 - ✅ 支持文件夹和项目的选择回调 - ✅ 支持选中状态高亮显示 - ✅ 完全可复用,适用于多种场景 **Props 接口**: ```typescript interface FolderTreeProps { nodes: TreeNode[]; // 树形数据 showProjects?: boolean; // 是否显示项目节点 searchQuery?: string; // 搜索关键词 expandedNodes: Set; // 展开的节点集合 onToggleExpand: (nodeId: string) => void; onFolderSelect?: ( folderId: number, folderName: string, folderPath: string ) => void; onProjectSelect?: (projectId: string, projectName: string) => void; selectedFolderId?: number; // 当前选中的文件夹 selectedProjectId?: string; // 当前选中的项目 } ``` ### 2. FolderSelector 组件 **文件路径**:`client/src/components/features/project/FolderSelector.tsx` **功能特性**: - ✅ Popover 弹出层交互 - ✅ 支持三种过滤模式: - `mine`:仅显示"我的项目"及其子文件夹 - `collab`:仅显示"协作项目"及其子文件夹 - `all`:显示全部文件夹 - ✅ 只显示文件夹,不显示项目 - ✅ 显示选中文件夹的完整路径 - ✅ 默认展开"我的项目"根文件夹 **Props 接口**: ```typescript interface FolderSelectorProps { value?: number; // 当前选中的文件夹 ID onChange?: (folderId: number, folderName: string, folderPath: string) => void; filterMode?: "all" | "mine" | "collab"; disabled?: boolean; className?: string; placeholder?: string; } ``` ### 3. CreateProjectModal 集成 **修改内容**: 1. **表单 Schema 更新**: ```typescript const createProjectSchema = z.object({ name: z.string().min(1, '请输入项目名称'), folderId: z.number().min(1, '请选择项目文件夹'), // 新增 projectType: z.enum([...]), // ... 其他字段 }); ``` 2. **默认值设置**: ```typescript defaultValues: { name: '', folderId: 1, // 默认选择"我的项目" // ... 其他字段 } ``` 3. **UI 布局**: - 在"项目名称"字段下方添加"保存位置"字段 - 使用 `FolderSelector` 组件 - 设置 `filterMode="mine"` 只显示"我的项目" ## 技术亮点 ### 1. 组件复用性设计 通过参数化配置,`FolderTree` 可以适用于多种场景: ```typescript // 场景 1:文件夹选择器(箭头展开,文字选择) // 场景 2:项目浏览器(文件夹整行展开,项目可选) // 场景 3:通用选择器(箭头展开,文字选择文件夹或项目) // 场景 4:搜索结果(带高亮) ``` ### 2. 精确的交互控制 **箭头和内容区域分离点击**: ```typescript // 箭头点击 - 只负责展开/折叠 const handleArrowClick = (e: React.MouseEvent) => { e.stopPropagation(); // 阻止事件冒泡 if (hasChildren) { onToggleExpand(node.id); } }; // 内容点击 - 根据选择模式决定行为 const handleContentClick = () => { if (selectionMode === "folder-selectable" && isFolder) { // 选择文件夹 onFolderSelect?.(node.folderId, node.name, folderPath); } else if (selectionMode === "project-only") { // 文件夹展开,项目选择 if (isFolder && hasChildren) { onToggleExpand(node.id); } else if (isProject) { onProjectSelect?.(node.id, node.name); } } // ... 其他模式 }; ``` **样式控制**: ```typescript // 箭头区域:有子节点时显示 hover 效果 // 内容区域:可点击时显示 hover 效果
``` ### 2. 数据过滤策略 在 `FolderSelector` 中实现了灵活的过滤逻辑: ```typescript const treeData = useMemo(() => { const fullTree = buildProjectTree(mockFolders, mockProjects); if (filterMode === "mine") { return fullTree.filter((node) => node.folderId === 1); } else if (filterMode === "collab") { return fullTree.filter((node) => node.folderId === 2); } return fullTree; }, [filterMode]); ``` ### 3. 数据过滤策略 - 使用 `useWatch` 监听表单字段变化 - 使用 `Set` 管理展开状态,性能优异 - 通过回调函数实现父子组件通信 ### 4. 状态管理 - 默认展开"我的项目"根文件夹 - 显示选中文件夹的完整路径 - 选择后自动关闭 Popover - 支持键盘导航和点击交互 - 选中状态高亮显示 ## 文件清单 ### 新增文件 1. `client/src/components/features/project/FolderTree.tsx` - 通用文件夹树组件 - 约 180 行代码 2. `client/src/components/features/project/FolderSelector.tsx` - 文件夹选择器组件 - 约 130 行代码 3. `docs/方案/folder-selector-implementation.md` - 实现方案文档 ### 修改文件 1. `client/src/components/features/project/CreateProjectModal.tsx` - 添加 `folderId` 字段到表单 schema - 添加 `FolderSelector` 组件 - 设置默认值为 1(我的项目) ## 使用示例 ### 基础使用 ```tsx import { FolderSelector } from "@/components/features/project/FolderSelector"; function MyComponent() { const [folderId, setFolderId] = useState(1); return ( { console.log("选中文件夹:", { id, name, path }); setFolderId(id); }} filterMode="mine" placeholder="选择项目文件夹" /> ); } ``` ### 在表单中使用 ```tsx import { useForm } from "react-hook-form"; import { FolderSelector } from "@/components/features/project/FolderSelector"; function CreateForm() { const form = useForm({ defaultValues: { folderId: 1, }, }); return ( form.setValue("folderId", id)} filterMode="mine" /> ); } ``` ## 后续扩展 ### 1. 支持协作项目 修改 `filterMode` 为 `'collab'` 即可: ```tsx ``` ### 2. 支持搜索功能 在 `FolderSelector` 中添加搜索输入框: ```tsx const [searchQuery, setSearchQuery] = useState(''); // 在 Popover 中添加搜索框 setSearchQuery(e.target.value)} /> // 传递给 FolderTree ``` ### 3. 重构 SmartSearchInput 可以使用 `FolderTree` 替代 `SmartSearchInput` 中的树形实现: ```tsx import { FolderTree } from "@/components/features/project/FolderTree"; // 在 SmartSearchInput 中 ; ``` ## 测试建议 ### 功能测试 - [ ] 打开创建项目弹窗,验证"保存位置"字段显示 - [ ] 点击文件夹选择器,验证 Popover 弹出 - [ ] 验证只显示"我的项目"及其子文件夹 - [ ] 验证不显示项目节点 - [ ] 点击文件夹,验证选择功能 - [ ] 验证选中后 Popover 关闭 - [ ] 验证选中文件夹路径显示 ### 边界测试 - [ ] 验证默认值为"我的项目"(folderId: 1) - [ ] 验证表单验证(必填项) - [ ] 验证禁用状态 - [ ] 验证空数据状态 ### 交互测试 - [ ] 验证展开/收起功能 - [ ] 验证选中状态高亮 - [ ] 验证 hover 效果 - [ ] 验证键盘导航 ## 总结 本次实现完成了以下目标: 1. ✅ 创建了可复用的 `FolderTree` 组件 2. ✅ 实现了 `FolderSelector` 文件夹选择器 3. ✅ 在 `CreateProjectModal` 中集成文件夹选择功能 4. ✅ 支持多种过滤模式(我的项目/协作项目/全部) 5. ✅ 提供了良好的用户体验和扩展性 组件设计遵循了单一职责原则和开闭原则,具有良好的可维护性和可扩展性。 ## 更新记录 ### 2026-01-14 - 交互优化 **优化内容**:实现箭头和内容区域分离点击 **问题**: - 原实现中点击整行都会触发相应动作 - 无法精确控制展开和选择的交互 **解决方案**: 1. 添加 `selectionMode` 参数,支持三种选择模式 2. 箭头区域独立处理点击事件(`handleArrowClick`) 3. 内容区域独立处理点击事件(`handleContentClick`) 4. 使用 `e.stopPropagation()` 阻止事件冒泡 **选择模式说明**: | 模式 | 箭头点击 | 文件夹内容点击 | 项目内容点击 | 使用场景 | | ------------------- | --------- | -------------- | ------------ | ---------------- | | `folder-selectable` | 展开/折叠 | **选择文件夹** | - | FolderSelector | | `project-only` | 展开/折叠 | 展开/折叠 | **选择项目** | SmartSearchInput | | `both-selectable` | 展开/折叠 | **选择文件夹** | **选择项目** | 通用场景 | **代码示例**: ```typescript // FolderSelector 使用 folder-selectable 模式 // SmartSearchInput 使用 project-only 模式 ``` **交互细节**: 1. **箭头区域**: - 只在有子节点时显示 - 独立的 hover 效果(`hover:bg-accent`) - 点击时阻止事件冒泡 - 只负责展开/折叠 2. **内容区域**(图标 + 文字): - 根据 `selectionMode` 决定是否可点击 - 可点击时显示 hover 效果 - 点击时触发选择或展开逻辑 - 使用 `-mx-1` 和 `px-1` 扩大点击区域 3. **样式优化**: - 箭头和内容区域独立圆角 - 选中状态整行高亮 - hover 效果只在可交互区域显示 **用户体验提升**: - ✅ 交互更精确,不会误触 - ✅ 视觉反馈更清晰 - ✅ 符合用户直觉(箭头=展开,文字=选择) - ✅ 支持多种使用场景