This commit is contained in:
2026-05-01 22:16:01 +08:00
parent 8d46c05129
commit 33dd105630
36 changed files with 1999 additions and 236 deletions

View File

@@ -1,5 +1,6 @@
import { ArrowRight } from 'lucide-react';
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
import { UnifiedModal } from '../common/UnifiedModal';
import { getVisiblePlatformCreationTypes } from './platformEntryCreationTypes';
@@ -86,8 +87,8 @@ export function PlatformEntryCreationTypeModal({
return (
<UnifiedModal
open={isOpen}
title="选择创作类型"
description="先选玩法类型,再进入对应创作工作台。"
title={NEW_WORK_ENTRY_CONFIG.typeModal.title}
description={NEW_WORK_ENTRY_CONFIG.typeModal.description}
onClose={onClose}
closeDisabled={isBusy}
size="lg"

View File

@@ -655,6 +655,7 @@ function buildPuzzleCompileActionFromFormPayload(
...(workDescription ? { workDescription } : {}),
...(pictureDescription ? { pictureDescription } : {}),
referenceImageSrc: payload?.referenceImageSrc || null,
imageModel: payload?.imageModel ?? null,
candidateCount: 1,
};
}
@@ -687,6 +688,7 @@ function buildPuzzleFormPayloadFromSession(
workDescription,
pictureDescription,
referenceImageSrc: null,
imageModel: null,
};
}
@@ -714,6 +716,10 @@ function buildPuzzleFormPayloadFromAction(
payload.action === 'compile_puzzle_draft'
? (payload.referenceImageSrc ?? null)
: null,
imageModel:
payload.action === 'compile_puzzle_draft'
? (payload.imageModel ?? null)
: null,
};
}
@@ -941,8 +947,9 @@ export function PlatformEntryFlowShellImpl({
const [match3dProfile, setMatch3DProfile] =
useState<Match3DWorkProfile | null>(null);
const [match3dRun, setMatch3DRun] = useState<Match3DRunSnapshot | null>(null);
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] =
useState<'match3d-result' | 'work-detail'>('match3d-result');
const [match3dRuntimeReturnStage, setMatch3DRuntimeReturnStage] = useState<
'match3d-result' | 'work-detail'
>('match3d-result');
const [isMatch3DLoadingLibrary, setIsMatch3DLoadingLibrary] = useState(false);
const [bigFishRun, setBigFishRun] =
useState<BigFishRuntimeSnapshotResponse | null>(null);
@@ -993,8 +1000,10 @@ export function PlatformEntryFlowShellImpl({
const [deletingCreationWorkId, setDeletingCreationWorkId] = useState<
string | null
>(null);
const [claimingPuzzlePointIncentiveProfileId, setClaimingPuzzlePointIncentiveProfileId] =
useState<string | null>(null);
const [
claimingPuzzlePointIncentiveProfileId,
setClaimingPuzzlePointIncentiveProfileId,
] = useState<string | null>(null);
const isBigFishCreationVisible = isPlatformCreationTypeVisible('big-fish');
const [profilePlayStats, setProfilePlayStats] =
useState<ProfilePlayStatsResponse | null>(null);
@@ -1099,7 +1108,9 @@ export function PlatformEntryFlowShellImpl({
return galleryResponse.items;
} catch (error) {
setMatch3DGalleryEntries([]);
setMatch3DError(resolveMatch3DErrorMessage(error, '读取抓大鹅广场失败。'));
setMatch3DError(
resolveMatch3DErrorMessage(error, '读取抓大鹅广场失败。'),
);
return [];
}
}, [resolveMatch3DErrorMessage]);
@@ -1293,7 +1304,11 @@ export function PlatformEntryFlowShellImpl({
);
return mergePlatformPublicGalleryEntries(
platformBootstrap.publishedGalleryEntries,
[...bigFishPublicEntries, ...match3dPublicEntries, ...puzzlePublicEntries],
[
...bigFishPublicEntries,
...match3dPublicEntries,
...puzzlePublicEntries,
],
).slice(0, 6);
}, [
isBigFishCreationVisible,
@@ -1665,24 +1680,6 @@ export function PlatformEntryFlowShellImpl({
await bigFishFlow.openWorkspace();
}, [bigFishFlow]);
const openMatch3DAgentWorkspace = useCallback(async () => {
setMatch3DSession(null);
setMatch3DProfile(null);
setMatch3DRun(null);
setMatch3DError(null);
setStreamingMatch3DReplyText('');
setIsStreamingMatch3DReply(false);
await match3dFlow.openWorkspace();
}, [
match3dFlow,
setIsStreamingMatch3DReply,
setMatch3DError,
setMatch3DProfile,
setMatch3DRun,
setMatch3DSession,
setStreamingMatch3DReplyText,
]);
const openPuzzleAgentWorkspace = useCallback(async () => {
setPuzzleRun(null);
setPuzzleOperation(null);
@@ -1729,6 +1726,7 @@ export function PlatformEntryFlowShellImpl({
workTitle: payload.workTitle ?? payload.seedText ?? '',
workDescription: payload.workDescription ?? '',
pictureDescription: payload.pictureDescription ?? '',
imageModel: payload.imageModel ?? null,
});
setPuzzleOperation(response.operation);
puzzleFlow.setSession(response.session);
@@ -1826,12 +1824,7 @@ export function PlatformEntryFlowShellImpl({
const handleCreationHubCreateType = useCallback(
(type: PlatformCreationTypeId) => {
if (
type === 'rpg' ||
type === 'match3d' ||
type === 'airp' ||
type === 'visual-novel'
) {
if (type === 'match3d' || type === 'airp' || type === 'visual-novel') {
return;
}
@@ -1839,6 +1832,13 @@ export function PlatformEntryFlowShellImpl({
return;
}
if (type === 'rpg') {
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
return;
}
if (type === 'big-fish') {
runProtectedAction(() => {
void openBigFishAgentWorkspace();
@@ -1857,6 +1857,7 @@ export function PlatformEntryFlowShellImpl({
openPuzzleAgentWorkspace,
prepareCreationLaunch,
runProtectedAction,
sessionController,
],
);
@@ -2332,8 +2333,8 @@ export function PlatformEntryFlowShellImpl({
propKind === 'extendTime'
? extendLocalPuzzleTime(currentRun)
: propKind === 'freezeTime'
? applyLocalPuzzleFreezeTime(currentRun)
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
? applyLocalPuzzleFreezeTime(currentRun)
: setLocalPuzzlePaused(currentRun, propKind === 'reference');
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return nextRun;
@@ -2367,7 +2368,9 @@ export function PlatformEntryFlowShellImpl({
selectedPuzzleDetail,
);
if (isLocalPuzzleRun(puzzleRun)) {
const nextRun = restartLocalPuzzleLevel(puzzleRunRef.current ?? puzzleRun);
const nextRun = restartLocalPuzzleLevel(
puzzleRunRef.current ?? puzzleRun,
);
puzzleRunRef.current = nextRun;
setPuzzleRun(nextRun);
return;
@@ -2531,68 +2534,68 @@ export function PlatformEntryFlowShellImpl({
setPuzzleError,
]);
const advancePuzzleLevel = useCallback(async (target?: {
profileId?: string;
levelId?: string | null;
}) => {
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
return;
}
const currentLevel = puzzleRun.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return;
}
setIsPuzzleBusy(true);
setIsPuzzleNextLevelGenerating(true);
setPuzzleError(null);
try {
const targetProfileId = target?.profileId?.trim();
if (
targetProfileId &&
targetProfileId !== currentLevel.profileId &&
puzzleRun.nextLevelMode === 'similarWorks'
) {
await startPuzzleRunFromProfile(
targetProfileId,
'puzzle-gallery-detail',
undefined,
false,
null,
);
const advancePuzzleLevel = useCallback(
async (target?: { profileId?: string; levelId?: string | null }) => {
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
return;
}
const { run } = isLocalPuzzleRun(puzzleRun)
? await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
})
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
if (!isLocalPuzzleRun(puzzleRun)) {
void platformBootstrap.refreshSaveArchives();
const currentLevel = puzzleRun.currentLevel;
if (!currentLevel || currentLevel.status !== 'cleared') {
return;
}
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
setIsPuzzleNextLevelGenerating(false);
setIsPuzzleBusy(false);
}
}, [
isPuzzleBusy,
isPuzzleLeaderboardBusy,
platformBootstrap,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
startPuzzleRunFromProfile,
]);
setIsPuzzleBusy(true);
setIsPuzzleNextLevelGenerating(true);
setPuzzleError(null);
try {
const targetProfileId = target?.profileId?.trim();
if (
targetProfileId &&
targetProfileId !== currentLevel.profileId &&
puzzleRun.nextLevelMode === 'similarWorks'
) {
await startPuzzleRunFromProfile(
targetProfileId,
'puzzle-gallery-detail',
undefined,
false,
null,
);
return;
}
const { run } = isLocalPuzzleRun(puzzleRun)
? await advanceLocalPuzzleNextLevel({
run: puzzleRun,
sourceSessionId:
selectedPuzzleDetail?.sourceSessionId ??
puzzleSession?.sessionId ??
null,
})
: await advancePuzzleNextLevel(puzzleRun.runId);
setPuzzleRun(run);
if (!isLocalPuzzleRun(puzzleRun)) {
void platformBootstrap.refreshSaveArchives();
}
} catch (error) {
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
} finally {
setIsPuzzleNextLevelGenerating(false);
setIsPuzzleBusy(false);
}
},
[
isPuzzleBusy,
isPuzzleLeaderboardBusy,
platformBootstrap,
puzzleRun,
puzzleSession,
resolvePuzzleErrorMessage,
selectedPuzzleDetail,
startPuzzleRunFromProfile,
],
);
const leaveAgentWorkspace = useCallback(() => {
enterCreateTab();
@@ -2935,14 +2938,10 @@ export function PlatformEntryFlowShellImpl({
.then((response) => {
const updatedWork = response.item;
setPuzzleWorks((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setPuzzleGalleryEntries((current) =>
current.map((item) =>
mergePuzzleWorkSummary(item, updatedWork),
),
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
);
setSelectedPuzzleDetail((current) =>
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
@@ -3316,7 +3315,9 @@ export function PlatformEntryFlowShellImpl({
return;
}
const restoredSession = await match3dFlow.restoreDraft(item.sourceSessionId);
const restoredSession = await match3dFlow.restoreDraft(
item.sourceSessionId,
);
if (!restoredSession) {
await refreshMatch3DShelf().catch(() => undefined);
return;
@@ -3395,7 +3396,9 @@ export function PlatformEntryFlowShellImpl({
if (isPuzzleGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToPuzzleWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前拼图作品信息不完整,暂时无法进入玩法。');
setPublicWorkDetailError(
'当前拼图作品信息不完整,暂时无法进入玩法。',
);
return;
}
setPublicWorkDetailError(null);
@@ -3411,7 +3414,9 @@ export function PlatformEntryFlowShellImpl({
if (isMatch3DGalleryEntry(selectedPublicWorkDetail)) {
const work = mapPublicWorkDetailToMatch3DWork(selectedPublicWorkDetail);
if (!work) {
setPublicWorkDetailError('当前抓大鹅作品信息不完整,暂时无法进入玩法。');
setPublicWorkDetailError(
'当前抓大鹅作品信息不完整,暂时无法进入玩法。',
);
return;
}
setPublicWorkDetailError(null);
@@ -4487,7 +4492,9 @@ export function PlatformEntryFlowShellImpl({
className="flex h-full min-h-0 flex-col"
>
<Suspense
fallback={<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />}
fallback={
<LazyPanelFallback label="正在加载抓大鹅共创工作区..." />
}
>
<Match3DAgentWorkspace
session={match3dSession}
@@ -4520,7 +4527,8 @@ export function PlatformEntryFlowShellImpl({
>
<Match3DResultView
profile={
match3dProfile ?? buildMatch3DProfileFromSession(match3dSession)!
match3dProfile ??
buildMatch3DProfileFromSession(match3dSession)!
}
draft={match3dSession.draft}
isBusy={isMatch3DBusy}
@@ -4537,7 +4545,9 @@ export function PlatformEntryFlowShellImpl({
refreshMatch3DShelf(),
refreshMatch3DGallery(),
]);
openPublicWorkDetail(mapMatch3DWorkToPublicWorkDetail(profile));
openPublicWorkDetail(
mapMatch3DWorkToPublicWorkDetail(profile),
);
}}
onStartTestRun={(profile) => {
setMatch3DProfile(profile);
@@ -4565,7 +4575,9 @@ export function PlatformEntryFlowShellImpl({
error={match3dError}
onBack={() => {
if (match3dRun?.runId && match3dRun.status === 'running') {
void stopMatch3DRun(match3dRun.runId).catch(() => undefined);
void stopMatch3DRun(match3dRun.runId).catch(
() => undefined,
);
}
setSelectionStage(match3dRuntimeReturnStage);
}}
@@ -4596,7 +4608,9 @@ export function PlatformEntryFlowShellImpl({
onClickItem={(payload) => {
const runId = payload.runId ?? match3dRun?.runId;
if (!runId) {
return Promise.reject(new Error('抓大鹅运行态缺少 runId。'));
return Promise.reject(
new Error('抓大鹅运行态缺少 runId。'),
);
}
return clickMatch3DItem(runId, payload);
}}
@@ -5084,7 +5098,9 @@ export function PlatformEntryFlowShellImpl({
setShowCreationTypeModal(false);
}}
onSelectRpg={() => {
// RPG 创作入口当前为敬请期待;保留回调防御,避免旧入口绕过锁定态。
runProtectedAction(() => {
void sessionController.openRpgAgentWorkspace();
});
}}
onSelectBigFish={() => {
runProtectedAction(() => {

View File

@@ -0,0 +1,41 @@
import { expect, test } from 'vitest';
import { NEW_WORK_ENTRY_CONFIG } from '../../config/newWorkEntryConfig';
import {
getVisiblePlatformCreationTypes,
isPlatformCreationTypeVisible,
PLATFORM_CREATION_TYPES,
} from './platformEntryCreationTypes';
test('platform creation types are derived from new work entry config', () => {
const puzzleConfig = NEW_WORK_ENTRY_CONFIG.creationTypes.find(
(item) => item.id === 'puzzle',
);
expect(puzzleConfig).toBeTruthy();
expect(PLATFORM_CREATION_TYPES).toContainEqual(
expect.objectContaining({
id: 'puzzle',
title: puzzleConfig?.title,
subtitle: puzzleConfig?.subtitle,
badge: puzzleConfig?.badge,
locked: false,
hidden: false,
}),
);
});
test('new work entry config controls visibility and open order', () => {
const visibleIds = getVisiblePlatformCreationTypes().map((item) => item.id);
expect(isPlatformCreationTypeVisible('big-fish')).toBe(false);
expect(visibleIds).not.toContain('big-fish');
expect(visibleIds[0]).toBe('rpg');
expect(visibleIds).toEqual([
'rpg',
'puzzle',
'match3d',
'airp',
'visual-novel',
]);
});

View File

@@ -1,10 +1,9 @@
export type PlatformCreationTypeId =
| 'rpg'
| 'big-fish'
| 'match3d'
| 'puzzle'
| 'airp'
| 'visual-novel';
import {
NEW_WORK_ENTRY_CONFIG,
type NewWorkEntryCreationTypeId,
} from '../../config/newWorkEntryConfig';
export type PlatformCreationTypeId = NewWorkEntryCreationTypeId;
export type PlatformCreationTypeCard = {
id: PlatformCreationTypeId;
@@ -39,51 +38,15 @@ export function isPlatformCreationTypeVisible(id: PlatformCreationTypeId) {
}
/**
* 创作页与类型弹层共用同一份模板元数据,避免多入口文案和可用状态漂移。
* 创作页与类型弹层共用同一份新建作品入口配置,避免多入口文案和开放状态漂移。
* `hidden` 只控制平台入口是否展示,不影响既有玩法链路和路由能力。
*/
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
{
id: 'rpg',
title: '角色扮演',
subtitle: '敬请期待',
badge: '敬请期待',
locked: true,
},
{
id: 'big-fish',
title: '大鱼吃小鱼',
subtitle: '实时成长玩法',
badge: '可创建',
locked: false,
hidden: true,
},
{
id: 'puzzle',
title: '拼图',
subtitle: '创意礼物,生活分享',
badge: '可创建',
locked: false,
},
{
id: 'match3d',
title: '抓大鹅',
subtitle: '敬请期待',
badge: '敬请期待',
locked: true,
},
{
id: 'airp',
title: 'AIRP',
subtitle: '敬请期待',
badge: '敬请期待',
locked: true,
},
{
id: 'visual-novel',
title: '视觉小说',
subtitle: '敬请期待',
badge: '敬请期待',
locked: true,
},
];
export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] =
NEW_WORK_ENTRY_CONFIG.creationTypes.map((item) => ({
id: item.id,
title: item.title,
subtitle: item.subtitle,
badge: item.badge,
locked: !item.open,
hidden: !item.visible,
}));