This commit is contained in:
2026-05-08 20:48:29 +08:00
parent abf1f1ebea
commit 94975e4735
82 changed files with 7786 additions and 1012 deletions

View File

@@ -118,6 +118,11 @@ export interface RpgEntryHomeViewProps {
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void;
recommendRuntimeContent?: ReactNode;
activeRecommendEntryKey?: string | null;
isStartingRecommendEntry?: boolean;
recommendRuntimeError?: string | null;
onSelectRecommendEntry?: (entry: PlatformPublicGalleryCard) => void;
onOpenLibraryDetail: (
entry: CustomWorldLibraryEntry<CustomWorldProfile>,
) => void;
@@ -656,6 +661,131 @@ function CreationLibraryCard({
);
}
function RecommendRuntimeMeta({
entry,
authorAvatarUrl,
onOpenDetail,
}: {
entry: PlatformPublicGalleryCard;
authorAvatarUrl?: string | null;
onOpenDetail: () => void;
}) {
const playCount = getPlatformWorldPlayCount(entry);
const remixCount = getPlatformWorldRemixCount(entry);
const likeCount = getPlatformWorldLikeCount(entry);
const authorName = entry.authorDisplayName.trim() || '玩家';
const authorAvatarLabel = getPublicAuthorAvatarLabel(authorName);
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
const displayName = formatPlatformWorkDisplayName(entry.worldName);
const statItems = [
{ label: '游玩', value: playCount, icon: Gamepad2 },
{ label: '点赞', value: likeCount, icon: Heart },
{ label: '改造', value: remixCount, icon: MessageCircle },
];
return (
<section
className="platform-recommend-work-meta"
aria-label={`${entry.worldName} 作品信息`}
>
<div className="platform-recommend-work-meta__stats">
{statItems.map(({ label, value, icon: Icon }) => (
<span
key={label}
className="platform-recommend-work-meta__stat"
aria-label={`${label} ${formatCompactCount(value)}`}
>
<Icon className="h-4 w-4" aria-hidden="true" />
<span>{formatCompactCount(value)}</span>
</span>
))}
</div>
<div className="platform-recommend-work-meta__row">
<button
type="button"
onClick={onOpenDetail}
className="platform-recommend-work-meta__identity"
aria-label={`打开 ${entry.worldName} 详情`}
>
<span
className="platform-recommend-work-meta__avatar"
aria-hidden="true"
>
{normalizedAuthorAvatarUrl ? (
<img
src={normalizedAuthorAvatarUrl}
alt=""
className="h-full w-full rounded-full object-cover"
/>
) : (
authorAvatarLabel
)}
</span>
<span className="platform-recommend-work-meta__text">
<span className="platform-recommend-work-meta__author">
{authorName}
</span>
<span className="platform-recommend-work-meta__title">
{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>
</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,
@@ -2727,6 +2857,11 @@ export function RpgEntryHomeView({
onResumeSave,
onOpenCreateTypePicker,
onOpenGalleryDetail,
recommendRuntimeContent,
activeRecommendEntryKey = null,
isStartingRecommendEntry = false,
recommendRuntimeError = null,
onSelectRecommendEntry,
onOpenLibraryDetail,
onDeleteLibraryEntry,
deletingLibraryEntryId = null,
@@ -2796,7 +2931,6 @@ export function RpgEntryHomeView({
);
const [discoverChannel, setDiscoverChannel] =
useState<DiscoverChannel>('recommend');
const mobileRecommendFeedRef = useRef<HTMLElement | null>(null);
const mobileDiscoverFeedRef = useRef<HTMLElement | null>(null);
const [mobileCenteredCardKey, setMobileCenteredCardKey] = useState<
string | null
@@ -3494,19 +3628,15 @@ export function RpgEntryHomeView({
}, [discoverChannel, latestEntries, recommendedFeedEntries]);
const mobileFeedCarouselEnabled =
!isDesktopLayout &&
((activeTab === 'home' && recommendedFeedEntries.length > 0) ||
(activeTab === 'category' &&
(discoverChannel === 'recommend' || discoverChannel === 'today')));
activeTab === 'category' &&
(discoverChannel === 'recommend' || discoverChannel === 'today');
useEffect(() => {
if (!mobileFeedCarouselEnabled) {
setMobileCenteredCardKey(null);
return undefined;
}
const feedElement =
activeTab === 'home'
? mobileRecommendFeedRef.current
: mobileDiscoverFeedRef.current;
const feedElement = mobileDiscoverFeedRef.current;
const scrollElement = feedElement?.closest('.platform-tab-panel');
if (!feedElement || !scrollElement) {
setMobileCenteredCardKey(null);
@@ -3577,13 +3707,7 @@ export function RpgEntryHomeView({
scrollElement.removeEventListener('scroll', scheduleUpdate);
window.removeEventListener('resize', scheduleUpdate);
};
}, [
discoverChannel,
discoverFeedEntries,
activeTab,
mobileFeedCarouselEnabled,
recommendedFeedEntries,
]);
}, [discoverChannel, discoverFeedEntries, activeTab, mobileFeedCarouselEnabled]);
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
(tab) => tab.id === activeRankingTab,
) as (typeof PLATFORM_RANKING_TABS)[number];
@@ -3592,6 +3716,12 @@ export function RpgEntryHomeView({
buildPlatformRankingEntries(publicEntries, activeRankingTab).slice(0, 30),
[activeRankingTab, publicEntries],
);
const activeRecommendEntry =
recommendedFeedEntries.find(
(entry) => buildPublicGalleryCardKey(entry) === activeRecommendEntryKey,
) ??
recommendedFeedEntries[0] ??
null;
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
@@ -3663,36 +3793,75 @@ export function RpgEntryHomeView({
</div>
) : null}
<section
ref={mobileRecommendFeedRef}
className="platform-mobile-home-feed platform-mobile-recommend-feed"
>
<section className="platform-recommend-runtime-panel">
{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 className="platform-recommend-runtime-state">
...
</div>
) : recommendRuntimeError ? (
<button
type="button"
onClick={() =>
activeRecommendEntry
? onOpenGalleryDetail(activeRecommendEntry)
: undefined
}
className="platform-recommend-runtime-state platform-recommend-runtime-state--button"
>
{recommendRuntimeError}
</button>
) : isStartingRecommendEntry || !recommendRuntimeContent ? (
<div className="platform-recommend-runtime-state">...</div>
) : (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
<div className="platform-recommend-runtime-viewport">
{recommendRuntimeContent}
</div>
)}
</section>
{activeRecommendEntry ? (
<RecommendRuntimeMeta
entry={activeRecommendEntry}
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(activeRecommendEntry)}
onOpenDetail={() => onOpenGalleryDetail(activeRecommendEntry)}
/>
) : 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}
</div>
);