WIP: AGC 资源画布与替换改造 V3.0 #316

Draft
suzmii wants to merge 131 commits from feat/agc-canvas-resource-workbench-v3 into master
4 changed files with 63 additions and 71 deletions
Showing only changes of commit 04ab7ee0da - Show all commits
+4 -8
View File
@@ -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);
});
});