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
6.7 KiB
ResourcePicker 对接后端 API
日期: 2026-02-10
类型: Feature Implementation
影响范围: 前端组件, API Hooks, 类型定义
📝 变更概述
将 ResourcePicker 组件及其父组件从使用 mock 数据切换到对接后端项目元素 API,实现真实的角色/场景/道具数据查询和标签展示。
🎯 动机
问题:
ResourcePicker使用硬编码的 mock 数据(mockCharacterTagsSimple、mockLocationTags、mockPropTagsSimple)- 无法展示实际项目的角色/场景/道具
- 标签数据与后端不同步
解决方案:
- 对接后端
/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
👥 影响用户
- ✅ 用户可以在分镜编辑时看到真实的项目角色/场景/道具
- ✅ 标签数据与资源库保持一致
- ✅ 新增的元素立即可用(无需刷新页面)