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 (
);
}
function EmptyShelf({ text }: { text: string }) {
return (
{text}
);
}
function SaveArchivePreview({
entry,
label,
className,
}: {
entry: ProfileSaveArchiveSummary;
label: string;
className: string;
}) {
return (
{entry.coverImageSrc ? (

) : (
)}
{label}
);
}
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 (
);
}
function CreationLibraryCard({
entry,
onClick,
}: {
entry: CustomWorldLibraryEntry;
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 (
);
}
function SaveArchiveCard({
entry,
onClick,
loading = false,
}: {
entry: ProfileSaveArchiveSummary;
onClick: () => void;
loading?: boolean;
}) {
const summaryText =
entry.summaryText || entry.subtitle || '继续推进上一次保存的故事。';
return (
);
}
function PlatformTabButton({
active,
label,
icon: Icon,
onClick,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
}) {
return (
);
}
function DesktopTabButton({
active,
label,
icon: Icon,
onClick,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
}) {
return (
);
}
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 (
);
}
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 (
);
}
function ProfileStatCardSkeleton() {
return (
);
}
function ProfileShortcutButton({
label,
icon,
onClick,
}: {
label: string;
icon: ComponentType<{ className?: string }>;
onClick?: (() => void) | null;
}) {
const Icon = icon;
return (
);
}
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[];
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,
) => 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 = (
{platformError ? (
{platformError}
) : null}
{isLoadingPlatform ? (
) : featuredShelf.length > 0 ? (
{featuredShelf.map((entry: CustomWorldGalleryCard) => (
onOpenGalleryDetail(entry)}
/>
))}
) : (
)}
{isLoadingPlatform ? (
) : latestEntries.length > 0 ? (
{latestEntries.map((entry: CustomWorldGalleryCard) => (
onOpenGalleryDetail(entry)}
/>
))}
) : (
)}
);
if (activeTab === 'create') {
content = (
{isLoadingPlatform ? (
) : myEntries.length > 0 ? (
{myEntries.map(
(entry: CustomWorldLibraryEntry) => (
onOpenLibraryDetail(entry)}
/>
),
)}
) : (
)}
);
}
if (activeTab === 'saves') {
content = (
{authUi?.user ? (
<>
{saveError ? (
{saveError}
) : null}
{isLoadingPlatform ? (
) : saveEntries.length > 0 ? (
{saveEntries.map((entry) => (
onResumeSave(entry)}
/>
))}
) : (
)}
>
) : (
尚未登录
)}
);
}
if (activeTab === 'profile') {
content = (
{authUi?.user ? (
<>
{authUi.user.displayName}
叙世号 {publicUserCode}
{describeLoginMethod(authUi.user.loginMethod)}
{describeBindingStatus(authUi.user.bindingStatus)}
{isLoadingDashboard ? (
<>
>
) : dashboardError ? (
<>
>
) : (
<>
>
)}
{dashboardError
? dashboardError
: `更新于 ${formatDashboardUpdatedAt(profileDashboard?.updatedAt)}`}
>
) : (
尚未登录
)}
);
}
const desktopContent =
activeTab === 'home' ? (
{platformError ? (
{platformError}
) : null}
LIVE
{isLoadingPlatform ? (
) : desktopTrendingEntries.length > 0 ? (
{desktopTrendingEntries.map((entry, index) => (
onOpenGalleryDetail(entry)}
/>
))}
) : (
)}
{isLoadingPlatform ? (
) : desktopFeaturedGrid.length > 0 ? (
{desktopFeaturedGrid.map((entry) => (
onOpenGalleryDetail(entry)}
className="h-[16rem] w-full min-w-0"
/>
))}
) : (
)}
{desktopLibraryPreview.length > 0
? '最近作品'
: historyEntries.length > 0
? '最近浏览'
: isAuthenticated
? '创作状态'
: '账户状态'}
{desktopLibraryPreview.length > 0 ? (
{desktopLibraryPreview.map((entry) => (
))}
) : historyEntries.length > 0 ? (
{historyEntries.slice(0, 2).map((entry) => (
))}
) : (
{isAuthenticated
? '创建一个草稿后,这里会出现你最近保存的作品。'
: '登录后可同步你的创作、游玩进度与平台资料。'}
)}
{isLoadingPlatform ? (
) : desktopReleaseGrid.length > 0 ? (
{desktopReleaseGrid.map((entry) => (
onOpenGalleryDetail(entry)}
className="h-[17rem] w-full min-w-0"
/>
))}
) : (
)}
) : (
content
);
return (
{content}
onTabChange('home')}
/>
onTabChange('create')}
/>
onTabChange('saves')}
/>
onTabChange('profile')}
/>
{desktopContent}
);
}