This commit is contained in:
2026-05-08 21:46:11 +08:00
parent 94975e4735
commit e410f7974e
13 changed files with 757 additions and 426 deletions

View File

@@ -122,7 +122,8 @@ export interface RpgEntryHomeViewProps {
activeRecommendEntryKey?: string | null;
isStartingRecommendEntry?: boolean;
recommendRuntimeError?: string | null;
onSelectRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
onSelectNextRecommendEntry?: () => void;
onSelectPreviousRecommendEntry?: () => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
@@ -149,6 +150,8 @@ const HERO_SURFACE_CLASS =
'platform-surface platform-surface--hero platform-interactive-card min-w-0';
const MOBILE_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface min-w-0 space-y-4 overflow-hidden pb-2';
const MOBILE_RECOMMEND_PAGE_STAGE_CLASS =
'platform-page-stage min-w-0 space-y-4 overflow-hidden pb-2';
const DESKTOP_PAGE_STAGE_CLASS =
'platform-page-stage platform-remap-surface min-w-0 space-y-5 pb-4';
const DESKTOP_LAYOUT_QUERY = '(min-width: 1024px)';
@@ -165,6 +168,7 @@ const AVATAR_ALLOWED_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp']);
const PLATFORM_WORK_COVER_CAROUSEL_INTERVAL_MS = 4200;
const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
const RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX = 36;
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
@@ -664,12 +668,15 @@ function CreationLibraryCard({
function RecommendRuntimeMeta({
entry,
authorAvatarUrl,
onOpenDetail,
onSelectNext,
onSelectPrevious,
}: {
entry: PlatformPublicGalleryCard;
authorAvatarUrl?: string | null;
onOpenDetail: () => void;
onSelectNext?: () => void;
onSelectPrevious?: () => void;
}) {
const swipeStartYRef = useRef<number | null>(null);
const playCount = getPlatformWorldPlayCount(entry);
const remixCount = getPlatformWorldRemixCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
@@ -682,11 +689,37 @@ function RecommendRuntimeMeta({
{ label: '点赞', value: likeCount, icon: Heart },
{ label: '改造', value: remixCount, icon: MessageCircle },
];
const handlePointerEnd = (clientY: number) => {
const startY = swipeStartYRef.current;
swipeStartYRef.current = null;
if (startY === null) {
return;
}
const deltaY = clientY - startY;
if (Math.abs(deltaY) < RECOMMEND_ENTRY_SWIPE_THRESHOLD_PX) {
return;
}
if (deltaY < 0) {
onSelectNext?.();
return;
}
onSelectPrevious?.();
};
return (
<section
className="platform-recommend-work-meta"
aria-label={`${entry.worldName} 作品信息`}
onPointerDown={(event) => {
swipeStartYRef.current = event.clientY;
}}
onPointerUp={(event) => handlePointerEnd(event.clientY)}
onPointerCancel={() => {
swipeStartYRef.current = null;
}}
>
<div className="platform-recommend-work-meta__stats">
{statItems.map(({ label, value, icon: Icon }) => (
@@ -702,11 +735,8 @@ function RecommendRuntimeMeta({
</div>
<div className="platform-recommend-work-meta__row">
<button
type="button"
onClick={onOpenDetail}
<div
className="platform-recommend-work-meta__identity"
aria-label={`打开 ${entry.worldName} 详情`}
>
<span
className="platform-recommend-work-meta__avatar"
@@ -730,62 +760,12 @@ function RecommendRuntimeMeta({
{displayName}
</span>
</span>
</button>
<button
type="button"
onClick={onOpenDetail}
className="platform-recommend-work-meta__detail-button"
aria-label={`查看 ${entry.worldName} 详情`}
title="详情"
>
<ArrowRight className="h-4 w-4" />
</button>
</div>
</div>
</section>
);
}
function RecommendWorkSwitchItem({
entry,
active,
onSelect,
}: {
entry: PlatformPublicGalleryCard;
active: boolean;
onSelect: () => void;
}) {
const displayName = formatPlatformWorkDisplayName(entry.worldName);
const typeLabel = describePublicGalleryCardKind(entry);
const playCount = getPlatformWorldPlayCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
return (
<button
type="button"
onClick={onSelect}
aria-label={`切换到 ${entry.worldName}`}
aria-pressed={active}
className={`platform-recommend-switch-card ${active ? 'platform-recommend-switch-card--active' : ''}`}
>
<span className="platform-recommend-switch-card__kind">{typeLabel}</span>
<span className="platform-recommend-switch-card__title">
{displayName}
</span>
<span className="platform-recommend-switch-card__stats">
<span>
<Gamepad2 className="h-3 w-3" aria-hidden="true" />
{formatCompactCount(playCount)}
</span>
<span>
<Heart className="h-3 w-3" aria-hidden="true" />
{formatCompactCount(likeCount)}
</span>
</span>
</button>
);
}
function SaveArchiveCard({
entry,
onClick,
@@ -2861,7 +2841,8 @@ export function RpgEntryHomeView({
activeRecommendEntryKey = null,
isStartingRecommendEntry = false,
recommendRuntimeError = null,
onSelectRecommendEntry,
onSelectNextRecommendEntry,
onSelectPreviousRecommendEntry,
onOpenLibraryDetail,
onDeleteLibraryEntry,
deletingLibraryEntryId = null,
@@ -3722,6 +3703,12 @@ export function RpgEntryHomeView({
) ??
recommendedFeedEntries[0] ??
null;
const selectNextRecommendEntry = useCallback(() => {
onSelectNextRecommendEntry?.();
}, [onSelectNextRecommendEntry]);
const selectPreviousRecommendEntry = useCallback(() => {
onSelectPreviousRecommendEntry?.();
}, [onSelectPreviousRecommendEntry]);
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
@@ -3785,7 +3772,7 @@ export function RpgEntryHomeView({
const mobileRecommendContent: ReactNode = (
<div
className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage platform-mobile-recommend-stage`}
className={`${MOBILE_RECOMMEND_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">
@@ -3823,42 +3810,9 @@ export function RpgEntryHomeView({
<RecommendRuntimeMeta
entry={activeRecommendEntry}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
onOpenDetail={() => onOpenGalleryDetail(activeRecommendEntry)}
onSelectNext={selectNextRecommendEntry}
onSelectPrevious={selectPreviousRecommendEntry}
/>
) : null}
{recommendedFeedEntries.length > 0 ? (
<section
className="platform-recommend-switcher"
aria-label="推荐作品"
>
{recommendedFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
const active =
activeRecommendEntryKey === cardKey ||
Boolean(
!activeRecommendEntryKey &&
activeRecommendEntry &&
buildPublicGalleryCardKey(activeRecommendEntry) === cardKey,
);
return (
<RecommendWorkSwitchItem
key={`${cardKey}:recommend-switch`}
entry={entry}
active={active}
onSelect={() => {
if (onSelectRecommendEntry) {
onSelectRecommendEntry(entry);
return;
}
onOpenGalleryDetail(entry);
}}
/>
);
})}
</section>
) : !isLoadingPlatform ? (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
) : null}
@@ -4706,10 +4660,25 @@ export function RpgEntryHomeView({
<PlatformTabButton
key={tab}
active={activeTab === tab}
label={tabLabels[tab]}
icon={tabIcons[tab]}
label={
activeTab === 'home' && tab === 'home'
? '下一个'
: tabLabels[tab]
}
icon={
activeTab === 'home' && tab === 'home'
? ChevronDown
: tabIcons[tab]
}
emphasized={tab === 'create'}
onClick={() => onTabChange(tab)}
onClick={() => {
if (activeTab === 'home' && tab === 'home') {
selectNextRecommendEntry();
return;
}
onTabChange(tab);
}}
/>
))}
</div>