资源画布首屏加载增加转圈提示

进入资源画布到首屏预览落地之间画布是空的,新增居中加载态:Loader2 转圈加文案,role=status 可被读屏播报
加载判定抽到 resourceBookIsLoading:有资源且布局未就绪或首屏预览一张都没落地时显示;预览失败同样视为落地,避免一直转圈
补充 resourceBookModel 加载态用例
This commit is contained in:
2026-09-09 16:31:02 +08:00
parent 985db358a7
commit bafae0f127
4 changed files with 110 additions and 0 deletions
+15
View File
@@ -5634,6 +5634,21 @@ iframe.preview-frame {
pointer-events: none;
}
.game-resource-book-loading {
position: absolute;
inset: 0;
z-index: 40;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
background: rgb(255 253 250 / 78%);
color: #a08073;
font-size: 13px;
pointer-events: none;
}
.game-resource-book-manager[data-book-motion='running']
.game-resource-book-scene-world {
opacity: 0;
@@ -10,6 +10,7 @@ import {
Image,
Info,
ListFilter,
Loader2,
Maximize2,
Minus,
Music2,
@@ -98,6 +99,7 @@ import {
} from './resourceBookLayout';
import {
initialResourceBookState,
resourceBookIsLoading,
resourceBookReducer,
resourceBookSceneCategory,
resourceBookShowsChildCards,
@@ -2189,6 +2191,36 @@ export default function ProjectDevelopmentView({
sortMode,
visibleCategoryOrder,
]);
const resourceBookOverviewReady = useMemo(
() =>
visibleCategoryOrder.every((category) => {
const rect = resourceBookOverviewRects.get(category);
return Boolean(rect && rect.width > 0 && rect.height > 0);
}),
[resourceBookOverviewRects, visibleCategoryOrder],
);
const resourceBookSettledPreviewCount = useMemo(() => {
let settled = 0;
for (const resource of visibleResources) {
const identity = resourceCardPreviews.identityByResourceId.get(
resource.id,
);
if (!identity) continue;
const status = resourceCardPreviews.previews.get(identity)?.status;
if (status === 'loaded' || status === 'failed') settled += 1;
}
return settled;
}, [
resourceCardPreviews.identityByResourceId,
resourceCardPreviews.previews,
visibleResources,
]);
const resourceBookLoading = resourceBookIsLoading({
resourceCount: visibleResources.length,
overviewReady: resourceBookOverviewReady,
cardsReady: sortMode !== 'dependency' || resourceGraphInitializationReady,
settledPreviewCount: resourceBookSettledPreviewCount,
});
const setResourceCanvasViewport = useCallback(
(
category: ResourceCategory,
@@ -5590,6 +5622,20 @@ export default function ProjectDevelopmentView({
onWheel={handleResourceBookWheel}
/>
<ResourceBookTransitionLayer />
{resourceBookLoading ? (
<div
className="game-resource-book-loading"
role="status"
aria-live="polite"
>
<Loader2
size={22}
aria-hidden="true"
className="animate-spin"
/>
<span></span>
</div>
) : null}
<div className="game-resource-book-zoom">
<button
type="button"
@@ -78,3 +78,24 @@ export function resourceBookShowsOverviewCards(state: ResourceBookState) {
export function resourceBookShowsChildCards(state: ResourceBookState) {
return state.view === 'child';
}
/**
* 进入资源画布时,卡片布局与首屏预览都要等一会儿,这段时间画布是空的。
* 只要还有资源、但布局未就绪或首屏预览一个都没落地,就显示加载态。
* 预览失败也算落地,避免一直转圈。
*/
export function resourceBookIsLoading({
resourceCount,
overviewReady,
cardsReady,
settledPreviewCount,
}: {
resourceCount: number;
overviewReady: boolean;
cardsReady: boolean;
settledPreviewCount: number;
}) {
if (resourceCount <= 0) return false;
if (!cardsReady || !overviewReady) return true;
return settledPreviewCount <= 0;
}
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest';
import {
initialResourceBookState,
resourceBookIsLoading,
resourceBookReducer,
resourceBookSceneCategory,
resourceBookShowsOverviewCards,
@@ -103,3 +104,30 @@ describe('resource book state machine', () => {
).toEqual(switched);
});
});
describe('resource book loading state', () => {
const ready = {
resourceCount: 3,
overviewReady: true,
cardsReady: true,
settledPreviewCount: 1,
};
it('never blocks an empty project', () => {
expect(resourceBookIsLoading({ ...ready, resourceCount: 0 })).toBe(false);
});
it('loads while the layout or first preview is still pending', () => {
expect(resourceBookIsLoading({ ...ready, cardsReady: false })).toBe(true);
expect(resourceBookIsLoading({ ...ready, overviewReady: false })).toBe(
true,
);
expect(resourceBookIsLoading({ ...ready, settledPreviewCount: 0 })).toBe(
true,
);
});
it('stops once a first preview settles', () => {
expect(resourceBookIsLoading(ready)).toBe(false);
});
});