1
This commit is contained in:
@@ -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 ? (
|
||||
|
||||
Reference in New Issue
Block a user