后台游戏审核页展示买断制泥点价格
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Has been cancelled
Project CI / AI game creator shell Rust crates (pull_request) Has been cancelled
Project CI / Backend tests (pull_request) Has been cancelled
Project CI / Native shell tests (pull_request) Has been cancelled
Project CI / Frontend tests (pull_request) Has been cancelled
Project CI / Repository checks (pull_request) Has been cancelled
Project CI / AI game creator shell web tests (pull_request) Has been cancelled

- adminApiTypes:AdminGameDistributionReviewEntry 与 AdminGameDistributionGameDetail 新增 priceMudPoints: number 字段
- 审核页待审列表新增「价格」列:priceMudPoints 为 0、缺失或旧响应时显示「免费」,否则显示「N 泥点」
- 审核详情资料区新增「价格」项,与列表共用同一格式化口径,避免出现 NaN
- 同步审核表 nth-child 列宽定义:新增价格列 96px,提交时间与审核列顺移,窄屏媒体查询一并覆盖第 6 列
- 补齐 AdminGameDistributionReviewPage 测试:付费 30 泥点、免费与缺字段回落各一条,并在详情用例断言价格展示
This commit is contained in:
2026-10-05 16:47:26 +08:00
parent 7d24b0ac50
commit 4d7cd98dc6
4 changed files with 75 additions and 3 deletions
+4
View File
@@ -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;
}
@@ -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(
<AdminGameDistributionReviewPage
token="admin-token"
onUnauthorized={vi.fn()}
/>,
);
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(
<AdminGameDistributionReviewPage
@@ -298,6 +343,9 @@ test('详情展示发布者和冻结资料,并试玩当前待审版本', async
fireEvent.click(screen.getByRole('button', { name: '详情' }));
expect((await screen.findAllByText(/测试作者/u)).length).toBeGreaterThan(0);
expect(screen.getByText('测试详细介绍')).toBeTruthy();
// 详情资料区展示买断制价格(0 或缺字段按“免费”)。
expect(screen.getByText('价格')).toBeTruthy();
expect(screen.getByText('30 泥点')).toBeTruthy();
fireEvent.click(screen.getByRole('button', { name: '试玩当前待审版本' }));
const frame = await screen.findByTitle('测试游戏 待审版本试玩');
expect(frame.getAttribute('src')).toBe(
@@ -111,6 +111,11 @@ function formatBytes(value: number) {
return `${value} B`;
}
/** 买断制价格口径:0 或缺失(旧响应 / undefined)都按“免费”展示,避免出现 NaN。 */
function formatPriceMudPoints(value: number | null | undefined) {
return typeof value === 'number' && value > 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({
<code>{entry.packageSha256.slice(0, 12)}</code>
</div>
</td>
<td>{formatPriceMudPoints(entry.priceMudPoints)}</td>
<td>{formatAdminDateTime(entry.createdAt)}</td>
<td className="admin-game-review-cell--actions">
<AdminActionRow className="admin-review-actions">
@@ -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),
@@ -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;
}
}