feat: add child motion entry and fix auth env
Some checks failed
CI / verify (push) Has been cancelled
Some checks failed
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user