项目界面无法跳转的问题修复

让 /project 复用创作主页桌面导航壳

为项目页补充保留左侧导航并切回推荐页的回归测试

同步项目入口文档约束
This commit is contained in:
2026-06-21 19:39:52 +08:00
parent da34d19b46
commit 976561d385
4 changed files with 73 additions and 26 deletions
@@ -115,6 +115,7 @@ Tab
- `/creation/<play>` 现有玩法工作台路由保持原有 stage 映射。
- 桌面端点击“创作”时调用 `pushAppHistoryPath('/creation')`
- 桌面端原“草稿”入口改为“项目”,点击 `pushAppHistoryPath('/project')`
- 桌面端 `/project` 必须复用 `/creation` 的平台桌面 chrome,保留左侧完整导航栏和右侧顶部栏;`ProjectGalleryView` 只替换右侧主内容区,不能作为脱离导航的独立全屏页渲染,避免用户进入项目页后无法返回或切换页签。
- 移动端导航列表不展示“创作”和“项目”。
- 移动端不拦截用户直接访问 `/creation``/project`
@@ -15251,10 +15251,18 @@ export function PlatformEntryFlowShellImpl({
/>
</Suspense>
);
const projectGalleryContent = (
<Suspense fallback={<LazyPanelFallback label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
</Suspense>
);
const handlePlatformHomeTabChange = useCallback(
(tab: PlatformHomeTab) => {
platformBootstrap.setPlatformTab(tab);
if (selectionStage === 'creation-home' && tab !== 'create') {
if (
(selectionStage === 'creation-home' && tab !== 'create') ||
selectionStage === 'project'
) {
setSelectionStage('platform');
pushAppHistoryPath('/');
}
@@ -15264,11 +15272,14 @@ export function PlatformEntryFlowShellImpl({
const platformHomeView = (
<PlatformEntryHomeView
activeTab={
selectionStage === 'creation-home'
selectionStage === 'creation-home' || selectionStage === 'project'
? 'create'
: platformBootstrap.platformTab
}
allowHiddenActiveTab={selectionStage === 'creation-home'}
activeDesktopNavTab={selectionStage === 'project' ? 'projects' : undefined}
allowHiddenActiveTab={
selectionStage === 'creation-home' || selectionStage === 'project'
}
onTabChange={handlePlatformHomeTabChange}
hasSavedGame={hasSavedGame}
savedSnapshot={savedSnapshot}
@@ -15298,9 +15309,11 @@ export function PlatformEntryFlowShellImpl({
: platformBootstrap.dashboardError
}
createTabContent={
selectionStage === 'creation-home'
? creationLandingContent
: creationStartContent
selectionStage === 'project'
? projectGalleryContent
: selectionStage === 'creation-home'
? creationLandingContent
: creationStartContent
}
draftTabContent={draftHubContent}
onContinueGame={handleContinueGame}
@@ -15408,21 +15421,6 @@ export function PlatformEntryFlowShellImpl({
</Suspense>
</motion.div>
)}
{selectionStage === 'project' && (
<motion.div
key="project-gallery"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="project-gallery-stage-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
<Suspense fallback={<LazyPanelFallback label="正在加载项目..." />}>
<ProjectGalleryView
onOpenProject={openEditorProject}
/>
</Suspense>
</motion.div>
)}
{selectionStage === 'creation-home' && (
<motion.div
key="creation-home"
@@ -15434,6 +15432,17 @@ export function PlatformEntryFlowShellImpl({
{platformHomeView}
</motion.div>
)}
{selectionStage === 'project' && (
<motion.div
key="project-gallery"
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
className="creation-home-stage flex h-full min-h-0 min-w-0 flex-col overflow-hidden"
>
{platformHomeView}
</motion.div>
)}
{selectionStage === 'platform' && (
<motion.div
key="platform-home"
@@ -94,6 +94,10 @@ import {
regenerateBabyObjectMatchDraftAssets,
saveBabyObjectMatchDraft,
} from '../../services/edutainment-baby-object';
import {
listEditorProjects,
loadEditorAssetLibrary,
} from '../../services/image-editor/editorProjectClient';
import { jumpHopClient } from '../../services/jump-hop/jumpHopClient';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
@@ -703,6 +707,14 @@ vi.mock('../../services/edutainment-baby-object', () => ({
saveBabyObjectMatchDraft: vi.fn(),
}));
vi.mock('../../services/image-editor/editorProjectClient', () => ({
createEditorProject: vi.fn(),
deleteEditorProject: vi.fn(),
listEditorProjects: vi.fn(),
loadEditorAssetLibrary: vi.fn(async () => ({ assets: [] })),
renameEditorProject: vi.fn(),
}));
vi.mock('../../services/wooden-fish/woodenFishClient', () => ({
woodenFishClient: {
checkpointRun: vi.fn(),
@@ -2717,6 +2729,11 @@ beforeEach(() => {
});
vi.mocked(listRpgEntryWorldLibrary).mockResolvedValue([]);
vi.mocked(listRpgEntryWorldGallery).mockResolvedValue([]);
vi.mocked(listEditorProjects).mockResolvedValue([]);
vi.mocked(loadEditorAssetLibrary).mockResolvedValue({
assets: [],
folders: [],
});
vi.mocked(getRpgEntryWorldGalleryDetail).mockImplementation(
async (ownerUserId, profileId) =>
buildMockRpgGalleryDetail({
@@ -9169,6 +9186,27 @@ test('creation home desktop rail can return to platform tabs', async () => {
expect(window.location.pathname).toBe('/');
});
test('project page keeps desktop rail and can return to platform tabs', async () => {
const user = userEvent.setup();
mockDesktopPlatformLayout();
window.history.replaceState(null, '', '/project');
const { container } = render(<TestWrapper withAuth />);
expect(await screen.findByRole('main', { name: '项目' })).toBeTruthy();
const nav = container.querySelector('.platform-desktop-rail');
expect(nav).toBeTruthy();
expect(
within(nav as HTMLElement).getByRole('button', { name: '项目' }),
).toBeTruthy();
await user.click(within(nav as HTMLElement).getByRole('button', { name: '推荐' }));
await waitFor(() => {
expect(screen.queryByRole('main', { name: '项目' })).toBeNull();
});
expect(window.location.pathname).toBe('/');
});
test('direct mobile creation home keeps landing content without mobile nav entry', async () => {
window.history.replaceState(null, '', '/creation');
const { container } = render(<TestWrapper withAuth />);
@@ -228,6 +228,7 @@ export type PlatformHomeTab =
type PlatformNavTab = PlatformHomeTab | 'projects';
export interface RpgEntryHomeViewProps {
activeTab: PlatformHomeTab;
activeDesktopNavTab?: PlatformNavTab;
isDesktopLayout?: boolean;
onTabChange: (tab: PlatformHomeTab) => void;
hasSavedGame: boolean;
@@ -1939,10 +1940,6 @@ function DesktopTabButton({
);
}
function isPlatformHomeTab(tab: PlatformNavTab): tab is PlatformHomeTab {
return tab !== 'projects';
}
function PlatformTabPanel({
tab,
activeTab,
@@ -2584,6 +2581,7 @@ function RechargePaymentConfirmationMask({ orderId }: { orderId: string }) {
export function RpgEntryHomeView({
activeTab,
activeDesktopNavTab,
isDesktopLayout: isDesktopLayoutProp,
onTabChange,
saveEntries,
@@ -2973,6 +2971,7 @@ export function RpgEntryHomeView({
saves: '草稿',
profile: '我的',
} as const satisfies Record<PlatformNavTab, string>;
const resolvedDesktopNavTab = activeDesktopNavTab ?? activeTab;
useEffect(() => {
if (allowHiddenActiveTab) {
@@ -5034,7 +5033,7 @@ export function RpgEntryHomeView({
{visibleDesktopNavTabs.map((tab) => (
<DesktopTabButton
key={tab}
active={isPlatformHomeTab(tab) && activeTab === tab}
active={resolvedDesktopNavTab === tab}
label={tabLabels[tab]}
icon={tabIcons[tab]}
creationHomeIconSrc={CREATION_HOME_NAV_ICON_SRC[tab]}