feat: add child motion entry and fix auth env
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-10 18:27:51 +08:00
parent 86fc382413
commit 46a254f142
22 changed files with 2868 additions and 58 deletions

View File

@@ -3,7 +3,7 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { beforeEach, expect, test, vi } from 'vitest';
import { afterEach, beforeEach, expect, test, vi } from 'vitest';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
@@ -2332,6 +2332,10 @@ beforeEach(() => {
);
});
afterEach(() => {
vi.unstubAllEnvs();
});
test('create tab shows template tabs and embeds puzzle form by default', async () => {
const user = userEvent.setup();
@@ -2364,7 +2368,7 @@ test('create tab shows template tabs and embeds puzzle form by default', async (
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByPlaceholderText('问一问百梦')).toBeNull();
expect(screen.queryByRole('button', { name: //u })).toBeNull();
expect(screen.queryByRole('tab', { name: //u })).toBeNull();
expect(screen.getByRole('tab', { name: //u })).toBeTruthy();
expect(createRpgCreationSession).not.toHaveBeenCalled();
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
@@ -2879,6 +2883,50 @@ test('logged out public detail gates big fish start before local runtime', async
expect(recordBigFishPlay).not.toHaveBeenCalled();
});
test('public code search blocks edutainment work when entry switch is disabled', async () => {
vi.stubEnv('VITE_ENABLE_EDUTAINMENT_ENTRY', 'false');
const user = userEvent.setup();
const edutainmentPuzzleWork: PuzzleWorkSummary = {
workId: 'puzzle-work-edutainment-1',
profileId: 'puzzle-profile-edutainment-1',
ownerUserId: 'user-2',
sourceSessionId: 'puzzle-session-edutainment-1',
authorDisplayName: '动作 Demo 作者',
levelName: '儿童动作热身 Demo',
summary: '寓教于乐专属动作 Demo。',
themeTags: ['运动', '安全', '拼图', '寓教于乐'],
coverImageSrc: null,
coverAssetId: null,
publicationStatus: 'published',
updatedAt: '2026-05-09T10:00:00.000Z',
publishedAt: '2026-05-09T10:00:00.000Z',
playCount: 3,
remixCount: 0,
likeCount: 0,
publishReady: true,
};
vi.mocked(listPuzzleGallery).mockResolvedValue({
items: [edutainmentPuzzleWork],
});
vi.mocked(getPuzzleGalleryDetail).mockResolvedValue({
item: edutainmentPuzzleWork,
});
render(<TestWrapper withAuth />);
await openDiscoverHub(user);
const searchInput = await screen.findByPlaceholderText(
'搜索作品号、名称、作者、描述',
);
await user.type(searchInput, 'PZ-TMENT1');
await user.click(screen.getByRole('button', { name: '搜索' }));
expect(await screen.findByText('未找到结果')).toBeTruthy();
expect(screen.queryByText('儿童动作热身 Demo')).toBeNull();
expect(getPuzzleGalleryDetail).not.toHaveBeenCalled();
});
test('creation hub clears all private work shelves immediately after logout state', async () => {
const user = userEvent.setup();
const loggedInAuth = createAuthValue();
@@ -3027,7 +3075,6 @@ test('published puzzle works appear on home and mobile game category channel', a
});
test('home recommendation starts embedded puzzle without global auth reset on local failure', async () => {
const user = userEvent.setup();
const publishedPuzzleWork = {
workId: 'puzzle-work-public-1',
profileId: 'puzzle-profile-public-1',

View File

@@ -25,7 +25,10 @@ import {
RpgEntryHomeView,
type RpgEntryHomeViewProps,
} from './RpgEntryHomeView';
import type { PlatformPublicGalleryCard } from './rpgEntryWorldPresentation';
import type {
PlatformPublicGalleryCard,
PlatformPuzzleGalleryCard,
} from './rpgEntryWorldPresentation';
const {
mockBuildReferralCenter,
@@ -425,6 +428,23 @@ const longTextRankEntry = {
updatedAt: '2026-04-29T10:00:00.000Z',
} satisfies PlatformPublicGalleryCard;
function buildTaggedPuzzleEntry(
id: string,
worldName: string,
themeTags: string[],
overrides: Partial<PlatformPuzzleGalleryCard> = {},
) {
return {
...puzzlePublicEntry,
workId: `puzzle-work-${id}`,
profileId: `puzzle-profile-${id}`,
publicWorkCode: `PZ-${id.toUpperCase()}`,
worldName,
themeTags,
...overrides,
} satisfies PlatformPuzzleGalleryCard;
}
function mockDesktopLayout() {
Object.defineProperty(window, 'matchMedia', {
configurable: true,
@@ -688,6 +708,7 @@ function renderStatefulLoggedOutHomeView(
afterEach(() => {
vi.useRealTimers();
vi.clearAllMocks();
vi.unstubAllEnvs();
mockGetRpgProfileReferralInviteCenter.mockResolvedValue(
mockBuildReferralCenter(),
);
@@ -1097,6 +1118,108 @@ test('discover search fuzzy matches public work id, name, author and description
expect(onOpenGalleryDetail).toHaveBeenCalledWith(entries[1]);
});
test('mobile discover keeps edutainment works in the last dedicated channel only', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
const generalEntry = buildTaggedPuzzleEntry('normal01', '普通拼图作品', [
'儿童教育',
]);
const edutainmentEntry = buildTaggedPuzzleEntry(
'edu001',
'儿童动作热身 Demo',
['运动', '安全', '拼图', '寓教于乐'],
{
playCount: 99,
remixCount: 30,
likeCount: 50,
recentPlayCount7d: 88,
publishedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
summaryText: '寓教于乐专属内容',
},
);
renderStatefulLoggedOutHomeView({
latestEntries: [edutainmentEntry, generalEntry],
onSearchPublicCode,
});
await user.click(screen.getByRole('button', { name: '发现' }));
const discoverPanel = document.getElementById('platform-tab-panel-category');
if (!discoverPanel) {
throw new Error('缺少发现面板');
}
const channels = Array.from(
discoverPanel.querySelectorAll('.platform-mobile-home-channel'),
).map((button) => button.textContent);
expect(channels).toEqual(['推荐', '今日', '分类', '排行', '寓教于乐']);
expect(within(discoverPanel).getByText('普通拼图作品')).toBeTruthy();
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
await user.click(screen.getByRole('button', { name: '今日' }));
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
await user.click(screen.getByRole('button', { name: '分类' }));
expect(screen.getByRole('button', { name: '儿童教育' })).toBeTruthy();
expect(screen.queryByRole('button', { name: '寓教于乐' })).toBeTruthy();
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
await user.click(screen.getByRole('button', { name: '排行' }));
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
await user.click(screen.getByRole('button', { name: '寓教于乐' }));
expect(
within(discoverPanel).getByRole('button', {
name: / Demo/u,
}),
).toBeTruthy();
expect(within(discoverPanel).queryByText('普通拼图作品')).toBeNull();
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, '儿童动作热身{enter}');
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).queryByText('儿童动作热身 Demo')).toBeNull();
expect(onSearchPublicCode).not.toHaveBeenCalled();
});
test('mobile discover hides edutainment channel and work when switch is disabled', async () => {
vi.stubEnv('VITE_ENABLE_EDUTAINMENT_ENTRY', 'false');
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
const edutainmentEntry = buildTaggedPuzzleEntry(
'eduoff1',
'关闭后隐藏的热身 Demo',
['寓教于乐'],
{
summaryText: '关闭后不可见',
publishedAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
);
renderStatefulLoggedOutHomeView({
latestEntries: [edutainmentEntry],
onSearchPublicCode,
});
await user.click(screen.getByRole('button', { name: '发现' }));
const discoverPanel = document.getElementById('platform-tab-panel-category');
if (!discoverPanel) {
throw new Error('缺少发现面板');
}
const channels = Array.from(
discoverPanel.querySelectorAll('.platform-mobile-home-channel'),
).map((button) => button.textContent);
expect(channels).toEqual(['推荐', '今日', '分类', '排行']);
expect(within(discoverPanel).queryByText('关闭后隐藏的热身 Demo')).toBeNull();
const searchInput = screen.getByPlaceholderText('搜索作品号、名称、作者、描述');
await user.type(searchInput, 'PZ-EDUOFF1{enter}');
expect(await within(discoverPanel).findByText('搜索结果')).toBeTruthy();
expect(within(discoverPanel).queryByText('关闭后隐藏的热身 Demo')).toBeNull();
expect(onSearchPublicCode).not.toHaveBeenCalled();
});
test('discover search keeps public code fallback when local works do not match', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();

View File

@@ -71,6 +71,14 @@ import {
} from '../../services/rpg-entry/rpgProfileClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import {
canExposePublicWork,
EDUTAINMENT_WORK_TAG,
filterEdutainmentPublicWorks,
filterGeneralPublicWorks,
findPublicWorkForHistoryEntry,
isEdutainmentEntryEnabled,
} from '../platform-entry/platformEdutainmentVisibility';
import { ResolvedAssetImage } from '../ResolvedAssetImage';
import { RpgEntryBrandLogo } from './RpgEntryBrandLogo';
import {
@@ -176,7 +184,12 @@ const RECOMMEND_ENTRY_COMMIT_ANIMATION_MS = 180;
const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
type DiscoverChannel =
| 'recommend'
| 'today'
| 'category'
| 'ranking'
| 'edutainment';
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
const COMMUNITY_QR_CODES = [
@@ -201,6 +214,10 @@ const DISCOVER_CHANNELS: Array<{
{ id: 'category', label: '分类' },
{ id: 'ranking', label: '排行' },
];
const EDUTAINMENT_DISCOVER_CHANNEL = {
id: 'edutainment',
label: EDUTAINMENT_WORK_TAG,
} as const;
const PLATFORM_RANKING_TABS: Array<{
id: PlatformRankingTab;
@@ -1246,9 +1263,11 @@ function buildPublicCategoryGroups(
) {
const publicEntryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredEntries, ...latestEntries].forEach((entry) => {
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
});
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
(entry) => {
publicEntryMap.set(buildPublicGalleryCardKey(entry), entry);
},
);
const categoryMap = new Map<string, PlatformPublicGalleryCard[]>();
Array.from(publicEntryMap.values()).forEach((entry) => {
@@ -1279,6 +1298,21 @@ function getPlatformPublicEntries(
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
filterGeneralPublicWorks([...featuredEntries, ...latestEntries]).forEach(
(entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
},
);
return Array.from(entryMap.values());
}
function getAllPlatformPublicEntries(
featuredEntries: PlatformPublicGalleryCard[],
latestEntries: PlatformPublicGalleryCard[],
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredEntries, ...latestEntries].forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
@@ -3079,21 +3113,62 @@ export function RpgEntryHomeView({
const [avatarError, setAvatarError] = useState<string | null>(null);
const [isSavingAvatar, setIsSavingAvatar] = useState(false);
const isAuthenticated = Boolean(authUi?.user);
const edutainmentEntryEnabled = isEdutainmentEntryEnabled();
const isDesktopLayout = usePlatformDesktopLayout();
const openRecommendGalleryDetail =
onOpenRecommendGalleryDetail ?? onOpenGalleryDetail;
const featuredShelf = useMemo(
() => featuredEntries.slice(0, 6),
const generalFeaturedEntries = useMemo(
() => filterGeneralPublicWorks(featuredEntries),
[featuredEntries],
);
const categoryGroups = useMemo(
() => buildPublicCategoryGroups(featuredEntries, latestEntries),
const featuredShelf = useMemo(
() => generalFeaturedEntries.slice(0, 6),
[generalFeaturedEntries],
);
const generalLatestEntries = useMemo(
() => filterGeneralPublicWorks(latestEntries),
[latestEntries],
);
const allEdutainmentEntries = useMemo(
() => filterEdutainmentPublicWorks([...featuredEntries, ...latestEntries]),
[featuredEntries, latestEntries],
);
const edutainmentEntries = useMemo(
() => (edutainmentEntryEnabled ? allEdutainmentEntries : []),
[allEdutainmentEntries, edutainmentEntryEnabled],
);
const visibleDiscoverChannels = useMemo(
() =>
edutainmentEntryEnabled
? [...DISCOVER_CHANNELS, EDUTAINMENT_DISCOVER_CHANNEL]
: DISCOVER_CHANNELS,
[edutainmentEntryEnabled],
);
const categoryGroups = useMemo(
() =>
buildPublicCategoryGroups(generalFeaturedEntries, generalLatestEntries),
[generalFeaturedEntries, generalLatestEntries],
);
const publicEntries = useMemo(
() => getPlatformPublicEntries(featuredEntries, latestEntries),
() =>
getPlatformPublicEntries(generalFeaturedEntries, generalLatestEntries),
[generalFeaturedEntries, generalLatestEntries],
);
const allPublicEntries = useMemo(
() => getAllPlatformPublicEntries(featuredEntries, latestEntries),
[featuredEntries, latestEntries],
);
const visibleHistoryEntries = useMemo(
() =>
historyEntries.filter((entry) => {
const matchedPublicWork = findPublicWorkForHistoryEntry(
entry,
allPublicEntries,
);
return !matchedPublicWork || canExposePublicWork(matchedPublicWork);
}),
[allPublicEntries, historyEntries],
);
const workSearchResults = useMemo(
() =>
filterPlatformWorkSearchResults(publicEntries, activeWorkSearchKeyword),
@@ -3188,6 +3263,12 @@ export function RpgEntryHomeView({
}
}, [activeTab, isAuthenticated, onTabChange, visibleTabs]);
useEffect(() => {
if (!visibleDiscoverChannels.some((channel) => channel.id === discoverChannel)) {
setDiscoverChannel('recommend');
}
}, [discoverChannel, visibleDiscoverChannels]);
useEffect(() => {
setVisitedTabs((currentTabs) => {
if (currentTabs.has(activeTab)) {
@@ -3670,6 +3751,10 @@ export function RpgEntryHomeView({
publicEntries,
trimmedKeyword,
);
const hiddenEdutainmentMatches = filterPlatformWorkSearchResults(
allEdutainmentEntries,
trimmedKeyword,
);
if (
matchedEntries.length > 0 &&
isExactPublicWorkCodeSearch(matchedEntries, trimmedKeyword) &&
@@ -3686,6 +3771,11 @@ export function RpgEntryHomeView({
return;
}
if (hiddenEdutainmentMatches.length > 0) {
setActiveWorkSearchKeyword(trimmedKeyword);
return;
}
setActiveWorkSearchKeyword('');
if (!onSearchPublicCode || isSearchingPublicCode) {
return;
@@ -3700,50 +3790,58 @@ export function RpgEntryHomeView({
submitWorkSearch(mobileSearchKeyword);
};
const desktopHeroEntry =
featuredShelf[0] ?? latestEntries[0] ?? myEntries[0] ?? null;
featuredShelf[0] ?? generalLatestEntries[0] ?? myEntries[0] ?? null;
const desktopHeroCover = desktopHeroEntry
? resolvePlatformWorldCoverImage(desktopHeroEntry)
: null;
const desktopHeroStripEntries = (
featuredShelf.length > 0 ? featuredShelf : latestEntries
featuredShelf.length > 0 ? featuredShelf : generalLatestEntries
).slice(0, 5);
// 网页端保留原有宽屏布局,只把模块数据同步到移动端首页频道语义。
const desktopRecommendEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredShelf, ...latestEntries].forEach((entry) => {
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
return Array.from(entryMap.values());
}, [featuredShelf, latestEntries]);
}, [featuredShelf, generalLatestEntries]);
const desktopTodayEntries = useMemo(
() => filterTodayPublishedEntries(latestEntries),
[latestEntries],
() => filterTodayPublishedEntries(generalLatestEntries),
[generalLatestEntries],
);
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
const desktopLibraryPreview = myEntries.slice(0, 2);
const recommendedFeedEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...featuredShelf, ...latestEntries].forEach((entry) => {
[...featuredShelf, ...generalLatestEntries].forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
return Array.from(entryMap.values());
}, [featuredShelf, latestEntries]);
}, [featuredShelf, generalLatestEntries]);
const discoverFeedEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
const sourceEntries =
discoverChannel === 'recommend'
? recommendedFeedEntries
: filterTodayPublishedEntries(latestEntries);
: filterTodayPublishedEntries(generalLatestEntries);
sourceEntries.forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
return Array.from(entryMap.values());
}, [discoverChannel, latestEntries, recommendedFeedEntries]);
}, [discoverChannel, generalLatestEntries, recommendedFeedEntries]);
const edutainmentFeedEntries = useMemo(() => {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
edutainmentEntries.forEach((entry) => {
entryMap.set(buildPublicGalleryCardKey(entry), entry);
});
return Array.from(entryMap.values());
}, [edutainmentEntries]);
const mobileFeedCarouselEnabled =
!isDesktopLayout &&
activeTab === 'category' &&
@@ -4022,7 +4120,7 @@ export function RpgEntryHomeView({
isAuthenticated,
openRecommendGalleryDetail,
]);
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
const leadPublicEntry = featuredShelf[0] ?? generalLatestEntries[0] ?? null;
const openLeadPublicEntry = () => {
if (leadPublicEntry) {
openRecommendGalleryDetail(leadPublicEntry);
@@ -4217,7 +4315,7 @@ export function RpgEntryHomeView({
) : (
<>
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
{DISCOVER_CHANNELS.map((channel) => {
{visibleDiscoverChannels.map((channel) => {
const active = discoverChannel === channel.id;
return (
<button
@@ -4296,6 +4394,31 @@ export function RpgEntryHomeView({
<EmptyShelf text="公开广场暂时还没有可分类的作品。" />
)}
</section>
) : discoverChannel === 'edutainment' ? (
<section className="platform-mobile-home-feed">
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : edutainmentFeedEntries.length > 0 ? (
<div className="grid min-w-0 gap-3">
{edutainmentFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
return (
<WorldCard
key={`${cardKey}:mobile-edutainment`}
entry={entry}
onClick={() => onOpenGalleryDetail(entry)}
className="w-full"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
feedCardKey={cardKey}
/>
);
})}
</div>
) : (
<EmptyShelf text="暂时还没有可展示的作品。" />
)}
</section>
) : (
<section
ref={mobileDiscoverFeedRef}
@@ -4332,8 +4455,122 @@ export function RpgEntryHomeView({
</div>
);
const desktopDiscoverContent: ReactNode = (
<div className={DESKTOP_PAGE_STAGE_CLASS}>
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
{visibleDiscoverChannels.map((channel) => {
const active = discoverChannel === channel.id;
return (
<button
key={`desktop-${channel.id}`}
type="button"
onClick={() => setDiscoverChannel(channel.id)}
className={`platform-mobile-home-channel shrink-0 ${active ? 'platform-mobile-home-channel--active' : ''}`}
>
{channel.label}
</button>
);
})}
</div>
{platformError ? (
<div className="rounded-[1.5rem] border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
{platformError}
</div>
) : null}
{discoverChannel === 'ranking' ? (
mobileRankingPanel
) : discoverChannel === 'category' ? (
<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-discover-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-discover-category:${activeCategoryGroup.tag}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
</>
) : (
<EmptyShelf text="暂时还没有可分类的作品。" />
)}
</section>
) : discoverChannel === 'edutainment' ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader title={EDUTAINMENT_WORK_TAG} detail="EDUTAINMENT" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : edutainmentFeedEntries.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-3">
{edutainmentFeedEntries.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-edutainment`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="暂时还没有可展示的作品。" />
)}
</section>
) : (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={discoverChannel === 'today' ? '今日游戏' : '推荐'}
detail={discoverChannel === 'today' ? 'TODAY GAMES' : 'RECOMMENDED'}
/>
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : discoverFeedEntries.length > 0 ? (
<div className="grid gap-4 xl:grid-cols-3">
{discoverFeedEntries.map((entry) => (
<WorldCard
key={`${buildPublicGalleryCardKey(entry)}:desktop-discover-feed:${discoverChannel}`}
entry={entry}
onClick={() => openRecommendGalleryDetail(entry)}
className="w-full min-w-0"
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
</div>
) : (
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
)}
</section>
)}
</div>
);
const categoryContent: ReactNode = isDesktopLayout ? (
<div className={DESKTOP_PAGE_STAGE_CLASS}>{mobileRankingPanel}</div>
desktopDiscoverContent
) : (
mobileDiscoverContent
);
@@ -4773,7 +5010,7 @@ export function RpgEntryHomeView({
</div>
<div
className={`grid gap-5 ${desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? '2xl:grid-cols-[minmax(0,1.2fr)_minmax(22rem,0.8fr)]' : ''}`}
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" />
@@ -4796,7 +5033,7 @@ export function RpgEntryHomeView({
)}
</section>
{desktopLibraryPreview.length > 0 || historyEntries.length > 0 ? (
{desktopLibraryPreview.length > 0 || visibleHistoryEntries.length > 0 ? (
<section className="platform-desktop-panel px-5 py-5">
<SectionHeader
title={desktopLibraryPreview.length > 0 ? '最近作品' : '最近浏览'}
@@ -4841,7 +5078,7 @@ export function RpgEntryHomeView({
</div>
) : (
<div className="mt-3 space-y-3">
{historyEntries.slice(0, 2).map((entry) => {
{visibleHistoryEntries.slice(0, 2).map((entry) => {
const displayName = formatPlatformWorkDisplayName(
entry.worldName,
);