This commit is contained in:
2026-04-30 17:49:07 +08:00
parent 805d6f8cae
commit 9d684cb7b3
615 changed files with 15368 additions and 6172 deletions

View File

@@ -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(() => {

View File

@@ -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);
});

View File

@@ -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"

View File

@@ -37,9 +37,9 @@ export const PLATFORM_CREATION_TYPES: PlatformCreationTypeCard[] = [
{
id: 'rpg',
title: '角色扮演',
subtitle: '剧情演绎,冒险成长',
badge: '可创建',
locked: false,
subtitle: '敬请期待',
badge: '敬请期待',
locked: true,
},
{
id: 'big-fish',