feat: add baby object match edutainment flow
Some checks failed
CI / verify (push) Has been cancelled

This commit is contained in:
2026-05-12 16:08:59 +08:00
parent cf074837a4
commit d41f260a2a
58 changed files with 5628 additions and 466 deletions

View File

@@ -2,6 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
@@ -65,6 +66,8 @@ type CustomWorldCreationHubProps = {
onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null;
onClaimPuzzlePointIncentive?: ((item: PuzzleWorkSummary) => void) | null;
claimingPuzzleProfileId?: string | null;
babyObjectMatchItems?: BabyObjectMatchDraft[];
onOpenBabyObjectMatchDetail?: ((item: BabyObjectMatchDraft) => void) | null;
visualNovelItems?: VisualNovelWorkSummary[];
onOpenVisualNovelDetail?: ((item: VisualNovelWorkSummary) => void) | null;
onDeleteVisualNovel?: ((item: VisualNovelWorkSummary) => void) | null;
@@ -167,6 +170,8 @@ export function CustomWorldCreationHub({
onDeletePuzzle = null,
onClaimPuzzlePointIncentive = null,
claimingPuzzleProfileId = null,
babyObjectMatchItems = [],
onOpenBabyObjectMatchDetail = null,
visualNovelItems = [],
onOpenVisualNovelDetail = null,
onDeleteVisualNovel = null,
@@ -189,6 +194,7 @@ export function CustomWorldCreationHub({
match3dItems,
squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [],
puzzleItems,
babyObjectMatchItems,
visualNovelItems,
canDeleteRpg: Boolean(onDeletePublished),
canDeleteBigFish: Boolean(onDeleteBigFish),
@@ -209,6 +215,7 @@ export function CustomWorldCreationHub({
onOpenPuzzleDetail,
onDeletePuzzle: onDeletePuzzle ?? undefined,
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
onOpenBabyObjectMatchDetail: onOpenBabyObjectMatchDetail ?? undefined,
onOpenVisualNovelDetail: onOpenVisualNovelDetail ?? undefined,
onDeleteVisualNovel: onDeleteVisualNovel ?? undefined,
getItemState: getWorkState,
@@ -216,6 +223,7 @@ export function CustomWorldCreationHub({
[
bigFishItems,
isSquareHoleCreationVisible,
babyObjectMatchItems,
items,
match3dItems,
onDeleteBigFish,
@@ -228,6 +236,7 @@ export function CustomWorldCreationHub({
onOpenBigFishDetail,
onOpenDraft,
onOpenMatch3DDetail,
onOpenBabyObjectMatchDetail,
onOpenPuzzleDetail,
onOpenSquareHoleDetail,
onOpenVisualNovelDetail,
@@ -259,6 +268,37 @@ export function CustomWorldCreationHub({
[activeFilter, shelfItems],
);
function handleOpenShelfItem(item: CreationWorkShelfItem) {
switch (item.source.kind) {
case 'puzzle':
onOpenPuzzleDetail?.(item.source.item);
return;
case 'baby-object-match':
onOpenBabyObjectMatchDetail?.(item.source.item);
return;
case 'visual-novel':
onOpenVisualNovelDetail?.(item.source.item);
return;
case 'big-fish':
onOpenBigFishDetail?.(item.source.item);
return;
case 'match3d':
onOpenMatch3DDetail?.(item.source.item);
return;
case 'square-hole':
onOpenSquareHoleDetail?.(item.source.item);
return;
case 'rpg':
if (item.status === 'draft') {
onOpenDraft(item.source.item);
return;
}
if (item.source.item.profileId) {
onEnterPublished(item.source.item.profileId);
}
}
}
function buildDeleteAction(item: CreationWorkShelfItem) {
if (!item.canDelete) {

View File

@@ -1,5 +1,6 @@
import { expect, test, vi } from 'vitest';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import { buildCreationWorkShelfItems } from './creationWorkShelf';
test('buildCreationWorkShelfItems maps visual novel items with VN public code', () => {
@@ -81,3 +82,62 @@ test('buildCreationWorkShelfItems attaches open and delete actions through shelf
expect(onOpenPuzzleDetail).toHaveBeenCalledWith(puzzleWork);
expect(onDeletePuzzle).toHaveBeenCalledWith(puzzleWork);
});
test('buildCreationWorkShelfItems maps baby object match local drafts', () => {
const baseDraft: BabyObjectMatchDraft = {
draftId: 'baby-object-draft-1',
profileId: 'baby-object-profile-12345678',
templateId: 'baby-object-match',
templateName: '宝贝识物',
workTitle: '宝贝识物',
workDescription: '苹果和香蕉识物分类',
itemNames: ['苹果', '香蕉'],
itemAssets: [
{
itemId: 'baby-object-item-1',
itemName: '苹果',
imageSrc: '/apple.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '苹果',
},
{
itemId: 'baby-object-item-2',
itemName: '香蕉',
imageSrc: '/banana.png',
assetObjectId: null,
generationProvider: 'placeholder',
prompt: '香蕉',
},
],
themeTags: ['寓教于乐'],
publicationStatus: 'draft',
createdAt: '2026-05-11T00:00:00.000Z',
updatedAt: '2026-05-11T00:00:00.000Z',
publishedAt: null,
};
const items = buildCreationWorkShelfItems({
rpgItems: [],
bigFishItems: [],
puzzleItems: [],
babyObjectMatchItems: [
baseDraft,
{
...baseDraft,
draftId: 'baby-object-draft-2',
profileId: 'baby-object-profile-87654321',
publicationStatus: 'published',
publishedAt: '2026-05-11T01:00:00.000Z',
updatedAt: '2026-05-11T01:00:00.000Z',
},
],
});
expect(items[0]?.kind).toBe('baby-object-match');
expect(items[0]?.status).toBe('published');
expect(items[0]?.publicWorkCode).toBe('BO-87654321');
expect(items[0]?.sharePath).toContain('/works/detail?work=BO-87654321');
expect(items[1]?.status).toBe('draft');
expect(items[1]?.publicWorkCode).toBeNull();
});

View File

@@ -1,5 +1,6 @@
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
@@ -7,6 +8,7 @@ import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contrac
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
import {
buildBabyObjectMatchPublicWorkCode,
buildBigFishPublicWorkCode,
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
@@ -21,6 +23,7 @@ export type CreationWorkShelfKind =
| 'match3d'
| 'square-hole'
| 'puzzle'
| 'baby-object-match'
| 'visual-novel';
export type CreationWorkShelfStatus = 'draft' | 'published';
@@ -77,6 +80,10 @@ export type CreationWorkShelfSource =
| {
kind: 'visual-novel';
item: VisualNovelWorkSummary;
}
| {
kind: 'baby-object-match';
item: BabyObjectMatchDraft;
};
export type CreationWorkShelfActions = {
@@ -116,6 +123,7 @@ export function buildCreationWorkShelfItems(params: {
match3dItems?: Match3DWorkSummary[];
squareHoleItems?: SquareHoleWorkSummary[];
puzzleItems: PuzzleWorkSummary[];
babyObjectMatchItems?: BabyObjectMatchDraft[];
visualNovelItems?: VisualNovelWorkSummary[];
canDeleteRpg?: boolean;
canDeleteBigFish?: boolean;
@@ -135,6 +143,7 @@ export function buildCreationWorkShelfItems(params: {
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
onDeletePuzzle?: (item: PuzzleWorkSummary) => void;
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
onOpenBabyObjectMatchDetail?: (item: BabyObjectMatchDraft) => void;
onOpenVisualNovelDetail?: (item: VisualNovelWorkSummary) => void;
onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void;
getItemState?: (
@@ -148,6 +157,7 @@ export function buildCreationWorkShelfItems(params: {
match3dItems = [],
squareHoleItems = [],
puzzleItems,
babyObjectMatchItems = [],
visualNovelItems = [],
canDeleteRpg = false,
canDeleteBigFish = false,
@@ -167,6 +177,7 @@ export function buildCreationWorkShelfItems(params: {
onOpenPuzzleDetail,
onDeletePuzzle,
onClaimPuzzlePointIncentive,
onOpenBabyObjectMatchDetail,
onOpenVisualNovelDetail,
onDeleteVisualNovel,
getItemState,
@@ -205,6 +216,11 @@ export function buildCreationWorkShelfItems(params: {
onClaimPointIncentive: onClaimPuzzlePointIncentive,
}),
),
...babyObjectMatchItems.map((item) =>
mapBabyObjectMatchDraftToShelfItem(item, {
onOpen: onOpenBabyObjectMatchDetail,
}),
),
...visualNovelItems.map((item) =>
mapVisualNovelWorkToShelfItem(item, canDeleteVisualNovel, {
onOpen: onOpenVisualNovelDetail,
@@ -446,6 +462,55 @@ function mapPuzzleWorkToShelfItem(
};
}
function mapBabyObjectMatchDraftToShelfItem(
item: BabyObjectMatchDraft,
adapter: WorkShelfAdapter<BabyObjectMatchDraft>,
): CreationWorkShelfItem {
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
const publicWorkCode =
status === 'published'
? buildBabyObjectMatchPublicWorkCode(item.profileId)
: null;
const coverImageSrc =
item.itemAssets.find((asset) => asset.imageSrc.trim())?.imageSrc ?? null;
return {
id: item.profileId,
kind: 'baby-object-match',
status,
title: item.workTitle.trim() || item.templateName,
summary:
item.workDescription.trim() ||
`${item.itemNames[0]}${item.itemNames[1]}识物分类`,
updatedAt: item.updatedAt,
coverImageSrc,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
publicWorkCode,
sharePath:
publicWorkCode && status === 'published'
? buildPublicWorkStagePath('work-detail', publicWorkCode)
: null,
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
canDelete: false,
canShare: status === 'published' && Boolean(publicWorkCode),
badges: [
buildStatusBadge(status),
{ id: 'type', label: '宝贝识物', tone: 'neutral' },
],
metrics:
status === 'published'
? buildPublishedMetrics({
playCount: 0,
remixCount: 0,
likeCount: 0,
})
: [],
actions: buildWorkShelfActions(item, adapter),
source: { kind: 'baby-object-match', item },
};
}
function mapVisualNovelWorkToShelfItem(
item: VisualNovelWorkSummary,
canDelete: boolean,
@@ -541,7 +606,6 @@ function mapSquareHoleWorkToShelfItem(
};
}
function buildWorkShelfActions<TItem>(
item: TItem,
adapter: WorkShelfAdapter<TItem>,