1
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-14 21:49:44 +08:00
parent fa435aa6a6
commit 6363267bca
13 changed files with 2743 additions and 237 deletions

View File

@@ -1,14 +1,27 @@
import { type ComponentType, useMemo } from 'react';
import {
BookOpen,
Camera,
ChevronRight,
Clock3,
Coins,
Copy,
Crown,
MessageCircle,
Pencil,
Settings,
Ticket,
UserPlus,
} from 'lucide-react';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
} from '../../../packages/shared/src/contracts/runtime';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';
import type { AuthUser } from '../../services/authService';
import type { CustomWorldProfile } from '../../types';
import {
CHROME_ICONS,
getNineSliceStyle,
UI_CHROME,
} from '../../uiAssets';
import { getNineSliceStyle, UI_CHROME } from '../../uiAssets';
import { useAuthUi } from '../auth/AuthUiContext';
import { PixelIcon } from '../PixelIcon';
import {
@@ -20,43 +33,20 @@ import {
resolvePlatformWorldLeadPortrait,
} from './platformWorldPresentation';
function SectionHeader({
title,
detail,
actionLabel,
onAction,
}: {
title: string;
detail: string;
actionLabel?: string;
onAction?: (() => void) | null;
}) {
export type PlatformHomeTab = 'home' | 'create' | 'profile';
function SectionHeader({ title, detail }: { title: string; detail: string }) {
return (
<div className="mb-3 flex items-end justify-between gap-3">
<div>
<div className="text-[10px] tracking-[0.24em] text-zinc-500">
{detail}
</div>
<div className="mt-1 text-base font-bold text-white">{title}</div>
<div className="mb-3">
<div className="text-[10px] tracking-[0.24em] text-zinc-500">
{detail}
</div>
{actionLabel && onAction ? (
<button
type="button"
onClick={onAction}
className="rounded-full border border-white/10 bg-black/25 px-3 py-1.5 text-[11px] text-zinc-200 transition hover:border-white/20 hover:text-white"
>
{actionLabel}
</button>
) : null}
<div className="mt-1 text-base font-bold text-white">{title}</div>
</div>
);
}
function EmptyShelf({
text,
}: {
text: string;
}) {
function EmptyShelf({ text }: { text: string }) {
return (
<div
className="pixel-nine-slice pixel-panel rounded-[1.35rem] text-sm leading-6 text-zinc-300"
@@ -150,7 +140,173 @@ function WorldCard({
);
}
function PlatformTabButton({
active,
label,
iconSrc,
onClick,
}: {
active: boolean;
label: string;
iconSrc: string;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className={`flex h-full w-full items-center justify-center rounded-[1rem] px-2 py-1.5 transition ${
active
? 'border border-white/15 bg-white/8 text-white shadow-[inset_0_1px_0_rgba(255,255,255,0.08)]'
: 'text-zinc-400 hover:bg-white/5 hover:text-zinc-100'
}`}
>
<span className="flex flex-col items-center justify-center gap-1">
<span
className={`flex h-6 w-6 items-center justify-center rounded-full ${
active
? 'bg-white/10 shadow-[0_0_14px_rgba(255,255,255,0.14)]'
: 'bg-black/10'
}`}
>
<PixelIcon
src={iconSrc}
className={`h-[1.125rem] w-[1.125rem] ${
active ? 'opacity-100' : 'opacity-65 grayscale'
}`}
/>
</span>
<span
className={`text-[11px] font-semibold tracking-[0.18em] ${
active ? 'text-white' : 'text-zinc-400'
}`}
>
{label}
</span>
</span>
</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 formatPlayTime(playTimeMs: number) {
const totalSeconds = Math.max(0, Math.floor(playTimeMs / 1000));
const days = Math.floor(totalSeconds / 86400);
const hours = Math.floor((totalSeconds % 86400) / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
if (days > 0) {
return `${days}${hours}小时`;
}
if (hours > 0) {
return `${hours}小时 ${minutes}`;
}
return `${minutes}`;
}
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({
label,
value,
icon,
}: {
label: string;
value: string;
icon: ComponentType<{ className?: string }>;
}) {
const Icon = icon;
return (
<div className="rounded-[1.35rem] border border-white/10 bg-black/18 px-4 py-3">
<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-white">{value}</div>
</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="flex min-h-[5.25rem] flex-col items-center justify-center gap-2 rounded-[1.2rem] border border-white/10 bg-black/16 px-3 py-3 text-center transition hover:border-white/20 hover:bg-white/6"
>
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/8 text-zinc-100">
<Icon className="h-[1.125rem] w-[1.125rem]" />
</div>
<div className="text-sm font-semibold text-white">{label}</div>
</button>
);
}
export function PlatformHomeView({
activeTab,
onTabChange,
hasSavedGame,
savedSnapshot,
featuredEntries,
@@ -159,11 +315,13 @@ export function PlatformHomeView({
isLoadingPlatform,
platformError,
onContinueGame,
onRefresh,
onOpenCreateWorld,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenLibraryDetail,
}: {
activeTab: PlatformHomeTab;
onTabChange: (tab: PlatformHomeTab) => void;
hasSavedGame: boolean;
savedSnapshot: HydratedSavedGameSnapshot | null;
featuredEntries: CustomWorldGalleryCard[];
@@ -172,12 +330,18 @@ export function PlatformHomeView({
isLoadingPlatform: boolean;
platformError: string | null;
onContinueGame: () => void;
onRefresh: () => void;
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: CustomWorldGalleryCard) => void;
onOpenLibraryDetail: (entry: CustomWorldLibraryEntry<CustomWorldProfile>) => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
}) {
const authUi = useAuthUi();
const featuredShelf = useMemo(
() => featuredEntries.slice(0, 6),
[featuredEntries],
);
const snapshotWorldName =
savedSnapshot?.gameState.customWorldProfile?.name ??
savedSnapshot?.gameState.currentScenePreset?.name ??
@@ -186,169 +350,401 @@ export function PlatformHomeView({
savedSnapshot?.gameState.playerCharacter?.title ??
savedSnapshot?.gameState.playerCharacter?.name ??
'旅人';
const featuredShelf = featuredEntries.slice(0, 6);
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 = savedSnapshot?.gameState.playerCurrency ?? 0;
const totalPlayTime = formatPlayTime(
savedSnapshot?.gameState.runtimeStats.playTimeMs ?? 0,
);
const playedWorkCount = hasSavedGame ? 1 : 0;
const tabIcons = {
home: "/Icons/Admurin's Pixel Items/Admurin's Pixel Items/Miscellaneous/Singles/192_RustyTrinket_House.png",
create: '/Icons/01_Scroll.png',
profile: '/UI/Icon_Eq_Head.png',
} as const;
const recentPlayItems = savedSnapshot
? [
{
id: 'latest-save',
title: snapshotWorldName,
subtitle: snapshotCharacterName,
summary: snapshotDigest,
updatedAt: savedSnapshot.savedAt,
},
]
: [];
return (
<div className="flex h-full min-h-0 flex-col">
<div className="mb-4 flex items-center justify-between gap-3">
<div className="flex min-w-0 items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full border border-amber-300/20 bg-amber-500/10">
<PixelIcon src={CHROME_ICONS.refreshOptions} className="h-5 w-5" />
</div>
<div className="min-w-0">
<div className="text-[10px] tracking-[0.24em] text-zinc-500">
GENARRATIVE PLATFORM
let content = (
<div className="space-y-4 pb-2">
<button
type="button"
onClick={hasSavedGame ? onContinueGame : onOpenCreateWorld}
className="pixel-nine-slice pixel-pressable relative block w-full overflow-hidden text-left"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 18,
paddingY: 16,
})}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(250,204,21,0.16),transparent_36%),linear-gradient(135deg,rgba(15,23,42,0.78),rgba(8,10,14,0.95))]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<div className="flex items-start justify-between gap-4">
<span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-amber-100">
{hasSavedGame ? 'CONTINUE' : 'CREATE'}
</span>
<div className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] text-zinc-100">
{hasSavedGame ? '继续冒险' : '创建世界'}
</div>
<div className="truncate text-lg font-black text-white">
广
</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>
</div>
<div className="flex items-center gap-2">
<button
type="button"
onClick={onRefresh}
className="rounded-full border border-white/10 bg-black/25 px-3 py-2 text-[11px] text-zinc-200 transition hover:border-white/20 hover:text-white"
>
</button>
{authUi?.user ? (
<button
type="button"
onClick={() => authUi.openAccountModal()}
className="rounded-full border border-white/10 bg-black/25 px-3 py-2 text-[11px] text-zinc-100 transition hover:border-white/20 hover:text-white"
</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="space-y-4 pb-2">
<button
type="button"
onClick={onOpenCreateTypePicker}
className="pixel-nine-slice pixel-pressable relative block w-full overflow-hidden text-left"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 18,
paddingY: 16,
})}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.18),transparent_38%),linear-gradient(180deg,rgba(8,10,14,0.18),rgba(8,10,14,0.92))]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<span className="w-fit rounded-full border border-sky-300/20 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
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>
</div>
</button>
<section>
<SectionHeader title="我的创作" detail="草稿与已发布" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取你的作品..." />
) : myEntries.length > 0 ? (
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
{myEntries.map(
(entry: CustomWorldLibraryEntry<CustomWorldProfile>) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:mine`}
entry={entry}
badge={entry.visibility === 'published' ? '已发布' : '草稿'}
metaLabel={
entry.visibility === 'published'
? formatPlatformWorldTime(entry.publishedAt)
: '仅自己可见'
}
onClick={() => onOpenLibraryDetail(entry)}
/>
),
)}
</div>
) : (
<EmptyShelf text="你还没有保存任何自定义世界,先创建一个草稿开始吧。" />
)}
</section>
</div>
);
}
if (activeTab === 'profile') {
content = (
<div className="space-y-4 pb-2">
{authUi?.user ? (
<>
<section className="overflow-hidden rounded-[1.8rem] border border-white/10 bg-[linear-gradient(180deg,rgba(248,244,236,0.96),rgba(232,225,214,0.92))] p-4 text-slate-900 shadow-[0_18px_50px_rgba(0,0,0,0.18)]">
<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="relative h-16 w-16 shrink-0 rounded-[1.4rem] bg-[linear-gradient(135deg,#2a3141,#66718a)] text-white shadow-[0_12px_24px_rgba(15,23,42,0.22)]"
>
<span className="flex h-full w-full items-center justify-center text-2xl font-black">
{avatarLabel}
</span>
<span className="absolute -bottom-1 -right-1 flex h-6 w-6 items-center justify-center rounded-full border border-white/30 bg-white/85 text-slate-700">
<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-slate-900">
{authUi.user.displayName}
</div>
<button
type="button"
onClick={() => authUi.openAccountModal()}
className="flex h-7 w-7 items-center justify-center rounded-full bg-slate-900/6 text-slate-700 transition hover:bg-slate-900/10"
>
<Pencil className="h-3.5 w-3.5" />
</button>
</div>
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-slate-500">
<span> {publicUserCode}</span>
<button
type="button"
onClick={() => copyText(publicUserCode)}
className="flex items-center gap-1 rounded-full bg-slate-900/6 px-2 py-1 text-slate-700 transition hover:bg-slate-900/10"
>
<Copy className="h-3 w-3" />
</button>
</div>
<div className="mt-2 flex flex-wrap items-center gap-2 text-xs">
<span className="rounded-full bg-slate-900/6 px-2.5 py-1 text-slate-700">
{describeLoginMethod(authUi.user.loginMethod)}
</span>
<span className="rounded-full bg-slate-900/6 px-2.5 py-1 text-slate-700">
{describeBindingStatus(authUi.user.bindingStatus)}
</span>
</div>
</div>
</div>
<button
type="button"
className="flex shrink-0 items-center gap-2 rounded-[1.1rem] bg-[linear-gradient(135deg,#5d79ff,#8ba2ff)] px-3 py-2 text-left text-white shadow-[0_12px_24px_rgba(93,121,255,0.28)]"
>
<Crown className="h-4 w-4" />
<div>
<div className="text-xs font-bold"></div>
<div className="text-[10px] text-white/80"></div>
</div>
<ChevronRight className="h-4 w-4 opacity-80" />
</button>
</div>
</section>
<section
className="pixel-nine-slice"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
{authUi.user.displayName}
</button>
) : null}
<div className="grid grid-cols-3 gap-3">
<ProfileStatCard
label="剩余叙世币"
value={`${remainingNarrativeCoins}`}
icon={Coins}
/>
<ProfileStatCard
label="总游戏时长"
value={totalPlayTime}
icon={Clock3}
/>
<ProfileStatCard
label="玩过作品"
value={`${playedWorkCount}`}
icon={BookOpen}
/>
</div>
</section>
<section
className="pixel-nine-slice"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<SectionHeader title="最近游玩" detail="继续上次进度" />
{recentPlayItems.length > 0 ? (
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{recentPlayItems.map((item) => (
<button
key={item.id}
type="button"
onClick={onContinueGame}
className="relative flex h-[10.5rem] w-[17rem] shrink-0 overflow-hidden rounded-[1.4rem] border border-white/10 bg-[linear-gradient(135deg,rgba(25,32,46,0.95),rgba(9,12,18,0.96))] p-4 text-left"
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_right,rgba(96,165,250,0.18),transparent_35%),radial-gradient(circle_at_bottom_left,rgba(250,204,21,0.12),transparent_28%)]" />
<div className="relative z-10 flex h-full flex-col">
<div className="flex items-start justify-between gap-3">
<span className="rounded-full border border-sky-300/20 bg-sky-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-sky-100">
RECENT PLAY
</span>
<span className="text-[11px] text-zinc-400">
{formatSnapshotTime(item.updatedAt)}
</span>
</div>
<div className="mt-auto">
<div className="line-clamp-1 text-xl font-black text-white">
{item.title}
</div>
<div className="mt-1 text-sm text-zinc-300">
{item.subtitle}
</div>
<div className="mt-2 line-clamp-3 text-xs leading-5 text-zinc-400">
{item.summary}
</div>
</div>
</div>
</button>
))}
</div>
) : (
<EmptyShelf text="还没有最近游玩的存档,去首页挑一个世界开始冒险吧。" />
)}
</section>
<section
className="pixel-nine-slice"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<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="pixel-nine-slice"
style={getNineSliceStyle(UI_CHROME.panel, {
paddingX: 16,
paddingY: 14,
})}
>
<button
type="button"
onClick={() => authUi.openAccountModal()}
className="flex w-full items-center justify-between gap-3 rounded-[1.25rem] border border-white/10 bg-black/16 px-4 py-4 text-left transition hover:border-white/20 hover:bg-white/6"
>
<div className="flex items-center gap-3">
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-white/8 text-zinc-100">
<Settings className="h-[1.125rem] w-[1.125rem]" />
</div>
<div>
<div className="text-base font-semibold text-white"></div>
<div className="text-xs text-zinc-400"></div>
</div>
</div>
<ChevronRight className="h-4 w-4 text-zinc-500" />
</button>
</section>
</>
) : (
<EmptyShelf text="当前还没有读取到账户信息。" />
)}
</div>
);
}
return (
<div className="flex h-full min-h-0 flex-col">
<div className="mb-4">
<div className="text-lg font-black text-white"></div>
<div className="mt-1 text-[10px] tracking-[0.28em] text-zinc-500">
GENARRATIVE
</div>
</div>
<div className="min-h-0 flex-1 overflow-y-auto pr-1 scrollbar-hide">
<div className="space-y-4 pb-2">
<button
type="button"
onClick={hasSavedGame ? onContinueGame : onOpenCreateWorld}
className="pixel-nine-slice pixel-pressable relative block w-full overflow-hidden text-left"
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 18, paddingY: 16 })}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(250,204,21,0.16),transparent_36%),linear-gradient(135deg,rgba(15,23,42,0.78),rgba(8,10,14,0.95))]" />
<div className="relative z-10 flex min-h-[10rem] flex-col justify-between">
<div className="flex items-start justify-between gap-4">
<span className="rounded-full border border-amber-300/20 bg-amber-500/10 px-3 py-1 text-[10px] tracking-[0.2em] text-amber-100">
{hasSavedGame ? 'CONTINUE' : 'CREATE'}
</span>
<div className="rounded-full border border-white/10 bg-black/30 px-3 py-1 text-[11px] text-zinc-100">
{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} 的上一次冒险已保存在云端,点这里直接回到故事现场。`
: '从设定、角色到场景网络,一次生成一部可游玩的自定义 RPG再决定是否发布到广场。'}
</div>
</div>
</div>
</button>
{content}
</div>
{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="为你挑选"
actionLabel="看看最新"
onAction={onRefresh}
/>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取精选作品..." />
) : featuredShelf.length > 0 ? (
<div className="flex gap-3 overflow-x-auto pb-1 scrollbar-hide">
{featuredShelf.map((entry) => (
<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) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:latest`}
entry={entry}
badge={formatPlatformWorldTime(entry.publishedAt)}
metaLabel={entry.authorDisplayName}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有新作品。" />
)}
</section>
<section>
<SectionHeader title="我的作品" detail="草稿与已发布" />
<div className="grid gap-3 md:grid-cols-2 xl:grid-cols-3">
<button
type="button"
onClick={onOpenCreateWorld}
className="pixel-nine-slice pixel-pressable relative min-h-[13rem] overflow-hidden text-left"
style={getNineSliceStyle(UI_CHROME.panel, { paddingX: 18, paddingY: 16 })}
>
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,rgba(56,189,248,0.16),transparent_36%),linear-gradient(180deg,rgba(8,10,14,0.2),rgba(8,10,14,0.92))]" />
<div className="relative z-10 flex h-full flex-col">
<div className="flex h-10 w-10 items-center justify-center rounded-full border border-sky-300/20 bg-sky-500/10">
<PixelIcon src={CHROME_ICONS.refreshOptions} className="h-5 w-5" />
</div>
<div className="mt-auto">
<div className="text-2xl font-black text-white">
</div>
<div className="mt-2 text-sm leading-6 text-zinc-300">
稿
</div>
</div>
</div>
</button>
{myEntries.map((entry) => (
<WorldCard
key={`${entry.ownerUserId}:${entry.profileId}:mine`}
entry={entry}
badge={entry.visibility === 'published' ? '已发布' : '草稿'}
metaLabel={entry.visibility === 'published' ? formatPlatformWorldTime(entry.publishedAt) : '仅自己可见'}
onClick={() => onOpenLibraryDetail(entry)}
/>
))}
</div>
{!isLoadingPlatform && myEntries.length === 0 ? (
<div className="mt-3">
<EmptyShelf text="你还没有保存任何自定义世界,先创建一个草稿开始吧。" />
</div>
) : null}
</section>
<div
className="mt-4 border-t border-white/5 pt-3"
style={{ paddingBottom: 'calc(env(safe-area-inset-bottom) + 0.2rem)' }}
>
<div className="grid h-14 grid-cols-3 gap-1 rounded-[1.2rem] bg-black/18 px-1 py-1">
<PlatformTabButton
active={activeTab === 'home'}
label="首页"
iconSrc={tabIcons.home}
onClick={() => onTabChange('home')}
/>
<PlatformTabButton
active={activeTab === 'create'}
label="创作"
iconSrc={tabIcons.create}
onClick={() => onTabChange('create')}
/>
<PlatformTabButton
active={activeTab === 'profile'}
label="我的"
iconSrc={tabIcons.profile}
onClick={() => onTabChange('profile')}
/>
</div>
</div>
</div>