diff --git a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md index e1ab45503..d8b7d5064 100644 --- a/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md +++ b/docs/【玩法创作】创作主页与项目入口改版计划-2026-06-18.md @@ -129,6 +129,7 @@ Tab: - 桌面端从 `/creation` 切换到 `/project` 后,“项目”页签进入选中态,“创作”页签必须恢复为普通默认态,不保留创作主页的主按钮强调表现。 - `/project` 项目页头部必须提供 `返回主站` 按钮,点击后切回平台首页 `/`,行为与桌面侧栏切回推荐页保持一致。 - 移动端导航列表不展示“创作”和“项目”。 +- 移动端首次进入或刷新 `/` 平台首页时显示欢迎弹窗,文案提示移动端仅支持作品展示,体验创作工具请使用电脑端访问;点击 `好` 关闭后,本次页面生命周期内切换下方页签再回到首页不再弹出。 - 移动端直接访问 `/creation` 时显示桌面端打开引导,不加载创作工具主页;`/creation/` 继续直达对应玩法工作台。`/project` 暂不新增移动端拦截。 ## 实施拆分 @@ -195,6 +196,7 @@ git diff --check - 桌面导航显示“项目”,不显示“草稿”。 - `/project` 头部点击“返回主站”回到 `/`,并退出项目页主内容。 - 移动视口底部不显示“创作”和“项目”。 +- 移动端进入 `/` 首页时显示 `欢迎` 弹窗,正文包含移动端仅支持作品展示和电脑端访问提示,按钮为 `好`。 - 移动端直达 `/creation` 显示“请在桌面端打开创作主页”,点击“返回首页”回到 `/`。 - “我的”页没有项目快捷入口。 - 陶泥儿精选素材瀑布流不出现账号级非项目素材、公开作品补充、上传素材、mock 素材、假作者或假泥点成本。 diff --git a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md index 66c975cff..0dd894415 100644 --- a/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md +++ b/docs/【玩法创作】平台入口与玩法链路-2026-05-15.md @@ -202,7 +202,7 @@ RPG / 拼图等运行态存档仍以 `/api/profile/save-archives` 的后端列 跳一跳作品架删除入口必须走 `/api/creation/jump-hop/works/{profile_id}`,并通过 SpacetimeDB 同步删除 work profile、源 session、运行态 run 与事件,再刷新作品架和公开广场;不得只做前端本地隐藏。 -推荐页匿名游玩不再限定为跳一跳。移动端一级 `推荐` Tab 是内嵌运行态刷卡流,会自动选择推荐作品并启动对应玩法;桌面端首页不启动这套移动推荐运行态,而是渲染桌面发现壳,展示 `今日游戏`、`推荐`、`作品分类` 等桌面内容。推荐页候选顺序由前端轻量推荐算法 `platformRecommendation.ts` 统一生成:先按公开作品 key 去重,再使用公开读模型已有的精选来源、近 7 日游玩、点赞、改造、总游玩、发布时间新鲜度、封面和标签完整度做确定性评分,最后优先交错不同玩法类型;只要还有其它玩法候选,就不要连续推荐同一玩法,只有候选池已没有其它玩法时才允许同玩法相邻。该算法不得新增前端业务真相或绕过公开作品 read model。断点事实统一走 `platformEntryResponsive.ts` 的 `usePlatformDesktopLayout()`,平台壳和首页视图必须共用同一个判断,避免桌面发现页与移动推荐页同时挂载、重复触发请求或启动运行态。移动端推荐页拿到推荐作品列表后必须预加载每个作品的卡片封面、主封面和玩法兜底封面;启动或切换作品时先展示当前带玩法标签和标题的作品卡面遮罩,嵌入 runtime 在卡面下层加载,不得再从卡面闪切到另一层单独纯封面图。作品切换提交后,当前 runtime 遮罩接手已在屏幕上的卡面时必须瞬时贴合,不允许再执行“卡面到同一卡面”的淡入或重绘过渡;推荐页 runtime 必须通过统一 `ready` 门控等待对应运行态 run / profile、lazy runtime 组件和 runtime DOM 内图片资源都准备好,且必须持续观察后续新增图片、内联 `background-image` 和换签中的资源标记,不能只在首次挂载时扫描主图或封面;`ready` 返回 `true` 后才由外层放开游戏画面并只让卡面遮罩渐隐。遮罩层级必须高于并隔离下层 runtime,防止运行态 HUD、canvas 或高 `z-index` 子层穿透到封面上;ready 前不展示“加载中”文案,但封面内必须保留无文案加载动效或进度条,避免用户误以为卡片损坏,也不得把未准备好的运行态直接暴露给用户。切换推荐作品时,如果上一条作品的启动请求、退出收口或目标玩法 busy 状态尚未结束,应继续显示当前作品卡面遮罩并等待下一轮自动启动;只有目标作品启动明确失败时,才显示“作品暂时无法进入,请稍后再试。”这类失败态。推荐页内拼图通关后的同 run 相似作品推进不视为推荐作品切换,不能重新显示启动封面;如果需要跨公开作品进入下一关,则必须走推荐页统一切卡入口,不能复用拼图 runtime 的跨作品 handoff,也不能直接把当前 run 改写到另一个作品,`activeRecommendEntryKey` 只能由推荐页统一选择下一作品后更新。推荐页嵌入运行态启动时按真实身份分流:已登录用户或本地已有 access token 时继续使用账号 Bearer,但请求选项必须是 local auth impact,避免单卡 401 清空整站登录态;只有确认为匿名访客时才申请短期 Runtime Guest Token,并只把它作为局部请求头传给运行态客户端,不写入全局登录态、不触发 refresh,也不把匿名流量伪装成普通用户。当前覆盖矩阵为:跳一跳、视觉小说、抓大鹅 Match3D、方洞挑战、拼图、敲木鱼、大鱼吃小鱼、汪汪声浪。每个模板的启动请求、推荐页内后续运行态动作以及需要上报的 play/finish/leaderboard/next-level 类请求,都必须继续按该身份分流;公开读取入口仍可匿名读取,创作、个人作品、删除、发布、Remix 等账号/所有权动作仍保持普通用户鉴权。推荐 runtime 的 `none` / `background` / `runtime-guest` 请求计划和拼图 `default` / `isolated` runtime auth mode 由 `platformRecommendRuntimeAuthModel.ts` 统一判定,平台壳只负责读取 token、申请 Runtime Guest Token 和传递 request options。推荐 runtime 自动启动只由 `platformPublicGalleryFlow.ts` 输出 `noop` / `clear` / `start(entry)` 决策,平台壳只执行清空 state 或启动指定作品。 +推荐页匿名游玩不再限定为跳一跳。移动端一级 `推荐` Tab 是内嵌运行态刷卡流,会自动选择推荐作品并启动对应玩法;移动端首次进入或刷新 `/` 平台首页时弹出一次欢迎提示,说明移动端仅支持作品展示、体验创作工具需使用电脑端访问,用户点击 `好` 后关闭;本次页面生命周期内切换下方页签再回到首页不再弹出。桌面端首页不启动这套移动推荐运行态,而是渲染桌面发现壳,展示 `今日游戏`、`推荐`、`作品分类` 等桌面内容。推荐页候选顺序由前端轻量推荐算法 `platformRecommendation.ts` 统一生成:先按公开作品 key 去重,再使用公开读模型已有的精选来源、近 7 日游玩、点赞、改造、总游玩、发布时间新鲜度、封面和标签完整度做确定性评分,最后优先交错不同玩法类型;只要还有其它玩法候选,就不要连续推荐同一玩法,只有候选池已没有其它玩法时才允许同玩法相邻。该算法不得新增前端业务真相或绕过公开作品 read model。断点事实统一走 `platformEntryResponsive.ts` 的 `usePlatformDesktopLayout()`,平台壳和首页视图必须共用同一个判断,避免桌面发现页与移动推荐页同时挂载、重复触发请求或启动运行态。移动端推荐页拿到推荐作品列表后必须预加载每个作品的卡片封面、主封面和玩法兜底封面;启动或切换作品时先展示当前带玩法标签和标题的作品卡面遮罩,嵌入 runtime 在卡面下层加载,不得再从卡面闪切到另一层单独纯封面图。作品切换提交后,当前 runtime 遮罩接手已在屏幕上的卡面时必须瞬时贴合,不允许再执行“卡面到同一卡面”的淡入或重绘过渡;推荐页 runtime 必须通过统一 `ready` 门控等待对应运行态 run / profile、lazy runtime 组件和 runtime DOM 内图片资源都准备好,且必须持续观察后续新增图片、内联 `background-image` 和换签中的资源标记,不能只在首次挂载时扫描主图或封面;`ready` 返回 `true` 后才由外层放开游戏画面并只让卡面遮罩渐隐。遮罩层级必须高于并隔离下层 runtime,防止运行态 HUD、canvas 或高 `z-index` 子层穿透到封面上;ready 前不展示“加载中”文案,但封面内必须保留无文案加载动效或进度条,避免用户误以为卡片损坏,也不得把未准备好的运行态直接暴露给用户。切换推荐作品时,如果上一条作品的启动请求、退出收口或目标玩法 busy 状态尚未结束,应继续显示当前作品卡面遮罩并等待下一轮自动启动;只有目标作品启动明确失败时,才显示“作品暂时无法进入,请稍后再试。”这类失败态。推荐页内拼图通关后的同 run 相似作品推进不视为推荐作品切换,不能重新显示启动封面;如果需要跨公开作品进入下一关,则必须走推荐页统一切卡入口,不能复用拼图 runtime 的跨作品 handoff,也不能直接把当前 run 改写到另一个作品,`activeRecommendEntryKey` 只能由推荐页统一选择下一作品后更新。推荐页嵌入运行态启动时按真实身份分流:已登录用户或本地已有 access token 时继续使用账号 Bearer,但请求选项必须是 local auth impact,避免单卡 401 清空整站登录态;只有确认为匿名访客时才申请短期 Runtime Guest Token,并只把它作为局部请求头传给运行态客户端,不写入全局登录态、不触发 refresh,也不把匿名流量伪装成普通用户。当前覆盖矩阵为:跳一跳、视觉小说、抓大鹅 Match3D、方洞挑战、拼图、敲木鱼、大鱼吃小鱼、汪汪声浪。每个模板的启动请求、推荐页内后续运行态动作以及需要上报的 play/finish/leaderboard/next-level 类请求,都必须继续按该身份分流;公开读取入口仍可匿名读取,创作、个人作品、删除、发布、Remix 等账号/所有权动作仍保持普通用户鉴权。推荐 runtime 的 `none` / `background` / `runtime-guest` 请求计划和拼图 `default` / `isolated` runtime auth mode 由 `platformRecommendRuntimeAuthModel.ts` 统一判定,平台壳只负责读取 token、申请 Runtime Guest Token 和传递 request options。推荐 runtime 自动启动只由 `platformPublicGalleryFlow.ts` 输出 `noop` / `clear` / `start(entry)` 决策,平台壳只执行清空 state 或启动指定作品。 ## 敲木鱼 diff --git a/public/branding/mobile-home-welcome-taonier-ip.png b/public/branding/mobile-home-welcome-taonier-ip.png new file mode 100644 index 000000000..acb8e7deb Binary files /dev/null and b/public/branding/mobile-home-welcome-taonier-ip.png differ diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index f678d91c3..518037c8a 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -536,6 +536,10 @@ import { PlatformEntryHomeView, type PlatformHomeTab, } from './PlatformEntryHomeView'; +import { + PlatformMobileHomeWelcomeDialog, + shouldOpenMobileHomeWelcomeDialog, +} from './PlatformMobileHomeWelcomeDialog'; import { PlatformProfileReferralModal } from './PlatformProfileReferralModal'; import { usePlatformDesktopLayout } from './platformEntryResponsive'; import { @@ -1692,6 +1696,8 @@ export function PlatformEntryFlowShellImpl({ : 'platform-theme--light'; const isDesktopLayout = usePlatformDesktopLayout(); const [showCreationTypeModal, setShowCreationTypeModal] = useState(false); + const [isMobileHomeWelcomeDismissed, setIsMobileHomeWelcomeDismissed] = + useState(false); const [isCreationCommunityOpen, setIsCreationCommunityOpen] = useState(false); const [draftGenerationPointNotice, setDraftGenerationPointNotice] = @@ -15319,6 +15325,16 @@ export function PlatformEntryFlowShellImpl({ }, [platformBootstrap, selectionStage, setSelectionStage], ); + const shouldOpenMobileHomeWelcome = shouldOpenMobileHomeWelcomeDialog({ + isDesktopLayout, + selectionStage, + platformTab: platformBootstrap.platformTab, + pathname: + typeof window === 'undefined' + ? resolvePathForSelectionStage(selectionStage) + : window.location.pathname, + isDismissed: isMobileHomeWelcomeDismissed, + }); const platformHomeView = ( setPublishSharePayload(null)} /> + setIsMobileHomeWelcomeDismissed(true)} + /> { + expect( + shouldShowMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + pathname: '/', + }), + ).toBe(true); + expect( + shouldShowMobileHomeWelcomeDialog({ + isDesktopLayout: true, + selectionStage: 'platform', + platformTab: 'home', + pathname: '/', + }), + ).toBe(false); + expect( + shouldShowMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'category', + pathname: '/', + }), + ).toBe(false); + expect( + shouldShowMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'creation-home', + platformTab: 'home', + pathname: '/creation', + }), + ).toBe(false); +}); + +test('keeps mobile home welcome dismissed across bottom tab switches until refresh', () => { + expect( + shouldOpenMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + pathname: '/', + isDismissed: false, + }), + ).toBe(true); + expect( + shouldOpenMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'category', + pathname: '/', + isDismissed: true, + }), + ).toBe(false); + expect( + shouldOpenMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + pathname: '/', + isDismissed: true, + }), + ).toBe(false); + expect( + shouldOpenMobileHomeWelcomeDialog({ + isDesktopLayout: false, + selectionStage: 'platform', + platformTab: 'home', + pathname: '/', + isDismissed: false, + }), + ).toBe(true); +}); + +test('renders mobile home welcome copy and closes with the acknowledge button', () => { + const onClose = vi.fn(); + + const { container } = render( + , + ); + + expect(screen.getByRole('dialog', { name: '欢迎' })).toBeTruthy(); + expect(screen.getByRole('heading', { name: '欢迎' })).toBeTruthy(); + const copy = document.querySelector( + '.platform-mobile-home-welcome-dialog__copy', + ); + expect(copy?.textContent).toBe( + '尼嚎,新陶泥er!移动端仅支持作品展示体验创作工具请使用电脑端访问该地址', + ); + expect(copy?.querySelectorAll('br')).toHaveLength(2); + 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/PlatformMobileHomeWelcomeDialog.tsx b/src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx new file mode 100644 index 000000000..97e3afe27 --- /dev/null +++ b/src/components/platform-entry/PlatformMobileHomeWelcomeDialog.tsx @@ -0,0 +1,90 @@ +import { normalizeAppPath } from '../../routing/appPageRoutes'; +import { PlatformActionButton } from '../common/PlatformActionButton'; +import { UnifiedModal } from '../common/UnifiedModal'; +import type { SelectionStage } from './platformEntryTypes'; + +type MobileHomeWelcomeDecisionInput = { + isDesktopLayout: boolean; + selectionStage: SelectionStage; + platformTab: string; + pathname: string; +}; + +type MobileHomeWelcomeOpenInput = MobileHomeWelcomeDecisionInput & { + isDismissed: boolean; +}; + +type PlatformMobileHomeWelcomeDialogProps = { + open: boolean; + platformThemeClass: string; + onClose: () => void; +}; + +export function shouldShowMobileHomeWelcomeDialog({ + isDesktopLayout, + selectionStage, + platformTab, + pathname, +}: MobileHomeWelcomeDecisionInput) { + return ( + !isDesktopLayout && + selectionStage === 'platform' && + platformTab === 'home' && + normalizeAppPath(pathname) === '/' + ); +} + +export function shouldOpenMobileHomeWelcomeDialog({ + isDismissed, + ...decisionInput +}: MobileHomeWelcomeOpenInput) { + return !isDismissed && shouldShowMobileHomeWelcomeDialog(decisionInput); +} + +export function PlatformMobileHomeWelcomeDialog({ + open, + platformThemeClass, + onClose, +}: PlatformMobileHomeWelcomeDialogProps) { + return ( + + 好 + + } + > + +

欢迎

+

+ 尼嚎,新陶泥er! +
+ 移动端仅支持作品展示 +
+ 体验创作工具请使用电脑端访问该地址 +

+
+ ); +} diff --git a/src/index.css b/src/index.css index ada336823..41a737a7a 100644 --- a/src/index.css +++ b/src/index.css @@ -8822,6 +8822,63 @@ button.image-canvas-editor__reference-chip:disabled { box-shadow: 0 24px 80px rgba(0, 0, 0, 0.16); } +.platform-mobile-home-welcome-overlay { + background: + radial-gradient(circle at 50% 18%, rgba(255, 254, 252, 0.54), transparent 32%), + rgba(61, 31, 16, 0.24); +} + +.platform-mobile-home-welcome-dialog { + width: min(100%, 21rem); + overflow: hidden; + border-radius: 1.65rem; + border: 1px solid rgba(255, 255, 255, 0.68); + background: + radial-gradient(circle at 18% 0%, rgba(240, 203, 169, 0.28), transparent 38%), + var(--platform-modal-fill); + box-shadow: + 0 24px 58px rgba(112, 57, 30, 0.2), + inset 0 1px 0 rgba(255, 255, 255, 0.82); +} + +.platform-mobile-home-welcome-dialog__body { + display: flex; + flex-direction: column; + align-items: center; + padding: 1.5rem 1.35rem 0.85rem; + text-align: center; +} + +.platform-mobile-home-welcome-dialog__footer { + justify-content: center; + border-top: 0; + padding: 0.35rem 1.35rem 1.35rem; +} + +.platform-mobile-home-welcome-dialog__icon { + display: block; + width: 5.65rem; + height: 5.65rem; + object-fit: contain; + filter: drop-shadow(0 14px 20px rgba(182, 98, 63, 0.16)); +} + +.platform-mobile-home-welcome-dialog__title { + margin-top: 0.85rem; + color: var(--platform-text-strong); + font-size: 1.32rem; + font-weight: 900; + line-height: 1.18; +} + +.platform-mobile-home-welcome-dialog__copy { + margin-top: 0.65rem; + color: var(--platform-text-base); + font-size: 0.9rem; + font-weight: 700; + line-height: 1.75; +} + .platform-modal-backdrop { background: var(--platform-overlay-fill); color: var(--platform-text-strong);