refactor: 深化前端入口作品流与作品架模块

This commit is contained in:
2026-06-03 15:34:52 +08:00
parent 1eeb14c50f
commit cf0840d9e9
8 changed files with 434 additions and 157 deletions

View File

@@ -7,10 +7,10 @@ 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';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import type { CustomWorldProfile } from '../../types';
import type {
@@ -20,10 +20,10 @@ import type {
import { isPlatformCreationTypeVisible } from '../platform-entry/platformEntryCreationTypes';
import {
buildCreationWorkShelfItems,
getCreationWorkShelfItemTime,
type CreationWorkShelfItem,
type CreationWorkShelfMetricId,
type CreationWorkShelfRuntimeState,
getCreationWorkShelfItemTime,
} from './creationWorkShelf';
import {
CustomWorldCreationStartCard,
@@ -274,6 +274,7 @@ export function CustomWorldCreationHub({
barkBattleItems,
items,
match3dItems,
squareHoleItems,
onDeleteBigFish,
onDeleteMatch3D,
onDeleteSquareHole,
@@ -341,44 +342,8 @@ export function CustomWorldCreationHub({
function handleOpenShelfItem(item: CreationWorkShelfItem) {
onOpenShelfItem?.(item);
switch (item.source.kind) {
case 'puzzle':
onOpenPuzzleDetail?.(item.source.item);
return;
case 'baby-object-match':
onOpenBabyObjectMatchDetail?.(item.source.item);
return;
case 'visual-novel':
onOpenVisualNovelDetail?.(item.source.item);
return;
case 'bark-battle':
onOpenBarkBattleDetail?.(item.source.item);
return;
case 'big-fish':
onOpenBigFishDetail?.(item.source.item);
return;
case 'match3d':
onOpenMatch3DDetail?.(item.source.item);
return;
case 'square-hole':
onOpenSquareHoleDetail?.(item.source.item);
return;
case '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);
return;
}
if (item.source.item.profileId) {
onEnterPublished(item.source.item.profileId);
}
}
// 中文注释:玩法差异由 Work Shelf Adapter 承载Hub 只负责响应卡片点击。
item.actions.open();
}
function buildDeleteAction(item: CreationWorkShelfItem) {

View File

@@ -108,6 +108,7 @@ import type {
VisualNovelWorkDetail,
VisualNovelWorkSummary,
} from '../../../packages/shared/src/contracts/visualNovel';
import type { WoodenFishWorkSummaryResponse } from '../../../packages/shared/src/contracts/woodenFish';
import { buildCustomWorldPlayableCharacters } from '../../data/characterPresets';
import {
buildPublicWorkStagePath,
@@ -216,17 +217,12 @@ import {
buildSquareHoleGenerationAnchorEntries,
buildWoodenFishGenerationAnchorEntries,
createMiniGameDraftGenerationState,
resolveMiniGameDraftGenerationStartedAtMs,
type MiniGameDraftGenerationKind,
type MiniGameDraftGenerationPhase,
type MiniGameDraftGenerationState,
resolveMiniGameDraftGenerationStartedAtMs,
} from '../../services/miniGameDraftGenerationProgress';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import { UnifiedCreationPage } from '../unified-creation/UnifiedCreationPage';
import {
getUnifiedCreationSpec,
type UnifiedCreationPlayId,
} from '../unified-creation/unifiedCreationSpecs';
import {
buildBabyObjectMatchPublicWorkCode,
buildBarkBattlePublicWorkCode,
@@ -350,7 +346,6 @@ 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';
@@ -390,6 +385,11 @@ import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreation
import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld';
import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultAutosave';
import { useRpgCreationSessionController } from '../rpg-entry/useRpgCreationSessionController';
import { UnifiedCreationPage } from '../unified-creation/UnifiedCreationPage';
import {
getUnifiedCreationSpec,
type UnifiedCreationPlayId,
} from '../unified-creation/unifiedCreationSpecs';
import {
buildVisualNovelEntryGenerationAnchorEntries,
buildVisualNovelEntryGenerationProgress,
@@ -438,11 +438,18 @@ import {
PlatformErrorDialog,
type PlatformErrorDialogPayload,
} from './PlatformErrorDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import {
getPlatformPublicGalleryEntryKey,
getPlatformRecommendRuntimeKind,
isSamePlatformPublicGalleryEntry,
mergePlatformPublicGalleryEntries,
type RecommendRuntimeKind,
} from './platformPublicGalleryFlow';
import {
PlatformTaskCompletionDialog,
type PlatformTaskCompletionDialogPayload,
} from './PlatformTaskCompletionDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
@@ -511,17 +518,6 @@ type BarkBattleRuntimeReturnStage =
| 'work-detail'
| 'platform';
type BigFishRuntimeSessionSource = 'draft' | 'work' | null;
type RecommendRuntimeKind =
| 'bark-battle'
| 'big-fish'
| 'edutainment'
| 'jump-hop'
| 'match3d'
| 'puzzle'
| 'square-hole'
| 'wooden-fish'
| 'visual-novel'
| 'rpg';
type SquareHoleRuntimeReturnStage =
| 'square-hole-result'
| 'work-detail'
@@ -625,77 +621,6 @@ const PUZZLE_DRAFT_GENERATION_POINT_COST = 2;
const MATCH3D_DRAFT_GENERATION_POINT_COST = 10;
const BARK_BATTLE_DRAFT_GENERATION_POINT_COST = 3;
function getPlatformPublicGalleryEntryTime(entry: PlatformPublicGalleryCard) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
const timestamp = new Date(rawTime).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
const kind = isBigFishGalleryEntry(entry)
? 'big-fish'
: isPuzzleGalleryEntry(entry)
? 'puzzle'
: isJumpHopGalleryEntry(entry)
? 'jump-hop'
: isWoodenFishGalleryEntry(entry)
? 'wooden-fish'
: isMatch3DGalleryEntry(entry)
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: isBarkBattleGalleryEntry(entry)
? 'bark-battle'
: isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle';
}
if (isJumpHopGalleryEntry(entry)) {
return 'jump-hop';
}
if (isWoodenFishGalleryEntry(entry)) {
return 'wooden-fish';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
if (isSquareHoleGalleryEntry(entry)) {
return 'square-hole';
}
if (isVisualNovelGalleryEntry(entry)) {
return 'visual-novel';
}
if (isBarkBattleGalleryEntry(entry)) {
return 'bark-battle';
}
if (isEdutainmentGalleryEntry(entry)) {
return 'edutainment';
}
return 'rpg';
}
function isRecommendRuntimeReadyForEntry(
entry: PlatformPublicGalleryCard,
state: RecommendRuntimeState,
@@ -739,33 +664,6 @@ function isRecommendRuntimeReadyForEntry(
return true;
}
function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard,
) {
return (
getPlatformPublicGalleryEntryKey(left) ===
getPlatformPublicGalleryEntryKey(right)
);
}
function mergePlatformPublicGalleryEntries(
rpgEntries: CustomWorldGalleryCard[],
puzzleEntries: PlatformPublicGalleryCard[],
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...rpgEntries, ...puzzleEntries].forEach((entry) => {
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
});
return Array.from(entryMap.values()).sort(
(left, right) =>
getPlatformPublicGalleryEntryTime(right) -
getPlatformPublicGalleryEntryTime(left),
);
}
function mapRpgGalleryCardToPublicWorkDetail(
entry: CustomWorldGalleryCard,
): PlatformPublicGalleryCard {

View File

@@ -0,0 +1,204 @@
import { expect, test } from 'vitest';
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
import {
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
import {
getPlatformPublicGalleryEntryKey,
getPlatformPublicGalleryEntryTime,
getPlatformRecommendRuntimeKind,
isSamePlatformPublicGalleryEntry,
mergePlatformPublicGalleryEntries,
type RecommendRuntimeKind,
} from './platformPublicGalleryFlow';
type TypedPlatformPublicGalleryCard = Extract<
PlatformPublicGalleryCard,
{ sourceType: string }
>;
type PlatformGallerySourceType = TypedPlatformPublicGalleryCard['sourceType'];
type TypedPlatformPublicGalleryCardOverrides = Partial<
Omit<TypedPlatformPublicGalleryCard, 'sourceType'>
>;
function buildRpgEntry(
overrides: Partial<CustomWorldGalleryCard> = {},
): CustomWorldGalleryCard {
return {
ownerUserId: 'user-1',
profileId: 'rpg-profile',
publicWorkCode: 'CW-RPG',
authorPublicUserCode: null,
visibility: 'published',
publishedAt: '2026-06-01T00:00:00.000Z',
updatedAt: '2026-06-01T01:00:00.000Z',
authorDisplayName: '玩家',
worldName: 'RPG 世界',
subtitle: '公开作品',
summaryText: '公开作品摘要',
coverImageSrc: null,
themeMode: 'martial',
playableNpcCount: 1,
landmarkCount: 1,
...overrides,
};
}
function buildTypedEntry(
sourceType: PlatformGallerySourceType,
overrides: TypedPlatformPublicGalleryCardOverrides = {},
): PlatformPublicGalleryCard {
const common = {
workId: `${sourceType}-work`,
profileId: `${sourceType}-profile`,
publicWorkCode: `${sourceType}-code`,
ownerUserId: 'user-1',
authorDisplayName: '玩家',
worldName: `${sourceType} 作品`,
subtitle: '公开作品',
summaryText: '公开作品摘要',
coverImageSrc: null,
themeTags: [sourceType],
visibility: 'published' as const,
publishedAt: '2026-06-01T00:00:00.000Z',
updatedAt: '2026-06-01T01:00:00.000Z',
};
switch (sourceType) {
case 'puzzle':
return { ...common, ...overrides, sourceType };
case 'big-fish':
return { ...common, ...overrides, sourceType };
case 'match3d':
return { ...common, ...overrides, sourceType };
case 'square-hole':
return { ...common, ...overrides, sourceType };
case 'visual-novel':
return { ...common, ...overrides, sourceType };
case 'jump-hop':
return { ...common, ...overrides, sourceType };
case 'wooden-fish':
return { ...common, ...overrides, sourceType };
case 'edutainment':
return {
...common,
...overrides,
sourceType,
templateId: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID,
templateName: EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_NAME,
};
case 'bark-battle':
return {
...common,
...overrides,
sourceType,
authorPublicUserCode: null,
coverRenderMode: 'image',
coverCharacterImageSrcs: [],
themeMode: 'martial',
playableNpcCount: 1,
landmarkCount: 1,
};
default: {
const exhaustive: never = sourceType;
return exhaustive;
}
}
}
test('platform public gallery flow resolves stable key and runtime kind for every play kind', () => {
const cases: Array<
[sourceType: PlatformGallerySourceType, keyKind: string, kind: RecommendRuntimeKind]
> = [
['big-fish', 'big-fish', 'big-fish'],
['puzzle', 'puzzle', 'puzzle'],
['jump-hop', 'jump-hop', 'jump-hop'],
['wooden-fish', 'wooden-fish', 'wooden-fish'],
['match3d', 'match3d', 'match3d'],
['square-hole', 'square-hole', 'square-hole'],
['visual-novel', 'visual-novel', 'visual-novel'],
['bark-battle', 'bark-battle', 'bark-battle'],
[
'edutainment',
`edutainment:${EDUTAINMENT_BABY_OBJECT_MATCH_TEMPLATE_ID}`,
'edutainment',
],
];
cases.forEach(([sourceType, keyKind, kind]) => {
const entry = buildTypedEntry(sourceType);
expect(getPlatformPublicGalleryEntryKey(entry)).toBe(
`${keyKind}:user-1:${sourceType}-profile`,
);
expect(getPlatformRecommendRuntimeKind(entry)).toBe(kind);
});
const rpgEntry = buildRpgEntry();
expect(getPlatformPublicGalleryEntryKey(rpgEntry)).toBe(
'rpg:user-1:rpg-profile',
);
expect(getPlatformRecommendRuntimeKind(rpgEntry)).toBe('rpg');
});
test('platform public gallery flow compares entries by resolved identity', () => {
const left = buildTypedEntry('puzzle');
const sameIdentity = buildTypedEntry('puzzle', {
workId: 'other-work',
worldName: '新标题',
});
const otherKind = buildTypedEntry('match3d', {
ownerUserId: left.ownerUserId,
profileId: left.profileId,
});
expect(isSamePlatformPublicGalleryEntry(left, sameIdentity)).toBe(true);
expect(isSamePlatformPublicGalleryEntry(left, otherKind)).toBe(false);
});
test('platform public gallery flow merges duplicate identities and sorts newest first', () => {
const staleRpgEntry = buildRpgEntry({
profileId: 'shared-rpg',
worldName: '旧版 RPG',
publishedAt: '2026-06-01T00:00:00.000Z',
});
const freshRpgEntry = buildRpgEntry({
profileId: 'shared-rpg',
worldName: '新版 RPG',
publishedAt: '2026-06-04T00:00:00.000Z',
});
const middleRpgEntry = buildRpgEntry({
profileId: 'middle-rpg',
worldName: '中间 RPG',
publishedAt: '2026-06-02T00:00:00.000Z',
});
const updatedOnlyEntry = buildTypedEntry('big-fish', {
profileId: 'updated-only',
publishedAt: null,
updatedAt: '2026-06-03T00:00:00.000Z',
});
const invalidTimeEntry = buildTypedEntry('puzzle', {
profileId: 'invalid-time',
publishedAt: 'not-a-date',
updatedAt: 'still-not-a-date',
});
const merged = mergePlatformPublicGalleryEntries(
[staleRpgEntry, middleRpgEntry],
[invalidTimeEntry, updatedOnlyEntry, freshRpgEntry],
);
expect(merged).toHaveLength(4);
expect(merged.map((entry) => entry.profileId)).toEqual([
'shared-rpg',
'updated-only',
'middle-rpg',
'invalid-time',
]);
expect(merged[0]?.worldName).toBe('新版 RPG');
expect(getPlatformPublicGalleryEntryTime(invalidTimeEntry)).toBe(0);
});

View File

@@ -0,0 +1,128 @@
import type { CustomWorldGalleryCard } from '../../../packages/shared/src/contracts/runtime';
import {
isBarkBattleGalleryEntry,
isBigFishGalleryEntry,
isEdutainmentGalleryEntry,
isJumpHopGalleryEntry,
isMatch3DGalleryEntry,
isPuzzleGalleryEntry,
isSquareHoleGalleryEntry,
isVisualNovelGalleryEntry,
isWoodenFishGalleryEntry,
type PlatformPublicGalleryCard,
} from '../rpg-entry/rpgEntryWorldPresentation';
export type RecommendRuntimeKind =
| 'bark-battle'
| 'big-fish'
| 'edutainment'
| 'jump-hop'
| 'match3d'
| 'puzzle'
| 'square-hole'
| 'wooden-fish'
| 'visual-novel'
| 'rpg';
export function getPlatformPublicGalleryEntryTime(
entry: PlatformPublicGalleryCard,
) {
const rawTime = entry.publishedAt ?? entry.updatedAt;
const timestamp = new Date(rawTime).getTime();
return Number.isNaN(timestamp) ? 0 : timestamp;
}
export function getPlatformPublicGalleryEntryKey(
entry: PlatformPublicGalleryCard,
) {
// 同一作品身份由玩法、作者与 profile 共同确定,避免不同玩法共享 profileId 时误合并。
const kind = isBigFishGalleryEntry(entry)
? 'big-fish'
: isPuzzleGalleryEntry(entry)
? 'puzzle'
: isJumpHopGalleryEntry(entry)
? 'jump-hop'
: isWoodenFishGalleryEntry(entry)
? 'wooden-fish'
: isMatch3DGalleryEntry(entry)
? 'match3d'
: isSquareHoleGalleryEntry(entry)
? 'square-hole'
: isVisualNovelGalleryEntry(entry)
? 'visual-novel'
: isBarkBattleGalleryEntry(entry)
? 'bark-battle'
: isEdutainmentGalleryEntry(entry)
? `edutainment:${entry.templateId}`
: 'rpg';
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
}
export function getPlatformRecommendRuntimeKind(
entry: PlatformPublicGalleryCard,
): RecommendRuntimeKind {
if (isBigFishGalleryEntry(entry)) {
return 'big-fish';
}
if (isPuzzleGalleryEntry(entry)) {
return 'puzzle';
}
if (isJumpHopGalleryEntry(entry)) {
return 'jump-hop';
}
if (isWoodenFishGalleryEntry(entry)) {
return 'wooden-fish';
}
if (isMatch3DGalleryEntry(entry)) {
return 'match3d';
}
if (isSquareHoleGalleryEntry(entry)) {
return 'square-hole';
}
if (isVisualNovelGalleryEntry(entry)) {
return 'visual-novel';
}
if (isBarkBattleGalleryEntry(entry)) {
return 'bark-battle';
}
if (isEdutainmentGalleryEntry(entry)) {
return 'edutainment';
}
return 'rpg';
}
export function isSamePlatformPublicGalleryEntry(
left: PlatformPublicGalleryCard,
right: PlatformPublicGalleryCard,
) {
return (
getPlatformPublicGalleryEntryKey(left) ===
getPlatformPublicGalleryEntryKey(right)
);
}
export function mergePlatformPublicGalleryEntries(
rpgEntries: CustomWorldGalleryCard[],
puzzleEntries: PlatformPublicGalleryCard[],
) {
const entryMap = new Map<string, PlatformPublicGalleryCard>();
[...rpgEntries, ...puzzleEntries].forEach((entry) => {
entryMap.set(getPlatformPublicGalleryEntryKey(entry), entry);
});
return Array.from(entryMap.values()).sort(
(left, right) =>
getPlatformPublicGalleryEntryTime(right) -
getPlatformPublicGalleryEntryTime(left),
);
}