Files
Genarrative/src/components/game-shell/PlatformHomeView.tsx
高物 1c72066bab
Some checks failed
CI / verify (push) Has been cancelled
1
2026-04-20 15:45:14 +08:00

1551 lines
58 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import {
Archive,
ArrowRight,
Bell,
BookOpen,
Camera,
ChevronRight,
Clock3,
Coins,
Copy,
Crown,
House,
MessageCircle,
Pencil,
Search,
Settings,
Sparkles,
Ticket,
UserPlus,
UserRound,
} from 'lucide-react';
import { type ComponentType, useMemo } from 'react';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
ProfileDashboardCardKey,
ProfileDashboardSummary,
ProfileSaveArchiveSummary,
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
import type { PlatformBrowseHistoryEntry } from '../../services/platformBrowseHistory';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { PlatformBrandLogo } from './PlatformBrandLogo';
import {
buildPlatformWorldTags,
describePlatformThemeLabel,
formatPlatformWorldTime,
type PlatformWorldCardLike,
resolvePlatformWorldCoverImage,
resolvePlatformWorldLeadPortrait,
} from './platformWorldPresentation';
export type PlatformHomeTab = 'home' | 'create' | 'saves' | 'profile';
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
const HERO_SURFACE_CLASS =
'platform-surface platform-surface--hero platform-interactive-card';
const MOBILE_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface space-y-4 pb-2';
const DESKTOP_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface space-y-5 pb-4';
function SectionHeader({ title, detail }: { title: string; detail: string }) {
return (
<div className="mb-3">
<div className="text-[10px] font-semibold tracking-[0.26em] text-zinc-500">
{detail}
</div>
<div className="mt-1 text-base font-semibold text-[var(--platform-text-strong)]">
{title}
</div>
</div>
);
}
function EmptyShelf({ text }: { text: string }) {
return (
<div
className={`${PANEL_SURFACE_CLASS} rounded-[1.35rem] px-4 py-3 text-sm leading-6 text-zinc-300`}
>
{text}
</div>
);
}
function SaveArchivePreview({
entry,
label,
className,
}: {
entry: ProfileSaveArchiveSummary;
label: string;
className: string;
}) {
return (
<div
aria-hidden="true"
className={`platform-remap-surface relative shrink-0 overflow-hidden rounded-[1.35rem] border border-white/12 bg-black/18 shadow-[var(--platform-desktop-hover-shadow)] ${className}`}
>
{entry.coverImageSrc ? (
<img
src={entry.coverImageSrc}
alt=""
className="absolute inset-0 h-full w-full object-cover"
/>
) : (
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_34%),linear-gradient(145deg,rgba(255,94,125,0.92),rgba(255,150,116,0.88))]" />
)}
<div className="absolute inset-0 bg-[var(--platform-card-overlay-soft)]" />
<div className="absolute inset-x-0 bottom-0 px-2.5 py-2">
<span className="inline-flex max-w-full items-center rounded-full border border-white/15 bg-black/24 px-2.5 py-1 text-[9px] font-semibold tracking-[0.14em] text-white/88">
{label}
</span>
</div>
</div>
);
}
function WorldCard({
entry,
badge,
metaLabel,
onClick,
className,
}: {
entry: PlatformWorldCardLike;
badge: string;
metaLabel: string;
onClick: () => void;
className?: string;
}) {
const coverImage = resolvePlatformWorldCoverImage(entry);
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
const tags = [
...new Set(
buildPlatformWorldTags(entry)
.map((tag) => tag.trim())
.filter(Boolean),
),
].slice(0, 3);
return (
<button
type="button"
onClick={onClick}
className={`platform-surface platform-interactive-card relative flex h-[15rem] w-[15.25rem] shrink-0 flex-col overflow-hidden px-3.5 py-3.5 text-left ${className ?? ''}`}
>
{coverImage ? (
<img
src={coverImage}
alt={entry.worldName}
className="absolute inset-0 h-full w-full object-cover opacity-40"
/>
) : null}
{leadPortrait ? (
<img
src={leadPortrait}
alt=""
aria-hidden="true"
className="absolute bottom-2 right-2 h-24 w-24 object-contain opacity-25"
/>
) : null}
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="relative z-10 flex h-full flex-col">
<div className="flex items-start justify-between gap-3">
<span className="platform-pill platform-pill--warm">{badge}</span>
<span className="platform-pill platform-pill--neutral px-2.5">
{metaLabel}
</span>
</div>
<div className="mt-auto">
<div className="line-clamp-1 text-xl font-black text-white">
{entry.worldName}
</div>
{entry.subtitle ? (
<div className="mt-1 line-clamp-1 text-[11px] tracking-[0.16em] text-zinc-300/85">
{entry.subtitle}
</div>
) : null}
<div className="mt-2 line-clamp-2 text-xs leading-5 text-zinc-200/90">
{entry.summaryText || '等待补充世界摘要。'}
</div>
<div className="mt-3 flex flex-wrap gap-2">
{tags.length > 0 ? (
tags.map((tag, index) => (
<span
key={`world-tag-${index}-${tag || 'empty'}`}
className="platform-pill platform-pill--neutral px-2.5"
>
{tag}
</span>
))
) : (
<span className="platform-pill platform-pill--neutral px-2.5">
{describePlatformThemeLabel(entry.themeMode)}
</span>
)}
</div>
</div>
</div>
</button>
);
}
function CreationLibraryCard({
entry,
onClick,
}: {
entry: CustomWorldLibraryEntry<CustomWorldProfile>;
onClick: () => void;
}) {
const coverImage = resolvePlatformWorldCoverImage(entry);
const leadPortrait = resolvePlatformWorldLeadPortrait(entry);
const statusLabel = entry.visibility === 'published' ? '已发布' : '草稿';
const metaLabel =
entry.visibility === 'published'
? formatPlatformWorldTime(entry.publishedAt)
: '仅自己可见';
const primaryTag =
buildPlatformWorldTags(entry)
.map((tag) => tag.trim())
.filter(Boolean)[0] ?? describePlatformThemeLabel(entry.themeMode);
const summaryText =
entry.summaryText || entry.subtitle || '继续补完这个世界的设定与游玩入口。';
return (
<button
type="button"
onClick={onClick}
className="platform-surface platform-interactive-card relative flex min-h-[13rem] w-full min-w-0 flex-col overflow-hidden px-3 py-3 text-left sm:min-h-[14rem] sm:px-3.5 sm:py-3.5"
>
{coverImage ? (
<img
src={coverImage}
alt={entry.worldName}
className="absolute inset-0 h-full w-full object-cover opacity-38"
/>
) : null}
{leadPortrait ? (
<img
src={leadPortrait}
alt=""
aria-hidden="true"
className="absolute bottom-1.5 right-1.5 h-16 w-16 object-contain opacity-24 sm:h-20 sm:w-20"
/>
) : null}
<div className="absolute inset-0 bg-[var(--platform-card-overlay-strong)]" />
<div className="relative z-10 flex h-full min-w-0 flex-col">
<div className="flex min-w-0 flex-wrap items-center gap-1.5">
<span
className={`inline-flex items-center rounded-full border px-2 py-1 text-[10px] font-semibold tracking-[0.12em] ${
entry.visibility === 'published'
? 'border-emerald-300/25 bg-emerald-500/12 text-emerald-50'
: 'border-amber-300/25 bg-amber-500/12 text-amber-50'
}`}
>
{statusLabel}
</span>
<span className="inline-flex min-w-0 max-w-full items-center rounded-full border border-white/12 bg-black/18 px-2 py-1 text-[10px] font-medium text-zinc-300">
<span className="truncate">{metaLabel}</span>
</span>
</div>
<div className="mt-auto min-w-0">
<div className="line-clamp-2 break-words text-base font-black leading-[1.15] text-white sm:text-[1.12rem]">
{entry.worldName}
</div>
{entry.subtitle ? (
<div className="mt-1 line-clamp-1 break-words text-[11px] tracking-[0.08em] text-zinc-300/84">
{entry.subtitle}
</div>
) : null}
<div className="mt-2 line-clamp-3 break-words text-[11px] leading-5 text-zinc-200/88 sm:text-xs">
{summaryText}
</div>
<div className="mt-3 flex flex-wrap items-center gap-1.5">
<span className="inline-flex min-w-0 max-w-full items-center rounded-full border border-white/12 bg-black/18 px-2 py-1 text-[10px] font-semibold tracking-[0.1em] text-zinc-100/90">
<span className="truncate">{primaryTag}</span>
</span>
<span className="inline-flex items-center gap-1 text-[11px] font-semibold text-zinc-200">
<span>
{entry.visibility === 'published' ? '进入世界' : '继续创作'}
</span>
<ArrowRight className="h-3.5 w-3.5 shrink-0" />
</span>
</div>
</div>
</div>
</button>
);
}
function SaveArchiveCard({
entry,
onClick,
loading = false,
}: {
entry: ProfileSaveArchiveSummary;
onClick: () => void;
loading?: boolean;
}) {
const summaryText =
entry.summaryText || entry.subtitle || '继续推进上一次保存的故事。';
return (
<button
type="button"
onClick={onClick}
disabled={loading}
className={`platform-surface platform-surface--soft platform-interactive-card relative flex min-h-[13rem] w-full overflow-hidden p-3.5 text-left sm:min-h-[12.5rem] sm:p-4 ${loading ? 'opacity-80' : ''}`}
>
<div className="absolute inset-0 bg-[var(--platform-card-overlay-deep)]" />
<div className="relative z-10 flex h-full w-full flex-col gap-3">
<div className="flex flex-wrap items-center justify-between gap-2">
<span className="platform-pill platform-pill--cool">ARCHIVE</span>
<span className="rounded-full border border-white/10 bg-black/18 px-2.5 py-1 text-[11px] font-medium text-zinc-300">
{loading ? '恢复中' : formatSnapshotTime(entry.lastPlayedAt)}
</span>
</div>
<div className="flex min-w-0 flex-1 items-stretch gap-3 sm:gap-4">
<div className="min-w-0 flex-1">
<div className="line-clamp-2 break-words text-[1.15rem] font-black leading-tight text-white sm:text-xl">
{entry.worldName}
</div>
{entry.subtitle ? (
<div className="mt-1 line-clamp-1 break-words text-sm text-zinc-300">
{entry.subtitle}
</div>
) : null}
<div className="mt-2 line-clamp-3 break-words text-xs leading-5 text-zinc-400 sm:text-sm">
{summaryText}
</div>
<div className="mt-4 inline-flex items-center gap-1.5 text-xs font-semibold text-zinc-200">
<span>{loading ? '正在恢复' : '继续游玩'}</span>
<ArrowRight className="h-3.5 w-3.5 shrink-0" />
</div>
</div>
<SaveArchivePreview
entry={entry}
label={loading ? '恢复中' : '最近存档'}
className="h-[7.4rem] w-[5.6rem] sm:h-[8rem] sm:w-[6.4rem]"
/>
</div>
</div>
</button>
);
}
function PlatformTabButton({
active,
label,
icon: Icon,
onClick,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={`platform-bottom-nav__button ${active ? 'platform-bottom-nav__button--active' : ''}`}
>
<span className="flex flex-col items-center justify-center gap-1">
<span className="platform-bottom-nav__icon-shell">
<Icon className="platform-bottom-nav__icon h-[1.05rem] w-[1.05rem]" />
</span>
<span className="platform-bottom-nav__label text-[11px] font-semibold tracking-[0.18em]">
{label}
</span>
</span>
</button>
);
}
function DesktopTabButton({
active,
label,
icon: Icon,
onClick,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={`platform-desktop-rail__button ${active ? 'platform-desktop-rail__button--active' : ''}`}
>
<span className="platform-desktop-rail__icon-shell">
<Icon className="platform-desktop-rail__icon h-[1.1rem] w-[1.1rem]" />
</span>
<span className="platform-desktop-rail__label text-[11px] font-semibold tracking-[0.2em]">
{label}
</span>
</button>
);
}
function DesktopTrendingItem({
entry,
rank,
onClick,
}: {
entry: CustomWorldGalleryCard;
rank: number;
onClick: () => void;
}) {
const coverImage = resolvePlatformWorldCoverImage(entry);
const tags = buildPlatformWorldTags(entry).filter(Boolean).slice(0, 2);
return (
<button
type="button"
onClick={onClick}
className="platform-desktop-trending-item flex items-center gap-4 px-4 py-4 text-left"
>
<div className="relative h-[5.5rem] w-[4.3rem] shrink-0 overflow-hidden rounded-[1rem] border border-[var(--platform-subpanel-border)] bg-[rgba(255,255,255,0.66)]">
{coverImage ? (
<img
src={coverImage}
alt={entry.worldName}
className="h-full w-full object-cover"
/>
) : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(91,24,46,0.26))]" />
</div>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2 text-[10px] tracking-[0.18em] text-zinc-500">
<span>{`${rank}`.padStart(2, '0')}</span>
<span>{formatPlatformWorldTime(entry.publishedAt)}</span>
</div>
<div className="mt-2 line-clamp-1 text-lg font-semibold text-[var(--platform-text-strong)]">
{entry.worldName}
</div>
<div className="mt-1 line-clamp-2 text-sm leading-6 text-zinc-300/86">
{entry.summaryText || entry.subtitle || '等待补充世界摘要。'}
</div>
<div className="mt-3 flex flex-wrap gap-2">
{tags.length > 0 ? (
tags.map((tag, index) => (
<span
key={`${entry.profileId}-trend-tag-${index}-${tag}`}
className="platform-pill platform-pill--neutral px-2.5"
>
{tag}
</span>
))
) : (
<span className="platform-pill platform-pill--neutral px-2.5">
{describePlatformThemeLabel(entry.themeMode)}
</span>
)}
</div>
</div>
<ChevronRight className="h-4 w-4 shrink-0 text-zinc-500" />
</button>
);
}
function formatSnapshotTime(value: string | null | undefined) {
if (!value) {
return '刚刚保存';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function describeLoginMethod(loginMethod: AuthUser['loginMethod']) {
switch (loginMethod) {
case 'phone':
return '手机号';
case 'wechat':
return '微信';
default:
return '账号密码';
}
}
function describeBindingStatus(bindingStatus: AuthUser['bindingStatus']) {
return bindingStatus === 'pending_bind_phone' ? '待绑定手机号' : '正常';
}
function formatCompactPlayTime(playTimeMs: number) {
const totalMinutes = Math.max(0, Math.floor(playTimeMs / 60000));
const days = totalMinutes / 1440;
if (days >= 10) {
return `${Math.floor(days)}`;
}
if (days >= 1) {
return `${days.toFixed(days >= 3 ? 0 : 1)}`;
}
const hours = totalMinutes / 60;
if (hours >= 1) {
return `${hours.toFixed(hours >= 10 ? 0 : 1)}小时`;
}
return `${Math.max(0, totalMinutes)}`;
}
function formatDashboardCount(value: number) {
const normalizedValue = Math.max(0, Math.round(value));
if (normalizedValue >= 100000000) {
return `${(normalizedValue / 100000000).toFixed(1)}亿`;
}
if (normalizedValue >= 10000) {
return `${(normalizedValue / 10000).toFixed(1)}`;
}
return normalizedValue.toLocaleString('zh-CN');
}
function formatDashboardUpdatedAt(value: string | null | undefined) {
if (!value) {
return '暂无更新记录';
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return value;
}
return date.toLocaleString('zh-CN', {
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
function buildPublicUserCode(user: AuthUser | null | undefined) {
const raw =
user?.id.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
user?.username.replace(/[^a-zA-Z0-9]/gu, '').toUpperCase() ||
'00000000';
return `SY-${raw.slice(-8).padStart(8, '0')}`;
}
function getUserAvatarLabel(user: AuthUser | null | undefined) {
return (user?.displayName || user?.username || '叙')
.slice(0, 1)
.toUpperCase();
}
function copyText(value: string) {
if (typeof navigator === 'undefined' || !navigator.clipboard?.writeText) {
return;
}
void navigator.clipboard.writeText(value);
}
function ProfileStatCard({
cardKey,
label,
value,
onClick,
icon,
}: {
cardKey: ProfileDashboardCardKey;
label: string;
value: string;
onClick?: ((cardKey: ProfileDashboardCardKey) => void) | null;
icon: ComponentType<{ className?: string }>;
}) {
const Icon = icon;
return (
<button
type="button"
onClick={onClick ? () => onClick(cardKey) : undefined}
className="platform-subpanel rounded-[1.35rem] px-4 py-3 text-left transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
>
<div className="flex items-center gap-2 text-zinc-400">
<Icon className="h-4 w-4" />
<span className="text-[11px] tracking-[0.16em]">{label}</span>
</div>
<div className="mt-3 text-lg font-black text-[var(--platform-text-strong)]">
{value}
</div>
</button>
);
}
function ProfileStatCardSkeleton() {
return (
<div className="platform-subpanel rounded-[1.35rem] px-4 py-3">
<div className="h-4 w-20 animate-pulse rounded-full bg-white/10" />
<div className="mt-3 h-7 w-16 animate-pulse rounded-full bg-white/12" />
</div>
);
}
function ProfileShortcutButton({
label,
icon,
onClick,
}: {
label: string;
icon: ComponentType<{ className?: string }>;
onClick?: (() => void) | null;
}) {
const Icon = icon;
return (
<button
type="button"
onClick={onClick ?? undefined}
className="platform-subpanel flex min-h-[5.25rem] flex-col items-center justify-center gap-2 rounded-[1.2rem] px-3 py-3 text-center transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
>
<div className="platform-profile-chip flex h-10 w-10 items-center justify-center rounded-full">
<Icon className="h-[1.125rem] w-[1.125rem]" />
</div>
<div className="text-sm font-semibold text-[var(--platform-text-strong)]">
{label}
</div>
</button>
);
}
export function PlatformHomeView({
activeTab,
onTabChange,
hasSavedGame,
savedSnapshot,
saveEntries,
saveError,
featuredEntries,
latestEntries,
myEntries,
historyEntries,
profileDashboard,
isLoadingPlatform,
isLoadingDashboard,
isResumingSaveWorldKey,
platformError,
dashboardError,
onContinueGame,
onResumeSave,
onOpenCreateWorld,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenLibraryDetail,
onOpenProfileDashboardCard,
}: {
activeTab: PlatformHomeTab;
onTabChange: (tab: PlatformHomeTab) => void;
hasSavedGame: boolean;
savedSnapshot: HydratedSavedGameSnapshot | null;
saveEntries: ProfileSaveArchiveSummary[];
saveError: string | null;
featuredEntries: CustomWorldGalleryCard[];
latestEntries: CustomWorldGalleryCard[];
myEntries: CustomWorldLibraryEntry<CustomWorldProfile>[];
historyEntries: PlatformBrowseHistoryEntry[];
profileDashboard: ProfileDashboardSummary | null;
isLoadingPlatform: boolean;
isLoadingDashboard: boolean;
isResumingSaveWorldKey: string | null;
platformError: string | null;
dashboardError: string | null;
onContinueGame: (snapshot?: HydratedSavedGameSnapshot | null) => void;
onResumeSave: (entry: ProfileSaveArchiveSummary) => void;
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: CustomWorldGalleryCard) => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
onOpenProfileDashboardCard?: (cardKey: ProfileDashboardCardKey) => void;
}) {
const authUi = useAuthUi();
const isAuthenticated = Boolean(authUi?.user);
const featuredShelf = useMemo(
() => featuredEntries.slice(0, 6),
[featuredEntries],
);
const snapshotWorldName =
savedSnapshot?.gameState.customWorldProfile?.name ??
savedSnapshot?.gameState.currentScenePreset?.name ??
'继续冒险';
const snapshotCharacterName =
savedSnapshot?.gameState.playerCharacter?.title ??
savedSnapshot?.gameState.playerCharacter?.name ??
'旅人';
const snapshotDigest =
savedSnapshot?.gameState.storyEngineMemory?.continueGameDigest ??
savedSnapshot?.currentStory?.text ??
savedSnapshot?.gameState.customWorldProfile?.summary ??
'上一次冒险已经保存,可以从这里继续推进故事。';
const publicUserCode = buildPublicUserCode(authUi?.user);
const avatarLabel = getUserAvatarLabel(authUi?.user);
const remainingNarrativeCoins = profileDashboard?.walletBalance ?? 0;
const totalPlayTime = formatCompactPlayTime(
profileDashboard?.totalPlayTimeMs ?? 0,
);
const playedWorkCount = profileDashboard?.playedWorldCount ?? 0;
const tabIcons = {
home: House,
create: Sparkles,
saves: Archive,
profile: UserRound,
} as const;
const openUserSurface = () => {
if (authUi?.user) {
authUi.openAccountModal();
return;
}
authUi?.openLoginModal();
};
const desktopHeroEntry =
featuredShelf[0] ?? latestEntries[0] ?? myEntries[0] ?? null;
const desktopHeroCover = desktopHeroEntry
? resolvePlatformWorldCoverImage(desktopHeroEntry)
: null;
const desktopHeroStripEntries = (
featuredShelf.length > 0 ? featuredShelf : latestEntries
).slice(0, 5);
const desktopTrendingEntries = latestEntries.slice(0, 3);
const desktopFeaturedGrid = featuredShelf.slice(0, 4);
const desktopReleaseGrid = latestEntries.slice(0, 6);
const desktopLibraryPreview = myEntries.slice(0, 2);
let content = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
className={`${HERO_SURFACE_CLASS} relative block w-full overflow-hidden px-[18px] py-4 text-left`}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_30%),radial-gradient(circle_at_right,rgba(255,205,178,0.18),transparent_28%),linear-gradient(135deg,rgba(255,47,112,0.92),rgba(255,136,104,0.9))]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<div className="flex items-start justify-between gap-4">
<span className="platform-pill platform-pill--warm">
{hasSavedGame ? 'CONTINUE' : 'CREATE'}
</span>
<div className="platform-pill platform-pill--neutral px-3 text-[11px] tracking-[0.08em]">
{hasSavedGame ? '继续冒险' : '创建世界'}
</div>
</div>
<div>
<div className="text-3xl font-black text-white">
{hasSavedGame ? snapshotWorldName : '写下一个能被游玩的世界'}
</div>
<div className="mt-2 max-w-[28rem] text-sm leading-6 text-zinc-200/88">
{hasSavedGame
? `${snapshotCharacterName} 的进度已保存,点这里回到上一次停下来的故事节点。`
: '从设定、角色到场景网络,先生成一版可玩的世界底稿,再继续精修和发布。'}
</div>
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-white/90">
<span>{hasSavedGame ? '继续推进故事' : '进入创作工作台'}</span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
</div>
</button>
{platformError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{platformError}
</div>
) : null}
<section>
<SectionHeader title="精选推荐" detail="为你挑选" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取精选作品..." />
) : featuredShelf.length > 0 ? (
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{featuredShelf.map((entry: CustomWorldGalleryCard) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:featured`}
entry={entry}
badge="推荐"
metaLabel={describePlatformThemeLabel(entry.themeMode)}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="还没有公开作品,先创建你的第一个世界吧。" />
)}
</section>
<section>
<SectionHeader title="最新发布" detail="玩家广场" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取最新发布..." />
) : latestEntries.length > 0 ? (
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{latestEntries.map((entry: CustomWorldGalleryCard) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:latest`}
entry={entry}
badge={formatPlatformWorldTime(entry.publishedAt)}
metaLabel={entry.authorDisplayName}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有新作品。" />
)}
</section>
</div>
);
if (activeTab === 'create') {
content = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
<button
type="button"
onClick={onOpenCreateTypePicker}
className={`${HERO_SURFACE_CLASS} relative block w-full overflow-hidden px-[18px] py-4 text-left`}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.16),transparent_38%),radial-gradient(circle_at_right,rgba(255,201,172,0.18),transparent_30%),linear-gradient(180deg,rgba(255,90,141,0.88),rgba(255,144,105,0.88))]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<span className="platform-pill platform-pill--cool w-fit">
CREATE
</span>
<div>
<div className="text-3xl font-black text-white"></div>
<div className="mt-2 max-w-[28rem] text-sm leading-6 text-zinc-200/88">
</div>
<div className="mt-4 flex items-center gap-2 text-sm font-semibold text-white/90">
<span></span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
</div>
</button>
<section>
<SectionHeader title="我的创作" detail="草稿与已发布" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取你的作品..." />
) : myEntries.length > 0 ? (
<div className="grid grid-cols-2 gap-2.5 sm:gap-3 xl:grid-cols-3">
{myEntries.map(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => (
<CreationLibraryCard
key={`${entry.ownerUserId}:${entry.profileId}:mine`}
entry={entry}
onClick={() => onOpenLibraryDetail(entry)}
/>
),
)}
</div>
) : (
<EmptyShelf
text={
isAuthenticated
? '你还没有保存任何自定义世界,先创建一个草稿开始吧。'
: '登录后查看你的作品。'
}
/>
)}
</section>
</div>
);
}
if (activeTab === 'saves') {
content = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
{authUi?.user ? (
<>
{saveError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{saveError}
</div>
) : null}
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<SectionHeader title="全部存档" detail="最近更新时间排序" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取存档..." />
) : saveEntries.length > 0 ? (
<div className="grid gap-3 md:grid-cols-2">
{saveEntries.map((entry) => (
<SaveArchiveCard
key={entry.worldKey}
entry={entry}
loading={isResumingSaveWorldKey === entry.worldKey}
onClick={() => onResumeSave(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="还没有可恢复的存档,去首页开始一段新的游玩吧。" />
)}
</section>
</>
) : (
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<div className="platform-subpanel rounded-[1.35rem] px-4 py-4">
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<button
type="button"
onClick={() => authUi?.openLoginModal()}
className="platform-button platform-button--primary mt-4"
>
</button>
</div>
</section>
)}
</div>
);
}
if (activeTab === 'profile') {
content = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
{authUi?.user ? (
<>
<section className="platform-profile-hero rounded-[1.8rem] p-4">
<div className="flex items-start justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<button
type="button"
onClick={() => authUi.openAccountModal()}
className="platform-profile-avatar relative h-16 w-16 shrink-0 rounded-[1.4rem]"
>
<span className="flex h-full w-full items-center justify-center text-2xl font-black">
{avatarLabel}
</span>
<span className="platform-profile-camera absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full">
<Camera className="h-3.5 w-3.5" />
</span>
</button>
<div className="min-w-0">
<div className="flex items-center gap-2">
<div className="truncate text-xl font-black text-[var(--platform-text-strong)]">
{authUi.user.displayName}
</div>
<button
type="button"
onClick={() => authUi.openAccountModal()}
className="platform-profile-icon-button flex h-7 w-7 items-center justify-center rounded-full"
>
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-[var(--platform-text-soft)]">
<span> {publicUserCode}</span>
<button
type="button"
onClick={() => copyText(publicUserCode)}
className="platform-profile-chip flex items-center gap-1 rounded-full px-2 py-1"
>
<Copy className="h-3 w-3" />
</button>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
<span className="platform-profile-chip rounded-full px-2.5 py-1">
{describeLoginMethod(authUi.user.loginMethod)}
</span>
<span className="platform-profile-chip rounded-full px-2.5 py-1">
{describeBindingStatus(authUi.user.bindingStatus)}
</span>
</div>
</div>
</div>
<button
type="button"
className="platform-profile-action flex shrink-0 items-center gap-2 rounded-[1.1rem] px-3 py-2 text-left"
>
<Crown className="h-4 w-4" />
<div>
<div className="text-xs font-bold"></div>
<div className="text-[10px] opacity-80"></div>
</div>
<ChevronRight className="h-4 w-4 opacity-80" />
</button>
</div>
</section>
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<div className="grid grid-cols-3 gap-3">
{isLoadingDashboard ? (
<>
<ProfileStatCardSkeleton />
<ProfileStatCardSkeleton />
<ProfileStatCardSkeleton />
</>
) : dashboardError ? (
<>
<ProfileStatCard
cardKey="wallet"
label="剩余叙世币"
value="暂不可用"
icon={Coins}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playTime"
label="总游戏时长"
value="暂不可用"
icon={Clock3}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playedWorks"
label="玩过作品"
value="暂不可用"
icon={BookOpen}
onClick={onOpenProfileDashboardCard}
/>
</>
) : (
<>
<ProfileStatCard
cardKey="wallet"
label="剩余叙世币"
value={formatDashboardCount(remainingNarrativeCoins)}
icon={Coins}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playTime"
label="总游戏时长"
value={totalPlayTime}
icon={Clock3}
onClick={onOpenProfileDashboardCard}
/>
<ProfileStatCard
cardKey="playedWorks"
label="玩过作品"
value={formatDashboardCount(playedWorkCount)}
icon={BookOpen}
onClick={onOpenProfileDashboardCard}
/>
</>
)}
</div>
<div className="mt-3 text-[11px] text-zinc-500">
{dashboardError
? dashboardError
: `更新于 ${formatDashboardUpdatedAt(profileDashboard?.updatedAt)}`}
</div>
</section>
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<SectionHeader title="常用功能" detail="快捷入口" />
<div className="grid grid-cols-3 gap-3">
<ProfileShortcutButton label="邀请好友" icon={UserPlus} />
<ProfileShortcutButton label="填邀请码" icon={Ticket} />
<ProfileShortcutButton label="玩家社区" icon={MessageCircle} />
</div>
</section>
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<button
type="button"
onClick={() => authUi.openSettingsModal()}
className="platform-subpanel platform-interactive-card flex w-full items-center justify-between gap-3 rounded-[1.25rem] px-4 py-4 text-left transition hover:border-[var(--platform-surface-hover-border)] hover:bg-[var(--platform-button-secondary-fill)]"
>
<div className="flex items-center gap-3">
<div className="platform-profile-chip flex h-10 w-10 items-center justify-center rounded-full">
<Settings className="h-[1.125rem] w-[1.125rem]" />
</div>
<div>
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<div className="text-xs text-zinc-400"></div>
</div>
</div>
<ChevronRight className="h-4 w-4 text-zinc-500" />
</button>
</section>
</>
) : (
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
<div className="platform-subpanel rounded-[1.35rem] px-4 py-4">
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
</div>
<button
type="button"
onClick={() => authUi?.openLoginModal()}
className="platform-button platform-button--primary mt-4"
>
</button>
</div>
</section>
)}
</div>
);
}
const desktopContent =
activeTab === 'home' ? (
<div className={DESKTOP_PAGE_STAGE_CLASS}>
{platformError ? (
<div className="rounded-[1.5rem] border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-100">
{platformError}
</div>
) : null}
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.55fr)_22rem]">
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
className={`${HERO_SURFACE_CLASS} relative block overflow-hidden px-7 py-6 text-left`}
>
{desktopHeroCover ? (
<img
src={desktopHeroCover}
alt=""
aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover opacity-34"
/>
) : null}
<div className="absolute inset-0 bg-[linear-gradient(115deg,rgba(255,31,111,0.94),rgba(255,109,104,0.8)_52%,rgba(255,164,124,0.9))]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(255,255,255,0.18),transparent_28%),radial-gradient(circle_at_78%_24%,rgba(255,208,178,0.18),transparent_20%)]" />
<div className="relative z-10 flex min-h-[24rem] flex-col justify-between">
<div className="flex items-start justify-between gap-4">
<span className="platform-pill platform-pill--warm">
{hasSavedGame ? 'CONTINUE STORY' : 'CREATE WORLD'}
</span>
<span className="platform-pill platform-pill--neutral px-3">
{hasSavedGame ? '最近存档' : '创作入口'}
</span>
</div>
<div className="max-w-[35rem]">
<div className="text-5xl font-semibold leading-[1.08] text-white">
{hasSavedGame
? snapshotWorldName
: '把你的世界观直接变成可游玩的舞台'}
</div>
<div className="mt-4 text-base leading-8 text-zinc-200/86">
{hasSavedGame
? `${snapshotCharacterName} 的进度已经保存,桌面端可以直接从这里回到上一次停下来的关键节点。`
: '从设定、角色、世界结构到可玩流程,一次生成创作底稿,再继续精修并发布到平台广场。'}
</div>
<div className="mt-5 inline-flex items-center gap-2 rounded-full border border-white/18 bg-white/18 px-4 py-2 text-sm font-semibold text-white/92">
<span>
{hasSavedGame ? '继续推进故事' : '进入创作工作台'}
</span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
{desktopHeroStripEntries.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-5">
{desktopHeroStripEntries.map((entry, index) => {
const coverImage = resolvePlatformWorldCoverImage(entry);
return (
<div
key={`${entry.ownerUserId}:${entry.profileId}:hero-strip`}
className="platform-subpanel overflow-hidden rounded-[1.15rem]"
>
<div className="relative aspect-[1.35/1] overflow-hidden">
{coverImage ? (
<img
src={coverImage}
alt=""
aria-hidden="true"
className="h-full w-full object-cover"
/>
) : null}
<div className="absolute inset-0 bg-[linear-gradient(180deg,rgba(255,255,255,0.04),rgba(91,24,46,0.34))]" />
</div>
<div className="flex items-center gap-2 px-3 py-2 text-[11px] text-zinc-300/82">
<span className="text-zinc-500">
{`${index + 1}`.padStart(2, '0')}
</span>
<span className="line-clamp-1">
{entry.worldName}
</span>
</div>
</div>
);
})}
</div>
) : null}
</div>
</button>
<section className="platform-desktop-panel px-5 py-5">
<div className="mb-4 flex items-start justify-between gap-3">
<SectionHeader title="趋势关注" detail="TRENDING NOW" />
<span className="platform-pill platform-pill--neutral px-3">
LIVE
</span>
</div>
{isLoadingPlatform ? (
<EmptyShelf text="正在整理趋势作品..." />
) : desktopTrendingEntries.length > 0 ? (
<div className="space-y-3">
{desktopTrendingEntries.map((entry, index) => (
<DesktopTrendingItem
key={`${entry.ownerUserId}:${entry.profileId}:desktop-trend`}
entry={entry}
rank={index + 1}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有趋势作品。" />
)}
</section>
</div>
<div className="grid gap-5 2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]">
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="精选推荐" detail="CURATED WORLDS" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取精选作品..." />
) : desktopFeaturedGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-2">
{desktopFeaturedGrid.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:desktop-featured`}
entry={entry}
badge="推荐"
metaLabel={describePlatformThemeLabel(entry.themeMode)}
onClick={() => onOpenGalleryDetail(entry)}
className="h-[16rem] w-full min-w-0"
/>
))}
</div>
) : (
<EmptyShelf text="还没有公开作品,先创建你的第一个世界吧。" />
)}
</section>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={hasSavedGame ? '继续进度' : '创作入口'}
detail="QUICK ACCESS"
/>
<button
type="button"
onClick={() => {
if (hasSavedGame) {
onContinueGame();
return;
}
onOpenCreateWorld();
}}
className="platform-surface platform-surface--soft platform-interactive-card relative block w-full overflow-hidden px-5 py-5 text-left"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(255,164,198,0.18),transparent_28%),radial-gradient(circle_at_bottom_left,rgba(255,202,176,0.18),transparent_32%)]" />
<div className="relative z-10">
<div className="flex items-center justify-between gap-3">
<span className="platform-pill platform-pill--cool">
{hasSavedGame ? 'SAVE POINT' : 'START HERE'}
</span>
<ArrowRight className="h-4 w-4 text-zinc-400" />
</div>
<div className="mt-4 text-2xl font-semibold text-white">
{hasSavedGame ? snapshotWorldName : '从这里开启新的创作'}
</div>
<div className="mt-2 text-sm leading-7 text-zinc-300/84">
{hasSavedGame
? `当前角色:${snapshotCharacterName}`
: '快速进入自定义世界创作,继续补齐设定、角色与核心冲突。'}
</div>
<div className="mt-3 line-clamp-3 text-sm leading-6 text-zinc-400">
{hasSavedGame
? snapshotDigest
: '先生成一版可玩的世界底稿,再继续编辑并发布。'}
</div>
</div>
</button>
<div className="mt-5">
<div className="text-[10px] font-semibold tracking-[0.24em] text-zinc-500">
{desktopLibraryPreview.length > 0
? '最近作品'
: historyEntries.length > 0
? '最近浏览'
: isAuthenticated
? '创作状态'
: '账户状态'}
</div>
{desktopLibraryPreview.length > 0 ? (
<div className="mt-3 space-y-3">
{desktopLibraryPreview.map((entry) => (
<button
key={`${entry.ownerUserId}:${entry.profileId}:desktop-mine`}
type="button"
onClick={() => onOpenLibraryDetail(entry)}
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
>
<div className="min-w-0">
<div className="line-clamp-1 text-base font-semibold text-white">
{entry.worldName}
</div>
<div className="mt-1 text-sm text-zinc-400">
{entry.visibility === 'published'
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
: '草稿待完善'}
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
{entry.visibility === 'published' ? '已发布' : '草稿'}
</span>
</button>
))}
</div>
) : historyEntries.length > 0 ? (
<div className="mt-3 space-y-3">
{historyEntries.slice(0, 2).map((entry) => (
<button
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
type="button"
onClick={() =>
onOpenGalleryDetail({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
visibility: 'published',
publishedAt: entry.visitedAt,
updatedAt: entry.visitedAt,
worldName: entry.worldName,
subtitle: entry.subtitle,
summaryText: entry.summaryText,
coverImageSrc: entry.coverImageSrc,
themeMode: entry.themeMode,
authorDisplayName: entry.authorDisplayName,
playableNpcCount: 0,
landmarkCount: 0,
})
}
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
>
<div className="min-w-0">
<div className="line-clamp-1 text-base font-semibold text-white">
{entry.worldName}
</div>
<div className="mt-1 text-sm text-zinc-400">
{entry.authorDisplayName}
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
</span>
</button>
))}
</div>
) : (
<div className="platform-subpanel mt-3 rounded-[1.35rem] px-4 py-4 text-sm leading-6 text-[var(--platform-text-base)]">
{isAuthenticated
? '创建一个草稿后,这里会出现你最近保存的作品。'
: '登录后可同步你的创作、游玩进度与平台资料。'}
</div>
)}
</div>
</section>
</div>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="最新发布" detail="PLAYER SQUARE" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取最新发布..." />
) : desktopReleaseGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-3">
{desktopReleaseGrid.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:desktop-latest`}
entry={entry}
badge={formatPlatformWorldTime(entry.publishedAt)}
metaLabel={entry.authorDisplayName}
onClick={() => onOpenGalleryDetail(entry)}
className="h-[17rem] w-full min-w-0"
/>
))}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有新作品。" />
)}
</section>
</div>
) : (
content
);
return (
<div className="flex h-full min-h-0 flex-col">
<div className="flex h-full min-h-0 flex-col lg:hidden">
<div className="mb-4">
<PlatformBrandLogo />
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide">
{content}
</div>
<div
className="mt-4 border-t pt-3"
style={{
borderColor: 'var(--platform-line-soft)',
paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)',
}}
>
<div className="platform-bottom-nav grid h-14 grid-cols-4 gap-1 rounded-[1.2rem] px-1 py-1">
<PlatformTabButton
active={activeTab === 'home'}
label="首页"
icon={tabIcons.home}
onClick={() => onTabChange('home')}
/>
<PlatformTabButton
active={activeTab === 'create'}
label="创作"
icon={tabIcons.create}
onClick={() => onTabChange('create')}
/>
<PlatformTabButton
active={activeTab === 'saves'}
label="存档"
icon={tabIcons.saves}
onClick={() => onTabChange('saves')}
/>
<PlatformTabButton
active={activeTab === 'profile'}
label="我的"
icon={tabIcons.profile}
onClick={() => onTabChange('profile')}
/>
</div>
</div>
</div>
<div className="hidden h-full min-h-0 lg:flex lg:flex-col">
<div className="platform-desktop-shell flex h-full min-h-0 flex-col p-5 xl:p-6">
<div className="platform-desktop-topbar flex items-center gap-4 px-5 py-4">
<div className="flex min-w-0 flex-1 items-center gap-5">
<PlatformBrandLogo className="shrink-0" decorative />
<div className="platform-desktop-search flex min-w-0 max-w-[34rem] flex-1 items-center gap-3 px-4 py-3 text-zinc-400">
<Search className="h-4 w-4 shrink-0" />
<span className="truncate text-sm">
</span>
</div>
</div>
<div className="flex items-center gap-3">
<button
type="button"
onClick={openUserSurface}
className="platform-icon-button"
aria-label="通知与账户"
>
<Bell className="h-4 w-4" />
</button>
<button
type="button"
onClick={openUserSurface}
className="platform-desktop-search flex items-center gap-3 px-3 py-2.5 text-left"
>
<span
className="flex h-11 w-11 items-center justify-center rounded-full text-base font-black text-white"
style={{
background: 'var(--platform-profile-avatar-fill)',
boxShadow: 'var(--platform-profile-avatar-shadow)',
}}
>
{avatarLabel}
</span>
<span className="min-w-0">
<span className="block truncate text-sm font-semibold text-[var(--platform-text-strong)]">
{authUi?.user?.displayName || '进入账户'}
</span>
<span className="block truncate text-xs text-zinc-400">
{authUi?.user ? publicUserCode : '登录后同步创作与进度'}
</span>
</span>
</button>
</div>
</div>
<div className="mt-5 flex min-h-0 gap-5">
<aside className="platform-desktop-rail flex w-[5.8rem] shrink-0 flex-col gap-3 p-3">
<DesktopTabButton
active={activeTab === 'home'}
label="首页"
icon={tabIcons.home}
onClick={() => onTabChange('home')}
/>
<DesktopTabButton
active={activeTab === 'create'}
label="创作"
icon={tabIcons.create}
onClick={() => onTabChange('create')}
/>
<DesktopTabButton
active={activeTab === 'saves'}
label="存档"
icon={tabIcons.saves}
onClick={() => onTabChange('saves')}
/>
<DesktopTabButton
active={activeTab === 'profile'}
label="我的"
icon={tabIcons.profile}
onClick={() => onTabChange('profile')}
/>
</aside>
<div className="platform-desktop-scroll flex-1">
{desktopContent}
</div>
</div>
</div>
</div>
</div>
);
}