refactor: 收口公开作品详情策略
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
190
src/components/platform-entry/platformPublicWorkDetailFlow.ts
Normal file
190
src/components/platform-entry/platformPublicWorkDetailFlow.ts
Normal 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';
|
||||
}
|
||||
Reference in New Issue
Block a user