This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions

View File

@@ -1,5 +1,4 @@
import {
Archive,
ArrowRight,
Bell,
BookOpen,
@@ -8,20 +7,20 @@ import {
ChevronRight,
Clock3,
Coins,
Compass,
Copy,
Gamepad2,
Heart,
House,
LogIn,
MessageCircle,
Pencil,
Plus,
Search,
Settings,
SlidersHorizontal,
Sparkles,
Star,
Ticket,
Trophy,
UserPlus,
UserRound,
} from 'lucide-react';
@@ -83,6 +82,7 @@ import {
isMatch3DGalleryEntry,
isPuzzleGalleryEntry,
isSquareHoleGalleryEntry,
isVisualNovelGalleryEntry,
type PlatformPublicGalleryCard,
type PlatformWorldCardLike,
resolvePlatformWorldCoverImage,
@@ -136,6 +136,7 @@ export interface RpgEntryHomeViewProps {
onOpenPlayedWork?: (work: ProfilePlayedWorkSummary) => void;
onRechargeSuccess?: () => void | Promise<void>;
createTabContent?: ReactNode;
draftTabContent?: ReactNode;
}
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
@@ -161,7 +162,7 @@ const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
type MobileHomeChannel = 'recommend' | 'today' | 'category';
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
const COMMUNITY_QR_CODES = [
@@ -177,13 +178,14 @@ const COMMUNITY_QR_CODES = [
},
] as const;
const MOBILE_HOME_CHANNELS: Array<{
id: MobileHomeChannel;
const DISCOVER_CHANNELS: Array<{
id: DiscoverChannel;
label: string;
}> = [
{ id: 'recommend', label: '推荐' },
{ id: 'today', label: '今日游戏' },
{ id: 'category', label: '游戏分类' },
{ id: 'today', label: '今日' },
{ id: 'category', label: '分类' },
{ id: 'ranking', label: '排行' },
];
const PLATFORM_RANKING_TABS: Array<{
@@ -377,6 +379,7 @@ function WorldCard({
feedCardKey,
enableCoverCarousel = false,
isCoverCarouselActive = false,
variant = 'standard',
}: {
entry: PlatformPublicGalleryCard;
onClick: () => void;
@@ -385,6 +388,7 @@ function WorldCard({
feedCardKey?: string;
enableCoverCarousel?: boolean;
isCoverCarouselActive?: boolean;
variant?: 'standard' | 'immersive';
}) {
const fallbackCoverImage = resolvePlatformWorldCoverImage(entry);
const coverSlides = useMemo(() => {
@@ -465,7 +469,7 @@ function WorldCard({
onClick={onClick}
aria-label={cardLabel}
data-mobile-feed-card-key={feedCardKey}
className={`platform-public-work-card platform-surface platform-interactive-card relative flex w-[min(21rem,88vw)] shrink-0 flex-col overflow-hidden p-0 text-left ${className ?? ''}`}
className={`platform-public-work-card ${variant === 'immersive' ? 'platform-public-work-card--immersive' : ''} platform-surface platform-interactive-card relative flex w-[min(21rem,88vw)] shrink-0 flex-col overflow-hidden p-0 text-left ${className ?? ''}`}
>
<div className="platform-public-work-card__cover relative aspect-video overflow-hidden">
{coverImage ? (
@@ -724,6 +728,7 @@ function PlatformTabButton({
<button
type="button"
onClick={onClick}
aria-label={label}
className={`platform-bottom-nav__button ${emphasized ? 'platform-bottom-nav__button--primary' : ''} ${active ? 'platform-bottom-nav__button--active' : ''}`}
>
<span className="platform-bottom-nav__button-content">
@@ -874,7 +879,10 @@ function PlatformRankingItem({
const coverImage = resolvePlatformWorldCoverImage(entry);
const displayName = formatPlatformWorkDisplayName(entry.worldName);
const tags = buildPlatformWorldDisplayTags(entry, 2);
const actionLabel = isPuzzleGalleryEntry(entry) ? '试玩' : '进入';
const actionLabel =
isPuzzleGalleryEntry(entry) || isVisualNovelGalleryEntry(entry)
? '试玩'
: '进入';
return (
<button
@@ -936,7 +944,10 @@ function PlatformCategoryGameItem({
describePublicGalleryCardKind(entry),
...tags.filter((tag) => tag !== categoryTag),
].slice(0, 3);
const actionLabel = isPuzzleGalleryEntry(entry) ? '试玩' : '进入';
const actionLabel =
isPuzzleGalleryEntry(entry) || isVisualNovelGalleryEntry(entry)
? '试玩'
: '进入';
const summaryText =
entry.summaryText || entry.subtitle || `${displayName} 正在等待摘要。`;
@@ -1139,6 +1150,8 @@ function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
@@ -1249,6 +1262,8 @@ function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
? '抓鹅'
: isSquareHoleGalleryEntry(entry)
? '方洞'
: isVisualNovelGalleryEntry(entry)
? '视觉'
: describePlatformThemeLabel(entry.themeMode);
return formatPlatformWorkDisplayTag(kind);
}
@@ -2549,20 +2564,30 @@ function ProfilePlayedWorksModal({
stats,
isLoading,
error,
saveEntries,
saveError,
isResumingSaveWorldKey,
onClose,
onOpenWork,
onResumeSave,
}: {
stats: ProfilePlayStatsResponse | null;
isLoading: boolean;
error: string | null;
saveEntries: ProfileSaveArchiveSummary[];
saveError: string | null;
isResumingSaveWorldKey: string | null;
onClose: () => void;
onOpenWork?: (work: ProfilePlayedWorkSummary) => void;
onResumeSave: (entry: ProfileSaveArchiveSummary) => void;
}) {
const playedWorks = stats?.playedWorks ?? [];
const hasArchiveEntries = saveEntries.length > 0;
const hasPlayedWorks = playedWorks.length > 0;
return (
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/48 px-3 py-5">
<div className="relative max-h-[min(92vh,42rem)] w-full max-w-[34rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
<div className="relative max-h-[min(92vh,42rem)] w-full max-w-[38rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
<button
type="button"
onClick={onClose}
@@ -2590,6 +2615,11 @@ function ProfilePlayedWorksModal({
{error}
</div>
) : null}
{saveError ? (
<div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">
{saveError}
</div>
) : null}
{isLoading ? (
<div className="mt-5 space-y-3">
@@ -2600,43 +2630,73 @@ function ProfilePlayedWorksModal({
/>
))}
</div>
) : playedWorks.length > 0 ? (
<div className="mt-5 space-y-3">
{playedWorks.map((work) => (
<button
type="button"
key={`${work.worldKey}:${work.lastPlayedAt}`}
onClick={() => onOpenWork?.(work)}
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left transition hover:border-[#ff4056] hover:bg-white"
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<div className="line-clamp-1 text-base font-black text-zinc-950">
{work.worldTitle}
</div>
{work.worldSubtitle ? (
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
{work.worldSubtitle}
) : hasArchiveEntries || hasPlayedWorks ? (
<div className="mt-5 space-y-5">
{hasArchiveEntries ? (
<section>
<div className="mb-2 text-xs font-black text-zinc-500">
</div>
<div className="grid gap-3">
{saveEntries.map((entry) => (
<SaveArchiveCard
key={`${entry.worldKey}:played-archive`}
entry={entry}
loading={isResumingSaveWorldKey === entry.worldKey}
onClick={() => onResumeSave(entry)}
/>
))}
</div>
</section>
) : null}
{hasPlayedWorks ? (
<section>
<div className="mb-2 text-xs font-black text-zinc-500">
</div>
<div className="space-y-3">
{playedWorks.map((work) => (
<button
type="button"
key={`${work.worldKey}:${work.lastPlayedAt}`}
onClick={() => onOpenWork?.(work)}
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left transition hover:border-[#ff4056] hover:bg-white"
>
<div className="flex min-w-0 items-start justify-between gap-3">
<div className="min-w-0">
<div className="line-clamp-1 text-base font-black text-zinc-950">
{work.worldTitle}
</div>
{work.worldSubtitle ? (
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
{work.worldSubtitle}
</div>
) : null}
</div>
<span className="shrink-0 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-bold text-[#ff4056]">
{formatPlayedWorkType(work.worldType)}
</span>
</div>
) : null}
</div>
<span className="shrink-0 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-bold text-[#ff4056]">
{formatPlayedWorkType(work.worldType)}
</span>
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
<span className="truncate">
{formatPlayedWorkId(work)}
</span>
<span className="truncate">
{formatSnapshotTime(work.lastPlayedAt)}
</span>
<span className="truncate">
{' '}
{formatCompactPlayTime(
work.lastObservedPlayTimeMs,
)}
</span>
</div>
</button>
))}
</div>
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
<span className="truncate">
{formatPlayedWorkId(work)}
</span>
<span className="truncate">
{formatSnapshotTime(work.lastPlayedAt)}
</span>
<span className="truncate">
{formatCompactPlayTime(work.lastObservedPlayTimeMs)}
</span>
</div>
</button>
))}
</section>
) : null}
</div>
) : (
<div className="mt-5 rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-sm text-zinc-600">
@@ -2681,6 +2741,7 @@ export function RpgEntryHomeView({
onOpenPlayedWork,
onRechargeSuccess,
createTabContent,
draftTabContent,
}: RpgEntryHomeViewProps) {
const authUi = useAuthUi();
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
@@ -2733,9 +2794,10 @@ export function RpgEntryHomeView({
const [selectedCategoryTag, setSelectedCategoryTag] = useState<string | null>(
null,
);
const [mobileHomeChannel, setMobileHomeChannel] =
useState<MobileHomeChannel>('recommend');
const mobileFeedRef = useRef<HTMLElement | null>(null);
const [discoverChannel, setDiscoverChannel] =
useState<DiscoverChannel>('recommend');
const mobileRecommendFeedRef = useRef<HTMLElement | null>(null);
const mobileDiscoverFeedRef = useRef<HTMLElement | null>(null);
const [mobileCenteredCardKey, setMobileCenteredCardKey] = useState<
string | null
>(null);
@@ -2842,18 +2904,29 @@ export function RpgEntryHomeView({
isReferralCenterInitialized &&
Boolean(referralCenter) &&
referralCenter?.hasRedeemedCode !== true;
const tabIcons = {
home: House,
category: Trophy,
create: Sparkles,
saves: Archive,
profile: UserRound,
} as const;
const tabIcons: Record<
PlatformHomeTab,
ComponentType<{ className?: string }>
> = isAuthenticated
? {
home: Sparkles,
category: Compass,
create: Plus,
saves: Pencil,
profile: UserRound,
}
: {
home: Gamepad2,
category: Compass,
create: Sparkles,
saves: Pencil,
profile: UserRound,
};
const tabLabels = {
home: '首页',
category: '排行',
home: '推荐',
category: '发现',
create: '创作',
saves: '存档',
saves: '草稿',
profile: '我的',
} as const;
@@ -3398,11 +3471,19 @@ export function RpgEntryHomeView({
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
const desktopLibraryPreview = myEntries.slice(0, 2);
const mobileFeedEntries = useMemo(() => {
const recommendedFeedEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredShelf, ...latestEntries].forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
return Array.from(entryMap.values());
}, [featuredShelf, latestEntries]);
const discoverFeedEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
const sourceEntries =
mobileHomeChannel === 'recommend'
? [...featuredShelf, ...latestEntries]
discoverChannel === 'recommend'
? recommendedFeedEntries
: filterTodayPublishedEntries(latestEntries);
sourceEntries.forEach((entry) => {
@@ -3410,16 +3491,22 @@ export function RpgEntryHomeView({
});
return Array.from(entryMap.values());
}, [featuredShelf, latestEntries, mobileHomeChannel]);
}, [discoverChannel, latestEntries, recommendedFeedEntries]);
const mobileFeedCarouselEnabled =
!isDesktopLayout && activeTab === 'home' && mobileHomeChannel !== 'category';
!isDesktopLayout &&
((activeTab === 'home' && recommendedFeedEntries.length > 0) ||
(activeTab === 'category' &&
(discoverChannel === 'recommend' || discoverChannel === 'today')));
useEffect(() => {
if (!mobileFeedCarouselEnabled) {
setMobileCenteredCardKey(null);
return undefined;
}
const feedElement = mobileFeedRef.current;
const feedElement =
activeTab === 'home'
? mobileRecommendFeedRef.current
: mobileDiscoverFeedRef.current;
const scrollElement = feedElement?.closest('.platform-tab-panel');
if (!feedElement || !scrollElement) {
setMobileCenteredCardKey(null);
@@ -3490,7 +3577,13 @@ export function RpgEntryHomeView({
scrollElement.removeEventListener('scroll', scheduleUpdate);
window.removeEventListener('resize', scheduleUpdate);
};
}, [mobileFeedCarouselEnabled, mobileFeedEntries, mobileHomeChannel]);
}, [
discoverChannel,
discoverFeedEntries,
activeTab,
mobileFeedCarouselEnabled,
recommendedFeedEntries,
]);
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
(tab) => tab.id === activeRankingTab,
) as (typeof PLATFORM_RANKING_TABS)[number];
@@ -3499,9 +3592,6 @@ export function RpgEntryHomeView({
buildPlatformRankingEntries(publicEntries, activeRankingTab).slice(0, 30),
[activeRankingTab, publicEntries],
);
const categoryPageClass = isDesktopLayout
? DESKTOP_PAGE_STAGE_CLASS
: MOBILE_PAGE_STAGE_CLASS;
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
@@ -3512,7 +3602,101 @@ export function RpgEntryHomeView({
onTabChange('category');
};
const mobileHomeContent: ReactNode = (
const mobileRankingPanel: ReactNode = (
<section
className={`${PANEL_SURFACE_CLASS} platform-ranking-panel px-4 py-3.5 sm:px-5 sm:py-4`}
>
<div
className="platform-ranking-tabs flex min-w-0 gap-2 overflow-x-auto pb-1 scrollbar-hide"
role="tablist"
aria-label="作品排行"
>
{PLATFORM_RANKING_TABS.map((tab) => {
const active = tab.id === activeRankingTab;
return (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={active}
onClick={() => setActiveRankingTab(tab.id)}
className={`platform-ranking-tab shrink-0 ${active ? 'platform-ranking-tab--active' : ''}`}
>
{tab.label}
</button>
);
})}
</div>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : rankingEntries.length > 0 ? (
<div className="mt-3 grid min-w-0 gap-2.5">
{rankingEntries.map((entry, index) => (
<PlatformRankingItem
key={`${buildPublicGalleryCardKey(entry)}:ranking:${activeRankingTab}`}
entry={entry}
rank={index + 1}
metricLabel={activeRankingConfig.metricLabel}
metricValue={getPlatformRankingMetricValue(
entry,
activeRankingTab,
)}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text={activeRankingConfig.emptyText} />
)}
</section>
);
const mobileRecommendContent: ReactNode = (
<div
className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage platform-mobile-recommend-stage`}
>
{platformError ? (
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
{platformError}
</div>
) : null}
<section
ref={mobileRecommendFeedRef}
className="platform-mobile-home-feed platform-mobile-recommend-feed"
>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : recommendedFeedEntries.length > 0 ? (
<div className="grid min-w-0 gap-4">
{recommendedFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
return (
<WorldCard
key={`${cardKey}:mobile-recommend`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
feedCardKey={cardKey}
enableCoverCarousel={mobileFeedCarouselEnabled}
isCoverCarouselActive={mobileCenteredCardKey === cardKey}
variant="immersive"
/>
);
})}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
)}
</section>
</div>
);
const mobileDiscoverContent: ReactNode = (
<div className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage`}>
<PublicCodeSearchBar
value={mobileSearchKeyword}
@@ -3531,13 +3715,13 @@ export function RpgEntryHomeView({
) : (
<>
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
{MOBILE_HOME_CHANNELS.map((channel) => {
const active = mobileHomeChannel === channel.id;
{DISCOVER_CHANNELS.map((channel) => {
const active = discoverChannel === channel.id;
return (
<button
key={channel.id}
type="button"
onClick={() => setMobileHomeChannel(channel.id)}
onClick={() => setDiscoverChannel(channel.id)}
className={`platform-mobile-home-channel shrink-0 ${active ? 'platform-mobile-home-channel--active' : ''}`}
>
{channel.label}
@@ -3552,7 +3736,9 @@ export function RpgEntryHomeView({
</div>
) : null}
{mobileHomeChannel === 'category' ? (
{discoverChannel === 'ranking' ? (
mobileRankingPanel
) : discoverChannel === 'category' ? (
<section className="platform-category-list-panel">
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
@@ -3609,17 +3795,20 @@ export function RpgEntryHomeView({
)}
</section>
) : (
<section ref={mobileFeedRef} className="platform-mobile-home-feed">
<section
ref={mobileDiscoverFeedRef}
className="platform-mobile-home-feed"
>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : mobileFeedEntries.length > 0 ? (
) : discoverFeedEntries.length > 0 ? (
<div className="grid min-w-0 gap-3">
{mobileFeedEntries.map((entry: PlatformPublicGalleryCard) => {
{discoverFeedEntries.map((entry: PlatformPublicGalleryCard) => {
const cardKey = buildPublicGalleryCardKey(entry);
return (
<WorldCard
key={`${cardKey}:mobile-feed:${mobileHomeChannel}`}
key={`${cardKey}:mobile-feed:${discoverChannel}`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
@@ -3641,60 +3830,13 @@ export function RpgEntryHomeView({
</div>
);
const categoryContent: ReactNode = (
<div className={categoryPageClass}>
<section
className={`${PANEL_SURFACE_CLASS} platform-ranking-panel px-4 py-3.5 sm:px-5 sm:py-4`}
>
<div
className="platform-ranking-tabs flex min-w-0 gap-2 overflow-x-auto pb-1 scrollbar-hide"
role="tablist"
aria-label="作品排行"
>
{PLATFORM_RANKING_TABS.map((tab) => {
const active = tab.id === activeRankingTab;
return (
<button
key={tab.id}
type="button"
role="tab"
aria-selected={active}
onClick={() => setActiveRankingTab(tab.id)}
className={`platform-ranking-tab shrink-0 ${active ? 'platform-ranking-tab--active' : ''}`}
>
{tab.label}
</button>
);
})}
</div>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : rankingEntries.length > 0 ? (
<div className="mt-3 grid min-w-0 gap-2.5">
{rankingEntries.map((entry, index) => (
<PlatformRankingItem
key={`${buildPublicGalleryCardKey(entry)}:ranking:${activeRankingTab}`}
entry={entry}
rank={index + 1}
metricLabel={activeRankingConfig.metricLabel}
metricValue={getPlatformRankingMetricValue(
entry,
activeRankingTab,
)}
onClick={() => onOpenGalleryDetail(entry)}
/>
))}
</div>
) : (
<EmptyShelf text={activeRankingConfig.emptyText} />
)}
</section>
</div>
const categoryContent: ReactNode = isDesktopLayout ? (
<div className={DESKTOP_PAGE_STAGE_CLASS}>{mobileRankingPanel}</div>
) : (
mobileDiscoverContent
);
const createContent: ReactNode = createTabContent ?? (
const fallbackCreateStartContent: ReactNode = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
<button
type="button"
@@ -3720,7 +3862,11 @@ export function RpgEntryHomeView({
</div>
</div>
</button>
</div>
);
const fallbackDraftContent: ReactNode = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
<section>
<SectionHeader title="我的创作" detail="草稿与已发布" />
{isLoadingPlatform ? (
@@ -3756,53 +3902,10 @@ export function RpgEntryHomeView({
</div>
);
const savesContent: ReactNode = (
<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-700">
{saveError}
</div>
) : null}
const createContent: ReactNode = createTabContent ?? fallbackCreateStartContent;
<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>
const savesContent: ReactNode = (
draftTabContent ?? fallbackDraftContent
);
const profileContent: ReactNode = (
@@ -4326,7 +4429,7 @@ export function RpgEntryHomeView({
);
const tabContentById = {
home: isDesktopLayout ? desktopHomeContent : mobileHomeContent,
home: isDesktopLayout ? desktopHomeContent : mobileRecommendContent,
category: categoryContent,
create: createContent,
saves: savesContent,
@@ -4399,7 +4502,7 @@ export function RpgEntryHomeView({
if (!isDesktopLayout) {
return (
<div className="platform-mobile-entry-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
<RpgEntryBrandLogo />
{!isAuthenticated ? (
<button
@@ -4410,19 +4513,23 @@ export function RpgEntryHomeView({
<LogIn className="h-3.5 w-3.5" />
</button>
) : null}
) : (
<button
type="button"
onClick={openUserSurface}
className="platform-icon-button platform-mobile-topbar__action shrink-0"
aria-label="通知与账户"
>
<Bell className="h-4 w-4" />
</button>
)}
</div>
<div className="platform-tab-panel-stack min-w-0 flex-1">
{tabPanels}
</div>
<div
className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0 border-t pt-2"
style={{
borderColor: 'var(--platform-line-soft)',
}}
>
<div className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0">
<div
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : visibleTabs.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}
>
@@ -4472,8 +4579,12 @@ export function RpgEntryHomeView({
stats={profilePlayStats}
isLoading={isProfilePlayStatsLoading}
error={profilePlayStatsError}
saveEntries={saveEntries}
saveError={saveError}
isResumingSaveWorldKey={isResumingSaveWorldKey}
onClose={onCloseProfilePlayStats ?? (() => undefined)}
onOpenWork={onOpenPlayedWork}
onResumeSave={onResumeSave}
/>
) : null}
{isWalletLedgerOpen ? (
@@ -4606,8 +4717,12 @@ export function RpgEntryHomeView({
stats={profilePlayStats}
isLoading={isProfilePlayStatsLoading}
error={profilePlayStatsError}
saveEntries={saveEntries}
saveError={saveError}
isResumingSaveWorldKey={isResumingSaveWorldKey}
onClose={onCloseProfilePlayStats ?? (() => undefined)}
onOpenWork={onOpenPlayedWork}
onResumeSave={onResumeSave}
/>
) : null}
{isWalletLedgerOpen ? (