diff --git a/apps/ai-game-creator-shell/src/view/project-development/index.tsx b/apps/ai-game-creator-shell/src/view/project-development/index.tsx index c45aa1a30..31b1b75a2 100644 --- a/apps/ai-game-creator-shell/src/view/project-development/index.tsx +++ b/apps/ai-game-creator-shell/src/view/project-development/index.tsx @@ -188,6 +188,13 @@ import { zoomResourceBookViewport, } from './resourceBookViewport'; import { createResourceCanvasPageCategorySignature } from './resourceCanvasController'; +import { + buildResourceFilterTagOptions, + filterCanvasResources, + RESOURCE_FILTER_ALL_REGION, + resourceFilterIsActive, + type ResourceFilterRegion, +} from './resourceCanvasFilterModel'; import { createResourceCanvasCardSizeByResourceId, fitResourceCanvasViewportToContent, @@ -231,6 +238,11 @@ import { resolveResourceEditRequestIdentity, type ResourceEditRequestIdentity, } from './resourceEditModel'; +import { ResourceFilterPanel } from './ResourceFilterPanel'; +import { + openResourceCardFloatingPanelState, + type ResourceCardFloatingPanelKind, +} from './resourceFloatingPanelModel'; import { ResourceInfoFieldsView, ResourceInfoPanelView, @@ -1402,6 +1414,15 @@ export default function ProjectDevelopmentView({ */ const [resourceSearchOpen, setResourceSearchOpen] = useState(false); const resourceSearchId = useId(); + /** + * 筛选浮层的开合。与搜索浮层同一个右下角锚点,两者不得同时展开。 + * + * 这里**只**存开合与已选标签;「区域」不存第二份——它就是画布当前栏目, + * 由下方 `resourceFilterRegion` 从 `resourceBookState` 派生。 + */ + const [resourceFilterOpen, setResourceFilterOpen] = useState(false); + const resourceFilterId = useId(); + const [resourceFilterTags, setResourceFilterTags] = useState([]); const [resourceCanvasViewports, setResourceCanvasViewports] = useState(defaultResourceCanvasViewports); const resourceCanvasViewportTargetsRef = useRef( @@ -1535,6 +1556,8 @@ export default function ProjectDevelopmentView({ /** 右下角 Dock 里的搜索按钮:开合搜索浮层,也是浮层「点外部 / Esc」边界的一半。 */ const resourceSearchTriggerRef = useRef(null); const resourceSearchPopoverRef = useRef(null); + /** 右下角 Dock 里的筛选按钮:开合筛选浮层,也是该浮层「点外部 / Esc」边界的一半。 */ + const resourceFilterTriggerRef = useRef(null); const resourceFocusProjectPathRef = useRef(projectPath); /** 框选命中测试用的资源卡图层与当前画本视口,渲染期同步赋值。 */ const marqueeHitTestLayersRef = useRef([]); @@ -1576,6 +1599,24 @@ export default function ProjectDevelopmentView({ resourceCharacterAnimationRequestRef.current = null; }, []); + /** + * 开启资源卡旁的浮层:**开一个必然收起另一个**。 + * + * 「快速编辑」与「生成动画」挂在同一位置,两个都开就会叠在一起;互斥规则收在 + * `resourceFloatingPanelModel` 一处并被两个入口共用,因此不会只修好一边。 + */ + const applyResourceCardFloatingPanelOpen = useCallback( + (kind: ResourceCardFloatingPanelKind) => { + const { closed } = openResourceCardFloatingPanelState(kind); + if (closed === 'quick-edit') { + closeResourceQuickEditPanel(); + } else { + closeResourceCharacterAnimationPanel(); + } + }, + [closeResourceCharacterAnimationPanel, closeResourceQuickEditPanel], + ); + /** * 清资源画布焦点:对齐美术画布 `clearCanvasFocus()`。 * @@ -2212,25 +2253,56 @@ export default function ProjectDevelopmentView({ .map((resource) => resource.id), ); }, [resources, selectedResourceId]); - const normalizedSearch = searchText.trim().toLowerCase(); /** - * 画布可见资源只由搜索框收窄:功能分类筛选条、画布标签筛选与左侧栏目大纲导航 - * 已按用户要求移除,分区栏目本身(`categoryOrder` / 栏目标题栏 / 总览缩略卡片)不受影响。 + * 筛选区域 = 画布当前栏目本身,**不另存一份区域状态**。 + * + * 画布不在分页栏目里(总览态、或停在「所有资源」页)时视为「全部区域」;切换区域 + * 走下方既有 `openResourceBookChild`,与栏目切换完全同一条路径,因此滚轮 / 总览卡片 / + * 下一页换栏目时这里自动跟随,不需要双向同步。 + */ + const resourceFilterRegion: ResourceFilterRegion = + resourceBookView === 'child' && resourceBookState.category !== null + ? (resourceBookState.category as ResourceFilterRegion) + : RESOURCE_FILTER_ALL_REGION; + /** + * 画布可见资源由筛选浮层的三个维度收窄:区域、标签(AND)、关键词。 + * + * 关键词与搜索浮层共用同一份 `searchText`;判据收在 `resourceCanvasFilterModel` + * 一处,两个入口对同一关键词给出同一结果。分区栏目本身(`categoryOrder` / + * 栏目标题栏 / 总览缩略卡片)不受影响,`visibleResourcesByCategory` 由这里派生, + * 所以「区域=具体栏目时只筛该栏目」与「区域=全部时筛全量」共用同一份实现。 */ const visibleResources = useMemo( () => - canvasResources.filter((resource) => - normalizedSearch - ? [ - resource.label, - resource.path, - resource.mediaType, - resource.taskTitle ?? '', - ].some((value) => value.toLowerCase().includes(normalizedSearch)) - : true, - ), - [canvasResources, normalizedSearch], + filterCanvasResources(canvasResources, { + region: resourceFilterRegion, + tags: resourceFilterTags, + keyword: searchText, + }), + [canvasResources, resourceFilterRegion, resourceFilterTags, searchText], ); + /** + * 标签库按「区域 + 关键词」派生,**故意不含已选标签**。 + * + * 若拿上面已经过标签收窄的 `visibleResources` 派生,选了一个标签后列表就只剩该标签, + * 用户再也选不出第二个标签,AND 多选等于形同废掉。这里独立算一次区域 + 关键词的 + * 中间集合,保证「当前区域内、符合关键词的标签」始终可选。 + */ + const resourceFilterTagItems = useMemo( + () => + buildResourceFilterTagOptions( + filterCanvasResources(canvasResources, { + region: resourceFilterRegion, + tags: [], + keyword: searchText, + }), + ), + [canvasResources, resourceFilterRegion, searchText], + ); + const resourceFilterActive = resourceFilterIsActive({ + tags: resourceFilterTags, + keyword: searchText, + }); const visibleResourceIds = useMemo( () => new Set(visibleResources.map((resource) => resource.id)), [visibleResources], @@ -5348,6 +5420,8 @@ export default function ProjectDevelopmentView({ if (!resource) { return; } + // 与「生成动画」互斥:两个浮层挂在同一位置,开这个就收起那个。 + applyResourceCardFloatingPanelOpen('quick-edit'); setQuickEditSourceLayer(layer); const panelDraft = createResourceQuickEditPanelDraft(layer); setQuickEditPanel(panelDraft); @@ -5359,7 +5433,7 @@ export default function ProjectDevelopmentView({ }; setResourceWorkbenchNotice(''); }, - [canvasResources], + [applyResourceCardFloatingPanelOpen, canvasResources], ); /** @@ -5542,7 +5616,8 @@ export default function ProjectDevelopmentView({ if (!panelDraft) { return; } - closeResourceQuickEditPanel(); + // 与「快速编辑」互斥:两个浮层挂在同一位置,开这个就收起那个。 + applyResourceCardFloatingPanelOpen('character-animation'); setCharacterAnimationSourceLayer(layer); setCharacterAnimationPanel({ ...panelDraft, @@ -5556,7 +5631,7 @@ export default function ProjectDevelopmentView({ }; setResourceWorkbenchNotice(''); }, - [canvasResources, closeResourceQuickEditPanel], + [applyResourceCardFloatingPanelOpen, canvasResources], ); /** @@ -6367,10 +6442,30 @@ export default function ProjectDevelopmentView({ title="搜索资源(Ctrl/Cmd+F)" aria-expanded={resourceSearchOpen} aria-controls={resourceSearchId} - onClick={() => setResourceSearchOpen((current) => !current)} + onClick={() => { + // 搜索与筛选是同一个右下角锚点上的两个浮层,不得同时展开。 + setResourceFilterOpen(false); + setResourceSearchOpen((current) => !current); + }} >