恢复移动端创作工具门禁

移动端底部导航仅保留我的入口
移动端创作、项目与画布直达统一展示桌面端提示
补充移动端入口与画布阻断回归测试
纠正旧模板退役时越权扩大的移动端产品口径
This commit is contained in:
2026-08-03 15:13:26 +08:00
parent d1a657b5d2
commit d3eeaf979d
8 changed files with 166 additions and 33 deletions
@@ -1,9 +1,11 @@
/* @vitest-environment jsdom */
import { fireEvent, render, screen, within } from '@testing-library/react';
import { useState } from 'react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { PlatformEntryFlowShellImpl } from './PlatformEntryActiveFlowShell';
import type { SelectionStage } from './platformEntryActiveTypes';
const authUiMock = vi.hoisted(() => ({
value: {
@@ -15,10 +17,18 @@ const authUiMock = vi.hoisted(() => ({
},
}));
const responsiveMock = vi.hoisted(() => ({
isDesktopLayout: true,
}));
vi.mock('../auth/AuthUiContext', () => ({
useAuthUi: () => authUiMock.value,
}));
vi.mock('./platformEntryResponsive', () => ({
usePlatformDesktopLayout: () => responsiveMock.isDesktopLayout,
}));
vi.mock('../creation-home/CreationLandingView', () => ({
CreationLandingView: ({
onOpenProject,
@@ -102,10 +112,26 @@ vi.mock('./usePlatformProfileCenterController', () => ({
}),
}));
function StatefulPlatformEntryFlowShell({
initialStage,
}: {
initialStage: SelectionStage;
}) {
const [selectionStage, setSelectionStage] = useState(initialStage);
return (
<PlatformEntryFlowShellImpl
selectionStage={selectionStage}
setSelectionStage={(nextStage) => setSelectionStage(nextStage)}
/>
);
}
describe('PlatformEntryActiveFlowShell', () => {
beforeEach(() => {
window.history.replaceState(null, '', '/creation');
authUiMock.value.openLoginModal.mockReset();
responsiveMock.isDesktopLayout = true;
});
it('keeps the active desktop rail and the shared account capsule', async () => {
@@ -174,11 +200,12 @@ describe('PlatformEntryActiveFlowShell', () => {
).toBe('page');
});
it('keeps creation, project, and profile reachable from the mobile dock', async () => {
it('keeps only profile reachable from the mobile dock', async () => {
responsiveMock.isDesktopLayout = false;
const setSelectionStage = vi.fn();
render(
<PlatformEntryFlowShellImpl
selectionStage="project"
selectionStage="platform"
setSelectionStage={setSelectionStage}
/>,
);
@@ -190,12 +217,7 @@ describe('PlatformEntryActiveFlowShell', () => {
within(navigation)
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['创作', '项目', '我的']);
expect(
within(navigation)
.getByRole('button', { name: '项目' })
.getAttribute('aria-current'),
).toBe('page');
).toEqual(['我的']);
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
expect(window.location.pathname).toBe('/profile');
@@ -204,6 +226,48 @@ describe('PlatformEntryActiveFlowShell', () => {
});
});
it.each(['creation-home', 'project', 'image-editor'] as const)(
'shows the desktop guide instead of mounting the %s page on mobile',
async (selectionStage) => {
responsiveMock.isDesktopLayout = false;
render(
<PlatformEntryFlowShellImpl
selectionStage={selectionStage}
setSelectionStage={vi.fn()}
/>,
);
expect(
await screen.findByRole('main', { name: '桌面端创作提示' }),
).toBeTruthy();
expect(
screen.queryByRole('main', { name: '陶泥儿创作主页' }),
).toBeNull();
expect(screen.queryByRole('main', { name: '项目' })).toBeNull();
expect(
screen.queryByRole('main', { name: '图片画布编辑器' }),
).toBeNull();
},
);
it('shows the desktop guide instead of the editor after opening a canvas tool from the mobile root', async () => {
responsiveMock.isDesktopLayout = false;
render(<StatefulPlatformEntryFlowShell initialStage="platform" />);
fireEvent.click(
await screen.findByRole('button', { name: '打开音乐工具' }),
);
expect(
await screen.findByRole('main', { name: '桌面端创作提示' }),
).toBeTruthy();
expect(
screen.queryByRole('main', { name: '图片画布编辑器' }),
).toBeNull();
});
it('switches between creation and projects and passes search to the active page', async () => {
const setSelectionStage = vi.fn();
const { rerender } = render(
@@ -1,4 +1,11 @@
import { FolderKanban, Palette, Search, UserRound } from 'lucide-react';
import {
FolderKanban,
Home,
Monitor,
Palette,
Search,
UserRound,
} from 'lucide-react';
import {
type ComponentType,
type FormEvent,
@@ -20,6 +27,8 @@ import {
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import { useAuthUi } from '../auth/AuthUiContext';
import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
import {
resolveActivePublicUserCode,
resolveActiveUserAvatarLabel,
@@ -161,6 +170,45 @@ function LoadingPanel({ label }: { label: string }) {
);
}
function MobileCreationDesktopGuide({
onReturnHome,
}: {
onReturnHome: () => void;
}) {
return (
<main
className="flex min-h-full flex-1 items-center justify-center px-4 py-8"
aria-label="桌面端创作提示"
>
<PlatformSubpanel
as="section"
radius="xl"
padding="none"
className="platform-remap-surface w-full max-w-sm px-5 py-6 text-center"
>
<span className="mx-auto flex h-14 w-14 items-center justify-center rounded-[1.05rem] bg-[rgba(199,101,50,0.12)] text-[var(--platform-accent-strong)]">
<Monitor aria-hidden="true" className="h-7 w-7" />
</span>
<h1 className="mt-4 text-xl font-black leading-tight text-[var(--platform-text-strong)]">
</h1>
<p className="mt-3 text-sm font-semibold leading-6 text-[var(--platform-text-base)]">
</p>
<PlatformActionButton
onClick={onReturnHome}
size="md"
shape="pill"
className="mt-5 min-h-11 w-full"
>
<Home aria-hidden="true" className="h-4 w-4" />
</PlatformActionButton>
</PlatformSubpanel>
</main>
);
}
export function PlatformEntryFlowShellImpl({
selectionStage,
setSelectionStage,
@@ -173,6 +221,8 @@ export function PlatformEntryFlowShellImpl({
const [isApiKeysOpen, setIsApiKeysOpen] = useState(false);
const [searchInput, setSearchInput] = useState('');
const [activeSearchKeyword, setActiveSearchKeyword] = useState('');
const [isMobileDesktopGuideOpen, setIsMobileDesktopGuideOpen] =
useState(false);
const isDesktopLayout = usePlatformDesktopLayout();
const refreshDashboard = useCallback(async () => {
@@ -206,14 +256,22 @@ export function PlatformEntryFlowShellImpl({
});
const openCreation = useCallback(() => {
if (!isDesktopLayout) {
setIsMobileDesktopGuideOpen(true);
return;
}
pushAppHistoryPath('/creation');
setSelectionStage('creation-home', { path: '/creation' });
}, [setSelectionStage]);
}, [isDesktopLayout, setSelectionStage]);
const openProjects = useCallback(() => {
if (!isDesktopLayout) {
setIsMobileDesktopGuideOpen(true);
return;
}
pushAppHistoryPath('/project');
setSelectionStage('project', { path: '/project' });
}, [setSelectionStage]);
}, [isDesktopLayout, setSelectionStage]);
const openProfile = useCallback(() => {
pushAppHistoryPath('/profile');
@@ -222,6 +280,10 @@ export function PlatformEntryFlowShellImpl({
const openEditorProject = useCallback(
(projectId: string, options?: { guide?: boolean; tool?: string }) => {
if (!isDesktopLayout) {
setIsMobileDesktopGuideOpen(true);
return;
}
const params = new URLSearchParams();
params.set('projectid', projectId);
if (options?.guide) {
@@ -234,7 +296,7 @@ export function PlatformEntryFlowShellImpl({
pushAppHistoryPath(path);
setSelectionStage('image-editor', { path });
},
[setSelectionStage],
[isDesktopLayout, setSelectionStage],
);
const replaceWithProjectGallery = useCallback(() => {
@@ -242,6 +304,26 @@ export function PlatformEntryFlowShellImpl({
setSelectionStage('project', { path: '/project' });
}, [setSelectionStage]);
const isMobileToolStage =
!isDesktopLayout &&
(selectionStage === 'creation-home' ||
selectionStage === 'project' ||
selectionStage === 'image-editor');
if ((!isDesktopLayout && isMobileDesktopGuideOpen) || isMobileToolStage) {
return (
<MobileCreationDesktopGuide
onReturnHome={() => {
setIsMobileDesktopGuideOpen(false);
if (selectionStage === 'platform') {
return;
}
setSelectionStage('platform', { path: '/' });
}}
/>
);
}
if (selectionStage === 'image-editor') {
return (
<div className="image-editor-stage-shell flex h-full min-h-0 min-w-0 flex-col overflow-hidden">
@@ -469,22 +551,9 @@ export function PlatformEntryFlowShellImpl({
</div>
<div className="platform-mobile-bottom-dock min-w-0 shrink-0 lg:hidden">
<nav
className="platform-bottom-nav grid grid-cols-3"
className="platform-bottom-nav grid grid-cols-1"
aria-label="移动平台导航"
>
<ActiveBottomNavButton
active={isCreationStage}
emphasized={isCreationStage}
icon={Palette}
label="创作"
onClick={openCreation}
/>
<ActiveBottomNavButton
active={selectionStage === 'project'}
icon={FolderKanban}
label="项目"
onClick={openProjects}
/>
<ActiveBottomNavButton
active={isProfileStage}
icon={UserRound}