公开作品详情页不再显示占位作者名「创作者」,按作者 ID 补查真实昵称
Project CI / Backend tests (pull_request) Failing after 30s
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m27s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 5m23s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 4m15s
Project CI / Repository checks (pull_request) Failing after 33s
Project CI / Frontend tests (pull_request) Successful in 3m59s
Project CI / AI game creator shell web tests (pull_request) Successful in 4m10s
Project CI / Native shell tests (pull_request) Successful in 8m37s

- 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 悬浮显示完整名字
This commit is contained in:
2026-10-04 17:59:02 +08:00
parent 2e74092743
commit e593236f9b
3 changed files with 91 additions and 8 deletions
@@ -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 (
<div
className={['game-detail-display', className].filter(Boolean).join(' ')}
@@ -232,12 +234,14 @@ export function GameDetailDisplay({
{rating ? (
<div className="game-detail-hero__rating">{rating}</div>
) : null}
<div className="game-detail-hero__author">
<span className="game-detail-avatar">
{game.author.name.slice(0, 1)}
</span>
<span>由 {game.author.name} 制作</span>
</div>
{authorName ? (
<div className="game-detail-hero__author">
<span className="game-detail-avatar" aria-hidden="true">
{Array.from(authorName)[0]}
</span>
<span title={authorName}>由 {authorName} 制作</span>
</div>
) : null}
<div className="game-detail-hero__actions">
{actions}
{typeof game.playCount === 'number' ? (
@@ -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({
<ArrowLeft /> 返回
</button>
<GameDetailDisplay
game={game}
game={displayGame}
coverObjectKey={game.coverObjectKey}
screenshotObjectKeys={game.screenshots}
coverColor={game.coverColor}
@@ -0,0 +1,69 @@
import { useEffect, useState } from 'react';
import { getPublicAuthUserById } from '../../services/authService';
/**
* 作者展示名缺失时服务端会退化成角色词,不是用户名:
* api-server 的 `game_payload`(公开目录/公开详情/作者侧)写的是
* `author_name.as_deref().unwrap_or("创作者")`,后台行 `admin_game_payload` 写的是
* `unwrap_or("未知作者")`。这两个词一律当成「没有名字」,否则会把占位词当昵称渲染出来。
*/
const GAME_AUTHOR_PLACEHOLDER_NAMES: Record<string, true> = {
创作者: 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;
}