为项目页增加返回主站入口

在项目页头部增加返回主站按钮。

将返回动作接入平台首页路由。

补充项目页返回主站测试与文档说明。
This commit is contained in:
2026-06-22 15:01:44 +08:00
parent 13a1044cf2
commit d19f8ba1da
6 changed files with 89 additions and 14 deletions
@@ -15253,7 +15253,14 @@ export function PlatformEntryFlowShellImpl({
);
const projectGalleryContent = (
<Suspense fallback={<LazyPanelFallback label="正在加载项目..." />}>
<ProjectGalleryView onOpenProject={openEditorProject} />
<ProjectGalleryView
onOpenProject={openEditorProject}
onReturnToMainSite={() => {
platformBootstrap.setPlatformTab('home');
setSelectionStage('platform');
pushAppHistoryPath('/');
}}
/>
</Suspense>
);
const handlePlatformHomeTabChange = useCallback(
@@ -2,7 +2,7 @@
import { render, screen, waitFor, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import type { ContextType } from 'react';
import type { ComponentProps, ContextType } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ApiClientError } from '../../services/apiClient';
@@ -72,6 +72,18 @@ const projectItems = [
},
];
function renderProjectGalleryView(
props: Partial<ComponentProps<typeof ProjectGalleryView>> = {},
) {
return render(
<ProjectGalleryView
onOpenProject={vi.fn()}
onReturnToMainSite={vi.fn()}
{...props}
/>,
);
}
describe('ProjectGalleryView', () => {
afterEach(() => {
listEditorProjectsMock.mockReset();
@@ -85,7 +97,7 @@ describe('ProjectGalleryView', () => {
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
const user = userEvent.setup();
render(<ProjectGalleryView onOpenProject={onOpenProject} />);
renderProjectGalleryView({ onOpenProject });
await screen.findByText('角色设定板');
await user.click(screen.getByRole('button', { name: '打开项目角色设定板' }));
@@ -93,6 +105,19 @@ describe('ProjectGalleryView', () => {
expect(onOpenProject).toHaveBeenCalledWith('editor-project-1');
});
it('returns to the main site from the project header', async () => {
const onReturnToMainSite = vi.fn();
listEditorProjectsMock.mockResolvedValueOnce(projectItems);
const user = userEvent.setup();
renderProjectGalleryView({ onReturnToMainSite });
await screen.findByText('角色设定板');
await user.click(screen.getByRole('button', { name: '返回主站' }));
expect(onReturnToMainSite).toHaveBeenCalledTimes(1);
});
it('uses canvas-center layer composition as the project cover', async () => {
listEditorProjectsMock.mockResolvedValueOnce([
{
@@ -143,7 +168,7 @@ describe('ProjectGalleryView', () => {
},
]);
render(<ProjectGalleryView onOpenProject={vi.fn()} />);
renderProjectGalleryView();
await screen.findByText('封面项目');
const cover = document.querySelector('.project-gallery__canvas-cover');
@@ -172,7 +197,10 @@ describe('ProjectGalleryView', () => {
render(
<AuthUiContext.Provider value={createAuthValue({ openLoginModal })}>
<ProjectGalleryView onOpenProject={vi.fn()} />
<ProjectGalleryView
onOpenProject={vi.fn()}
onReturnToMainSite={vi.fn()}
/>
</AuthUiContext.Provider>,
);
@@ -191,7 +219,10 @@ describe('ProjectGalleryView', () => {
render(
<AuthUiContext.Provider value={createAuthValue({ requireAuth })}>
<ProjectGalleryView onOpenProject={onOpenProject} />
<ProjectGalleryView
onOpenProject={onOpenProject}
onReturnToMainSite={vi.fn()}
/>
</AuthUiContext.Provider>,
);
@@ -205,7 +236,7 @@ describe('ProjectGalleryView', () => {
it('renders project loading errors through the shared status message', async () => {
listEditorProjectsMock.mockRejectedValueOnce(new Error('读取项目失败'));
render(<ProjectGalleryView onOpenProject={vi.fn()} />);
renderProjectGalleryView();
const alert = await screen.findByRole('alert');
@@ -223,7 +254,7 @@ describe('ProjectGalleryView', () => {
deleteEditorProjectMock.mockResolvedValueOnce('editor-project-2');
const user = userEvent.setup();
render(<ProjectGalleryView onOpenProject={vi.fn()} />);
renderProjectGalleryView();
await screen.findByText('角色设定板');
await user.click(screen.getByRole('button', { name: '打开项目角色设定板菜单' }));
@@ -252,7 +283,7 @@ describe('ProjectGalleryView', () => {
deleteEditorProjectMock.mockResolvedValue('deleted');
const user = userEvent.setup();
render(<ProjectGalleryView onOpenProject={vi.fn()} />);
renderProjectGalleryView();
await screen.findByText('角色设定板');
await user.click(screen.getByRole('button', { name: '选择' }));
@@ -279,7 +310,7 @@ describe('ProjectGalleryView', () => {
});
const user = userEvent.setup();
render(<ProjectGalleryView onOpenProject={onOpenProject} />);
renderProjectGalleryView({ onOpenProject });
const newProjectButton = await screen.findByRole('button', {
name: '新建项目',
+15 -2
View File
@@ -1,4 +1,5 @@
import {
ArrowLeft,
Check,
CheckSquare,
MoreHorizontal,
@@ -34,6 +35,7 @@ import { ProjectCanvasCover } from './ProjectCanvasCover';
type ProjectGalleryViewProps = {
onOpenProject: (projectId: string) => void;
onReturnToMainSite: () => void;
};
type RenameDraft = {
@@ -58,7 +60,10 @@ function formatProjectUpdatedAt(value: string) {
}).format(date);
}
export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
export function ProjectGalleryView({
onOpenProject,
onReturnToMainSite,
}: ProjectGalleryViewProps) {
const authUi = useAuthUi();
const [projects, setProjects] = useState<EditorProjectSnapshot[]>([]);
const [isLoading, setIsLoading] = useState(true);
@@ -298,11 +303,19 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) {
return (
<main className="project-gallery" aria-label="项目">
<header className="project-gallery__header">
<div>
<div className="project-gallery__heading">
<h1></h1>
<span>{projects.length} </span>
</div>
<div className="project-gallery__header-actions">
<PlatformActionButton
tone="secondary"
size="sm"
onClick={onReturnToMainSite}
>
<ArrowLeft className="h-4 w-4" />
</PlatformActionButton>
<PlatformActionButton
tone="secondary"
size="sm"
@@ -9251,6 +9251,22 @@ test('desktop project tab switch clears creation primary styling', async () => {
);
});
test('project page header can return to main site', async () => {
const user = userEvent.setup();
mockDesktopPlatformLayout();
window.history.replaceState(null, '', '/project');
render(<TestWrapper withAuth />);
expect(await screen.findByRole('main', { name: '项目' })).toBeTruthy();
await user.click(screen.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 />);
+6
View File
@@ -3001,6 +3001,10 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
padding: 0 1.5rem;
}
.project-gallery__heading {
min-width: 0;
}
.project-gallery__header h1 {
margin: 0;
color: #111827;
@@ -3019,6 +3023,8 @@ html[data-mobile-keyboard-open='true'] .platform-mobile-bottom-dock {
.project-gallery__header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
flex-wrap: wrap;
gap: 0.6rem;
}