From d489488ca2c75a09b2abc3a42ef0c64c792f89b0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 5 Jun 2026 16:19:35 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A8=B3=E5=AE=9A=E6=8E=A8=E8=8D=90?= =?UTF-8?q?=E9=A1=B5=E6=8B=BC=E5=9B=BE=E4=B8=8B=E4=B8=80=E5=85=B3=E4=BD=93?= =?UTF-8?q?=E9=AA=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .hermes/shared-memory/decision-log.md | 2 +- ...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 4 +- .../PlatformEntryFlowShellImpl.tsx | 49 ++++--- .../RpgEntryHomeView.recharge.test.tsx | 128 +++++++++++++++++- src/components/rpg-entry/RpgEntryHomeView.tsx | 125 ++++++++++++++++- src/index.css | 25 ++++ 6 files changed, 299 insertions(+), 34 deletions(-) diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 4888ef7b..04be2504 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -207,7 +207,7 @@ ## 2026-05-26 推è页拼图下一关 pending æ—¶ä¿ç•™å½“å‰è¿è¡Œæ€ - 背景:推èé¡µåµŒå…¥æ‹¼å›¾åœ¨ç‚¹å‡»â€œä¸‹ä¸€å…³â€æ—¶ï¼Œ`advancePuzzleNextLevel` çš„æœåŠ¡ç«¯è¯·æ±‚ä¼šçŸ­æš‚å¤„äºŽ pending。旧逻辑把推èå¡çš„ `isStartingRecommendEntry` 和拼图局部 busy 混在一起,导致外层直接切回“加载中...â€ï¼ŒæŠŠå½“å‰ `PuzzleRuntimeShell` 一起å¸è½½ï¼Œè§†è§‰ä¸Šåƒæ˜¯åˆ‡å…³é—ªå›žã€‚ -- 决策:推è页嵌入拼图切关 pending 期间必须ä¿ç•™å½“å‰è¿è¡Œæ€ä¸Žæ£‹ç›˜ï¼Œåªè®©æ‹¼å›¾å£³å†…部 busy è¡¨çŽ°æ‰¿æŽ¥åŒæ­¥ï¼›`isStartingRecommendEntry` åªè¡¨ç¤ºæŽ¨è作å“尚未真正å¯åŠ¨å‡ºæ¥ï¼Œä¸å†æŠŠå·²æœ‰åµŒå…¥æ‹¼å›¾ run 的局部 busy ä¸€å¹¶å½“æˆæ•´å¡åŠ è½½æ€ã€‚若下一关è½åˆ°ç›¸ä¼¼ä½œå“,å‰ç«¯è¿˜å¿…须把新作å“写回推èç¼“å­˜å¹¶åŒæ­¥ `activeRecommendEntryKey`,é¿å…è¿è¡Œæ€è¿›å…¥æ–°ä½œå“但推èå¡å…ƒä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和åŽç»­â€œä¸‹ä¸€ä¸ªâ€ä»é”šå®šæ—§ä½œå“。 +- 决策:推è页嵌入拼图切关 pending 期间必须ä¿ç•™å½“å‰è¿è¡Œæ€ä¸Žæ£‹ç›˜ï¼Œåªè®©æ‹¼å›¾å£³å†…部 busy è¡¨çŽ°æ‰¿æŽ¥åŒæ­¥ï¼›`isStartingRecommendEntry` åªè¡¨ç¤ºæŽ¨è作å“尚未真正å¯åŠ¨å‡ºæ¥ï¼Œä¸å†æŠŠå·²æœ‰åµŒå…¥æ‹¼å›¾ run 的局部 busy ä¸€å¹¶å½“æˆæ•´å¡åŠ è½½æ€ã€‚若下一关è½åˆ°ç›¸ä¼¼ä½œå“,å‰ç«¯è¿˜å¿…须把新作å“写回推èç¼“å­˜å¹¶åŒæ­¥ `activeRecommendEntryKey`,é¿å…è¿è¡Œæ€è¿›å…¥æ–°ä½œå“但推èå¡å…ƒä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和åŽç»­â€œä¸‹ä¸€ä¸ªâ€ä»é”šå®šæ—§ä½œå“ï¼›ä½†è¿™ä¸ªåŒæ­¥ä»å±žäºŽåŒä¸€ä¸ª run 内部推进,ä¸å¾—è§¦å‘æŽ¨è rail 切å¡åŠ¨ç”»ã€çºµå‘ä½ç§»æˆ–å¯åЍå°é¢é‡ç½®ã€‚ - å½±å“范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€æŽ¨è页拼图切关测试与平å°é“¾è·¯æ–‡æ¡£ã€‚ - éªŒè¯æ–¹å¼ï¼šç‚¹å‡»æŽ¨è页拼图“下一关â€åŽï¼Œåœ¨ `advancePuzzleNextLevel` 未返回å‰ï¼Œé¡µé¢ä»åº”ä¿ç•™ `puzzle-board`,且ä¸å‡ºçް `加载中...` å ä½ï¼›è¿”回相似作å“åŽï¼Œå½“剿ލèå¡çš„ `作å“ä¿¡æ¯` åº”æ˜¾ç¤ºæ–°ä½œå“æ ‡é¢˜ã€‚ - å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。 diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md index fa82cc45..063f48f8 100644 --- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md +++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md @@ -126,7 +126,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— - 拼图è¿è¡Œæ€é¡¶éƒ¨å…³å¡ä¿¡æ¯é‡‡ç”¨æ¸¸æˆåŒ–铭牌样å¼ï¼šæ©˜æ£•横å‘å…³å¡å牌承载 `第 N å…³` 和关å¡å,左侧固定使用 `media/logo.png` å¡é€šå½¢è±¡ï¼›å€’计时作为下挂米白å°ç‰Œç‹¬ç«‹æ˜¾ç¤ºï¼Œç´§è´´é“­ç‰Œä½†ä¸é®æŒ¡æ£‹ç›˜ã€‚该样å¼åªæ”¹å˜è¿è¡Œæ€ HUD è§†è§‰ï¼Œä¸æ”¹å˜è®¡æ—¶ã€æš‚åœã€å¤±è´¥åŒæ­¥æˆ–å…³å¡æŽ¨è¿›è§„åˆ™ã€‚ - 拼图è¿è¡Œæ€è¿›è¡Œä¸­å…³å¡çš„ `elapsedMs` 仿˜¯ç»“ç®—å­—æ®µï¼Œè®¾ç½®é¢æ¿çš„“当å‰ç”¨æ—¶â€å¿…须按 `startedAtMs`ã€æš‚åœç´¯è®¡å’Œå†»ç»“累计实时派生;ä¸è¦ç›´æŽ¥æŠŠè¿›è¡Œä¸­çš„ `currentLevel.elapsedMs` 当作展示值。 - 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œé€šå…³ç»“算弹层必须挂到页é¢çº§ fixed 浮层,ä¸èƒ½ç•™åœ¨æŽ¨èå¡ç‰‡è§†è§‰åŒºå†…çš„ absolute 覆盖层;推è页滑动å¡ç‰‡å’Œè¿è¡Œæ€è§†å£éƒ½ä½¿ç”¨ `overflow: hidden`,åŠå±å†…容区会è£å‰ªæŽ’行榜ã€ä¸‹ä¸€å…³æŒ‰é’®å’Œç›¸ä¼¼ä½œå“å¡ã€‚ -- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œâ€œä¸‹ä¸€å…³â€åº”优先切到相似作å“ï¼›å¦‚æžœå½“å‰æŽ¨è候选为空,æ‰å›žé€€åˆ°åŒä½œå“下一关,é¿å…åŒ¿åæŽ¨èæµåœ¨å¤šå…³å¡ä½œå“上æŒç»­åœç•™åœ¨åŒä¸€ä½œå“内。下一关请求 pending 期间必须ä¿ç•™å½“å‰ `PuzzleRuntimeShell` 和棋盘,ä¸å¾—把推è塿•´ä½“切回 `加载中...` å ä½æ€ï¼›å±€éƒ¨åŒæ­¥çжæ€ç”±æ‹¼å›¾è¿è¡Œæ€è‡ªå·±çš„ busy 表现承接。åŽç«¯è¿”回的新关å¡å±žäºŽå…¶å®ƒä½œå“时,å‰ç«¯å¿…é¡»åŒæ­¥ `selectedPuzzleDetail`ã€æŽ¨è页 `puzzleGalleryEntries` 缓存和 `activeRecommendEntryKey`,让底部作å“ä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和下一次“下一个â€åŸºå‡†éƒ½æŒ‡å‘新作å“。 +- 推è页嵌入拼图è¿è¡Œæ€æ—¶ï¼Œâ€œä¸‹ä¸€å…³â€åº”优先切到相似作å“ï¼›å¦‚æžœå½“å‰æŽ¨è候选为空,æ‰å›žé€€åˆ°åŒä½œå“下一关,é¿å…åŒ¿åæŽ¨èæµåœ¨å¤šå…³å¡ä½œå“上æŒç»­åœç•™åœ¨åŒä¸€ä½œå“内。下一关请求 pending 期间必须ä¿ç•™å½“å‰ `PuzzleRuntimeShell` 和棋盘,ä¸å¾—把推è塿•´ä½“切回 `加载中...` å ä½æ€ï¼›å±€éƒ¨åŒæ­¥çжæ€ç”±æ‹¼å›¾è¿è¡Œæ€è‡ªå·±çš„ busy 表现承接。åŽç«¯è¿”回的新关å¡å±žäºŽå…¶å®ƒä½œå“时,å‰ç«¯å¿…é¡»åŒæ­¥ `selectedPuzzleDetail`ã€æŽ¨è页 `puzzleGalleryEntries` 缓存和 `activeRecommendEntryKey`,让底部作å“ä¿¡æ¯ã€åˆ†äº« / 点赞 / 改造和下一次“下一个â€åŸºå‡†éƒ½æŒ‡å‘新作å“;但这ä»å±žäºŽåŒä¸€ä¸ª runtime run 内部推进,ä¸èƒ½è§¦å‘推è rail 切å¡åŠ¨ç”»ã€çºµå‘ä½ç§»æˆ–å¯åЍå°é¢é‡ç½®ï¼Œå·²æŒ‚载且 ready çš„è¿è¡Œæ€ç”»é¢åº”ä¿æŒç¨³å®šï¼Œåªé™é»˜æ›´æ–°ä½œå“ä¿¡æ¯å’Œæ“作基准。 - 推è页里的拼图作å“如果从è¿è¡Œæ€è¿›å…¥â€œæ”¹é€ â€ç»“果页,返回平å°åŽè¦æ¸…掉推è嵌入æ€çš„ `activeRecommendEntryKey` / `activeRecommendRuntimeKind` / `isStartingRecommendEntry`,å†é‡æ–°æŒ‰æŽ¨è页自动å¯åŠ¨é€»è¾‘è¿›å…¥ä½œå“,ä¸èƒ½å¤ç”¨å·²ç»è¢«æ¸…空的旧 `puzzleRun`。 - 拼图è¿è¡Œæ€å…许å‰ç«¯ä½Žå»¶è¿Ÿäº¤äº’è¡¨çŽ°ï¼Œä½†é€šå…³ã€æŽ’è¡Œæ¦œã€å¥–励和作å“状æ€ä»ä»¥åŽç«¯ç¡®è®¤ä¸ºå‡†ã€‚ @@ -156,7 +156,7 @@ RPG / 拼图等è¿è¡Œæ€å­˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ— åˆ é™¤ç­‰ç ´åæ€§åŠ¨ä½œå½“å‰æœªæŽ¥å…¥ jump-hop 删除 API;如果åŽç»­è¦åœ¨ä½œå“æž¶æä¾›åˆ é™¤å…¥å£ï¼Œå¿…须先补é½åŽç«¯/SpacetimeDB/å‰ç«¯æ•´æ¡åˆ é™¤é“¾è·¯ï¼Œå†å¼€æ”¾æŒ‰é’®ã€‚ -推èé¡µåŒ¿åæ¸¸çŽ©ä¸å†é™å®šä¸ºè·³ä¸€è·³ã€‚移动端一级 `推è` Tab 是内嵌è¿è¡Œæ€åˆ·å¡æµï¼Œä¼šè‡ªåŠ¨é€‰æ‹©æŽ¨è作å“å¹¶å¯åŠ¨å¯¹åº”çŽ©æ³•ï¼›æ¡Œé¢ç«¯é¦–页ä¸å¯åŠ¨è¿™å¥—ç§»åŠ¨æŽ¨èè¿è¡Œæ€ï¼Œè€Œæ˜¯æ¸²æŸ“桌é¢å‘现壳,展示 `今日游æˆ`ã€`推è`ã€`作å“分类` 等桌é¢å†…容。断点事实统一走 `platformEntryResponsive.ts` çš„ `usePlatformDesktopLayout()`,平å°å£³å’Œé¦–页视图必须共用åŒä¸€ä¸ªåˆ¤æ–­ï¼Œé¿å…桌é¢å‘现页与移动推èé¡µåŒæ—¶æŒ‚è½½ã€é‡å¤è§¦å‘请求或å¯åЍè¿è¡Œæ€ã€‚推è页嵌入è¿è¡Œæ€å¯åŠ¨æ—¶æŒ‰çœŸå®žèº«ä»½åˆ†æµï¼šå·²ç™»å½•用户或本地已有 access token æ—¶ç»§ç»­ä½¿ç”¨è´¦å· Bearer,但请求选项必须是 local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·çŸ­æœŸ Runtime Guest Tokenï¼Œå¹¶åªæŠŠå®ƒä½œä¸ºå±€éƒ¨è¯·æ±‚å¤´ä¼ ç»™è¿è¡Œæ€å®¢æˆ·ç«¯ï¼Œä¸å†™å…¥å…¨å±€ç™»å½•æ€ã€ä¸è§¦å‘ refreshï¼Œä¹Ÿä¸æŠŠåŒ¿åæµé‡ä¼ªè£…æˆæ™®é€šç”¨æˆ·ã€‚当å‰è¦†ç›–矩阵为:跳一跳ã€è§†è§‰å°è¯´ã€æŠ“大鹅 Match3Dã€æ–¹æ´žæŒ‘æˆ˜ã€æ‹¼å›¾ã€æ•²æœ¨é±¼ã€å¤§é±¼åƒå°é±¼ã€æ±ªæ±ªå£°æµªã€‚æ¯ä¸ªæ¨¡æ¿çš„å¯åŠ¨è¯·æ±‚ã€æŽ¨è页内åŽç»­è¿è¡Œæ€åŠ¨ä½œä»¥åŠéœ€è¦ä¸ŠæŠ¥çš„ play/finish/leaderboard/next-level 类请求,都必须继续按该身份分æµï¼›å…¬å¼€è¯»å–å…¥å£ä»å¯åŒ¿å读å–,创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix 等账å·/所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ +推èé¡µåŒ¿åæ¸¸çŽ©ä¸å†é™å®šä¸ºè·³ä¸€è·³ã€‚移动端一级 `推è` Tab 是内嵌è¿è¡Œæ€åˆ·å¡æµï¼Œä¼šè‡ªåŠ¨é€‰æ‹©æŽ¨è作å“å¹¶å¯åŠ¨å¯¹åº”çŽ©æ³•ï¼›æ¡Œé¢ç«¯é¦–页ä¸å¯åŠ¨è¿™å¥—ç§»åŠ¨æŽ¨èè¿è¡Œæ€ï¼Œè€Œæ˜¯æ¸²æŸ“桌é¢å‘现壳,展示 `今日游æˆ`ã€`推è`ã€`作å“分类` 等桌é¢å†…容。断点事实统一走 `platformEntryResponsive.ts` çš„ `usePlatformDesktopLayout()`,平å°å£³å’Œé¦–页视图必须共用åŒä¸€ä¸ªåˆ¤æ–­ï¼Œé¿å…桌é¢å‘现页与移动推èé¡µåŒæ—¶æŒ‚è½½ã€é‡å¤è§¦å‘请求或å¯åЍè¿è¡Œæ€ã€‚移动端推è页å¯åŠ¨æˆ–åˆ‡æ¢ä½œå“时先展示当å‰ä½œå“å°é¢ï¼ŒåµŒå…¥ runtime 在å°é¢ä¸‹å±‚åŠ è½½ï¼›åªæœ‰å¯¹åº”è¿è¡Œæ€ run / profile 已准备且 lazy runtime ç»„ä»¶å®ŒæˆæŒ‚è½½åŽï¼Œå°é¢æ‰æ¸éšï¼Œä¸åœ¨ä¸­é€”å±•ç¤ºâ€œåŠ è½½ä¸­â€æ–‡æ¡ˆã€‚拼图下一关在åŒä¸€ä¸ª run å†…æŽ¨è¿›åˆ°ç›¸ä¼¼ä½œå“æ—¶ä¸è§†ä¸ºæŽ¨è作å“切æ¢ï¼Œä¸èƒ½é‡æ–°æ˜¾ç¤ºå¯åЍå°é¢ã€‚推è页嵌入è¿è¡Œæ€å¯åŠ¨æ—¶æŒ‰çœŸå®žèº«ä»½åˆ†æµï¼šå·²ç™»å½•用户或本地已有 access token æ—¶ç»§ç»­ä½¿ç”¨è´¦å· Bearer,但请求选项必须是 local auth impact,é¿å…å•å¡ 401 清空整站登录æ€ï¼›åªæœ‰ç¡®è®¤ä¸ºåŒ¿å访客时æ‰ç”³è¯·çŸ­æœŸ Runtime Guest Tokenï¼Œå¹¶åªæŠŠå®ƒä½œä¸ºå±€éƒ¨è¯·æ±‚å¤´ä¼ ç»™è¿è¡Œæ€å®¢æˆ·ç«¯ï¼Œä¸å†™å…¥å…¨å±€ç™»å½•æ€ã€ä¸è§¦å‘ refreshï¼Œä¹Ÿä¸æŠŠåŒ¿åæµé‡ä¼ªè£…æˆæ™®é€šç”¨æˆ·ã€‚当å‰è¦†ç›–矩阵为:跳一跳ã€è§†è§‰å°è¯´ã€æŠ“大鹅 Match3Dã€æ–¹æ´žæŒ‘æˆ˜ã€æ‹¼å›¾ã€æ•²æœ¨é±¼ã€å¤§é±¼åƒå°é±¼ã€æ±ªæ±ªå£°æµªã€‚æ¯ä¸ªæ¨¡æ¿çš„å¯åŠ¨è¯·æ±‚ã€æŽ¨è页内åŽç»­è¿è¡Œæ€åŠ¨ä½œä»¥åŠéœ€è¦ä¸ŠæŠ¥çš„ play/finish/leaderboard/next-level 类请求,都必须继续按该身份分æµï¼›å…¬å¼€è¯»å–å…¥å£ä»å¯åŒ¿å读å–,创作ã€ä¸ªäººä½œå“ã€åˆ é™¤ã€å‘布ã€Remix 等账å·/所有æƒåŠ¨ä½œä»ä¿æŒæ™®é€šç”¨æˆ·é‰´æƒã€‚ ## 敲木鱼 diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx index c87120ba..b4d77e98 100644 --- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx +++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx @@ -545,6 +545,7 @@ type BabyObjectMatchGenerationPhase = 'generating' | 'ready' | 'failed'; type RecommendRuntimeState = { activeKind: RecommendRuntimeKind | null; + barkBattlePublishedConfig: BarkBattlePublishedConfig | null; babyObjectMatchDraft: BabyObjectMatchDraft | null; bigFishRun: BigFishRuntimeSnapshotResponse | null; jumpHopRun: JumpHopRunResponse['run'] | null; @@ -730,7 +731,7 @@ function isRecommendRuntimeReadyForEntry( return Boolean(state.visualNovelRun); } if (expectedKind === 'bark-battle') { - return true; + return Boolean(state.barkBattlePublishedConfig); } if (expectedKind === 'edutainment') { return Boolean(state.babyObjectMatchDraft); @@ -15003,6 +15004,29 @@ export function PlatformEntryFlowShellImpl({ isDesktopLayout, ]); + const activeRecommendEntry = + activeRecommendEntryKey && !isDesktopLayout + ? (recommendRuntimeEntries.find( + (entry) => + getPlatformPublicGalleryEntryKey(entry) === + activeRecommendEntryKey, + ) ?? null) + : null; + const isActiveRecommendRuntimeReady = + activeRecommendEntry !== null && + isRecommendRuntimeReadyForEntry(activeRecommendEntry, { + activeKind: activeRecommendRuntimeKind, + barkBattlePublishedConfig, + babyObjectMatchDraft, + bigFishRun, + jumpHopRun, + match3dRun, + puzzleRun, + squareHoleRun, + visualNovelRun, + woodenFishRun, + }); + useEffect(() => { if ( isDesktopLayout || @@ -15020,25 +15044,6 @@ export function PlatformEntryFlowShellImpl({ return; } - const activeRecommendEntry = activeRecommendEntryKey - ? (recommendRuntimeEntries.find( - (entry) => - getPlatformPublicGalleryEntryKey(entry) === activeRecommendEntryKey, - ) ?? null) - : null; - const isActiveRecommendRuntimeReady = - activeRecommendEntry !== null && - isRecommendRuntimeReadyForEntry(activeRecommendEntry, { - activeKind: activeRecommendRuntimeKind, - babyObjectMatchDraft, - bigFishRun, - jumpHopRun, - match3dRun, - puzzleRun, - squareHoleRun, - visualNovelRun, - woodenFishRun, - }); if ( (activeRecommendEntry !== null && isActiveRecommendRuntimeReady) || isStartingRecommendEntry @@ -15054,9 +15059,12 @@ export function PlatformEntryFlowShellImpl({ }, [ activeRecommendEntryKey, activeRecommendRuntimeKind, + activeRecommendEntry, + barkBattlePublishedConfig, babyObjectMatchDraft, bigFishRun, jumpHopRun, + isActiveRecommendRuntimeReady, isStartingRecommendEntry, match3dRun, platformBootstrap.isLoadingPlatform, @@ -16399,6 +16407,7 @@ export function PlatformEntryFlowShellImpl({ onOpenRecommendGalleryDetail={openRecommendGalleryDetail} recommendRuntimeContent={recommendRuntimeContent} activeRecommendEntryKey={activeRecommendEntryKey} + isRecommendRuntimeReady={isActiveRecommendRuntimeReady} isStartingRecommendEntry={ isStartingRecommendEntry || isBigFishBusy || diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx index 07f39d36..20f7a2f9 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx @@ -823,6 +823,7 @@ function renderLoggedOutHomeView( | 'recommendRuntimeContent' | 'activeRecommendEntryKey' | 'isStartingRecommendEntry' + | 'isRecommendRuntimeReady' | 'recommendRuntimeError' | 'onSelectNextRecommendEntry' | 'onSelectPreviousRecommendEntry' @@ -883,6 +884,7 @@ function renderLoggedOutHomeView( } activeRecommendEntryKey={overrides.activeRecommendEntryKey} isStartingRecommendEntry={overrides.isStartingRecommendEntry} + isRecommendRuntimeReady={overrides.isRecommendRuntimeReady} recommendRuntimeError={overrides.recommendRuntimeError} onSelectNextRecommendEntry={overrides.onSelectNextRecommendEntry} onSelectPreviousRecommendEntry={ @@ -3703,7 +3705,10 @@ test('logged out mobile recommend page renders runtime instead of cover', () => ); expect(screen.getByTestId('recommend-runtime')).toBeTruthy(); - expect(document.querySelector('.platform-recommend-cover-only')).toBeNull(); + expect( + document.querySelector('.platform-recommend-runtime-cover'), + ).toBeTruthy(); + expect(screen.queryByText('加载中...')).toBeNull(); expect( document.querySelector('.platform-public-work-card__cover'), ).toBeNull(); @@ -3712,7 +3717,7 @@ test('logged out mobile recommend page renders runtime instead of cover', () => expect(onOpenGalleryDetail).not.toHaveBeenCalled(); }); -test('mobile recommend loading state is themed instead of hardcoded black', () => { +test('mobile recommend startup keeps cover visible without loading copy', () => { renderLoggedOutHomeView(vi.fn(), { latestEntries: [puzzlePublicEntry], activeRecommendEntryKey: 'puzzle:user-2:puzzle-profile-public-1', @@ -3720,8 +3725,123 @@ test('mobile recommend loading state is themed instead of hardcoded black', () = recommendRuntimeContent: null, }); - expect(document.querySelector('.platform-recommend-cover-only')).toBeNull(); - expect(screen.getByText('加载中...')).toBeTruthy(); + expect( + document.querySelector('.platform-recommend-runtime-cover'), + ).toBeTruthy(); + expect(screen.queryByText('加载中...')).toBeNull(); + expect(screen.getAllByText('奇幻拼图').length).toBeGreaterThan(0); +}); + +test('mobile recommend next level keeps runtime visual stable when active work changes', async () => { + const animationCallbacks: FrameRequestCallback[] = []; + Object.defineProperty(window, 'requestAnimationFrame', { + configurable: true, + writable: true, + value: vi.fn((callback: FrameRequestCallback) => { + animationCallbacks.push(callback); + return animationCallbacks.length; + }), + }); + Object.defineProperty(window, 'cancelAnimationFrame', { + configurable: true, + writable: true, + value: vi.fn(), + }); + const firstEntry = { + ...puzzlePublicEntry, + workId: 'puzzle-work-feed-1', + profileId: 'puzzle-profile-feed-1', + ownerUserId: 'user-feed-1', + publicWorkCode: 'PZ-FEED1', + worldName: '当剿‹¼å›¾', + coverImageSrc: 'current-cover.png', + } satisfies PlatformPublicGalleryCard; + const similarEntry = { + ...puzzlePublicEntry, + workId: 'puzzle-work-similar-1', + profileId: 'puzzle-profile-similar-1', + ownerUserId: 'user-feed-2', + publicWorkCode: 'PZ-SIMILAR1', + worldName: '相似拼图', + coverImageSrc: 'similar-cover.png', + } satisfies PlatformPublicGalleryCard; + + const { rerender } = renderLoggedOutHomeView(vi.fn(), { + latestEntries: [firstEntry, similarEntry], + activeRecommendEntryKey: 'puzzle:user-feed-1:puzzle-profile-feed-1', + isRecommendRuntimeReady: true, + }); + + act(() => { + animationCallbacks.splice(0).forEach((callback) => callback(16)); + }); + await waitFor(() => { + expect( + document.querySelector('.platform-recommend-runtime-cover')?.className, + ).toContain('platform-recommend-runtime-cover--hidden'); + }); + + rerender( + undefined), + musicVolume: 0.42, + setMusicVolume: vi.fn(), + platformTheme: 'light', + setPlatformTheme: vi.fn(), + isHydratingSettings: false, + isPersistingSettings: false, + settingsError: null, + }} + > + } + activeRecommendEntryKey="puzzle:user-feed-2:puzzle-profile-similar-1" + isRecommendRuntimeReady + onOpenLibraryDetail={vi.fn()} + onSearchPublicCode={vi.fn()} + /> + , + ); + + const rail = document.querySelector( + '.platform-recommend-swipe-rail', + ) as HTMLElement | null; + expect(rail?.className).toContain('platform-recommend-swipe-rail--settled'); + expect(rail?.style.transform).toBe('translate3d(0, 0px, 0)'); + expect(screen.getByLabelText('相似拼图 作å“ä¿¡æ¯')).toBeTruthy(); + expect( + document.querySelector('.platform-recommend-runtime-cover')?.className, + ).toContain('platform-recommend-runtime-cover--hidden'); }); test('logged in recommend runtime preloads adjacent work previews and drag switches like video feed', () => { diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx index 9d2a37d9..6766dbc8 100644 --- a/src/components/rpg-entry/RpgEntryHomeView.tsx +++ b/src/components/rpg-entry/RpgEntryHomeView.tsx @@ -39,6 +39,7 @@ import { type CSSProperties, type PointerEvent, type ReactNode, + Suspense, useCallback, useEffect, useMemo, @@ -195,6 +196,7 @@ export interface RpgEntryHomeViewProps { recommendRuntimeContent?: ReactNode; activeRecommendEntryKey?: string | null; isStartingRecommendEntry?: boolean; + isRecommendRuntimeReady?: boolean; recommendRuntimeError?: string | null; onSelectNextRecommendEntry?: (activeEntryKey?: string | null) => void; onSelectPreviousRecommendEntry?: (activeEntryKey?: string | null) => void; @@ -946,6 +948,115 @@ function RecommendRuntimePreviewCard({ ); } +function RecommendRuntimeCover({ + entry, + className = '', +}: { + entry: PlatformPublicGalleryCard; + className?: string; +}) { + const coverImage = resolvePlatformWorldCoverImage(entry); + const fallbackCoverImage = resolvePlatformWorldFallbackCoverImage(entry); + + return ( +