后台游戏审核页展示游戏名与作者,不再只显示 gameId
- AdminGameDistributionReviewPage.tsx:待审列表只回版本字段,新增 resolveGameMeta 用既有后台作品列表接口按 gameId 反查标题/作者,逐游戏去重并按会话缓存;单个游戏取不到只回落显示 gameId,不阻断审核列表 - 同文件:loadReviews 在拿到 entries 后并行补齐本页 distinct gameId 的元数据 - 同文件:游戏列由单个 gameId 文本改为「游戏名(主行)+ 作者名与 user_id(副行,截断显示,完整值放 title)」 - adminGameDistributionReview.css:以 .admin-game-review-game 网格布局替换 .admin-game-review-game-id,保留窄视口截断与 title 出口 - AdminGameDistributionReviewPage.test.tsx:补 listAdminGameDistributionGames 的 mock 与游戏列表 fixture,断言由 gameId 改为游戏名
This commit is contained in:
@@ -12,10 +12,12 @@ import { beforeEach, expect, test, vi } from 'vitest';
|
||||
import {
|
||||
createAdminGameDistributionPreviewSession,
|
||||
getAdminGameDistributionVersion,
|
||||
listAdminGameDistributionGames,
|
||||
listAdminGameDistributionReviews,
|
||||
reviewAdminGameDistributionVersion,
|
||||
} from '../api/adminApiClient';
|
||||
import type {
|
||||
AdminGameDistributionGameListResponse,
|
||||
AdminGameDistributionReviewEntry,
|
||||
AdminGameDistributionVersionDetailResponse,
|
||||
} from '../api/adminApiTypes';
|
||||
@@ -34,6 +36,7 @@ vi.mock('../api/adminApiClient', () => ({
|
||||
),
|
||||
createAdminGameDistributionPreviewSession: vi.fn(),
|
||||
getAdminGameDistributionVersion: vi.fn(),
|
||||
listAdminGameDistributionGames: vi.fn(),
|
||||
listAdminGameDistributionReviews: vi.fn(),
|
||||
reviewAdminGameDistributionVersion: vi.fn(),
|
||||
}));
|
||||
@@ -51,6 +54,28 @@ const entry: AdminGameDistributionReviewEntry = {
|
||||
updatedAt: '1791008678.817257Z',
|
||||
};
|
||||
|
||||
/** 游戏名与作者由既有后台作品列表接口按 gameId 反查(待审列表本身不含这两个字段)。 */
|
||||
const gameTitle = '测试游戏名';
|
||||
const gameListResponse: AdminGameDistributionGameListResponse = {
|
||||
games: [
|
||||
{
|
||||
gameId: 'game_1',
|
||||
title: gameTitle,
|
||||
author: { id: 'user-9f1', name: '136****8867', avatarUrl: null },
|
||||
status: 'unpublished',
|
||||
versionCount: 1,
|
||||
playCount: 0,
|
||||
activeVersionId: null,
|
||||
publicationRevision: 4,
|
||||
createdAt: '2026-09-20T08:00:00Z',
|
||||
updatedAt: '2026-09-20T08:00:00Z',
|
||||
deletedAt: null,
|
||||
versions: [],
|
||||
},
|
||||
],
|
||||
nextCursor: null,
|
||||
};
|
||||
|
||||
const detailResponse: AdminGameDistributionVersionDetailResponse = {
|
||||
game: {
|
||||
id: 'game_1',
|
||||
@@ -81,6 +106,7 @@ const detailResponse: AdminGameDistributionVersionDetailResponse = {
|
||||
|
||||
beforeEach(() => {
|
||||
vi.mocked(listAdminGameDistributionReviews).mockReset();
|
||||
vi.mocked(listAdminGameDistributionGames).mockReset();
|
||||
vi.mocked(reviewAdminGameDistributionVersion).mockReset();
|
||||
vi.mocked(getAdminGameDistributionVersion).mockReset();
|
||||
vi.mocked(createAdminGameDistributionPreviewSession).mockReset();
|
||||
@@ -88,6 +114,7 @@ beforeEach(() => {
|
||||
entries: [entry],
|
||||
nextCursor: null,
|
||||
});
|
||||
vi.mocked(listAdminGameDistributionGames).mockResolvedValue(gameListResponse);
|
||||
});
|
||||
|
||||
test('通过审核只提交当前 publicationRevision 并刷新列表', async () => {
|
||||
@@ -102,7 +129,7 @@ test('通过审核只提交当前 publicationRevision 并刷新列表', async ()
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
expect(screen.queryByLabelText('发行入口')).toBeNull();
|
||||
expect(screen.queryByText('通过后由系统分配发行地址')).toBeNull();
|
||||
@@ -141,7 +168,7 @@ test('点击拒绝后填写理由再提交审核接口', async () => {
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
@@ -176,7 +203,7 @@ test('待审版本行内只保留版本级审核动作,不提供游戏级安
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
// 「安全下架」是游戏级动作(强制下线线上版本),入口在「游戏管理」页;
|
||||
// 审核一个新版本时不该出现该动作。
|
||||
@@ -197,9 +224,9 @@ test('提交时间列把后端的 seconds.microsZ 渲染成可读本地时间',
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
const row = screen.getByText('game_1').closest('tr');
|
||||
const row = screen.getByText(gameTitle).closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
const submittedCell = within(row as HTMLElement).getAllByRole('cell')[3];
|
||||
// 解析不出来时会回落到 `-`,这正是本次修复前的线上表现。
|
||||
@@ -216,7 +243,7 @@ test('取消理由输入时不做审核操作', async () => {
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '拒绝' }));
|
||||
const dialog = await screen.findByRole('dialog');
|
||||
@@ -226,17 +253,29 @@ test('取消理由输入时不做审核操作', async () => {
|
||||
expect(reviewAdminGameDistributionVersion).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('窄视口下 gameId 截断显示,完整值由 title 暴露', async () => {
|
||||
test('游戏列显示游戏名与作者,不再把 gameId 当主展示', async () => {
|
||||
render(
|
||||
<AdminGameDistributionReviewPage
|
||||
token="admin-token"
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
const gameIdCell = screen.getByTitle('game_1');
|
||||
expect(gameIdCell.textContent).toBe('game_1');
|
||||
// 按 gameId 反查一次,第二次刷新走缓存(本次只加载一次列表)。
|
||||
expect(listAdminGameDistributionGames).toHaveBeenCalledWith(
|
||||
'admin-token',
|
||||
expect.objectContaining({ keyword: 'game_1' }),
|
||||
);
|
||||
|
||||
const row = screen.getByText(gameTitle).closest('tr');
|
||||
expect(row).not.toBeNull();
|
||||
const gameCell = within(row as HTMLElement).getAllByRole('cell')[0];
|
||||
// 主展示是游戏名 + 打码作者名;gameId 只留在悬浮标题里。
|
||||
expect(gameCell?.textContent).toContain('136****8867');
|
||||
expect(gameCell?.textContent).not.toContain('game_1');
|
||||
expect(screen.getByTitle('user-9f1').textContent).toBe('user-9f1');
|
||||
expect(gameCell?.querySelector('[title="game_1"]')).not.toBeNull();
|
||||
});
|
||||
|
||||
test('详情展示发布者和冻结资料,并试玩当前待审版本', async () => {
|
||||
@@ -254,7 +293,7 @@ test('详情展示发布者和冻结资料,并试玩当前待审版本', async
|
||||
onUnauthorized={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
await screen.findByText('game_1');
|
||||
await screen.findByText(gameTitle);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '详情' }));
|
||||
expect((await screen.findAllByText(/测试作者/u)).length).toBeGreaterThan(0);
|
||||
|
||||
@@ -13,12 +13,13 @@ import {
|
||||
TextField,
|
||||
} from '@genarrative/shared/components';
|
||||
import { RefreshCcw } from 'lucide-react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
||||
import {
|
||||
createAdminGameDistributionPreviewSession,
|
||||
getAdminAssetReadUrl,
|
||||
getAdminGameDistributionVersion,
|
||||
listAdminGameDistributionGames,
|
||||
listAdminGameDistributionReviews,
|
||||
reviewAdminGameDistributionVersion,
|
||||
} from '../api/adminApiClient';
|
||||
@@ -38,6 +39,13 @@ interface ReviewReasonPrompt {
|
||||
entry: AdminGameDistributionReviewEntry;
|
||||
}
|
||||
|
||||
/** 待审列表要展示的游戏名与作者:由既有后台作品列表接口按 gameId 反查得到。 */
|
||||
interface ReviewGameMeta {
|
||||
title: string;
|
||||
authorName: string;
|
||||
authorId: string;
|
||||
}
|
||||
|
||||
function applyFrozenGameMetadata(
|
||||
game: AdminGameDistributionGameDetail,
|
||||
metadata: Record<string, unknown> | null,
|
||||
@@ -137,6 +145,56 @@ export function AdminGameDistributionReviewPage({
|
||||
const [previewError, setPreviewError] = useState('');
|
||||
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
|
||||
const [assetUrls, setAssetUrls] = useState<Record<string, string>>({});
|
||||
const [gameMetaByGameId, setGameMetaByGameId] = useState<
|
||||
Record<string, ReviewGameMeta>
|
||||
>({});
|
||||
// 同一 gameId 的标题/作者在会话内不变,缓存避免每次刷新重复反查。
|
||||
const gameMetaCacheRef = useRef<Record<string, ReviewGameMeta>>({});
|
||||
|
||||
/**
|
||||
* 待审列表只回版本字段(无游戏名/作者),这里用既有后台作品列表接口按 gameId 反查,
|
||||
* 逐游戏一次请求并去重;单个游戏取不到只回落显示 gameId,不阻断审核列表。
|
||||
*/
|
||||
const resolveGameMeta = useCallback(
|
||||
async (gameIds: string[]) => {
|
||||
const resolved = await Promise.all(
|
||||
gameIds.map(async (gameId) => {
|
||||
const cached = gameMetaCacheRef.current[gameId];
|
||||
if (cached) {
|
||||
return [gameId, cached] as const;
|
||||
}
|
||||
try {
|
||||
const response = await listAdminGameDistributionGames(token, {
|
||||
limit: 5,
|
||||
keyword: gameId,
|
||||
});
|
||||
const matched = response.games.find(
|
||||
(game) => game.gameId === gameId,
|
||||
);
|
||||
if (!matched) return null;
|
||||
const meta: ReviewGameMeta = {
|
||||
title: matched.title?.trim() || matched.gameId,
|
||||
authorName: matched.author?.name?.trim() || '',
|
||||
authorId: matched.author?.id?.trim() || '',
|
||||
};
|
||||
gameMetaCacheRef.current[gameId] = meta;
|
||||
return [gameId, meta] as const;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
);
|
||||
const merged = Object.fromEntries(
|
||||
resolved.filter(
|
||||
(item): item is readonly [string, ReviewGameMeta] => item !== null,
|
||||
),
|
||||
);
|
||||
if (Object.keys(merged).length > 0) {
|
||||
setGameMetaByGameId((current) => ({ ...current, ...merged }));
|
||||
}
|
||||
},
|
||||
[token],
|
||||
);
|
||||
|
||||
const loadReviews = useCallback(async () => {
|
||||
setIsLoading(true);
|
||||
@@ -144,12 +202,15 @@ export function AdminGameDistributionReviewPage({
|
||||
try {
|
||||
const response = await listAdminGameDistributionReviews(token);
|
||||
setEntries(response.entries);
|
||||
await resolveGameMeta([
|
||||
...new Set(response.entries.map((entry) => entry.gameId)),
|
||||
]);
|
||||
} catch (error) {
|
||||
handlePageError(error, onUnauthorized, setErrorMessage);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [token, onUnauthorized]);
|
||||
}, [token, onUnauthorized, resolveGameMeta]);
|
||||
|
||||
useEffect(() => {
|
||||
void loadReviews();
|
||||
@@ -381,15 +442,32 @@ export function AdminGameDistributionReviewPage({
|
||||
]}
|
||||
renderRow={(entry) => {
|
||||
const busy = busyVersionId === entry.versionId;
|
||||
const gameMeta = gameMetaByGameId[entry.gameId];
|
||||
return (
|
||||
<tr key={entry.versionId}>
|
||||
<td>
|
||||
<code
|
||||
className="admin-game-review-game-id"
|
||||
title={entry.gameId}
|
||||
>
|
||||
{entry.gameId}
|
||||
</code>
|
||||
{/* gameId 不再作为主展示,只留在悬浮标题里做兜底标识。 */}
|
||||
<div className="admin-game-review-game" title={entry.gameId}>
|
||||
<span className="admin-table-cell-ellipsis">
|
||||
{gameMeta?.title ?? entry.gameId}
|
||||
</span>
|
||||
<small className="admin-game-review-game-author admin-muted-text">
|
||||
<span
|
||||
className="admin-game-review-game-author-name"
|
||||
title={gameMeta?.authorName || undefined}
|
||||
>
|
||||
{gameMeta?.authorName || '—'}
|
||||
</span>
|
||||
{gameMeta?.authorId ? (
|
||||
<span
|
||||
className="admin-game-review-game-author-id"
|
||||
title={gameMeta.authorId}
|
||||
>
|
||||
{gameMeta.authorId}
|
||||
</span>
|
||||
) : null}
|
||||
</small>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
v{entry.versionNumber}
|
||||
|
||||
@@ -56,22 +56,35 @@ table.admin-game-review-table td .admin-muted-text {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
/* gameId 是 37 字符的定长标识:窄视口截断显示,靠 title 暴露完整值。 */
|
||||
.admin-game-review-game-id {
|
||||
display: block;
|
||||
/* 游戏列:主行游戏名、副行作者(打码名 + user_id 截断,完整值放 title)。 */
|
||||
.admin-game-review-game {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.admin-game-review-game-author {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: baseline;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.admin-game-review-game-author-name {
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/*
|
||||
* 窄视口(≤1366)下把 gameId 降到 12px:13px 时它的文本宽 259px 只比列宽多 1px 的余量,
|
||||
* 字体回退或换行差异都会让它变成省略号;降到 12px 留出约 19px 余量,完整标识仍然可见。
|
||||
*/
|
||||
@media (max-width: 1366px) {
|
||||
table.admin-game-review-table td:first-child code {
|
||||
font-size: 12px;
|
||||
}
|
||||
.admin-game-review-game-author-id {
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
/* 移动端/窄屏:动作列保持不换行,仍靠省略号吸收 gameId 的宽度。 */
|
||||
|
||||
Reference in New Issue
Block a user