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

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

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

补充项目页返回主站测试与文档说明。
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
@@ -126,6 +126,7 @@ Tab
- 桌面端原“草稿”入口改为“项目”,点击 `pushAppHistoryPath('/project')`
- 桌面端 `/project` 必须复用 `/creation` 的平台桌面 chrome,保留左侧完整导航栏和右侧顶部栏;`ProjectGalleryView` 只替换右侧主内容区,不能作为脱离导航的独立全屏页渲染,避免用户进入项目页后无法返回或切换页签。
- 桌面端从 `/creation` 切换到 `/project` 后,“项目”页签进入选中态,“创作”页签必须恢复为普通默认态,不保留创作主页的主按钮强调表现。
- `/project` 项目页头部必须提供 `返回主站` 按钮,点击后切回平台首页 `/`,行为与桌面侧栏切回推荐页保持一致。
- 移动端导航列表不展示“创作”和“项目”。
- 移动端不拦截用户直接访问 `/creation``/project`
@@ -169,8 +170,8 @@ Tab
- `appPageRoutes.test.ts`:覆盖 `/creation -> creation-home`,并确认现有 `/creation/<play>` 不回归。
- `RpgEntryHomeView.recharge.test.tsx`:桌面显示“创作”“项目”,不显示“草稿”;移动端不显示“创作”和“项目”;我的页不再出现项目快捷入口。
- `CreationLandingView.test.tsx`:覆盖首屏主视觉、九大功能、最近项目、新建入口、查看全部、未登录隐藏最近项目、素材 Tab。
- `PlatformEntryFlowShellImpl` 相关测试:点击“创作”进入 `/creation`,点击“项目”进入 `/project`,新建项目进入 `/editor/canvas?projectid=xxx`
- `ProjectGalleryView.test.tsx`:封面抽取后项目封面、重命名、删除和选择模式保持通过。
- `PlatformEntryFlowShellImpl` 相关测试:点击“创作”进入 `/creation`,点击“项目”进入 `/project`项目页头部“返回主站”回到 `/`新建项目进入 `/editor/canvas?projectid=xxx`
- `ProjectGalleryView.test.tsx`:封面抽取后项目封面、头部“返回主站”、重命名、删除和选择模式保持通过。
验证命令:
@@ -191,6 +192,7 @@ git diff --check
- 登录后显示最近项目、新建项目、最多 4 个项目和“查看全部”。
- 点击新建项目进入 `/editor/canvas?projectid=xxx`
- 桌面导航显示“项目”,不显示“草稿”。
- `/project` 头部点击“返回主站”回到 `/`,并退出项目页主内容。
- 移动视口底部不显示“创作”和“项目”。
- “我的”页没有项目快捷入口。
- 陶泥儿精选素材瀑布流不出现 mock 素材、假作者或假泥点成本。
@@ -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;
}