From aad8eb2abac57f30565356a2ca17bfe5f629cd3d Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sun, 4 Oct 2026 12:34:42 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BD=9C=E5=93=81=E8=AF=A6=E6=83=85=E9=A1=B5?= =?UTF-8?q?=E5=9C=A8=E9=9D=9E=E5=85=AC=E5=BC=80=E7=8A=B6=E6=80=81=E4=B8=8B?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E8=AF=B7=E6=B1=82=E8=AF=84=E4=BB=B7=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=EF=BC=8C=E5=B0=81=E9=9D=A2=E6=98=BE=E7=A4=BA=E5=AE=A1?= =?UTF-8?q?=E6=A0=B8=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useGameReviews 新增第 3 参 publiclyVisible:非公开(game.status !== 'published',含审核中 / 审核未通过 / 未公开 / 已下架)时 loadPage / loadMine / submit 直接 return,不置错误态 - GameReviews 两栏在非公开时渲染既有 PlatformEmptyState「暂无评价」,不出现错误块 / 重试 / 评分失败 - 封面占位文案新增 resolveNonPublicCoverLabel(pending_review → 审核中、rejected → 审核未通过,其余退回作品级文案),样式 .game-detail-cover__pending 落在本域 CSS - hero 评分与分页在非公开时不渲染 - 已公开(published)行为保持不变 - 新增行为测试:非公开时零请求 + 两栏空态 + 封面文案映射;已公开仍照常发请求 --- .../game-distribution/GameDetailPage.tsx | 30 +++- .../GameDistributionPages.test.tsx | 138 ++++++++++++++++-- .../game-distribution/GameReviews.test.tsx | 28 +++- .../game-distribution/GameReviews.tsx | 62 +++++--- .../game-distribution/gameDistribution.css | 16 ++ .../gameVersionHistoryModel.ts | 25 ++++ .../game-distribution/useGameReviews.ts | 50 +++++-- 7 files changed, 294 insertions(+), 55 deletions(-) diff --git a/src/components/game-distribution/GameDetailPage.tsx b/src/components/game-distribution/GameDetailPage.tsx index 23e389b46..30666c0de 100644 --- a/src/components/game-distribution/GameDetailPage.tsx +++ b/src/components/game-distribution/GameDetailPage.tsx @@ -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) => } + renderAsset={(asset) => + asset.role === 'cover' && coverPlaceholderLabel ? ( + + {coverPlaceholderLabel} + + ) : ( + + ) + } controlsLabel={resolveControlsLabel(version, game.inputModes)} versionLabel={versionLabel} - rating={} + rating={publiclyVisible ? : null} actions={ <> ) : null} - +

我的评价

- {!canReview ? ( + {!publiclyVisible ? ( + 暂无评价 + ) : !canReview ? ( 登录后评价 @@ -218,9 +226,11 @@ export function GameReviews({

全部评价

- + {publiclyVisible ? : null}
- {state.listLoading ? ( + {!publiclyVisible ? ( + 暂无评价 + ) : state.listLoading ? (

正在加载评价…

) : state.listError ? ( 暂无评价 )} - + {publiclyVisible ? ( + + ) : null}
); diff --git a/src/components/game-distribution/gameDistribution.css b/src/components/game-distribution/gameDistribution.css index 2ec1a61ac..ea2a86285 100644 --- a/src/components/game-distribution/gameDistribution.css +++ b/src/components/game-distribution/gameDistribution.css @@ -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; diff --git a/src/components/game-distribution/gameVersionHistoryModel.ts b/src/components/game-distribution/gameVersionHistoryModel.ts index d96fcf3d5..e5d10b63a 100644 --- a/src/components/game-distribution/gameVersionHistoryModel.ts +++ b/src/components/game-distribution/gameVersionHistoryModel.ts @@ -23,6 +23,31 @@ export const VERSION_STATUS_LABEL: Record = { revoked: '已撤回', }; +/** + * 非公开作品在封面位置显示的占位文案。 + * + * 作品没公开时封面素材不可换签(封面位只会剩渐变占位),这里按最新版本状态给出最贴近的 + * 说明;其余状态退回作品级公开文案,避免在封面上编造审核结论。 + */ +export const NON_PUBLIC_COVER_VERSION_LABEL: Partial< + Record +> = { + 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, diff --git a/src/components/game-distribution/useGameReviews.ts b/src/components/game-distribution/useGameReviews.ts index 88f5ded33..0b5ebd55d 100644 --- a/src/components/game-distribution/useGameReviews.ts +++ b/src/components/game-distribution/useGameReviews.ts @@ -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( 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(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(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 分');