补充创作者主页与关注粉丝工程文档 (#638)
Project CI / AI game creator shell Rust crates (push) Successful in 5m56s
Project CI / AI game creator shell Rust lane 2/2 (push) Successful in 6m36s
Project CI / AI game creator shell Rust lane 1/2 (push) Successful in 7m37s
Project CI / Backend tests (push) Successful in 9m22s
Project CI / Frontend tests (push) Successful in 4m40s
Project CI / AI game creator shell web tests (push) Successful in 3m50s
Project CI / Repository checks (push) Successful in 6m30s
Project CI / Native shell tests (push) Successful in 8m32s

本 PR 补齐创作者主页与关注粉丝功能的产品合同和工程设计,供后续按里程碑实施;本次只修改文档。

桌面导航第四项进入自己的创作者主页,“我的”移到第五项。游戏详情可进入作者主页并关注/取消关注;自己或他人的关注、粉丝列表均公开可查看,点击用户头像或昵称可进入其主页。本人支持取消/重新关注、回关和移除粉丝,并明确两个关注方向独立。

工程设计包含关系表与 DTO、认证和事务边界、跨页状态同步、共享组件、深链及验收要求,拆分关系查询和页面交互两个里程碑。按用户要求,游戏列表沿用最多 48 项的现有限制,不增加游戏目录分页改造。

验证:
- 已合入最新 origin/master(21c79f54a);共享决策记录冲突保留双方新增内容。
- npm run check:encoding 通过。
- npm run check:doc-index 通过。
- git diff --check 与暂存差异检查通过。
- 相对 master 仅六份文档发生变化;没有业务代码或 schema 修改,未执行业务测试或部署。

Reviewed-on: #638
Co-authored-by: Linghong <ink29535@proton.me>
Co-committed-by: Linghong <ink29535@proton.me>
This commit was merged in pull request #638.
This commit is contained in:
2026-10-05 22:05:11 +08:00
committed by 孔令弘
parent 96859666ff
commit 51cb05f418
100 changed files with 10581 additions and 5269 deletions
@@ -6,6 +6,7 @@ import {
Palette,
Search,
UserRound,
UsersRound,
} from 'lucide-react';
import {
type ComponentType,
@@ -27,6 +28,11 @@ import {
pushAppHistoryPath,
replaceAppHistoryPath,
} from '../../routing/activeAppPageRoutes';
import {
connectionsPath,
type CreatorConnectionKind,
creatorPath,
} from '../../services/creatorProfileClient';
import { loadFrontendRuntimeConfig } from '../../services/frontendRuntimeConfigService';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import { usePlatformWalletStore } from '../../stores/usePlatformWalletStore';
@@ -35,6 +41,7 @@ import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import { ClientDownloadEntry } from '../creation-home/ClientDownloadEntry';
import { setCreatorViewer } from '../creator/creatorRelationshipState';
import { PaymentCheckoutView } from './PaymentCheckoutView';
import {
PlatformActiveMobileWelcomeDialog,
@@ -80,6 +87,14 @@ const GameGalleryPage = lazy(async () => {
return { default: module.GameGalleryPage };
});
const CreatorPage = lazy(async () => {
const module = await import('../creator/CreatorPage');
return { default: module.CreatorPage };
});
const CreatorConnectionsPage = lazy(async () => {
const module = await import('../creator/CreatorConnectionsPage');
return { default: module.CreatorConnectionsPage };
});
const GameDetailPage = lazy(async () => {
const module = await import('../game-distribution/GameDetailPage');
return { default: module.GameDetailPage };
@@ -181,16 +196,20 @@ function MobileProfileDock({
onOpenProfile,
gameActive,
onOpenGames,
creatorActive,
onOpenCreator,
}: {
active: boolean;
onOpenProfile: () => void;
gameActive: boolean;
onOpenGames: () => void;
creatorActive: boolean;
onOpenCreator: () => void;
}) {
return (
<div className="platform-mobile-bottom-dock min-w-0 shrink-0 lg:hidden">
<nav
className="platform-bottom-nav grid grid-cols-2"
className="platform-bottom-nav grid grid-cols-3"
aria-label="移动平台导航"
>
<ActiveBottomNavButton
@@ -199,6 +218,12 @@ function MobileProfileDock({
label="游戏"
onClick={onOpenGames}
/>
<ActiveBottomNavButton
active={creatorActive}
icon={UsersRound}
label="创作者主页"
onClick={onOpenCreator}
/>
<ActiveBottomNavButton
active={active}
icon={UserRound}
@@ -410,6 +435,42 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('profile-payment', { path: '/profile/payment' });
}, [setSelectionStage]);
const creatorViewer = authUi?.canAccessProtectedData
? (authUi.user?.id ?? null)
: null;
useEffect(() => {
setCreatorViewer(creatorViewer);
}, [creatorViewer]);
const openCreator = useCallback(
(id?: string) => {
const path = id ? creatorPath(id) : '/creators';
pushAppHistoryPath(path);
setSelectionStage('creator-home', { path });
},
[setSelectionStage],
);
const openCreatorConnections = useCallback(
(id: string, tab: CreatorConnectionKind) => {
const path = connectionsPath(id, tab);
pushAppHistoryPath(path);
setSelectionStage('creator-connections', { path });
},
[setSelectionStage],
);
const backFromCreator = useCallback(() => {
if (hasAppHistoryBackEntry()) {
window.history.back();
return;
}
const owner =
selectionStage === 'creator-connections'
? new URLSearchParams(window.location.search).get('id')
: null;
// 兜底返回替换当前条目,避免主页深链与本人主页互相新增历史。
const path = owner ? creatorPath(owner) : '/games';
replaceAppHistoryPath(path);
setSelectionStage(owner ? 'creator-home' : 'games', { path });
}, [selectionStage, setSelectionStage]);
const openGames = useCallback(() => {
setSelectionStage('games', { path: '/games' });
}, [setSelectionStage]);
@@ -561,6 +622,8 @@ export function PlatformEntryFlowShellImpl({
<MobileProfileDock
active={false}
gameActive={false}
creatorActive={false}
onOpenCreator={() => openCreator()}
onOpenProfile={openProfile}
onOpenGames={openGames}
/>
@@ -599,6 +662,9 @@ export function PlatformEntryFlowShellImpl({
const isCreationStage =
!isProfileStage &&
(selectionStage === 'platform' || selectionStage === 'creation-home');
const isCreatorStage =
selectionStage === 'creator-home' ||
selectionStage === 'creator-connections';
const isGamesStage =
selectionStage === 'games' ||
selectionStage === 'game-detail' ||
@@ -614,6 +680,7 @@ export function PlatformEntryFlowShellImpl({
typeof window === 'undefined' ? '' : window.location.search,
);
const gameId = gameSearchParams.get('id');
const creatorConnectionTab = gameSearchParams.get('tab');
const publishGameId = gameSearchParams.get('game');
const avatarUrl = authUi?.user?.avatarUrl?.trim() || null;
const avatarLabel = resolveActiveUserAvatarLabel(authUi?.user);
@@ -677,6 +744,13 @@ export function PlatformEntryFlowShellImpl({
label="游戏"
onClick={openGames}
/>
<ActiveRailButton
active={isCreatorStage}
icon={UsersRound}
iconSrc="/creation-home/nav-profile.png"
label="创作者主页"
onClick={() => openCreator()}
/>
<ActiveRailButton
active={isProfileStage}
icon={UserRound}
@@ -829,6 +903,32 @@ export function PlatformEntryFlowShellImpl({
searchKeyword={activeSearchKeyword}
/>
</Suspense>
) : isCreatorStage ? (
<Suspense
fallback={<LoadingPanel label="正在加载创作者主页…" />}
>
{selectionStage === 'creator-connections' ? (
<CreatorConnectionsPage
id={gameId}
tab={
creatorConnectionTab === 'following' ||
creatorConnectionTab === 'followers'
? creatorConnectionTab
: null
}
onOpenCreator={openCreator}
onOpenConnections={openCreatorConnections}
onBack={backFromCreator}
/>
) : (
<CreatorPage
id={gameId}
onOpenConnections={openCreatorConnections}
onOpenGame={openGameDetail}
onBack={backFromCreator}
/>
)}
</Suspense>
) : isGamesStage ? (
<Suspense
fallback={<LoadingPanel label="正在加载游戏广场..." />}
@@ -867,6 +967,7 @@ export function PlatformEntryFlowShellImpl({
) : selectionStage === 'game-detail' ? (
<GameDetailPage
gameId={gameId}
onOpenCreator={openCreator}
ownerView={gameSearchParams.get('owner') === '1'}
onBack={backFromGameDetail}
onPlay={openGamePlay}
@@ -889,6 +990,8 @@ export function PlatformEntryFlowShellImpl({
<MobileProfileDock
active={isProfileStage}
gameActive={isGamesStage}
creatorActive={isCreatorStage}
onOpenCreator={() => openCreator()}
onOpenProfile={openProfile}
onOpenGames={openGames}
/>