Merge branch 'master' into codex/remove-match3d-static-demo
This commit is contained in:
@@ -131,7 +131,10 @@ describe('CustomWorldGenerationView', () => {
|
||||
'justify-start',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'pt-[4%]',
|
||||
'z-30',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'pt-[2%]',
|
||||
);
|
||||
expect(screen.getByText('总进度').className).toContain('text-[9px]');
|
||||
expect(screen.getByText('42%').className).toContain('text-[1.15rem]');
|
||||
@@ -149,7 +152,7 @@ describe('CustomWorldGenerationView', () => {
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
.getAttribute('data-ring-start-degrees'),
|
||||
).toBe('225');
|
||||
).toBe('155');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: progressTitle })
|
||||
@@ -168,6 +171,9 @@ describe('CustomWorldGenerationView', () => {
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').tagName).toBe(
|
||||
'svg',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').getAttribute('class')).toContain(
|
||||
'z-0',
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring')
|
||||
@@ -183,6 +189,16 @@ describe('CustomWorldGenerationView', () => {
|
||||
.getByTestId('generation-hero-progress-ring-track')
|
||||
.getAttribute('stroke-width'),
|
||||
).toBe('18');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-track')
|
||||
.getAttribute('transform'),
|
||||
).toBe('rotate(155 200 200)');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-fill')
|
||||
.getAttribute('transform'),
|
||||
).toBe('rotate(155 200 200)');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-fill')
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useEffect, useId, useRef } from 'react';
|
||||
|
||||
import generationHeroVideo from '../../media/create_bg_video.mp4';
|
||||
|
||||
const GENERATION_PROGRESS_RING_START_DEGREES = 225;
|
||||
const GENERATION_PROGRESS_RING_START_DEGREES = 155;
|
||||
const GENERATION_PROGRESS_RING_SWEEP_DEGREES = 270;
|
||||
const GENERATION_PROGRESS_RING_VIEWBOX = 400;
|
||||
const GENERATION_PROGRESS_RING_CENTER = GENERATION_PROGRESS_RING_VIEWBOX / 2;
|
||||
@@ -173,7 +173,7 @@ export function GenerationProgressHero({
|
||||
>
|
||||
<svg
|
||||
data-testid="generation-hero-progress-ring"
|
||||
className="pointer-events-none absolute inset-0 h-full w-full"
|
||||
className="pointer-events-none absolute inset-0 z-0 h-full w-full"
|
||||
viewBox={`0 0 ${GENERATION_PROGRESS_RING_VIEWBOX} ${GENERATION_PROGRESS_RING_VIEWBOX}`}
|
||||
aria-hidden="true"
|
||||
preserveAspectRatio="xMidYMid meet"
|
||||
@@ -220,13 +220,13 @@ export function GenerationProgressHero({
|
||||
/>
|
||||
</svg>
|
||||
<div
|
||||
className="relative z-10 flex h-full w-full flex-col items-center justify-start pt-[4%] text-center sm:pt-[3%]"
|
||||
className="relative z-30 flex h-full w-full flex-col items-center justify-start pt-[2%] text-center sm:pt-[1.5%]"
|
||||
data-testid="generation-hero-progress-content"
|
||||
>
|
||||
<div className="text-[9px] font-black tracking-[0.16em] text-[#7f441f] sm:text-[10px]">
|
||||
<div className="relative z-30 text-[9px] font-black tracking-[0.16em] text-[#7f441f] sm:text-[10px]">
|
||||
总进度
|
||||
</div>
|
||||
<div className="mt-1 text-[1.15rem] font-black leading-none text-[#e45e14] sm:mt-1.5 sm:text-[1.9rem]">
|
||||
<div className="relative z-30 mt-1 text-[1.15rem] font-black leading-none text-[#e45e14] sm:mt-1.5 sm:text-[1.9rem]">
|
||||
{safeProgress}%
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -116,7 +116,10 @@ describe('BarkBattleGeneratingView', () => {
|
||||
'justify-start',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'pt-[4%]',
|
||||
'z-30',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-content').className).toContain(
|
||||
'pt-[2%]',
|
||||
);
|
||||
expect(screen.getByText('玩家形象')).toBeTruthy();
|
||||
expect(screen.getByText('进行中 36%')).toBeTruthy();
|
||||
@@ -142,7 +145,7 @@ describe('BarkBattleGeneratingView', () => {
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
.getAttribute('data-ring-start-degrees'),
|
||||
).toBe('225');
|
||||
).toBe('155');
|
||||
expect(
|
||||
screen
|
||||
.getByRole('progressbar', { name: '汪汪声浪素材生成进度' })
|
||||
@@ -161,6 +164,9 @@ describe('BarkBattleGeneratingView', () => {
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').tagName).toBe(
|
||||
'svg',
|
||||
);
|
||||
expect(screen.getByTestId('generation-hero-progress-ring').getAttribute('class')).toContain(
|
||||
'z-0',
|
||||
);
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring')
|
||||
@@ -176,6 +182,16 @@ describe('BarkBattleGeneratingView', () => {
|
||||
.getByTestId('generation-hero-progress-ring-track')
|
||||
.getAttribute('stroke-width'),
|
||||
).toBe('18');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-track')
|
||||
.getAttribute('transform'),
|
||||
).toBe('rotate(155 200 200)');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-fill')
|
||||
.getAttribute('transform'),
|
||||
).toBe('rotate(155 200 200)');
|
||||
expect(
|
||||
screen
|
||||
.getByTestId('generation-hero-progress-ring-fill')
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contract
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
|
||||
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
@@ -64,6 +65,9 @@ type CustomWorldCreationHubProps = {
|
||||
jumpHopItems?: JumpHopWorkSummaryResponse[];
|
||||
onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onDeleteJumpHop?: ((item: JumpHopWorkSummaryResponse) => void) | null;
|
||||
woodenFishItems?: WoodenFishWorkSummaryResponse[];
|
||||
onOpenWoodenFishDetail?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
|
||||
onDeleteWoodenFish?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
|
||||
puzzleItems?: PuzzleWorkSummary[];
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
onDeletePuzzle?: ((item: PuzzleWorkSummary) => void) | null;
|
||||
@@ -174,6 +178,9 @@ export function CustomWorldCreationHub({
|
||||
jumpHopItems = [],
|
||||
onOpenJumpHopDetail,
|
||||
onDeleteJumpHop = null,
|
||||
woodenFishItems = [],
|
||||
onOpenWoodenFishDetail = null,
|
||||
onDeleteWoodenFish = null,
|
||||
puzzleItems = [],
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle = null,
|
||||
@@ -207,6 +214,7 @@ export function CustomWorldCreationHub({
|
||||
match3dItems,
|
||||
squareHoleItems: isSquareHoleCreationVisible ? squareHoleItems : [],
|
||||
jumpHopItems,
|
||||
woodenFishItems,
|
||||
puzzleItems,
|
||||
babyObjectMatchItems,
|
||||
barkBattleItems,
|
||||
@@ -217,6 +225,7 @@ export function CustomWorldCreationHub({
|
||||
canDeleteSquareHole:
|
||||
isSquareHoleCreationVisible && Boolean(onDeleteSquareHole),
|
||||
canDeleteJumpHop: Boolean(onDeleteJumpHop),
|
||||
canDeleteWoodenFish: Boolean(onDeleteWoodenFish),
|
||||
canDeletePuzzle: Boolean(onDeletePuzzle),
|
||||
canDeleteBabyObjectMatch: Boolean(onDeleteBabyObjectMatch),
|
||||
canDeleteBarkBattle: Boolean(onDeleteBarkBattle),
|
||||
@@ -232,6 +241,8 @@ export function CustomWorldCreationHub({
|
||||
onDeleteSquareHole: onDeleteSquareHole ?? undefined,
|
||||
onOpenJumpHopDetail: onOpenJumpHopDetail ?? undefined,
|
||||
onDeleteJumpHop: onDeleteJumpHop ?? undefined,
|
||||
onOpenWoodenFishDetail: onOpenWoodenFishDetail ?? undefined,
|
||||
onDeleteWoodenFish: onDeleteWoodenFish ?? undefined,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle: onDeletePuzzle ?? undefined,
|
||||
onClaimPuzzlePointIncentive: onClaimPuzzlePointIncentive ?? undefined,
|
||||
@@ -259,6 +270,7 @@ export function CustomWorldCreationHub({
|
||||
onDeleteBarkBattle,
|
||||
onDeleteVisualNovel,
|
||||
onDeleteJumpHop,
|
||||
onDeleteWoodenFish,
|
||||
onClaimPuzzlePointIncentive,
|
||||
onOpenBigFishDetail,
|
||||
onOpenDraft,
|
||||
@@ -268,6 +280,7 @@ export function CustomWorldCreationHub({
|
||||
onOpenPuzzleDetail,
|
||||
onOpenSquareHoleDetail,
|
||||
onOpenVisualNovelDetail,
|
||||
onOpenWoodenFishDetail,
|
||||
onEnterPublished,
|
||||
getWorkState,
|
||||
puzzleItems,
|
||||
@@ -275,6 +288,7 @@ export function CustomWorldCreationHub({
|
||||
onOpenSquareHoleDetail,
|
||||
onOpenJumpHopDetail,
|
||||
jumpHopItems,
|
||||
woodenFishItems,
|
||||
visualNovelItems,
|
||||
],
|
||||
);
|
||||
@@ -325,6 +339,9 @@ export function CustomWorldCreationHub({
|
||||
case 'jump-hop':
|
||||
onOpenJumpHopDetail?.(item.source.item);
|
||||
return;
|
||||
case 'wooden-fish':
|
||||
onOpenWoodenFishDetail?.(item.source.item);
|
||||
return;
|
||||
case 'rpg':
|
||||
if (item.status === 'draft') {
|
||||
onOpenDraft(item.source.item);
|
||||
|
||||
@@ -60,6 +60,7 @@ const CREATION_WORK_KIND_FALLBACK_COVER: Record<CreationWorkShelfKind, string> =
|
||||
match3d: '/creation-type-references/match3d.webp',
|
||||
'square-hole': '/creation-type-references/square-hole.webp',
|
||||
'jump-hop': '/creation-type-references/jump-hop.webp',
|
||||
'wooden-fish': '/wooden-fish/default-hit-object.png',
|
||||
puzzle: '/creation-type-references/puzzle.webp',
|
||||
'baby-object-match': '/creation-type-references/creative-agent.webp',
|
||||
'bark-battle': '/creation-type-references/bark-battle.webp',
|
||||
|
||||
@@ -56,6 +56,47 @@ test('buildCreationWorkShelfItems maps visual novel items with VN public code',
|
||||
expect(items[1]?.publicWorkCode).toBeNull();
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems maps wooden fish items with WF public code', () => {
|
||||
const onOpenWoodenFishDetail = vi.fn();
|
||||
const woodenFishWork = {
|
||||
runtimeKind: 'wooden-fish' as const,
|
||||
workId: 'wooden-fish-work-1',
|
||||
profileId: 'wooden-fish-profile-12345678',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'wooden-fish-session-1',
|
||||
workTitle: '苹果敲木鱼',
|
||||
workDescription: '苹果主题木鱼。',
|
||||
themeTags: ['苹果', '休闲'],
|
||||
coverImageSrc: '/wooden-fish/apple-cover.png',
|
||||
publicationStatus: 'published',
|
||||
playCount: 9,
|
||||
updatedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishedAt: '2026-05-20T00:00:00.000Z',
|
||||
publishReady: true,
|
||||
generationStatus: 'ready' as const,
|
||||
};
|
||||
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [],
|
||||
woodenFishItems: [woodenFishWork],
|
||||
onOpenWoodenFishDetail,
|
||||
});
|
||||
|
||||
items[0]?.actions.open();
|
||||
|
||||
expect(items).toHaveLength(1);
|
||||
expect(items[0]?.kind).toBe('wooden-fish');
|
||||
expect(items[0]?.status).toBe('published');
|
||||
expect(items[0]?.publicWorkCode).toBe('WF-12345678');
|
||||
expect(items[0]?.sharePath).toContain('/works/detail?work=WF-12345678');
|
||||
expect(items[0]?.openActionLabel).toBe('查看详情');
|
||||
expect(items[0]?.badges.some((badge) => badge.label === '敲木鱼')).toBe(true);
|
||||
expect(items[0]?.metrics.find((metric) => metric.id === 'play-count')?.value).toBe(9);
|
||||
expect(onOpenWoodenFishDetail).toHaveBeenCalledWith(woodenFishWork);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems keeps published bark battle over duplicate draft', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
|
||||
@@ -8,6 +8,7 @@ import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contr
|
||||
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
|
||||
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import { buildPublicWorkStagePath } from '../../routing/appPageRoutes';
|
||||
import {
|
||||
buildBabyObjectMatchPublicWorkCode,
|
||||
@@ -19,6 +20,7 @@ import {
|
||||
buildPuzzlePublicWorkCode,
|
||||
buildSquareHolePublicWorkCode,
|
||||
buildVisualNovelPublicWorkCode,
|
||||
buildWoodenFishPublicWorkCode,
|
||||
} from '../../services/publicWorkCode';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
|
||||
@@ -34,6 +36,7 @@ export type CreationWorkShelfKind =
|
||||
| 'match3d'
|
||||
| 'square-hole'
|
||||
| 'jump-hop'
|
||||
| 'wooden-fish'
|
||||
| 'puzzle'
|
||||
| 'baby-object-match'
|
||||
| 'bark-battle'
|
||||
@@ -90,6 +93,10 @@ export type CreationWorkShelfSource =
|
||||
kind: 'jump-hop';
|
||||
item: JumpHopWorkSummaryResponse;
|
||||
}
|
||||
| {
|
||||
kind: 'wooden-fish';
|
||||
item: WoodenFishWorkSummaryResponse;
|
||||
}
|
||||
| {
|
||||
kind: 'puzzle';
|
||||
item: PuzzleWorkSummary;
|
||||
@@ -145,6 +152,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
match3dItems?: Match3DWorkSummary[];
|
||||
squareHoleItems?: SquareHoleWorkSummary[];
|
||||
jumpHopItems?: JumpHopWorkSummaryResponse[];
|
||||
woodenFishItems?: WoodenFishWorkSummaryResponse[];
|
||||
puzzleItems: PuzzleWorkSummary[];
|
||||
babyObjectMatchItems?: BabyObjectMatchDraft[];
|
||||
barkBattleItems?: BarkBattleWorkSummary[];
|
||||
@@ -154,6 +162,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteMatch3D?: boolean;
|
||||
canDeleteSquareHole?: boolean;
|
||||
canDeleteJumpHop?: boolean;
|
||||
canDeleteWoodenFish?: boolean;
|
||||
canDeletePuzzle?: boolean;
|
||||
canDeleteBabyObjectMatch?: boolean;
|
||||
canDeleteBarkBattle?: boolean;
|
||||
@@ -169,6 +178,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteSquareHole?: (item: SquareHoleWorkSummary) => void;
|
||||
onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onDeleteJumpHop?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onOpenWoodenFishDetail?: (item: WoodenFishWorkSummaryResponse) => void;
|
||||
onDeleteWoodenFish?: (item: WoodenFishWorkSummaryResponse) => void;
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
onDeletePuzzle?: (item: PuzzleWorkSummary) => void;
|
||||
onClaimPuzzlePointIncentive?: (item: PuzzleWorkSummary) => void;
|
||||
@@ -189,6 +200,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
match3dItems = [],
|
||||
squareHoleItems = [],
|
||||
jumpHopItems = [],
|
||||
woodenFishItems = [],
|
||||
puzzleItems,
|
||||
babyObjectMatchItems = [],
|
||||
barkBattleItems = [],
|
||||
@@ -198,6 +210,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
canDeleteMatch3D = false,
|
||||
canDeleteSquareHole = false,
|
||||
canDeleteJumpHop = false,
|
||||
canDeleteWoodenFish = false,
|
||||
canDeletePuzzle = false,
|
||||
canDeleteBabyObjectMatch = false,
|
||||
canDeleteBarkBattle = false,
|
||||
@@ -213,6 +226,8 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteSquareHole,
|
||||
onOpenJumpHopDetail,
|
||||
onDeleteJumpHop,
|
||||
onOpenWoodenFishDetail,
|
||||
onDeleteWoodenFish,
|
||||
onOpenPuzzleDetail,
|
||||
onDeletePuzzle,
|
||||
onClaimPuzzlePointIncentive,
|
||||
@@ -257,6 +272,12 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDelete: onDeleteJumpHop,
|
||||
}),
|
||||
),
|
||||
...woodenFishItems.map((item) =>
|
||||
mapWoodenFishWorkToShelfItem(item, canDeleteWoodenFish, {
|
||||
onOpen: onOpenWoodenFishDetail,
|
||||
onDelete: onDeleteWoodenFish,
|
||||
}),
|
||||
),
|
||||
...puzzleItems.map((item) =>
|
||||
mapPuzzleWorkToShelfItem(item, canDeletePuzzle, {
|
||||
onOpen: onOpenPuzzleDetail,
|
||||
@@ -815,6 +836,54 @@ function mapJumpHopWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
function mapWoodenFishWorkToShelfItem(
|
||||
item: WoodenFishWorkSummaryResponse,
|
||||
canDelete: boolean,
|
||||
adapter: WorkShelfAdapter<WoodenFishWorkSummaryResponse>,
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildWoodenFishPublicWorkCode(item.profileId) : null;
|
||||
const title = item.workTitle.trim() || '敲木鱼';
|
||||
const summary =
|
||||
item.workDescription.trim() || (status === 'draft' ? '未填写作品描述' : '');
|
||||
|
||||
return {
|
||||
id: item.workId,
|
||||
kind: 'wooden-fish',
|
||||
status,
|
||||
title,
|
||||
summary,
|
||||
authorDisplayName: resolveAuthorDisplayName(item),
|
||||
updatedAt: item.updatedAt,
|
||||
coverImageSrc: normalizeCoverImageSrc(item.coverImageSrc),
|
||||
coverRenderMode: 'image',
|
||||
coverCharacterImageSrcs: [],
|
||||
publicWorkCode,
|
||||
sharePath:
|
||||
publicWorkCode && status === 'published'
|
||||
? buildPublicWorkStagePath('work-detail', publicWorkCode)
|
||||
: null,
|
||||
openActionLabel: status === 'published' ? '查看详情' : '继续创作',
|
||||
canDelete,
|
||||
canShare: status === 'published' && Boolean(publicWorkCode),
|
||||
badges: [
|
||||
buildStatusBadge(status),
|
||||
{ id: 'type', label: '敲木鱼', tone: 'neutral' },
|
||||
],
|
||||
metrics:
|
||||
status === 'published'
|
||||
? buildPublishedMetrics({
|
||||
playCount: item.playCount,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
})
|
||||
: [],
|
||||
actions: buildWorkShelfActions(item, adapter),
|
||||
source: { kind: 'wooden-fish', item },
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function resolveAuthorDisplayName(
|
||||
...sources: Array<unknown>
|
||||
@@ -1026,6 +1095,8 @@ function isPersistedCreationWorkGenerating(item: CreationWorkShelfItem) {
|
||||
return item.source.item.generationStatus === 'generating';
|
||||
case 'puzzle':
|
||||
return isPersistedPuzzleDraftGenerating(item.source.item);
|
||||
case 'wooden-fish':
|
||||
return item.source.item.generationStatus === 'generating';
|
||||
case 'bark-battle':
|
||||
return isPersistedBarkBattleDraftGenerating(item.source.item);
|
||||
default:
|
||||
|
||||
@@ -343,6 +343,7 @@ import {
|
||||
type WoodenFishWorkProfileResponse,
|
||||
type WoodenFishWorkspaceCreateRequest,
|
||||
} from '../../services/wooden-fish/woodenFishClient';
|
||||
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
|
||||
import type { CustomWorldProfile } from '../../types';
|
||||
import { useAuthUi } from '../auth/AuthUiContext';
|
||||
import { PublishShareModal } from '../common/PublishShareModal';
|
||||
@@ -2375,6 +2376,15 @@ function getGenerationNoticeShelfKeys(item: CreationWorkShelfItem): string[] {
|
||||
item.source.item.workId,
|
||||
item.source.item.draftId,
|
||||
]);
|
||||
case 'wooden-fish':
|
||||
return collectDraftNoticeKeys('wooden-fish', [
|
||||
item.id,
|
||||
item.source.item.workId,
|
||||
item.source.item.profileId,
|
||||
item.source.item.sourceSessionId,
|
||||
]);
|
||||
default:
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3164,6 +3174,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
>(null);
|
||||
const [woodenFishWork, setWoodenFishWork] =
|
||||
useState<WoodenFishWorkProfileResponse | null>(null);
|
||||
const [woodenFishWorks, setWoodenFishWorks] = useState<
|
||||
WoodenFishWorkSummaryResponse[]
|
||||
>([]);
|
||||
const [woodenFishGalleryEntries, setWoodenFishGalleryEntries] = useState<
|
||||
WoodenFishGalleryCardResponse[]
|
||||
>([]);
|
||||
@@ -3911,6 +3924,20 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
}, []);
|
||||
|
||||
const refreshWoodenFishShelf = useCallback(async () => {
|
||||
try {
|
||||
const worksResponse = await woodenFishClient.listWorks();
|
||||
setWoodenFishWorks(worksResponse.items);
|
||||
return worksResponse.items;
|
||||
} catch (error) {
|
||||
setWoodenFishWorks([]);
|
||||
setWoodenFishError(
|
||||
resolvePuzzleErrorMessage(error, '读取敲木鱼作品列表失败。'),
|
||||
);
|
||||
return [];
|
||||
}
|
||||
}, [resolvePuzzleErrorMessage]);
|
||||
|
||||
const refreshPuzzleShelf = useCallback(async () => {
|
||||
setIsPuzzleLoadingLibrary(true);
|
||||
|
||||
@@ -4481,6 +4508,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
],
|
||||
[jumpHopWorks, pendingDraftShelfItems],
|
||||
);
|
||||
const woodenFishShelfItems = useMemo(
|
||||
() => woodenFishWorks,
|
||||
[woodenFishWorks],
|
||||
);
|
||||
const match3dShelfItems = useMemo(
|
||||
() => [
|
||||
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
|
||||
@@ -4563,6 +4594,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
),
|
||||
...woodenFishShelfItems.flatMap((item) =>
|
||||
collectDraftNoticeKeys('wooden-fish', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
),
|
||||
...match3dShelfItems.flatMap((item) =>
|
||||
collectDraftNoticeKeys('match3d', [
|
||||
item.workId,
|
||||
@@ -4606,6 +4644,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
barkBattleShelfItems,
|
||||
bigFishShelfItems,
|
||||
jumpHopShelfItems,
|
||||
woodenFishShelfItems,
|
||||
creationHubItems,
|
||||
isSquareHoleCreationVisible,
|
||||
match3dShelfItems,
|
||||
@@ -9059,6 +9098,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const response = await woodenFishClient.publishWork(profileId);
|
||||
setWoodenFishWork(response.item);
|
||||
void refreshWoodenFishShelf();
|
||||
void refreshWoodenFishGallery();
|
||||
openPublishShareModal({
|
||||
title: response.item.summary.workTitle || '敲木鱼',
|
||||
publicWorkCode: buildWoodenFishPublicWorkCode(
|
||||
@@ -9076,6 +9117,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
}, [
|
||||
openPublishShareModal,
|
||||
refreshWoodenFishGallery,
|
||||
refreshWoodenFishShelf,
|
||||
setSelectionStage,
|
||||
woodenFishWork?.summary.profileId,
|
||||
]);
|
||||
@@ -11714,6 +11757,48 @@ export function PlatformEntryFlowShellImpl({
|
||||
[openPublicWorkDetail, setSelectionStage],
|
||||
);
|
||||
|
||||
const openWoodenFishDraft = useCallback(
|
||||
async (item: WoodenFishWorkSummaryResponse) => {
|
||||
markDraftNoticeSeen(
|
||||
collectDraftNoticeKeys('wooden-fish', [
|
||||
item.workId,
|
||||
item.profileId,
|
||||
item.sourceSessionId,
|
||||
]),
|
||||
);
|
||||
|
||||
if (item.publicationStatus === 'published') {
|
||||
void openWoodenFishPublicWorkDetail(item.profileId);
|
||||
return;
|
||||
}
|
||||
|
||||
setWoodenFishError(null);
|
||||
setPublicWorkDetailError(null);
|
||||
setIsWoodenFishBusy(true);
|
||||
try {
|
||||
const detail = await woodenFishClient.getWorkDetail(item.profileId);
|
||||
setWoodenFishSession(null);
|
||||
setWoodenFishRun(null);
|
||||
setWoodenFishWork(detail.item);
|
||||
setWoodenFishRuntimeReturnStage('wooden-fish-result');
|
||||
enterCreateTab();
|
||||
setSelectionStage('wooden-fish-result');
|
||||
} catch (error) {
|
||||
setWoodenFishError(
|
||||
resolveRpgCreationErrorMessage(error, '读取敲木鱼草稿失败。'),
|
||||
);
|
||||
} finally {
|
||||
setIsWoodenFishBusy(false);
|
||||
}
|
||||
},
|
||||
[
|
||||
enterCreateTab,
|
||||
markDraftNoticeSeen,
|
||||
openWoodenFishPublicWorkDetail,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const openPublicGalleryDetail = useCallback(
|
||||
(entry: PlatformPublicGalleryCard) => {
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
@@ -14582,6 +14667,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
if (isVisualNovelCreationOpen) {
|
||||
void refreshVisualNovelShelf();
|
||||
}
|
||||
void refreshWoodenFishShelf();
|
||||
void refreshBabyObjectMatchShelf();
|
||||
void refreshBarkBattleShelf();
|
||||
}
|
||||
@@ -14594,6 +14680,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
refreshBarkBattleShelf,
|
||||
refreshMatch3DShelf,
|
||||
refreshPuzzleShelf,
|
||||
refreshWoodenFishShelf,
|
||||
refreshSquareHoleShelf,
|
||||
refreshVisualNovelShelf,
|
||||
selectionStage,
|
||||
@@ -14688,6 +14775,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
void refreshSquareHoleShelf();
|
||||
}
|
||||
void refreshPuzzleShelf();
|
||||
void refreshWoodenFishShelf();
|
||||
if (isVisualNovelCreationOpen) {
|
||||
void refreshVisualNovelShelf();
|
||||
}
|
||||
@@ -14741,6 +14829,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
|
||||
bigFishItems={isBigFishCreationVisible ? bigFishShelfItems : []}
|
||||
jumpHopItems={isJumpHopCreationVisible ? jumpHopShelfItems : []}
|
||||
woodenFishItems={woodenFishShelfItems}
|
||||
onOpenBigFishDetail={
|
||||
isBigFishCreationVisible
|
||||
? (item) => {
|
||||
@@ -14769,6 +14858,13 @@ export function PlatformEntryFlowShellImpl({
|
||||
: null
|
||||
}
|
||||
onDeleteJumpHop={null}
|
||||
onOpenWoodenFishDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
markCreationFlowReturnToDraftShelf();
|
||||
void openWoodenFishDraft(item);
|
||||
});
|
||||
}}
|
||||
onDeleteWoodenFish={null}
|
||||
match3dItems={match3dShelfItems}
|
||||
onOpenMatch3DDetail={(item) => {
|
||||
runProtectedAction(() => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
const requestJsonMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
const { createCreationAgentClientMock } = vi.hoisted(() => ({
|
||||
createCreationAgentClientMock: vi.fn(),
|
||||
}));
|
||||
@@ -9,7 +11,7 @@ vi.mock('../creation-agent', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../apiClient', () => ({
|
||||
requestJson: vi.fn(),
|
||||
requestJson: requestJsonMock,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -22,6 +24,7 @@ beforeEach(() => {
|
||||
streamMessage: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
});
|
||||
requestJsonMock.mockReset();
|
||||
});
|
||||
|
||||
test('wooden fish creation keeps image2 generation requests alive long enough', async () => {
|
||||
@@ -34,3 +37,16 @@ test('wooden fish creation keeps image2 generation requests alive long enough',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
test('wooden fish list works uses creation works endpoint', async () => {
|
||||
const { woodenFishClient } = await import('./woodenFishClient');
|
||||
requestJsonMock.mockResolvedValueOnce({ items: [] });
|
||||
|
||||
await woodenFishClient.listWorks();
|
||||
|
||||
expect(requestJsonMock).toHaveBeenCalledWith(
|
||||
'/api/creation/wooden-fish/works',
|
||||
{ method: 'GET' },
|
||||
'读取敲木鱼作品列表失败',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,6 +13,7 @@ import type {
|
||||
WoodenFishWorkDetailResponse,
|
||||
WoodenFishWorkMutationResponse,
|
||||
WoodenFishWorkProfileResponse,
|
||||
WoodenFishWorksResponse,
|
||||
WoodenFishWorkspaceCreateRequest,
|
||||
WoodenFishWorkSummaryResponse,
|
||||
} from '../../../packages/shared/src/contracts/woodenFish';
|
||||
@@ -57,6 +58,7 @@ export type {
|
||||
WoodenFishWorkDetailResponse,
|
||||
WoodenFishWorkMutationResponse,
|
||||
WoodenFishWorkProfileResponse,
|
||||
WoodenFishWorksResponse,
|
||||
WoodenFishWorkspaceCreateRequest,
|
||||
};
|
||||
export type CreateWoodenFishSessionRequest = WoodenFishWorkspaceCreateRequest;
|
||||
@@ -186,6 +188,15 @@ export async function getWoodenFishWorkDetail(profileId: string) {
|
||||
return normalizeWoodenFishWorkDetailResponse(response);
|
||||
}
|
||||
|
||||
export async function listWoodenFishWorks() {
|
||||
const response = await requestJson<WoodenFishWorksResponse>(
|
||||
WOODEN_FISH_WORKS_API_BASE,
|
||||
{ method: 'GET' },
|
||||
'读取敲木鱼作品列表失败',
|
||||
);
|
||||
return response;
|
||||
}
|
||||
|
||||
export async function listWoodenFishGallery() {
|
||||
return requestJson<WoodenFishGalleryResponse>(
|
||||
`${WOODEN_FISH_RUNTIME_API_BASE}/gallery`,
|
||||
@@ -312,6 +323,7 @@ export const woodenFishClient = {
|
||||
getSession: getWoodenFishCreationSession,
|
||||
getWorkDetail: getWoodenFishWorkDetail,
|
||||
listGallery: listWoodenFishGallery,
|
||||
listWorks: listWoodenFishWorks,
|
||||
publishWork: publishWoodenFishWork,
|
||||
startRun: startWoodenFishRuntimeRun,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user