feat: refresh creation config and visual assets

This commit is contained in:
2026-05-20 14:02:36 +08:00
parent 83e92fc3c4
commit ef09a23c35
509 changed files with 19470 additions and 43 deletions

View File

@@ -6069,6 +6069,10 @@ export function PlatformEntryFlowShellImpl({
pushAppHistoryPath('/runtime/baby-love-drawing');
}, [setSelectionStage]);
const startChildMotionDemo = useCallback(() => {
window.location.assign('/child-motion-demo');
}, []);
const resolveBabyObjectMatchRuntimeDraft = useCallback(
async (entry: PlatformPublicGalleryCard) => {
if (!isEdutainmentGalleryEntry(entry)) {
@@ -11806,6 +11810,7 @@ export function PlatformEntryFlowShellImpl({
onOpenCreateWorld={openCreationTypePicker}
onOpenCreateTypePicker={openCreationTypePicker}
onOpenGalleryDetail={openPublicGalleryDetail}
onOpenChildMotionDemo={startChildMotionDemo}
onOpenBabyLoveDrawing={startBabyLoveDrawingRuntime}
onOpenRecommendGalleryDetail={openRecommendGalleryDetail}
recommendRuntimeContent={recommendRuntimeContent}

View File

@@ -182,3 +182,29 @@ test('edutainment switch hides baby object match creation entry from database co
getVisiblePlatformCreationTypes(hiddenCards).map((item) => item.id),
).toEqual(['puzzle']);
});
test('baby object match entry is visible and open when database marks it creatable', () => {
const cards = derivePlatformCreationTypes([
{
id: 'baby-object-match',
title: '宝贝识物',
subtitle: '亲子识物分类',
badge: '可创建',
imageSrc: '/child-motion-demo/picture-book-grass-stage.png',
visible: true,
open: true,
sortOrder: 90,
updatedAtMicros: 1,
},
]);
expect(getVisiblePlatformCreationTypes(cards)).toEqual([
expect.objectContaining({
id: 'baby-object-match',
hidden: false,
locked: false,
}),
]);
expect(isPlatformCreationTypeVisible(cards, 'baby-object-match')).toBe(true);
expect(isPlatformCreationTypeOpen(cards, 'baby-object-match')).toBe(true);
});

View File

@@ -761,6 +761,7 @@ function renderLoggedOutHomeView(
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onOpenRecommendGalleryDetail'
| 'onOpenChildMotionDemo'
| 'onSearchPublicCode'
| 'recommendRuntimeContent'
| 'activeRecommendEntryKey'
@@ -814,6 +815,7 @@ function renderLoggedOutHomeView(
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
onOpenChildMotionDemo={overrides.onOpenChildMotionDemo}
onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail}
recommendRuntimeContent={
overrides.recommendRuntimeContent ?? (
@@ -912,6 +914,7 @@ function renderStatefulLoggedOutHomeView(
| 'latestEntries'
| 'onOpenGalleryDetail'
| 'onOpenRecommendGalleryDetail'
| 'onOpenChildMotionDemo'
| 'onSearchPublicCode'
| 'recommendRuntimeContent'
| 'activeRecommendEntryKey'
@@ -970,6 +973,7 @@ function renderStatefulLoggedOutHomeView(
onOpenCreateWorld={vi.fn()}
onOpenCreateTypePicker={vi.fn()}
onOpenGalleryDetail={overrides.onOpenGalleryDetail ?? vi.fn()}
onOpenChildMotionDemo={overrides.onOpenChildMotionDemo}
onOpenRecommendGalleryDetail={overrides.onOpenRecommendGalleryDetail}
recommendRuntimeContent={
overrides.recommendRuntimeContent ?? (
@@ -2207,6 +2211,7 @@ test('discover search fuzzy matches public work id, name, author and description
test('mobile discover keeps edutainment works in the last dedicated channel only', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
const onOpenChildMotionDemo = vi.fn();
const generalEntry = buildTaggedPuzzleEntry('normal01', '普通拼图作品', [
'儿童教育',
]);
@@ -2227,6 +2232,7 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
renderStatefulLoggedOutHomeView({
latestEntries: [edutainmentEntry, generalEntry],
onOpenChildMotionDemo,
onSearchPublicCode,
});
await user.click(screen.getByRole('button', { name: '发现' }));
@@ -2259,6 +2265,12 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
name: / Demo/u,
}),
).toBeTruthy();
const warmupButton = within(discoverPanel).getByRole('button', {
name: //u,
});
expect(warmupButton).toBeTruthy();
await user.click(warmupButton);
expect(onOpenChildMotionDemo).toHaveBeenCalledTimes(1);
expect(within(discoverPanel).queryByText('普通拼图作品')).toBeNull();
const searchInput =
@@ -2269,6 +2281,23 @@ test('mobile discover keeps edutainment works in the last dedicated channel only
expect(onSearchPublicCode).not.toHaveBeenCalled();
});
test('desktop discover shows child motion demo in edutainment channel', async () => {
mockDesktopLayout();
const user = userEvent.setup();
const onOpenChildMotionDemo = vi.fn();
renderStatefulLoggedOutHomeView({
onOpenChildMotionDemo,
});
await user.click(screen.getByRole('button', { name: '发现' }));
await user.click(screen.getByRole('button', { name: '寓教于乐' }));
const warmupButton = screen.getByRole('button', { name: //u });
expect(warmupButton).toBeTruthy();
await user.click(warmupButton);
expect(onOpenChildMotionDemo).toHaveBeenCalledTimes(1);
});
test('mobile discover hides edutainment channel and work when switch is disabled', async () => {
vi.stubEnv('VITE_ENABLE_EDUTAINMENT_ENTRY', 'false');
const user = userEvent.setup();

View File

@@ -170,6 +170,7 @@ export interface RpgEntryHomeViewProps {
onOpenCreateWorld: () => void;
onOpenCreateTypePicker: () => void;
onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void;
onOpenChildMotionDemo?: () => void;
onOpenBabyLoveDrawing?: () => void;
onOpenRecommendGalleryDetail?: (entry: PlatformPublicGalleryCard) => void;
recommendRuntimeContent?: ReactNode;
@@ -320,6 +321,11 @@ const BABY_LOVE_DRAWING_DEFAULT_CARD = {
subtitle: '空白画板',
summary: '挥动小手画一张画。',
};
const CHILD_MOTION_DEMO_DEFAULT_CARD = {
title: '热身关卡',
subtitle: '动作识别热身',
summary: '站位、招手和左右手活动。',
};
const PLATFORM_RANKING_TABS: Array<{
id: PlatformRankingTab;
@@ -3642,6 +3648,7 @@ export function RpgEntryHomeView({
onResumeSave,
onOpenCreateTypePicker,
onOpenGalleryDetail,
onOpenChildMotionDemo,
onOpenBabyLoveDrawing,
onOpenRecommendGalleryDetail,
recommendRuntimeContent,
@@ -5352,7 +5359,9 @@ export function RpgEntryHomeView({
<section className="platform-mobile-home-feed">
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : edutainmentFeedEntries.length > 0 || onOpenBabyLoveDrawing ? (
) : edutainmentFeedEntries.length > 0 ||
onOpenChildMotionDemo ||
onOpenBabyLoveDrawing ? (
<div className="grid min-w-0 gap-3">
{edutainmentFeedEntries.map((entry) => {
const cardKey = buildPublicGalleryCardKey(entry);
@@ -5368,6 +5377,24 @@ export function RpgEntryHomeView({
/>
);
})}
{onOpenChildMotionDemo ? (
<button
type="button"
className="platform-edutainment-level-card"
onClick={onOpenChildMotionDemo}
>
<span className="platform-edutainment-level-card__icon">
<Camera className="h-7 w-7" />
</span>
<span className="platform-edutainment-level-card__body">
<strong>{CHILD_MOTION_DEMO_DEFAULT_CARD.title}</strong>
<span>{CHILD_MOTION_DEMO_DEFAULT_CARD.subtitle}</span>
</span>
<span className="platform-edutainment-level-card__summary">
{CHILD_MOTION_DEMO_DEFAULT_CARD.summary}
</span>
</button>
) : null}
{onOpenBabyLoveDrawing ? (
<button
type="button"
@@ -5530,7 +5557,9 @@ export function RpgEntryHomeView({
<SectionHeader title={EDUTAINMENT_WORK_TAG} detail="EDUTAINMENT" />
{isLoadingPlatform ? (
<EmptyShelf text="正在读取公开作品..." />
) : edutainmentFeedEntries.length > 0 || onOpenBabyLoveDrawing ? (
) : edutainmentFeedEntries.length > 0 ||
onOpenChildMotionDemo ||
onOpenBabyLoveDrawing ? (
<div className="grid gap-4 xl:grid-cols-3">
{edutainmentFeedEntries.map((entry) => (
<WorldCard
@@ -5541,6 +5570,24 @@ export function RpgEntryHomeView({
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
/>
))}
{onOpenChildMotionDemo ? (
<button
type="button"
className="platform-edutainment-level-card"
onClick={onOpenChildMotionDemo}
>
<span className="platform-edutainment-level-card__icon">
<Camera className="h-7 w-7" />
</span>
<span className="platform-edutainment-level-card__body">
<strong>{CHILD_MOTION_DEMO_DEFAULT_CARD.title}</strong>
<span>{CHILD_MOTION_DEMO_DEFAULT_CARD.subtitle}</span>
</span>
<span className="platform-edutainment-level-card__summary">
{CHILD_MOTION_DEMO_DEFAULT_CARD.summary}
</span>
</button>
) : null}
{onOpenBabyLoveDrawing ? (
<button
type="button"