1
This commit is contained in:
@@ -6,6 +6,7 @@ import { useState } from 'react';
|
||||
import { beforeEach, expect, test, vi } from 'vitest';
|
||||
|
||||
import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
|
||||
import type { CreativeAgentSessionSnapshot } from '../../../packages/shared/src/contracts/creativeAgent';
|
||||
import type {
|
||||
CustomWorldAgentSessionSnapshot,
|
||||
CustomWorldWorkSummary,
|
||||
@@ -43,6 +44,13 @@ import {
|
||||
submitBigFishInput,
|
||||
} from '../../services/big-fish-runtime';
|
||||
import { listBigFishWorks } from '../../services/big-fish-works';
|
||||
import {
|
||||
cancelCreativeAgentSession,
|
||||
confirmCreativePuzzleTemplate,
|
||||
createCreativeAgentSession,
|
||||
streamCreativeAgentMessage,
|
||||
streamCreativeDraftEdit,
|
||||
} from '../../services/creative-agent';
|
||||
import { match3dCreationClient } from '../../services/match3d-creation';
|
||||
import {
|
||||
clickMatch3DItem,
|
||||
@@ -109,6 +117,20 @@ import {
|
||||
recordRpgEntryWorldGalleryPlay,
|
||||
remixRpgEntryWorldGallery,
|
||||
} from '../../services/rpg-entry/rpgEntryLibraryClient';
|
||||
import { squareHoleCreationClient } from '../../services/square-hole-creation';
|
||||
import {
|
||||
dropSquareHoleShape,
|
||||
finishSquareHoleTimeUp,
|
||||
restartSquareHoleRun,
|
||||
startSquareHoleRun,
|
||||
stopSquareHoleRun,
|
||||
} from '../../services/square-hole-runtime';
|
||||
import {
|
||||
deleteSquareHoleWork,
|
||||
getSquareHoleWorkDetail,
|
||||
listSquareHoleGallery,
|
||||
listSquareHoleWorks,
|
||||
} from '../../services/square-hole-works';
|
||||
import { type CustomWorldProfile, WorldType } from '../../types';
|
||||
import {
|
||||
AuthUiContext,
|
||||
@@ -136,18 +158,51 @@ async function clickFirstAsyncButtonByName(
|
||||
await user.click(buttons[0]!);
|
||||
}
|
||||
|
||||
async function openCreationHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
async function openCreateTemplateHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /拼图.*创意礼物/u }),
|
||||
await screen.findByText('10分钟创作一个精品互动玩法'),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByRole('tab', { name: '拼图' })).toBeTruthy();
|
||||
expect(screen.getByText('拼图工作区:missing-session')).toBeTruthy();
|
||||
}
|
||||
|
||||
async function openDraftHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '草稿');
|
||||
const panel = getPlatformTabPanel('saves');
|
||||
await waitFor(() => {
|
||||
expect(panel.getAttribute('aria-hidden')).toBe('false');
|
||||
});
|
||||
expect(
|
||||
await within(panel).findByRole('button', { name: /全部/u }),
|
||||
).toBeTruthy();
|
||||
}
|
||||
|
||||
async function openDiscoverHub(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '发现');
|
||||
const panel = getPlatformTabPanel('category');
|
||||
await waitFor(() => {
|
||||
expect(panel.getAttribute('aria-hidden')).toBe('false');
|
||||
});
|
||||
expect(
|
||||
await within(panel).findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
),
|
||||
).toBeTruthy();
|
||||
return panel;
|
||||
}
|
||||
|
||||
async function openProfilePlayedWorks(user: ReturnType<typeof userEvent.setup>) {
|
||||
await clickFirstButtonByName(user, '我的');
|
||||
await user.click(await screen.findByRole('button', { name: /玩过/u }));
|
||||
expect(await screen.findByText('可继续')).toBeTruthy();
|
||||
}
|
||||
|
||||
async function openExistingRpgDraft(
|
||||
user: ReturnType<typeof userEvent.setup>,
|
||||
actionName: string | RegExp = /继续(?:完善|创作)/u,
|
||||
) {
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: actionName }));
|
||||
}
|
||||
|
||||
@@ -289,6 +344,40 @@ vi.mock('../../services/match3d-runtime', () => ({
|
||||
stopMatch3DRun: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/square-hole-creation', () => ({
|
||||
squareHoleCreationClient: {
|
||||
createSession: vi.fn(),
|
||||
executeAction: vi.fn(),
|
||||
getSession: vi.fn(),
|
||||
sendMessage: vi.fn(),
|
||||
streamMessage: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../services/square-hole-runtime', () => ({
|
||||
dropSquareHoleShape: vi.fn(),
|
||||
finishSquareHoleTimeUp: vi.fn(),
|
||||
getSquareHoleRun: vi.fn(),
|
||||
restartSquareHoleRun: vi.fn(),
|
||||
startSquareHoleRun: vi.fn(),
|
||||
stopSquareHoleRun: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/square-hole-works', () => ({
|
||||
deleteSquareHoleWork: vi.fn(),
|
||||
getSquareHoleWorkDetail: vi.fn(),
|
||||
listSquareHoleGallery: vi.fn(),
|
||||
listSquareHoleWorks: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/creative-agent', () => ({
|
||||
cancelCreativeAgentSession: vi.fn(),
|
||||
confirmCreativePuzzleTemplate: vi.fn(),
|
||||
createCreativeAgentSession: vi.fn(),
|
||||
streamCreativeAgentMessage: vi.fn(),
|
||||
streamCreativeDraftEdit: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../services/puzzle-runtime/puzzleLocalRuntime', async () => {
|
||||
const actual = await vi.importActual<
|
||||
typeof import('../../services/puzzle-runtime/puzzleLocalRuntime')
|
||||
@@ -573,6 +662,168 @@ const mockAuthUser: AuthUser = {
|
||||
createdAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
function buildMockCreativeAgentSession(
|
||||
overrides: Partial<CreativeAgentSessionSnapshot> = {},
|
||||
): CreativeAgentSessionSnapshot {
|
||||
const sessionId = overrides.sessionId ?? 'creative-agent-session-1';
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
stage: 'waiting_user',
|
||||
inputSummary: {
|
||||
text: null,
|
||||
entryContext: 'creation_home',
|
||||
images: [],
|
||||
materialSummary: null,
|
||||
unsupportedCapabilities: [],
|
||||
},
|
||||
messages: [
|
||||
{
|
||||
id: 'creative-agent-message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '说一个灵感,我来帮你做成互动内容。',
|
||||
createdAt: '2026-05-05T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
puzzleTemplateCatalog: [],
|
||||
puzzleTemplateSelection: null,
|
||||
puzzleImageGenerationPlan: null,
|
||||
targetBinding: null,
|
||||
updatedAt: '2026-05-05T10:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockSquareHoleAgentSession(
|
||||
overrides: Partial<Parameters<typeof buildMockSquareHoleAgentSessionImpl>[0]> = {},
|
||||
) {
|
||||
return buildMockSquareHoleAgentSessionImpl(overrides);
|
||||
}
|
||||
|
||||
function buildMockSquareHoleAgentSessionImpl(
|
||||
overrides: Partial<{
|
||||
sessionId: string;
|
||||
stage: string;
|
||||
messages: Array<{ id: string; role: string; kind: string; text: string; createdAt: string }>;
|
||||
updatedAt: string;
|
||||
}> = {},
|
||||
) {
|
||||
const sessionId = overrides.sessionId ?? 'square-hole-session-1';
|
||||
|
||||
return {
|
||||
sessionId,
|
||||
currentTurn: 0,
|
||||
progressPercent: 20,
|
||||
stage: 'collecting_config',
|
||||
anchorPack: {
|
||||
theme: {
|
||||
key: 'theme',
|
||||
label: '题材主题',
|
||||
value: '霓虹形状',
|
||||
status: 'confirmed',
|
||||
},
|
||||
twistRule: {
|
||||
key: 'twistRule',
|
||||
label: '反直觉规则',
|
||||
value: '颜色会误导洞口',
|
||||
status: 'confirmed',
|
||||
},
|
||||
shapeCount: {
|
||||
key: 'shapeCount',
|
||||
label: '形状数量',
|
||||
value: '12',
|
||||
status: 'confirmed',
|
||||
},
|
||||
difficulty: {
|
||||
key: 'difficulty',
|
||||
label: '难度',
|
||||
value: '5',
|
||||
status: 'confirmed',
|
||||
},
|
||||
},
|
||||
config: {
|
||||
themeText: '霓虹形状',
|
||||
twistRule: '颜色会误导洞口',
|
||||
shapeCount: 12,
|
||||
difficulty: 5,
|
||||
shapeOptions: [
|
||||
{
|
||||
optionId: 'shape-square',
|
||||
shapeKind: 'square',
|
||||
label: '方块',
|
||||
targetHoleId: 'hole-square',
|
||||
imagePrompt: '霓虹方块',
|
||||
imageSrc: null,
|
||||
},
|
||||
],
|
||||
holeOptions: [
|
||||
{
|
||||
holeId: 'hole-square',
|
||||
holeKind: 'square',
|
||||
label: '方洞',
|
||||
imagePrompt: '发光方洞',
|
||||
imageSrc: null,
|
||||
},
|
||||
],
|
||||
backgroundPrompt: '霓虹街机背景',
|
||||
coverImageSrc: null,
|
||||
backgroundImageSrc: null,
|
||||
},
|
||||
draft: null,
|
||||
messages: [
|
||||
{
|
||||
id: 'square-hole-message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '先确定方洞挑战的题材和反直觉规则。',
|
||||
createdAt: '2026-05-01T10:00:00.000Z',
|
||||
},
|
||||
],
|
||||
lastAssistantReply: '先确定方洞挑战的题材和反直觉规则。',
|
||||
publishedProfileId: null,
|
||||
updatedAt: '2026-05-01T10:00:00.000Z',
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockSquareHoleRun(profileId: string) {
|
||||
return {
|
||||
runId: `square-hole-run-${profileId}`,
|
||||
profileId,
|
||||
ownerUserId: 'user-2',
|
||||
status: 'running',
|
||||
snapshotVersion: 1,
|
||||
startedAtMs: 1_000,
|
||||
durationLimitMs: 600_000,
|
||||
remainingMs: 600_000,
|
||||
totalShapeCount: 12,
|
||||
completedShapeCount: 0,
|
||||
combo: 0,
|
||||
bestCombo: 0,
|
||||
score: 0,
|
||||
ruleLabel: '颜色会误导洞口',
|
||||
currentShape: {
|
||||
shapeId: 'shape-1',
|
||||
shapeKind: 'square',
|
||||
label: '方块',
|
||||
targetHoleId: 'hole-square',
|
||||
color: '#ff5f7e',
|
||||
imageSrc: null,
|
||||
},
|
||||
holes: [
|
||||
{
|
||||
holeId: 'hole-square',
|
||||
holeKind: 'square',
|
||||
label: '方洞',
|
||||
x: 0.2,
|
||||
y: 0.5,
|
||||
},
|
||||
],
|
||||
lastFeedback: null,
|
||||
};
|
||||
}
|
||||
|
||||
function buildMockPuzzleRun(
|
||||
profileId: string,
|
||||
levelName: string,
|
||||
@@ -1779,6 +2030,50 @@ beforeEach(() => {
|
||||
vi.mocked(stopMatch3DRun).mockResolvedValue({
|
||||
run: buildMockMatch3DRun('match3d-profile-stopped'),
|
||||
});
|
||||
vi.mocked(squareHoleCreationClient.createSession).mockResolvedValue({
|
||||
session: buildMockSquareHoleAgentSession(),
|
||||
});
|
||||
vi.mocked(squareHoleCreationClient.getSession).mockResolvedValue({
|
||||
session: buildMockSquareHoleAgentSession(),
|
||||
});
|
||||
vi.mocked(squareHoleCreationClient.streamMessage).mockResolvedValue(
|
||||
buildMockSquareHoleAgentSession(),
|
||||
);
|
||||
vi.mocked(squareHoleCreationClient.executeAction).mockResolvedValue({
|
||||
session: buildMockSquareHoleAgentSession(),
|
||||
});
|
||||
vi.mocked(listSquareHoleWorks).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
vi.mocked(listSquareHoleGallery).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
vi.mocked(getSquareHoleWorkDetail).mockRejectedValue(
|
||||
new Error('未找到方洞挑战作品'),
|
||||
);
|
||||
vi.mocked(deleteSquareHoleWork).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
vi.mocked(startSquareHoleRun).mockResolvedValue({
|
||||
run: buildMockSquareHoleRun('square-hole-profile-1'),
|
||||
});
|
||||
vi.mocked(dropSquareHoleShape).mockResolvedValue({
|
||||
feedback: {
|
||||
accepted: true,
|
||||
rejectReason: null,
|
||||
message: '投入成功',
|
||||
},
|
||||
run: buildMockSquareHoleRun('square-hole-profile-1'),
|
||||
});
|
||||
vi.mocked(restartSquareHoleRun).mockResolvedValue({
|
||||
run: buildMockSquareHoleRun('square-hole-profile-1'),
|
||||
});
|
||||
vi.mocked(finishSquareHoleTimeUp).mockResolvedValue({
|
||||
run: buildMockSquareHoleRun('square-hole-profile-1'),
|
||||
});
|
||||
vi.mocked(stopSquareHoleRun).mockResolvedValue({
|
||||
run: buildMockSquareHoleRun('square-hole-profile-1'),
|
||||
});
|
||||
vi.mocked(listPuzzleWorks).mockResolvedValue({
|
||||
items: [],
|
||||
});
|
||||
@@ -1871,44 +2166,119 @@ beforeEach(() => {
|
||||
});
|
||||
vi.mocked(getRpgCreationSession).mockResolvedValue(mockSession);
|
||||
vi.mocked(streamRpgCreationMessage).mockResolvedValue(mockSession);
|
||||
vi.mocked(createCreativeAgentSession).mockResolvedValue({
|
||||
session: buildMockCreativeAgentSession(),
|
||||
});
|
||||
vi.mocked(streamCreativeAgentMessage).mockImplementation(
|
||||
async (sessionId, payload) =>
|
||||
buildMockCreativeAgentSession({
|
||||
sessionId,
|
||||
stage: 'collaborating',
|
||||
messages: [
|
||||
{
|
||||
id: 'creative-agent-message-1',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '说一个灵感,我来帮你做成互动内容。',
|
||||
createdAt: '2026-05-05T10:00:00.000Z',
|
||||
},
|
||||
{
|
||||
id: payload.clientMessageId,
|
||||
role: 'user',
|
||||
kind: 'chat',
|
||||
text: payload.content
|
||||
.map((part) =>
|
||||
part.type === 'input_text' ? part.text.trim() : '参考图',
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join(' / '),
|
||||
createdAt: '2026-05-05T10:01:00.000Z',
|
||||
},
|
||||
{
|
||||
id: 'creative-agent-message-2',
|
||||
role: 'assistant',
|
||||
kind: 'chat',
|
||||
text: '收到,我先帮你整理成可创作方案。',
|
||||
createdAt: '2026-05-05T10:01:01.000Z',
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
vi.mocked(cancelCreativeAgentSession).mockResolvedValue({
|
||||
session: buildMockCreativeAgentSession({ stage: 'failed' }),
|
||||
});
|
||||
vi.mocked(confirmCreativePuzzleTemplate).mockResolvedValue({
|
||||
session: buildMockCreativeAgentSession(),
|
||||
});
|
||||
vi.mocked(streamCreativeDraftEdit).mockResolvedValue(
|
||||
buildMockCreativeAgentSession(),
|
||||
);
|
||||
});
|
||||
|
||||
test('create hub hides RPG while keeping Match3D open and future templates locked', async () => {
|
||||
test('create tab shows template tabs and embeds puzzle form by default', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openCreateTemplateHub(user);
|
||||
|
||||
const match3dButton = screen.getByRole('button', {
|
||||
name: /抓大鹅.*经典消除玩法/u,
|
||||
});
|
||||
const airpButton = screen.getByRole('button', { name: /AIRP/u });
|
||||
const visualNovelButton = screen.getByRole('button', {
|
||||
name: /视觉小说/u,
|
||||
});
|
||||
|
||||
expect((match3dButton as HTMLButtonElement).disabled).toBe(false);
|
||||
expect((airpButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect((visualNovelButton as HTMLButtonElement).disabled).toBe(true);
|
||||
expect(screen.getByRole('tablist', { name: '选择模板' })).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).getAttribute('aria-selected'),
|
||||
).toBe('true');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/puzzle.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '方洞挑战' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/square-hole.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '视觉小说' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/visual-novel.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'AIRP' }).querySelector('img')?.src,
|
||||
).toContain('/creation-type-references/airp.webp');
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-white'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.getByRole('tab', { name: '拼图' }).querySelector('.text-inherit'),
|
||||
).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /智能创作/u })).toBeNull();
|
||||
expect(screen.queryByPlaceholderText('问一问百梦')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /角色扮演/u })).toBeNull();
|
||||
expect(screen.queryByRole('tab', { name: /抓大鹅/u })).toBeNull();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('platform create hub does not prefetch hidden big fish platform data', async () => {
|
||||
test('embedded puzzle form routes through requireAuth while logged out', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
openLoginModal: () => {},
|
||||
requireAuth,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openCreateTemplateHub(user);
|
||||
const generateButton = await screen.findByRole('button', {
|
||||
name: /生成草稿/u,
|
||||
});
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /拼图.*创意礼物/u }),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||||
expect(listBigFishWorks).not.toHaveBeenCalled();
|
||||
expect(listBigFishGallery).not.toHaveBeenCalled();
|
||||
await user.click(generateButton);
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(createCreativeAgentSession).not.toHaveBeenCalled();
|
||||
expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('opening RPG agent workspace does not refetch session snapshot in a render loop', async () => {
|
||||
@@ -2002,7 +2372,7 @@ test('create tab opens compiled agent draft in result refinement page', async ()
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
|
||||
|
||||
@@ -2056,7 +2426,7 @@ test('create tab resumes agent workspace when draft has no compiled result yet',
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();
|
||||
|
||||
@@ -2112,7 +2482,7 @@ test('create tab resumes agent workspace when session has no draft profile even
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续完善/u }));
|
||||
|
||||
@@ -2122,7 +2492,7 @@ test('create tab resumes agent workspace when session has no draft profile even
|
||||
expect(screen.queryByText('世界档案')).toBeNull();
|
||||
});
|
||||
|
||||
test('opening a compiled draft with a missing agent session falls back to create hub', async () => {
|
||||
test('opening a compiled draft with a missing agent session falls back to draft hub', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listRpgCreationWorks)
|
||||
@@ -2166,20 +2536,22 @@ test('opening a compiled draft with a missing agent session falls back to create
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /继续完善/u }));
|
||||
|
||||
const fallbackDraftPanel = getPlatformTabPanel('saves');
|
||||
await waitFor(() => {
|
||||
expect(fallbackDraftPanel.getAttribute('aria-hidden')).toBe('false');
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getByText(
|
||||
'这份共创草稿已失效,已为你返回创作中心,请重新开始创作。',
|
||||
within(fallbackDraftPanel).getByText(
|
||||
'这份共创草稿已失效,已为你返回草稿列表,请重新开始创作。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(window.location.search).toBe('');
|
||||
expect(listRpgCreationWorks).toHaveBeenCalledTimes(2);
|
||||
expect(screen.getByText('还没有作品')).toBeTruthy();
|
||||
expect(within(fallbackDraftPanel).getByText('还没有作品')).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('Agent工作区:custom-world-agent-session-missing'),
|
||||
).toBeNull();
|
||||
@@ -2374,6 +2746,7 @@ test('logged out public detail gates big fish start before local runtime', async
|
||||
/>,
|
||||
);
|
||||
|
||||
await openDiscoverHub(user);
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
);
|
||||
@@ -2469,23 +2842,22 @@ test('creation hub clears all private work shelves immediately after logout stat
|
||||
|
||||
const { rerender } = render(<TestWrapper authValue={loggedInAuth} />);
|
||||
|
||||
await openCreationHub(user);
|
||||
const createPanel = getPlatformTabPanel('create');
|
||||
await openDraftHub(user);
|
||||
const draftPanel = getPlatformTabPanel('saves');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(listRpgCreationWorks).toHaveBeenCalled();
|
||||
});
|
||||
expect(await within(createPanel).findByText('拼图退出缓存作品')).toBeTruthy();
|
||||
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
|
||||
expect(within(createPanel).queryByText('大鱼退出缓存作品')).toBeNull();
|
||||
expect(await within(draftPanel).findByText('拼图退出缓存作品')).toBeTruthy();
|
||||
expect(within(draftPanel).queryByText('RPG 退出缓存作品')).toBeNull();
|
||||
expect(within(draftPanel).queryByText('大鱼退出缓存作品')).toBeNull();
|
||||
|
||||
rerender(<TestWrapper authValue={loggedOutAuth} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(within(createPanel).queryByText('RPG 退出缓存作品')).toBeNull();
|
||||
expect(within(createPanel).queryByText('拼图退出缓存作品')).toBeNull();
|
||||
expect(screen.queryByText('RPG 退出缓存作品')).toBeNull();
|
||||
expect(screen.queryByText('拼图退出缓存作品')).toBeNull();
|
||||
});
|
||||
expect(within(createPanel).getByText('还没有作品')).toBeTruthy();
|
||||
});
|
||||
|
||||
test('published puzzle works appear on home and mobile game category channel', async () => {
|
||||
@@ -2522,12 +2894,15 @@ test('published puzzle works appear on home and mobile game category channel', a
|
||||
expect(screen.getAllByText('星桥机关').length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '游戏分类' }));
|
||||
await clickFirstButtonByName(user, '发现');
|
||||
await user.click(screen.getByRole('button', { name: '分类' }));
|
||||
|
||||
const homePanel = getPlatformTabPanel('home');
|
||||
expect(within(homePanel).getAllByText('星桥机关').length).toBeGreaterThan(0);
|
||||
const discoverPanel = getPlatformTabPanel('category');
|
||||
expect(
|
||||
within(homePanel).getAllByRole('button', { name: /机关/u }).length,
|
||||
within(discoverPanel).getAllByText('星桥机关').length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(
|
||||
within(discoverPanel).getAllByRole('button', { name: /机关/u }).length,
|
||||
).toBeGreaterThan(0);
|
||||
expect(screen.queryByRole('button', { name: 'PC游戏' })).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: '即点即玩' })).toBeNull();
|
||||
@@ -2564,12 +2939,13 @@ test('published big fish works stay hidden from platform home and game category
|
||||
});
|
||||
expect(screen.queryByText('机械深海 大鱼吃小鱼')).toBeNull();
|
||||
|
||||
await user.click(screen.getByRole('button', { name: '游戏分类' }));
|
||||
await clickFirstButtonByName(user, '发现');
|
||||
await user.click(screen.getByRole('button', { name: '分类' }));
|
||||
|
||||
const homePanel = getPlatformTabPanel('home');
|
||||
expect(within(homePanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
|
||||
const discoverPanel = getPlatformTabPanel('category');
|
||||
expect(within(discoverPanel).queryByText('机械深海 大鱼吃小鱼')).toBeNull();
|
||||
expect(
|
||||
within(homePanel).queryAllByRole('button', { name: /大鱼/u }).length,
|
||||
within(discoverPanel).queryAllByRole('button', { name: /大鱼/u }).length,
|
||||
).toBe(0);
|
||||
});
|
||||
|
||||
@@ -2603,6 +2979,7 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await clickFirstButtonByName(user, '发现');
|
||||
await user.click(await screen.findByRole('button', { name: '排行' }));
|
||||
await waitFor(() => {
|
||||
expect(document.getElementById('platform-tab-panel-category')).toBeTruthy();
|
||||
@@ -2647,32 +3024,6 @@ test('published puzzle detail returns to the ranking platform tab', async () =>
|
||||
});
|
||||
});
|
||||
|
||||
test('selecting puzzle creation while logged out routes through requireAuth', async () => {
|
||||
const user = userEvent.setup();
|
||||
const requireAuth = vi.fn();
|
||||
|
||||
render(
|
||||
<TestWrapper
|
||||
authValue={createAuthValue({
|
||||
user: null,
|
||||
openLoginModal: () => {},
|
||||
requireAuth,
|
||||
})}
|
||||
/>,
|
||||
);
|
||||
|
||||
await openCreationHub(user);
|
||||
const puzzleButton = await screen.findByRole('button', {
|
||||
name: /拼图.*创意礼物/u,
|
||||
});
|
||||
|
||||
expect((puzzleButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(puzzleButton);
|
||||
expect(requireAuth).toHaveBeenCalledTimes(1);
|
||||
expect(createRpgCreationSession).not.toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('restoring an agent workspace while logged out opens login modal before loading the protected session', async () => {
|
||||
const openLoginModal = vi.fn();
|
||||
|
||||
@@ -2779,7 +3130,7 @@ test('refreshing RPG agent path restores stored agent workspace pointer', async
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
test('new puzzle creation entry maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
test('embedded puzzle form maps raw bearer token errors to user-facing auth copy', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
|
||||
@@ -2792,36 +3143,35 @@ test('new puzzle creation entry maps raw bearer token errors to user-facing auth
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
const puzzleButton = screen.getByRole('button', {
|
||||
name: /拼图.*创意礼物/u,
|
||||
});
|
||||
await openCreateTemplateHub(user);
|
||||
const generateButton = screen.getByRole('button', { name: /生成草稿/u });
|
||||
|
||||
expect((puzzleButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(puzzleButton);
|
||||
expect((generateButton as HTMLButtonElement).disabled).toBe(false);
|
||||
await user.click(generateButton);
|
||||
|
||||
expect(listPuzzleWorks).toHaveBeenCalled();
|
||||
expect(createPuzzleAgentSession).toHaveBeenCalledTimes(1);
|
||||
expect(createCreativeAgentSession).not.toHaveBeenCalled();
|
||||
expect(
|
||||
await within(getPlatformTabPanel('create')).findByText(
|
||||
await screen.findByText(
|
||||
'当前登录状态已失效,请重新登录后继续。',
|
||||
),
|
||||
).toBeTruthy();
|
||||
expect(screen.queryByText('缺少 Authorization Bearer Token')).toBeNull();
|
||||
});
|
||||
|
||||
test('hidden big fish creation entry does not render in platform create hub', async () => {
|
||||
test('create tab does not render legacy gameplay creation entries', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openCreateTemplateHub(user);
|
||||
|
||||
expect(screen.queryByText('选择创作类型')).toBeNull();
|
||||
expect(screen.queryByRole('button', { name: /大鱼吃小鱼/u })).toBeNull();
|
||||
expect(createBigFishCreationSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('puzzle creation timeout exits busy state and shows a readable error', async () => {
|
||||
test('embedded puzzle form timeout exits busy state and shows a readable error', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(createPuzzleAgentSession).mockRejectedValueOnce(
|
||||
@@ -2832,9 +3182,9 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openCreateTemplateHub(user);
|
||||
|
||||
const button = screen.getByRole('button', { name: /拼图.*创意礼物/u });
|
||||
const button = screen.getByRole('button', { name: /生成草稿/u });
|
||||
await user.click(button);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -2845,12 +3195,12 @@ test('puzzle creation timeout exits busy state and shows a readable error', asyn
|
||||
).toBeGreaterThan(0);
|
||||
});
|
||||
expect(button as HTMLButtonElement).toHaveProperty('disabled', false);
|
||||
expect(screen.queryByText(/正在准备拼图共创工作区/u)).toBeNull();
|
||||
expect(createCreativeAgentSession).not.toHaveBeenCalled();
|
||||
expect(streamCreativeAgentMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('visible match3d creation card opens workspace even when public galleries fail', async () => {
|
||||
test('hidden match3d creation card stays closed even when public galleries fail', async () => {
|
||||
const user = userEvent.setup();
|
||||
const match3dSession = buildMockMatch3DAgentSession();
|
||||
|
||||
vi.mocked(listRpgEntryWorldGallery).mockRejectedValueOnce(
|
||||
new Error('读取作品广场失败'),
|
||||
@@ -2858,23 +3208,16 @@ test('visible match3d creation card opens workspace even when public galleries f
|
||||
vi.mocked(listMatch3DGallery).mockRejectedValueOnce(
|
||||
new Error('读取抓大鹅广场失败'),
|
||||
);
|
||||
vi.mocked(match3dCreationClient.createSession).mockResolvedValueOnce({
|
||||
session: match3dSession,
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openCreateTemplateHub(user);
|
||||
expect(screen.queryByText('读取作品广场失败')).toBeNull();
|
||||
expect(screen.queryByText('读取抓大鹅广场失败')).toBeNull();
|
||||
await user.click(
|
||||
screen.getByRole('button', { name: /抓大鹅.*经典消除玩法/u }),
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(match3dCreationClient.createSession).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
expect(await screen.findByText('抓大鹅工作区:match3d-agent-session-1')).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByRole('tab', { name: /抓大鹅.*经典消除玩法/u }),
|
||||
).toBeNull();
|
||||
expect(match3dCreationClient.createSession).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('puzzle draft result back button returns to creation hub', async () => {
|
||||
@@ -2905,7 +3248,7 @@ test('puzzle draft result back button returns to creation hub', async () => {
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续创作/u }));
|
||||
@@ -2919,11 +3262,9 @@ test('puzzle draft result back button returns to creation hub', async () => {
|
||||
await user.click(screen.getByRole('button', { name: '返回' }));
|
||||
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /拼图.*创意礼物/u }),
|
||||
await screen.findByText('10分钟创作一个精品互动玩法'),
|
||||
).toBeTruthy();
|
||||
expect(
|
||||
screen.queryByText('雨夜里有一只会发光的猫站在遗迹台阶上。'),
|
||||
).toBeNull();
|
||||
expect(screen.getByText('雨夜里有一只会发光的猫站在遗迹台阶上。')).toBeTruthy();
|
||||
expect(screen.queryByText('拼图结果页')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -2955,7 +3296,7 @@ test('published puzzle work card restores its source session for editing', async
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /继续创作/u })).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续创作/u }));
|
||||
@@ -3089,6 +3430,7 @@ test('formal puzzle runtime uses frontend move merge logic and backend leaderboa
|
||||
item: puzzleWork,
|
||||
});
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3274,6 +3616,7 @@ test('formal puzzle similar work keeps current run level progression', async ()
|
||||
item: profileId === similarWork.profileId ? similarWork : entryWork,
|
||||
}));
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3375,6 +3718,7 @@ test('first puzzle runtime back click can open remix result page', async () => {
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3428,6 +3772,7 @@ test('public code search opens a published puzzle by PZ code', async () => {
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3471,6 +3816,7 @@ test('public code search opens a published big fish work by BF code', async () =
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3523,6 +3869,7 @@ test('public code search opens a published Match3D work by M3 code and starts ru
|
||||
});
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
await openDiscoverHub(user);
|
||||
|
||||
const searchInput = await screen.findByPlaceholderText(
|
||||
'搜索作品号、名称、作者、描述',
|
||||
@@ -3672,7 +4019,7 @@ test('failed draft work continues on generation progress view instead of agent w
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
expect(await screen.findByText('失败中的潮雾列岛')).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续创作/u }));
|
||||
|
||||
@@ -4193,7 +4540,7 @@ test('agent result view does not keep legacy publish blockers when preview uses
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
expect(await screen.findByRole('button', { name: /继续完善/u })).toBeTruthy();
|
||||
await user.click(await screen.findByRole('button', { name: /继续完善/u }));
|
||||
|
||||
@@ -4370,9 +4717,7 @@ test('agent draft result back button returns to creation hub without syncing res
|
||||
await user.click(screen.getByRole('button', { name: /返回创作/u }));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
screen.getByRole('button', { name: /拼图.*创意礼物/u }),
|
||||
).toBeTruthy();
|
||||
expect(screen.getByText('10分钟创作一个精品互动玩法')).toBeTruthy();
|
||||
});
|
||||
|
||||
expect(
|
||||
@@ -4625,7 +4970,9 @@ test('agent draft result can open from server result preview without embedded le
|
||||
);
|
||||
});
|
||||
|
||||
test('authenticated users with save archives default into the saves tab', async () => {
|
||||
test('authenticated users can open save archives from the profile played panel', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listProfileSaveArchives).mockResolvedValue([
|
||||
{
|
||||
worldKey: 'custom:world-1',
|
||||
@@ -4642,15 +4989,18 @@ test('authenticated users with save archives default into the saves tab', async
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
expect((await screen.findAllByText('全部存档')).length).toBeGreaterThan(0);
|
||||
await openProfilePlayedWorks(user);
|
||||
|
||||
expect((await screen.findAllByText('潮雾列岛')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('最近更新时间排序').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('旧灯塔与失控航路').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('SAVE ARCHIVE')).toBeNull();
|
||||
expect(screen.queryByText('ARCHIVE')).toBeNull();
|
||||
expect(screen.queryByText('最近存档')).toBeNull();
|
||||
});
|
||||
|
||||
test('puzzle save archive highlights work title and level subtitle', async () => {
|
||||
const user = userEvent.setup();
|
||||
|
||||
vi.mocked(listProfileSaveArchives).mockResolvedValue([
|
||||
{
|
||||
worldKey: 'puzzle:puzzle-profile-1',
|
||||
@@ -4667,6 +5017,8 @@ test('puzzle save archive highlights work title and level subtitle', async () =>
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openProfilePlayedWorks(user);
|
||||
|
||||
expect((await screen.findAllByText('雨夜猫塔')).length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('第 2 关 · 星桥机关').length).toBeGreaterThan(0);
|
||||
expect(screen.queryByText('ARCHIVE')).toBeNull();
|
||||
@@ -4689,16 +5041,16 @@ test('manual tab switch is preserved after platform bootstrap requests finish',
|
||||
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
expect(
|
||||
await screen.findByRole('button', { name: /拼图.*创意礼物/u }),
|
||||
await screen.findByText('10分钟创作一个精品互动玩法'),
|
||||
).toBeTruthy();
|
||||
|
||||
resolveGalleryRequest([]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(
|
||||
within(getPlatformTabPanel('create')).getByRole('button', {
|
||||
name: /拼图.*创意礼物/u,
|
||||
}),
|
||||
within(getPlatformTabPanel('create')).getByText(
|
||||
'10分钟创作一个精品互动玩法',
|
||||
),
|
||||
).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -4750,6 +5102,8 @@ test('save tab can resume a selected archive directly into the game', async () =
|
||||
|
||||
render(<TestWrapper withAuth onContinueGame={handleContinueGame} />);
|
||||
|
||||
await openProfilePlayedWorks(user);
|
||||
|
||||
await clickFirstAsyncButtonByName(user, /潮雾列岛/u);
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -4827,7 +5181,7 @@ test('creation hub published work can open detail view before deleting from deta
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
@@ -4902,7 +5256,7 @@ test('creation hub published work enters existing detail view', async () => {
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
|
||||
expect(await screen.findByText('详情')).toBeTruthy();
|
||||
@@ -4976,7 +5330,7 @@ test('creation hub published work experience button enters world directly', asyn
|
||||
|
||||
render(<TestWrapper withAuth onSelectWorld={handleCustomWorldSelect} />);
|
||||
|
||||
await openCreationHub(user);
|
||||
await openDraftHub(user);
|
||||
await user.click(await screen.findByRole('button', { name: /查看详情/u }));
|
||||
await user.click(await screen.findByRole('button', { name: '启动' }));
|
||||
|
||||
@@ -5060,7 +5414,7 @@ test('creation hub published work card keeps delete action guarded by detail flo
|
||||
|
||||
render(<TestWrapper withAuth />);
|
||||
|
||||
await clickFirstButtonByName(user, '创作');
|
||||
await openDraftHub(user);
|
||||
|
||||
expect(await screen.findByRole('button', { name: /查看详情/u })).toBeTruthy();
|
||||
await user.click(screen.getByRole('button', { name: '删除' }));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
Archive,
|
||||
ArrowRight,
|
||||
Bell,
|
||||
BookOpen,
|
||||
@@ -8,20 +7,20 @@ import {
|
||||
ChevronRight,
|
||||
Clock3,
|
||||
Coins,
|
||||
Compass,
|
||||
Copy,
|
||||
Gamepad2,
|
||||
Heart,
|
||||
House,
|
||||
LogIn,
|
||||
MessageCircle,
|
||||
Pencil,
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
SlidersHorizontal,
|
||||
Sparkles,
|
||||
Star,
|
||||
Ticket,
|
||||
Trophy,
|
||||
UserPlus,
|
||||
UserRound,
|
||||
} from 'lucide-react';
|
||||
@@ -83,6 +82,7 @@ import {
|
||||
isMatch3DGalleryEntry,
|
||||
isPuzzleGalleryEntry,
|
||||
isSquareHoleGalleryEntry,
|
||||
isVisualNovelGalleryEntry,
|
||||
type PlatformPublicGalleryCard,
|
||||
type PlatformWorldCardLike,
|
||||
resolvePlatformWorldCoverImage,
|
||||
@@ -136,6 +136,7 @@ export interface RpgEntryHomeViewProps {
|
||||
onOpenPlayedWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||
onRechargeSuccess?: () => void | Promise<void>;
|
||||
createTabContent?: ReactNode;
|
||||
draftTabContent?: ReactNode;
|
||||
}
|
||||
|
||||
const PANEL_SURFACE_CLASS = 'platform-surface platform-surface--soft';
|
||||
@@ -161,7 +162,7 @@ const PROFILE_INVITE_REDEEM_ENTRY_VISIBLE_MS = 24 * 60 * 60 * 1000;
|
||||
const PROFILE_INVITE_QUERY_KEYS = ['inviteCode', 'invite_code'] as const;
|
||||
|
||||
type ProfilePopupPanel = 'invite' | 'redeem' | 'community';
|
||||
type MobileHomeChannel = 'recommend' | 'today' | 'category';
|
||||
type DiscoverChannel = 'recommend' | 'today' | 'category' | 'ranking';
|
||||
type PlatformRankingTab = 'hot' | 'remix' | 'new' | 'like';
|
||||
|
||||
const COMMUNITY_QR_CODES = [
|
||||
@@ -177,13 +178,14 @@ const COMMUNITY_QR_CODES = [
|
||||
},
|
||||
] as const;
|
||||
|
||||
const MOBILE_HOME_CHANNELS: Array<{
|
||||
id: MobileHomeChannel;
|
||||
const DISCOVER_CHANNELS: Array<{
|
||||
id: DiscoverChannel;
|
||||
label: string;
|
||||
}> = [
|
||||
{ id: 'recommend', label: '推荐' },
|
||||
{ id: 'today', label: '今日游戏' },
|
||||
{ id: 'category', label: '游戏分类' },
|
||||
{ id: 'today', label: '今日' },
|
||||
{ id: 'category', label: '分类' },
|
||||
{ id: 'ranking', label: '排行' },
|
||||
];
|
||||
|
||||
const PLATFORM_RANKING_TABS: Array<{
|
||||
@@ -377,6 +379,7 @@ function WorldCard({
|
||||
feedCardKey,
|
||||
enableCoverCarousel = false,
|
||||
isCoverCarouselActive = false,
|
||||
variant = 'standard',
|
||||
}: {
|
||||
entry: PlatformPublicGalleryCard;
|
||||
onClick: () => void;
|
||||
@@ -385,6 +388,7 @@ function WorldCard({
|
||||
feedCardKey?: string;
|
||||
enableCoverCarousel?: boolean;
|
||||
isCoverCarouselActive?: boolean;
|
||||
variant?: 'standard' | 'immersive';
|
||||
}) {
|
||||
const fallbackCoverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const coverSlides = useMemo(() => {
|
||||
@@ -465,7 +469,7 @@ function WorldCard({
|
||||
onClick={onClick}
|
||||
aria-label={cardLabel}
|
||||
data-mobile-feed-card-key={feedCardKey}
|
||||
className={`platform-public-work-card platform-surface platform-interactive-card relative flex w-[min(21rem,88vw)] shrink-0 flex-col overflow-hidden p-0 text-left ${className ?? ''}`}
|
||||
className={`platform-public-work-card ${variant === 'immersive' ? 'platform-public-work-card--immersive' : ''} platform-surface platform-interactive-card relative flex w-[min(21rem,88vw)] shrink-0 flex-col overflow-hidden p-0 text-left ${className ?? ''}`}
|
||||
>
|
||||
<div className="platform-public-work-card__cover relative aspect-video overflow-hidden">
|
||||
{coverImage ? (
|
||||
@@ -724,6 +728,7 @@ function PlatformTabButton({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
aria-label={label}
|
||||
className={`platform-bottom-nav__button ${emphasized ? 'platform-bottom-nav__button--primary' : ''} ${active ? 'platform-bottom-nav__button--active' : ''}`}
|
||||
>
|
||||
<span className="platform-bottom-nav__button-content">
|
||||
@@ -874,7 +879,10 @@ function PlatformRankingItem({
|
||||
const coverImage = resolvePlatformWorldCoverImage(entry);
|
||||
const displayName = formatPlatformWorkDisplayName(entry.worldName);
|
||||
const tags = buildPlatformWorldDisplayTags(entry, 2);
|
||||
const actionLabel = isPuzzleGalleryEntry(entry) ? '试玩' : '进入';
|
||||
const actionLabel =
|
||||
isPuzzleGalleryEntry(entry) || isVisualNovelGalleryEntry(entry)
|
||||
? '试玩'
|
||||
: '进入';
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -936,7 +944,10 @@ function PlatformCategoryGameItem({
|
||||
describePublicGalleryCardKind(entry),
|
||||
...tags.filter((tag) => tag !== categoryTag),
|
||||
].slice(0, 3);
|
||||
const actionLabel = isPuzzleGalleryEntry(entry) ? '试玩' : '进入';
|
||||
const actionLabel =
|
||||
isPuzzleGalleryEntry(entry) || isVisualNovelGalleryEntry(entry)
|
||||
? '试玩'
|
||||
: '进入';
|
||||
const summaryText =
|
||||
entry.summaryText || entry.subtitle || `${displayName} 正在等待摘要。`;
|
||||
|
||||
@@ -1139,6 +1150,8 @@ function buildPublicGalleryCardKey(entry: PlatformPublicGalleryCard) {
|
||||
? 'match3d'
|
||||
: isSquareHoleGalleryEntry(entry)
|
||||
? 'square-hole'
|
||||
: isVisualNovelGalleryEntry(entry)
|
||||
? 'visual-novel'
|
||||
: 'rpg';
|
||||
return `${kind}:${entry.ownerUserId}:${entry.profileId}`;
|
||||
}
|
||||
@@ -1249,6 +1262,8 @@ function describePublicGalleryCardKind(entry: PlatformPublicGalleryCard) {
|
||||
? '抓鹅'
|
||||
: isSquareHoleGalleryEntry(entry)
|
||||
? '方洞'
|
||||
: isVisualNovelGalleryEntry(entry)
|
||||
? '视觉'
|
||||
: describePlatformThemeLabel(entry.themeMode);
|
||||
return formatPlatformWorkDisplayTag(kind);
|
||||
}
|
||||
@@ -2549,20 +2564,30 @@ function ProfilePlayedWorksModal({
|
||||
stats,
|
||||
isLoading,
|
||||
error,
|
||||
saveEntries,
|
||||
saveError,
|
||||
isResumingSaveWorldKey,
|
||||
onClose,
|
||||
onOpenWork,
|
||||
onResumeSave,
|
||||
}: {
|
||||
stats: ProfilePlayStatsResponse | null;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
saveEntries: ProfileSaveArchiveSummary[];
|
||||
saveError: string | null;
|
||||
isResumingSaveWorldKey: string | null;
|
||||
onClose: () => void;
|
||||
onOpenWork?: (work: ProfilePlayedWorkSummary) => void;
|
||||
onResumeSave: (entry: ProfileSaveArchiveSummary) => void;
|
||||
}) {
|
||||
const playedWorks = stats?.playedWorks ?? [];
|
||||
const hasArchiveEntries = saveEntries.length > 0;
|
||||
const hasPlayedWorks = playedWorks.length > 0;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-[80] flex items-center justify-center bg-black/48 px-3 py-5">
|
||||
<div className="relative max-h-[min(92vh,42rem)] w-full max-w-[34rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
|
||||
<div className="relative max-h-[min(92vh,42rem)] w-full max-w-[38rem] overflow-hidden rounded-[1.35rem] bg-white text-zinc-950 shadow-2xl">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClose}
|
||||
@@ -2590,6 +2615,11 @@ function ProfilePlayedWorksModal({
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
{saveError ? (
|
||||
<div className="mt-4 rounded-xl border border-rose-200 bg-rose-50 px-3 py-2 text-sm text-rose-700">
|
||||
{saveError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-5 space-y-3">
|
||||
@@ -2600,43 +2630,73 @@ function ProfilePlayedWorksModal({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : playedWorks.length > 0 ? (
|
||||
<div className="mt-5 space-y-3">
|
||||
{playedWorks.map((work) => (
|
||||
<button
|
||||
type="button"
|
||||
key={`${work.worldKey}:${work.lastPlayedAt}`}
|
||||
onClick={() => onOpenWork?.(work)}
|
||||
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left transition hover:border-[#ff4056] hover:bg-white"
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-black text-zinc-950">
|
||||
{work.worldTitle}
|
||||
</div>
|
||||
{work.worldSubtitle ? (
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{work.worldSubtitle}
|
||||
) : hasArchiveEntries || hasPlayedWorks ? (
|
||||
<div className="mt-5 space-y-5">
|
||||
{hasArchiveEntries ? (
|
||||
<section>
|
||||
<div className="mb-2 text-xs font-black text-zinc-500">
|
||||
可继续
|
||||
</div>
|
||||
<div className="grid gap-3">
|
||||
{saveEntries.map((entry) => (
|
||||
<SaveArchiveCard
|
||||
key={`${entry.worldKey}:played-archive`}
|
||||
entry={entry}
|
||||
loading={isResumingSaveWorldKey === entry.worldKey}
|
||||
onClick={() => onResumeSave(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
) : null}
|
||||
|
||||
{hasPlayedWorks ? (
|
||||
<section>
|
||||
<div className="mb-2 text-xs font-black text-zinc-500">
|
||||
玩过
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
{playedWorks.map((work) => (
|
||||
<button
|
||||
type="button"
|
||||
key={`${work.worldKey}:${work.lastPlayedAt}`}
|
||||
onClick={() => onOpenWork?.(work)}
|
||||
className="w-full rounded-2xl border border-zinc-200 bg-zinc-50 px-4 py-3 text-left transition hover:border-[#ff4056] hover:bg-white"
|
||||
>
|
||||
<div className="flex min-w-0 items-start justify-between gap-3">
|
||||
<div className="min-w-0">
|
||||
<div className="line-clamp-1 text-base font-black text-zinc-950">
|
||||
{work.worldTitle}
|
||||
</div>
|
||||
{work.worldSubtitle ? (
|
||||
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
|
||||
{work.worldSubtitle}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-bold text-[#ff4056]">
|
||||
{formatPlayedWorkType(work.worldType)}
|
||||
</span>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="shrink-0 rounded-full bg-rose-50 px-2.5 py-1 text-[11px] font-bold text-[#ff4056]">
|
||||
{formatPlayedWorkType(work.worldType)}
|
||||
</span>
|
||||
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
|
||||
<span className="truncate">
|
||||
作品号 {formatPlayedWorkId(work)}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
最近 {formatSnapshotTime(work.lastPlayedAt)}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
时长{' '}
|
||||
{formatCompactPlayTime(
|
||||
work.lastObservedPlayTimeMs,
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-3 grid gap-2 text-xs text-zinc-500 sm:grid-cols-3">
|
||||
<span className="truncate">
|
||||
作品号 {formatPlayedWorkId(work)}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
最近 {formatSnapshotTime(work.lastPlayedAt)}
|
||||
</span>
|
||||
<span className="truncate">
|
||||
时长 {formatCompactPlayTime(work.lastObservedPlayTimeMs)}
|
||||
</span>
|
||||
</div>
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
) : null}
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-5 rounded-xl border border-zinc-200 bg-zinc-50 px-4 py-4 text-sm text-zinc-600">
|
||||
@@ -2681,6 +2741,7 @@ export function RpgEntryHomeView({
|
||||
onOpenPlayedWork,
|
||||
onRechargeSuccess,
|
||||
createTabContent,
|
||||
draftTabContent,
|
||||
}: RpgEntryHomeViewProps) {
|
||||
const authUi = useAuthUi();
|
||||
const [desktopSearchKeyword, setDesktopSearchKeyword] = useState('');
|
||||
@@ -2733,9 +2794,10 @@ export function RpgEntryHomeView({
|
||||
const [selectedCategoryTag, setSelectedCategoryTag] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [mobileHomeChannel, setMobileHomeChannel] =
|
||||
useState<MobileHomeChannel>('recommend');
|
||||
const mobileFeedRef = useRef<HTMLElement | null>(null);
|
||||
const [discoverChannel, setDiscoverChannel] =
|
||||
useState<DiscoverChannel>('recommend');
|
||||
const mobileRecommendFeedRef = useRef<HTMLElement | null>(null);
|
||||
const mobileDiscoverFeedRef = useRef<HTMLElement | null>(null);
|
||||
const [mobileCenteredCardKey, setMobileCenteredCardKey] = useState<
|
||||
string | null
|
||||
>(null);
|
||||
@@ -2842,18 +2904,29 @@ export function RpgEntryHomeView({
|
||||
isReferralCenterInitialized &&
|
||||
Boolean(referralCenter) &&
|
||||
referralCenter?.hasRedeemedCode !== true;
|
||||
const tabIcons = {
|
||||
home: House,
|
||||
category: Trophy,
|
||||
create: Sparkles,
|
||||
saves: Archive,
|
||||
profile: UserRound,
|
||||
} as const;
|
||||
const tabIcons: Record<
|
||||
PlatformHomeTab,
|
||||
ComponentType<{ className?: string }>
|
||||
> = isAuthenticated
|
||||
? {
|
||||
home: Sparkles,
|
||||
category: Compass,
|
||||
create: Plus,
|
||||
saves: Pencil,
|
||||
profile: UserRound,
|
||||
}
|
||||
: {
|
||||
home: Gamepad2,
|
||||
category: Compass,
|
||||
create: Sparkles,
|
||||
saves: Pencil,
|
||||
profile: UserRound,
|
||||
};
|
||||
const tabLabels = {
|
||||
home: '首页',
|
||||
category: '排行',
|
||||
home: '推荐',
|
||||
category: '发现',
|
||||
create: '创作',
|
||||
saves: '存档',
|
||||
saves: '草稿',
|
||||
profile: '我的',
|
||||
} as const;
|
||||
|
||||
@@ -3398,11 +3471,19 @@ export function RpgEntryHomeView({
|
||||
const desktopFeaturedGrid = desktopRecommendEntries.slice(0, 4);
|
||||
const desktopCategoryGrid = activeCategoryEntries.slice(0, 6);
|
||||
const desktopLibraryPreview = myEntries.slice(0, 2);
|
||||
const mobileFeedEntries = useMemo(() => {
|
||||
const recommendedFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
[...featuredShelf, ...latestEntries].forEach((entry) => {
|
||||
entryMap.set(buildPublicGalleryCardKey(entry), entry);
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries]);
|
||||
const discoverFeedEntries = useMemo(() => {
|
||||
const entryMap = new Map<string, PlatformPublicGalleryCard>();
|
||||
const sourceEntries =
|
||||
mobileHomeChannel === 'recommend'
|
||||
? [...featuredShelf, ...latestEntries]
|
||||
discoverChannel === 'recommend'
|
||||
? recommendedFeedEntries
|
||||
: filterTodayPublishedEntries(latestEntries);
|
||||
|
||||
sourceEntries.forEach((entry) => {
|
||||
@@ -3410,16 +3491,22 @@ export function RpgEntryHomeView({
|
||||
});
|
||||
|
||||
return Array.from(entryMap.values());
|
||||
}, [featuredShelf, latestEntries, mobileHomeChannel]);
|
||||
}, [discoverChannel, latestEntries, recommendedFeedEntries]);
|
||||
const mobileFeedCarouselEnabled =
|
||||
!isDesktopLayout && activeTab === 'home' && mobileHomeChannel !== 'category';
|
||||
!isDesktopLayout &&
|
||||
((activeTab === 'home' && recommendedFeedEntries.length > 0) ||
|
||||
(activeTab === 'category' &&
|
||||
(discoverChannel === 'recommend' || discoverChannel === 'today')));
|
||||
useEffect(() => {
|
||||
if (!mobileFeedCarouselEnabled) {
|
||||
setMobileCenteredCardKey(null);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const feedElement = mobileFeedRef.current;
|
||||
const feedElement =
|
||||
activeTab === 'home'
|
||||
? mobileRecommendFeedRef.current
|
||||
: mobileDiscoverFeedRef.current;
|
||||
const scrollElement = feedElement?.closest('.platform-tab-panel');
|
||||
if (!feedElement || !scrollElement) {
|
||||
setMobileCenteredCardKey(null);
|
||||
@@ -3490,7 +3577,13 @@ export function RpgEntryHomeView({
|
||||
scrollElement.removeEventListener('scroll', scheduleUpdate);
|
||||
window.removeEventListener('resize', scheduleUpdate);
|
||||
};
|
||||
}, [mobileFeedCarouselEnabled, mobileFeedEntries, mobileHomeChannel]);
|
||||
}, [
|
||||
discoverChannel,
|
||||
discoverFeedEntries,
|
||||
activeTab,
|
||||
mobileFeedCarouselEnabled,
|
||||
recommendedFeedEntries,
|
||||
]);
|
||||
const activeRankingConfig = PLATFORM_RANKING_TABS.find(
|
||||
(tab) => tab.id === activeRankingTab,
|
||||
) as (typeof PLATFORM_RANKING_TABS)[number];
|
||||
@@ -3499,9 +3592,6 @@ export function RpgEntryHomeView({
|
||||
buildPlatformRankingEntries(publicEntries, activeRankingTab).slice(0, 30),
|
||||
[activeRankingTab, publicEntries],
|
||||
);
|
||||
const categoryPageClass = isDesktopLayout
|
||||
? DESKTOP_PAGE_STAGE_CLASS
|
||||
: MOBILE_PAGE_STAGE_CLASS;
|
||||
const leadPublicEntry = featuredShelf[0] ?? latestEntries[0] ?? null;
|
||||
const openLeadPublicEntry = () => {
|
||||
if (leadPublicEntry) {
|
||||
@@ -3512,7 +3602,101 @@ export function RpgEntryHomeView({
|
||||
onTabChange('category');
|
||||
};
|
||||
|
||||
const mobileHomeContent: ReactNode = (
|
||||
const mobileRankingPanel: ReactNode = (
|
||||
<section
|
||||
className={`${PANEL_SURFACE_CLASS} platform-ranking-panel px-4 py-3.5 sm:px-5 sm:py-4`}
|
||||
>
|
||||
<div
|
||||
className="platform-ranking-tabs flex min-w-0 gap-2 overflow-x-auto pb-1 scrollbar-hide"
|
||||
role="tablist"
|
||||
aria-label="作品排行"
|
||||
>
|
||||
{PLATFORM_RANKING_TABS.map((tab) => {
|
||||
const active = tab.id === activeRankingTab;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
onClick={() => setActiveRankingTab(tab.id)}
|
||||
className={`platform-ranking-tab shrink-0 ${active ? 'platform-ranking-tab--active' : ''}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : rankingEntries.length > 0 ? (
|
||||
<div className="mt-3 grid min-w-0 gap-2.5">
|
||||
{rankingEntries.map((entry, index) => (
|
||||
<PlatformRankingItem
|
||||
key={`${buildPublicGalleryCardKey(entry)}:ranking:${activeRankingTab}`}
|
||||
entry={entry}
|
||||
rank={index + 1}
|
||||
metricLabel={activeRankingConfig.metricLabel}
|
||||
metricValue={getPlatformRankingMetricValue(
|
||||
entry,
|
||||
activeRankingTab,
|
||||
)}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text={activeRankingConfig.emptyText} />
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
|
||||
const mobileRecommendContent: ReactNode = (
|
||||
<div
|
||||
className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage platform-mobile-recommend-stage`}
|
||||
>
|
||||
{platformError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{platformError}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<section
|
||||
ref={mobileRecommendFeedRef}
|
||||
className="platform-mobile-home-feed platform-mobile-recommend-feed"
|
||||
>
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : recommendedFeedEntries.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-4">
|
||||
{recommendedFeedEntries.map((entry) => {
|
||||
const cardKey = buildPublicGalleryCardKey(entry);
|
||||
|
||||
return (
|
||||
<WorldCard
|
||||
key={`${cardKey}:mobile-recommend`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
authorAvatarUrl={getPublicEntryAuthorAvatarUrl(entry)}
|
||||
feedCardKey={cardKey}
|
||||
enableCoverCarousel={mobileFeedCarouselEnabled}
|
||||
isCoverCarouselActive={mobileCenteredCardKey === cardKey}
|
||||
variant="immersive"
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="公开广场暂时还没有可展示的作品。" />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
|
||||
const mobileDiscoverContent: ReactNode = (
|
||||
<div className={`${MOBILE_PAGE_STAGE_CLASS} platform-mobile-home-stage`}>
|
||||
<PublicCodeSearchBar
|
||||
value={mobileSearchKeyword}
|
||||
@@ -3531,13 +3715,13 @@ export function RpgEntryHomeView({
|
||||
) : (
|
||||
<>
|
||||
<div className="platform-mobile-home-channelbar flex min-w-0 gap-4 overflow-x-auto pb-1 scrollbar-hide">
|
||||
{MOBILE_HOME_CHANNELS.map((channel) => {
|
||||
const active = mobileHomeChannel === channel.id;
|
||||
{DISCOVER_CHANNELS.map((channel) => {
|
||||
const active = discoverChannel === channel.id;
|
||||
return (
|
||||
<button
|
||||
key={channel.id}
|
||||
type="button"
|
||||
onClick={() => setMobileHomeChannel(channel.id)}
|
||||
onClick={() => setDiscoverChannel(channel.id)}
|
||||
className={`platform-mobile-home-channel shrink-0 ${active ? 'platform-mobile-home-channel--active' : ''}`}
|
||||
>
|
||||
{channel.label}
|
||||
@@ -3552,7 +3736,9 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{mobileHomeChannel === 'category' ? (
|
||||
{discoverChannel === 'ranking' ? (
|
||||
mobileRankingPanel
|
||||
) : discoverChannel === 'category' ? (
|
||||
<section className="platform-category-list-panel">
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
@@ -3609,17 +3795,20 @@ export function RpgEntryHomeView({
|
||||
)}
|
||||
</section>
|
||||
) : (
|
||||
<section ref={mobileFeedRef} className="platform-mobile-home-feed">
|
||||
<section
|
||||
ref={mobileDiscoverFeedRef}
|
||||
className="platform-mobile-home-feed"
|
||||
>
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : mobileFeedEntries.length > 0 ? (
|
||||
) : discoverFeedEntries.length > 0 ? (
|
||||
<div className="grid min-w-0 gap-3">
|
||||
{mobileFeedEntries.map((entry: PlatformPublicGalleryCard) => {
|
||||
{discoverFeedEntries.map((entry: PlatformPublicGalleryCard) => {
|
||||
const cardKey = buildPublicGalleryCardKey(entry);
|
||||
|
||||
return (
|
||||
<WorldCard
|
||||
key={`${cardKey}:mobile-feed:${mobileHomeChannel}`}
|
||||
key={`${cardKey}:mobile-feed:${discoverChannel}`}
|
||||
entry={entry}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
className="w-full"
|
||||
@@ -3641,60 +3830,13 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
);
|
||||
|
||||
const categoryContent: ReactNode = (
|
||||
<div className={categoryPageClass}>
|
||||
<section
|
||||
className={`${PANEL_SURFACE_CLASS} platform-ranking-panel px-4 py-3.5 sm:px-5 sm:py-4`}
|
||||
>
|
||||
<div
|
||||
className="platform-ranking-tabs flex min-w-0 gap-2 overflow-x-auto pb-1 scrollbar-hide"
|
||||
role="tablist"
|
||||
aria-label="作品排行"
|
||||
>
|
||||
{PLATFORM_RANKING_TABS.map((tab) => {
|
||||
const active = tab.id === activeRankingTab;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={active}
|
||||
onClick={() => setActiveRankingTab(tab.id)}
|
||||
className={`platform-ranking-tab shrink-0 ${active ? 'platform-ranking-tab--active' : ''}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取公开作品..." />
|
||||
) : rankingEntries.length > 0 ? (
|
||||
<div className="mt-3 grid min-w-0 gap-2.5">
|
||||
{rankingEntries.map((entry, index) => (
|
||||
<PlatformRankingItem
|
||||
key={`${buildPublicGalleryCardKey(entry)}:ranking:${activeRankingTab}`}
|
||||
entry={entry}
|
||||
rank={index + 1}
|
||||
metricLabel={activeRankingConfig.metricLabel}
|
||||
metricValue={getPlatformRankingMetricValue(
|
||||
entry,
|
||||
activeRankingTab,
|
||||
)}
|
||||
onClick={() => onOpenGalleryDetail(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text={activeRankingConfig.emptyText} />
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
const categoryContent: ReactNode = isDesktopLayout ? (
|
||||
<div className={DESKTOP_PAGE_STAGE_CLASS}>{mobileRankingPanel}</div>
|
||||
) : (
|
||||
mobileDiscoverContent
|
||||
);
|
||||
|
||||
const createContent: ReactNode = createTabContent ?? (
|
||||
const fallbackCreateStartContent: ReactNode = (
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<button
|
||||
type="button"
|
||||
@@ -3720,7 +3862,11 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
const fallbackDraftContent: ReactNode = (
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
<section>
|
||||
<SectionHeader title="我的创作" detail="草稿与已发布" />
|
||||
{isLoadingPlatform ? (
|
||||
@@ -3756,53 +3902,10 @@ export function RpgEntryHomeView({
|
||||
</div>
|
||||
);
|
||||
|
||||
const savesContent: ReactNode = (
|
||||
<div className={MOBILE_PAGE_STAGE_CLASS}>
|
||||
{authUi?.user ? (
|
||||
<>
|
||||
{saveError ? (
|
||||
<div className="rounded-2xl border border-rose-400/20 bg-rose-500/10 px-4 py-3 text-sm leading-6 text-rose-700">
|
||||
{saveError}
|
||||
</div>
|
||||
) : null}
|
||||
const createContent: ReactNode = createTabContent ?? fallbackCreateStartContent;
|
||||
|
||||
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
|
||||
<SectionHeader title="全部存档" detail="最近更新时间排序" />
|
||||
{isLoadingPlatform ? (
|
||||
<EmptyShelf text="正在读取存档..." />
|
||||
) : saveEntries.length > 0 ? (
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
{saveEntries.map((entry) => (
|
||||
<SaveArchiveCard
|
||||
key={entry.worldKey}
|
||||
entry={entry}
|
||||
loading={isResumingSaveWorldKey === entry.worldKey}
|
||||
onClick={() => onResumeSave(entry)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<EmptyShelf text="还没有可恢复的存档,去首页开始一段新的游玩吧。" />
|
||||
)}
|
||||
</section>
|
||||
</>
|
||||
) : (
|
||||
<section className={`${PANEL_SURFACE_CLASS} px-4 py-3.5`}>
|
||||
<div className="platform-subpanel rounded-[1.35rem] px-4 py-4">
|
||||
<div className="text-base font-semibold text-[var(--platform-text-strong)]">
|
||||
尚未登录
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => authUi?.openLoginModal()}
|
||||
className="platform-button platform-button--primary mt-4"
|
||||
>
|
||||
登录
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
</div>
|
||||
const savesContent: ReactNode = (
|
||||
draftTabContent ?? fallbackDraftContent
|
||||
);
|
||||
|
||||
const profileContent: ReactNode = (
|
||||
@@ -4326,7 +4429,7 @@ export function RpgEntryHomeView({
|
||||
);
|
||||
|
||||
const tabContentById = {
|
||||
home: isDesktopLayout ? desktopHomeContent : mobileHomeContent,
|
||||
home: isDesktopLayout ? desktopHomeContent : mobileRecommendContent,
|
||||
category: categoryContent,
|
||||
create: createContent,
|
||||
saves: savesContent,
|
||||
@@ -4399,7 +4502,7 @@ export function RpgEntryHomeView({
|
||||
if (!isDesktopLayout) {
|
||||
return (
|
||||
<div className="platform-mobile-entry-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
|
||||
<div className="mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
|
||||
<div className="platform-mobile-topbar mb-3 flex shrink-0 items-center justify-between gap-3 px-0.5">
|
||||
<RpgEntryBrandLogo />
|
||||
{!isAuthenticated ? (
|
||||
<button
|
||||
@@ -4410,19 +4513,23 @@ export function RpgEntryHomeView({
|
||||
<LogIn className="h-3.5 w-3.5" />
|
||||
登录
|
||||
</button>
|
||||
) : null}
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
onClick={openUserSurface}
|
||||
className="platform-icon-button platform-mobile-topbar__action shrink-0"
|
||||
aria-label="通知与账户"
|
||||
>
|
||||
<Bell className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="platform-tab-panel-stack min-w-0 flex-1">
|
||||
{tabPanels}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0 border-t pt-2"
|
||||
style={{
|
||||
borderColor: 'var(--platform-line-soft)',
|
||||
}}
|
||||
>
|
||||
<div className="platform-mobile-bottom-dock mt-3 min-w-0 shrink-0">
|
||||
<div
|
||||
className={`platform-bottom-nav grid ${visibleTabs.length === 5 ? 'grid-cols-5' : visibleTabs.length === 4 ? 'grid-cols-4' : visibleTabs.length === 3 ? 'grid-cols-3' : 'grid-cols-2'}`}
|
||||
>
|
||||
@@ -4472,8 +4579,12 @@ export function RpgEntryHomeView({
|
||||
stats={profilePlayStats}
|
||||
isLoading={isProfilePlayStatsLoading}
|
||||
error={profilePlayStatsError}
|
||||
saveEntries={saveEntries}
|
||||
saveError={saveError}
|
||||
isResumingSaveWorldKey={isResumingSaveWorldKey}
|
||||
onClose={onCloseProfilePlayStats ?? (() => undefined)}
|
||||
onOpenWork={onOpenPlayedWork}
|
||||
onResumeSave={onResumeSave}
|
||||
/>
|
||||
) : null}
|
||||
{isWalletLedgerOpen ? (
|
||||
@@ -4606,8 +4717,12 @@ export function RpgEntryHomeView({
|
||||
stats={profilePlayStats}
|
||||
isLoading={isProfilePlayStatsLoading}
|
||||
error={profilePlayStatsError}
|
||||
saveEntries={saveEntries}
|
||||
saveError={saveError}
|
||||
isResumingSaveWorldKey={isResumingSaveWorldKey}
|
||||
onClose={onCloseProfilePlayStats ?? (() => undefined)}
|
||||
onOpenWork={onOpenPlayedWork}
|
||||
onResumeSave={onResumeSave}
|
||||
/>
|
||||
) : null}
|
||||
{isWalletLedgerOpen ? (
|
||||
|
||||
@@ -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