diff --git a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md index b448a361d..fa38fb23c 100644 --- a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md +++ b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md @@ -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/` 不回归。 - `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 素材、假作者或假泥点成本。 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index 9997573c7..d060c9c6e 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -15253,7 +15253,14 @@ export function PlatformEntryFlowShellImpl({ ); const projectGalleryContent = ( }> - + { + platformBootstrap.setPlatformTab('home'); + setSelectionStage('platform'); + pushAppHistoryPath('/'); + }} + /> ); const handlePlatformHomeTabChange = useCallback( diff --git a/src/components/project/ProjectGalleryView.test.tsx b/src/components/project/ProjectGalleryView.test.tsx index 35e4c0e52..b1ebc5ff6 100644 --- a/src/components/project/ProjectGalleryView.test.tsx +++ b/src/components/project/ProjectGalleryView.test.tsx @@ -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> = {}, +) { + return render( + , + ); +} + describe('ProjectGalleryView', () => { afterEach(() => { listEditorProjectsMock.mockReset(); @@ -85,7 +97,7 @@ describe('ProjectGalleryView', () => { listEditorProjectsMock.mockResolvedValueOnce(projectItems); const user = userEvent.setup(); - render(); + 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(); + renderProjectGalleryView(); await screen.findByText('封面项目'); const cover = document.querySelector('.project-gallery__canvas-cover'); @@ -172,7 +197,10 @@ describe('ProjectGalleryView', () => { render( - + , ); @@ -191,7 +219,10 @@ describe('ProjectGalleryView', () => { render( - + , ); @@ -205,7 +236,7 @@ describe('ProjectGalleryView', () => { it('renders project loading errors through the shared status message', async () => { listEditorProjectsMock.mockRejectedValueOnce(new Error('读取项目失败')); - render(); + renderProjectGalleryView(); const alert = await screen.findByRole('alert'); @@ -223,7 +254,7 @@ describe('ProjectGalleryView', () => { deleteEditorProjectMock.mockResolvedValueOnce('editor-project-2'); const user = userEvent.setup(); - render(); + 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(); + renderProjectGalleryView(); await screen.findByText('角色设定板'); await user.click(screen.getByRole('button', { name: '选择' })); @@ -279,7 +310,7 @@ describe('ProjectGalleryView', () => { }); const user = userEvent.setup(); - render(); + renderProjectGalleryView({ onOpenProject }); const newProjectButton = await screen.findByRole('button', { name: '新建项目', diff --git a/src/components/project/ProjectGalleryView.tsx b/src/components/project/ProjectGalleryView.tsx index b54d4c902..1a29fdf55 100644 --- a/src/components/project/ProjectGalleryView.tsx +++ b/src/components/project/ProjectGalleryView.tsx @@ -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([]); const [isLoading, setIsLoading] = useState(true); @@ -298,11 +303,19 @@ export function ProjectGalleryView({ onOpenProject }: ProjectGalleryViewProps) { return (
-
+

项目

{projects.length} 个画布项目
+ + + 返回主站 + { ); }); +test('project page header can return to main site', async () => { + const user = userEvent.setup(); + mockDesktopPlatformLayout(); + window.history.replaceState(null, '', '/project'); + render(); + + 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(); diff --git a/src/index.css b/src/index.css index f1a780144..d8b891884 100644 --- a/src/index.css +++ b/src/index.css @@ -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; }