This commit is contained in:
2026-05-08 11:44:42 +08:00
parent b08127031c
commit abf1f1ebea
249 changed files with 39411 additions and 887 deletions
File diff suppressed because it is too large Load Diff
@@ -939,6 +939,23 @@ test('shows a reachable login entry in logged out mobile shell', async () => {
expect(openLoginModal).toHaveBeenCalledTimes(1);
});
test('logged out bottom nav keeps creation centered with recommend icon', () => {
const { container } = renderLoggedOutHomeView(vi.fn());
const nav = container.querySelector('.platform-bottom-nav');
expect(nav).toBeTruthy();
const buttons = within(nav as HTMLElement).getAllByRole('button');
expect(buttons.map((button) => button.textContent)).toEqual([
'推荐',
'创作',
'发现',
]);
expect(buttons[0]?.querySelector('.lucide-gamepad-2')).toBeTruthy();
expect(buttons[1]?.querySelector('.lucide-sparkles')).toBeTruthy();
expect(buttons[2]?.querySelector('.lucide-compass')).toBeTruthy();
});
test('mobile home search submits public work code', async () => {
const user = userEvent.setup();
const onSearchPublicCode = vi.fn();
File diff suppressed because it is too large Load Diff
@@ -9,6 +9,9 @@ import type { CustomWorldProfile } from '../../types';
export function resolveRpgEntryErrorMessage(error: unknown, fallback: string) {
if (isTimeoutError(error)) {
if (/智能创作/u.test(fallback)) {
return '开启智能创作工作区超时,请确认运行时后端已启动后重试。';
}
if (/拼图/u.test(fallback)) {
return '开启拼图创作工作台超时,请确认运行时后端已启动后重试。';
}
@@ -2,9 +2,13 @@ import { expect, test } from 'vitest';
import {
buildPuzzleWorkCoverSlides,
buildPlatformWorldDisplayTags,
formatPlatformWorkDisplayName,
formatPlatformWorkDisplayTags,
formatPlatformWorldTime,
isVisualNovelGalleryEntry,
mapVisualNovelWorkToPlatformGalleryCard,
resolvePlatformPublicWorkCode,
} from './rpgEntryWorldPresentation';
test('formatPlatformWorldTime formats backend seconds timestamp text as date', () => {
@@ -106,3 +110,25 @@ test('buildPuzzleWorkCoverSlides prefers each level formal image', () => {
},
]);
});
test('maps visual novel work to platform gallery card with VN public code', () => {
const card = mapVisualNovelWorkToPlatformGalleryCard({
runtimeKind: 'visual-novel',
profileId: 'vn-profile-demo-12345678',
ownerUserId: 'user-1',
title: '雨夜终章',
description: '失踪列车上的选择。',
coverImageSrc: '/vn-cover.png',
tags: ['悬疑', '列车'],
publishStatus: 'published',
publishReady: true,
playCount: 7,
updatedAt: '2026-05-07T00:00:00.000Z',
publishedAt: '2026-05-07T00:00:00.000Z',
});
expect(isVisualNovelGalleryEntry(card)).toBe(true);
expect(card.publicWorkCode).toBe('VN-12345678');
expect(resolvePlatformPublicWorkCode(card)).toBe('VN-12345678');
expect(buildPlatformWorldDisplayTags(card, 2)).toEqual(['悬疑', '列车']);
});
@@ -2,6 +2,7 @@ import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleDraftLevel } from '../../../packages/shared/src/contracts/puzzleAgentDraft';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
import type {
CustomWorldGalleryCard,
CustomWorldLibraryEntry,
@@ -18,6 +19,7 @@ import {
buildMatch3DPublicWorkCode,
buildPuzzlePublicWorkCode,
buildSquareHolePublicWorkCode,
buildVisualNovelPublicWorkCode,
} from '../../services/publicWorkCode';
import type { CustomWorldProfile } from '../../types';
@@ -30,7 +32,8 @@ export type PlatformWorldCardLike =
| PlatformBigFishGalleryCard
| PlatformMatch3DGalleryCard
| PlatformSquareHoleGalleryCard
| PlatformPuzzleGalleryCard;
| PlatformPuzzleGalleryCard
| PlatformVisualNovelGalleryCard;
export type PlatformPuzzleGalleryCard = {
sourceType: 'puzzle';
@@ -132,12 +135,35 @@ export type PlatformSquareHoleGalleryCard = {
updatedAt: string;
};
export type PlatformVisualNovelGalleryCard = {
sourceType: 'visual-novel';
workId: string;
profileId: string;
sourceSessionId?: string | null;
publicWorkCode: string;
ownerUserId: string;
authorDisplayName: string;
worldName: string;
subtitle: string;
summaryText: string;
coverImageSrc: string | null;
themeTags: string[];
playCount?: number;
remixCount?: number;
likeCount?: number;
recentPlayCount7d?: number;
visibility: 'published';
publishedAt: string | null;
updatedAt: string;
};
export type PlatformPublicGalleryCard =
| CustomWorldGalleryCard
| PlatformBigFishGalleryCard
| PlatformMatch3DGalleryCard
| PlatformSquareHoleGalleryCard
| PlatformPuzzleGalleryCard;
| PlatformPuzzleGalleryCard
| PlatformVisualNovelGalleryCard;
export function isLibraryWorldEntry(
entry: PlatformWorldCardLike,
@@ -169,6 +195,12 @@ export function isSquareHoleGalleryEntry(
return 'sourceType' in entry && entry.sourceType === 'square-hole';
}
export function isVisualNovelGalleryEntry(
entry: PlatformWorldCardLike,
): entry is PlatformVisualNovelGalleryCard {
return 'sourceType' in entry && entry.sourceType === 'visual-novel';
}
export function mapPuzzleWorkToPlatformGalleryCard(
work: PuzzleWorkSummary,
): PlatformPuzzleGalleryCard {
@@ -280,6 +312,32 @@ export function mapBigFishWorkToPlatformGalleryCard(
};
}
export function mapVisualNovelWorkToPlatformGalleryCard(
work: VisualNovelWorkSummary,
): PlatformVisualNovelGalleryCard {
return {
sourceType: 'visual-novel',
workId: work.profileId,
profileId: work.profileId,
sourceSessionId: null,
publicWorkCode: buildVisualNovelPublicWorkCode(work.profileId),
ownerUserId: work.ownerUserId,
authorDisplayName: '玩家',
worldName: work.title,
subtitle: '视觉小说模板',
summaryText: work.description,
coverImageSrc: work.coverImageSrc,
themeTags: work.tags.length > 0 ? work.tags : ['视觉小说'],
playCount: work.playCount ?? 0,
remixCount: 0,
likeCount: 0,
recentPlayCount7d: 0,
visibility: 'published',
publishedAt: work.publishedAt,
updatedAt: work.updatedAt,
};
}
export function resolvePlatformWorldStats(entry: PlatformWorldCardLike) {
return {
playCount: 'playCount' in entry ? (entry.playCount ?? 0) : 0,
@@ -452,6 +510,12 @@ export function buildPlatformWorldTags(entry: PlatformWorldCardLike) {
: ['方洞'];
}
if (isVisualNovelGalleryEntry(entry)) {
return entry.themeTags.length > 0
? entry.themeTags.slice(0, 3)
: ['视觉小说'];
}
if (!isLibraryWorldEntry(entry)) {
return [
describePlatformThemeLabel(entry.themeMode),
@@ -534,6 +598,10 @@ export function resolvePlatformPublicWorkCode(
return entry.publicWorkCode;
}
if (isVisualNovelGalleryEntry(entry)) {
return entry.publicWorkCode;
}
return entry.publicWorkCode;
}
@@ -6,12 +6,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
import type { CustomWorldAgentSessionSnapshot } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldWorkSummary';
import type { RpgCreationResultView } from '../../../packages/shared/src/contracts/rpgCreationResultView';
import { WorldType, type CustomWorldProfile } from '../../types';
import {
executeRpgCreationAction,
getRpgCreationOperation,
upsertRpgWorldProfile,
} from '../../services/rpg-creation';
import { type CustomWorldProfile,WorldType } from '../../types';
import { useRpgCreationResultAutosave } from './useRpgCreationResultAutosave';
import { useRpgEntryLibraryDetail } from './useRpgEntryLibraryDetail';
@@ -173,6 +173,7 @@ describe('RPG Agent 草稿恢复', () => {
setCustomWorldResultViewSource,
setSelectionStage,
setPlatformTabToCreate: vi.fn(),
setPlatformTabToDraft: vi.fn(),
setPlatformError: vi.fn(),
appendBrowseHistoryEntry: vi.fn(async () => {}),
refreshCustomWorldWorks: vi.fn(async () => []),
@@ -329,14 +329,8 @@ export function useRpgEntryBootstrap(
!hasInitialAgentSession &&
!hasExplicitPlatformTabSelectionRef.current
) {
setPlatformTabState(
isAuthenticated &&
canReadProtectedData &&
saveArchivesResult.status === 'fulfilled' &&
saveArchivesResult.value.length > 0
? 'saves'
: 'home',
);
// 中文注释:saves 现在承载草稿列表,存档入口已并入“我的-玩过”,默认仍回到推荐页。
setPlatformTabState('home');
}
} finally {
if (isActive) {
@@ -54,6 +54,7 @@ type UseRpgEntryLibraryDetailParams = {
setCustomWorldResultViewSource: (source: CustomWorldResultViewSource) => void;
setSelectionStage: (stage: SelectionStage) => void;
setPlatformTabToCreate: () => void;
setPlatformTabToDraft: () => void;
setPlatformError: (error: string | null) => void;
appendBrowseHistoryEntry: (
entry: PlatformBrowseHistoryWriteEntry,
@@ -106,6 +107,7 @@ export function useRpgEntryLibraryDetail(
setCustomWorldResultViewSource,
setSelectionStage,
setPlatformTabToCreate,
setPlatformTabToDraft,
setPlatformError,
appendBrowseHistoryEntry,
refreshCustomWorldWorks,
@@ -347,7 +349,7 @@ export function useRpgEntryLibraryDetail(
setCustomWorldResultViewSource(null);
await refreshCustomWorldWorks().catch(() => []);
setPlatformError(
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
);
} else {
setPlatformError(
@@ -355,7 +357,7 @@ export function useRpgEntryLibraryDetail(
);
}
setPlatformTabToCreate();
setPlatformTabToDraft();
setSelectionStage('platform');
return;
}
@@ -405,6 +407,7 @@ export function useRpgEntryLibraryDetail(
setGeneratedCustomWorldProfile,
setPlatformError,
setPlatformTabToCreate,
setPlatformTabToDraft,
setSavedCustomWorldEntries,
setSelectionStage,
suppressAgentDraftResultAutoOpen,