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.6 KiB
6.6 KiB
时间轴资源块点击自动切换资源库Tab功能
日期: 2026-01-28
类型: 功能增强
影响范围: 时间轴、资源库面板
概述
实现了时间轴资源块点击时,自动切换资源库面板到对应资源类型 tab 的功能。
需求
用户点击时间轴中的资源块时:
- 点击角色资源 → 打开资源库 + 自动切换到角色 tab
- 点击场景资源 → 打开资源库 + 自动切换到场景 tab
- 点击道具资源 → 打开资源库 + 自动切换到道具 tab
- 点击实拍资源 → 打开资源库 + 自动切换到实拍 tab
实现方案
采用方案 1:通过 UIStore 集中管理,符合 React 和 Zustand 最佳实践。
架构设计
TimelineItem (资源块点击)
↓
TimelineTrack.onResourceSelect
↓
useTimelineLogic.handleResourceSelect
↓
[查找资源对象,获取类型] → setResourcePanelActiveTab(type)
↓
UIStore.resourcePanelActiveTab (全局状态)
↓
LeftSidebar.activeCategory
↓
ProjectResourcePanel.filterType
↓
[自动切换到对应 Tab]
技术实现
1. UIStore 状态管理
文件: client/src/stores/uiStore.ts
新增状态和操作:
type ResourcePanelTab = 'character' | 'scene' | 'prop' | 'footage';
interface UIState {
// ... 其他状态
resourcePanelActiveTab: ResourcePanelTab; // 新增:资源面板活动 tab
// ... 其他操作
setResourcePanelActiveTab: (tab: ResourcePanelTab) => void; // 新增:设置活动 tab
}
状态持久化配置(已添加到 partialize):
- 用户切换的 tab 会被保存到 localStorage
- 下次打开时自动恢复上次选择的 tab
2. 时间轴资源选择逻辑
文件: client/src/hooks/useTimelineLogic.ts
增强 handleResourceSelect 函数:
const handleResourceSelect = useCallback(
(_resourceId: string) => {
const wasSelected = selectedResourceId === _resourceId;
selectResource(_resourceId);
// 查找资源对象以获取其类型
const resource = resources?.find((r) => r.id === _resourceId);
if (wasSelected) {
toggleResourcesPanel();
} else {
setResourcesPanelOpen(true);
// 根据资源类型切换到对应的 tab
if (resource?.type) {
const tabMap: Record<string, 'character' | 'scene' | 'prop' | 'footage'> = {
character: 'character',
scene: 'scene',
prop: 'prop',
footage: 'footage',
};
const targetTab = tabMap[resource.type];
if (targetTab) {
setResourcePanelActiveTab(targetTab);
}
}
}
},
[selectResource, setResourcesPanelOpen, toggleResourcesPanel, selectedResourceId, resources, setResourcePanelActiveTab]
);
关键点:
- 通过
resources数组查找当前资源的类型 - 使用类型映射表确保类型安全
- 只在打开面板时切换 tab(避免重复切换时的干扰)
3. 资源面板状态绑定
文件: client/src/components/features/project/ProjectResourcePanel.tsx
绑定全局状态:
const { resourcePanelActiveTab, setResourcePanelActiveTab } = useUIStore();
// 优先使用外部传入的 activeCategory,其次使用全局状态,最后使用内部状态
const filterType = activeCategory !== undefined ? activeCategory : resourcePanelActiveTab || internalFilterType;
const handleFilterTypeChange = (type: string) => {
if (onCategoryChange) {
onCategoryChange(type);
} else {
// 更新全局状态
setResourcePanelActiveTab(type as 'character' | 'scene' | 'prop' | 'footage');
setInternalFilterType(type);
}
};
状态优先级:
- 外部 props (
activeCategory) - 最高优先级,支持完全受控 - 全局状态 (
resourcePanelActiveTab) - 中等优先级,支持跨组件通信 - 本地状态 (
internalFilterType) - 最低优先级,后备方案
4. 左侧边栏状态同步
文件: client/src/components/layout/LeftSidebar.tsx
移除本地状态,使用全局状态:
// 移除: const [activeCategory, setActiveCategory] = useState<string>(RESOURCE_CATEGORIES[0].key);
// 改为使用全局状态
const { resourcePanelActiveTab, setResourcePanelActiveTab } = useUIStore();
// 传递到子组件
<ProjectResourcePanel
projectId={viewingProjectId}
searchQuery={resourceSearchQuery}
activeCategory={resourcePanelActiveTab}
onCategoryChange={(category) => setResourcePanelActiveTab(category as 'character' | 'scene' | 'prop' | 'footage')}
/>
修改文件
- ✅
client/src/stores/uiStore.ts- 添加全局状态和操作 - ✅
client/src/hooks/useTimelineLogic.ts- 增强资源选择逻辑 - ✅
client/src/components/features/project/ProjectResourcePanel.tsx- 绑定全局状态 - ✅
client/src/components/layout/LeftSidebar.tsx- 移除本地状态,使用全局状态
测试要点
功能测试
- 点击时间轴中的角色资源块,资源库应打开并显示角色 tab
- 点击时间轴中的场景资源块,资源库应打开并显示场景 tab
- 点击时间轴中的道具资源块,资源库应打开并显示道具 tab
- 点击时间轴中的实拍资源块,资源库应打开并显示实拍 tab
- 点击已选中的资源块,应切换资源库的打开/关闭状态
- 手动切换 tab,状态应正确保存
状态持久化测试
- 切换到某个 tab 后刷新页面,应恢复上次选择的 tab
- 跨浏览器标签页,状态应保持一致
边界情况测试
- 资源类型为空或未知时,不应崩溃
- 资源库面板已打开时再次点击资源块,行为应符合预期
优点
- 集中状态管理:符合 Zustand 架构,易于维护和调试
- 状态持久化:用户体验更好,记住用户选择
- 类型安全:TypeScript 严格类型检查,减少运行时错误
- 易于扩展:未来可从其他地方控制资源面板 tab(如快捷键、搜索结果等)
- 向后兼容:保留了 props 传递方式,支持完全受控组件
性能考虑
- ✅ 使用
useCallback避免不必要的重新渲染 - ✅ 状态更新只在必要时触发
- ✅ 资源查找操作使用
Array.find(),时间复杂度 O(n),可接受
未来优化
- 如果资源列表非常大(>1000),考虑使用 Map 优化查找性能
- 可以添加资源块悬停预览功能
- 支持快捷键切换资源 tab