refactor: 收口公开作品详情策略

This commit is contained in:
2026-06-03 22:22:13 +08:00
parent caac418e0e
commit 00820e6571
6 changed files with 506 additions and 53 deletions

View File

@@ -515,6 +515,10 @@ import {
mergePlatformPublicGalleryEntries,
type RecommendRuntimeKind,
} from './platformPublicGalleryFlow';
import {
resolvePlatformPublicWorkActionMode,
resolvePlatformPublicWorkDetailOpenStrategy,
} from './platformPublicWorkDetailFlow';
import {
buildPuzzleResultProfileId,
buildPuzzleResultWorkId,
@@ -4025,13 +4029,13 @@ export function PlatformEntryFlowShellImpl({
const resultViewError =
autosaveCoordinator.customWorldAutoSaveError ??
sessionController.customWorldError;
const isSelectedPublicWorkOwned = Boolean(
authUi?.user?.id &&
selectedPublicWorkDetail?.ownerUserId === authUi.user.id,
);
const selectedPublicWorkActionMode = isSelectedPublicWorkOwned
? 'edit'
const selectedPublicWorkActionMode = selectedPublicWorkDetail
? resolvePlatformPublicWorkActionMode(
selectedPublicWorkDetail,
authUi?.user?.id,
)
: 'remix';
const isSelectedPublicWorkOwned = selectedPublicWorkActionMode === 'edit';
useEffect(() => {
if (
@@ -11574,54 +11578,33 @@ export function PlatformEntryFlowShellImpl({
const openPublicGalleryDetail = useCallback(
(entry: PlatformPublicGalleryCard) => {
if (isBigFishGalleryEntry(entry)) {
openPublicWorkDetail(entry);
return;
const strategy = resolvePlatformPublicWorkDetailOpenStrategy(entry);
switch (strategy.type) {
case 'use-entry':
openPublicWorkDetail(entry);
return;
case 'load-puzzle-detail':
void openPuzzlePublicWorkDetail(strategy.profileId, {
tab: platformBootstrap.platformTab,
});
return;
case 'load-jump-hop-detail':
void openJumpHopPublicWorkDetail(strategy.profileId);
return;
case 'load-wooden-fish-detail':
void openWoodenFishPublicWorkDetail(strategy.profileId);
return;
case 'load-visual-novel-detail':
void openVisualNovelPublicWorkDetail(strategy.profileId);
return;
case 'load-rpg-detail':
void openRpgPublicWorkDetail(strategy.entry);
return;
default: {
const exhaustive: never = strategy;
return exhaustive;
}
}
if (isPuzzleGalleryEntry(entry)) {
void openPuzzlePublicWorkDetail(entry.profileId, {
tab: platformBootstrap.platformTab,
});
return;
}
if (isMatch3DGalleryEntry(entry)) {
openPublicWorkDetail(entry);
return;
}
if (isSquareHoleGalleryEntry(entry)) {
openPublicWorkDetail(entry);
return;
}
if (isJumpHopGalleryEntry(entry)) {
void openJumpHopPublicWorkDetail(entry.profileId);
return;
}
if (isWoodenFishGalleryEntry(entry)) {
void openWoodenFishPublicWorkDetail(entry.profileId);
return;
}
if (isVisualNovelGalleryEntry(entry)) {
void openVisualNovelPublicWorkDetail(entry.profileId);
return;
}
if (isBarkBattleGalleryEntry(entry)) {
openPublicWorkDetail(entry);
return;
}
if (isEdutainmentGalleryEntry(entry)) {
openPublicWorkDetail(entry);
return;
}
void openRpgPublicWorkDetail(entry);
},
[
openPuzzlePublicWorkDetail,

View File

@@ -0,0 +1,229 @@
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 {
getPlatformPublicWorkDetailKind,
type PlatformPublicWorkDetailKind,
type PlatformPublicWorkDetailOpenStrategy,
resolvePlatformPublicWorkActionMode,
resolvePlatformPublicWorkDetailOpenStrategy,
} from './platformPublicWorkDetailFlow';
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 work detail flow resolves detail kind for every play kind', () => {
const cases: Array<
[sourceType: PlatformGallerySourceType, kind: PlatformPublicWorkDetailKind]
> = [
['big-fish', 'big-fish'],
['puzzle', 'puzzle'],
['jump-hop', 'jump-hop'],
['wooden-fish', 'wooden-fish'],
['match3d', 'match3d'],
['square-hole', 'square-hole'],
['visual-novel', 'visual-novel'],
['bark-battle', 'bark-battle'],
['edutainment', 'edutainment'],
];
cases.forEach(([sourceType, kind]) => {
expect(getPlatformPublicWorkDetailKind(buildTypedEntry(sourceType))).toBe(
kind,
);
});
expect(getPlatformPublicWorkDetailKind(buildRpgEntry())).toBe('rpg');
});
test('platform public work detail flow resolves open strategy', () => {
const rpgEntry = buildRpgEntry();
const cases: Array<
[
entry: PlatformPublicGalleryCard,
strategy: PlatformPublicWorkDetailOpenStrategy,
]
> = [
[
buildTypedEntry('big-fish'),
{
type: 'use-entry',
kind: 'big-fish',
},
],
[
buildTypedEntry('match3d'),
{
type: 'use-entry',
kind: 'match3d',
},
],
[
buildTypedEntry('square-hole'),
{
type: 'use-entry',
kind: 'square-hole',
},
],
[
buildTypedEntry('bark-battle'),
{
type: 'use-entry',
kind: 'bark-battle',
},
],
[
buildTypedEntry('edutainment'),
{
type: 'use-entry',
kind: 'edutainment',
},
],
[
buildTypedEntry('puzzle'),
{
type: 'load-puzzle-detail',
profileId: 'puzzle-profile',
},
],
[
buildTypedEntry('jump-hop'),
{
type: 'load-jump-hop-detail',
profileId: 'jump-hop-profile',
},
],
[
buildTypedEntry('wooden-fish'),
{
type: 'load-wooden-fish-detail',
profileId: 'wooden-fish-profile',
},
],
[
buildTypedEntry('visual-novel'),
{
type: 'load-visual-novel-detail',
profileId: 'visual-novel-profile',
},
],
[
rpgEntry,
{
type: 'load-rpg-detail',
entry: rpgEntry,
},
],
];
cases.forEach(([entry, strategy]) => {
expect(resolvePlatformPublicWorkDetailOpenStrategy(entry)).toEqual(
strategy,
);
});
});
test('platform public work detail flow resolves edit mode only for owned works', () => {
const entry = buildTypedEntry('puzzle');
expect(resolvePlatformPublicWorkActionMode(entry, 'user-1')).toBe('edit');
expect(resolvePlatformPublicWorkActionMode(entry, ' user-1 ')).toBe('edit');
expect(resolvePlatformPublicWorkActionMode(entry, 'user-2')).toBe('remix');
expect(resolvePlatformPublicWorkActionMode(entry, null)).toBe('remix');
});

View File

@@ -0,0 +1,190 @@
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 PlatformPublicWorkDetailKind =
| 'bark-battle'
| 'big-fish'
| 'edutainment'
| 'jump-hop'
| 'match3d'
| 'puzzle'
| 'rpg'
| 'square-hole'
| 'visual-novel'
| 'wooden-fish';
export type PlatformPublicWorkDetailOpenStrategy =
| {
type: 'use-entry';
kind: Exclude<
PlatformPublicWorkDetailKind,
'jump-hop' | 'puzzle' | 'rpg' | 'visual-novel' | 'wooden-fish'
>;
}
| {
type: 'load-puzzle-detail';
profileId: string;
}
| {
type: 'load-jump-hop-detail';
profileId: string;
}
| {
type: 'load-wooden-fish-detail';
profileId: string;
}
| {
type: 'load-visual-novel-detail';
profileId: string;
}
| {
type: 'load-rpg-detail';
entry: CustomWorldGalleryCard;
};
export type PlatformPublicWorkActionMode = 'edit' | 'remix';
export function isRpgPublicWorkDetailEntry(
entry: PlatformPublicGalleryCard,
): entry is CustomWorldGalleryCard {
return !('sourceType' in entry);
}
export function getPlatformPublicWorkDetailKind(
entry: PlatformPublicGalleryCard,
): PlatformPublicWorkDetailKind {
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 resolvePlatformPublicWorkDetailOpenStrategy(
entry: PlatformPublicGalleryCard,
): PlatformPublicWorkDetailOpenStrategy {
if (isBigFishGalleryEntry(entry)) {
return {
type: 'use-entry',
kind: 'big-fish',
};
}
if (isPuzzleGalleryEntry(entry)) {
return {
type: 'load-puzzle-detail',
profileId: entry.profileId,
};
}
if (isJumpHopGalleryEntry(entry)) {
return {
type: 'load-jump-hop-detail',
profileId: entry.profileId,
};
}
if (isWoodenFishGalleryEntry(entry)) {
return {
type: 'load-wooden-fish-detail',
profileId: entry.profileId,
};
}
if (isMatch3DGalleryEntry(entry)) {
return {
type: 'use-entry',
kind: 'match3d',
};
}
if (isSquareHoleGalleryEntry(entry)) {
return {
type: 'use-entry',
kind: 'square-hole',
};
}
if (isVisualNovelGalleryEntry(entry)) {
return {
type: 'load-visual-novel-detail',
profileId: entry.profileId,
};
}
if (isBarkBattleGalleryEntry(entry)) {
return {
type: 'use-entry',
kind: 'bark-battle',
};
}
if (isEdutainmentGalleryEntry(entry)) {
return {
type: 'use-entry',
kind: 'edutainment',
};
}
if (isRpgPublicWorkDetailEntry(entry)) {
return {
type: 'load-rpg-detail',
entry,
};
}
const exhaustive: never = entry;
return exhaustive;
}
export function resolvePlatformPublicWorkActionMode(
entry: PlatformPublicGalleryCard,
viewerUserId: string | null | undefined,
): PlatformPublicWorkActionMode {
return viewerUserId?.trim() && entry.ownerUserId === viewerUserId.trim()
? 'edit'
: 'remix';
}