From 0ef1a4ec15a511ead2b6fd86f29e4c92b70fef8b Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Fri, 11 Sep 2026 20:29:45 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E7=BA=BF=E8=B5=84=E6=BA=90=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E6=B5=AE=E5=B1=82=E5=B9=B6=E6=8A=8A=E8=B5=84=E6=BA=90?= =?UTF-8?q?=E5=8D=A1=E6=97=81=E4=B8=A4=E4=B8=AA=E6=B5=AE=E5=B1=82=E6=94=B9?= =?UTF-8?q?=E6=88=90=E4=BA=92=E6=96=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - index.tsx:新增 resourceFilterOpen 与 resourceFilterTags 两个 state,区域不新增 state(唯一新增的筛选相关 state 是已选标签) - index.tsx:可见资源由内联关键词过滤改为调用 filterCanvasResources,维度为区域 / 标签 / 关键词;区域由 resourceBookState 派生(不在分页栏目里时为「全部区域」),因此用户用滚轮 / 总览卡片 / 下一页换栏目时区域自动跟随,无需双向同步 - index.tsx:标签库按「区域 + 关键词」派生并故意不含已选标签——若拿已按标签收窄的集合派生,选了一个标签后列表只剩该标签,用户再也选不出第二个标签,AND 多选会形同废掉 - index.tsx:Dock 上紧邻搜索按钮新增筛选按钮,复用 game-resource-book-zoom-button 与 is-active 口径;两个浮层共用同一个右下角锚点,因此开筛选即收起搜索、开搜索即收起筛选 - index.tsx:挂载 ResourceFilterPanel,区域变更转交既有 openResourceBookChild(与栏目切换同一条路径),关键词与搜索浮层共用同一份 searchText - resourceCanvasFilterModel.ts:resourceFilterIsActive 只看关键词与标签、不含区域——区域就是画布当前栏目,用户换栏时它照样在变,算进高亮会让按钮在「只是换了一栏看」时也常亮 - 新增 resourceFloatingPanelModel.ts 与其测试:资源卡旁「快速编辑」与「生成动画」的互斥规则收在一处并被两个开启入口共用,修掉「先开生成动画再点快速编辑两个浮层并存」的缺口;关闭仍只关自己,生成收尾不会吞掉用户刚打开的另一个面板 - 断言:新增 5 条互斥用例(含两个方向各自一条与来回点击序列),加上既有筛选用例;变异验证实测——把互斥模型的 closed 写成恒为 quick-edit(等价只修一个方向)→ 4 failed,还原后 5 passed --- .../src/view/project-development/index.tsx | 160 ++++++++++++++++-- .../resourceCanvasFilterModel.ts | 14 +- .../resourceFloatingPanelModel.ts | 31 ++++ .../tests/resourceCanvasFilterModel.test.ts | 5 +- .../tests/resourceFloatingPanelModel.test.ts | 124 ++++++++++++++ 5 files changed, 308 insertions(+), 26 deletions(-) create mode 100644 apps/ai-game-creator-shell/src/view/project-development/resourceFloatingPanelModel.ts create mode 100644 apps/ai-game-creator-shell/tests/resourceFloatingPanelModel.test.ts 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); + }} >