WIP: AGC 资源画布与替换改造 V3.0 #316
@@ -5634,18 +5634,14 @@ iframe.preview-frame {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.game-resource-book-loading {
|
||||
.game-resource-book-thumbnail-loading {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
inset: 42px 0 0;
|
||||
z-index: 3;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
background: rgb(255 253 250 / 78%);
|
||||
color: #a08073;
|
||||
font-size: 13px;
|
||||
color: #c46a40;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
|
||||
@@ -912,6 +912,7 @@ function ResourceBookThumbnail({
|
||||
resources,
|
||||
onOpen,
|
||||
registerElement,
|
||||
loading = false,
|
||||
}: {
|
||||
category: ResourceCategory;
|
||||
resources: ProjectResource[];
|
||||
@@ -920,6 +921,7 @@ function ResourceBookThumbnail({
|
||||
category: ResourceCategory,
|
||||
element: HTMLElement | null,
|
||||
) => void;
|
||||
loading?: boolean;
|
||||
}) {
|
||||
const stacks = Array.from(
|
||||
resources.reduce((groups, resource) => {
|
||||
@@ -978,6 +980,15 @@ function ResourceBookThumbnail({
|
||||
<span>打开{categoryLabels[category]}</span>
|
||||
<span aria-hidden="true">↗</span>
|
||||
</span>
|
||||
{loading ? (
|
||||
<span
|
||||
className="game-resource-book-thumbnail-loading"
|
||||
role="status"
|
||||
aria-label="正在加载该栏目资源"
|
||||
>
|
||||
<Loader2 size={18} aria-hidden="true" className="animate-spin" />
|
||||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -2191,36 +2202,40 @@ 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;
|
||||
// 每个功能画布框内各自判断加载态:该栏目一张预览都没落地时转圈。
|
||||
const resourceBookCategorySettledPreviews = useMemo(() => {
|
||||
const counts = new Map<
|
||||
ResourceCategory,
|
||||
{ previewable: number; settled: number }
|
||||
>();
|
||||
for (const resource of visibleResources) {
|
||||
const kind = projectResourceCardPreviewKind(resource);
|
||||
// 版本、占位和音频卡片本来就不请求卡片预览,不能计入加载等待。
|
||||
if (kind === 'version' || kind === 'placeholder' || kind === 'audio') {
|
||||
continue;
|
||||
}
|
||||
const entry = counts.get(resource.category) ?? {
|
||||
previewable: 0,
|
||||
settled: 0,
|
||||
};
|
||||
entry.previewable += 1;
|
||||
const identity = resourceCardPreviews.identityByResourceId.get(
|
||||
resource.id,
|
||||
);
|
||||
if (!identity) continue;
|
||||
const status = resourceCardPreviews.previews.get(identity)?.status;
|
||||
if (status === 'loaded' || status === 'failed') settled += 1;
|
||||
const status = identity
|
||||
? resourceCardPreviews.previews.get(identity)?.status
|
||||
: undefined;
|
||||
if (status === 'loaded' || status === 'failed') {
|
||||
entry.settled += 1;
|
||||
}
|
||||
counts.set(resource.category, entry);
|
||||
}
|
||||
return settled;
|
||||
return counts;
|
||||
}, [
|
||||
resourceCardPreviews.identityByResourceId,
|
||||
resourceCardPreviews.previews,
|
||||
visibleResources,
|
||||
]);
|
||||
const resourceBookLoading = resourceBookIsLoading({
|
||||
resourceCount: visibleResources.length,
|
||||
overviewReady: resourceBookOverviewReady,
|
||||
cardsReady: sortMode !== 'dependency' || resourceGraphInitializationReady,
|
||||
settledPreviewCount: resourceBookSettledPreviewCount,
|
||||
});
|
||||
const setResourceCanvasViewport = useCallback(
|
||||
(
|
||||
category: ResourceCategory,
|
||||
@@ -5622,20 +5637,6 @@ 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"
|
||||
@@ -5778,6 +5779,15 @@ export default function ProjectDevelopmentView({
|
||||
}
|
||||
onOpen={openResourceBookChild}
|
||||
registerElement={registerResourceBookThumbnail}
|
||||
loading={resourceBookIsLoading({
|
||||
resourceCount:
|
||||
resourceBookCategorySettledPreviews.get(category)
|
||||
?.previewable ?? 0,
|
||||
settledPreviewCount:
|
||||
resourceBookCategorySettledPreviews.get(
|
||||
category,
|
||||
)?.settled ?? 0,
|
||||
})}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -80,22 +80,15 @@ export function resourceBookShowsChildCards(state: ResourceBookState) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入资源画布时,卡片布局与首屏预览都要等一会儿,这段时间画布是空的。
|
||||
* 只要还有资源、但布局未就绪或首屏预览一个都没落地,就显示加载态。
|
||||
* 预览失败也算落地,避免一直转圈。
|
||||
* 每个功能画布框内各自显示加载态:该栏目还有资源、但一张预览都没落地时转圈。
|
||||
* 预览失败同样算落地,避免一直转圈;加载态不拦截点击,没加载出来也能进栏目。
|
||||
*/
|
||||
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;
|
||||
return resourceCount > 0 && settledPreviewCount <= 0;
|
||||
}
|
||||
|
||||
@@ -106,28 +106,21 @@ describe('resource book state machine', () => {
|
||||
});
|
||||
|
||||
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('never spins for an empty section', () => {
|
||||
expect(
|
||||
resourceBookIsLoading({ resourceCount: 0, settledPreviewCount: 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('spins while a section still has no settled preview', () => {
|
||||
expect(
|
||||
resourceBookIsLoading({ resourceCount: 3, settledPreviewCount: 0 }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('stops once a first preview settles', () => {
|
||||
expect(resourceBookIsLoading(ready)).toBe(false);
|
||||
it('stops as soon as one preview in that section settles', () => {
|
||||
expect(
|
||||
resourceBookIsLoading({ resourceCount: 3, settledPreviewCount: 1 }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user