diff --git a/apps/ai-game-creator-shell/src/styles.css b/apps/ai-game-creator-shell/src/styles.css index e0e6d2bea..a607f465c 100644 --- a/apps/ai-game-creator-shell/src/styles.css +++ b/apps/ai-game-creator-shell/src/styles.css @@ -5640,13 +5640,6 @@ iframe.preview-frame { flex-direction: column; flex: 1 1 auto; min-height: 0; - /* - * 顶部工具条带(搜索框、筛选条、状态提示)在流内占一条独立排布带, - * `--game-resource-book-tools-height` 由实测高度写入。铺满管理区的画本场景按同一 - * 变量把自己缩到这条带下面,栏目标题栏与栏目大纲就不会和搜索框落在同一条排布带上 - * (分页态与总览态都适用)。 - */ - padding-top: var(--game-resource-book-tools-height, 0px); } .game-resource-book-manager { @@ -5658,8 +5651,8 @@ iframe.preview-frame { .game-resource-book-scene { position: absolute; z-index: 20; - /* 顶边让出工具条带:栏目标题栏与栏目大纲整体下移,不与搜索框争同一条排布带。 */ - inset: var(--game-resource-book-tools-height, 0px) 0 0; + /* 铺满管理区:栏目标题栏、栏目大纲与资源卡都从管理区顶边开始正常占位。 */ + inset: 0; overflow: hidden; pointer-events: none; transition: none; @@ -6166,35 +6159,26 @@ iframe.preview-frame { } /* - * 工具条带:搜索框、筛选条与状态提示在这里按独立排布行排列,画本场景从这条带下面 - * 开始(见 `.game-resource-manager` 的 padding-top)。工具条里的浮层元素仍高于场景 - * (z-index 40 > 场景的 20),工具条内容溢出时不会被画本盖住。 + * 资源搜索浮层。 + * + * 搜索条不再常驻:原来它和筛选条一起在管理区顶部占一条独立排布带(管理区 padding-top + * 加画本场景下移),栏目标题栏被挤到那条带下面仍然和搜索框叠在同一条水平带上。现在常态 + * 只有右下角 Dock 里的搜索按钮,Ctrl/Cmd+F(macOS 为 Cmd+F)或点按钮才把浮层叫出来, + * Esc / 点外部收起(口径见 `useImageCanvasFloatingOptionDismiss`)。 + * + * 浮层贴着右下角 Dock 向上展开:`bottom: 58px` = Dock 的 `bottom: 14px` + Dock 高度 + * `34px` + 10px 间隙,所以它永远不会落到栏目标题栏(管理区顶边 min-height 42px)那条带上。 + * `z-index: 40` 高于画本场景的 20,浮在资源卡与栏目标题栏之上。 */ -.game-resource-book-tools { - position: absolute; - top: 0; - right: 0; - left: 0; - z-index: 40; - display: flex; - flex-direction: column; - gap: 8px; - padding: 10px 12px 4px; -} - -.game-resource-book-tools > * { - margin: 0; - width: 100%; -} - .game-resource-search { - position: relative; + position: absolute; + right: 14px; + bottom: 58px; z-index: 40; display: flex; align-items: center; gap: 8px; - align-self: center; - width: min(300px, 100%); + width: min(320px, calc(100% - 28px)); padding: 0 10px; border: 1px solid #ecdcd4; border-radius: 10px; @@ -6203,6 +6187,15 @@ iframe.preview-frame { color: #a08073; } +/* + * 搜索条件生效时(浮层收起也生效)Dock 上的搜索按钮保持高亮:筛选条件不会随浮层一起 + * 消失,按钮必须能看出"当前有搜索",否则用户会以为资源丢了。复用 Dock 按钮的 hover 色。 + */ +.game-resource-book-zoom-button.is-active { + background: #f7ebe4; + color: #b5582f; +} + .game-resource-search input { width: 100%; min-width: 0; @@ -6215,6 +6208,35 @@ iframe.preview-frame { font-size: 12px; } +/* + * 浮层提示(工作台状态提示、附件导入失败):不再占管理区顶部的排布带高度,改成绝对 + * 定位的浮层,落在栏目标题栏(管理区顶边 min-height 42px)下面 16px 处居中。 + * + * 容器整体 `pointer-events: none`,只有提示条自己恢复 `pointer-events: auto`:这条横跨 + * 管理区的空盒子如果接收点击,就会把「资源总览」这类按钮的命中区域吃掉——那正是 + * "回到资源总览点不动"的成因,所以命中的门槛只留在提示条自己的盒子上。 + */ +.game-resource-book-notices { + position: absolute; + top: 58px; + left: 50%; + z-index: 40; + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + width: max-content; + max-width: min(420px, calc(100% - 28px)); + transform: translateX(-50%); + pointer-events: none; +} + +.game-resource-book-notices > * { + width: auto; + max-width: 100%; + pointer-events: auto; +} + .game-resource-classification-dialog { width: min(480px, 100%); } @@ -6294,7 +6316,7 @@ iframe.preview-frame { align-items: center; justify-content: space-between; gap: 10px; - margin: 8px 12px 0; + margin: 0; padding: 8px 10px; border: 1px solid #edc7b5; border-radius: 10px; @@ -6314,7 +6336,7 @@ iframe.preview-frame { .game-attachment-errors { display: grid; gap: 4px; - margin: 8px 12px 0; + margin: 0; padding: 8px 10px; border: 1px solid #edc1ad; border-radius: 10px; @@ -8878,16 +8900,6 @@ iframe.preview-frame { font-size: 12px; } -/* - * 窄屏(移动端优先):筛选条与标签开始换行时,搜索框改为整行独占, - * 工具条带变高后画本场景按实测高度继续下移,任何宽度下都不与标题栏重叠。 - */ -@media (max-width: 560px) { - .game-resource-search { - width: 100%; - } -} - @media (max-width: 620px) { .game-resource-recovery-item, .game-resource-recovery-error { 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 8967dd75c..f2a81d7d8 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 @@ -406,21 +406,6 @@ function resourceCanvasElementSize(element: HTMLElement | null) { }; } -/** - * 资源工具条(搜索框、筛选条、状态提示)在管理区顶部占一条独立排布带,画本场景的盒子 - * 从这条带下面开始。按视口排布资源时要减掉这条带,否则"适应内容"仍按旧的 - * "场景一直顶到管理区顶端"居中,把卡片排到工具条底下。 - */ -function resourceCanvasSceneSize( - size: { width: number; height: number }, - chromeHeight: number, -) { - return { - width: size.width, - height: Math.max(0, size.height - Math.max(0, chromeHeight)), - }; -} - function resourceCanvasViewportsEqual( left: CanvasViewport, right: CanvasViewport, @@ -1222,11 +1207,6 @@ export default function ProjectDevelopmentView({ createResourceBookTransitionController(), ); const resourceBookManagerRef = useRef(null); - /** - * 工具条带的第一条流式元素(搜索框)。工具条带本身没有独立包裹节点,命中它就能把 - * "第一条流式元素顶边到管理区顶边"的高度算出来,正好是画本场景要让出的那条带。 - */ - const resourceBookToolsRef = useRef(null); const resourceBookPlanKeysRef = useRef( new Map(), ); @@ -1240,17 +1220,14 @@ export default function ProjectDevelopmentView({ width: 900, height: 640, }); - const [resourceBookToolsHeight, setResourceBookToolsHeight] = useState(0); - /** - * 场景局部的可排布尺寸:管理区盒子减掉顶部工具条带。视口居中与"适应内容"都按它算。 - */ - const sceneFitCanvasSize = useMemo( - () => - resourceCanvasSceneSize(resourceBookSceneSize, resourceBookToolsHeight), - [resourceBookSceneSize, resourceBookToolsHeight], - ); const [sortMode, setSortMode] = useState('dependency'); const [searchText, setSearchText] = useState(''); + /** + * 搜索浮层的开合。搜索条不再常驻:常态只留右下角 Dock 里的搜索按钮与 + * Ctrl/Cmd+F(见下方两处效果),浮层收起不清 `searchText`,筛选条件继续生效。 + */ + const [resourceSearchOpen, setResourceSearchOpen] = useState(false); + const resourceSearchId = useId(); const [resourceCanvasViewports, setResourceCanvasViewports] = useState(defaultResourceCanvasViewports); const resourceCanvasViewportTargetsRef = useRef( @@ -1374,6 +1351,9 @@ export default function ProjectDevelopmentView({ const skipNextResourceCardClickRef = useRef(null); const skipNextResourceCardClickTimerRef = useRef(null); const resourceSearchRef = useRef(null); + /** 右下角 Dock 里的搜索按钮:开合搜索浮层,也是浮层「点外部 / Esc」边界的一半。 */ + const resourceSearchTriggerRef = useRef(null); + const resourceSearchPopoverRef = useRef(null); const resourceFocusProjectPathRef = useRef(projectPath); /** 框选命中测试用的资源卡图层与当前画本视口,渲染期同步赋值。 */ const marqueeHitTestLayersRef = useRef([]); @@ -2041,16 +2021,13 @@ export default function ProjectDevelopmentView({ return; } const managerRect = manager.getBoundingClientRect(); - // 画本场景是管理区内缩掉顶部工具条带之后的盒子;总览栏目矩形与场景世界都按它定位。 - const sceneRect = - manager - .querySelector('.game-resource-book-scene') - ?.getBoundingClientRect() ?? managerRect; + // 画本场景铺满管理区(`inset: 0`,顶部不再有工具条带):管理区盒子就是场景盒子, + // 总览栏目矩形与场景世界都按同一个原点定位。 setResourceBookSceneSize((current) => - Math.abs(current.width - sceneRect.width) < 0.5 && - Math.abs(current.height - sceneRect.height) < 0.5 + Math.abs(current.width - managerRect.width) < 0.5 && + Math.abs(current.height - managerRect.height) < 0.5 ? current - : { width: sceneRect.width, height: sceneRect.height }, + : { width: managerRect.width, height: managerRect.height }, ); const next = new Map(); resourceBookThumbnailElementsRef.current.forEach((element, category) => { @@ -2112,28 +2089,59 @@ export default function ProjectDevelopmentView({ }; }, [measureResourceBookOverview, resourceBookView, canvasResources.length]); - useLayoutEffect(() => { - const tools = resourceBookToolsRef.current; - if (!tools) { + /** + * 搜索浮层的「点外部 / Esc 关闭」。 + * + * 直接复用美术画布的 `useImageCanvasFloatingOptionDismiss`,不另写判据:它在 + * document 上同时接管 click 与 Escape,点在边界外即关、点在边界内保留;Escape 走 + * 同一口径在 document 上截断,画布全局的 Esc(清选中)不会跟着一起触发。 + * 边界是浮层自己与右下角那颗搜索按钮:点按钮只走按钮自己的开合,不会先被判定成外部。 + */ + useImageCanvasFloatingOptionDismiss({ + isOpen: resourceSearchOpen && mode === 'resources' && !uiEditorRoute, + boundaryRefs: [resourceSearchPopoverRef, resourceSearchTriggerRef], + onDismiss: () => setResourceSearchOpen(false), + }); + + /** + * Ctrl/Cmd+F 叫出搜索浮层。 + * + * 搜索条不再常驻占管理区顶部那条排布带(那条带与栏目标题栏争同一条水平带),改成 + * 快捷键 + 右下角小按钮临时叫出。关闭不清 `searchText`:筛选条件与「清除搜索并定位」 + * 的语义与常驻搜索条时期一致。 + */ + useEffect(() => { + if (mode !== 'resources' || uiEditorRoute) { return undefined; } - const measureTools = () => { - const height = tools.offsetTop + tools.offsetHeight; - setResourceBookToolsHeight((current) => - Math.abs(current - height) < 0.5 ? current : height, - ); + const handleResourceSearchShortcut = (event: KeyboardEvent) => { + if ( + event.key.toLowerCase() !== 'f' || + !(event.ctrlKey || event.metaKey) || + event.altKey || + event.shiftKey + ) { + return; + } + event.preventDefault(); + setResourceSearchOpen(true); }; - measureTools(); - if (typeof ResizeObserver === 'undefined') { - window.addEventListener('resize', measureTools); - return () => window.removeEventListener('resize', measureTools); + window.addEventListener('keydown', handleResourceSearchShortcut); + return () => + window.removeEventListener('keydown', handleResourceSearchShortcut); + }, [mode, uiEditorRoute]); + + /** + * 浮层打开后焦点落到搜索框:PRD「聚焦资源被删除时焦点落到资源搜索框」的落点入口, + * `resourceSearchRef` 由此不再是死 ref。 + */ + useEffect(() => { + if (!resourceSearchOpen) { + return; } - // 工具条会随窗口宽度换行(窄屏下搜索框与筛选条各占一行),高度必须实测: - // 场景盒子按它下移,两者才不会落在同一条排布带上。 - const observer = new ResizeObserver(measureTools); - observer.observe(tools); - return () => observer.disconnect(); - }, [mode, resourceBookView, searchText]); + resourceSearchRef.current?.focus(); + }, [resourceSearchOpen]); + const viewedResourceCategory = mode === 'resources' ? activePageCategory : null; useEffect(() => { @@ -2310,8 +2318,8 @@ export default function ProjectDevelopmentView({ // 返回总览时也不会跳成另一张图。 const resourceBookCategoryViewCenters = useMemo(() => { const canvasSize = - sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0 - ? sceneFitCanvasSize + resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0 + ? resourceBookSceneSize : { width: RESOURCE_CANVAS_FALLBACK_WIDTH, height: RESOURCE_CANVAS_FALLBACK_HEIGHT, @@ -2330,7 +2338,7 @@ export default function ProjectDevelopmentView({ ] as const; }), ); - }, [sceneFitCanvasSize, resourceCanvasViewports, sortMode]); + }, [resourceBookSceneSize, resourceCanvasViewports, sortMode]); const setResourceCanvasViewport = useCallback( ( category: ResourceCategory, @@ -2823,14 +2831,14 @@ export default function ProjectDevelopmentView({ const categoryFitKey = `${projectPath}\n${manifest.projectId}\n${sortMode}\n${category}`; const fitCanvasSize = - sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0 - ? sceneFitCanvasSize + resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0 + ? resourceBookSceneSize : { width: RESOURCE_CANVAS_FALLBACK_WIDTH, height: RESOURCE_CANVAS_FALLBACK_HEIGHT, }; const fitMaxScale = - sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0 + resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0 ? RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE : 1; const categoryBounds = resourceCanvasContentBounds( @@ -2875,7 +2883,7 @@ export default function ProjectDevelopmentView({ // sort/category pair. Mark that fit as settled so the paged-canvas // layout effect does not immediately recalculate it from a shared DOM // measurement and overwrite the stored per-category viewport. - if (sceneFitCanvasSize.width > 0 && sceneFitCanvasSize.height > 0) { + if (resourceBookSceneSize.width > 0 && resourceBookSceneSize.height > 0) { resourceCanvasFitKeysRef.current.add(categoryFitKey); } @@ -2888,7 +2896,7 @@ export default function ProjectDevelopmentView({ manifest.projectId, projectPath, resourceBookState, - sceneFitCanvasSize, + resourceBookSceneSize, resourceCardSizeByResourceId, resourcePositionsByCategory, setResourceCanvasViewport, @@ -3833,14 +3841,14 @@ export default function ProjectDevelopmentView({ activePageCategory, fitResourceCanvasViewportToContent({ bounds: resourceCanvasFitBounds, - canvasSize: sceneFitCanvasSize, + canvasSize: resourceBookSceneSize, padding: RESOURCE_CANVAS_FIT_PADDING, maxScale: RESOURCE_CANVAS_BOOK_FIT_MAX_SCALE, }), ); }, [ activePageCategory, - sceneFitCanvasSize, + resourceBookSceneSize, resourceBookView, resourceCanvasFitBounds, setResourceCanvasViewport, @@ -3852,8 +3860,8 @@ export default function ProjectDevelopmentView({ const sceneElement = resourceBookManagerRef.current; const rect = sceneElement?.getBoundingClientRect(); const anchorX = rect ? rect.width / 2 : 0; - // 锚点取真正看得见的画本区域中心:顶部工具条带不算画布。 - const anchorY = rect ? (rect.height - resourceBookToolsHeight) / 2 : 0; + // 画本场景就是管理区盒子(顶部不再有工具条带),锚点取盒子中心即可。 + const anchorY = rect ? rect.height / 2 : 0; if (resourceBookView === 'main') { const viewport = resourceBookMainViewportRef.current; const next = zoomResourceBookViewport(viewport, factor, { @@ -3877,12 +3885,7 @@ export default function ProjectDevelopmentView({ if (next.scale === viewport.scale) return; setResourceCanvasViewport(category, next); }, - [ - activePageCategory, - resourceBookToolsHeight, - resourceBookView, - setResourceCanvasViewport, - ], + [activePageCategory, resourceBookView, setResourceCanvasViewport], ); useLayoutEffect(() => { @@ -3918,10 +3921,9 @@ export default function ProjectDevelopmentView({ } event.preventDefault(); const rect = sceneElement.getBoundingClientRect(); - // 工具条带里没有画布,滚轮锚点先减掉它,缩放才不会以被盖住的区域为中心。 const screenPoint = { x: event.clientX - rect.left, - y: event.clientY - rect.top - resourceBookToolsHeight, + y: event.clientY - rect.top, }; if (resourceBookView === 'main') { const current = resourceBookMainViewportRef.current; @@ -3953,12 +3955,7 @@ export default function ProjectDevelopmentView({ }); setResourceCanvasViewport(category, result.viewport, true); }, - [ - activePageCategory, - resourceBookToolsHeight, - resourceBookView, - setResourceCanvasViewport, - ], + [activePageCategory, resourceBookView, setResourceCanvasViewport], ); useEffect(() => { @@ -5742,19 +5739,39 @@ export default function ProjectDevelopmentView({ >