diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 8fecfe485..91544e8cc 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -1138,6 +1138,8 @@ export interface AdminGameDistributionReviewEntry { packageBytes: number; status: string; publicationRevision: number; + /** 买断制泥点价格;0 表示免费。旧响应可能缺该字段。 */ + priceMudPoints: number; reviewReason: string | null; createdAt: string; updatedAt: string; @@ -1166,6 +1168,8 @@ export interface AdminGameDistributionGameDetail { orientation: string; status: string; publicationRevision: number; + /** 买断制泥点价格;0 表示免费。旧响应可能缺该字段。 */ + priceMudPoints: number; playCount: number; createdAt: string; } diff --git a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx index 3ca7ab6be..b3d0be77c 100644 --- a/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx +++ b/apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsx @@ -48,6 +48,7 @@ const entry: AdminGameDistributionReviewEntry = { packageBytes: 2048, status: 'pending_review', publicationRevision: 4, + priceMudPoints: 30, reviewReason: null, // 真实后台下发的是 `seconds.microsZ`(非 ISO 8601),见 shared-kernel `format_timestamp_micros`。 createdAt: '1791008678.817257Z', @@ -92,6 +93,7 @@ const detailResponse: AdminGameDistributionVersionDetailResponse = { orientation: 'landscape', status: 'unpublished', publicationRevision: 7, + priceMudPoints: 30, playCount: 0, createdAt: '2026-09-20T08:00:00Z', }, @@ -228,7 +230,8 @@ test('提交时间列把后端的 seconds.microsZ 渲染成可读本地时间', const row = screen.getByText(gameTitle).closest('tr'); expect(row).not.toBeNull(); - const submittedCell = within(row as HTMLElement).getAllByRole('cell')[3]; + // 列序:游戏 / 版本 / 发行包 / 价格 / 提交时间 / 审核。 + const submittedCell = within(row as HTMLElement).getAllByRole('cell')[4]; // 解析不出来时会回落到 `-`,这正是本次修复前的线上表现。 expect(submittedCell?.textContent).not.toBe('-'); expect(submittedCell?.textContent).toMatch( @@ -236,6 +239,48 @@ test('提交时间列把后端的 seconds.microsZ 渲染成可读本地时间', ); }); +test('价格列按买断制口径展示,免费与缺失价格都不显示 NaN', async () => { + const freeEntry: AdminGameDistributionReviewEntry = { + ...entry, + versionId: 'version-2', + gameId: 'game_2', + versionNumber: 1, + priceMudPoints: 0, + }; + // 旧响应可能完全没有该字段;列表必须回落到“免费”而不是 NaN。 + const legacyEntry = { + ...entry, + versionId: 'version-3', + gameId: 'game_3', + versionNumber: 3, + priceMudPoints: undefined, + } as unknown as AdminGameDistributionReviewEntry; + vi.mocked(listAdminGameDistributionReviews).mockResolvedValue({ + entries: [entry, freeEntry, legacyEntry], + nextCursor: null, + }); + + render( + , + ); + await screen.findByText(gameTitle); + + const cellAt = (text: string) => { + const row = screen.getByText(text).closest('tr'); + expect(row).not.toBeNull(); + // 列序:游戏 / 版本 / 发行包 / 价格 / 提交时间 / 审核。 + return within(row as HTMLElement).getAllByRole('cell')[3]?.textContent; + }; + + expect(cellAt(gameTitle)).toBe('30 泥点'); + expect(cellAt('game_2')).toBe('免费'); + expect(cellAt('game_3')).toBe('免费'); + expect(screen.queryByText(/NaN/u)).toBeNull(); +}); + test('取消理由输入时不做审核操作', async () => { render( 0 ? `${value} 泥点` : '免费'; +} + function createReviewIdempotencyKey(versionId: string) { const random = typeof crypto !== 'undefined' && 'randomUUID' in crypto @@ -437,6 +442,7 @@ export function AdminGameDistributionReviewPage({ { key: 'gameId', label: '游戏' }, { key: 'versionNumber', label: '版本' }, { key: 'packageBytes', label: '发行包' }, + { key: 'priceMudPoints', label: '价格' }, { key: 'createdAt', label: '提交时间' }, { key: 'actions', label: '审核' }, ]} @@ -482,6 +488,7 @@ export function AdminGameDistributionReviewPage({ {entry.packageSha256.slice(0, 12)} + {formatPriceMudPoints(entry.priceMudPoints)} {formatAdminDateTime(entry.createdAt)} @@ -629,6 +636,10 @@ function AdminGameDistributionReviewDetailView({ versionLabel={`v${detail.version.versionNumber}`} infoCards={[ { label: '状态', value: detail.version.status }, + { + label: '价格', + value: formatPriceMudPoints(detail.game.priceMudPoints), + }, { label: '提交时间', value: formatAdminDateTime(detail.version.createdAt), diff --git a/apps/admin-web/src/pages/adminGameDistributionReview.css b/apps/admin-web/src/pages/adminGameDistributionReview.css index 2eed66619..28e6ef8cd 100644 --- a/apps/admin-web/src/pages/adminGameDistributionReview.css +++ b/apps/admin-web/src/pages/adminGameDistributionReview.css @@ -27,14 +27,21 @@ table.admin-game-review-table td:nth-child(3) { width: 124px; } +/* 价格列:只展示「免费」或「N 泥点」,窄列即可。 */ table.admin-game-review-table th:nth-child(4), table.admin-game-review-table td:nth-child(4) { - width: 172px; + width: 96px; white-space: nowrap; } table.admin-game-review-table th:nth-child(5), table.admin-game-review-table td:nth-child(5) { + width: 172px; + white-space: nowrap; +} + +table.admin-game-review-table th:nth-child(6), +table.admin-game-review-table td:nth-child(6) { width: 240px; } @@ -100,7 +107,9 @@ table.admin-game-review-table td .admin-muted-text { table.admin-game-review-table th:nth-child(4), table.admin-game-review-table td:nth-child(4), table.admin-game-review-table th:nth-child(5), - table.admin-game-review-table td:nth-child(5) { + table.admin-game-review-table td:nth-child(5), + table.admin-game-review-table th:nth-child(6), + table.admin-game-review-table td:nth-child(6) { width: auto; } }