取消资源画本返回主画布的二次平移动画

新增 returning-main 阶段,区分返回主画布与进入子画布的 transform 过渡。

返回期间取消残留分页事件和进行中的子画布切换,避免返回流程被重新改写。

补充状态机与 appSurface 回归测试,并同步技术方案文档。
This commit is contained in:
2026-09-05 09:25:52 +08:00
parent 5bcaca7233
commit 65ffd4cc69
6 changed files with 106 additions and 14 deletions
+5 -7
View File
@@ -5192,11 +5192,14 @@ iframe.preview-frame {
transition: none;
}
.game-resource-book-scene--entering .game-resource-book-scene-world,
.game-resource-book-scene--returning .game-resource-book-scene-world {
.game-resource-book-scene--entering .game-resource-book-scene-world {
transition: transform 420ms cubic-bezier(0.2, 0.78, 0.2, 1);
}
.game-resource-book-scene--returning-main .game-resource-book-scene-world {
transition: none;
}
.game-resource-book-scene--child {
pointer-events: auto;
}
@@ -5471,11 +5474,6 @@ iframe.preview-frame {
linear-gradient(145deg, #fffdfa 0%, #fff8f2 100%);
}
.game-resource-book-manager--entering .game-resource-book-main,
.game-resource-book-manager--returning .game-resource-book-main {
transition: transform 420ms cubic-bezier(0.2, 0.78, 0.2, 1);
}
.game-resource-book-main-heading {
display: flex;
align-items: flex-end;
@@ -2640,6 +2640,13 @@ export default function ProjectDevelopmentView({
return;
}
if (resourceBookState.phase === 'returning') {
if (!resourceBookState.pendingCategory) {
// A return-to-main transition has no destination category. Ignore
// any wheel step that was already queued before the collapse click;
// otherwise it can resurrect a child-enter transition and append a
// second viewport movement after the return leg.
return;
}
// A wheel gesture can arrive while a click-driven book transition is
// still in flight. Retarget the transition through the same tokenized
// path so the old timer cannot restore a stale category.
@@ -2661,6 +2668,7 @@ export default function ProjectDevelopmentView({
cancelResourceCanvasPan,
cancelResourceCardDrag,
openResourceBookChild,
resourceBookState.pendingCategory,
resourceBookState.phase,
resourcePageCategories,
],
@@ -1,7 +1,11 @@
import type { ProjectResourceCategory } from './resourceProjectionModel';
export type ResourceBookCategory = ProjectResourceCategory;
export type ResourceBookPhase = 'idle' | 'entering' | 'returning';
export type ResourceBookPhase =
| 'idle'
| 'entering'
| 'returning'
| 'returning-main';
export type ResourceBookState = {
view: 'main' | 'child';
@@ -74,7 +78,7 @@ export function resourceBookReducer(
case 'finish-transition':
return state.token === action.token ? { ...state, phase: 'idle' } : state;
case 'return-to-main':
if (state.view !== 'child' || state.phase === 'returning') {
if (state.view !== 'child') {
return state;
}
return {
@@ -97,11 +101,10 @@ export function resourceBookReducer(
view: 'main',
category: null,
pendingCategory: null,
// Keep the return leg in the animated phase while the scene
// viewport transitions from the child viewport back to the
// main viewport. The controller dispatches finish-transition
// after the same animation window used by child entry.
phase: 'entering',
// Returning to the main canvas is already completed by the
// shared-card shrink leg. Keep a distinct phase so the main
// viewport does not receive a second transform transition.
phase: 'returning-main',
}
: state;
case 'reset':
@@ -786,6 +786,31 @@ export function registerProjectWorkbenchFoundationTests() {
{ name: '收起资源' },
),
);
fireEvent.click(
within(
document.querySelector<HTMLElement>(
'.game-resource-book-scene-titlebar.is-active',
) as HTMLElement,
).getByRole('button', { name: '收起资源' }),
);
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 340));
});
expect(
screen
.getByRole('region', { name: '资源主画布' })
.closest('[data-resource-book-view]')
?.getAttribute('data-resource-book-transition'),
).toBe('returning-main');
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 540));
});
expect(
screen
.getByRole('region', { name: '资源主画布' })
.closest('[data-resource-book-view]')
?.getAttribute('data-resource-book-transition'),
).toBe('idle');
await openResourceBookCategory('设计文档');
dispatchPageWheel(dependencyCanvas);
@@ -73,6 +73,62 @@ describe('resource book state machine', () => {
expect(resourceBookSceneCategory(overview)).toBe('art');
});
it('marks a return to the main canvas separately from a child enter transition', () => {
const entering = resourceBookReducer(initialResourceBookState, {
type: 'open-category',
category: 'document',
token: 1,
});
const child = resourceBookReducer(entering, {
type: 'commit-enter',
token: 1,
});
const returning = resourceBookReducer(child, {
type: 'return-to-main',
token: 2,
});
const main = resourceBookReducer(returning, {
type: 'commit-return',
token: 2,
});
expect(main).toMatchObject({
view: 'main',
category: null,
pendingCategory: null,
phase: 'returning-main',
});
});
it('lets an explicit collapse cancel an in-flight child switch', () => {
const entering = resourceBookReducer(initialResourceBookState, {
type: 'open-category',
category: 'document',
token: 1,
});
const child = resourceBookReducer(entering, {
type: 'commit-enter',
token: 1,
});
const switching = resourceBookReducer(child, {
type: 'open-category',
category: 'art',
token: 2,
});
const main = resourceBookReducer(switching, {
type: 'return-to-main',
token: 3,
});
expect(main).toMatchObject({
view: 'child',
category: 'document',
pendingCategory: null,
phase: 'returning',
token: 3,
});
});
it('ignores stale transition callbacks', () => {
const entering = resourceBookReducer(initialResourceBookState, {
type: 'open-category',