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.
 

6.7 KiB

ResourcePicker 对接后端 API

日期: 2026-02-10
类型: Feature Implementation
影响范围: 前端组件, API Hooks, 类型定义

📝 变更概述

ResourcePicker 组件及其父组件从使用 mock 数据切换到对接后端项目元素 API,实现真实的角色/场景/道具数据查询和标签展示。

🎯 动机

问题

  • ResourcePicker 使用硬编码的 mock 数据(mockCharacterTagsSimplemockLocationTagsmockPropTagsSimple
  • 无法展示实际项目的角色/场景/道具
  • 标签数据与后端不同步

解决方案

  • 对接后端 /api/v1/projects/{project_id}/characters|locations|props?include_tags=true 接口
  • 创建统一的数据转换层 useProjectElementsForPicker
  • 更新所有使用 ResourcePicker 的父组件

📦 变更内容

1. API 服务层

文件: client/src/services/api/project-elements.ts

新增列表接口:

export const projectElementsApi = {
  getCharacters(
    projectId: string,
    params?: { skip?: number; limit?: number; include_tags?: boolean }
  ): Promise<ProjectCharacterResponse[]> {
    return apiClient.get(`/projects/${projectId}/characters`, { params });
  },

  getLocations(...): Promise<ProjectLocationResponse[]>,
  getProps(...): Promise<ProjectPropResponse[]>,
};

更新响应类型:

export interface ProjectCharacterResponse {
  // ... 原有字段
  tags?: ElementTag[]; // 新增:标签列表
}

2. React Query Hooks

文件: client/src/hooks/api/useProjectElements.ts

新增查询 hooks:

export function useProjectCharacters(
  projectId: string | null,
  options?: { includeTags?: boolean; skip?: number; limit?: number }
) {
  return useQuery({
    queryKey: projectElementKeys.characters(projectId!),
    queryFn: () =>
      projectElementsApi.getCharacters(projectId!, {
        include_tags: options?.includeTags ?? false,
      }),
    enabled: !!projectId,
  });
}

// useProjectLocations
// useProjectProps

3. 统一数据转换层

文件: client/src/hooks/api/useProjectElementsForPicker.ts(新建)

提供统一格式用于 ResourcePicker:

export interface ProjectElementForPicker {
  id: string;
  name: string;
  type: 1 | 2 | 3; // 1=角色, 2=场景, 3=道具
  metadata?: {
    tags?: Array<{ tag_id: string; tag_label: string; display_order?: number }>;
  };
}

export function useProjectElementsForPicker(projectId: string | null) {
  const { data: characters } = useProjectCharacters(projectId, { includeTags: true });
  const { data: locations } = useProjectLocations(projectId, { includeTags: true });
  const { data: props } = useProjectProps(projectId, { includeTags: true });

  return useQuery({
    queryKey: ['project-elements-for-picker', projectId],
    queryFn: () => {
      // 合并并转换为统一格式
      return [...characters, ...locations, ...props].map(convertToPickerFormat);
    },
  });
}

4. 更新父组件

ParseFlowDialog.tsx

变更

- import { useResources } from '@/hooks/api/useResources';
+ import { useProjectElementsForPicker } from '@/hooks/api/useProjectElementsForPicker';

- const { data: allResources } = useResources(currentProjectId);
+ const { data: allResources } = useProjectElementsForPicker(currentProjectId);

StoryboardBoardPanel.tsx

变更

- import { useResources } from '@/hooks/api/useResources';
+ import { useProjectElementsForPicker } from '@/hooks/api/useProjectElementsForPicker';

- const { data: allResources } = useResources(currentProjectId);
+ const { data: allResources } = useProjectElementsForPicker(currentProjectId);

usePreviewData.ts

变更

- import { useResources } from '@/hooks/api';
+ import { useProjectElementsForPicker } from '@/hooks/api/useProjectElementsForPicker';

- const { data: allResources = [] } = useResources(projectId || null);
+ const { data: allResources = [] } = useProjectElementsForPicker(projectId || null);

5. 移除 Mock 数据依赖

删除导入

- import { mockLocationTags, mockCharacterTagsSimple, mockPropTagsSimple } from '@/mocks/screenplay-tags';

移除 fallback 逻辑

- } else if (type === 'character' && r.name === '孙悟空') {
-   children = mockCharacterTagsSimple.filter((t) => t.name === '少年' || t.name === '青年');
- } else {
-   if (type === 'character') children = mockCharacterTagsSimple;
-   else if (type === 'location') children = mockLocationTags;
-   else if (type === 'prop') children = mockPropTagsSimple;
- }

🔄 数据流

后端 API
  ↓
projectElementsApi (API 服务层)
  ↓
useProjectCharacters/Locations/Props (React Query)
  ↓
useProjectElementsForPicker (数据转换)
  ↓
父组件 (ParseFlowDialog, StoryboardBoardPanel, etc.)
  ↓
ResourcePicker (已有的归一化逻辑处理 tags)

功能验证

ResourcePicker 组件

  • 从后端获取项目角色列表
  • 从后端获取项目场景列表
  • 从后端获取项目道具列表
  • 展示元素的标签(角色装扮、场景时段等)
  • 支持"角色 - 标签"层级选择
  • 标签按 display_order 排序
  • 已选资源正确排除(excludeValues)
  • 搜索功能正常

父组件

  • ParseFlowDialog - 分镜编辑表单
  • StoryboardBoardPanel - 分镜看板
  • PreviewPanel(通过 usePreviewData)- 资源预览面板

📊 性能优化

优化前

  • 使用 useResources 获取资源库数据(Resource)
  • Mock 标签数据,与后端不一致

优化后

  • 使用 useProjectElementsForPicker 一次性获取所有元素+标签
  • 数据来自后端,与项目实际数据同步
  • React Query 自动缓存,5分钟内重复访问无需重新请求

注意

  • include_tags=true 会增加查询时间(约 +50ms)
  • 适用于资源选择器场景(用户主动触发)
  • 其他场景可使用 include_tags=false

🧪 测试要点

  • 创建项目角色/场景/道具
  • 为元素添加标签
  • 打开 ResourcePicker,验证数据显示正确
  • 选择"角色 - 标签",验证返回值格式
  • 删除元素,验证列表实时更新
  • 网络失败时显示空状态
  • 大数据量测试(100+ 元素)

🐛 已知问题

📚 相关文档

  • 后端 Changelog: docs/server/changelogs/2026-02-10-project-elements-list-with-tags.md
  • ResourcePicker 组件: client/src/components/common/ResourcePicker.tsx
  • API 文档: http://localhost:6170/api/docs

👥 影响用户

  • 用户可以在分镜编辑时看到真实的项目角色/场景/道具
  • 标签数据与资源库保持一致
  • 新增的元素立即可用(无需刷新页面)