调整桌面首页进入创作主页
桌面端打开根路径默认进入创作主页 移动端根路径继续保持推荐首页 补充首页初始路由测试与创作主页文档说明
This commit is contained in:
@@ -59,9 +59,9 @@
|
||||
## 2026-06-18 `/creation` 独立为陶泥儿创作工具主页
|
||||
|
||||
- 背景:图片画布项目已经成为独立项目资产,旧“创作”站内 Tab 和一级“草稿”入口不能清晰表达桌面端创作工具主页与项目管理入口。
|
||||
- 决策:桌面端新增独立 `/creation` 创作工具主页;顶级“创作”入口跳转 `/creation`,原“草稿”入口替换为“项目”并跳转 `/project`。移动端隐藏“创作”和“项目”入口,但不拦截直接 URL。页面文案统一使用“陶泥儿”,外部品牌只作为设计参考,不进入用户可见 UI、测试名、产品文案或验收口径。`陶泥儿精选` 是用户素材瀑布流,不展示玩法入口列表;创作入口事实源继续来自 `/api/creation-entry/config`,项目入口通过 `createEditorProject` 和 `/editor/canvas?projectid=xxx` 链路进入画布。
|
||||
- 决策:桌面端新增独立 `/creation` 创作工具主页,桌面端直接打开站点首页 `/` 时也默认展示新版创作主页;顶级“创作”入口跳转 `/creation`,原“草稿”入口替换为“项目”并跳转 `/project`。移动端首页 `/` 继续保持原推荐首页,移动端隐藏“创作”和“项目”入口,但不拦截直接 URL。页面文案统一使用“陶泥儿”,外部品牌只作为设计参考,不进入用户可见 UI、测试名、产品文案或验收口径。`陶泥儿精选` 是用户素材瀑布流,不展示玩法入口列表;创作入口事实源继续来自 `/api/creation-entry/config`,项目入口通过 `createEditorProject` 和 `/editor/canvas?projectid=xxx` 链路进入画布。
|
||||
- 影响范围:平台入口导航、`SelectionStage` 路由、`/creation` 首页、`/project` 项目入口、`/editor/canvas` 项目打开链路、“我的”页快捷入口和创作入口相关测试。
|
||||
- 验证方式:`/creation` 解析为创作主页,`/creation/<play>` 仍进入对应玩法工作台;桌面导航显示“创作 / 项目”且不显示“草稿”;移动端底部不显示“创作 / 项目”;页面不出现外站品牌或 `Discord` 字样;新建项目进入 `/editor/canvas?projectid=xxx`。
|
||||
- 验证方式:桌面 `/` 初始阶段和 `/creation` 解析为创作主页,移动端 `/` 仍是推荐首页,`/creation/<play>` 仍进入对应玩法工作台;桌面导航显示“创作 / 项目”且不显示“草稿”;移动端底部不显示“创作 / 项目”;页面不出现外站品牌或 `Discord` 字样;新建项目进入 `/editor/canvas?projectid=xxx`。
|
||||
- 关联文档:`docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md`、`docs/【玩法创作】平台入口与玩法链路-2026-05-15.md`。
|
||||
|
||||
## 2026-06-18 图片画布 Seedance 2.0 参考媒体提交边界
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
## 目标
|
||||
|
||||
- 新增桌面端独立 `/creation` 创作工具主页。
|
||||
- 桌面端直接打开站点首页 `/` 时默认展示新版创作主页内容;移动端继续保持原推荐首页。
|
||||
- 顶级导航中“创作”进入 `/creation`,“草稿”改为“项目”并进入 `/project`。
|
||||
- 移动端隐藏“创作”和“项目”入口,只保留浏览和个人相关入口。
|
||||
- 登录后在创作主页展示最近项目,并通过新建项目进入 `/editor/canvas?projectid=xxx`。
|
||||
@@ -112,6 +113,7 @@ Tab:
|
||||
|
||||
- 新增 `SelectionStage = 'creation-home'`。
|
||||
- `/creation` 映射到 `creation-home`。
|
||||
- 桌面端初始打开 `/` 时也进入 `creation-home`;移动端初始打开 `/` 仍进入 `platform` 推荐首页。
|
||||
- `/creation/<play>` 现有玩法工作台路由保持原有 stage 映射。
|
||||
- 桌面端点击“创作”时调用 `pushAppHistoryPath('/creation')`。
|
||||
- 桌面端原“草稿”入口改为“项目”,点击 `pushAppHistoryPath('/project')`。
|
||||
@@ -124,6 +126,7 @@ Tab:
|
||||
### 路由与导航
|
||||
|
||||
- 增加 `/creation` 路由映射和测试。
|
||||
- 增加桌面 `/` 初始阶段映射测试,确认不会影响移动端首页。
|
||||
- 调整桌面导航文案与点击行为。
|
||||
- 移动端导航移除“创作”和“项目”入口。
|
||||
- 保持现有 `/creation/<play>` 工作台、生成页、结果页和运行态路由不回归。
|
||||
@@ -172,6 +175,7 @@ git diff --check
|
||||
|
||||
浏览器回归:
|
||||
|
||||
- 桌面打开站点首页 `/`,首屏显示新版创作主页。
|
||||
- 桌面打开首页,点击“创作”进入 `/creation`。
|
||||
- `/creation` 显示 `陶泥儿 - 开启全民精品游戏创作` 和 `陶泥儿精选`。
|
||||
- 页面不出现外站品牌或 `Discord` 字样。
|
||||
|
||||
+78
-11
@@ -3,9 +3,27 @@
|
||||
import { render, screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import App from './App';
|
||||
import { resolveInitialSelectionStageFromPath } from './routing/appPageRoutes';
|
||||
|
||||
function mockMatchMedia(matches: boolean) {
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
configurable: true,
|
||||
writable: true,
|
||||
value: vi.fn().mockImplementation((query: string) => ({
|
||||
matches,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: vi.fn(),
|
||||
removeEventListener: vi.fn(),
|
||||
addListener: vi.fn(),
|
||||
removeListener: vi.fn(),
|
||||
dispatchEvent: vi.fn(),
|
||||
})),
|
||||
});
|
||||
}
|
||||
|
||||
vi.mock('./components/platform-entry/PlatformEntryFlowShell', async () => {
|
||||
const React = await import('react');
|
||||
@@ -14,18 +32,30 @@ vi.mock('./components/platform-entry/PlatformEntryFlowShell', async () => {
|
||||
>;
|
||||
|
||||
return {
|
||||
PlatformEntryFlowShell: ({ setSelectionStage }: PlatformEntryFlowShellProps) =>
|
||||
PlatformEntryFlowShell: ({
|
||||
selectionStage,
|
||||
setSelectionStage,
|
||||
}: PlatformEntryFlowShellProps) =>
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
onClick: () => {
|
||||
setSelectionStage('image-editor', {
|
||||
path: '/editor/canvas?projectid=project-from-test',
|
||||
});
|
||||
'div',
|
||||
null,
|
||||
React.createElement(
|
||||
'div',
|
||||
{ 'data-testid': 'selection-stage' },
|
||||
selectionStage,
|
||||
),
|
||||
React.createElement(
|
||||
'button',
|
||||
{
|
||||
type: 'button',
|
||||
onClick: () => {
|
||||
setSelectionStage('image-editor', {
|
||||
path: '/editor/canvas?projectid=project-from-test',
|
||||
});
|
||||
},
|
||||
},
|
||||
},
|
||||
'打开最近项目',
|
||||
'打开最近项目',
|
||||
),
|
||||
),
|
||||
};
|
||||
});
|
||||
@@ -34,8 +64,45 @@ vi.mock('./RpgRuntimeApp', () => ({
|
||||
RpgRuntimeApp: () => <div>运行态</div>,
|
||||
}));
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('resolveInitialSelectionStageFromPath', () => {
|
||||
it('opens the desktop root route on the creation home stage', () => {
|
||||
expect(resolveInitialSelectionStageFromPath('/', true)).toBe(
|
||||
'creation-home',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps the mobile root route on the platform stage', () => {
|
||||
expect(resolveInitialSelectionStageFromPath('/', false)).toBe('platform');
|
||||
});
|
||||
|
||||
it('keeps explicit routes mapped to their own stages on desktop', () => {
|
||||
expect(resolveInitialSelectionStageFromPath('/creation/puzzle', true)).toBe(
|
||||
'puzzle-agent-workspace',
|
||||
);
|
||||
expect(resolveInitialSelectionStageFromPath('/creation', true)).toBe(
|
||||
'creation-home',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('App navigation history', () => {
|
||||
it('renders the desktop home URL with the creation home stage', () => {
|
||||
mockMatchMedia(true);
|
||||
window.history.replaceState(null, '', '/');
|
||||
|
||||
render(<App />);
|
||||
|
||||
expect(screen.getByTestId('selection-stage').textContent).toBe(
|
||||
'creation-home',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps project canvas navigation as one history entry with projectid', async () => {
|
||||
mockMatchMedia(true);
|
||||
window.history.replaceState(null, '', '/creation');
|
||||
const pushStateSpy = vi.spyOn(window.history, 'pushState');
|
||||
const user = userEvent.setup();
|
||||
|
||||
+10
-3
@@ -9,6 +9,7 @@ import {
|
||||
|
||||
import { useAuthUi } from './components/auth/AuthUiContext';
|
||||
import { PlatformEntryFlowShell } from './components/platform-entry/PlatformEntryFlowShell';
|
||||
import { getInitialPlatformDesktopLayout } from './components/platform-entry/platformEntryResponsive';
|
||||
import type {
|
||||
CustomWorldRuntimeLaunchOptions,
|
||||
SelectionStage,
|
||||
@@ -19,8 +20,8 @@ import {
|
||||
normalizeAppPath,
|
||||
pushAppHistoryPath,
|
||||
readPublicWorkCodeFromLocationSearch,
|
||||
resolveInitialSelectionStageFromPath,
|
||||
resolvePathForSelectionStage,
|
||||
resolveSelectionStageFromPath,
|
||||
} from './routing/appPageRoutes';
|
||||
import type { RpgRuntimeAppIntent } from './RpgRuntimeApp';
|
||||
import type { CustomWorldProfile } from './types';
|
||||
@@ -59,7 +60,10 @@ export default function App() {
|
||||
isRpgRuntimeRoute(window.location.pathname),
|
||||
);
|
||||
const [selectionStage, setRawSelectionStage] = useState<SelectionStage>(() =>
|
||||
resolveSelectionStageFromPath(window.location.pathname),
|
||||
resolveInitialSelectionStageFromPath(
|
||||
window.location.pathname,
|
||||
getInitialPlatformDesktopLayout(),
|
||||
),
|
||||
);
|
||||
const [runtimeReturnStage, setRuntimeReturnStage] =
|
||||
useState<SelectionStage>('platform');
|
||||
@@ -84,7 +88,10 @@ export default function App() {
|
||||
|
||||
setIsRuntimeActive(false);
|
||||
setRawSelectionStage(
|
||||
resolveSelectionStageFromPath(window.location.pathname),
|
||||
resolveInitialSelectionStageFromPath(
|
||||
window.location.pathname,
|
||||
getInitialPlatformDesktopLayout(),
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -101,6 +101,20 @@ export function resolveSelectionStageFromPath(
|
||||
return ROUTE_STAGE_BY_PATH.get(normalizeAppPath(pathname)) ?? 'platform';
|
||||
}
|
||||
|
||||
export function resolveInitialSelectionStageFromPath(
|
||||
pathname: string,
|
||||
isDesktopLayout: boolean,
|
||||
): SelectionStage {
|
||||
const normalizedPath = normalizeAppPath(pathname);
|
||||
const selectionStage = resolveSelectionStageFromPath(pathname);
|
||||
|
||||
if (normalizedPath === '/' && isDesktopLayout) {
|
||||
return 'creation-home';
|
||||
}
|
||||
|
||||
return selectionStage;
|
||||
}
|
||||
|
||||
export function resolvePathForSelectionStage(stage: SelectionStage) {
|
||||
return APP_STAGE_ROUTES[stage] ?? APP_STAGE_ROUTES.platform;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user