From d0b15dbc54a45768145e00f5e1188107102d8c3b Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Mon, 14 Sep 2026 23:01:35 +0800 Subject: [PATCH] =?UTF-8?q?AGC=20=E7=94=9F=E6=88=90=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E4=BE=A7=E6=A0=8F=EF=BC=9A=E6=94=B6=E8=B5=B7=E4=B9=9F=E8=A1=A5?= =?UTF-8?q?=E4=B8=8A=E9=80=80=E5=9C=BA=E5=8A=A8=E7=94=BB=EF=BC=8C=E9=80=80?= =?UTF-8?q?=E5=9C=BA=E6=9C=9F=E9=97=B4=E5=86=85=E5=AE=B9=E4=B8=8D=E8=B7=B3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 进场本来就有动画(`…-enter`),收起却是瞬间卸载,观感上"闪一下没了"。现在收起先播退场再卸载:`open` 变 false 后进入 `leaving`,根节点挂 `is-leaving` 播 `…-leave`(与进场同向反向,160ms,`pointer-events: none`),播完才卸载 - 时长在组件与 CSS 两处各写一次,组件导出 `RESOURCE_CANVAS_ASSET_GENERATION_TASKS_LEAVE_MILLIS` 并由用例钉住两处一致 - 减动效偏好下 CSS 已关掉动画,组件那边同步按 0ms 处理,收起立即卸载,不白等 160ms - 退场期间沿用收起前那一份列表(`lastRenderedRef`):宿主会在同一帧里收起侧栏并把在途任务收口,直接吃新 props 会让退场动画里的内容跳一下;滚动区的空态分支也改读这份冻结快照,不能读实时 `ordered`(这是本批调试时用假时钟用例抓到的真问题) - 用例:面板用例新增「收起时先播完收起动画再卸载,动画期间列表内容不跳」;样式用例新增「is-leaving 挂上收起动画、pointer-events: none、减动效同样关掉」;既有生成任务相关性用例改为等退场动画走完再断言(新增 `waitForTasksSidebarLeaveAnimationToFinish`) - 文档:AGC 入口矩阵与 decision-log 同步退场动画口径 验证:tsc --noEmit 通过;appSurface 全量 441 条通过;侧栏面板 9 条、样式 10 条、开合与入口 3 条通过;变异验证:把早返回改回「立即卸载」与把 CSS 的 `is-leaving` 选择器写错,对应断言分别变红 --- ...rceCanvasAssetGenerationTasksPanelView.tsx | 93 +++++++++++++++---- ...ourceCanvasAssetGenerationTasksSidebar.css | 25 ++++- .../appSurface/project-development.suite.ts | 18 +++- ...ceCanvasAssetGenerationTasksPanel.test.tsx | 47 ++++++++++ ...asAssetGenerationTasksSidebarStyle.test.ts | 32 +++++++ .../shared-memory/decision-log.md | 1 + ...C】栏目画布底部工具栏入口矩阵-2026-09-13.md | 2 +- 7 files changed, 197 insertions(+), 21 deletions(-) diff --git a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx index 07ded6984..ed1a33d0b 100644 --- a/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx +++ b/apps/ai-game-creator-shell/src/features/resource-canvas/ResourceCanvasAssetGenerationTasksPanelView.tsx @@ -1,7 +1,7 @@ import './resourceCanvasAssetGenerationTasksSidebar.css'; import { ListChecks, X } from 'lucide-react'; -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useMemo, useRef, useState } from 'react'; import { RESOURCE_CANVAS_ASSET_GENERATION_STATUS_LABELS, @@ -16,6 +16,12 @@ import { /** 「已完成」分栏的展示上限:触顶后只提示还有多少条,不无限拉长侧栏。 */ export const RESOURCE_CANVAS_ASSET_GENERATION_DONE_SECTION_LIMIT = 20; +/** + * 收起动画时长,必须与 `resourceCanvasAssetGenerationTasksSidebar.css` 里的 + * `game-resource-generation-tasks-leave` 一致:收起时侧栏要先播完这一段再卸载。 + */ +export const RESOURCE_CANVAS_ASSET_GENERATION_TASKS_LEAVE_MILLIS = 160; + export type ResourceCanvasAssetGenerationTasksPanelViewProps = { tasks: readonly ResourceCanvasAssetGenerationTask[]; /** 侧栏是否展开;折叠时只留贴边把手。 */ @@ -102,6 +108,13 @@ export function ResourceCanvasAssetGenerationTasksPanelView({ onFocusTask, }: ResourceCanvasAssetGenerationTasksPanelViewProps) { const [nowMillis, setNowMillis] = useState(() => Date.now()); + /** + * 收起动画期:`open` 已经变 false,但侧栏还要留在 DOM 里把 `…-leave` 播完 + * (`entering` 是刚打开时给根节点挂进场动画的那一档)。 + */ + const [phase, setPhase] = useState<'idle' | 'entering' | 'leaving'>( + open ? 'entering' : 'idle', + ); const inFlightCount = tasks.filter( (task) => !resourceCanvasAssetGenerationTaskIsTerminal(task), ).length; @@ -120,6 +133,29 @@ export function ResourceCanvasAssetGenerationTasksPanelView({ 0, RESOURCE_CANVAS_ASSET_GENERATION_DONE_SECTION_LIMIT, ); + /** + * 收起动画期间必须沿用最后一份列表:宿主会在同一帧里收起侧栏并把「在途」收口成 + * 已完成,直接吃新 props 会让退出动画里的内容跳一下。 + */ + const lastRenderedRef = useRef({ + inFlightCount, + ordered, + active, + visibleDone, + done, + }); + if (open) { + lastRenderedRef.current = { + inFlightCount, + ordered, + active, + visibleDone, + done, + }; + } + const rendered = open + ? { inFlightCount, ordered, active, visibleDone, done } + : lastRenderedRef.current; // 已耗时是前端计时(后端只给时间戳):只在还有未终态任务时走秒表,全部收口后停掉。 useEffect(() => { @@ -130,18 +166,41 @@ export function ResourceCanvasAssetGenerationTasksPanelView({ return () => clearInterval(timer); }, [hasLiveTask]); + /** + * open → 进场,close → 先留一帧播 `…-leave` 再卸载。 + * + * 减少动效偏好下这一段动画在 CSS 里被关掉,所以那次收起要**立即**卸载,不能白等 160ms。 + */ + useEffect(() => { + if (open) { + setPhase('entering'); + return undefined; + } + setPhase((current) => (current === 'idle' ? 'idle' : 'leaving')); + const reducedMotion = + typeof window.matchMedia === 'function' && + window.matchMedia('(prefers-reduced-motion: reduce)').matches; + const timer = window.setTimeout( + () => setPhase('idle'), + reducedMotion ? 0 : RESOURCE_CANVAS_ASSET_GENERATION_TASKS_LEAVE_MILLIS, + ); + return () => window.clearTimeout(timer); + }, [open]); + // 折叠态不再在画布左侧留贴边把手:开合口只剩工具条上那一枚「生成任务」按钮 - // (带在途计数),收起时侧栏完全让出画布。 - if (!open) { + // (带在途计数),收起时侧栏完全让出画布(播完收起动画之后)。 + if (!open && phase === 'idle') { return null; } return (