diff --git a/packages/shared/src/components/GameDetailDisplay/index.tsx b/packages/shared/src/components/GameDetailDisplay/index.tsx index e9252d574..1cd432067 100644 --- a/packages/shared/src/components/GameDetailDisplay/index.tsx +++ b/packages/shared/src/components/GameDetailDisplay/index.tsx @@ -1,7 +1,7 @@ import './index.css'; import { Maximize2, Smartphone, Sparkles } from 'lucide-react'; -import { type ReactElement, type ReactNode,useState } from 'react'; +import { type ReactElement, type ReactNode, useState } from 'react'; /** 详情展示里素材出现的位置:hero 大图与缩略图条。 */ export type GameDetailAssetRole = 'cover' | 'thumb'; @@ -210,6 +210,8 @@ export function GameDetailDisplay({ }: GameDetailDisplayProps) { const controls = (controlsLabel ?? '').trim(); const version = (versionLabel ?? '').trim(); + /** 作者名由宿主解析(公开详情会按作者 ID 补查真实昵称);空名不渲染,避免出现「由 — 制作」。 */ + const authorName = (game.author.name ?? '').trim(); return (
{rating}
) : null} -
- - {game.author.name.slice(0, 1)} - - 由 {game.author.name} 制作 -
+ {authorName ? ( +
+ + 由 {authorName} 制作 +
+ ) : null}
{actions} {typeof game.playCount === 'number' ? ( diff --git a/src/components/game-distribution/GameDetailPage.tsx b/src/components/game-distribution/GameDetailPage.tsx index 30666c0de..1fee3520d 100644 --- a/src/components/game-distribution/GameDetailPage.tsx +++ b/src/components/game-distribution/GameDetailPage.tsx @@ -38,6 +38,7 @@ import { VERSION_STATUS_LABEL, VERSION_STATUS_NEEDS_AUTHOR_ACTION, } from './gameVersionHistoryModel'; +import { useGameAuthorDisplayName } from './useGameAuthorName'; import { useGameReviews } from './useGameReviews'; type GameDetailPageProps = { @@ -292,6 +293,15 @@ function GameDetailContent({ */ const publiclyVisible = game.status === 'published'; const reviews = useGameReviews(game.id, canReview, publiclyVisible); + /** + * 作者展示名:公开投影只会退化成占位角色词,真实昵称按作者 ID 补查一次 + * (见 useGameAuthorName);查不到时给空串,展示层就不渲染「由 X 制作」那一行。 + */ + const authorName = useGameAuthorDisplayName(game.author); + const displayGame = { + ...game, + author: { ...game.author, name: authorName ?? '' }, + }; /** 作者视角的条目没有公开投影 `currentVersion`,可玩版本取版本历史里的已发布版本。 */ const publishedVersion = ownerGame?.versions.find((item) => item.status === 'published') ?? null; @@ -314,7 +324,7 @@ function GameDetailContent({ 返回 = { + 创作者: true, + 未知作者: true, +}; + +export type GameAuthorIdentity = { + id?: string | null; + name?: string | null; +}; + +function isRealAuthorName(value: string | null | undefined) { + const name = (value ?? '').trim(); + return name.length > 0 && GAME_AUTHOR_PLACEHOLDER_NAMES[name] !== true; +} + +/** + * 游戏作者的对外展示名。 + * + * 游戏行上的 `author_name` 由创建接口固定写空(api-server `create_game` 传 None), + * 所以公开投影里的 `author.name` 只会是占位角色词,不是用户名。真实昵称只能按 + * `author.id`(owner_user_id)走公开用户查询补一次:`GET /api/auth/public-users/by-id/{userId}`, + * 与陶泥号查询 `/api/auth/public-users/by-code/{code}` 同一族接口,不需要登录态。 + * + * 返回 `null` 表示还在查询——展示层此时不要渲染名字,避免先闪一下占位词; + * 返回空串表示查不到(账号失效 / 网络失败),由展示层降级。 + * 回包名本来就是真实昵称时不再多发请求:后端补上作者名后这里自然退化为普通渲染。 + */ +export function useGameAuthorDisplayName( + author: GameAuthorIdentity | null | undefined, +): string | null { + const fallbackName = (author?.name ?? '').trim(); + const hasFallbackName = isRealAuthorName(fallbackName); + const authorId = (author?.id ?? '').trim(); + const [lookedUp, setLookedUp] = useState<{ + id: string; + name: string; + } | null>(null); + + useEffect(() => { + if (hasFallbackName || !authorId) return; + let alive = true; + void getPublicAuthUserById(authorId) + .then((user) => { + if (alive) setLookedUp({ id: authorId, name: user.displayName.trim() }); + }) + .catch(() => { + // 作者账号查不到(已失效 / 非公开)时不编造名字,按空名交给展示层降级。 + if (alive) setLookedUp({ id: authorId, name: '' }); + }); + return () => { + alive = false; + }; + }, [authorId, hasFallbackName]); + + if (hasFallbackName) return fallbackName; + if (!authorId) return ''; + if (!lookedUp || lookedUp.id !== authorId) return null; + return lookedUp.name; +}