为项目页增加返回主站入口
在项目页头部增加返回主站按钮。 将返回动作接入平台首页路由。 补充项目页返回主站测试与文档说明。
This commit is contained in:
@@ -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: '新建项目',
|
||||
|
||||
@@ -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 />);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user