diff --git a/.eslintrc.cjs b/.eslintrc.cjs
index 3b4cf7c4d..3dbd5cd07 100644
--- a/.eslintrc.cjs
+++ b/.eslintrc.cjs
@@ -46,6 +46,7 @@ module.exports = {
{
files: [
'apps/admin-web/src/pages/*.tsx',
+ 'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx',
'src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx',
],
rules: {
@@ -72,6 +73,7 @@ module.exports = {
'src/components/image-editor/**/*.{ts,tsx}',
'src/components/project/**/*.{ts,tsx}',
'src/components/platform-entry/PlatformActiveProfileView*.tsx',
+ 'src/components/platform-entry/PlatformActiveMobileWelcomeDialog*.tsx',
'src/components/platform-entry/PlatformEntryActiveFlowShell*.tsx',
'src/components/platform-entry/PlatformEntryFlowShell.tsx',
'src/components/platform-entry/PlatformProfileApiKeysModal.tsx',
diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md
index 2c846bb1a..322461402 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` 是桌面端和移动端共用的“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力。移动端直达 `/creation`、`/project` 或 `/editor/canvas`,以及从首页触发项目 / 画布动作时,只显示桌面端创作提示,不挂载对应工具页面。
+桌面端侧边栏的一级入口为 `创作 / 项目 / 我的`;移动端底部 dock 只保留 `我的`,根入口默认展示“我的”并保持该 Tab 选中。移动端每次进入站点壳时先显示原有 IP 欢迎遮罩,明确“移动端仅支持作品展示”,遮罩只能通过“好”按钮关闭,关闭后本次页面生命周期内不再重复,刷新后重新显示。`/creation` 是桌面端独立创作工具主页,`/project` 是桌面端画布项目入口,`/profile` 是桌面端和移动端共用的“我的”稳定路由,继续承载账号、钱包、统计和通用设置等平台公共能力。移动端直达 `/creation`、`/project` 或 `/editor/canvas`,以及从首页触发项目 / 画布动作时,只显示与移除前一致的桌面端创作主页提示,不挂载对应工具页面;提示页底部继续保留唯一的“我的”Tab 作为返回入口。
## 当前后端路线
diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
index 875a7a0ea..3a548f4c4 100644
--- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
+++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md
@@ -12,7 +12,7 @@
- `/project` 展示当前账号的图片编辑器项目,项目卡继续进入 `/editor/canvas`。
- `/profile` 是“我的”稳定路由,保留头像与昵称编辑、陶泥号复制、泥点余额与账单、累计统计、泥点充值、兑换码、玩家社区、反馈与建议、通用设置、开发者 API Key 和法律信息等平台公共能力。
- 桌面顶栏保留现役项目 / 素材搜索、泥点入口和账号胶囊。搜索只筛选当前编辑器项目与已读取的公开编辑器素材,不恢复旧公开作品号搜索、旧广场、旧作品详情或旧运行态。
-- 桌面端公共侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只保留“我的”,不暴露“创作 / 项目”。移动端直达 `/creation`、`/project` 或 `/editor/canvas` 时显示桌面端创作提示,不挂载创作主页、项目列表或图片画布;从移动端首页触发项目或画布动作时也只显示同一提示。
+- 桌面端公共侧边栏固定显示“创作 / 项目 / 我的”;移动端底部 dock 只保留“我的”,不暴露“创作 / 项目”,根入口默认展示“我的”并保持该 Tab 选中。移动端每次进入站点壳时先显示移除前的 IP 欢迎遮罩,明确“移动端仅支持作品展示”;遮罩不允许通过背景或 Escape 关闭,只能点击“好”,关闭后本次页面生命周期内不再重复,刷新后重新显示。移动端直达 `/creation`、`/project` 或 `/editor/canvas` 时显示移除前的桌面端创作主页提示,不挂载创作主页、项目列表或图片画布;提示页底部继续保留唯一的“我的”Tab 作为返回入口。三类工具统一阻断沿用 2026-08-03 的现役产品边界,不回退为旧源码只拦截 `/creation` 的较弱行为。
现役入口和公共资料能力只能依赖 `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/scripts/vite-retired-css-plugin.test.ts b/scripts/vite-retired-css-plugin.test.ts
index cabf5f58f..2bc063111 100644
--- a/scripts/vite-retired-css-plugin.test.ts
+++ b/scripts/vite-retired-css-plugin.test.ts
@@ -311,5 +311,10 @@ describe('retired creation template asset boundary', () => {
isForbiddenPublicAssetPath('/generated-editor-images/asset.png'),
).toBe(true);
expect(isForbiddenPublicAssetPath('/creation-home/logo.png')).toBe(false);
+ expect(
+ isForbiddenPublicAssetPath(
+ '/branding/mobile-home-welcome-taonier-ip.png',
+ ),
+ ).toBe(false);
});
});
diff --git a/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.test.tsx b/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.test.tsx
new file mode 100644
index 000000000..fcde1abd6
--- /dev/null
+++ b/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.test.tsx
@@ -0,0 +1,97 @@
+/* @vitest-environment jsdom */
+
+import { fireEvent, render, screen } from '@testing-library/react';
+import { expect, test, vi } from 'vitest';
+
+import {
+ PlatformActiveMobileWelcomeDialog,
+ shouldOpenActiveMobileWelcomeDialog,
+ shouldShowActiveMobileWelcomeDialog,
+} from './PlatformActiveMobileWelcomeDialog';
+
+test('shows the active mobile welcome when entering the mobile site shell', () => {
+ expect(
+ shouldShowActiveMobileWelcomeDialog({
+ isDesktopLayout: false,
+ selectionStage: 'platform',
+ platformTab: 'platform',
+ pathname: '/',
+ }),
+ ).toBe(true);
+ expect(
+ shouldShowActiveMobileWelcomeDialog({
+ isDesktopLayout: true,
+ selectionStage: 'platform',
+ platformTab: 'platform',
+ pathname: '/',
+ }),
+ ).toBe(false);
+ expect(
+ shouldShowActiveMobileWelcomeDialog({
+ isDesktopLayout: false,
+ selectionStage: 'profile',
+ platformTab: 'profile',
+ pathname: '/profile',
+ }),
+ ).toBe(true);
+ expect(
+ shouldShowActiveMobileWelcomeDialog({
+ isDesktopLayout: false,
+ selectionStage: 'creation-home',
+ platformTab: 'creation-home',
+ pathname: '/creation',
+ }),
+ ).toBe(true);
+});
+
+test('keeps the active mobile welcome dismissed across site navigation', () => {
+ expect(
+ shouldOpenActiveMobileWelcomeDialog({
+ isDesktopLayout: false,
+ selectionStage: 'platform',
+ platformTab: 'platform',
+ pathname: '/',
+ isDismissed: false,
+ }),
+ ).toBe(true);
+ expect(
+ shouldOpenActiveMobileWelcomeDialog({
+ isDesktopLayout: false,
+ selectionStage: 'profile',
+ platformTab: 'profile',
+ pathname: '/profile',
+ isDismissed: true,
+ }),
+ ).toBe(false);
+});
+
+test('renders the restored copy and closes with the acknowledge button', () => {
+ const onClose = vi.fn();
+
+ render(
+ ,
+ );
+
+ expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy();
+ expect(
+ screen.getByRole('heading', { level: 2, name: '欢迎' }),
+ ).toBeTruthy();
+ expect(
+ document.querySelector('.platform-mobile-home-welcome-dialog__copy')
+ ?.textContent,
+ ).toBe(
+ '尼嚎,新陶泥er!移动端仅支持作品展示体验创作工具请使用电脑端访问该地址',
+ );
+ expect(
+ document
+ .querySelector('.platform-mobile-home-welcome-dialog__icon')
+ ?.getAttribute('src'),
+ ).toBe('/branding/mobile-home-welcome-taonier-ip.png');
+
+ fireEvent.click(screen.getByRole('button', { name: '好' }));
+ expect(onClose).toHaveBeenCalledTimes(1);
+});
diff --git a/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx b/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx
new file mode 100644
index 000000000..16aef64b9
--- /dev/null
+++ b/src/components/platform-entry/PlatformActiveMobileWelcomeDialog.tsx
@@ -0,0 +1,82 @@
+import { PlatformActionButton } from '../common/PlatformActionButton';
+import { UnifiedModal } from '../common/UnifiedModal';
+
+type MobileHomeWelcomeDecisionInput = {
+ isDesktopLayout: boolean;
+ selectionStage: string;
+ platformTab: string;
+ pathname: string;
+};
+
+type MobileHomeWelcomeOpenInput = MobileHomeWelcomeDecisionInput & {
+ isDismissed: boolean;
+};
+
+type PlatformActiveMobileWelcomeDialogProps = {
+ open: boolean;
+ platformThemeClass: string;
+ onClose: () => void;
+};
+
+export function shouldShowActiveMobileWelcomeDialog({
+ isDesktopLayout,
+}: MobileHomeWelcomeDecisionInput) {
+ return !isDesktopLayout;
+}
+
+export function shouldOpenActiveMobileWelcomeDialog({
+ isDismissed,
+ ...decisionInput
+}: MobileHomeWelcomeOpenInput) {
+ return (
+ !isDismissed && shouldShowActiveMobileWelcomeDialog(decisionInput)
+ );
+}
+
+export function PlatformActiveMobileWelcomeDialog({
+ open,
+ platformThemeClass,
+ onClose,
+}: PlatformActiveMobileWelcomeDialogProps) {
+ return (
+
+ 好
+
+ }
+ >
+
+ 欢迎
+
+ 尼嚎,新陶泥er!
+
+ 移动端仅支持作品展示
+
+ 体验创作工具请使用电脑端访问该地址
+
+
+ );
+}
diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
index eef0b0e1d..1b6e2e872 100644
--- a/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
+++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx
@@ -483,6 +483,15 @@ describe('PlatformEntryActiveFlowShell', () => {
.getAllByRole('button')
.map((button) => button.getAttribute('aria-label')),
).toEqual(['我的']);
+ expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
+ expect(
+ within(navigation)
+ .getByRole('button', { name: '我的' })
+ .getAttribute('aria-current'),
+ ).toBe('page');
+ expect(
+ screen.queryByRole('main', { name: '陶泥儿创作主页' }),
+ ).toBeNull();
fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
expect(window.location.pathname).toBe('/profile');
@@ -491,6 +500,48 @@ describe('PlatformEntryActiveFlowShell', () => {
});
});
+ it('restores the original blocking welcome on mobile until acknowledged', async () => {
+ responsiveMock.isDesktopLayout = false;
+ const { rerender } = render(
+ ,
+ );
+
+ expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy();
+ expect(
+ screen.getByText(
+ '尼嚎,新陶泥er!移动端仅支持作品展示体验创作工具请使用电脑端访问该地址',
+ ),
+ ).toBeTruthy();
+ expect(
+ document
+ .querySelector('.platform-mobile-home-welcome-dialog__icon')
+ ?.getAttribute('src'),
+ ).toBe('/branding/mobile-home-welcome-taonier-ip.png');
+
+ fireEvent.click(screen.getByRole('button', { name: '好' }));
+ expect(screen.queryByRole('dialog', { name: '欢迎' })).toBeNull();
+ expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
+ const navigation = screen.getByRole('navigation', {
+ name: '移动平台导航',
+ });
+ expect(
+ within(navigation)
+ .getByRole('button', { name: '我的' })
+ .getAttribute('aria-current'),
+ ).toBe('page');
+
+ rerender(
+ ,
+ );
+ expect(screen.queryByRole('dialog', { name: '欢迎' })).toBeNull();
+ });
+
it.each(['creation-home', 'project', 'image-editor'] as const)(
'shows the desktop guide instead of mounting the %s page on mobile',
async (selectionStage) => {
@@ -506,25 +557,52 @@ describe('PlatformEntryActiveFlowShell', () => {
expect(
await screen.findByRole('main', { name: '桌面端创作提示' }),
).toBeTruthy();
- expect(screen.queryByRole('main', { name: '陶泥儿创作主页' })).toBeNull();
+ expect(screen.getByText('请在桌面端打开创作主页')).toBeTruthy();
+ expect(
+ screen.queryByRole('main', { name: '陶泥儿创作主页' }),
+ ).toBeNull();
expect(screen.queryByRole('main', { name: '项目' })).toBeNull();
- expect(screen.queryByRole('main', { name: '图片画布编辑器' })).toBeNull();
+ expect(
+ screen.queryByRole('main', { name: '图片画布编辑器' }),
+ ).toBeNull();
+ const navigation = screen.getByRole('navigation', {
+ name: '移动平台导航',
+ });
+ expect(
+ within(navigation)
+ .getAllByRole('button')
+ .map((button) => button.getAttribute('aria-label')),
+ ).toEqual(['我的']);
+ expect(
+ within(navigation)
+ .getByRole('button', { name: '我的' })
+ .getAttribute('aria-current'),
+ ).toBeNull();
},
);
- it('shows the desktop guide instead of the editor after opening a canvas tool from the mobile root', async () => {
+ it('returns from the mobile tool guide through the only profile tab', async () => {
+ responsiveMock.isDesktopLayout = false;
+ render();
+
+ fireEvent.click(screen.getByRole('button', { name: '好' }));
+ const navigation = screen.getByRole('navigation', {
+ name: '移动平台导航',
+ });
+ fireEvent.click(within(navigation).getByRole('button', { name: '我的' }));
+
+ expect(window.location.pathname).toBe('/profile');
+ expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
+ });
+
+ it('does not mount creation actions on 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();
+ expect(await screen.findByRole('main', { name: '我的' })).toBeTruthy();
+ expect(screen.queryByRole('button', { name: '打开音乐工具' })).toBeNull();
+ expect(screen.queryByRole('main', { name: '陶泥儿创作主页' })).toBeNull();
});
it('switches between creation and projects and passes search to the active page', async () => {
diff --git a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
index 32310a11d..dd9511914 100644
--- a/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
+++ b/src/components/platform-entry/PlatformEntryActiveFlowShell.tsx
@@ -30,6 +30,10 @@ import { useAuthUi } from '../auth/AuthUiContext';
import { FLOATING_FEEDBACK_FORM_URL } from '../common/floatingFeedbackEntryModel';
import { PlatformActionButton } from '../common/PlatformActionButton';
import { PlatformSubpanel } from '../common/PlatformSubpanel';
+import {
+ PlatformActiveMobileWelcomeDialog,
+ shouldOpenActiveMobileWelcomeDialog,
+} from './PlatformActiveMobileWelcomeDialog';
import {
resolveActivePublicUserCode,
resolveActiveUserAvatarLabel,
@@ -137,6 +141,30 @@ function ActiveBottomNavButton({
);
}
+function MobileProfileDock({
+ active,
+ onOpenProfile,
+}: {
+ active: boolean;
+ onOpenProfile: () => void;
+}) {
+ return (
+
+ );
+}
+
function ActivePlatformBrand() {
return (
- 请在桌面端打开创作工具
+ 请在桌面端打开创作主页
图片画布、项目管理和素材库需要更大的操作空间。
@@ -224,7 +252,21 @@ export function PlatformEntryFlowShellImpl({
const [activeSearchKeyword, setActiveSearchKeyword] = useState('');
const [isMobileDesktopGuideOpen, setIsMobileDesktopGuideOpen] =
useState(false);
+ const [isMobileHomeWelcomeDismissed, setIsMobileHomeWelcomeDismissed] =
+ useState(false);
const isDesktopLayout = usePlatformDesktopLayout();
+ const platformThemeClass =
+ authUi?.platformTheme === 'dark'
+ ? 'platform-theme--dark'
+ : 'platform-theme--light';
+ const shouldOpenMobileHomeWelcome = shouldOpenActiveMobileWelcomeDialog({
+ isDesktopLayout,
+ selectionStage,
+ platformTab: selectionStage,
+ pathname:
+ typeof window === 'undefined' ? '/' : window.location.pathname,
+ isDismissed: isMobileHomeWelcomeDismissed,
+ });
const currentWalletOwnerUserId =
authUi?.canAccessProtectedData && authUi.user?.id ? authUi.user.id : null;
const walletOwnerUserId = usePlatformWalletStore(
@@ -276,7 +318,9 @@ export function PlatformEntryFlowShellImpl({
void refreshDashboard();
}, [refreshDashboard]);
- const isProfileStage = selectionStage === 'profile';
+ const isProfileStage =
+ selectionStage === 'profile' ||
+ (!isDesktopLayout && selectionStage === 'platform');
const profileCenter = usePlatformProfileCenterController({
activeTab: isProfileStage ? 'profile' : 'project',
@@ -348,15 +392,23 @@ export function PlatformEntryFlowShellImpl({
if ((!isDesktopLayout && isMobileDesktopGuideOpen) || isMobileToolStage) {
return (
- {
- setIsMobileDesktopGuideOpen(false);
- if (selectionStage === 'platform') {
- return;
- }
- setSelectionStage('platform', { path: '/' });
- }}
- />
+ <>
+ {
+ setIsMobileDesktopGuideOpen(false);
+ if (selectionStage === 'platform') {
+ return;
+ }
+ setSelectionStage('platform', { path: '/' });
+ }}
+ />
+
+ setIsMobileHomeWelcomeDismissed(true)}
+ />
+ >
);
}
@@ -580,19 +632,10 @@ export function PlatformEntryFlowShellImpl({
-
+
@@ -661,6 +704,11 @@ export function PlatformEntryFlowShellImpl({
orderId={profileCenter.wechatRechargeOrderConfirmationState.orderId}
/>
) : null}
+ setIsMobileHomeWelcomeDismissed(true)}
+ />
>
);
}
diff --git a/vite.config.ts b/vite.config.ts
index 2db56028a..b0d0a6c17 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -190,7 +190,6 @@ const ACTIVE_TAILWIND_SOURCES = `@import 'tailwindcss' source(none);
const RETIRED_PUBLIC_ASSET_PATHS = [
'/audio',
'/branding/jump-hop-taonier-character.png',
- '/branding/mobile-home-welcome-taonier-ip.png',
'/anthro-cat-illustrations',
'/bark-battle-assets',
'/character',
diff --git a/vitest.config.ts b/vitest.config.ts
index 9ee1ef4ba..dc7fa68f8 100644
--- a/vitest.config.ts
+++ b/vitest.config.ts
@@ -65,6 +65,7 @@ export default defineConfig({
'src/components/image-editor/**/*.test.tsx',
'src/components/project/**/*.test.ts',
'src/components/project/**/*.test.tsx',
+ 'src/components/platform-entry/PlatformActiveMobileWelcomeDialog.test.tsx',
'src/components/platform-entry/PlatformActiveProfileView.test.tsx',
'src/components/platform-entry/PlatformEntryActiveFlowShell.test.tsx',
'src/components/platform-entry/PlatformProfileModalShell.test.tsx',