From e593236f9b34f5d46e1efa5248536488731da273 Mon Sep 17 00:00:00 2001 From: Suzumiya Date: Sun, 4 Oct 2026 17:59:02 +0800 Subject: [PATCH] =?UTF-8?q?=E5=85=AC=E5=BC=80=E4=BD=9C=E5=93=81=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E9=A1=B5=E4=B8=8D=E5=86=8D=E6=98=BE=E7=A4=BA=E5=8D=A0?= =?UTF-8?q?=E4=BD=8D=E4=BD=9C=E8=80=85=E5=90=8D=E3=80=8C=E5=88=9B=E4=BD=9C?= =?UTF-8?q?=E8=80=85=E3=80=8D=EF=BC=8C=E6=8C=89=E4=BD=9C=E8=80=85=20ID=20?= =?UTF-8?q?=E8=A1=A5=E6=9F=A5=E7=9C=9F=E5=AE=9E=E6=98=B5=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - useGameAuthorName.ts(新增):useGameAuthorDisplayName —— 回包名是占位词(「创作者」/「未知作者」)或空时,按 author.id 调 GET /api/auth/public-users/by-id/{id} 取 displayName;查询中返回 null(避免先闪占位词),查不到返回空串、不编造名字 - GameDetailPage.tsx:详情页用解析后的作者名装配展示对象(displayGame),替代直接用公开投影里的 author.name - packages/shared/src/components/GameDetailDisplay/index.tsx:作者名为空时不渲染「由 X 制作」那一行;头像首字改用 Array.from(name)[0](避免 emoji 代理对被截断);作者行补 title 悬浮显示完整名字 --- .../components/GameDetailDisplay/index.tsx | 18 +++-- .../game-distribution/GameDetailPage.tsx | 12 +++- .../game-distribution/useGameAuthorName.ts | 69 +++++++++++++++++++ 3 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 src/components/game-distribution/useGameAuthorName.ts 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; +}