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.
 

12 KiB

项目 ID 匹配问题修复

问题描述

项目面板里的项目 ID 和资源面板顶部的项目选择里的项目 ID 不一致,导致:

  1. 从项目面板选择项目后,左侧搜索框显示空白
  2. 项目面板的选中状态无法正确高亮
  3. 项目选择器无法正确匹配当前选中的项目
  4. 关键问题:素材面板下拉选择项目后,路由变为 /editor/project-1 而不是 /editor/1
  5. 新问题:左边项目面板选择了项目,素材面板顶部的项目选择下拉列表中没有高亮显示当前选中的项目

问题分析

数据源

Mock 数据client/src/mocks/projects.tsclient/src/mocks/folders.ts

  • 项目的 id 字段:数字类型(例如:1, 2, 3
  • 所有组件都应该使用相同的 mock 数据

树形结构构建

文件client/src/utils/treeBuilder.ts

在构建树形节点时(第 32-38 行):

children.push({
  id: `project-${project.id}`,           // ❌ 字符串:"project-1"
  name: `${project.name}.kd`,
  type: 'project',
  projectId: project.id,                 // ✅ 数字:1
  color: 'purple',
});

问题

  • TreeNode.id 使用了字符串前缀格式:"project-1"
  • TreeNode.projectId 保持原始数字:1
  • 这导致不同组件使用不同的字段进行匹配

组件层面的问题

1. ProjectSmartSearchInput(修复前)

文件client/src/components/features/project/ProjectSmartSearchInput.tsx

function findProjectName(nodes: TreeNode[]): string {
  for (const node of nodes) {
    if (node.id === selectedProjectId) {  // ❌ 错误匹配
      return node.name;
    }
    // ...
  }
}

问题

  • selectedProjectId 是路由参数(字符串 "1"
  • node.id"project-1" 格式
  • "1" !== "project-1" → 匹配失败

2. ProjectTreeNode(修复前)

文件client/src/components/features/project/ProjectTreeNode.tsx

const { selectedProjectId } = useUIStore();

const isSelected = isProject && node.projectId &&
  selectedProjectId === node.projectId.toString();  // ✅ 这个是对的

问题

  • 使用了 useUIStoreselectedProjectId
  • 但现在应该使用路由的 projectId

解决方案

修复 1:ProjectSmartSearchInput - 显示项目名称

文件client/src/components/features/project/ProjectSmartSearchInput.tsx

问题 1:选中项目后,搜索框无法显示项目名称

修改内容:使用 node.projectId 而不是 node.id 进行匹配

// 获取选中项目的显示名称
const selectedProjectName = useMemo(() => {
  if (!selectedProjectId) return '';

  // 将 selectedProjectId 转换为数字进行匹配
  const projectIdNum = parseInt(selectedProjectId, 10);
  if (isNaN(projectIdNum)) return '';

  function findProjectName(nodes: TreeNode[]): string {
    for (const node of nodes) {
      // ✅ 使用 projectId 而不是 id 来匹配
      if (node.projectId === projectIdNum) {
        return node.name;
      }
      if (node.children) {
        const childResult = findProjectName(node.children);
        if (childResult) return childResult;
      }
    }
    return '';
  }

  return findProjectName(projectTree);
}, [selectedProjectId, projectTree]);

关键变更

  1. selectedProjectId 转换为数字
  2. 使用 node.projectId 进行匹配(数字类型)
  3. 添加了 isNaN 检查以防止无效输入

问题 2:点击项目时传递的是错误的 ID

原代码(错误):

const handleClick = () => {
  if (isFolder && hasChildren) {
    onToggleExpand(node.id);
  } else if (isProject) {
    onSelect(node.id, node.name);  // ❌ 传递 "project-1"
  }
};

修复后

const handleClick = () => {
  if (isFolder && hasChildren) {
    onToggleExpand(node.id);
  } else if (isProject && node.projectId) {
    // ✅ 传递 projectId 而不是 node.id
    onSelect(node.projectId.toString(), node.name);  // 传递 "1"
  }
};

导致的问题

  • 原代码传递 node.id = "project-1"
  • 路由导航到 /editor/project-1(错误)
  • 应该传递 node.projectId.toString() = "1"
  • 正确路由应该是 /editor/1

修复 2:ProjectSmartSearchInput - 下拉列表选中状态

文件client/src/components/features/project/ProjectSmartSearchInput.tsx

问题:下拉列表中没有高亮显示当前选中的项目

原因ProjectTreeItem 组件没有接收 selectedProjectId 参数,无法判断是否为选中状态

修复步骤

  1. 添加 selectedProjectId 参数到接口
interface ProjectTreeItemProps {
  node: TreeNode;
  level: number;
  searchQuery: string;
  expandedNodes: Set<string>;
  selectedProjectId?: string;  // ✅ 新增
  onToggleExpand: (nodeId: string) => void;
  onSelect: (projectId: string, projectName: string) => void;
  highlightText: (text: string, query: string) => React.ReactNode;
}
  1. 添加选中状态判断逻辑
function ProjectTreeItem({
  node,
  level,
  searchQuery,
  expandedNodes,
  selectedProjectId,  // ✅ 接收参数
  onToggleExpand,
  onSelect,
  highlightText,
}: ProjectTreeItemProps) {
  // ...

  // ✅ 判断是否为选中状态
  const isSelected = isProject && node.projectId && selectedProjectId &&
    selectedProjectId === node.projectId.toString();

  return (
    <div className="w-full">
      <div
        className={cn(
          'flex cursor-pointer items-center rounded-sm px-2 py-1.5 text-sm hover:bg-accent hover:text-accent-foreground',
          isProject && 'cursor-pointer',
          isSelected && 'bg-primary/10 text-primary'  // ✅ 选中状态样式
        )}
        // ...
      >
  1. 传递 selectedProjectId 给子组件
// 顶层渲染
filteredNodes.map((node) => (
  <ProjectTreeItem
    key={node.id}
    node={node}
    level={0}
    searchQuery={value}
    expandedNodes={allExpandedNodes}
    selectedProjectId={selectedProjectId}  //  传递参数
    onToggleExpand={toggleExpand}
    onSelect={handleProjectSelect}
    highlightText={highlightText}
  />
))

// 递归渲染子节点
node.children.map((child) => (
  <ProjectTreeItem
    key={child.id}
    node={child}
    level={level + 1}
    searchQuery={searchQuery}
    expandedNodes={expandedNodes}
    selectedProjectId={selectedProjectId}  //  传递参数
    onToggleExpand={onToggleExpand}
    onSelect={onSelect}
    highlightText={highlightText}
  />
))

效果

  • 当前选中的项目在下拉列表中以浅蓝色背景高亮显示
  • 文字颜色变为主题色
  • 与项目面板的选中状态保持一致

修复 3:ProjectTreeNode

文件client/src/components/features/project/ProjectTreeNode.tsx

修改内容:使用路由的 projectId 而不是 useUIStoreselectedProjectId

export function ProjectTreeNode({
  node,
  level,
  expandedNodes,
  onToggleExpand,
  onCreateProject,
}: ProjectTreeNodeProps) {
  const { setCreateFolderModalOpen, selectProject } = useUIStore();
  const { projectId, setProjectId } = useProjectRoute();  // ✅ 使用路由
  
  // ...

  // ✅ 判断是否为选中状态 - 使用路由的 projectId
  const isSelected = isProject && node.projectId && projectId &&
    projectId === node.projectId.toString();
}

关键变更

  1. 移除 selectedProjectIduseUIStore
  2. 使用 useProjectRoute 获取路由的 projectId
  3. 使用路由的 projectId 进行选中状态判断

ID 类型说明

不同场景下的 ID 格式

场景 ID 格式 类型 示例
Mock 数据 project.id 数字 number 1
路由参数 /:projectId 字符串 string "1"
TreeNode.id 带前缀字符串 string "project-1"
TreeNode.projectId 数字 number 1
API 调用 字符串 string "1"

匹配规则

// ✅ 正确的匹配方式
const routeProjectId = "1";              // 来自路由
const node = { projectId: 1 };           // 来自树节点
const isMatch = routeProjectId === node.projectId.toString();

// ❌ 错误的匹配方式
const routeProjectId = "1";
const node = { id: "project-1" };
const isMatch = routeProjectId === node.id;  // false

数据流验证

正确的数据流

Mock 数据 (project.id: 1)
  ↓
TreeNode (projectId: 1, id: "project-1")
  ↓
用户点击项目
  ↓
selectProject("1", ...)
  ↓
路由导航 /editor/1
  ↓
useParams() → projectId: "1"
  ↓
ProjectTreeNode: projectId === "1" → isSelected = true
  ↓
ProjectSmartSearchInput: projectId(1) === node.projectId(1) → 显示项目名称

测试验证

测试用例

  1. 项目面板选择测试

    • 点击项目后,项目高亮显示
    • 左侧搜索框显示正确的项目名称
    • URL 变化为 /editor/:projectId
  2. 左侧搜索框选择测试

    • 选择项目后,项目面板(如果打开)高亮对应项目
    • 搜索框显示正确的项目名称
    • 素材面板加载对应项目的素材
  3. 路由直接访问测试

    • 直接访问 /editor/1
    • 项目面板高亮项目 1
    • 左侧搜索框显示项目 1 的名称
  4. ID 类型转换测试

    • 字符串 projectId 正确转换为数字
    • 无效的 projectId 不会导致错误

经验教训

1. ID 类型一致性

在整个应用中保持 ID 类型的一致性非常重要:

  • 路由参数总是字符串
  • Mock 数据的 ID 是数字
  • 需要在组件层面进行类型转换

2. 数据结构设计

TreeNode 的设计中:

  • id:用于 React key 和树节点唯一标识(可以使用前缀)
  • projectId:用于业务逻辑匹配(应该保持原始类型)

这样的设计避免了冲突,但需要明确文档说明。

3. 状态来源统一

在路由驱动架构中:

  • 所有组件应该从路由读取 projectId
  • 避免使用多个状态源(如 uiStore.selectedProjectId)
  • 保持单一数据源原则

4. 类型安全

TypeScript 的类型系统可以帮助发现这类问题:

// 明确 ID 的类型
type ProjectId = number;        // 数据库 ID
type RouteProjectId = string;   // 路由参数
type TreeNodeId = string;       // 树节点 ID(带前缀)

interface TreeNode {
  id: TreeNodeId;               // "project-1"
  projectId?: ProjectId;        // 1
}

后续优化建议

1. 统一 ID 处理工具

创建 ID 转换工具:

// utils/idConverter.ts
export const idConverter = {
  toTreeNodeId: (projectId: number) => `project-${projectId}`,
  fromTreeNodeId: (treeNodeId: string) => parseInt(treeNodeId.replace('project-', ''), 10),
  toRouteId: (projectId: number) => projectId.toString(),
  fromRouteId: (routeId: string) => parseInt(routeId, 10),
};

2. 类型定义优化

使用 branded types 确保类型安全:

type ProjectId = number & { __brand: 'ProjectId' };
type RouteProjectId = string & { __brand: 'RouteProjectId' };
type TreeNodeId = string & { __brand: 'TreeNodeId' };

3. 文档完善

在 TreeNode 类型定义中添加注释:

interface TreeNode {
  /** 树节点唯一标识,格式:`project-${projectId}` 或 `folder-${folderId}` */
  id: string;
  
  /** 项目的数据库 ID,用于 API 调用和业务逻辑匹配 */
  projectId?: number;
  
  /** 文件夹的数据库 ID */
  folderId?: number;
}

修复日期:2026-01-14
修复状态 已完成
影响范围:ProjectSmartSearchInput, ProjectTreeNode