1
This commit is contained in:
@@ -56,6 +56,7 @@ import {
|
||||
streamBigFishCreationMessage,
|
||||
} from '../../services/big-fish-creation';
|
||||
import {
|
||||
likeBigFishGalleryWork,
|
||||
listBigFishGallery,
|
||||
remixBigFishGalleryWork,
|
||||
} from '../../services/big-fish-gallery';
|
||||
@@ -94,11 +95,13 @@ import {
|
||||
} from '../../services/puzzle-agent';
|
||||
import {
|
||||
getPuzzleGalleryDetail,
|
||||
likePuzzleGalleryWork,
|
||||
listPuzzleGallery,
|
||||
remixPuzzleGalleryWork,
|
||||
} from '../../services/puzzle-gallery';
|
||||
import {
|
||||
advanceLocalPuzzleNextLevel,
|
||||
advancePuzzleNextLevel,
|
||||
getPuzzleRun,
|
||||
startPuzzleRun,
|
||||
submitPuzzleLeaderboard,
|
||||
@@ -121,6 +124,7 @@ import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreati
|
||||
import {
|
||||
deleteRpgEntryWorldProfile,
|
||||
getRpgEntryWorldGalleryDetailByCode,
|
||||
likeRpgEntryWorldGallery,
|
||||
recordRpgEntryWorldGalleryPlay,
|
||||
remixRpgEntryWorldGallery,
|
||||
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
@@ -202,6 +206,13 @@ function getPlatformPublicGalleryEntryKey(entry: PlatformPublicGalleryCard) {
|
||||
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||||
}
|
||||
|
||||
function isSamePlatformPublicGalleryEntry(
|
||||
left: PlatformPublicGalleryCard,
|
||||
right: PlatformPublicGalleryCard,
|
||||
) {
|
||||
return getPlatformPublicGalleryEntryKey(left) === getPlatformPublicGalleryEntryKey(right);
|
||||
}
|
||||
|
||||
function mergePlatformPublicGalleryEntries(
|
||||
rpgEntries: CustomWorldGalleryCard[],
|
||||
puzzleEntries: PlatformPublicGalleryCard[],
|
||||
@@ -281,6 +292,7 @@ function mapPublicWorkDetailToBigFishWork(
|
||||
workId: entry.workId,
|
||||
sourceSessionId: entry.profileId,
|
||||
ownerUserId: entry.ownerUserId,
|
||||
authorDisplayName: entry.authorDisplayName,
|
||||
title: entry.worldName,
|
||||
subtitle: entry.subtitle,
|
||||
summary: entry.summaryText,
|
||||
@@ -299,6 +311,20 @@ function mapPublicWorkDetailToBigFishWork(
|
||||
};
|
||||
}
|
||||
|
||||
function mergePuzzleWorkSummary(
|
||||
current: PuzzleWorkSummary,
|
||||
updated: PuzzleWorkSummary,
|
||||
): PuzzleWorkSummary {
|
||||
return current.profileId === updated.profileId ? updated : current;
|
||||
}
|
||||
|
||||
function mergeBigFishWorkSummary(
|
||||
current: BigFishWorkSummary,
|
||||
updated: BigFishWorkSummary,
|
||||
): BigFishWorkSummary {
|
||||
return current.sourceSessionId === updated.sourceSessionId ? updated : current;
|
||||
}
|
||||
|
||||
async function resolvePublicWorkAuthorSummary(
|
||||
entry: PlatformPublicGalleryCard,
|
||||
): Promise<PublicUserSummary | null> {
|
||||
@@ -457,15 +483,85 @@ function buildPuzzleResultProfileId(sessionId: string | null | undefined) {
|
||||
function buildPuzzleCompileActionFromFormPayload(
|
||||
payload: CreatePuzzleAgentSessionRequest | null,
|
||||
): PuzzleAgentActionRequest {
|
||||
const workTitle = payload?.workTitle?.trim() || payload?.seedText?.trim();
|
||||
const workDescription = payload?.workDescription?.trim();
|
||||
const pictureDescription = payload?.pictureDescription?.trim();
|
||||
|
||||
return {
|
||||
action: 'compile_puzzle_draft',
|
||||
promptText:
|
||||
payload?.pictureDescription?.trim() || payload?.seedText?.trim(),
|
||||
promptText: pictureDescription || workTitle,
|
||||
...(workTitle ? { workTitle } : {}),
|
||||
...(workDescription ? { workDescription } : {}),
|
||||
...(pictureDescription ? { pictureDescription } : {}),
|
||||
referenceImageSrc: payload?.referenceImageSrc || null,
|
||||
candidateCount: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleFormPayloadFromSession(
|
||||
session: PuzzleAgentSessionSnapshot,
|
||||
): CreatePuzzleAgentSessionRequest {
|
||||
const formDraft = session.draft?.formDraft;
|
||||
const workTitle =
|
||||
formDraft?.workTitle?.trim() ||
|
||||
session.draft?.workTitle?.trim() ||
|
||||
session.draft?.levelName?.trim() ||
|
||||
session.anchorPack.themePromise.value.trim() ||
|
||||
session.seedText?.trim() ||
|
||||
'';
|
||||
const workDescription =
|
||||
formDraft?.workDescription?.trim() ||
|
||||
session.draft?.workDescription?.trim() ||
|
||||
session.draft?.summary?.trim() ||
|
||||
'';
|
||||
const pictureDescription =
|
||||
formDraft?.pictureDescription?.trim() ||
|
||||
session.draft?.levels?.[0]?.pictureDescription?.trim() ||
|
||||
session.anchorPack.visualSubject.value.trim() ||
|
||||
'';
|
||||
|
||||
return {
|
||||
seedText: workTitle,
|
||||
workTitle,
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildPuzzleFormPayloadFromAction(
|
||||
payload: PuzzleAgentActionRequest,
|
||||
): CreatePuzzleAgentSessionRequest | null {
|
||||
if (
|
||||
payload.action !== 'compile_puzzle_draft' &&
|
||||
payload.action !== 'save_puzzle_form_draft'
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workTitle = payload.workTitle?.trim() ?? '';
|
||||
const workDescription = payload.workDescription?.trim() ?? '';
|
||||
const pictureDescription =
|
||||
payload.pictureDescription?.trim() || payload.promptText?.trim() || '';
|
||||
|
||||
return {
|
||||
seedText: workTitle,
|
||||
workTitle,
|
||||
workDescription,
|
||||
pictureDescription,
|
||||
referenceImageSrc:
|
||||
payload.action === 'compile_puzzle_draft'
|
||||
? (payload.referenceImageSrc ?? null)
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
function isPuzzleFormOnlyDraft(session: PuzzleAgentSessionSnapshot | null) {
|
||||
return Boolean(
|
||||
session?.stage === 'collecting_anchors' && session.draft?.formDraft,
|
||||
);
|
||||
}
|
||||
|
||||
const CustomWorldGenerationView = lazy(async () => {
|
||||
const module = await import('../CustomWorldGenerationView');
|
||||
return {
|
||||
@@ -1125,6 +1221,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
onActionComplete: async ({ payload, response, setSession }) => {
|
||||
setPuzzleOperation(response.operation);
|
||||
setSession(response.session);
|
||||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||||
if (formPayload) {
|
||||
setPuzzleFormDraftPayload(formPayload);
|
||||
}
|
||||
|
||||
if (payload.action === 'publish_puzzle_work') {
|
||||
await Promise.allSettled([
|
||||
@@ -1167,6 +1267,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
},
|
||||
beforeExecuteAction: ({ payload }) => {
|
||||
const formPayload = buildPuzzleFormPayloadFromAction(payload);
|
||||
if (formPayload) {
|
||||
setPuzzleFormDraftPayload(formPayload);
|
||||
}
|
||||
|
||||
if (payload.action !== 'compile_puzzle_draft') {
|
||||
return;
|
||||
}
|
||||
@@ -1224,19 +1329,16 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleOperation(null);
|
||||
setPuzzleGenerationState(null);
|
||||
setPuzzleFormDraftPayload(null);
|
||||
puzzleFlow.setSession(null);
|
||||
puzzleFlow.setError(null);
|
||||
puzzleFlow.setStreamingReplyText('');
|
||||
puzzleFlow.setIsStreamingReply(false);
|
||||
enterCreateTab();
|
||||
setShowCreationTypeModal(false);
|
||||
setSelectionStage('puzzle-agent-workspace');
|
||||
}, [enterCreateTab, puzzleFlow, setSelectionStage]);
|
||||
const nextSession = await puzzleFlow.openWorkspace({});
|
||||
if (nextSession) {
|
||||
void refreshPuzzleShelf();
|
||||
}
|
||||
}, [puzzleFlow, refreshPuzzleShelf]);
|
||||
|
||||
const createPuzzleDraftFromForm = useCallback(
|
||||
async (payload: CreatePuzzleAgentSessionRequest) => {
|
||||
setPuzzleFormDraftPayload(payload);
|
||||
const nextSession = await puzzleFlow.openWorkspace(payload);
|
||||
const nextSession = puzzleFlow.session ?? (await puzzleFlow.openWorkspace(payload));
|
||||
if (!nextSession) {
|
||||
return;
|
||||
}
|
||||
@@ -1249,6 +1351,36 @@ export function PlatformEntryFlowShellImpl({
|
||||
[puzzleFlow],
|
||||
);
|
||||
|
||||
const savePuzzleFormDraft = useCallback(
|
||||
async (payload: CreatePuzzleAgentSessionRequest) => {
|
||||
const session = puzzleFlow.session;
|
||||
if (!session || session.stage !== 'collecting_anchors') {
|
||||
return;
|
||||
}
|
||||
|
||||
setPuzzleFormDraftPayload(payload);
|
||||
|
||||
try {
|
||||
const response = await executePuzzleAgentAction(session.sessionId, {
|
||||
action: 'save_puzzle_form_draft',
|
||||
promptText: payload.pictureDescription ?? null,
|
||||
workTitle: payload.workTitle ?? payload.seedText ?? '',
|
||||
workDescription: payload.workDescription ?? '',
|
||||
pictureDescription: payload.pictureDescription ?? '',
|
||||
});
|
||||
setPuzzleOperation(response.operation);
|
||||
puzzleFlow.setSession(response.session);
|
||||
setPuzzleError(null);
|
||||
void refreshPuzzleShelf();
|
||||
} catch (error) {
|
||||
setPuzzleError(
|
||||
resolvePuzzleErrorMessage(error, '保存拼图表单草稿失败。'),
|
||||
);
|
||||
}
|
||||
},
|
||||
[puzzleFlow, refreshPuzzleShelf, resolvePuzzleErrorMessage, setPuzzleError],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (platformBootstrap.canReadProtectedData) {
|
||||
hadReadableProtectedDataRef.current = true;
|
||||
@@ -1318,7 +1450,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
|
||||
const handleCreationHubCreateType = useCallback(
|
||||
(type: PlatformCreationTypeId) => {
|
||||
if (type === 'airp' || type === 'visual-novel') {
|
||||
if (type === 'rpg' || type === 'airp' || type === 'visual-novel') {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1326,13 +1458,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'rpg') {
|
||||
runProtectedAction(() => {
|
||||
void sessionController.openRpgAgentWorkspace();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'big-fish') {
|
||||
runProtectedAction(() => {
|
||||
void openBigFishAgentWorkspace();
|
||||
@@ -1351,7 +1476,6 @@ export function PlatformEntryFlowShellImpl({
|
||||
openPuzzleAgentWorkspace,
|
||||
prepareCreationLaunch,
|
||||
runProtectedAction,
|
||||
sessionController,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1459,6 +1583,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
returnStage: PuzzleRuntimeReturnStage = 'work-detail',
|
||||
detailItem?: PuzzleWorkSummary,
|
||||
mirrorErrorToPublicDetail = false,
|
||||
levelId?: string | null,
|
||||
) => {
|
||||
if (isPuzzleBusy) {
|
||||
return;
|
||||
@@ -1470,7 +1595,10 @@ export function PlatformEntryFlowShellImpl({
|
||||
try {
|
||||
const item =
|
||||
detailItem ?? (await getPuzzleGalleryDetail(profileId)).item;
|
||||
const { run } = await startPuzzleRun({ profileId: item.profileId });
|
||||
const { run } = await startPuzzleRun({
|
||||
profileId: item.profileId,
|
||||
levelId: levelId ?? null,
|
||||
});
|
||||
setSelectedPuzzleDetail(item);
|
||||
setPuzzleRun(run);
|
||||
setPuzzleRuntimeReturnStage(returnStage);
|
||||
@@ -1513,6 +1641,8 @@ export function PlatformEntryFlowShellImpl({
|
||||
ownerUserId: authUi?.user?.id ?? 'current-user',
|
||||
sourceSessionId: puzzleSession?.sessionId ?? null,
|
||||
authorDisplayName: authUi?.user?.displayName ?? '玩家',
|
||||
workTitle: draft.workTitle || draft.levelName,
|
||||
workDescription: draft.workDescription || draft.summary,
|
||||
levelName: draft.levelName,
|
||||
summary: draft.summary,
|
||||
themeTags: draft.themeTags,
|
||||
@@ -1787,7 +1917,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
]);
|
||||
|
||||
const advancePuzzleLevel = useCallback(async () => {
|
||||
if (!puzzleRun || isPuzzleBusy) {
|
||||
if (!puzzleRun || isPuzzleBusy || isPuzzleLeaderboardBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1801,13 +1931,15 @@ export function PlatformEntryFlowShellImpl({
|
||||
setPuzzleError(null);
|
||||
|
||||
try {
|
||||
const { run } = await advanceLocalPuzzleNextLevel({
|
||||
run: puzzleRun,
|
||||
sourceSessionId:
|
||||
selectedPuzzleDetail?.sourceSessionId ??
|
||||
puzzleSession?.sessionId ??
|
||||
null,
|
||||
});
|
||||
const { run } = isLocalPuzzleRun(puzzleRun)
|
||||
? await advanceLocalPuzzleNextLevel({
|
||||
run: puzzleRun,
|
||||
sourceSessionId:
|
||||
selectedPuzzleDetail?.sourceSessionId ??
|
||||
puzzleSession?.sessionId ??
|
||||
null,
|
||||
})
|
||||
: await advancePuzzleNextLevel(puzzleRun.runId);
|
||||
setPuzzleRun(run);
|
||||
} catch (error) {
|
||||
setPuzzleError(resolvePuzzleErrorMessage(error, '准备下一关失败。'));
|
||||
@@ -1817,6 +1949,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
}, [
|
||||
isPuzzleBusy,
|
||||
isPuzzleLeaderboardBusy,
|
||||
puzzleRun,
|
||||
puzzleSession,
|
||||
resolvePuzzleErrorMessage,
|
||||
@@ -2022,14 +2155,17 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
|
||||
runProtectedAction(() => {
|
||||
const displayName =
|
||||
work.workTitle?.trim() || work.levelName.trim() || '未命名拼图';
|
||||
const confirmed = window.confirm(
|
||||
`确认删除作品《${work.levelName}》吗?删除后会从你的作品列表和公开广场中移除。`,
|
||||
`确认删除作品《${displayName}》吗?删除后会从你的作品列表和公开广场中移除。`,
|
||||
);
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
setDeletingCreationWorkId(work.workId);
|
||||
setPuzzleFormDraftPayload(null);
|
||||
setPuzzleError(null);
|
||||
|
||||
void deletePuzzleWork(work.profileId)
|
||||
@@ -2095,6 +2231,127 @@ export function PlatformEntryFlowShellImpl({
|
||||
[setSelectionStage],
|
||||
);
|
||||
|
||||
const syncUpdatedPublicWorkDetail = useCallback(
|
||||
(updatedEntry: PlatformPublicGalleryCard) => {
|
||||
setSelectedPublicWorkDetail((current) =>
|
||||
current && isSamePlatformPublicGalleryEntry(current, updatedEntry)
|
||||
? updatedEntry
|
||||
: current,
|
||||
);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const likePublicWork = useCallback(
|
||||
(entry: PlatformPublicGalleryCard) => {
|
||||
if (isPublicWorkDetailBusy) {
|
||||
return;
|
||||
}
|
||||
|
||||
runProtectedAction(() => {
|
||||
setIsPublicWorkDetailBusy(true);
|
||||
setPublicWorkDetailError(null);
|
||||
|
||||
if (isBigFishGalleryEntry(entry)) {
|
||||
void likeBigFishGalleryWork(entry.profileId)
|
||||
.then((response) => {
|
||||
const updatedWork = response.items.find(
|
||||
(item) => item.sourceSessionId === entry.profileId,
|
||||
);
|
||||
if (!updatedWork) {
|
||||
return;
|
||||
}
|
||||
setBigFishGalleryEntries((current) =>
|
||||
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
|
||||
);
|
||||
setBigFishWorks((current) =>
|
||||
current.map((item) => mergeBigFishWorkSummary(item, updatedWork)),
|
||||
);
|
||||
syncUpdatedPublicWorkDetail(
|
||||
mapBigFishWorkToPublicWorkDetail(updatedWork),
|
||||
);
|
||||
setBigFishRuntimeWork((current) =>
|
||||
current ? mergeBigFishWorkSummary(current, updatedWork) : current,
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPublicWorkDetailError(
|
||||
resolveBigFishErrorMessage(
|
||||
error,
|
||||
'点赞大鱼吃小鱼作品失败。',
|
||||
),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPublicWorkDetailBusy(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPuzzleGalleryEntry(entry)) {
|
||||
void likePuzzleGalleryWork(entry.profileId)
|
||||
.then((response) => {
|
||||
const updatedWork = response.item;
|
||||
setPuzzleGalleryEntries((current) =>
|
||||
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
|
||||
);
|
||||
setPuzzleWorks((current) =>
|
||||
current.map((item) => mergePuzzleWorkSummary(item, updatedWork)),
|
||||
);
|
||||
setSelectedPuzzleDetail((current) =>
|
||||
current ? mergePuzzleWorkSummary(current, updatedWork) : current,
|
||||
);
|
||||
syncUpdatedPublicWorkDetail(
|
||||
mapPuzzleWorkToPublicWorkDetail(updatedWork),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPublicWorkDetailError(
|
||||
resolvePuzzleErrorMessage(error, '点赞拼图作品失败。'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPublicWorkDetailBusy(false);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
void likeRpgEntryWorldGallery(entry.ownerUserId, entry.profileId)
|
||||
.then((updatedEntry) => {
|
||||
setSelectedDetailEntry((current) =>
|
||||
current?.profileId === updatedEntry.profileId ? updatedEntry : current,
|
||||
);
|
||||
platformBootstrap.setPublishedGalleryEntries((current) =>
|
||||
current.map((item) =>
|
||||
item.profileId === updatedEntry.profileId
|
||||
? mapRpgGalleryCardToPublicWorkDetail(updatedEntry)
|
||||
: item,
|
||||
),
|
||||
);
|
||||
syncUpdatedPublicWorkDetail(
|
||||
mapRpgGalleryCardToPublicWorkDetail(updatedEntry),
|
||||
);
|
||||
})
|
||||
.catch((error) => {
|
||||
setPublicWorkDetailError(
|
||||
resolveRpgCreationErrorMessage(error, '点赞 RPG 作品失败。'),
|
||||
);
|
||||
})
|
||||
.finally(() => {
|
||||
setIsPublicWorkDetailBusy(false);
|
||||
});
|
||||
});
|
||||
},
|
||||
[
|
||||
isPublicWorkDetailBusy,
|
||||
platformBootstrap,
|
||||
resolveBigFishErrorMessage,
|
||||
resolvePuzzleErrorMessage,
|
||||
runProtectedAction,
|
||||
syncUpdatedPublicWorkDetail,
|
||||
],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const detailEntry =
|
||||
selectionStage === 'work-detail'
|
||||
@@ -2244,9 +2501,25 @@ export function PlatformEntryFlowShellImpl({
|
||||
);
|
||||
if (!restoredSession) {
|
||||
await refreshPuzzleShelf().catch(() => undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPuzzleFormOnlyDraft(restoredSession)) {
|
||||
setPuzzleFormDraftPayload(
|
||||
buildPuzzleFormPayloadFromSession(restoredSession),
|
||||
);
|
||||
setSelectionStage('puzzle-agent-workspace');
|
||||
} else {
|
||||
setPuzzleFormDraftPayload(null);
|
||||
}
|
||||
},
|
||||
[openPuzzleDetail, puzzleFlow, refreshPuzzleShelf, setPuzzleError],
|
||||
[
|
||||
openPuzzleDetail,
|
||||
puzzleFlow,
|
||||
refreshPuzzleShelf,
|
||||
setPuzzleError,
|
||||
setSelectionStage,
|
||||
],
|
||||
);
|
||||
|
||||
const startBigFishRunFromWork = useCallback(
|
||||
@@ -2649,6 +2922,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
workId: `big-fish:${sessionId}`,
|
||||
sourceSessionId: sessionId,
|
||||
ownerUserId: work.ownerUserId ?? '',
|
||||
authorDisplayName: work.worldSubtitle || '玩家',
|
||||
title: work.worldTitle,
|
||||
subtitle: work.worldSubtitle,
|
||||
summary: work.worldSubtitle,
|
||||
@@ -2977,6 +3251,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
<PlatformWorkDetailView
|
||||
entry={selectedPublicWorkDetail}
|
||||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||||
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
|
||||
isBusy={isPublicWorkDetailBusy || isPuzzleBusy || isBigFishBusy}
|
||||
error={publicWorkDetailError}
|
||||
onBack={() => {
|
||||
@@ -2984,6 +3259,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
clearSelectedPublicWorkAuthor();
|
||||
setSelectionStage('platform');
|
||||
}}
|
||||
onLike={() => {
|
||||
likePublicWork(selectedPublicWorkDetail);
|
||||
}}
|
||||
onStart={startSelectedPublicWork}
|
||||
onRemix={remixSelectedPublicWork}
|
||||
/>
|
||||
@@ -3008,6 +3286,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
<PlatformWorkDetailView
|
||||
entry={mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry)}
|
||||
authorAvatarUrl={selectedPublicWorkAuthor?.avatarUrl ?? null}
|
||||
authorDisplayName={selectedPublicWorkAuthor?.displayName ?? null}
|
||||
isBusy={detailNavigation.isMutatingDetail}
|
||||
error={detailNavigation.detailError}
|
||||
onBack={() => {
|
||||
@@ -3015,6 +3294,11 @@ export function PlatformEntryFlowShellImpl({
|
||||
clearSelectedPublicWorkAuthor();
|
||||
entryNavigation.backToPlatformHome();
|
||||
}}
|
||||
onLike={() => {
|
||||
likePublicWork(
|
||||
mapRpgGalleryCardToPublicWorkDetail(selectedDetailEntry),
|
||||
);
|
||||
}}
|
||||
onStart={handleStartSelectedWorld}
|
||||
onRemix={() => {
|
||||
remixPublicWork(
|
||||
@@ -3290,6 +3574,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
onCreateFromForm={(payload) => {
|
||||
void createPuzzleDraftFromForm(payload);
|
||||
}}
|
||||
onAutoSaveForm={(payload) => {
|
||||
void savePuzzleFormDraft(payload);
|
||||
}}
|
||||
/>
|
||||
</Suspense>
|
||||
</motion.div>
|
||||
@@ -3312,6 +3599,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
}
|
||||
anchorEntries={buildPuzzleGenerationAnchorEntries(
|
||||
puzzleSession,
|
||||
puzzleFormDraftPayload,
|
||||
)}
|
||||
progress={buildMiniGameDraftGenerationProgress(
|
||||
puzzleGenerationState,
|
||||
@@ -3344,7 +3632,9 @@ export function PlatformEntryFlowShellImpl({
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
{selectionStage === 'puzzle-result' && puzzleSession?.draft && (
|
||||
{selectionStage === 'puzzle-result' &&
|
||||
puzzleSession?.draft &&
|
||||
!isPuzzleFormOnlyDraft(puzzleSession) && (
|
||||
<motion.div
|
||||
key="puzzle-result"
|
||||
initial={{ opacity: 0, y: 12 }}
|
||||
@@ -3717,9 +4007,7 @@ export function PlatformEntryFlowShellImpl({
|
||||
setShowCreationTypeModal(false);
|
||||
}}
|
||||
onSelectRpg={() => {
|
||||
runProtectedAction(() => {
|
||||
void sessionController.openRpgAgentWorkspace();
|
||||
});
|
||||
// RPG 创作入口当前为敬请期待;保留回调防御,避免旧入口绕过锁定态。
|
||||
}}
|
||||
onSelectBigFish={() => {
|
||||
runProtectedAction(() => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { expect, test, vi } from 'vitest';
|
||||
|
||||
import type { PlatformPublicGalleryCard } from '../rpg-entry/rpgEntryWorldPresentation';
|
||||
@@ -29,13 +29,14 @@ function createPuzzleEntry(): PlatformPublicGalleryCard {
|
||||
};
|
||||
}
|
||||
|
||||
test('PlatformWorkDetailView renders compact stats and recent update time', () => {
|
||||
test('PlatformWorkDetailView renders compact stats and date time', () => {
|
||||
render(
|
||||
<PlatformWorkDetailView
|
||||
entry={createPuzzleEntry()}
|
||||
isBusy={false}
|
||||
error={null}
|
||||
onBack={vi.fn()}
|
||||
onLike={vi.fn()}
|
||||
onStart={vi.fn()}
|
||||
onRemix={vi.fn()}
|
||||
/>,
|
||||
@@ -43,12 +44,53 @@ test('PlatformWorkDetailView renders compact stats and recent update time', () =
|
||||
|
||||
expect(screen.getByText('改造')).toBeTruthy();
|
||||
expect(screen.getByText('游玩')).toBeTruthy();
|
||||
expect(screen.getByText('点赞')).toBeTruthy();
|
||||
expect(screen.getByText('最近更新')).toBeTruthy();
|
||||
expect(screen.getAllByText('点赞').length).toBeGreaterThanOrEqual(2);
|
||||
expect(screen.getByText('日期')).toBeTruthy();
|
||||
expect(screen.queryByText('改造次数')).toBeNull();
|
||||
expect(screen.queryByText('游玩次数')).toBeNull();
|
||||
expect(screen.queryByText('上线日期')).toBeNull();
|
||||
expect(screen.queryByText('最近更新')).toBeNull();
|
||||
expect(screen.getByText('2026-04-25')).toBeTruthy();
|
||||
expect(screen.getAllByText('次')).toHaveLength(2);
|
||||
expect(screen.getByText('赞')).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '点赞 4赞' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '作品改造' })).toBeTruthy();
|
||||
expect(screen.getByRole('button', { name: '启动' })).toBeTruthy();
|
||||
});
|
||||
|
||||
test('PlatformWorkDetailView prefers resolved public user display name', () => {
|
||||
render(
|
||||
<PlatformWorkDetailView
|
||||
entry={createPuzzleEntry()}
|
||||
authorDisplayName="新的作者昵称"
|
||||
isBusy={false}
|
||||
error={null}
|
||||
onBack={vi.fn()}
|
||||
onLike={vi.fn()}
|
||||
onStart={vi.fn()}
|
||||
onRemix={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(screen.getByText('新的作者昵称')).toBeTruthy();
|
||||
expect(screen.queryByText('137****6613')).toBeNull();
|
||||
});
|
||||
|
||||
test('PlatformWorkDetailView calls like handler', () => {
|
||||
const onLike = vi.fn();
|
||||
render(
|
||||
<PlatformWorkDetailView
|
||||
entry={createPuzzleEntry()}
|
||||
isBusy={false}
|
||||
error={null}
|
||||
onBack={vi.fn()}
|
||||
onLike={onLike}
|
||||
onStart={vi.fn()}
|
||||
onRemix={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole('button', { name: '点赞 4赞' }));
|
||||
|
||||
expect(onLike).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
@@ -27,9 +27,11 @@ import {
|
||||
export interface PlatformWorkDetailViewProps {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
authorAvatarUrl?: string | null;
|
||||
authorDisplayName?: string | null;
|
||||
isBusy: boolean;
|
||||
error: string | null;
|
||||
onBack: () => void;
|
||||
onLike: () => void;
|
||||
onStart: () => void;
|
||||
onRemix: () => void;
|
||||
}
|
||||
@@ -59,15 +61,19 @@ function getAuthorAvatarLabel(authorDisplayName: string) {
|
||||
export function PlatformWorkDetailView({
|
||||
entry,
|
||||
authorAvatarUrl,
|
||||
authorDisplayName,
|
||||
isBusy,
|
||||
error,
|
||||
onBack,
|
||||
onLike,
|
||||
onStart,
|
||||
onRemix,
|
||||
}: PlatformWorkDetailViewProps) {
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const publicWorkCode = resolvePlatformPublicWorkCode(entry);
|
||||
const normalizedAuthorAvatarUrl = authorAvatarUrl?.trim() ?? '';
|
||||
const resolvedAuthorDisplayName =
|
||||
authorDisplayName?.trim() || entry.authorDisplayName;
|
||||
const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
|
||||
'idle',
|
||||
);
|
||||
@@ -85,13 +91,6 @@ export function PlatformWorkDetailView({
|
||||
);
|
||||
const stats = resolvePlatformWorldStats(entry);
|
||||
const statItems = [
|
||||
{
|
||||
label: '改造',
|
||||
value: formatCompactCount(stats.remixCount),
|
||||
unit: '次',
|
||||
icon: GitFork,
|
||||
tone: 'remix',
|
||||
},
|
||||
{
|
||||
label: '游玩',
|
||||
value: formatCompactCount(stats.playCount),
|
||||
@@ -99,6 +98,13 @@ export function PlatformWorkDetailView({
|
||||
icon: Gamepad2,
|
||||
tone: 'play',
|
||||
},
|
||||
{
|
||||
label: '改造',
|
||||
value: formatCompactCount(stats.remixCount),
|
||||
unit: '次',
|
||||
icon: GitFork,
|
||||
tone: 'remix',
|
||||
},
|
||||
{
|
||||
label: '点赞',
|
||||
value: formatCompactCount(stats.likeCount),
|
||||
@@ -107,7 +113,7 @@ export function PlatformWorkDetailView({
|
||||
tone: 'like',
|
||||
},
|
||||
{
|
||||
label: '最近更新',
|
||||
label: '日期',
|
||||
value: formatPlatformWorldTime(stats.updatedAt ?? stats.publishedAt),
|
||||
icon: Clock3,
|
||||
tone: 'time',
|
||||
@@ -199,9 +205,7 @@ export function PlatformWorkDetailView({
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="platform-work-detail__name">
|
||||
{displayName}
|
||||
</div>
|
||||
<div className="platform-work-detail__name">{displayName}</div>
|
||||
<div className="platform-work-detail__author">
|
||||
<span className="platform-work-detail__author-avatar">
|
||||
{normalizedAuthorAvatarUrl ? (
|
||||
@@ -213,23 +217,25 @@ export function PlatformWorkDetailView({
|
||||
/>
|
||||
) : (
|
||||
<span className="platform-work-detail__author-avatar-label">
|
||||
{getAuthorAvatarLabel(entry.authorDisplayName)}
|
||||
{getAuthorAvatarLabel(resolvedAuthorDisplayName)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="platform-work-detail__author-name">
|
||||
{entry.authorDisplayName}
|
||||
{resolvedAuthorDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__remix"
|
||||
onClick={onRemix}
|
||||
className="platform-work-detail__like"
|
||||
onClick={onLike}
|
||||
disabled={isBusy}
|
||||
aria-label={`点赞 ${formatCompactCount(stats.likeCount)}赞`}
|
||||
title="点赞"
|
||||
>
|
||||
<GitFork className="h-5 w-5" />
|
||||
作品改造
|
||||
<Heart className="h-5 w-5 fill-current" />
|
||||
点赞
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -300,6 +306,15 @@ export function PlatformWorkDetailView({
|
||||
</div>
|
||||
|
||||
<div className="platform-work-detail__bottom">
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__remix"
|
||||
onClick={onRemix}
|
||||
disabled={isBusy}
|
||||
>
|
||||
<GitFork className="h-5 w-5" />
|
||||
作品改造
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="platform-work-detail__start"
|
||||
|
||||
@@ -37,9 +37,9 @@ export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
|
||||
{
|
||||
id: 'rpg',
|
||||
title: '角色扮演',
|
||||
subtitle: '剧情演绎,冒险成长',
|
||||
badge: '可创建',
|
||||
locked: false,
|
||||
subtitle: '敬请期待',
|
||||
badge: '敬请期待',
|
||||
locked: true,
|
||||
},
|
||||
{
|
||||
id: 'big-fish',
|
||||
|
||||
Reference in New Issue
Block a user