Sync local updates with origin/master

This commit is contained in:
2026-05-26 23:00:08 +08:00
parent 6b9c0fb3db
commit 927dcf5664
21 changed files with 655 additions and 73 deletions

View File

@@ -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')

View File

@@ -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>

View File

@@ -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')

View File

@@ -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);

View File

@@ -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',

View File

@@ -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: [],

View File

@@ -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:

View File

@@ -352,6 +352,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';
@@ -2384,6 +2385,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 [];
}
}
@@ -3173,6 +3183,9 @@ export function PlatformEntryFlowShellImpl({
>(null);
const [woodenFishWork, setWoodenFishWork] =
useState<WoodenFishWorkProfileResponse | null>(null);
const [woodenFishWorks, setWoodenFishWorks] = useState<
WoodenFishWorkSummaryResponse[]
>([]);
const [woodenFishGalleryEntries, setWoodenFishGalleryEntries] = useState<
WoodenFishGalleryCardResponse[]
>([]);
@@ -3920,6 +3933,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);
@@ -4499,6 +4526,10 @@ export function PlatformEntryFlowShellImpl({
],
[jumpHopWorks, pendingDraftShelfItems],
);
const woodenFishShelfItems = useMemo(
() => woodenFishWorks,
[woodenFishWorks],
);
const match3dShelfItems = useMemo(
() => [
...buildPendingMatch3DWorks(pendingDraftShelfItems.match3d, match3dWorks),
@@ -4581,6 +4612,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,
@@ -4624,6 +4662,7 @@ export function PlatformEntryFlowShellImpl({
barkBattleShelfItems,
bigFishShelfItems,
jumpHopShelfItems,
woodenFishShelfItems,
creationHubItems,
isSquareHoleCreationVisible,
match3dShelfItems,
@@ -9088,6 +9127,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(
@@ -9105,6 +9146,8 @@ export function PlatformEntryFlowShellImpl({
}
}, [
openPublishShareModal,
refreshWoodenFishGallery,
refreshWoodenFishShelf,
setSelectionStage,
woodenFishWork?.summary.profileId,
]);
@@ -11750,6 +11793,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)) {
@@ -14622,6 +14707,7 @@ export function PlatformEntryFlowShellImpl({
if (isVisualNovelCreationOpen) {
void refreshVisualNovelShelf();
}
void refreshWoodenFishShelf();
void refreshBabyObjectMatchShelf();
void refreshBarkBattleShelf();
}
@@ -14634,6 +14720,7 @@ export function PlatformEntryFlowShellImpl({
refreshBarkBattleShelf,
refreshMatch3DShelf,
refreshPuzzleShelf,
refreshWoodenFishShelf,
refreshSquareHoleShelf,
refreshVisualNovelShelf,
selectionStage,
@@ -14728,6 +14815,7 @@ export function PlatformEntryFlowShellImpl({
void refreshSquareHoleShelf();
}
void refreshPuzzleShelf();
void refreshWoodenFishShelf();
if (isVisualNovelCreationOpen) {
void refreshVisualNovelShelf();
}
@@ -14781,6 +14869,7 @@ export function PlatformEntryFlowShellImpl({
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={isBigFishCreationVisible ? bigFishShelfItems : []}
jumpHopItems={isJumpHopCreationVisible ? jumpHopShelfItems : []}
woodenFishItems={woodenFishShelfItems}
onOpenBigFishDetail={
isBigFishCreationVisible
? (item) => {
@@ -14809,6 +14898,13 @@ export function PlatformEntryFlowShellImpl({
: null
}
onDeleteJumpHop={null}
onOpenWoodenFishDetail={(item) => {
runProtectedAction(() => {
markCreationFlowReturnToDraftShelf();
void openWoodenFishDraft(item);
});
}}
onDeleteWoodenFish={null}
match3dItems={match3dShelfItems}
onOpenMatch3DDetail={(item) => {
runProtectedAction(() => {

View File

@@ -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' },
'读取敲木鱼作品列表失败',
);
});

View File

@@ -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,
};