Refine play type integration flow and docs
This commit is contained in:
@@ -560,7 +560,7 @@ test('creation hub shows RPG public work code from published library entry', ()
|
||||
expect(screen.queryByText('CW-00000001')).toBeNull();
|
||||
});
|
||||
|
||||
test('creation hub exposes persisted draft delete action directly on the card', () => {
|
||||
test('creation hub keeps persisted draft delete action off the card header', () => {
|
||||
const { container } = render(
|
||||
<CustomWorldCreationHub
|
||||
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
|
||||
@@ -579,7 +579,7 @@ test('creation hub exposes persisted draft delete action directly on the card',
|
||||
expect(
|
||||
container.querySelector('.creation-work-card__swipe-underlay'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
|
||||
});
|
||||
|
||||
test('creation hub reveals persisted draft delete action from left swipe', () => {
|
||||
@@ -641,6 +641,75 @@ test('creation hub reveals persisted draft delete action from keyboard', async (
|
||||
expect(screen.queryByRole('button', { name: '分享' })).toBeNull();
|
||||
});
|
||||
|
||||
test('creation hub reveals persisted draft delete action from long press menu', () => {
|
||||
const { container } = render(
|
||||
<CustomWorldCreationHub
|
||||
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
onDeletePublished={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
const card = screen.getByRole('button', { name: /继续完善《潮雾列岛》/u });
|
||||
fireEvent.contextMenu(card);
|
||||
|
||||
expect(
|
||||
container.querySelector('.creation-work-card-shell--actions-visible'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('creation hub gives every deletable work card a side delete action', () => {
|
||||
const { container } = render(
|
||||
<CustomWorldCreationHub
|
||||
items={[{ ...baseDraftItem, profileId: 'profile-1' }]}
|
||||
babyObjectMatchItems={[babyObjectMatchDraftItem]}
|
||||
puzzleItems={[
|
||||
{
|
||||
workId: 'puzzle:side-delete',
|
||||
profileId: 'puzzle-profile-side-delete',
|
||||
ownerUserId: 'user-1',
|
||||
authorDisplayName: '拼图作者',
|
||||
levelName: '侧边删除拼图',
|
||||
summary: '不同来源都应有侧边删除。',
|
||||
themeTags: ['灯塔'],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: new Date('2026-05-02T12:00:00.000Z').toISOString(),
|
||||
publishedAt: null,
|
||||
playCount: 0,
|
||||
remixCount: 0,
|
||||
likeCount: 0,
|
||||
publishReady: true,
|
||||
},
|
||||
]}
|
||||
loading={false}
|
||||
error={null}
|
||||
onRetry={() => {}}
|
||||
onCreateType={noopCreateType}
|
||||
onOpenDraft={() => {}}
|
||||
onEnterPublished={() => {}}
|
||||
onDeletePublished={() => {}}
|
||||
onDeleteBabyObjectMatch={() => {}}
|
||||
onDeletePuzzle={() => {}}
|
||||
entryConfig={testEntryConfig}
|
||||
creationTypes={testCreationTypes}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
|
||||
expect(
|
||||
container.querySelectorAll('.creation-work-card__swipe-underlay'),
|
||||
).toHaveLength(3);
|
||||
});
|
||||
|
||||
test('creation hub shows delete action for baby object match drafts', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDeleteBabyObjectMatch = vi.fn();
|
||||
@@ -719,7 +788,7 @@ test('creation hub works-only tab filters bark battle draft and published works'
|
||||
expect(onOpenBarkBattleDetail).toHaveBeenCalledWith(barkBattlePublishedItem);
|
||||
});
|
||||
|
||||
test('creation hub published work delete action is directly visible', async () => {
|
||||
test('creation hub published work delete action stays in revealed side actions', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDeletePuzzle = vi.fn();
|
||||
const onOpenPuzzleDetail = vi.fn();
|
||||
@@ -759,9 +828,11 @@ test('creation hub published work delete action is directly visible', async () =
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByRole('button', { name: '删除' })).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
|
||||
expect(screen.getByRole('button', { name: '分享' })).toBeTruthy();
|
||||
|
||||
screen.getByRole('button', { name: /查看详情《待删拼图》/u }).focus();
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||||
|
||||
expect(onDeletePuzzle).toHaveBeenCalledWith(
|
||||
@@ -770,7 +841,7 @@ test('creation hub published work delete action is directly visible', async () =
|
||||
expect(onOpenPuzzleDetail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('creation hub exposes work delete action directly on card', async () => {
|
||||
test('creation hub reveals draft work delete action from keyboard', async () => {
|
||||
const user = userEvent.setup();
|
||||
const onDeletePuzzle = vi.fn();
|
||||
const onOpenPuzzleDetail = vi.fn();
|
||||
@@ -810,6 +881,10 @@ test('creation hub exposes work delete action directly on card', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.queryByRole('button', { name: '删除' })).toBeNull();
|
||||
|
||||
screen.getByRole('button', { name: /继续创作《直接删除拼图》/u }).focus();
|
||||
await user.keyboard('{ArrowLeft}');
|
||||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||||
|
||||
expect(onDeletePuzzle).toHaveBeenCalledWith(
|
||||
@@ -858,7 +933,9 @@ test('creation hub keeps swipe delete action available', async () => {
|
||||
/>,
|
||||
);
|
||||
|
||||
const card = screen.getByRole('button', { name: /查看详情《左滑删除拼图》/u });
|
||||
const card = screen.getByRole('button', {
|
||||
name: /查看详情《左滑删除拼图》/u,
|
||||
});
|
||||
fireEvent.touchStart(card, {
|
||||
touches: [{ clientX: 180, clientY: 20 }],
|
||||
});
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
buildCreationWorkShelfItems,
|
||||
type CreationWorkShelfItem,
|
||||
type CreationWorkShelfMetricId,
|
||||
type CreationWorkShelfRuntimeState,
|
||||
} from './creationWorkShelf';
|
||||
import { CustomWorldCreationStartCard } from './CustomWorldCreationStartCard';
|
||||
import { CustomWorldWorkCard } from './CustomWorldWorkCard';
|
||||
@@ -66,7 +67,9 @@ type CustomWorldCreationHubProps = {
|
||||
onOpenJumpHopDetail?: (item: JumpHopWorkSummaryResponse) => void;
|
||||
onDeleteJumpHop?: ((item: JumpHopWorkSummaryResponse) => void) | null;
|
||||
woodenFishItems?: WoodenFishWorkSummaryResponse[];
|
||||
onOpenWoodenFishDetail?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
|
||||
onOpenWoodenFishDetail?:
|
||||
| ((item: WoodenFishWorkSummaryResponse) => void)
|
||||
| null;
|
||||
onDeleteWoodenFish?: ((item: WoodenFishWorkSummaryResponse) => void) | null;
|
||||
puzzleItems?: PuzzleWorkSummary[];
|
||||
onOpenPuzzleDetail?: (item: PuzzleWorkSummary) => void;
|
||||
@@ -84,7 +87,7 @@ type CustomWorldCreationHubProps = {
|
||||
onDeleteVisualNovel?: ((item: VisualNovelWorkSummary) => void) | null;
|
||||
getWorkState?: (
|
||||
item: CreationWorkShelfItem,
|
||||
) => { isGenerating?: boolean; hasUnreadUpdate?: boolean } | null;
|
||||
) => CreationWorkShelfRuntimeState | null;
|
||||
onOpenShelfItem?: (item: CreationWorkShelfItem) => void;
|
||||
mode?: 'full' | 'start-only' | 'works-only';
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
BadgeCheck,
|
||||
CircleAlert,
|
||||
Clock3,
|
||||
Loader2,
|
||||
Share2,
|
||||
@@ -439,11 +440,8 @@ export function CustomWorldWorkCard({
|
||||
return;
|
||||
}
|
||||
|
||||
updateSwipeOffset(
|
||||
gesture,
|
||||
event.clientX,
|
||||
event.clientY,
|
||||
() => event.preventDefault(),
|
||||
updateSwipeOffset(gesture, event.clientX, event.clientY, () =>
|
||||
event.preventDefault(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -473,9 +471,7 @@ export function CustomWorldWorkCard({
|
||||
}
|
||||
};
|
||||
|
||||
const beginTouchSwipeGesture = (
|
||||
event: ReactTouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
const beginTouchSwipeGesture = (event: ReactTouchEvent<HTMLDivElement>) => {
|
||||
if (swipeRevealWidth <= 0) {
|
||||
return;
|
||||
}
|
||||
@@ -494,20 +490,15 @@ export function CustomWorldWorkCard({
|
||||
};
|
||||
};
|
||||
|
||||
const updateTouchSwipeGesture = (
|
||||
event: ReactTouchEvent<HTMLDivElement>,
|
||||
) => {
|
||||
const updateTouchSwipeGesture = (event: ReactTouchEvent<HTMLDivElement>) => {
|
||||
const gesture = swipeGestureRef.current;
|
||||
const touch = event.touches[0];
|
||||
if (!gesture || gesture.pointerId !== -1 || !touch) {
|
||||
return;
|
||||
}
|
||||
|
||||
updateSwipeOffset(
|
||||
gesture,
|
||||
touch.clientX,
|
||||
touch.clientY,
|
||||
() => event.preventDefault(),
|
||||
updateSwipeOffset(gesture, touch.clientX, touch.clientY, () =>
|
||||
event.preventDefault(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -676,8 +667,8 @@ export function CustomWorldWorkCard({
|
||||
{displayTitle}
|
||||
</span>
|
||||
</div>
|
||||
<div className="creation-work-card__quick-actions">
|
||||
{canUseShareAction ? (
|
||||
{canUseShareAction ? (
|
||||
<div className="creation-work-card__quick-actions">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
@@ -713,38 +704,8 @@ export function CustomWorldWorkCard({
|
||||
>
|
||||
<Share2 aria-hidden="true" className="h-4 w-4" />
|
||||
</button>
|
||||
) : null}
|
||||
{onDelete ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
suppressOpenRef.current = false;
|
||||
closeSwipeActions();
|
||||
onDelete();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onPointerDown={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
onTouchStart={(event) => {
|
||||
event.stopPropagation();
|
||||
}}
|
||||
disabled={deleteBusy}
|
||||
title={deleteBusy ? '删除中' : '删除作品'}
|
||||
aria-label={deleteBusy ? '删除中' : '删除'}
|
||||
className="creation-work-card__quick-action-button creation-work-card__quick-action-button--danger"
|
||||
>
|
||||
{deleteBusy ? (
|
||||
<span className="text-xs leading-none">...</span>
|
||||
) : (
|
||||
<Trash2 aria-hidden="true" className="h-4 w-4" />
|
||||
)}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="creation-work-card__meta platform-category-game-item__meta">
|
||||
@@ -762,6 +723,16 @@ export function CustomWorldWorkCard({
|
||||
{item.summary}
|
||||
</div>
|
||||
|
||||
{item.hasGenerationFailure ? (
|
||||
<div
|
||||
aria-label={item.generationFailureSummary ?? '生成失败'}
|
||||
className="creation-work-card__failure-status"
|
||||
>
|
||||
<CircleAlert aria-hidden="true" className="h-3.5 w-3.5" />
|
||||
<span>{item.generationFailureSummary ?? '生成失败'}</span>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isPublished ? (
|
||||
<div className="creation-work-card__published-info">
|
||||
{item.pointIncentive ? (
|
||||
|
||||
@@ -93,7 +93,9 @@ test('buildCreationWorkShelfItems maps wooden fish items with WF public code', (
|
||||
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(
|
||||
items[0]?.metrics.find((metric) => metric.id === 'play-count')?.value,
|
||||
).toBe(9);
|
||||
expect(onOpenWoodenFishDetail).toHaveBeenCalledWith(woodenFishWork);
|
||||
});
|
||||
|
||||
@@ -211,9 +213,9 @@ test('buildCreationWorkShelfItems keeps separate bark battle draft and published
|
||||
expect(items.find((item) => item.status === 'published')?.id).toBe(
|
||||
'BB-PUB00001',
|
||||
);
|
||||
expect(items.find((item) => item.status === 'published')?.publicWorkCode).toBe(
|
||||
'BB-PUB00001',
|
||||
);
|
||||
expect(
|
||||
items.find((item) => item.status === 'published')?.publicWorkCode,
|
||||
).toBe('BB-PUB00001');
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems falls back to deterministic RPG public work code when library entry is missing', () => {
|
||||
@@ -303,10 +305,9 @@ test('buildCreationWorkShelfItems gives bark battle draft cover from character o
|
||||
expect(items.find((item) => item.id === 'BB-COVER001')?.coverImageSrc).toBe(
|
||||
'/draft-player-cover.png',
|
||||
);
|
||||
expect(items.find((item) => item.id === 'BB-COVER001')?.coverCharacterImageSrcs).toEqual([
|
||||
'/draft-player-cover.png',
|
||||
'/draft-opponent-cover.png',
|
||||
]);
|
||||
expect(
|
||||
items.find((item) => item.id === 'BB-COVER001')?.coverCharacterImageSrcs,
|
||||
).toEqual(['/draft-player-cover.png', '/draft-opponent-cover.png']);
|
||||
expect(items.find((item) => item.id === 'BB-COVER002')?.coverImageSrc).toBe(
|
||||
'/creation-type-references/bark-battle.webp',
|
||||
);
|
||||
@@ -457,14 +458,76 @@ test('buildCreationWorkShelfItems restores persisted generation state for puzzle
|
||||
],
|
||||
});
|
||||
|
||||
expect(items.find((item) => item.kind === 'puzzle')?.isGenerating).toBe(
|
||||
true,
|
||||
);
|
||||
expect(items.find((item) => item.kind === 'puzzle')?.isGenerating).toBe(true);
|
||||
expect(items.find((item) => item.kind === 'match3d')?.isGenerating).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems lets failure notice override persisted generating copy', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [
|
||||
{
|
||||
workId: 'puzzle:failed-generating',
|
||||
profileId: 'puzzle-profile-failed-generating',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'puzzle-session-failed-generating',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '失败拼图',
|
||||
summary: '正在生成拼图草稿。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-08T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'generating',
|
||||
},
|
||||
],
|
||||
getItemState: (item) =>
|
||||
item.kind === 'puzzle'
|
||||
? {
|
||||
isGenerating: false,
|
||||
suppressPersistedGenerating: true,
|
||||
summaryOverride: '拼图草稿生成失败,可重新打开处理。',
|
||||
}
|
||||
: null,
|
||||
});
|
||||
|
||||
expect(items[0]?.isGenerating).toBe(false);
|
||||
expect(items[0]?.summary).toBe('拼图草稿生成失败,可重新打开处理。');
|
||||
});
|
||||
|
||||
test('persisted failed puzzle draft is not treated as generating', () => {
|
||||
const items = buildCreationWorkShelfItems({
|
||||
rpgItems: [],
|
||||
bigFishItems: [],
|
||||
puzzleItems: [
|
||||
{
|
||||
workId: 'puzzle:failed',
|
||||
profileId: 'puzzle-profile-failed',
|
||||
ownerUserId: 'user-1',
|
||||
sourceSessionId: 'puzzle-session-failed',
|
||||
authorDisplayName: '测试作者',
|
||||
levelName: '失败拼图',
|
||||
summary: '服务端已回写失败。',
|
||||
themeTags: [],
|
||||
coverImageSrc: null,
|
||||
publicationStatus: 'draft',
|
||||
updatedAt: '2026-05-08T00:00:00.000Z',
|
||||
publishedAt: null,
|
||||
publishReady: false,
|
||||
generationStatus: 'failed',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(items[0]?.isGenerating).toBeFalsy();
|
||||
expect(items[0]?.summary).toBe('服务端已回写失败。');
|
||||
});
|
||||
|
||||
test('buildCreationWorkShelfItems maps baby object match local drafts', () => {
|
||||
const onOpenBabyObjectMatchDetail = vi.fn();
|
||||
const onDeleteBabyObjectMatch = vi.fn();
|
||||
@@ -1088,7 +1151,6 @@ test('bark battle draft generating state only follows pending assets', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
|
||||
test('CustomWorldWorkCard hides author on shelf draft and published cards', () => {
|
||||
const buildItem = (
|
||||
status: CreationWorkShelfItem['status'],
|
||||
@@ -1110,7 +1172,11 @@ test('CustomWorldWorkCard hides author on shelf draft and published cards', () =
|
||||
canDelete: false,
|
||||
canShare: false,
|
||||
badges: [
|
||||
{ id: 'status', label: status === 'draft' ? '草稿' : '已发布', tone: 'neutral' },
|
||||
{
|
||||
id: 'status',
|
||||
label: status === 'draft' ? '草稿' : '已发布',
|
||||
tone: 'neutral',
|
||||
},
|
||||
{ id: 'type', label: '汪汪', tone: 'neutral' },
|
||||
],
|
||||
metrics: [],
|
||||
|
||||
@@ -125,6 +125,8 @@ export type CreationWorkShelfItem = {
|
||||
kind: CreationWorkShelfKind;
|
||||
status: CreationWorkShelfStatus;
|
||||
isGenerating?: boolean;
|
||||
hasGenerationFailure?: boolean;
|
||||
generationFailureSummary?: string;
|
||||
hasUnreadUpdate?: boolean;
|
||||
title: string;
|
||||
summary: string;
|
||||
@@ -145,6 +147,16 @@ export type CreationWorkShelfItem = {
|
||||
source: CreationWorkShelfSource;
|
||||
};
|
||||
|
||||
export type CreationWorkShelfRuntimeState = {
|
||||
isGenerating?: boolean;
|
||||
hasGenerationFailure?: boolean;
|
||||
generationFailureSummary?: string;
|
||||
hasUnreadUpdate?: boolean;
|
||||
suppressPersistedGenerating?: boolean;
|
||||
titleOverride?: string;
|
||||
summaryOverride?: string;
|
||||
};
|
||||
|
||||
export function buildCreationWorkShelfItems(params: {
|
||||
rpgItems: CustomWorldWorkSummary[];
|
||||
rpgLibraryEntries?: CustomWorldLibraryEntry<CustomWorldProfile>[];
|
||||
@@ -191,7 +203,7 @@ export function buildCreationWorkShelfItems(params: {
|
||||
onDeleteVisualNovel?: (item: VisualNovelWorkSummary) => void;
|
||||
getItemState?: (
|
||||
item: CreationWorkShelfItem,
|
||||
) => { isGenerating?: boolean; hasUnreadUpdate?: boolean } | null;
|
||||
) => CreationWorkShelfRuntimeState | null;
|
||||
}) {
|
||||
const {
|
||||
rpgItems,
|
||||
@@ -307,18 +319,24 @@ export function buildCreationWorkShelfItems(params: {
|
||||
.map((item) => {
|
||||
const state = getItemState?.(item);
|
||||
const persistedIsGenerating = isPersistedCreationWorkGenerating(item);
|
||||
return state
|
||||
const isGenerating = Boolean(
|
||||
state?.isGenerating ||
|
||||
(!state?.suppressPersistedGenerating && persistedIsGenerating),
|
||||
);
|
||||
return state || isGenerating
|
||||
? {
|
||||
...item,
|
||||
isGenerating: Boolean(state.isGenerating || persistedIsGenerating),
|
||||
hasUnreadUpdate: state.hasUnreadUpdate,
|
||||
title: state?.titleOverride ?? item.title,
|
||||
summary: state?.summaryOverride ?? item.summary,
|
||||
isGenerating,
|
||||
hasGenerationFailure:
|
||||
state?.hasGenerationFailure ?? item.hasGenerationFailure,
|
||||
generationFailureSummary:
|
||||
state?.generationFailureSummary ??
|
||||
item.generationFailureSummary,
|
||||
hasUnreadUpdate: state?.hasUnreadUpdate,
|
||||
}
|
||||
: persistedIsGenerating
|
||||
? {
|
||||
...item,
|
||||
isGenerating: true,
|
||||
}
|
||||
: item;
|
||||
: item;
|
||||
})
|
||||
.sort(
|
||||
(left, right) =>
|
||||
@@ -327,7 +345,6 @@ export function buildCreationWorkShelfItems(params: {
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
function mergeBarkBattleShelfSourceItems(
|
||||
items: readonly BarkBattleWorkSummary[],
|
||||
): BarkBattleWorkSummary[] {
|
||||
@@ -376,8 +393,8 @@ function mapRpgWorkToShelfItem(
|
||||
: null;
|
||||
const publicWorkCode =
|
||||
item.status === 'published'
|
||||
? (libraryEntry?.publicWorkCode?.trim() ||
|
||||
(item.profileId ? buildCustomWorldPublicWorkCode(item.profileId) : null))
|
||||
? libraryEntry?.publicWorkCode?.trim() ||
|
||||
(item.profileId ? buildCustomWorldPublicWorkCode(item.profileId) : null)
|
||||
: null;
|
||||
const badges: CreationWorkShelfBadge[] = [
|
||||
buildStatusBadge(item.status),
|
||||
@@ -843,7 +860,9 @@ function mapWoodenFishWorkToShelfItem(
|
||||
): CreationWorkShelfItem {
|
||||
const status = item.publicationStatus === 'published' ? 'published' : 'draft';
|
||||
const publicWorkCode =
|
||||
status === 'published' ? buildWoodenFishPublicWorkCode(item.profileId) : null;
|
||||
status === 'published'
|
||||
? buildWoodenFishPublicWorkCode(item.profileId)
|
||||
: null;
|
||||
const title = item.workTitle.trim() || '敲木鱼';
|
||||
const summary =
|
||||
item.workDescription.trim() || (status === 'draft' ? '未填写作品描述' : '');
|
||||
@@ -884,10 +903,7 @@ function mapWoodenFishWorkToShelfItem(
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function resolveAuthorDisplayName(
|
||||
...sources: Array<unknown>
|
||||
) {
|
||||
function resolveAuthorDisplayName(...sources: Array<unknown>) {
|
||||
for (const source of sources) {
|
||||
const authorDisplayName =
|
||||
source &&
|
||||
@@ -961,7 +977,8 @@ export function resolvePuzzleLevelCoverImageSrc(
|
||||
const fallbackCandidateImageSrc = normalizeCoverImageSrc(
|
||||
level.candidates[level.candidates.length - 1]?.imageSrc,
|
||||
);
|
||||
const candidateImageSrc = selectedCandidateImageSrc || fallbackCandidateImageSrc;
|
||||
const candidateImageSrc =
|
||||
selectedCandidateImageSrc || fallbackCandidateImageSrc;
|
||||
|
||||
if (
|
||||
candidateImageSrc &&
|
||||
@@ -984,7 +1001,9 @@ function resolveMatch3DWorkCoverImageSrc(item: Match3DWorkSummary) {
|
||||
|
||||
const topLevelContainerImageSrc =
|
||||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.containerImageSrc) ||
|
||||
normalizeCoverImageSrc(item.generatedBackgroundAsset?.containerImageObjectKey);
|
||||
normalizeCoverImageSrc(
|
||||
item.generatedBackgroundAsset?.containerImageObjectKey,
|
||||
);
|
||||
if (topLevelContainerImageSrc) {
|
||||
return topLevelContainerImageSrc;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -7059,7 +7059,8 @@ export function RpgEntryHomeView({
|
||||
<Settings className="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
) : isAuthenticated && activeTab === 'create' ? (
|
||||
) : isAuthenticated &&
|
||||
(activeTab === 'create' || activeTab === 'saves') ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
@@ -7224,7 +7225,8 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{isAuthenticated && activeTab === 'create' ? (
|
||||
{isAuthenticated &&
|
||||
(activeTab === 'create' || activeTab === 'saves') ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
|
||||
+3
-1
@@ -131,7 +131,9 @@ test('match3d workspace can defer visible chrome to the unified creation page',
|
||||
expect(workspace?.className).not.toContain('overflow-hidden');
|
||||
expect(workspace?.className).not.toContain('platform-remap-surface');
|
||||
expect(screen.queryByRole('heading', { name: '想做个什么玩法?' })).toBeNull();
|
||||
expect(screen.getByLabelText('想做一个什么题材的抓大鹅?')).toBeTruthy();
|
||||
const themeInput = screen.getByLabelText('想做一个什么题材的抓大鹅?');
|
||||
expect(themeInput).toBeTruthy();
|
||||
expect(themeInput.className).not.toContain('h-full');
|
||||
});
|
||||
|
||||
test('match3d workspace omits legacy asset style fields from entry payload', () => {
|
||||
|
||||
@@ -239,7 +239,7 @@ export function Match3DCreationWorkspace({
|
||||
: 'min-h-0 flex-1 grid-rows-[minmax(0,1fr)_auto] lg:grid-rows-1'
|
||||
} ${isBusy ? 'opacity-55' : ''}`}
|
||||
>
|
||||
<label className="block min-h-0">
|
||||
<label className={unifiedChrome ? 'block' : 'block min-h-0'}>
|
||||
<span className="mb-2 block text-sm font-black text-[var(--platform-text-strong)]">
|
||||
想做一个什么题材的抓大鹅?
|
||||
</span>
|
||||
@@ -254,7 +254,11 @@ export function Match3DCreationWorkspace({
|
||||
themeText: event.target.value,
|
||||
}))
|
||||
}
|
||||
className="h-full min-h-[7.75rem] w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)] sm:min-h-[9rem] lg:min-h-[14rem]"
|
||||
className={`w-full resize-none rounded-[1.15rem] border border-[var(--platform-subpanel-border)] bg-white/90 px-4 py-3 text-base leading-6 text-[var(--platform-text-strong)] outline-none transition focus:border-[var(--platform-surface-hover-border)] focus:bg-white focus:ring-2 focus:ring-[var(--platform-warm-border)] ${
|
||||
unifiedChrome
|
||||
? 'min-h-[11rem] sm:min-h-[12rem]'
|
||||
: 'h-full min-h-[7.75rem] sm:min-h-[9rem] lg:min-h-[14rem]'
|
||||
}`}
|
||||
aria-label="想做一个什么题材的抓大鹅?"
|
||||
/>
|
||||
</label>
|
||||
|
||||
+9
@@ -202,6 +202,7 @@ test('puzzle workspace submits the work form instead of agent chat', () => {
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
workDescription: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [],
|
||||
@@ -367,6 +368,7 @@ test('puzzle workspace selects a history image from the upload card', async () =
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '保留历史图里的主体,改成晴天花园。',
|
||||
workDescription: '保留历史图里的主体,改成晴天花园。',
|
||||
pictureDescription: '保留历史图里的主体,改成晴天花园。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [],
|
||||
@@ -425,6 +427,7 @@ test('puzzle workspace falls back to compile action for restored sessions', () =
|
||||
expect(onCreateFromForm).not.toHaveBeenCalled();
|
||||
expect(onExecuteAction).toHaveBeenCalledWith({
|
||||
action: 'compile_puzzle_draft',
|
||||
workDescription: '潮雾中的灯塔与断桥',
|
||||
pictureDescription: '潮雾中的灯塔与断桥',
|
||||
promptText: '潮雾中的灯塔与断桥',
|
||||
referenceImageSrc: null,
|
||||
@@ -528,6 +531,7 @@ test('puzzle workspace restores form draft fields and autosaves edits', () => {
|
||||
|
||||
expect(onAutoSaveForm).toHaveBeenCalledWith({
|
||||
seedText: '旧街灯牌下的猫和发光雨伞。',
|
||||
workDescription: '旧街灯牌下的猫和发光雨伞。',
|
||||
pictureDescription: '旧街灯牌下的猫和发光雨伞。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [],
|
||||
@@ -576,6 +580,7 @@ test('puzzle workspace hides prompt and cost when AI redraw is off', async () =>
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: 'first-level.png',
|
||||
workDescription: 'first-level.png',
|
||||
pictureDescription: 'first-level.png',
|
||||
referenceImageSrc: 'data:image/png;base64,uploaded-square',
|
||||
referenceImageSrcs: [],
|
||||
@@ -632,6 +637,7 @@ test('puzzle workspace submits history image when AI redraw is off', async () =>
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '历史素材 · image.png',
|
||||
workDescription: '历史素材 · image.png',
|
||||
pictureDescription: '历史素材 · image.png',
|
||||
referenceImageSrc: '/generated-puzzle-assets/history/image.png',
|
||||
referenceImageSrcs: [],
|
||||
@@ -674,6 +680,7 @@ test('puzzle workspace submits uploaded reference image as data URL when AI redr
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '保留上传画面的主体和构图,改成雨夜灯街。',
|
||||
workDescription: '保留上传画面的主体和构图,改成雨夜灯街。',
|
||||
pictureDescription: '保留上传画面的主体和构图,改成雨夜灯街。',
|
||||
referenceImageSrc: 'data:image/png;base64,uploaded-square',
|
||||
referenceImageSrcs: [],
|
||||
@@ -764,6 +771,7 @@ test('puzzle workspace uploads prompt references as asset object ids', async ()
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
workDescription: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [
|
||||
@@ -852,6 +860,7 @@ test('puzzle workspace uploads prompt reference images from the description box'
|
||||
|
||||
expect(onCreateFromForm).toHaveBeenCalledWith({
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
workDescription: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
referenceImageSrcs: [
|
||||
|
||||
@@ -321,6 +321,7 @@ export function PuzzleCreationWorkspace({
|
||||
const autosavePayload = useMemo(
|
||||
() => ({
|
||||
seedText: pictureDescription,
|
||||
workDescription: pictureDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: mainReferenceImageSrcForPayload,
|
||||
referenceImageSrcs: promptReferenceImageSrcsForPayload,
|
||||
@@ -551,6 +552,7 @@ export function PuzzleCreationWorkspace({
|
||||
: pictureDescription || formState.referenceImageLabel || '上传拼图图片';
|
||||
const payload = {
|
||||
seedText: payloadPictureDescription,
|
||||
workDescription: payloadPictureDescription,
|
||||
pictureDescription: payloadPictureDescription,
|
||||
referenceImageSrc: mainReferenceImageSrcForPayload,
|
||||
referenceImageSrcs: promptReferenceImageSrcsForPayload,
|
||||
@@ -571,6 +573,7 @@ export function PuzzleCreationWorkspace({
|
||||
onExecuteAction({
|
||||
action: 'compile_puzzle_draft',
|
||||
promptText: payloadPictureDescription,
|
||||
workDescription: payloadPictureDescription,
|
||||
pictureDescription: payloadPictureDescription,
|
||||
referenceImageSrc: mainReferenceImageSrcForPayload,
|
||||
referenceImageSrcs: promptReferenceImageSrcsForPayload,
|
||||
|
||||
@@ -2119,6 +2119,31 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.creation-work-card__failure-status {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 0.26rem;
|
||||
overflow: hidden;
|
||||
border: 1px solid color-mix(in srgb, #c7653d 48%, transparent);
|
||||
border-radius: 9999px;
|
||||
background: color-mix(in srgb, #c7653d 12%, transparent);
|
||||
padding: 0.22rem 0.52rem;
|
||||
color: color-mix(in srgb, #c7653d 86%, var(--platform-text-strong));
|
||||
font-size: 0.68rem;
|
||||
font-weight: 800;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.creation-work-card__failure-status span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.creation-work-card__published-info {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
|
||||
@@ -36,9 +36,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
expect(progress?.steps[0]?.detail).toBe(
|
||||
'建立可恢复草稿,整理首关描述与关卡结构,约 8 秒。',
|
||||
);
|
||||
expect(progress?.steps[2]?.detail).toBe(
|
||||
'生成 1:1 拼图首图,预计 4 分钟。',
|
||||
);
|
||||
expect(progress?.steps[2]?.detail).toBe('生成 1:1 拼图首图,预计 4 分钟。');
|
||||
expect(progress?.estimatedRemainingMs).toBe(446_500);
|
||||
expect(progress?.overallProgress).toBe(0);
|
||||
expect(progress?.steps[0]?.completed).toBeGreaterThan(0);
|
||||
@@ -72,7 +70,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
error: null,
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 7000);
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 130_000);
|
||||
|
||||
expect(progress?.overallProgress).toBeGreaterThan(0);
|
||||
expect(progress?.overallProgress).toBeLessThan(88);
|
||||
@@ -105,8 +103,8 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
expect(longRunningProgress?.phaseId).toBe('compile');
|
||||
expect(longRunningProgress?.steps[0]?.status).toBe('active');
|
||||
expect(longRunningProgress?.steps[1]?.status).toBe('pending');
|
||||
expect(longRunningProgress?.overallProgress).toBeLessThanOrEqual(98);
|
||||
expect(longRunningProgress?.overallProgress).toBeGreaterThan(40);
|
||||
expect(longRunningProgress?.overallProgress).toBeLessThan(5);
|
||||
expect(longRunningProgress?.overallProgress).toBeGreaterThan(0);
|
||||
expect(realProgress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(realProgress?.steps[1]?.status).toBe('completed');
|
||||
expect(realProgress?.steps[2]?.status).toBe('active');
|
||||
@@ -145,7 +143,10 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
};
|
||||
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 20_000);
|
||||
const writeBackProgress = buildMiniGameDraftGenerationProgress(state, 206_000);
|
||||
const writeBackProgress = buildMiniGameDraftGenerationProgress(
|
||||
state,
|
||||
206_000,
|
||||
);
|
||||
|
||||
expect(progress?.steps.map((step) => step.label)).toEqual([
|
||||
'编译首关草稿',
|
||||
@@ -175,13 +176,14 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
|
||||
expect(progress?.phaseId).toBe('compile');
|
||||
expect(progress?.overallProgress).toBeLessThan(88);
|
||||
expect(progress?.overallProgress).toBeGreaterThan(80);
|
||||
expect(progress?.overallProgress).toBeLessThan(5);
|
||||
expect(progress?.overallProgress).toBeGreaterThan(0);
|
||||
expect(progress?.estimatedRemainingMs).toBe(0);
|
||||
expect(progress?.steps[0]?.status).toBe('active');
|
||||
expect(progress?.steps[0]?.completed).toBe(0.98);
|
||||
expect(progress?.steps.slice(1).every((step) => step.status === 'pending')).toBe(
|
||||
true,
|
||||
);
|
||||
expect(
|
||||
progress?.steps.slice(1).every((step) => step.status === 'pending'),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test('puzzle draft generation advances steps from backend progress percent only', () => {
|
||||
@@ -221,9 +223,11 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
);
|
||||
|
||||
expect(imageProgress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(imageProgress?.overallProgress).toBeLessThan(88);
|
||||
expect(imageProgress?.steps[2]?.status).toBe('active');
|
||||
expect(imageProgress?.steps[3]?.status).toBe('pending');
|
||||
expect(uiProgress?.phaseId).toBe('puzzle-ui-assets');
|
||||
expect(uiProgress?.overallProgress).toBeLessThan(94);
|
||||
expect(uiProgress?.steps[4]?.status).toBe('active');
|
||||
expect(uiProgress?.steps[5]?.status).toBe('pending');
|
||||
expect(writeProgress?.phaseId).toBe('puzzle-select-image');
|
||||
@@ -249,6 +253,7 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
const progress = buildMiniGameDraftGenerationProgress(state, 121_000);
|
||||
|
||||
expect(progress?.phaseId).toBe('puzzle-cover-image');
|
||||
expect(progress?.overallProgress).toBeLessThan(88);
|
||||
expect(progress?.steps[2]?.status).toBe('active');
|
||||
expect(progress?.steps[2]?.completed).toBeLessThan(0.02);
|
||||
});
|
||||
@@ -619,55 +624,58 @@ describe('miniGameDraftGenerationProgress', () => {
|
||||
});
|
||||
|
||||
test('puzzle generation anchors expose form payload as the display source', () => {
|
||||
const entries = buildPuzzleGenerationAnchorEntries({
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 1,
|
||||
progressPercent: 0,
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'themePromise',
|
||||
label: '题材承诺',
|
||||
value: '雨夜猫街',
|
||||
status: 'locked',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visualSubject',
|
||||
label: '画面主体',
|
||||
value: '一只猫在雨夜灯牌下回头。',
|
||||
status: 'locked',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visualMood',
|
||||
label: '视觉气质',
|
||||
value: '清晰、适合拼图切块',
|
||||
status: 'inferred',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'compositionHooks',
|
||||
label: '拼图记忆点',
|
||||
value: '主体轮廓、色块分区、局部细节',
|
||||
status: 'inferred',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tagsAndForbidden',
|
||||
label: '标签与禁忌',
|
||||
value: '猫咪、雨夜、拼图;禁止标题字',
|
||||
status: 'inferred',
|
||||
const entries = buildPuzzleGenerationAnchorEntries(
|
||||
{
|
||||
sessionId: 'puzzle-session-1',
|
||||
currentTurn: 1,
|
||||
progressPercent: 0,
|
||||
stage: 'collecting_anchors',
|
||||
anchorPack: {
|
||||
themePromise: {
|
||||
key: 'themePromise',
|
||||
label: '题材承诺',
|
||||
value: '雨夜猫街',
|
||||
status: 'locked',
|
||||
},
|
||||
visualSubject: {
|
||||
key: 'visualSubject',
|
||||
label: '画面主体',
|
||||
value: '一只猫在雨夜灯牌下回头。',
|
||||
status: 'locked',
|
||||
},
|
||||
visualMood: {
|
||||
key: 'visualMood',
|
||||
label: '视觉气质',
|
||||
value: '清晰、适合拼图切块',
|
||||
status: 'inferred',
|
||||
},
|
||||
compositionHooks: {
|
||||
key: 'compositionHooks',
|
||||
label: '拼图记忆点',
|
||||
value: '主体轮廓、色块分区、局部细节',
|
||||
status: 'inferred',
|
||||
},
|
||||
tagsAndForbidden: {
|
||||
key: 'tagsAndForbidden',
|
||||
label: '标签与禁忌',
|
||||
value: '猫咪、雨夜、拼图;禁止标题字',
|
||||
status: 'inferred',
|
||||
},
|
||||
},
|
||||
draft: null,
|
||||
messages: [],
|
||||
lastAssistantReply: null,
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
},
|
||||
draft: null,
|
||||
messages: [],
|
||||
lastAssistantReply: null,
|
||||
publishedProfileId: null,
|
||||
suggestedActions: [],
|
||||
resultPreview: null,
|
||||
updatedAt: '2026-04-29T00:00:00.000Z',
|
||||
}, {
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
});
|
||||
{
|
||||
seedText: '一只猫在雨夜灯牌下回头。',
|
||||
pictureDescription: '一只猫在雨夜灯牌下回头。',
|
||||
referenceImageSrc: null,
|
||||
},
|
||||
);
|
||||
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
|
||||
@@ -216,7 +216,10 @@ function resolvePuzzleBackendProgressPercent(
|
||||
state: MiniGameDraftGenerationState,
|
||||
) {
|
||||
const progressPercent = state.metadata?.puzzleProgressPercent;
|
||||
if (typeof progressPercent !== 'number' || !Number.isFinite(progressPercent)) {
|
||||
if (
|
||||
typeof progressPercent !== 'number' ||
|
||||
!Number.isFinite(progressPercent)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -571,13 +574,13 @@ export function createMiniGameDraftGenerationState(
|
||||
? 'square-hole-draft'
|
||||
: kind === 'match3d'
|
||||
? 'match3d-work-title'
|
||||
: kind === 'baby-object-match'
|
||||
? 'baby-object-draft'
|
||||
: kind === 'jump-hop'
|
||||
? 'jump-hop-draft'
|
||||
: kind === 'wooden-fish'
|
||||
? 'wooden-fish-draft'
|
||||
: 'compile',
|
||||
: kind === 'baby-object-match'
|
||||
? 'baby-object-draft'
|
||||
: kind === 'jump-hop'
|
||||
? 'jump-hop-draft'
|
||||
: kind === 'wooden-fish'
|
||||
? 'wooden-fish-draft'
|
||||
: 'compile',
|
||||
startedAtMs,
|
||||
completedAssetCount: 0,
|
||||
totalAssetCount: 0,
|
||||
@@ -784,10 +787,7 @@ function resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
// 中文注释:未收到后端真实里程碑时,跨步骤必须卡住;
|
||||
// 但当前步骤内的假进度要按整段等待时间继续向前走,避免短步骤几秒后停死。
|
||||
const fallbackDurationMs = Math.max(1, resolvePuzzleEstimatedWaitMs(state));
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / fallbackDurationMs),
|
||||
);
|
||||
return Math.max(0, Math.min(0.98, elapsedMs / fallbackDurationMs));
|
||||
}
|
||||
|
||||
function resolveElapsedActiveStepProgressRatio(
|
||||
@@ -809,53 +809,7 @@ function resolveElapsedActiveStepProgressRatio(
|
||||
? WOODEN_FISH_ESTIMATED_WAIT_MS
|
||||
: 1;
|
||||
|
||||
return Math.max(
|
||||
0,
|
||||
Math.min(0.98, elapsedMs / Math.max(1, estimatedWaitMs)),
|
||||
);
|
||||
}
|
||||
|
||||
function resolvePuzzleOverallProgress(
|
||||
state: MiniGameDraftGenerationState,
|
||||
activeStepProgressRatio: number,
|
||||
) {
|
||||
const backendProgressPercent = resolvePuzzleBackendProgressPercent(state);
|
||||
// 中文注释:88 以下的后端进度只保留为会话事实,不参与首帧总进度抬升。
|
||||
// 生成页恢复时必须先从 0% 起步,再由当前步骤内的假进度平滑推进。
|
||||
const backendProgressFloor =
|
||||
backendProgressPercent != null &&
|
||||
backendProgressPercent >= PUZZLE_COMPILE_MILESTONE_PROGRESS
|
||||
? backendProgressPercent
|
||||
: 0;
|
||||
const range =
|
||||
state.phase === 'puzzle-select-image'
|
||||
? {
|
||||
start: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-ui-assets'
|
||||
? {
|
||||
start: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_UI_MILESTONE_PROGRESS,
|
||||
}
|
||||
: state.phase === 'puzzle-cover-image' ||
|
||||
state.phase === 'puzzle-level-scene'
|
||||
? {
|
||||
start: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
end: PUZZLE_IMAGE_MILESTONE_PROGRESS,
|
||||
}
|
||||
: {
|
||||
start: 0,
|
||||
end: PUZZLE_COMPILE_MILESTONE_PROGRESS,
|
||||
};
|
||||
const fakeProgress =
|
||||
range.start + (range.end - range.start) * activeStepProgressRatio;
|
||||
const nextProgress = Math.min(
|
||||
PUZZLE_NON_READY_MAX_PROGRESS,
|
||||
Math.max(range.start, backendProgressFloor, fakeProgress),
|
||||
);
|
||||
|
||||
return nextProgress;
|
||||
return Math.max(0, Math.min(0.98, elapsedMs / Math.max(1, estimatedWaitMs)));
|
||||
}
|
||||
|
||||
export function buildMiniGameDraftGenerationProgress(
|
||||
@@ -867,7 +821,8 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
}
|
||||
|
||||
const effectiveNowMs =
|
||||
typeof state.finishedAtMs === 'number' && Number.isFinite(state.finishedAtMs)
|
||||
typeof state.finishedAtMs === 'number' &&
|
||||
Number.isFinite(state.finishedAtMs)
|
||||
? state.finishedAtMs
|
||||
: nowMs;
|
||||
const elapsedMs = Math.max(0, effectiveNowMs - state.startedAtMs);
|
||||
@@ -894,42 +849,42 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
...state,
|
||||
phase: woodenFishTimeline.phase,
|
||||
}
|
||||
: state.kind === 'big-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'square-hole' &&
|
||||
: state.kind === 'big-fish' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
phase: resolveBigFishPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'match3d' &&
|
||||
: state.kind === 'square-hole' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||||
phase: resolveSquareHolePhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state.kind === 'baby-object-match' &&
|
||||
: state.kind === 'match3d' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveBabyObjectMatchPhaseByElapsedMs(elapsedMs),
|
||||
phase: resolveMatch3DPhaseByElapsedMs(elapsedMs, state.phase),
|
||||
}
|
||||
: state.kind === 'jump-hop' &&
|
||||
: state.kind === 'baby-object-match' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveJumpHopPhaseByElapsedMs(elapsedMs),
|
||||
phase: resolveBabyObjectMatchPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
: state.kind === 'jump-hop' &&
|
||||
state.phase !== 'failed' &&
|
||||
state.phase !== 'ready'
|
||||
? {
|
||||
...state,
|
||||
phase: resolveJumpHopPhaseByElapsedMs(elapsedMs),
|
||||
}
|
||||
: state;
|
||||
|
||||
const puzzleTimedSteps =
|
||||
normalizedState.kind === 'puzzle'
|
||||
@@ -944,22 +899,23 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
const activeStepIndex = getActiveStepIndex(steps, normalizedState.phase);
|
||||
const activeStep = steps[activeStepIndex] ?? steps[0];
|
||||
const activeStepProgressRatio =
|
||||
normalizedState.kind === 'puzzle'
|
||||
normalizedState.phase === 'failed'
|
||||
? 0
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? normalizedState.phase === 'ready'
|
||||
? 1
|
||||
: normalizedState.phase === 'failed'
|
||||
? 0
|
||||
: resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
normalizedState,
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
effectiveNowMs,
|
||||
)
|
||||
: resolvePuzzleActiveStepElapsedProgressRatio(
|
||||
normalizedState,
|
||||
puzzleTimedSteps ?? buildPuzzleTimedSteps(normalizedState),
|
||||
activeStepIndex,
|
||||
elapsedMs,
|
||||
effectiveNowMs,
|
||||
)
|
||||
: normalizedState.totalAssetCount > 0
|
||||
? Math.min(
|
||||
1,
|
||||
normalizedState.completedAssetCount / normalizedState.totalAssetCount,
|
||||
normalizedState.completedAssetCount /
|
||||
normalizedState.totalAssetCount,
|
||||
)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 1
|
||||
@@ -968,16 +924,16 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
: normalizedState.kind === 'square-hole'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'match3d'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? resolveElapsedActiveStepProgressRatio(
|
||||
normalizedState.kind,
|
||||
@@ -1002,20 +958,11 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? Math.max(1, completedWeight)
|
||||
: normalizedState.phase === 'ready'
|
||||
? 100
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? resolvePuzzleOverallProgress(
|
||||
normalizedState,
|
||||
activeStepProgressRatio,
|
||||
)
|
||||
: completedWeight + activeStep.weight * activeStepProgressRatio;
|
||||
: completedWeight + activeStep.weight * activeStepProgressRatio;
|
||||
const cappedOverallProgress =
|
||||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||||
? overallProgress
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress)
|
||||
: overallProgress;
|
||||
: Math.min(PUZZLE_NON_READY_MAX_PROGRESS, overallProgress);
|
||||
|
||||
return {
|
||||
phaseId: normalizedState.phase,
|
||||
@@ -1036,9 +983,9 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
? '宝贝识物草稿已准备完成,可进入结果页继续发布。'
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? '跳一跳草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? '敲木鱼草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? '敲木鱼草稿已准备完成,可进入结果页试玩或发布。'
|
||||
: '首关草稿与正式图已准备完成,可进入结果页补作品信息。'
|
||||
: activeStep.detail),
|
||||
batchLabel: activeStep.label,
|
||||
overallProgress: clampProgress(cappedOverallProgress),
|
||||
@@ -1046,10 +993,13 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
totalWeight: 100,
|
||||
elapsedMs,
|
||||
estimatedRemainingMs:
|
||||
normalizedState.phase === 'ready'
|
||||
normalizedState.phase === 'ready' || normalizedState.phase === 'failed'
|
||||
? 0
|
||||
: normalizedState.kind === 'puzzle'
|
||||
? Math.max(0, resolvePuzzleEstimatedWaitMs(normalizedState) - elapsedMs)
|
||||
? Math.max(
|
||||
0,
|
||||
resolvePuzzleEstimatedWaitMs(normalizedState) - elapsedMs,
|
||||
)
|
||||
: normalizedState.kind === 'big-fish'
|
||||
? Math.max(0, 7_000 - elapsedMs)
|
||||
: normalizedState.kind === 'square-hole'
|
||||
@@ -1057,15 +1007,12 @@ export function buildMiniGameDraftGenerationProgress(
|
||||
: normalizedState.kind === 'match3d'
|
||||
? Math.max(0, MATCH3D_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'baby-object-match'
|
||||
? Math.max(
|
||||
0,
|
||||
BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS - elapsedMs,
|
||||
)
|
||||
? Math.max(0, BABY_OBJECT_MATCH_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'jump-hop'
|
||||
? Math.max(0, JUMP_HOP_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? Math.max(0, WOODEN_FISH_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: null,
|
||||
: normalizedState.kind === 'wooden-fish'
|
||||
? Math.max(0, WOODEN_FISH_ESTIMATED_WAIT_MS - elapsedMs)
|
||||
: null,
|
||||
activeStepIndex,
|
||||
steps: buildMiniGameProgressSteps(
|
||||
steps,
|
||||
|
||||
Reference in New Issue
Block a user