Merge remote-tracking branch 'origin/codex/wooden-fish-template'
This commit is contained in:
@@ -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 ?? (
|
||||
@@ -2214,6 +2218,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', '普通拼图作品', [
|
||||
'儿童教育',
|
||||
]);
|
||||
@@ -2234,6 +2239,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: '发现' }));
|
||||
@@ -2266,6 +2272,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 =
|
||||
@@ -2276,6 +2288,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();
|
||||
|
||||
@@ -136,6 +136,7 @@ import {
|
||||
isPuzzleGalleryEntry,
|
||||
isSquareHoleGalleryEntry,
|
||||
isVisualNovelGalleryEntry,
|
||||
isWoodenFishGalleryEntry,
|
||||
type PlatformPublicGalleryCard,
|
||||
type PlatformWorldCardLike,
|
||||
resolvePlatformPublicWorkCode,
|
||||
@@ -173,6 +174,7 @@ export interface RpgEntryHomeViewProps {
|
||||
onOpenCreateWorld: () => void;
|
||||
onOpenCreateTypePicker: () => void;
|
||||
onOpenGalleryDetail: (entry: PlatformPublicGalleryCard) => void;
|
||||
onOpenChildMotionDemo?: () => void;
|
||||
onOpenBabyLoveDrawing?: () => void;
|
||||
onOpenRecommendGalleryDetail?: (entry: PlatformPublicGalleryCard) => void;
|
||||
recommendRuntimeContent?: ReactNode;
|
||||
@@ -326,6 +328,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;
|
||||
@@ -1896,26 +1903,35 @@ async function getPublicWorkAuthorSummary(
|
||||
}
|
||||
|
||||
function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||
const kind = isBigFishGalleryEntry(entry)
|
||||
? '大鱼'
|
||||
: isPuzzleGalleryEntry(entry)
|
||||
? '拼图'
|
||||
: isMatch3DGalleryEntry(entry)
|
||||
? '抓鹅'
|
||||
: isSquareHoleGalleryEntry(entry)
|
||||
? '方洞'
|
||||
: isVisualNovelGalleryEntry(entry)
|
||||
? '视觉'
|
||||
: isBarkBattleGalleryEntry(entry)
|
||||
? '汪汪'
|
||||
: isEdutainmentGalleryEntry(entry)
|
||||
? entry.templateName
|
||||
: isJumpHopGalleryEntry(entry)
|
||||
? '跳一跳'
|
||||
: describePlatformThemeLabel(entry.themeMode);
|
||||
return formatPlatformWorkDisplayTag(kind);
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isPuzzleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isMatch3DGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isSquareHoleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isJumpHopGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('???');
|
||||
}
|
||||
if (isWoodenFishGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('???');
|
||||
}
|
||||
if (isVisualNovelGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isBarkBattleGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag('??');
|
||||
}
|
||||
if (isEdutainmentGalleryEntry(entry)) {
|
||||
return formatPlatformWorkDisplayTag(entry.templateName);
|
||||
}
|
||||
return formatPlatformWorkDisplayTag(describePlatformThemeLabel(entry.themeMode));
|
||||
}
|
||||
|
||||
function getPublicAuthorAvatarLabel(authorDisplayName: string) {
|
||||
return Array.from(authorDisplayName.trim() || '玩')[0] ?? '玩';
|
||||
}
|
||||
@@ -3767,6 +3783,7 @@ export function RpgEntryHomeView({
|
||||
onResumeSave,
|
||||
onOpenCreateTypePicker,
|
||||
onOpenGalleryDetail,
|
||||
onOpenChildMotionDemo,
|
||||
onOpenBabyLoveDrawing,
|
||||
onOpenRecommendGalleryDetail,
|
||||
recommendRuntimeContent,
|
||||
@@ -5477,7 +5494,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);
|
||||
@@ -5493,6 +5512,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"
|
||||
@@ -5655,7 +5692,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
|
||||
@@ -5666,6 +5705,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"
|
||||
|
||||
@@ -11,9 +11,11 @@ import {
|
||||
isBarkBattleGalleryEntry,
|
||||
isEdutainmentGalleryEntry,
|
||||
isVisualNovelGalleryEntry,
|
||||
isWoodenFishGalleryEntry,
|
||||
mapBabyObjectMatchDraftToPlatformGalleryCard,
|
||||
mapBarkBattleWorkToPlatformGalleryCard,
|
||||
mapVisualNovelWorkToPlatformGalleryCard,
|
||||
mapWoodenFishWorkToPlatformGalleryCard,
|
||||
type PlatformEdutainmentGalleryCard,
|
||||
type PlatformPuzzleGalleryCard,
|
||||
resolvePlatformPublicWorkCode,
|
||||
@@ -167,6 +169,34 @@ test('maps visual novel work to platform gallery card with VN public code', () =
|
||||
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['悬疑', '列车']);
|
||||
});
|
||||
|
||||
test('maps wooden fish work to platform gallery card with WF public code', () => {
|
||||
const card = mapWoodenFishWorkToPlatformGalleryCard({
|
||||
publicWorkCode: '',
|
||||
workId: 'wooden-fish-work-1',
|
||||
profileId: 'wooden-fish-profile-12345678',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '玩家',
|
||||
workTitle: '每日一敲',
|
||||
workDescription: '敲一下,好事发生。',
|
||||
coverImageSrc: '/generated-wooden-fish-assets/profile/hit-object.png',
|
||||
themeTags: [],
|
||||
publicationStatus: 'published',
|
||||
playCount: 12,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
generationStatus: 'ready',
|
||||
});
|
||||
|
||||
expect(isWoodenFishGalleryEntry(card)).toBe(true);
|
||||
expect(card.sourceType).toBe('wooden-fish');
|
||||
expect(card.publicWorkCode).toBe('WF-12345678');
|
||||
expect(resolvePlatformPublicWorkCode(card)).toBe('WF-12345678');
|
||||
expect(resolvePlatformWorldFallbackCoverImage(card)).toBe(
|
||||
'/wooden-fish/default-hit-object.png',
|
||||
);
|
||||
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['敲木鱼']);
|
||||
});
|
||||
|
||||
test('keeps baby object match public card code and template label intact', () => {
|
||||
const card: PlatformEdutainmentGalleryCard = {
|
||||
sourceType: 'edutainment',
|
||||
|
||||
@@ -23,6 +23,10 @@ import type {
|
||||
SquareHoleWorkSummary,
|
||||
} from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type {
|
||||
WoodenFishGalleryCardResponse,
|
||||
WoodenFishWorkProfileResponse,
|
||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
|
||||
import { resolveCustomWorldCampSceneImage } from '../../data/customWorldVisuals';
|
||||
import {
|
||||
@@ -34,7 +38,9 @@ import {
|
||||
buildPuzzlePublicWorkCode,
|
||||
buildSquareHolePublicWorkCode,
|
||||
buildVisualNovelPublicWorkCode,
|
||||
buildWoodenFishPublicWorkCode,
|
||||
} from '../../services/publicWorkCode';
|
||||
import { WOODEN_FISH_DEFAULT_HIT_OBJECT_SRC } from '../../services/wooden-fish/woodenFishDefaults';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
export const PLATFORM_WORK_NAME_DISPLAY_LIMIT = 8;
|
||||
@@ -50,6 +56,7 @@ export type PlatformWorldCardLike =
|
||||
| PlatformSquareHoleGalleryCard
|
||||
| PlatformPuzzleGalleryCard
|
||||
| PlatformJumpHopGalleryCard
|
||||
| PlatformWoodenFishGalleryCard
|
||||
| PlatformVisualNovelGalleryCard
|
||||
| PlatformBarkBattleGalleryCard
|
||||
| PlatformEdutainmentGalleryCard;
|
||||
@@ -205,6 +212,28 @@ export type PlatformJumpHopGalleryCard = {
|
||||
stylePreset?: string;
|
||||
};
|
||||
|
||||
export type PlatformWoodenFishGalleryCard = {
|
||||
sourceType: 'wooden-fish';
|
||||
workId: string;
|
||||
profileId: string;
|
||||
sourceSessionId?: string | null;
|
||||
publicWorkCode: string;
|
||||
ownerUserId: string;
|
||||
authorDisplayName: string;
|
||||
worldName: string;
|
||||
subtitle: string;
|
||||
summaryText: string;
|
||||
coverImageSrc: string | null;
|
||||
themeTags: string[];
|
||||
playCount?: number;
|
||||
remixCount?: number;
|
||||
likeCount?: number;
|
||||
recentPlayCount7d?: number;
|
||||
visibility: 'published';
|
||||
publishedAt: string | null;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type PlatformEdutainmentGalleryCard = {
|
||||
sourceType: 'edutainment';
|
||||
templateId: typeof EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID;
|
||||
@@ -264,6 +293,7 @@ export type PlatformPublicGalleryCard =
|
||||
| PlatformSquareHoleGalleryCard
|
||||
| PlatformPuzzleGalleryCard
|
||||
| PlatformJumpHopGalleryCard
|
||||
| PlatformWoodenFishGalleryCard
|
||||
| PlatformVisualNovelGalleryCard
|
||||
| PlatformBarkBattleGalleryCard
|
||||
| PlatformEdutainmentGalleryCard;
|
||||
@@ -310,6 +340,12 @@ export function isJumpHopGalleryEntry(
|
||||
return 'sourceType' in entry && entry.sourceType === 'jump-hop';
|
||||
}
|
||||
|
||||
export function isWoodenFishGalleryEntry(
|
||||
entry: PlatformWorldCardLike,
|
||||
): entry is PlatformWoodenFishGalleryCard {
|
||||
return 'sourceType' in entry && entry.sourceType === 'wooden-fish';
|
||||
}
|
||||
|
||||
export function isEdutainmentGalleryEntry(
|
||||
entry: PlatformWorldCardLike,
|
||||
): entry is PlatformEdutainmentGalleryCard {
|
||||
@@ -510,6 +546,39 @@ export function mapJumpHopWorkToPlatformGalleryCard(
|
||||
};
|
||||
}
|
||||
|
||||
export function mapWoodenFishWorkToPlatformGalleryCard(
|
||||
work: WoodenFishGalleryCardResponse | WoodenFishWorkProfileResponse,
|
||||
): PlatformWoodenFishGalleryCard {
|
||||
const summary = 'summary' in work ? work.summary : work;
|
||||
|
||||
return {
|
||||
sourceType: 'wooden-fish',
|
||||
workId: summary.workId,
|
||||
profileId: summary.profileId,
|
||||
sourceSessionId:
|
||||
'sourceSessionId' in summary ? (summary.sourceSessionId ?? null) : null,
|
||||
publicWorkCode:
|
||||
'publicWorkCode' in summary && summary.publicWorkCode.trim()
|
||||
? summary.publicWorkCode
|
||||
: buildWoodenFishPublicWorkCode(summary.profileId),
|
||||
ownerUserId: summary.ownerUserId,
|
||||
authorDisplayName:
|
||||
'authorDisplayName' in summary ? summary.authorDisplayName : '玩家',
|
||||
worldName: summary.workTitle,
|
||||
subtitle: '敲木鱼',
|
||||
summaryText: summary.workDescription,
|
||||
coverImageSrc: summary.coverImageSrc ?? null,
|
||||
themeTags: summary.themeTags.length > 0 ? summary.themeTags : ['敲木鱼'],
|
||||
playCount: summary.playCount ?? 0,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
recentPlayCount7d: 0,
|
||||
visibility: 'published',
|
||||
publishedAt: summary.publishedAt ?? null,
|
||||
updatedAt: summary.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
export function mapBabyObjectMatchDraftToPlatformGalleryCard(
|
||||
draft: BabyObjectMatchDraft,
|
||||
): PlatformEdutainmentGalleryCard {
|
||||
@@ -649,6 +718,10 @@ export function resolvePlatformWorldFallbackCoverImage(
|
||||
return '/creation-type-references/jump-hop.webp';
|
||||
}
|
||||
|
||||
if (isWoodenFishGalleryEntry(entry)) {
|
||||
return WOODEN_FISH_DEFAULT_HIT_OBJECT_SRC;
|
||||
}
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
return '/creation-type-references/big-fish.webp';
|
||||
}
|
||||
@@ -822,6 +895,12 @@ export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
|
||||
: ['跳一跳'];
|
||||
}
|
||||
|
||||
if (isWoodenFishGalleryEntry(entry)) {
|
||||
return entry.themeTags.length > 0
|
||||
? entry.themeTags.slice(0, 3)
|
||||
: ['敲木鱼'];
|
||||
}
|
||||
|
||||
if (isEdutainmentGalleryEntry(entry)) {
|
||||
return entry.themeTags.length > 0
|
||||
? entry.themeTags.slice(0, 3)
|
||||
@@ -924,6 +1003,10 @@ export function resolvePlatformPublicWorkCode(
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
if (isWoodenFishGalleryEntry(entry)) {
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
if (isEdutainmentGalleryEntry(entry)) {
|
||||
return entry.publicWorkCode;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user