This commit is contained in:
2026-05-11 20:27:41 +08:00
parent e30b733b17
commit 481a27fc53
60 changed files with 6357 additions and 1100 deletions

View File

@@ -9,16 +9,16 @@ import {
Coins,
Compass,
Copy,
GitFork,
Gamepad2,
GitFork,
Heart,
LogIn,
MessageCircle,
Pencil,
Plus,
Search,
Share2,
Settings,
Share2,
SlidersHorizontal,
Sparkles,
Star,
@@ -161,6 +161,7 @@ export interface RpgEntryHomeViewProps {
onRechargeSuccess?: () => void | Promise<void>;
createTabContent?: ReactNode;
draftTabContent?: ReactNode;
hasUnreadDraftUpdate?: boolean;
}
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
@@ -896,9 +897,7 @@ function RecommendRuntimeMeta({
onPointerCancel={onDragPointerCancel}
>
<div className="platform-recommend-work-meta__row">
<div
className="platform-recommend-work-meta__identity"
>
<div className="platform-recommend-work-meta__identity">
<span
className="platform-recommend-work-meta__avatar"
aria-hidden="true"
@@ -1044,23 +1043,30 @@ function PlatformTabButton({
icon: Icon,
onClick,
emphasized = false,
showDot = false,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
emphasized?: boolean;
showDot?: boolean;
}) {
const ariaLabel = showDot ? `${label},有新草稿` : label;
return (
<button
type="button"
onClick={onClick}
aria-label={label}
aria-label={ariaLabel}
className={`platform-bottom-nav__button ${emphasized ? 'platform-bottom-nav__button--primary' : ''} ${active ? 'platform-bottom-nav__button--active' : ''}`}
>
<span className="platform-bottom-nav__button-content">
<span className="platform-bottom-nav__icon-shell">
<Icon className="platform-bottom-nav__icon" />
{showDot ? (
<span aria-hidden="true" className="platform-nav-unread-dot" />
) : null}
</span>
<span className="platform-bottom-nav__label">{label}</span>
</span>
@@ -1074,21 +1080,29 @@ function DesktopTabButton({
icon: Icon,
onClick,
emphasized = false,
showDot = false,
}: {
active: boolean;
label: string;
icon: ComponentType<{ className?: string }>;
onClick: () => void;
emphasized?: boolean;
showDot?: boolean;
}) {
const ariaLabel = showDot ? `${label},有新草稿` : label;
return (
<button
type="button"
onClick={onClick}
aria-label={ariaLabel}
className={`platform-desktop-rail__button ${emphasized ? 'platform-desktop-rail__button--primary' : ''} ${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]" />
{showDot ? (
<span aria-hidden="true" className="platform-nav-unread-dot" />
) : null}
</span>
<span className="platform-desktop-rail__label text-[11px] font-semibold tracking-[0.2em]">
{label}
@@ -1496,7 +1510,7 @@ function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: 'rpg';
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
@@ -1608,7 +1622,7 @@ function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
? '方洞'
: isVisualNovelGalleryEntry(entry)
? '视觉'
: describePlatformThemeLabel(entry.themeMode);
: describePlatformThemeLabel(entry.themeMode);
return formatPlatformWorkDisplayTag(kind);
}
@@ -2857,9 +2871,7 @@ function ProfileReferralModal({
</button>
<div className="rounded-xl bg-zinc-50 px-3.5 py-3">
<div className="text-xs font-black text-zinc-900">
</div>
<div className="text-xs font-black text-zinc-900"></div>
{center?.invitedUsers?.length ? (
<div className="mt-3 max-h-44 space-y-2 overflow-y-auto pr-1">
{center.invitedUsers.map((user) => (
@@ -3031,9 +3043,7 @@ function ProfilePlayedWorksModal({
</span>
<span className="truncate">
{' '}
{formatCompactPlayTime(
work.lastObservedPlayTimeMs,
)}
{formatCompactPlayTime(work.lastObservedPlayTimeMs)}
</span>
</div>
</button>
@@ -3096,6 +3106,7 @@ export function RpgEntryHomeView({
onRechargeSuccess,
createTabContent,
draftTabContent,
hasUnreadDraftUpdate = false,
}: RpgEntryHomeViewProps) {
const authUi = useAuthUi();
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
@@ -3116,9 +3127,8 @@ export function RpgEntryHomeView({
);
const [isLoadingWalletLedger, setIsLoadingWalletLedger] = useState(false);
const [isTaskCenterOpen, setIsTaskCenterOpen] = useState(false);
const [taskCenter, setTaskCenter] = useState<ProfileTaskCenterResponse | null>(
null,
);
const [taskCenter, setTaskCenter] =
useState<ProfileTaskCenterResponse | null>(null);
const [taskCenterError, setTaskCenterError] = useState<string | null>(null);
const [isLoadingTaskCenter, setIsLoadingTaskCenter] = useState(false);
const [claimingTaskId, setClaimingTaskId] = useState<string | null>(null);
@@ -3333,7 +3343,9 @@ export function RpgEntryHomeView({
}, [activeTab, isAuthenticated, onTabChange, visibleTabs]);
useEffect(() => {
if (!visibleDiscoverChannels.some((channel) => channel.id === discoverChannel)) {
if (
!visibleDiscoverChannels.some((channel) => channel.id === discoverChannel)
) {
setDiscoverChannel('recommend');
}
}, [discoverChannel, visibleDiscoverChannels]);
@@ -3932,7 +3944,9 @@ export function RpgEntryHomeView({
const updateCenteredCard = () => {
frameId = null;
const cards = Array.from(
feedElement.querySelectorAll<HTMLElement>('[data-mobile-feed-card-key]'),
feedElement.querySelectorAll<HTMLElement>(
'[data-mobile-feed-card-key]',
),
);
const viewportRect = scrollElement.getBoundingClientRect();
const viewportCenterY =
@@ -3992,7 +4006,12 @@ export function RpgEntryHomeView({
scrollElement.removeEventListener('scroll', scheduleUpdate);
window.removeEventListener('resize', scheduleUpdate);
};
}, [discoverChannel, discoverFeedEntries, activeTab, mobileFeedCarouselEnabled]);
}, [
discoverChannel,
discoverFeedEntries,
activeTab,
mobileFeedCarouselEnabled,
]);
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
(tab) => tab.id === activeRankingTab,
) as (typeof PLATFORM_RANKING_TABS)[number];
@@ -4049,8 +4068,7 @@ export function RpgEntryHomeView({
setRecommendDragCommitDirection(direction);
const panelHeight =
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
const commitDistance =
panelHeight > 0 ? panelHeight : window.innerHeight;
const commitDistance = panelHeight > 0 ? panelHeight : window.innerHeight;
setRecommendDragOffsetY(
direction === 1 ? -commitDistance : commitDistance,
);
@@ -4103,7 +4121,8 @@ export function RpgEntryHomeView({
const deltaY = event.clientY - drag.startY;
drag.dragging =
drag.dragging || Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
drag.dragging ||
Math.abs(deltaY) >= RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX / 2;
if (!drag.dragging) {
return;
}
@@ -4113,12 +4132,7 @@ export function RpgEntryHomeView({
recommendCardStageRef.current?.getBoundingClientRect().height ?? 0;
const dragLimit =
cardHeight > 0 ? cardHeight : RECOMMEND_ENTRY_DRAG_LIMIT_PX;
setRecommendDragOffsetY(
Math.max(
-dragLimit,
Math.min(dragLimit, deltaY),
),
);
setRecommendDragOffsetY(Math.max(-dragLimit, Math.min(dragLimit, deltaY)));
}, []);
const endRecommendDrag = useCallback(
(event: PointerEvent<HTMLElement>) => {
@@ -4187,25 +4201,28 @@ export function RpgEntryHomeView({
useEffect(() => {
setRecommendShareState('idle');
}, [activeRecommendEntryKey]);
const shareRecommendEntry = useCallback((entry: PlatformPublicGalleryCard) => {
const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim();
if (!publicWorkCode) {
setRecommendShareState('failed');
return;
}
const shareText = `邀请你来玩《${entry.worldName}\n作品号${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`;
void copyTextToClipboard(shareText).then((copied) => {
setRecommendShareState(copied ? 'copied' : 'failed');
if (recommendShareResetTimerRef.current !== null) {
window.clearTimeout(recommendShareResetTimerRef.current);
const shareRecommendEntry = useCallback(
(entry: PlatformPublicGalleryCard) => {
const publicWorkCode = resolvePlatformPublicWorkCode(entry)?.trim();
if (!publicWorkCode) {
setRecommendShareState('failed');
return;
}
recommendShareResetTimerRef.current = window.setTimeout(() => {
recommendShareResetTimerRef.current = null;
setRecommendShareState('idle');
}, 1400);
});
}, []);
const shareText = `邀请你来玩《${entry.worldName}\n作品号${publicWorkCode}\n${buildPublicWorkDetailUrl(publicWorkCode)}`;
void copyTextToClipboard(shareText).then((copied) => {
setRecommendShareState(copied ? 'copied' : 'failed');
if (recommendShareResetTimerRef.current !== null) {
window.clearTimeout(recommendShareResetTimerRef.current);
}
recommendShareResetTimerRef.current = window.setTimeout(() => {
recommendShareResetTimerRef.current = null;
setRecommendShareState('idle');
}, 1400);
});
},
[],
);
const openActiveRecommendEntry = useCallback(() => {
if (!activeRecommendEntry) {
return;
@@ -4304,7 +4321,9 @@ export function RpgEntryHomeView({
<section className="platform-recommend-runtime-panel">
<RecommendCoverOnlyCard
entry={activeRecommendEntry}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(
activeRecommendEntry,
)}
onClick={openActiveRecommendEntry}
/>
</section>
@@ -4481,7 +4500,10 @@ export function RpgEntryHomeView({
</div>
</div>
<button type="button" className="platform-category-sort-button">
<button
type="button"
className="platform-category-sort-button"
>
<span></span>
<ChevronDown className="h-3.5 w-3.5" />
</button>
@@ -4535,22 +4557,26 @@ export function RpgEntryHomeView({
<EmptyShelf text="正在读取公开作品..." />
) : discoverFeedEntries.length > 0 ? (
<div className="grid min-w-0 gap-3">
{discoverFeedEntries.map((entry: PlatformPublicGalleryCard) => {
const cardKey = buildPublicGalleryCardKey(entry);
{discoverFeedEntries.map(
(entry: PlatformPublicGalleryCard) => {
const cardKey = buildPublicGalleryCardKey(entry);
return (
<WorldCard
key={`${cardKey}:mobile-feed:${discoverChannel}`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
feedCardKey={cardKey}
enableCoverCarousel={mobileFeedCarouselEnabled}
isCoverCarouselActive={mobileCenteredCardKey === cardKey}
/>
);
})}
return (
<WorldCard
key={`${cardKey}:mobile-feed:${discoverChannel}`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
feedCardKey={cardKey}
enableCoverCarousel={mobileFeedCarouselEnabled}
isCoverCarouselActive={
mobileCenteredCardKey === cardKey
}
/>
);
},
)}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
@@ -4676,11 +4702,9 @@ export function RpgEntryHomeView({
)}
</div>
);
const categoryContent: ReactNode = isDesktopLayout ? (
desktopDiscoverContent
) : (
mobileDiscoverContent
);
const categoryContent: ReactNode = isDesktopLayout
? desktopDiscoverContent
: mobileDiscoverContent;
const fallbackCreateStartContent: ReactNode = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
@@ -4748,11 +4772,10 @@ export function RpgEntryHomeView({
</div>
);
const createContent: ReactNode = createTabContent ?? fallbackCreateStartContent;
const createContent: ReactNode =
createTabContent ?? fallbackCreateStartContent;
const savesContent: ReactNode = (
draftTabContent ?? fallbackDraftContent
);
const savesContent: ReactNode = draftTabContent ?? fallbackDraftContent;
const profileContent: ReactNode = (
<div className={MOBILE_PAGE_STAGE_CLASS}>
@@ -5013,268 +5036,275 @@ export function RpgEntryHomeView({
/>
) : (
<>
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.55fr)_22rem]">
<button
type="button"
onClick={openLeadPublicEntry}
className={`${HERO_SURFACE_CLASS} relative block overflow-hidden px-7 py-6 text-left`}
>
{desktopHeroCover ? (
<ResolvedAssetBackdrop
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-[var(--platform-hero-overlay-strong)]" />
<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"></span>
<span className="platform-pill platform-pill--neutral px-3">
{leadPublicEntry
? describePublicGalleryCardKind(leadPublicEntry)
: '作品'}
</span>
</div>
<div className="max-w-[35rem]">
<div className="text-5xl font-semibold leading-[1.08] text-white">
{leadPublicEntry?.worldName ?? '浏览玩家作品'}
</div>
<div className="mt-4 text-base leading-8 text-zinc-200/86">
{leadPublicEntry?.summaryText ||
leadPublicEntry?.subtitle ||
'挑一个玩家作品,开始今天的游玩。'}
</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></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);
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
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 ? (
<ResolvedAssetBackdrop
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-[color:color-mix(in_srgb,var(--platform-text-base)_82%,transparent)]">
<span className="text-[var(--platform-text-soft)]">
{`${index + 1}`.padStart(2, '0')}
</span>
<span className="line-clamp-1">{displayName}</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="TODAY GAMES" />
<span className="platform-pill platform-pill--neutral px-3">
TODAY
</span>
</div>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取今日游戏..." />
) : desktopTodayEntries.length > 0 ? (
<div className="space-y-3">
{desktopTodayEntries.slice(0, 3).map((entry, index) => (
<DesktopTrendingItem
key={`${buildPublicGalleryCardKey(entry)}:desktop-today`}
entry={entry}
rank={index + 1}
onClick={() => openRecommendGalleryDetail(entry)}
<div className="grid gap-5 xl:grid-cols-[minmax(0,1.55fr)_22rem]">
<button
type="button"
onClick={openLeadPublicEntry}
className={`${HERO_SURFACE_CLASS} relative block overflow-hidden px-7 py-6 text-left`}
>
{desktopHeroCover ? (
<ResolvedAssetBackdrop
src={desktopHeroCover}
alt=""
aria-hidden="true"
className="absolute inset-0 h-full w-full object-cover opacity-34"
/>
))}
</div>
) : (
<EmptyShelf text="今天暂时还没有新游戏。" />
)}
</section>
</div>
) : null}
<div className="absolute inset-0 bg-[var(--platform-hero-overlay-strong)]" />
<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">
</span>
<span className="platform-pill platform-pill--neutral px-3">
{leadPublicEntry
? describePublicGalleryCardKind(leadPublicEntry)
: '作品'}
</span>
</div>
<div
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || visibleHistoryEntries.length > 0 ? '2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]' : ''}`}
>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="推荐" detail="RECOMMENDED" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取推荐作品..." />
) : desktopFeaturedGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-2">
{desktopFeaturedGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-featured`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="暂时还没有推荐作品。" />
)}
</section>
<div className="max-w-[35rem]">
<div className="text-5xl font-semibold leading-[1.08] text-white">
{leadPublicEntry?.worldName ?? '浏览玩家作品'}
</div>
<div className="mt-4 text-base leading-8 text-zinc-200/86">
{leadPublicEntry?.summaryText ||
leadPublicEntry?.subtitle ||
'挑一个玩家作品,开始今天的游玩。'}
</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></span>
<ArrowRight className="h-4 w-4" />
</div>
</div>
{desktopLibraryPreview.length > 0 || visibleHistoryEntries.length > 0 ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
detail="QUICK ACCESS"
/>
<div>
<div className="text-[10px] font-semibold tracking-[0.24em] text-[var(--platform-text-soft)]">
{desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
</div>
{desktopLibraryPreview.length > 0 ? (
<div className="mt-3 space-y-3">
{desktopLibraryPreview.map((entry) => {
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
return (
<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-[var(--platform-text-strong)]">
{displayName}
{desktopHeroStripEntries.length > 0 ? (
<div className="grid gap-3 sm:grid-cols-5">
{desktopHeroStripEntries.map((entry, index) => {
const coverImage = resolvePlatformWorldCoverImage(entry);
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
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 ? (
<ResolvedAssetBackdrop
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="mt-1 text-sm text-[var(--platform-text-soft)]">
{entry.visibility === 'published'
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
: '草稿待完善'}
<div className="flex items-center gap-2 px-3 py-2 text-[11px] text-[color:color-mix(in_srgb,var(--platform-text-base)_82%,transparent)]">
<span className="text-[var(--platform-text-soft)]">
{`${index + 1}`.padStart(2, '0')}
</span>
<span className="line-clamp-1">{displayName}</span>
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
{entry.visibility === 'published' ? '已发布' : '草稿'}
</span>
</button>
);
})}
);
})}
</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="TODAY GAMES" />
<span className="platform-pill platform-pill--neutral px-3">
TODAY
</span>
</div>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取今日游戏..." />
) : desktopTodayEntries.length > 0 ? (
<div className="space-y-3">
{desktopTodayEntries.slice(0, 3).map((entry, index) => (
<DesktopTrendingItem
key={`${buildPublicGalleryCardKey(entry)}:desktop-today`}
entry={entry}
rank={index + 1}
onClick={() => openRecommendGalleryDetail(entry)}
/>
))}
</div>
) : (
<div className="mt-3 space-y-3">
{visibleHistoryEntries.slice(0, 2).map((entry) => {
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
<EmptyShelf text="今天暂时还没有新游戏。" />
)}
</section>
</div>
<div
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || visibleHistoryEntries.length > 0 ? '2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]' : ''}`}
>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="推荐" detail="RECOMMENDED" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取推荐作品..." />
) : desktopFeaturedGrid.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-2">
{desktopFeaturedGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-featured`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="暂时还没有推荐作品。" />
)}
</section>
{desktopLibraryPreview.length > 0 ||
visibleHistoryEntries.length > 0 ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={
desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'
}
detail="QUICK ACCESS"
/>
<div>
<div className="text-[10px] font-semibold tracking-[0.24em] text-[var(--platform-text-soft)]">
{desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
</div>
{desktopLibraryPreview.length > 0 ? (
<div className="mt-3 space-y-3">
{desktopLibraryPreview.map((entry) => {
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
return (
<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-[var(--platform-text-strong)]">
{displayName}
</div>
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
{entry.visibility === 'published'
? `发布于 ${formatPlatformWorldTime(entry.publishedAt)}`
: '草稿待完善'}
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
{entry.visibility === 'published'
? '已发布'
: '草稿'}
</span>
</button>
);
})}
</div>
) : (
<div className="mt-3 space-y-3">
{visibleHistoryEntries.slice(0, 2).map((entry) => {
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);
return (
<button
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
type="button"
onClick={() =>
openRecommendGalleryDetail({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: null,
authorPublicUserCode: null,
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,
likeCount: 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-[var(--platform-text-strong)]">
{displayName}
</div>
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
{entry.authorDisplayName}
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
</span>
</button>
);
})}
</div>
)}
</div>
</section>
) : null}
</div>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取作品分类..." />
) : activeCategoryGroup && desktopCategoryGrid.length > 0 ? (
<>
<div className="mb-4 flex min-w-0 items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
{categoryGroups.map((group) => {
const active = group.tag === activeCategoryGroup.tag;
return (
<button
key={`${entry.ownerUserId}:${entry.profileId}:desktop-history`}
key={`${group.tag}:desktop-category`}
type="button"
onClick={() =>
openRecommendGalleryDetail({
ownerUserId: entry.ownerUserId,
profileId: entry.profileId,
publicWorkCode: null,
authorPublicUserCode: null,
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,
likeCount: 0,
})
}
className="platform-desktop-trending-item flex w-full items-center justify-between gap-3 px-4 py-4 text-left"
onClick={() => setSelectedCategoryTag(group.tag)}
className={`platform-category-chip shrink-0 ${active ? 'platform-category-chip--active' : ''}`}
>
<div className="min-w-0">
<div className="line-clamp-1 text-base font-semibold text-[var(--platform-text-strong)]">
{displayName}
</div>
<div className="mt-1 text-sm text-[var(--platform-text-soft)]">
{entry.authorDisplayName}
</div>
</div>
<span className="platform-pill platform-pill--neutral px-3">
</span>
{group.tag}
</button>
);
})}
</div>
)}
</div>
<div className="grid gap-4 xl:grid-cols-3">
{desktopCategoryGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-category:${activeCategoryGroup.tag}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
</>
) : (
<EmptyShelf text="暂时还没有可分类的作品。" />
)}
</section>
) : null}
</div>
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title="作品分类" detail="GAME CATEGORY" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取作品分类..." />
) : activeCategoryGroup && desktopCategoryGrid.length > 0 ? (
<>
<div className="mb-4 flex min-w-0 items-center gap-2 overflow-x-auto pb-1 scrollbar-hide">
{categoryGroups.map((group) => {
const active = group.tag === activeCategoryGroup.tag;
return (
<button
key={`${group.tag}:desktop-category`}
type="button"
onClick={() => setSelectedCategoryTag(group.tag)}
className={`platform-category-chip shrink-0 ${active ? 'platform-category-chip--active' : ''}`}
>
{group.tag}
</button>
);
})}
</div>
<div className="grid gap-4 xl:grid-cols-3">
{desktopCategoryGrid.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-category:${activeCategoryGroup.tag}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
</>
) : (
<EmptyShelf text="暂时还没有可分类的作品。" />
)}
</section>
</>
)}
</div>
@@ -5403,6 +5433,7 @@ export function RpgEntryHomeView({
: tabIcons[tab]
}
emphasized={tab === 'create'}
showDot={tab === 'saves' && hasUnreadDraftUpdate}
onClick={() => {
if (activeTab === 'home' && tab === 'home') {
selectNextRecommendEntry();
@@ -5548,6 +5579,7 @@ export function RpgEntryHomeView({
label={tabLabels[tab]}
icon={tabIcons[tab]}
emphasized={tab === 'create'}
showDot={tab === 'saves' && hasUnreadDraftUpdate}
onClick={() => {
if (!isAuthenticated && tab === 'home') {
onTabChange(tab);