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 ? (
+
+
+ {Array.from(authorName)[0]}
+
+ 由 {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;
+}