作品详情页在非公开状态下不再请求评价接口,封面显示审核状态
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m2s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m49s
Project CI / Backend tests (pull_request) Successful in 7m16s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / Native shell tests (pull_request) Successful in 6m59s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m40s
Project CI / Repository checks (pull_request) Successful in 5m42s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 3m2s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m16s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 4m49s
Project CI / Backend tests (pull_request) Successful in 7m16s
Project CI / Frontend tests (pull_request) Successful in 3m7s
Project CI / Native shell tests (pull_request) Successful in 6m59s
Project CI / AI game creator shell web tests (pull_request) Successful in 2m40s
Project CI / Repository checks (pull_request) Successful in 5m42s
- useGameReviews 新增第 3 参 publiclyVisible:非公开(game.status !== 'published',含审核中 / 审核未通过 / 未公开 / 已下架)时 loadPage / loadMine / submit 直接 return,不置错误态 - GameReviews 两栏在非公开时渲染既有 PlatformEmptyState「暂无评价」,不出现错误块 / 重试 / 评分失败 - 封面占位文案新增 resolveNonPublicCoverLabel(pending_review → 审核中、rejected → 审核未通过,其余退回作品级文案),样式 .game-detail-cover__pending 落在本域 CSS - hero 评分与分页在非公开时不渲染 - 已公开(published)行为保持不变 - 新增行为测试:非公开时零请求 + 两栏空态 + 封面文案映射;已公开仍照常发请求
This commit is contained in:
@@ -34,6 +34,7 @@ import { GameRatingSummary, GameReviews } from './GameReviews';
|
||||
import { GameVersionHistory } from './GameVersionHistory';
|
||||
import {
|
||||
GAME_STATUS_LABEL,
|
||||
resolveNonPublicCoverLabel,
|
||||
VERSION_STATUS_LABEL,
|
||||
VERSION_STATUS_NEEDS_AUTHOR_ACTION,
|
||||
} from './gameVersionHistoryModel';
|
||||
@@ -285,7 +286,12 @@ function GameDetailContent({
|
||||
onBack: () => void;
|
||||
onPlay: (gameId: string) => void;
|
||||
}) {
|
||||
const reviews = useGameReviews(game.id, canReview);
|
||||
/**
|
||||
* 只有已公开作品才有公开评价/评分投影;审核中、审核未通过、未公开、已下架
|
||||
* 一律不请求评价接口,封面位改为状态文案。
|
||||
*/
|
||||
const publiclyVisible = game.status === 'published';
|
||||
const reviews = useGameReviews(game.id, canReview, publiclyVisible);
|
||||
/** 作者视角的条目没有公开投影 `currentVersion`,可玩版本取版本历史里的已发布版本。 */
|
||||
const publishedVersion =
|
||||
ownerGame?.versions.find((item) => item.status === 'published') ?? null;
|
||||
@@ -296,6 +302,9 @@ function GameDetailContent({
|
||||
: ''
|
||||
: (game.currentVersion?.version ?? '');
|
||||
const latestVersion = ownerView ? (ownerGame?.latestVersion ?? null) : null;
|
||||
const coverPlaceholderLabel = publiclyVisible
|
||||
? ''
|
||||
: resolveNonPublicCoverLabel(game.status, latestVersion?.status);
|
||||
const latestReviewReason = latestVersion?.reviewReason?.trim() ?? '';
|
||||
const mobilePlaybackBlocked = isMobileViewport && !game.deviceSupport.mobile;
|
||||
const playbackBlocked = !version || mobilePlaybackBlocked;
|
||||
@@ -310,10 +319,18 @@ function GameDetailContent({
|
||||
screenshotObjectKeys={game.screenshots}
|
||||
coverColor={game.coverColor}
|
||||
icon={game.icon}
|
||||
renderAsset={(asset) => <GameDetailResolvedAsset {...asset} />}
|
||||
renderAsset={(asset) =>
|
||||
asset.role === 'cover' && coverPlaceholderLabel ? (
|
||||
<span className="game-detail-cover__pending">
|
||||
{coverPlaceholderLabel}
|
||||
</span>
|
||||
) : (
|
||||
<GameDetailResolvedAsset {...asset} />
|
||||
)
|
||||
}
|
||||
controlsLabel={resolveControlsLabel(version, game.inputModes)}
|
||||
versionLabel={versionLabel}
|
||||
rating={<GameRatingSummary state={reviews} />}
|
||||
rating={publiclyVisible ? <GameRatingSummary state={reviews} /> : null}
|
||||
actions={
|
||||
<>
|
||||
<PlatformActionButton
|
||||
@@ -381,7 +398,12 @@ function GameDetailContent({
|
||||
</section>
|
||||
) : null}
|
||||
</GameDetailDisplay>
|
||||
<GameReviews state={reviews} canReview={canReview} onLogin={onLogin} />
|
||||
<GameReviews
|
||||
state={reviews}
|
||||
canReview={canReview}
|
||||
onLogin={onLogin}
|
||||
publiclyVisible={publiclyVisible}
|
||||
/>
|
||||
<p className="game-detail-footnote">
|
||||
<Maximize2 aria-hidden="true" />{' '}
|
||||
游玩时可以切换全屏,游戏运行在独立的安全容器中。
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from '@testing-library/react';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import type { GameDistributionVersionStatus } from '../../../packages/shared/src/contracts/gameDistribution';
|
||||
import type {
|
||||
GameDistributionGame,
|
||||
GameDistributionMyGame,
|
||||
@@ -19,25 +20,31 @@ import type {
|
||||
import { GameDetailPage } from './GameDetailPage';
|
||||
import { GameGalleryPage } from './GameGalleryPage';
|
||||
import { GAME_PLAY_STARTUP_TIMEOUT_MS, GamePlayPage } from './GamePlayPage';
|
||||
import { resolveNonPublicCoverLabel } from './gameVersionHistoryModel';
|
||||
|
||||
const listGamesMock = vi.hoisted(() => vi.fn());
|
||||
const getGameMock = vi.hoisted(() => vi.fn());
|
||||
const getMyGameMock = vi.hoisted(() => vi.fn());
|
||||
const loadVersionRecoveryActionsMock = vi.hoisted(() => vi.fn());
|
||||
const resolvedAssetUrlMock = vi.hoisted(() => vi.fn());
|
||||
const listGameReviewsMock = vi.hoisted(() => vi.fn());
|
||||
const getMyGameReviewMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock('../../services/gameDistributionClient', () => ({
|
||||
listGames: listGamesMock,
|
||||
getGame: getGameMock,
|
||||
getMyGame: getMyGameMock,
|
||||
loadVersionRecoveryActions: loadVersionRecoveryActionsMock,
|
||||
listGameReviews: vi.fn().mockResolvedValue({
|
||||
reviews: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
ratingSummary: { averageScore: null, ratingCount: 0 },
|
||||
listGameReviews: listGameReviewsMock,
|
||||
getMyGameReview: getMyGameReviewMock,
|
||||
}));
|
||||
|
||||
// 作者视角详情页要按登录身份渲染评价区,这里固定一个可访问受保护数据的用户。
|
||||
vi.mock('../auth/AuthUiContext', () => ({
|
||||
useAuthUi: () => ({
|
||||
user: { id: 'author-1' },
|
||||
canAccessProtectedData: true,
|
||||
openLoginModal: vi.fn(),
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -124,6 +131,15 @@ beforeEach(() => {
|
||||
getGameMock.mockResolvedValue(createGame());
|
||||
getMyGameMock.mockResolvedValue(null);
|
||||
loadVersionRecoveryActionsMock.mockResolvedValue({});
|
||||
listGameReviewsMock.mockResolvedValue({
|
||||
reviews: [],
|
||||
page: 1,
|
||||
pageSize: 20,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
ratingSummary: { averageScore: null, ratingCount: 0 },
|
||||
});
|
||||
getMyGameReviewMock.mockResolvedValue({ review: null });
|
||||
resolvedAssetUrlMock.mockImplementation((objectKey: string) =>
|
||||
objectKey ? `https://assets.example.test/${objectKey}` : '',
|
||||
);
|
||||
@@ -293,6 +309,99 @@ describe('GameDetailPage 作者视角', () => {
|
||||
expect(screen.getByText('作品尚未公开,暂时没有在线可玩版本')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('未公开作品的封面显示审核中,且不请求评价与评分接口', async () => {
|
||||
getMyGameMock.mockResolvedValue(createOwnerGame());
|
||||
|
||||
render(
|
||||
<GameDetailPage
|
||||
gameId="game-1"
|
||||
onBack={vi.fn()}
|
||||
onPlay={vi.fn()}
|
||||
ownerView
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('作品状态');
|
||||
|
||||
// 封面位按最新版本状态给出「审核中」,不再只剩手柄占位图。
|
||||
const cover = document.querySelector('.game-detail-cover') as HTMLElement;
|
||||
expect(within(cover).getByText('审核中')).toBeTruthy();
|
||||
// 两栏评价都是空态:没有错误块、没有重试、没有评分加载失败。
|
||||
expect(screen.getAllByText('暂无评价')).toHaveLength(2);
|
||||
expect(screen.queryByText('评分加载失败')).toBeNull();
|
||||
expect(screen.queryByText('评分加载中…')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '重试' })).toBeNull();
|
||||
expect(screen.queryByRole('textbox')).toBeNull();
|
||||
// 代码级门禁:非公开作品一个评价/评分请求都不发。
|
||||
expect(listGameReviewsMock).not.toHaveBeenCalled();
|
||||
expect(getMyGameReviewMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('还没有提交过版本的非公开作品封面显示未公开', async () => {
|
||||
getMyGameMock.mockResolvedValue(
|
||||
createOwnerGame({ latestVersion: null, versions: [] }),
|
||||
);
|
||||
|
||||
render(
|
||||
<GameDetailPage
|
||||
gameId="game-1"
|
||||
onBack={vi.fn()}
|
||||
onPlay={vi.fn()}
|
||||
ownerView
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('还没有提交过版本');
|
||||
expect(
|
||||
within(
|
||||
document.querySelector('.game-detail-cover') as HTMLElement,
|
||||
).getByText('未公开'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('评分加载失败')).toBeNull();
|
||||
expect(listGameReviewsMock).not.toHaveBeenCalled();
|
||||
expect(getMyGameReviewMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each<[string, GameDistributionVersionStatus | null, string]>([
|
||||
['unpublished', null, '未公开'],
|
||||
['unpublished', 'awaiting_upload', '未公开'],
|
||||
['unpublished', 'pending_review', '审核中'],
|
||||
['unpublished', 'rejected', '审核未通过'],
|
||||
['suspended', 'pending_review', '审核中'],
|
||||
['suspended', null, '已被封禁'],
|
||||
])('封面文案 %s + %s -> %s', (gameStatus, versionStatus, expected) => {
|
||||
expect(resolveNonPublicCoverLabel(gameStatus, versionStatus)).toBe(
|
||||
expected,
|
||||
);
|
||||
});
|
||||
|
||||
it('审核未通过的作品封面用更准确的驳回文案', async () => {
|
||||
const rejected: GameDistributionVersionStatusEntry = {
|
||||
...reviewVersion,
|
||||
status: 'rejected',
|
||||
};
|
||||
getMyGameMock.mockResolvedValue(
|
||||
createOwnerGame({ latestVersion: rejected, versions: [rejected] }),
|
||||
);
|
||||
|
||||
render(
|
||||
<GameDetailPage
|
||||
gameId="game-1"
|
||||
onBack={vi.fn()}
|
||||
onPlay={vi.fn()}
|
||||
ownerView
|
||||
/>,
|
||||
);
|
||||
|
||||
await screen.findByText('作品状态');
|
||||
expect(
|
||||
within(
|
||||
document.querySelector('.game-detail-cover') as HTMLElement,
|
||||
).getByText('审核未通过'),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('评分加载失败')).toBeNull();
|
||||
});
|
||||
|
||||
it('以已发布版本作为在线版本,并为未完成版本回读下一步动作', async () => {
|
||||
getMyGameMock.mockResolvedValue(
|
||||
createOwnerGame({
|
||||
@@ -314,6 +423,11 @@ describe('GameDetailPage 作者视角', () => {
|
||||
expect(await screen.findByText('下一步:等待审核结果')).toBeTruthy();
|
||||
expect(loadVersionRecoveryActionsMock).toHaveBeenCalledWith(['version-2']);
|
||||
expect(within(screen.getByRole('list')).getByText('v1')).toBeTruthy();
|
||||
// 已公开作品照旧请求评价与评分,门禁只作用于非公开作品。
|
||||
await waitFor(() =>
|
||||
expect(listGameReviewsMock).toHaveBeenCalledWith('game-1', 1),
|
||||
);
|
||||
expect(getMyGameReviewMock).toHaveBeenCalledWith('game-1');
|
||||
const playButton = screen.getByRole('button', { name: /立即玩/u });
|
||||
expect((playButton as HTMLButtonElement).disabled).toBe(false);
|
||||
});
|
||||
@@ -678,9 +792,7 @@ describe('游戏封面与截图展示', () => {
|
||||
|
||||
await screen.findByRole('heading', { name: '星轨防线' });
|
||||
const heroImage = () =>
|
||||
document
|
||||
.querySelector('.game-detail-cover img')
|
||||
?.getAttribute('src');
|
||||
document.querySelector('.game-detail-cover img')?.getAttribute('src');
|
||||
expect(heroImage()).toBe(
|
||||
'https://assets.example.test/game-distribution/cover/alpha.png',
|
||||
);
|
||||
@@ -725,8 +837,8 @@ describe('游戏封面与截图展示', () => {
|
||||
|
||||
await screen.findByRole('heading', { name: '星轨防线' });
|
||||
expect(document.querySelectorAll('.game-detail-cover img')).toHaveLength(0);
|
||||
expect(
|
||||
document.querySelectorAll('.game-detail-cover__icon'),
|
||||
).toHaveLength(1);
|
||||
expect(document.querySelectorAll('.game-detail-cover__icon')).toHaveLength(
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -84,9 +84,16 @@ function deferred<T>() {
|
||||
});
|
||||
return { promise, resolve, reject };
|
||||
}
|
||||
function Harness() {
|
||||
const state = useGameReviews('game-1', true);
|
||||
return <GameReviews state={state} canReview onLogin={mocks.login} />;
|
||||
function Harness({ publiclyVisible = true }: { publiclyVisible?: boolean }) {
|
||||
const state = useGameReviews('game-1', true, publiclyVisible);
|
||||
return (
|
||||
<GameReviews
|
||||
state={state}
|
||||
canReview
|
||||
onLogin={mocks.login}
|
||||
publiclyVisible={publiclyVisible}
|
||||
/>
|
||||
);
|
||||
}
|
||||
async function form() {
|
||||
return screen.findByRole('textbox', { name: '评论(可选)' });
|
||||
@@ -280,6 +287,21 @@ describe('游戏评价', () => {
|
||||
await screen.findByText('暂无评分');
|
||||
});
|
||||
|
||||
it('非公开作品不请求评价与评分接口,两栏直接是暂无评价', async () => {
|
||||
render(<Harness publiclyVisible={false} />);
|
||||
|
||||
await screen.findByText('全部评价');
|
||||
expect(screen.getAllByText('暂无评价')).toHaveLength(2);
|
||||
expect(screen.queryByText('评分加载中…')).toBeNull();
|
||||
expect(screen.queryByText('评分加载失败')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '重试' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '登录后评价' })).toBeNull();
|
||||
expect(screen.queryByText('正在加载我的评价…')).toBeNull();
|
||||
expect(screen.queryByRole('textbox')).toBeNull();
|
||||
expect(mocks.list).not.toHaveBeenCalled();
|
||||
expect(mocks.mine).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('服务端分页保留总数,修改保留当前页、首次提交回第一页', async () => {
|
||||
mocks.mine.mockResolvedValue({ review: review() });
|
||||
mocks.list.mockImplementation(async (_id, page) =>
|
||||
|
||||
@@ -107,10 +107,16 @@ export function GameReviews({
|
||||
state,
|
||||
canReview,
|
||||
onLogin,
|
||||
publiclyVisible,
|
||||
}: {
|
||||
state: ReviewsState;
|
||||
canReview: boolean;
|
||||
onLogin: () => void;
|
||||
/**
|
||||
* 作品是否已公开。非公开作品没有公开评价投影,两栏一律按空态展示,
|
||||
* 不出现错误块、重试按钮或评分加载失败。
|
||||
*/
|
||||
publiclyVisible: boolean;
|
||||
}) {
|
||||
const length = reviewCommentLength(state.comment);
|
||||
const tooLong = length > GAME_REVIEW_COMMENT_LIMIT;
|
||||
@@ -119,7 +125,9 @@ export function GameReviews({
|
||||
<section className="game-reviews" aria-label="游戏评价">
|
||||
<div className="game-reviews__personal">
|
||||
<h2>我的评价</h2>
|
||||
{!canReview ? (
|
||||
{!publiclyVisible ? (
|
||||
<PlatformEmptyState>暂无评价</PlatformEmptyState>
|
||||
) : !canReview ? (
|
||||
<PlatformActionButton tone="secondary" onClick={onLogin}>
|
||||
登录后评价
|
||||
</PlatformActionButton>
|
||||
@@ -218,9 +226,11 @@ export function GameReviews({
|
||||
<div className="game-reviews__public">
|
||||
<div className="game-reviews__heading">
|
||||
<h2>全部评价</h2>
|
||||
<GameRatingSummary state={state} />
|
||||
{publiclyVisible ? <GameRatingSummary state={state} /> : null}
|
||||
</div>
|
||||
{state.listLoading ? (
|
||||
{!publiclyVisible ? (
|
||||
<PlatformEmptyState>暂无评价</PlatformEmptyState>
|
||||
) : state.listLoading ? (
|
||||
<p role="status">正在加载评价…</p>
|
||||
) : state.listError ? (
|
||||
<ReviewError
|
||||
@@ -236,27 +246,31 @@ export function GameReviews({
|
||||
) : (
|
||||
<PlatformEmptyState>暂无评价</PlatformEmptyState>
|
||||
)}
|
||||
<nav className="game-review-pagination" aria-label="评价分页">
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
disabled={state.page <= 1}
|
||||
onClick={() => void state.loadPage(state.page - 1)}
|
||||
>
|
||||
上一页
|
||||
</PlatformActionButton>
|
||||
<span>
|
||||
{totalPages === 0 ? '0 / 0 页' : `${state.page} / ${totalPages} 页`}
|
||||
</span>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
disabled={state.page >= totalPages}
|
||||
onClick={() => void state.loadPage(state.page + 1)}
|
||||
>
|
||||
下一页
|
||||
</PlatformActionButton>
|
||||
</nav>
|
||||
{publiclyVisible ? (
|
||||
<nav className="game-review-pagination" aria-label="评价分页">
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
disabled={state.page <= 1}
|
||||
onClick={() => void state.loadPage(state.page - 1)}
|
||||
>
|
||||
上一页
|
||||
</PlatformActionButton>
|
||||
<span>
|
||||
{totalPages === 0
|
||||
? '0 / 0 页'
|
||||
: `${state.page} / ${totalPages} 页`}
|
||||
</span>
|
||||
<PlatformActionButton
|
||||
tone="secondary"
|
||||
size="sm"
|
||||
disabled={state.page >= totalPages}
|
||||
onClick={() => void state.loadPage(state.page + 1)}
|
||||
>
|
||||
下一页
|
||||
</PlatformActionButton>
|
||||
</nav>
|
||||
) : null}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -366,6 +366,22 @@
|
||||
font-size: 0.75rem;
|
||||
font-weight: 800;
|
||||
}
|
||||
/*
|
||||
* 非公开作品的封面占位文案:封面素材不可换签时,封面位改为状态说明
|
||||
* (审核中 / 审核未通过 / 未公开 / 已被封禁),压在渐变占位之上。
|
||||
*/
|
||||
.game-detail-cover__pending {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 3;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
background: rgba(22, 21, 28, 0.55);
|
||||
color: #fff5dc;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.16em;
|
||||
}
|
||||
.game-loading-panel {
|
||||
display: grid;
|
||||
min-height: 18rem;
|
||||
|
||||
@@ -23,6 +23,31 @@ export const VERSION_STATUS_LABEL: Record<string, string> = {
|
||||
revoked: '已撤回',
|
||||
};
|
||||
|
||||
/**
|
||||
* 非公开作品在封面位置显示的占位文案。
|
||||
*
|
||||
* 作品没公开时封面素材不可换签(封面位只会剩渐变占位),这里按最新版本状态给出最贴近的
|
||||
* 说明;其余状态退回作品级公开文案,避免在封面上编造审核结论。
|
||||
*/
|
||||
export const NON_PUBLIC_COVER_VERSION_LABEL: Partial<
|
||||
Record<GameDistributionVersionStatus, string>
|
||||
> = {
|
||||
pending_review: '审核中',
|
||||
rejected: '审核未通过',
|
||||
};
|
||||
|
||||
/** 非公开作品的封面占位文案:优先最新版本状态,其次作品公开状态。 */
|
||||
export function resolveNonPublicCoverLabel(
|
||||
gameStatus: string,
|
||||
versionStatus?: GameDistributionVersionStatus | null,
|
||||
): string {
|
||||
return (
|
||||
(versionStatus ? NON_PUBLIC_COVER_VERSION_LABEL[versionStatus] : null) ??
|
||||
GAME_STATUS_LABEL[gameStatus] ??
|
||||
'未公开'
|
||||
);
|
||||
}
|
||||
|
||||
/** 服务端 `recoveryAction` 的中文说明;前端只翻译,不推断状态。 */
|
||||
export const RECOVERY_ACTION_LABEL: Record<
|
||||
GameDistributionRecoveryAction,
|
||||
|
||||
@@ -23,8 +23,17 @@ function errorMessage(error: unknown, fallback: string) {
|
||||
return error instanceof Error ? error.message : fallback;
|
||||
}
|
||||
|
||||
/** 外层以游戏和账号身份为 key,切换上下文会卸载并清空整个评价草稿。 */
|
||||
export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
/**
|
||||
* 外层以游戏和账号身份为 key,切换上下文会卸载并清空整个评价草稿。
|
||||
*
|
||||
* `publiclyVisible` 为 false(审核中 / 审核未通过 / 未公开 / 已下架)时,服务端对评价与
|
||||
* 评分一律按「作品不存在或不可公开访问」拒绝,前端直接不发起任何评价类请求,按空态展示。
|
||||
*/
|
||||
export function useGameReviews(
|
||||
gameId: string,
|
||||
canReview: boolean,
|
||||
publiclyVisible: boolean,
|
||||
) {
|
||||
const [summary, setSummary] = useState<GameDistributionRatingSummary | null>(
|
||||
null,
|
||||
);
|
||||
@@ -32,10 +41,10 @@ export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
null,
|
||||
);
|
||||
const [page, setPage] = useState(1);
|
||||
const [listLoading, setListLoading] = useState(true);
|
||||
const [listLoading, setListLoading] = useState(publiclyVisible);
|
||||
const [listError, setListError] = useState('');
|
||||
const [myReview, setMyReview] = useState<GameDistributionReview | null>(null);
|
||||
const [myLoading, setMyLoading] = useState(canReview);
|
||||
const [myLoading, setMyLoading] = useState(canReview && publiclyVisible);
|
||||
const [myError, setMyError] = useState('');
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [score, setScore] = useState<number | null>(null);
|
||||
@@ -50,6 +59,8 @@ export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
|
||||
const loadPage = useCallback(
|
||||
async (nextPage: number) => {
|
||||
// 非公开作品没有公开评价投影:不请求、不置错误态。
|
||||
if (!publiclyVisible) return;
|
||||
const serial = ++listSerial.current;
|
||||
pageRef.current = nextPage;
|
||||
setPage(nextPage);
|
||||
@@ -68,11 +79,11 @@ export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
setListLoading(false);
|
||||
}
|
||||
},
|
||||
[gameId],
|
||||
[gameId, publiclyVisible],
|
||||
);
|
||||
|
||||
const loadMine = useCallback(async () => {
|
||||
if (!canReview) return;
|
||||
if (!canReview || !publiclyVisible) return;
|
||||
const serial = ++mySerial.current;
|
||||
setMyLoading(true);
|
||||
setMyError('');
|
||||
@@ -86,18 +97,28 @@ export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
} finally {
|
||||
if (alive.current && serial === mySerial.current) setMyLoading(false);
|
||||
}
|
||||
}, [canReview, gameId]);
|
||||
}, [canReview, gameId, publiclyVisible]);
|
||||
|
||||
useEffect(() => {
|
||||
alive.current = true;
|
||||
void loadPage(1);
|
||||
void loadMine();
|
||||
if (!publiclyVisible) {
|
||||
// 非公开作品:不进任何评价/评分请求,按空态起步。
|
||||
listSerial.current += 1;
|
||||
mySerial.current += 1;
|
||||
setListLoading(false);
|
||||
setMyLoading(false);
|
||||
setListError('');
|
||||
setMyError('');
|
||||
} else {
|
||||
void loadPage(1);
|
||||
void loadMine();
|
||||
}
|
||||
return () => {
|
||||
alive.current = false;
|
||||
listSerial.current += 1;
|
||||
mySerial.current += 1;
|
||||
};
|
||||
}, [loadMine, loadPage]);
|
||||
}, [loadMine, loadPage, publiclyVisible]);
|
||||
|
||||
function edit() {
|
||||
if (!myReview || savingRef.current) return;
|
||||
@@ -116,7 +137,14 @@ export function useGameReviews(gameId: string, canReview: boolean) {
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
if (savingRef.current || !canReview || myLoading || myError) return;
|
||||
if (
|
||||
savingRef.current ||
|
||||
!canReview ||
|
||||
!publiclyVisible ||
|
||||
myLoading ||
|
||||
myError
|
||||
)
|
||||
return;
|
||||
const normalizedComment = normalizeReviewNewlines(comment);
|
||||
if (score === null || !Number.isInteger(score) || score < 1 || score > 10) {
|
||||
setSaveError('请选择 1–10 分');
|
||||
|
||||
Reference in New Issue
Block a user