diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index f67a51b93..d689dbe0a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -5706,7 +5706,7 @@ - 背景:旧创作模板退役时误把新版 `/creation`、桌面公共侧边栏和“我的”完整资料页一起缩减;只恢复视觉后,现役 profile client 又经 `rpg-entry` barrel 把旧作品库、旧 runtime request 和展示模型重新带入 Vite 与 TypeScript 图。 - 决策:桌面端继续使用原平台公共结构,一级导航固定为 `创作 / 项目 / 我的`;顶栏保留编辑器项目 / 素材搜索、泥点入口和账号胶囊;“我的”全宽保留资料编辑、陶泥号、三项统计、充值、兑换码、社区、反馈、通用设置、API Key 和法律信息。搜索只面向编辑器项目与公开编辑器素材,不恢复旧公开作品搜索。 - 依赖边界:公共 dashboard、钱包、充值、兑换码、邀请码、API Key 和设置请求迁入 `services/platform-entry`,公共账单展示迁入现役 profile model。Vite 新增退役模块 graph 门禁,ESLint 对现役源码禁止导入旧目录;目录 watch ignore、Tailwind source、tsconfig include 和 tree-shaking 都不能作为依赖隔离证明。 -- 路由与响应式边界:`/creation`、`/project`、`/profile` 都是可刷新、可前进 / 后退的稳定路由;桌面端使用侧边栏,移动端必须提供同样 `创作 / 项目 / 我的` 的三项底部 dock,不得因隐藏桌面侧边栏而丢失移动导航。 +- 路由与响应式边界(2026-08-03 纠正):`/creation`、`/project`、`/profile` 都是稳定路由,但旧模板退役不授权扩大移动端创作范围。桌面端使用 `创作 / 项目 / 我的` 侧边栏;移动端底部 dock 只保留“我的”,直达 `/creation`、`/project`、`/editor/canvas` 或从首页触发项目 / 画布动作时统一显示桌面端提示,不挂载创作主页、项目列表或图片画布。2026-07-18 同批加入的移动端三入口口径无效,不作为产品决策依据。 - 公共设置边界:`runtime_setting` 保持原表结构与历史数据,但它是音乐音量和平台主题的现役账号级公共能力,不归入旧玩法数据壳。鉴权后的 `GET/PUT /api/runtime/settings` 必须经 `spacetime-client` 调用 `get_runtime_setting_or_default` / `upsert_runtime_setting_and_return`;保留该路由不构成恢复旧 runtime API 的先例。 - 编译门禁:除旧业务目录外,`src/uiAssets.ts`、`src/types.ts`、`src/types/**`、`src/services/runtimeAudioFeedback.ts` 和 `src/services/publicWorkCode.ts` 也是顶层退役 module,必须同时退出 Vite module graph、TypeScript、ESLint 和 Vitest;`/audio/**`、`/chat.png`、`/fusion-pixel.ttf` 及旧 pixel / story-tab / 玩法 CSS 不得进入 dev 服务或生产产物。验收时必须同时检查 `tsc --listFilesOnly`、Vite 依赖图 / 产物和退役资产路径,不能只依赖 tree-shaking。 - Rust 产物边界:`module-runtime` 继续承载账号、钱包、公共设置、追踪和 feature gate,但 `CreationEntry*`、旧公开作品、存档、浏览历史与游玩统计 DTO / command / mapper / 规则必须退出实际 rlib;只保留历史表需要的 `RuntimeBrowseHistoryThemeMode`、完整保序的钱包流水来源枚举等持久化 ABI。`check:server-rs-ddd` 必须执行 `check:module-runtime-artifact`,同时验证旧符号和字面量为零、必要 ABI 仍存在,不能以源码存在 `#[cfg(any())]` 或路由未挂载代替产物证明。 diff --git a/docs/project-memory/shared-memory/development-workflow.md b/docs/project-memory/shared-memory/development-workflow.md index 8f26bcc0c..acc57dd6f 100644 --- a/docs/project-memory/shared-memory/development-workflow.md +++ b/docs/project-memory/shared-memory/development-workflow.md @@ -595,7 +595,7 @@ npm run check:server-rs-ddd - 移动端优先,再兼容网页端。 - 页面只展示后端返回的状态,不自行计算结论型业务状态。 -- 现役一级入口为 `/creation`、`/project`、`/profile`,桌面侧边栏和移动端底部 dock 都固定显示“创作 / 项目 / 我的”。`/creation` 只读取图片编辑器项目与 `GET /api/editor/showcase/resources`,不得重新接入旧模板入口配置、旧作品架或专属运行态。 +- 现役稳定路由为 `/creation`、`/project`、`/profile`。桌面侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只显示“我的”,并对 `/creation`、`/project`、`/editor/canvas` 及页面内项目 / 画布动作统一显示桌面端提示,不挂载创作工具、项目列表或图片画布。桌面端 `/creation` 只读取图片编辑器项目与 `GET /api/editor/showcase/resources`,不得重新接入旧模板入口配置、旧作品架或专属运行态。 - 旧创作模板目录和顶层旧业务模块必须持续退出 Vite、TypeScript、ESLint 与 Vitest;旧 `/api/creation-entry/config`、模板 API、公开作品详情和运行态 API 必须保持未挂载。SpacetimeDB 历史表、迁移白名单与必要兼容类型只作为数据壳保留,不得据此恢复业务逻辑。 - 优先复用现有面板、抽屉、弹窗,不新建独立大系统。 - 不在 UI 中默认写功能说明类文本。 diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index 75bc6edb5..2c846bb1a 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -21,7 +21,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台,把 A - 小程序 WebView 外壳:`miniprogram/`。 - 法律文本:`media/files/user_agreement.md`、`media/files/privacy_policy.md`、`media/files/disclaimer.md`。 -桌面端侧边栏和移动端底部 dock 的一级入口统一为 `创作 / 项目 / 我的`。`/creation` 是独立创作工具主页,`/project` 是画布项目入口,`/profile` 是“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力;刷新及浏览器前进 / 后退必须保持当前入口与选中态一致。 +桌面端侧边栏的一级入口为 `创作 / 项目 / 我的`;移动端底部 dock 只保留 `我的`。`/creation` 是桌面端独立创作工具主页,`/project` 是桌面端画布项目入口,`/profile` 是桌面端和移动端共用的“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力。移动端直达 `/creation`、`/project` 或 `/editor/canvas`,以及从首页触发项目 / 画布动作时,只显示桌面端创作提示,不挂载对应工具页面。 ## 当前后端路线 diff --git a/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md b/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md index 804bcd7a9..6f52dcf98 100644 --- a/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md +++ b/docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md @@ -26,7 +26,7 @@ - 为历史审计、迁移、资产归属核对所必需的最小只读表定义;不得借兼容读取重新暴露旧创作、发布、公开详情或运行接口。 - 编辑器、项目、账号、钱包、资产、HostBridge、运维和安全等平台公共能力。 - 通用 `feature_gate_config`、`GET/PUT /admin/api/feature-gates` 与后台 `#gray-release` 控制页。灰度页只读取通用 gate,不再请求旧 `/admin/api/creation-entry/config`,固定目标只登记现役功能;不得恢复 `creation-entry:*` 动态目标。 -- 新版 `/creation` 创作工具主页、`/project` 项目入口、稳定的 `/profile` 个人页路由、`creation-home` 展示组件与现役静态资产。桌面端保留“创作 / 项目 / 我的”公共侧边栏,移动端保留同样三项的底部 dock;“我的”保留头像 / 昵称编辑、陶泥号复制、钱包与账单、统计、充值、兑换码、玩家社区、反馈、通用设置、开发者 API Key 和法律信息,不恢复旧模板入口、旧作品架或生成队列。 +- 新版 `/creation` 创作工具主页、`/project` 项目入口、稳定的 `/profile` 个人页路由、`creation-home` 展示组件与现役静态资产。桌面端保留“创作 / 项目 / 我的”公共侧边栏;移动端底部 dock 只保留“我的”,不得因退役旧模板而扩大移动端创作范围。“我的”保留头像 / 昵称编辑、陶泥号复制、钱包与账单、统计、充值、兑换码、玩家社区、反馈、通用设置、开发者 API Key 和法律信息,不恢复旧模板入口、旧作品架或生成队列。 - `runtime_setting` 是账号级公共设置事实,不属于旧模板运行态。原表结构和数据不变,继续由鉴权后的 `GET/PUT /api/runtime/settings`、`get_runtime_setting_or_default` 与 `upsert_runtime_setting_and_return` procedure 支撑音乐音量和平台主题读写。 - 旧页面、测试、素材、handler、service、worker、生成 bindings 和纯业务 crate 的源码目录;它们仅用于历史追溯,不属于任何正式入口或编译目标。 @@ -65,7 +65,7 @@ ## 验收 - 旧 URL 不再命中旧页面或后端路由。 -- `/creation`、`/project` 与 `/profile` 在桌面端显示“创作 / 项目 / 我的”公共侧边栏,在 `390x844` 等移动视口显示同样三项的底部 dock;点击、刷新及浏览器前进 / 后退均保持路由与选中态一致。新创作主页只调用编辑器项目和公开编辑器素材接口;顶栏保持现役搜索、公共泥点入口与账号胶囊,不重新拼装平行账号按钮组。 +- `/creation`、`/project` 与 `/profile` 在桌面端显示“创作 / 项目 / 我的”公共侧边栏;在 `390x844` 等移动视口,底部 dock 只显示“我的”。移动端直达 `/creation`、`/project`、`/editor/canvas` 时显示桌面端创作提示,且不得挂载创作主页、项目列表或图片画布;移动端首页触发项目或画布动作时使用同一门禁。桌面端新创作主页只调用编辑器项目和公开编辑器素材接口;顶栏保持现役搜索、公共泥点入口与账号胶囊,不重新拼装平行账号按钮组。 - “我的”桌面布局按原平台公共资料页全宽展示四个常用入口、两行设置和法律栏;头像、昵称、复制、充值、兑换码、社区、反馈、API Key 等入口可用,但不发起旧模板、旧公开作品或旧运行态请求。 - 鉴权访问 `GET/PUT /api/runtime/settings` 不得返回 404,读写必须经 `spacetime-client` 调用现役 settings procedure;未鉴权请求返回 401,不恢复任何旧运行态设置路由。 - `tsc --listFilesOnly` 与 Vite 干净加载均不得出现旧业务目录、上述顶层退役 module 或小程序旧订阅授权实现。 diff --git a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md index 053e616b7..0a3214591 100644 --- a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md +++ b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md @@ -1,6 +1,6 @@ # 创作主页与项目入口改版计划 -> 2026-07-18 退役覆盖:本文关于旧模板入口、`/creation/`、移动端隐藏“创作 / 项目”和 `/api/creation-entry/config` 的内容均已被后续实现替代,只保留为阶段设计记录。现役口径是桌面侧边栏与移动端底部 dock 都显示“创作 / 项目 / 我的”,稳定路由为 `/creation`、`/project`、`/profile`;旧模板业务只保留历史数据壳。当前实现与验收以 `docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md` 为准。 +> 2026-08-03 纠正:2026-07-18 的旧模板退役只替代本文关于旧模板入口、`/creation/` 和 `/api/creation-entry/config` 的内容,不替代“移动端隐藏创作 / 项目并阻止进入画布”的既有边界。现役稳定路由仍为 `/creation`、`/project`、`/profile`,但创作主页、项目管理和图片画布只在桌面端挂载;移动端底部 dock 只保留“我的”,触发创作工具时显示桌面端提示。当前实现与验收以 `docs/technical/【架构下线】旧创作模板业务退役方案-2026-07-17.md` 为准。 日期:2026-06-18 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 013d80523..875a7a0ea 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -12,7 +12,7 @@ - `/project` 展示当前账号的图片编辑器项目,项目卡继续进入 `/editor/canvas`。 - `/profile` 是“我的”稳定路由,保留头像与昵称编辑、陶泥号复制、泥点余额与账单、累计统计、泥点充值、兑换码、玩家社区、反馈与建议、通用设置、开发者 API Key 和法律信息等平台公共能力。 - 桌面顶栏保留现役项目 / 素材搜索、泥点入口和账号胶囊。搜索只筛选当前编辑器项目与已读取的公开编辑器素材,不恢复旧公开作品号搜索、旧广场、旧作品详情或旧运行态。 -- 桌面端使用公共侧边栏,移动端使用同样包含“创作 / 项目 / 我的”的三项底部 dock;点击、刷新及浏览器前进 / 后退都必须保持 URL、标题和选中态一致。 +- 桌面端公共侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只保留“我的”,不暴露“创作 / 项目”。移动端直达 `/creation`、`/project` 或 `/editor/canvas` 时显示桌面端创作提示,不挂载创作主页、项目列表或图片画布;从移动端首页触发项目或画布动作时也只显示同一提示。 现役入口和公共资料能力只能依赖 `creation-home`、`project`、`image-editor`、公共组件及 `services/platform-entry` 等现役模块。Vite 模块门禁会拒绝 `components/rpg-entry`、`services/rpg-entry`、旧玩法目录和旧平台业务模块进入依赖图;Tailwind `@source`、TypeScript `include`、ESLint ignore 或 Vite watch ignore 都不能替代这条运行时依赖门禁。 diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx index 0d8f2ea0a..bf527b766 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx @@ -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 ( + 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( , ); @@ -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( + , + ); + + 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(); + + 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( diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx index dac883983..8cd069729 100644 --- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx +++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx @@ -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 ( +
+ + + +

+ 请在桌面端打开创作工具 +

+

+ 图片画布、项目管理和素材库需要更大的操作空间。 +

+ + +
+
+ ); +} + 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 ( + { + setIsMobileDesktopGuideOpen(false); + if (selectionStage === 'platform') { + return; + } + setSelectionStage('platform', { path: '/' }); + }} + /> + ); + } + if (selectionStage === 'image-editor') { return (
@@ -469,22 +551,9 @@ export function PlatformEntryFlowShellImpl({