1
This commit is contained in:
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,
|
||||
|
||||
Reference in New Issue
Block a user