# 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` 新增列表接口: ```typescript export const projectElementsApi = { getCharacters( projectId: string, params?: { skip?: number; limit?: number; include_tags?: boolean } ): Promise { return apiClient.get(`/projects/${projectId}/characters`, { params }); }, getLocations(...): Promise, getProps(...): Promise, }; ``` 更新响应类型: ```typescript export interface ProjectCharacterResponse { // ... 原有字段 tags?: ElementTag[]; // 新增:标签列表 } ``` ### 2. React Query Hooks **文件**: `client/src/hooks/api/useProjectElements.ts` 新增查询 hooks: ```typescript 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: ```typescript 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 **变更**: ```diff - import { useResources } from '@/hooks/api/useResources'; + import { useProjectElementsForPicker } from '@/hooks/api/useProjectElementsForPicker'; - const { data: allResources } = useResources(currentProjectId); + const { data: allResources } = useProjectElementsForPicker(currentProjectId); ``` #### StoryboardBoardPanel.tsx **变更**: ```diff - import { useResources } from '@/hooks/api/useResources'; + import { useProjectElementsForPicker } from '@/hooks/api/useProjectElementsForPicker'; - const { data: allResources } = useResources(currentProjectId); + const { data: allResources } = useProjectElementsForPicker(currentProjectId); ``` #### usePreviewData.ts **变更**: ```diff - 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 数据依赖 **删除导入**: ```diff - import { mockLocationTags, mockCharacterTagsSimple, mockPropTagsSimple } from '@/mocks/screenplay-tags'; ``` **移除 fallback 逻辑**: ```diff - } 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 组件**: - [x] 从后端获取项目角色列表 - [x] 从后端获取项目场景列表 - [x] 从后端获取项目道具列表 - [x] 展示元素的标签(角色装扮、场景时段等) - [x] 支持"角色 - 标签"层级选择 - [x] 标签按 `display_order` 排序 - [x] 已选资源正确排除(excludeValues) - [x] 搜索功能正常 **父组件**: - [x] ParseFlowDialog - 分镜编辑表单 - [x] StoryboardBoardPanel - 分镜看板 - [x] 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 ## 👥 影响用户 - ✅ 用户可以在分镜编辑时看到真实的项目角色/场景/道具 - ✅ 标签数据与资源库保持一致 - ✅ 新增的元素立即可用(无需刷新页面)