修复在线游玩加载体验:加载期黑屏/空白与发行资源传输开销

- packages/shared 新增共享游戏加载面 PlatformGameLoadingSurface:封面/标题 + 动效进度 + 按时长推进的阶段文案(准备/资源/引擎)+ 慢加载提示,附用例
- 网页游玩页用共享加载面替换近黑壳上的一行小字,加载上限改为共享常量 PLATFORM_GAME_LOADING_TIMEOUT_MS
- 后台审核试玩补加载面、20 秒超时与「重新创建试玩会话」入口,新会话重置加载态;补两条用例
- 发行网关(api-server)对文本类发行资源按 Accept-Encoding 下发 gzip,并下发强 ETag、命中 If-None-Match 返回 304;补 5 条 Rust 用例
- E2E 脚本 check-game-distribution-ops-rollback-e2e 补压缩/Vary/ETag/304/不接受 gzip 四条断言
- 同步 pitfalls、开发运维文档、nginx README 与两份里程碑记录
This commit is contained in:
2026-10-05 17:14:32 +08:00
parent 21c79f54a4
commit 3206f8f49b
19 changed files with 1141 additions and 29 deletions
@@ -1,6 +1,7 @@
/* @vitest-environment jsdom */
import {
act,
fireEvent,
render,
screen,
@@ -9,6 +10,8 @@ import {
} from '@testing-library/react';
import { beforeEach, expect, test, vi } from 'vitest';
import { PLATFORM_GAME_LOADING_TIMEOUT_MS } from '@genarrative/shared/components';
import {
createAdminGameDistributionPreviewSession,
getAdminGameDistributionVersion,
@@ -317,3 +320,99 @@ test('详情展示发布者和冻结资料,并试玩当前待审版本', async
),
);
});
/** 打开详情并点开试玩,返回试玩 iframe。 */
async function openPendingVersionPreview() {
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText(gameTitle);
fireEvent.click(screen.getByRole('button', { name: '详情' }));
await screen.findAllByText(/测试作者/u);
fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' }));
return screen.findByTitle('测试游戏 待审版本试玩');
}
test('试玩期间先给加载面,待审版本真正载入后让位给画面', async () => {
vi.mocked(getAdminGameDistributionVersion).mockResolvedValue(detailResponse);
vi.mocked(createAdminGameDistributionPreviewSession).mockResolvedValue({
previewUrl:
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-1/',
expiresAt: '2026-10-02T12:10:00Z',
versionId: 'version-1',
});
const frame = await openPendingVersionPreview();
// 加载期不能只留一片空白:审核员要看到正在加载哪个游戏。
const loadingSurface = screen.getByRole('status');
expect(loadingSurface.textContent).toContain('测试游戏');
expect(
loadingSurface.classList.contains('platform-game-loading--panel'),
).toBe(true);
fireEvent.load(frame);
await waitFor(() => expect(screen.queryByRole('status')).toBeNull());
});
test('试玩加载超过上限后给出超时说明与重建会话入口', async () => {
vi.mocked(getAdminGameDistributionVersion).mockResolvedValue(detailResponse);
vi.mocked(createAdminGameDistributionPreviewSession)
.mockResolvedValueOnce({
previewUrl:
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-1/',
expiresAt: '2026-10-02T12:10:00Z',
versionId: 'version-1',
})
.mockResolvedValueOnce({
previewUrl:
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-2/',
expiresAt: '2026-10-02T12:20:00Z',
versionId: 'version-1',
});
render(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
await screen.findByText(gameTitle);
fireEvent.click(screen.getByRole('button', { name: '详情' }));
await screen.findAllByText(/测试作者/u);
// 试玩会话与超时定时器都必须落在假定时器窗口内,才能按上限推进等待。
vi.useFakeTimers();
try {
fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' }));
await act(async () => {
await Promise.resolve();
});
expect(screen.getByTitle('测试游戏 待审版本试玩')).toBeTruthy();
expect(screen.queryByText('试玩版本加载超时')).toBeNull();
await act(async () => {
vi.advanceTimersByTime(PLATFORM_GAME_LOADING_TIMEOUT_MS);
});
expect(screen.getByRole('status').textContent).toContain(
'试玩版本加载超时',
);
fireEvent.click(screen.getByRole('button', { name: '重新创建试玩会话' }));
await act(async () => {
await Promise.resolve();
});
expect(createAdminGameDistributionPreviewSession).toHaveBeenCalledTimes(2);
// 新会话必须回到加载态,不能沿用上一个会话的「已就绪」或超时结论。
const rebuilt = screen.getByTitle('测试游戏 待审版本试玩');
expect(rebuilt.getAttribute('src')).toBe(
'http://127.0.0.1:8082/api/game-distribution/admin-previews/preview-2/',
);
expect(screen.queryByText('试玩版本加载超时')).toBeNull();
} finally {
vi.useRealTimers();
}
});
@@ -10,6 +10,8 @@ import {
formatAdminDateTime,
GameDetailDisplay,
Modal,
PLATFORM_GAME_LOADING_TIMEOUT_MS,
PlatformGameLoadingSurface,
TextField,
} from '@genarrative/shared/components';
import { RefreshCcw } from 'lucide-react';
@@ -591,6 +593,20 @@ function AdminGameDistributionReviewDetailView({
onReject: () => void;
onApprove: () => void;
}) {
// 试玩会话是按 version 现建的:新会话必须回到加载态,不能沿用上一个会话的 ready。
const [isPreviewFrameReady, setIsPreviewFrameReady] = useState(false);
const [isPreviewTimedOut, setIsPreviewTimedOut] = useState(false);
useEffect(() => {
setIsPreviewFrameReady(false);
setIsPreviewTimedOut(false);
if (!previewUrl) return undefined;
const timer = window.setTimeout(() => {
setIsPreviewTimedOut(true);
}, PLATFORM_GAME_LOADING_TIMEOUT_MS);
return () => window.clearTimeout(timer);
}, [previewUrl]);
if (!detail) {
return (
<AdminPage wide>
@@ -606,6 +622,10 @@ function AdminGameDistributionReviewDetailView({
);
}
const coverUrl = detail.game.coverObjectKey
? assetUrls[detail.game.coverObjectKey]
: '';
return (
<AdminPage wide>
<AdminPageHeading
@@ -682,13 +702,45 @@ function AdminGameDistributionReviewDetailView({
<AdminAlert tone="warning">{previewError}</AdminAlert>
) : null}
{previewUrl ? (
<iframe
title={`${detail.game.title} 待审版本试玩`}
src={previewUrl}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
className="admin-game-review-preview"
/>
<div className="admin-game-review-preview-shell">
{isPreviewFrameReady ? null : (
<PlatformGameLoadingSurface
tone="panel"
title={detail.game.title}
cover={
coverUrl ? (
<img alt={`${detail.game.title} 封面`} src={coverUrl} />
) : null
}
message={isPreviewTimedOut ? '试玩版本加载超时' : undefined}
hint={
isPreviewTimedOut
? '可以重新创建试玩会话,或稍后再试。'
: undefined
}
/>
)}
<iframe
title={`${detail.game.title} 待审版本试玩`}
src={previewUrl}
sandbox="allow-scripts"
referrerPolicy="no-referrer"
className="admin-game-review-preview"
onLoad={() => setIsPreviewFrameReady(true)}
/>
</div>
) : null}
{previewUrl && isPreviewTimedOut && !isPreviewFrameReady ? (
<AdminActionRow className="admin-review-actions">
<AdminButton
variant="secondary"
type="button"
disabled={isPreviewLoading}
onClick={onPreview}
>
{isPreviewLoading ? '准备试玩…' : '重新创建试玩会话'}
</AdminButton>
</AdminActionRow>
) : null}
</section>
</GameDetailDisplay>
+13 -1
View File
@@ -3590,16 +3590,28 @@ button:disabled {
color: var(--platform-text-strong);
}
.admin-game-review-preview-shell {
/* 加载面(PlatformGameLoadingSurface)按绝对定位铺在预览区上,外壳必须是定位祖先。 */
position: relative;
margin-top: 1rem;
min-height: 42rem;
overflow: hidden;
border-radius: 1rem;
}
.admin-game-review-preview {
display: block;
width: 100%;
min-height: 42rem;
margin-top: 1rem;
border: 1px solid var(--platform-surface-border);
border-radius: 1rem;
background: #fff;
}
.admin-game-review-preview-shell .platform-game-loading {
border-radius: 1rem;
}
/*
* 游戏管理列表(AdminGameManagementPage)专属排布,选择器全部框在本页表格内,
* 不影响其它后台页面: