From fbda61415670a5172b3b243b3463946e2a8153ea Mon Sep 17 00:00:00 2001
From: kdletters <61648117+kdletters@users.noreply.github.com>
Date: Tue, 26 May 2026 14:27:18 +0800
Subject: [PATCH] feat: surface platform errors in copyable dialogs
---
.hermes/shared-memory/decision-log.md | 16 +
.hermes/shared-memory/pitfalls.md | 24 +
...玩法创作】平å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md | 6 +-
...项目基线】当å‰äº§å“与工程约æŸ-2026-05-15.md | 11 +-
src/components/CustomWorldGenerationView.tsx | 16 +-
.../CustomWorldCreationHub.tsx | 12 +-
.../CustomWorldCreationStartCard.tsx | 9 +-
.../PlatformEntryCreationTypeModal.test.tsx | 1 -
.../PlatformEntryCreationTypeModal.tsx | 7 -
.../PlatformEntryFlowShellImpl.tsx | 415 ++++++++++++++++--
.../PlatformErrorDialog.test.tsx | 60 +++
.../platform-entry/PlatformErrorDialog.tsx | 120 +++++
.../platform-entry/PlatformWorkDetailView.tsx | 7 +-
.../puzzle-result/PuzzleResultView.tsx | 7 +-
.../RpgEntryHomeView.recharge.test.tsx | 56 +--
src/components/rpg-entry/RpgEntryHomeView.tsx | 139 +++---
16 files changed, 715 insertions(+), 191 deletions(-)
create mode 100644 src/components/platform-entry/PlatformErrorDialog.test.tsx
create mode 100644 src/components/platform-entry/PlatformErrorDialog.tsx
diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md
index c4a20efe..070fd6ca 100644
--- a/.hermes/shared-memory/decision-log.md
+++ b/.hermes/shared-memory/decision-log.md
@@ -16,6 +16,22 @@
---
+## 2026-05-26 å¹³å°è·¨æµç¨‹é”™è¯¯ç»Ÿä¸€ç”¨å¯å¤åˆ¶æ¥æºå¼¹çª—展示
+
+- 背景:拼图ç‰ç”Ÿæˆé“¾è·¯å¯èƒ½åŒæ—¶å˜åœ¨å¤šä¸ªè‰ç¨¿æˆ–游玩实例,页é¢å†…裸错误 banner å®¹æ˜“è®©ç”¨æˆ·è¯¯ä»¥ä¸ºå½“å‰æ£åœ¨çœ‹çš„æ‹¼å›¾å¤±è´¥ï¼Œä¹Ÿä¸æ–¹ä¾¿å¤åˆ¶å®Œæ•´é”™è¯¯ç»™å¼€å‘排查。
+- 决ç–:平å°å…¥å£ã€ç”Ÿæˆé¡µã€ç»“果页ã€ä½œå“详情ã€ä½œå“æž¶å’Œè¿è¡Œæ€çš„è·¨æµç¨‹é”™è¯¯ç»Ÿä¸€æ”¶å£åˆ° `PlatformErrorDialog`ï¼›å¼¹çª—å¿…é¡»å¸¦é”™è¯¯æ¥æºï¼Œä¾‹å¦‚æŸä¸ªè‰ç¨¿ã€ç”Ÿæˆä¼šè¯ã€ä½œå“详情或游玩实例,并æä¾›å¤åˆ¶æŒ‰é’®å¤åˆ¶æ¥æºä¸Žé”™è¯¯å†…容。页é¢å†…旧的裸错误 bannerã€åˆ›ä½œå…¥å£ modal 错误ã€ç”Ÿæˆé¡µé”™è¯¯å¾½æ ‡ç‰ä¸å†é‡å¤å±•ç¤ºï¼›è¡¨å•æ ¡éªŒå’Œå‘布确认弹窗里的局部业务错误ä»å¯ä¿ç•™åœ¨åŽŸå¼¹çª—å†…ã€‚
+- å½±å“范围:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/platform-entry/PlatformErrorDialog.tsx`ã€`src/components/CustomWorldGenerationView.tsx`ã€`src/components/custom-world-home/CustomWorldCreationHub.tsx`ã€`src/components/custom-world-home/CustomWorldCreationStartCard.tsx`ã€`src/components/platform-entry/PlatformWorkDetailView.tsx`ã€`src/components/platform-entry/PlatformEntryCreationTypeModal.tsx`ã€`src/components/puzzle-result/PuzzleResultView.tsx`。
+- éªŒè¯æ–¹å¼ï¼š`npm run test -- src/components/platform-entry/PlatformErrorDialog.test.tsx src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx`ã€`npm run typecheck`ã€`npm run check:encoding` 通过;手测时异æ¥å¤±è´¥åº”弹出包å«â€œé”™è¯¯æ¥æºâ€å’Œâ€œé”™è¯¯å†…容â€çš„弹窗,å¤åˆ¶æŒ‰é’®åº”å¤åˆ¶å®Œæ•´è¯Šæ–文本。
+- å…³è”æ–‡æ¡£ï¼š`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。
+
+## 2026-05-26 “我的â€é¡µä»»åŠ¡å¡è¯»åŽç«¯ä»»åŠ¡æ‘˜è¦å¹¶ç§»é™¤å¸¸é©»å¡«é‚€è¯·ç å…¥å£
+
+- 背景:移动端“我的â€é¡µæ¯æ—¥ä»»åС塿›¾ç¡¬ç¼–ç `0 / 1`,任务领å–完æˆåŽåªåˆ·æ–°å¼¹çª—内任务ä¸å¿ƒï¼Œå¡ç‰‡æœ¬èº«ä¸æ›´æ–°ï¼›é¡µé¢åº•部还ä¿ç•™æ—§çš„“填邀请ç â€æ¬¡çº§æŒ‰é’®ï¼Œå’Œå½“å‰äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼å£å¾„é‡å¤ã€‚
+- 决ç–:`RpgEntryHomeView` çš„æ¯æ—¥ä»»åŠ¡å¡ä»¥ `/api/profile/tasks` 返回的任务ä¸å¿ƒä¸ºäº‹å®žæºï¼Œå±•示当å‰å¯æ“作任务的奖励ã€è¿›åº¦å’Œçжæ€ï¼›é¢†å–æˆåŠŸåŽåŒæ¥ä½¿ç”¨ claim å“应里的 `center` 刷新å¡ç‰‡ã€‚移动端“我的â€é¡µä¸å†æ¸²æŸ“常驻“填邀请ç â€æ¬¡çº§å…¥å£ï¼Œé‚€è¯·ç 填写仅ä¿ç•™é‚€è¯·é“¾æŽ¥ query 自动打开弹窗和其它明确引导。
+- å½±å“范围:`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`ã€`docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md`。
+- éªŒè¯æ–¹å¼ï¼š`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` 应æ–è¨€ä»»åŠ¡å¡æ˜¾ç¤º `1 / 1`ã€é¢†å–åŽæ˜¾ç¤ºå·²å®Œæˆï¼Œä¸”新用户账å·ä¹Ÿæ²¡æœ‰ `次级入å£` / `填邀请ç ` 常驻按钮;`npm run typecheck`ã€`npm run check:encoding` 通过。
+- å…³è”æ–‡æ¡£ï¼š`docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md`。
+
## 2026-05-25 å¹³å°é¦–é¡µæŽ¨èæŒ‰æ¡Œé¢ä¸Žç§»åЍæ–点分æµ
- 背景:平å°é¦–页的推è页在桌é¢ä¸Žç§»åŠ¨ç«¯ä¹‹é—´åŽŸå…ˆå…±ç”¨åŒä¸€å¥—推èè¿è¡Œæ€é€»è¾‘,容易让桌é¢å’Œç§»åŠ¨ä¸¤å¥—å†…å®¹åŒæ—¶å¯åŠ¨ï¼Œä¹Ÿè®©é¦–é¡µçš„æŽ¨èå¡ä¸Žæ¡Œé¢å‘现壳互相抢状æ€ã€‚
diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md
index c9f1c0d1..f086ebc2 100644
--- a/.hermes/shared-memory/pitfalls.md
+++ b/.hermes/shared-memory/pitfalls.md
@@ -15,6 +15,30 @@
- å…³è”ï¼šç›¸å…³æ–‡ä»¶ã€æ–‡æ¡£ã€æäº¤æˆ– Issue
```
+## å¹³å°å¼‚æ¥é”™è¯¯å¿…é¡»å¸¦æ¥æºå¼¹çª—,ä¸è¦åªæ˜¾ç¤ºè£¸é”™è¯¯
+
+- 现象:用户先åŽè§¦å‘多个拼图或è‰ç¨¿ç”Ÿæˆæ—¶ï¼Œæ—§è¯·æ±‚失败åŽä¼šåœ¨å½“å‰é¡µé¢æ˜¾ç¤ºâ€œå›¾ç‰‡ç”Ÿæˆå¤±è´¥â€ç‰è£¸é”™è¯¯ï¼Œå®¹æ˜“è¯¯åˆ¤ä¸ºå½“å‰æ£åœ¨çœ‹çš„æ‹¼å›¾å¤±è´¥ï¼›é”™è¯¯æ–‡æœ¬ä¹Ÿä¸ä¾¿å¤åˆ¶ç»™å¼€å‘排查。
+- åŽŸå› ï¼šä¸åŒå…¥å£ã€ç”Ÿæˆé¡µã€ç»“果页ã€ä½œå“详情和è¿è¡Œæ€å„自渲染局部错误,没有统一æºå¸¦è‰ç¨¿ã€ç”Ÿæˆä¼šè¯ã€ä½œå“æˆ–æ¸¸çŽ©æ¥æºã€‚
+- 处ç†ï¼šè·¨æµç¨‹é”™è¯¯ç»Ÿä¸€ç”± `PlatformEntryFlowShellImpl` 汇总为 `PlatformErrorDialog`ï¼Œæ¥æºä½¿ç”¨çŽ©æ³•ã€è‰ç¨¿ / session / work / run æ ‡è¯†ç»„æˆï¼›å¼¹çª—æä¾›å¤åˆ¶æŒ‰é’®ã€‚å…³é—å¼¹çª—æ—¶åªæ¸…ç†å¯å®‰å…¨æ¸…ç†çš„错误状æ€ï¼›æ¢å¤ç±»é”™è¯¯ç”¨ dismiss key 防æ¢åå¤å¼¹å‡ºä½†ä¸æ“…自改底层状æ€ã€‚
+- 验è¯ï¼šè§¦å‘任一平å°çº§å¼‚æ¥å¤±è´¥æ—¶ï¼Œé¡µé¢åº”出现包å«â€œé”™è¯¯æ¥æºâ€å’Œâ€œé”™è¯¯å†…容â€çš„弹窗;å¤åˆ¶å†…å®¹åº”åŒ…å«æ¥æºå’Œé”™è¯¯æ£æ–‡ï¼›æ—§é¡µé¢å†…错误 banner ä¸å†é‡å¤å‡ºçŽ°ã€‚
+- å…³è”:`src/components/platform-entry/PlatformEntryFlowShellImpl.tsx`ã€`src/components/platform-entry/PlatformErrorDialog.tsx`ã€`docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md`。
+
+## “我的â€é¡µæ¯æ—¥ä»»åŠ¡å¡ä¸è¦ç¡¬ç¼–ç 进度
+
+- çŽ°è±¡ï¼šç”¨æˆ·å®Œæˆæˆ–领喿¯æ—¥ä»»åŠ¡åŽï¼Œä»»åŠ¡ä¸å¿ƒå¼¹çª—里的任务状æ€å·²ç»å˜åŒ–,但“我的â€é¡µå¡ç‰‡ä»æ˜¾ç¤º `0 / 1` 和“去完æˆâ€ã€‚
+- åŽŸå› ï¼šå¡ç‰‡é¦–版åªå†™äº†é™æ€å±•ç¤ºæ–‡æ¡ˆï¼Œæ²¡æœ‰è¯»å– `/api/profile/tasks` 返回的 `ProfileTaskCenterResponse`ï¼Œé¢†å–æŽ¥å£è¿”回的新 `center` 也åªç”¨äºŽå¼¹çª—。
+- 处ç†ï¼šè¿›å…¥â€œæˆ‘çš„â€é¡µæ—¶è¯»å–任务ä¸å¿ƒï¼Œå¡ç‰‡ç”¨å½“å‰å¯æ“作任务或已领å–任务派生奖励ã€è¿›åº¦æ¡å’Œæ“作状æ€ï¼›`claimRpgProfileTaskReward(...)` æˆåŠŸåŽç”¨å“应里的 `center` 覆盖本地任务ä¸å¿ƒã€‚
+- 验è¯ï¼š`npm run test -- src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx` 应覆盖å¡ç‰‡ä»ŽåŽç«¯ä»»åŠ¡æ‘˜è¦æ˜¾ç¤º `1 / 1`,领å–åŽæ˜¾ç¤ºå·²å®Œæˆã€‚
+- å…³è”:`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx`ã€`docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md`。
+
+## “我的â€é¡µä¸è¦æ¢å¤æ—§çš„å¡«é‚€è¯·ç æ¬¡çº§æŒ‰é’®
+
+- 现象:移动端“我的â€é¡µåœ¨äº”项常用功能和设置入å£ä¸‹æ–¹åˆå‡ºçŽ°ä¸€ä¸ªâ€œå¡«é‚€è¯·ç â€æŒ‰é’®ï¼Œçœ‹èµ·æ¥åƒæ—§å…¥å£æ®‹ç•™ã€‚
+- åŽŸå› ï¼šé‚€è¯·ç æµç¨‹è¿ç§»åŽä»æŒ‰æ–°ç”¨æˆ·çª—å£ä¿ç•™ `canShowReferralRedeemShortcut` 次级入å£ï¼›ä½†å½“å‰é¡µé¢å£å¾„å·²ç»å›ºå®šä¸ºäº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼ï¼Œé‚€è¯·ç 填写应由邀请链接 query 或明确引导打开弹窗。
+- 处ç†ï¼šç§»é™¤å¸¸é©» `次级入å£` / `填邀请ç ` 渲染,ä¸åˆ 除 `ProfileReferralModal` çš„ `redeem` 颿¿ï¼Œä¹Ÿä¸ç ´å `?inviteCode=` / `?invite_code=` 自动打开填写弹窗。
+- 验è¯ï¼šæ–°ç”¨æˆ·è´¦å·æ‰“开“我的â€é¡µæ—¶æ²¡æœ‰ `次级入å£` å’Œ `填邀请ç ` 按钮;带 `?inviteCode=spring-2026` 的登录用户ä»è‡ªåŠ¨æ‰“å¼€é‚€è¯·ç 弹窗并预填 `SPRING2026`。
+- å…³è”:`src/components/rpg-entry/RpgEntryHomeView.tsx`ã€`.hermes/skills/genarrative-profile-invite-flow/SKILL.md`。
+
## 创作å¡ç‰‡ç‚¹å‡»è¦ç›´è¾¾å·²æœ‰å…¥å£è¡¨å•,别å†ä¿ç•™ç©ºç™½å…¥å£é¡µ
- 现象:创作 Tab 模æ¿å¡ç‚¹å‡»åŽå¦‚æžœä»ç„¶åœç•™åœ¨åˆ›ä½œå¤§åŽ…ï¼Œæˆ–è€…å…ˆè¿›å…¥â€œX 创作入å£â€è¿™ç§ç©ºç™½é¡µï¼Œå°±ä¼šè®©ç”¨æˆ·å¤šèµ°ä¸€å±‚,还å¯èƒ½è¢«é”™è¯¯çš„ stage 白å啿‹‰å›žå¹³å°ã€‚
diff --git a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md
index 0e593209..600e0327 100644
--- a/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md
+++ b/docs/ã€çŽ©æ³•åˆ›ä½œã€‘å¹³å°å…¥å£ä¸ŽçŽ©æ³•é“¾è·¯-2026-05-15.md
@@ -12,6 +12,8 @@
åˆ›ä½œè¡¨å•æäº¤å‰çš„æ³¥ç‚¹ä½™é¢å‰ç½®æ ¡éªŒåªå…许用独立弹窗æç¤ºå¤±è´¥åŽŸå› ï¼Œä¸å¾—æŠŠç”¨æˆ·é€€å›žåˆ›ä½œå…¥å£æˆ–玩法模æ¿åˆ—表,也ä¸å¾—清空当å‰è¡¨å•状æ€ã€‚当å‰é€‚ç”¨æ‹¼å›¾ã€æŠ“å¤§é¹…å’Œæ±ªæ±ªå£°æµªç‰ä¼šåœ¨å‰ç«¯æäº¤å‰æ ¡éªŒæ³¥ç‚¹çš„生æˆå…¥å£ï¼›ä½™é¢ä¸è¶³ã€ä½™é¢è¯»å–失败都应åœç•™åœ¨å½“å‰å·¥ä½œå°ï¼Œç”±ç”¨æˆ·å…³é—æç¤ºåŽç»§ç»ç¼–辑或自行补足泥点。
+å¹³å°å…¥å£ã€ç”Ÿæˆé¡µã€ç»“果页ã€ä½œå“详情ã€ä½œå“æž¶å’Œè¿è¡Œæ€çš„è·¨æµç¨‹é”™è¯¯ç»Ÿä¸€æ”¶å£åˆ° `PlatformErrorDialog`ã€‚å¼¹çª—å¿…é¡»å¸¦æ˜Žç¡®é”™è¯¯æ¥æºï¼Œä¾‹å¦‚æŸä¸ªè‰ç¨¿ã€æŸæ¬¡ç”Ÿæˆã€ä½œå“详情或æŸä¸ªæ¸¸çŽ©å®žä¾‹ï¼Œå¹¶æä¾›å¤åˆ¶æŒ‰é’®å¤åˆ¶â€œé”™è¯¯æ¥æº + 错误内容â€ã€‚页é¢å†…ä¸å†é‡å¤æ¸²æŸ“裸错误 bannerï¼›è¡¨å•æ ¡éªŒã€å‘布确认弹窗里的局部业务错误å¯ä»¥ä¿ç•™åœ¨åŽŸå¼¹çª—å†…ã€‚
+
`PlatformEntryFlowShellImpl.tsx` 仿˜¯å¹³å°å…¥å£ç¼–排壳,åŽç»ç»´æŠ¤æ—¶åº”优先把独立 UI 片段ã€å…¬å¼€ä½œå“æ˜ å°„ã€è‰ç¨¿ç”Ÿæˆ notice å’Œè¿è¡Œæ€çŠ¶æ€ helper 拆到 `src/components/platform-entry/PlatformEntryFlowShellImpl/` 或åŒç›®å½•ç´§é‚» helper 文件。拆分åªå…è®¸æ”¹å˜æ–‡ä»¶ç»„ç»‡ï¼Œä¸æ”¹å˜å…¥å£é…置事实æºã€é»˜è®¤å¯¼å‡ºã€propsã€é¡µé¢é˜¶æ®µã€UI æ–‡æ¡ˆæˆ–çŽ°æœ‰äº¤äº’ï¼›å…¶ä¸æ‹¼å›¾é¦–访 onboarding 已拆为 `PlatformEntryFlowShellImpl/PuzzleOnboardingView.tsx`。
`platformEntryCreationTypes.ts` åªåšå‰ç«¯å±•示派生,分组时必须把åŽç«¯ `creationTypes` 里的 `categoryId` / `categoryLabel` 当作å¯ç¼ºå¤±å—段处ç†ï¼Œç©ºå€¼ç»Ÿä¸€å›žé€€åˆ° `recent` / `最近创作`,é¿å…æ—§æ•°æ®ã€å±€éƒ¨ mock 或异常返回把创作入å£åˆå§‹åŒ–直接打崩。
@@ -44,7 +46,7 @@
6. 点击 `generationStatus=generating` çš„è‰ç¨¿å¡å¿…é¡»æ¢å¤å¯¹åº”玩法的生æˆè¿›åº¦é¡µï¼Œä¸èƒ½è¿›å…¥ç©ºç™½ç»“果页或普通工作区;æ¢å¤ç”Ÿæˆé¡µçš„ `startedAtMs` 使用进入生æˆé¡µçš„当剿—¶é—´ï¼Œä½œå“æ‘˜è¦ `updatedAt` åªç”¨äºŽæŽ’åºå’Œæ‘˜è¦å±•示,ä¸å‚与å‡è¿›åº¦èµ·ç®—。
7. ç§æœ‰ generated 图片必须通过 `ResolvedAssetImage` / `/api/assets/read-url` æ¢ç¾è¯»å–。
-å‘现 Tabã€åˆ›ä½œ Tab 与è‰ç¨¿ Tab çš„é¡µé¢æ ¹å†…容区ä¸å†å¥— `platform-page-stage` 外层全局å¡ç‰‡å£³ï¼Œè®©åˆ—表ã€ç›é€‰å’ŒçŽ©æ³•å¡èŽ·å¾—æ›´å®½çš„æ¨ªå‘空间;推èé¡µå’Œæˆ‘çš„é¡µä»æŒ‰å„自页é¢è®¾è®¡ä¿ç•™åŽŸæœ‰å…¨å±€å¡ç‰‡å£å¾„。移动端“我的â€é¡µä»æŒ‰é¡¶éƒ¨å¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¼šå‘˜æ¨ªå¹…ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼ã€è®¾ç½®å…¥å£ã€æ¬¡çº§å…¥å£å¸¦å’Œæ³•律信æ¯ç»„织,但å—å·å¿…须维æŒå¹³å°æ™®é€š UI æ¡£ä½ï¼Œä¸èƒ½å› ä¸ºçª„å±æŠŠå¡ç‰‡æ ‡é¢˜ã€åŠŸèƒ½ label æˆ–æ³•å¾‹ä¿¡æ¯æ’‘æˆå±•示级å—å·ï¼›æœ€åŽä¸€å±å†…容必须能在底部 dock 上方完整滚动露出,ä¸å¾—è¢«å›ºå®šåº•éƒ¨å¯¼èˆªé®æŒ¡ã€‚
+å‘现 Tabã€åˆ›ä½œ Tab 与è‰ç¨¿ Tab çš„é¡µé¢æ ¹å†…容区ä¸å†å¥— `platform-page-stage` 外层全局å¡ç‰‡å£³ï¼Œè®©åˆ—表ã€ç›é€‰å’ŒçŽ©æ³•å¡èŽ·å¾—æ›´å®½çš„æ¨ªå‘空间;推èé¡µå’Œæˆ‘çš„é¡µä»æŒ‰å„自页é¢è®¾è®¡ä¿ç•™åŽŸæœ‰å…¨å±€å¡ç‰‡å£å¾„。移动端“我的â€é¡µä»æŒ‰é¡¶éƒ¨å¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¼šå‘˜æ¨ªå¹…ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼ã€è®¾ç½®å…¥å£å’Œæ³•律信æ¯ç»„织,ä¸ä¿ç•™æ—§çš„底部“填邀请ç â€æ¬¡çº§å…¥å£ï¼›æ¯æ—¥ä»»åŠ¡å¡å¿…é¡»è¯»å– `/api/profile/tasks` 的当å‰ä»»åŠ¡æ‘˜è¦å¹¶åœ¨é¢†å–åŽåŒæ¥åˆ·æ–°å¡ç‰‡è¿›åº¦ã€‚å—å·å¿…须维æŒå¹³å°æ™®é€š UI æ¡£ä½ï¼Œä¸èƒ½å› ä¸ºçª„å±æŠŠå¡ç‰‡æ ‡é¢˜ã€åŠŸèƒ½ label æˆ–æ³•å¾‹ä¿¡æ¯æ’‘æˆå±•示级å—å·ï¼›æœ€åŽä¸€å±å†…容必须能在底部 dock 上方完整滚动露出,ä¸å¾—è¢«å›ºå®šåº•éƒ¨å¯¼èˆªé®æŒ¡ã€‚
## RPG / 自定义世界
@@ -72,7 +74,7 @@ RPG ä»Žä½œå“æž¶ã€å¹¿åœºè¯¦æƒ…或作å“å·æœç´¢ç‚¹å‡»â€œå¯åЍâ€å‰ï¼Œå…¥å£
RPG è¿è¡Œæ€çš„æˆ˜æ–—终局ã€ç»§ç»å†’险ã€ç»§ç»æŽ¢ç´¢å’Œåˆ‡åœºæ™¯éƒ½å±žäºŽæœåŠ¡ç«¯ runtime 快照真相:`module-runtime-story` 必须在终局战斗 action åŽè°ƒç”¨ post-battle finalization,æŒä¹…写入 `story_continue_adventure`ã€`deferredOptions`ã€`deferredRuntimeState.storyEngineMemory.currentSceneActState` 和清ç†åŽçš„æˆ˜æ–—状æ€ï¼›`idle_travel_next_scene` / `camp_travel_home_scene` 必须由åŽç«¯å†™å…¥æ–°çš„ `currentScenePreset`ã€`currentSceneActState`ã€`currentEncounter` å’Œ `runtimeStats.scenesTraveled`。å‰ç«¯åªæ’放退场ã€è¿›åœºå’Œç»§ç»æŒ‰é’®è¡¨çŽ°ï¼Œä¸èƒ½ç”¨é»˜è®¤ `观察/试探/è°ƒæ¯` fallback 或本地动画å‡è£…推进剧情。旧 bootstrap å¿«ç…§å¯èƒ½åªæœ‰ `connectedSceneIds` / `forwardSceneId` 而没有 `connections`,åŽç«¯ç”Ÿæˆæˆ˜åŽæ—…è¡Œé€‰é¡¹æ—¶å¿…é¡»å…¼å®¹è¿™äº›å—æ®µã€‚
-RPG / 拼图ç‰è¿è¡Œæ€å˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ—表为真相,æ¢å¤åŠ¨ä½œç»§ç»èµ°å¯¹åº”æ¢å¤æŽ¥å£ï¼Œä½†ç§»åŠ¨ç«¯â€œæˆ‘çš„â€é¡µå·²ç»ä¸å†æä¾›ç‹¬ç«‹çš„ `æ¬¡çº§å…¥å£ > å˜æ¡£` 和设置入å£å˜æ¡£æŒ‰é’®ï¼›â€œçŽ©è¿‡â€å¼¹çª—å¯ä»¥ç»§ç»åˆå¹¶å±•示å¯ç»§ç»å˜æ¡£ï¼Œä¸ªäººä¸å¿ƒåªä¿ç•™è®¾ç½®ã€æ‰«ç ã€å¸¸ç”¨åŠŸèƒ½å’Œæ¡ä»¶æ€§æ¬¡çº§å…¥å£ã€‚移动端“我的â€é¡µçš„äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼åªæ”¾æ³¥ç‚¹å……值ã€é‚€è¯·å¥½å‹ã€å…‘æ¢ç ã€çŽ©å®¶ç¤¾åŒºã€å馈与建议,é¿å…æŠŠå˜æ¡£æŒ¤å…¥ä¸»å®«æ ¼ç ´åå‚考图布局。å‰ç«¯åªå±•示 `/api/profile/save-archives` 返回的列表并在用户选择åŽè°ƒç”¨å¯¹åº”æ¢å¤æŽ¥å£ï¼Œä¸èƒ½æœ¬åœ°æ‹¼è£…或ç›é€‰æ£å¼å˜æ¡£çœŸç›¸ã€‚
+RPG / 拼图ç‰è¿è¡Œæ€å˜æ¡£ä»ä»¥ `/api/profile/save-archives` çš„åŽç«¯åˆ—表为真相,æ¢å¤åŠ¨ä½œç»§ç»èµ°å¯¹åº”æ¢å¤æŽ¥å£ï¼Œä½†ç§»åŠ¨ç«¯â€œæˆ‘çš„â€é¡µå·²ç»ä¸å†æä¾›ç‹¬ç«‹çš„ `æ¬¡çº§å…¥å£ > å˜æ¡£` 和设置入å£å˜æ¡£æŒ‰é’®ï¼›â€œçŽ©è¿‡â€å¼¹çª—å¯ä»¥ç»§ç»åˆå¹¶å±•示å¯ç»§ç»å˜æ¡£ï¼Œä¸ªäººä¸å¿ƒåªä¿ç•™è®¾ç½®ã€æ‰«ç 和五项常用功能。移动端“我的â€é¡µçš„äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼åªæ”¾æ³¥ç‚¹å……值ã€é‚€è¯·å¥½å‹ã€å…‘æ¢ç ã€çŽ©å®¶ç¤¾åŒºã€å馈与建议,é¿å…æŠŠå˜æ¡£æˆ–å¡«é‚€è¯·ç æŒ¤å…¥ä¸»å®«æ ¼ç ´åå‚考图布局。å‰ç«¯åªå±•示 `/api/profile/save-archives` 返回的列表并在用户选择åŽè°ƒç”¨å¯¹åº”æ¢å¤æŽ¥å£ï¼Œä¸èƒ½æœ¬åœ°æ‹¼è£…或ç›é€‰æ£å¼å˜æ¡£çœŸç›¸ã€‚
## 拼图
diff --git a/docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md b/docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md
index 0007216b..2544e47d 100644
--- a/docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md
+++ b/docs/ã€é¡¹ç›®åŸºçº¿ã€‘当å‰äº§å“与工程约æŸ-2026-05-15.md
@@ -93,11 +93,12 @@ server-rs + Axum + SpacetimeDB
7. 主站入å£å·²é”定移动端页é¢çº§ç¼©æ”¾ï¼›å•个游æˆé¡µé¢ä¸è¦å†é‡å¤å®žçŽ°æ•´é¡µç¼©æ”¾é”定。
8. 图åƒè¾“入通用 UI 统一走 `src/components/common/CreativeImageInputPanel.tsx`ã€‚å¤–å±‚é¡µé¢æŒæœ‰ä¸šåŠ¡çŠ¶æ€ï¼Œç»„ä»¶åªæ‰¿æ‹…ä¸Šä¼ å¡ã€é¢„览ã€å‚考图缩略图ã€AI é‡ç»˜å¼€å…³ã€é”™è¯¯å±•示和æäº¤æŒ‰é’®ã€‚
9. å‘现页 `分类` å频é“çš„ç›é€‰å¿…须打开独立 dialog / drawer / modal,至少支æŒçŽ©æ³•ç±»åž‹è¿‡æ»¤ä¸ŽæŽ’åºåˆ‡æ¢ï¼›ç›é€‰ç»“果为空时显示空状æ€ï¼Œä¸æŠŠç›é€‰å†…容展开在当å‰åˆ—表下方。
-10. 移动端“我的â€é¡µé¡¶éƒ¨å“牌行承载扫ç 和设置入å£ï¼Œæ£æ–‡æŒ‰å‚考图顺åºç»„ç»‡ä¸ºå¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¼šå‘˜æ¨ªå¹…ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼ã€è®¾ç½®å…¥å£ã€å¯é€‰æ¬¡çº§å…¥å£å’Œæ³•律信æ¯ï¼›`media/profile/` ä¸çš„é™¶æ³¥ç´ æä½œä¸ºè¯¥é¡µå›¾å½¢èµ„äº§ã€‚å¸¸ç”¨åŠŸèƒ½å®«æ ¼å›ºå®šæ‰¿è½½æ³¥ç‚¹å……å€¼ã€é‚€è¯·å¥½å‹ã€å…‘æ¢ç ã€çŽ©å®¶ç¤¾åŒºã€å馈与建议;页é¢ä¸å†æä¾›ç‹¬ç«‹å˜æ¡£æŒ‰é’®å…¥å£ï¼Œå¡«é‚€è¯·ç 仅在新用户å¯å¡«å†™çª—å£å†…展示为次级入å£ã€‚
-11. “我的â€é¡µæ³¥ç‚¹ã€æ¸¸æˆæ—¶é•¿ã€å·²çŽ©æ¸¸æˆæ•°é‡ä¸‰å¼ 统计å¡åªå±•示å„è‡ªæ ‡ç¾å’Œå€¼ï¼Œä¸‰ä¸ªç»Ÿè®¡ icon 使用å°å°ºå¯¸æ™®é€š UI æ¡£ä½ï¼Œå†…容䏿¢è¡Œï¼Œä¸åœ¨ç»Ÿè®¡åŒºåº•éƒ¨å±•ç¤ºâ€œæ›´æ–°äºŽâ€æ—¶é—´ï¼›ç§»åŠ¨ç«¯æ˜µç§°ã€ä¼šå‘˜å¡ã€æ¯æ—¥ä»»åŠ¡ã€å¸¸ç”¨åŠŸèƒ½å’Œæ³•å¾‹ä¿¡æ¯ä¹Ÿåº”ä¿æŒ `10px` 到 `14px` 的普通 UI å—å·åŒºé—´ï¼Œé¿å…展示级å—å·æŒ¤åŽ‹å†…å®¹ã€‚
-12. 移动端“我的â€é¡µéœ€è¦å…¼å®¹çª„å±ï¼šå¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”项常用功能ã€å¯é€‰æ¬¡çº§å…¥å£å’Œæ³•律信æ¯éƒ½å¿…须能在底部固定 TabBar 上方完整滚动露出,ä¸å¾—与底部 dockã€åˆ˜æµ· safe-area 或相邻 UI å…ƒç´ é®æŒ¡é‡å 。
-13. RPG ç‰è¿è¡Œæ€çš„æˆ˜æ–—飘å—ã€è¡€é‡å˜åŒ–å’Œå³æ—¶å馈必须在暗色ã€å™ªå£°é«˜çš„åœºæ™¯èƒŒæ™¯ä¸Šä¿æŒå¯è¯»ï¼šä½¿ç”¨é«˜äº®æ–‡å—ã€æ·±è‰²æè¾¹ã€å¼ºé˜´å½±æˆ–å°é¢ç§¯åŠé€æ˜Žåº•,ä¸åªä¾èµ–红/ç»¿æ–‡å—æœ¬èº«è¡¨è¾¾ä¼¤å®³æˆ–治疗。
-14. å¹³å°äº®è‰² UI é…色以陶泥儿主视觉为准:暖白 / ç±³æåº•ã€é™¶åœŸæ©™ä¸»æŒ‰é’®ã€æ·±æ£•æ£æ–‡ä¸Žæµ…æè¾¹æ¡†ï¼›æ–°å¢žç•Œé¢ä¼˜å…ˆå¤ç”¨ `src/index.css` çš„ `--platform-*` 主题å˜é‡å’Œ `apps/admin-web/src/styles/admin.css` çš„åŒç³»è‰²å€¼ï¼Œä¸å†å¼•入粉红ã€è“绿ç‰ç‹¬ç«‹ä¸»è‰²æ–¹æ¡ˆã€‚
+10. 移动端“我的â€é¡µé¡¶éƒ¨å“牌行承载扫ç 和设置入å£ï¼Œæ£æ–‡æŒ‰å‚考图顺åºç»„ç»‡ä¸ºå¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¼šå‘˜æ¨ªå¹…ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”é¡¹å¸¸ç”¨åŠŸèƒ½å®«æ ¼ã€è®¾ç½®å…¥å£å’Œæ³•律信æ¯ï¼›`media/profile/` ä¸çš„é™¶æ³¥ç´ æä½œä¸ºè¯¥é¡µå›¾å½¢èµ„äº§ã€‚å¸¸ç”¨åŠŸèƒ½å®«æ ¼å›ºå®šæ‰¿è½½æ³¥ç‚¹å……å€¼ã€é‚€è¯·å¥½å‹ã€å…‘æ¢ç ã€çŽ©å®¶ç¤¾åŒºã€å馈与建议;页é¢ä¸å†æä¾›ç‹¬ç«‹å˜æ¡£æŒ‰é’®å…¥å£ï¼Œä¹Ÿä¸åœ¨åº•部ä¿ç•™æ—§çš„å¡«é‚€è¯·ç æ¬¡çº§å…¥å£ã€‚填邀请ç åªç”±é‚€è¯·é“¾æŽ¥ query 或其它明确引导打开独立弹窗,ä¸ä½œä¸ºâ€œæˆ‘çš„â€é¡µå¸¸é©»æŒ‰é’®ã€‚
+11. “我的â€é¡µæ¯æ—¥ä»»åŠ¡å¡å¿…须展示åŽç«¯ `/api/profile/tasks` 返回的当å‰ä»»åŠ¡æ‘˜è¦ï¼ŒåŒ…括奖励泥点数ã€è¿›åº¦å’Œé¢†å– / åŽ»å®Œæˆ / 已完æˆçжæ€ï¼›ä»»åС领喿ˆåŠŸåŽï¼Œå¡ç‰‡æ‘˜è¦å¿…须跟éšè¿”回的任务ä¸å¿ƒæ•°æ®åŒæ¥åˆ·æ–°ï¼Œä¸èƒ½ç»§ç»ç¡¬ç¼–ç `0 / 1` æˆ–åªæ›´æ–°å¼¹çª—内任务列表。
+12. “我的â€é¡µæ³¥ç‚¹ã€æ¸¸æˆæ—¶é•¿ã€å·²çŽ©æ¸¸æˆæ•°é‡ä¸‰å¼ 统计å¡åªå±•示å„è‡ªæ ‡ç¾å’Œå€¼ï¼Œä¸‰ä¸ªç»Ÿè®¡ icon 使用å°å°ºå¯¸æ™®é€š UI æ¡£ä½ï¼Œå†…容䏿¢è¡Œï¼Œä¸åœ¨ç»Ÿè®¡åŒºåº•éƒ¨å±•ç¤ºâ€œæ›´æ–°äºŽâ€æ—¶é—´ï¼›ç§»åŠ¨ç«¯æ˜µç§°ã€ä¼šå‘˜å¡ã€æ¯æ—¥ä»»åŠ¡ã€å¸¸ç”¨åŠŸèƒ½å’Œæ³•å¾‹ä¿¡æ¯ä¹Ÿåº”ä¿æŒ `10px` 到 `14px` 的普通 UI å—å·åŒºé—´ï¼Œé¿å…展示级å—å·æŒ¤åŽ‹å†…å®¹ã€‚
+13. 移动端“我的â€é¡µéœ€è¦å…¼å®¹çª„å±ï¼šå¤´åƒ / 昵称 / é™¶æ³¥å·ã€ä¸‰å¼ 统计å¡ã€æ¯æ—¥ä»»åŠ¡ã€äº”项常用功能和法律信æ¯éƒ½å¿…须能在底部固定 TabBar 上方完整滚动露出,ä¸å¾—与底部 dockã€åˆ˜æµ· safe-area 或相邻 UI å…ƒç´ é®æŒ¡é‡å 。
+14. RPG ç‰è¿è¡Œæ€çš„æˆ˜æ–—飘å—ã€è¡€é‡å˜åŒ–å’Œå³æ—¶å馈必须在暗色ã€å™ªå£°é«˜çš„åœºæ™¯èƒŒæ™¯ä¸Šä¿æŒå¯è¯»ï¼šä½¿ç”¨é«˜äº®æ–‡å—ã€æ·±è‰²æè¾¹ã€å¼ºé˜´å½±æˆ–å°é¢ç§¯åŠé€æ˜Žåº•,ä¸åªä¾èµ–红/ç»¿æ–‡å—æœ¬èº«è¡¨è¾¾ä¼¤å®³æˆ–治疗。
+15. å¹³å°äº®è‰² UI é…色以陶泥儿主视觉为准:暖白 / ç±³æåº•ã€é™¶åœŸæ©™ä¸»æŒ‰é’®ã€æ·±æ£•æ£æ–‡ä¸Žæµ…æè¾¹æ¡†ï¼›æ–°å¢žç•Œé¢ä¼˜å…ˆå¤ç”¨ `src/index.css` çš„ `--platform-*` 主题å˜é‡å’Œ `apps/admin-web/src/styles/admin.css` çš„åŒç³»è‰²å€¼ï¼Œä¸å†å¼•入粉红ã€è“绿ç‰ç‹¬ç«‹ä¸»è‰²æ–¹æ¡ˆã€‚
## 文案与编ç
diff --git a/src/components/CustomWorldGenerationView.tsx b/src/components/CustomWorldGenerationView.tsx
index 9a00c842..55efe604 100644
--- a/src/components/CustomWorldGenerationView.tsx
+++ b/src/components/CustomWorldGenerationView.tsx
@@ -13,7 +13,7 @@ interface CustomWorldGenerationViewProps {
anchorEntries?: CustomWorldStructuredAnchorEntry[];
progress: CustomWorldGenerationProgress | null;
isGenerating: boolean;
- error: string | null;
+ error?: string | null;
onBack: () => void;
onEditSetting: () => void;
onRetry: () => void;
@@ -110,7 +110,6 @@ export function CustomWorldGenerationView({
anchorEntries = [],
progress,
isGenerating,
- error,
onBack,
onEditSetting,
onRetry,
@@ -123,7 +122,6 @@ export function CustomWorldGenerationView({
settingDescription = '这段文本会直接驱动本轮世界框架ã€è§’色与场景生æˆã€‚',
progressTitle = '生æˆè¿›åº¦',
activeBadgeLabel = '世界建设ä¸',
- pausedBadgeLabel = '生æˆå·²æš‚åœ',
idleBadgeLabel = 'ç‰å¾…æ“作',
structuredEmptyText = 'æ£åœ¨æ•´ç†å½“å‰è®¾å®šç»“构,请ç¨åŽã€‚',
hideBatchModule = false,
@@ -169,11 +167,7 @@ export function CustomWorldGenerationView({
{backLabel}
- {isGenerating
- ? activeBadgeLabel
- : error
- ? pausedBadgeLabel
- : idleBadgeLabel}
+ {isGenerating ? activeBadgeLabel : idleBadgeLabel}
@@ -195,12 +189,6 @@ export function CustomWorldGenerationView({
/>
- {error ? (
-
- {error}
-
- ) : null}
-
{!isGenerating ? (
<>
diff --git a/src/components/custom-world-home/CustomWorldCreationHub.tsx b/src/components/custom-world-home/CustomWorldCreationHub.tsx
index ccd20cd5..12520cf8 100644
--- a/src/components/custom-world-home/CustomWorldCreationHub.tsx
+++ b/src/components/custom-world-home/CustomWorldCreationHub.tsx
@@ -1,12 +1,12 @@
import { useEffect, useMemo, useState } from 'react';
-import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { BarkBattleWorkSummary } from '../../../packages/shared/src/contracts/barkBattle';
+import type { BigFishWorkSummary } from '../../../packages/shared/src/contracts/bigFishWorkSummary';
import type { CustomWorldWorkSummary } from '../../../packages/shared/src/contracts/customWorldAgent';
import type { BabyObjectMatchDraft } from '../../../packages/shared/src/contracts/edutainmentBabyObject';
+import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { Match3DWorkSummary } from '../../../packages/shared/src/contracts/match3dWorks';
import type { PuzzleWorkSummary } from '../../../packages/shared/src/contracts/puzzleWorkSummary';
-import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type { CustomWorldLibraryEntry } from '../../../packages/shared/src/contracts/runtime';
import type { SquareHoleWorkSummary } from '../../../packages/shared/src/contracts/squareHoleWorks';
import type { VisualNovelWorkSummary } from '../../../packages/shared/src/contracts/visualNovel';
@@ -43,7 +43,6 @@ type CustomWorldCreationHubProps = {
loading: boolean;
error: string | null;
onRetry: () => void;
- createError?: string | null;
createBusy?: boolean;
entryConfig: CreationEntryConfig;
creationTypes: readonly PlatformCreationTypeCard[];
@@ -154,7 +153,6 @@ export function CustomWorldCreationHub({
loading,
error,
onRetry,
- createError = null,
createBusy = false,
entryConfig,
creationTypes,
@@ -360,7 +358,6 @@ export function CustomWorldCreationHub({
{showStartCard ? (
- {error}
+
diff --git a/src/components/custom-world-home/CustomWorldCreationStartCard.tsx b/src/components/custom-world-home/CustomWorldCreationStartCard.tsx
index c5021674..6647e42c 100644
--- a/src/components/custom-world-home/CustomWorldCreationStartCard.tsx
+++ b/src/components/custom-world-home/CustomWorldCreationStartCard.tsx
@@ -1,5 +1,5 @@
import { Coins, Trophy } from 'lucide-react';
-import { useMemo, useState, type UIEvent } from 'react';
+import { type UIEvent,useMemo, useState } from 'react';
import type { CreationEntryConfig } from '../../services/creationEntryConfigService';
import {
@@ -10,7 +10,6 @@ import {
type CustomWorldCreationStartCardProps = {
busy?: boolean;
- error?: string | null;
entryConfig: CreationEntryConfig;
creationTypes: readonly PlatformCreationTypeCard[];
onCreateType: (type: PlatformCreationTypeId) => void;
@@ -25,7 +24,6 @@ function shouldShowCreationBadge(badge: string) {
export function CustomWorldCreationStartCard({
busy = false,
- error = null,
entryConfig,
creationTypes,
onCreateType,
@@ -233,11 +231,6 @@ export function CustomWorldCreationStartCard({
})}
- {error ? (
-
- {error}
-
- ) : null}
);
diff --git a/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx b/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx
index 583c19ad..fdfe2575 100644
--- a/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx
+++ b/src/components/platform-entry/PlatformEntryCreationTypeModal.test.tsx
@@ -51,7 +51,6 @@ test('dispatches wooden fish creation type selection', () => {
{}}
diff --git a/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx b/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx
index 8d6698aa..21b297b9 100644
--- a/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx
+++ b/src/components/platform-entry/PlatformEntryCreationTypeModal.tsx
@@ -10,7 +10,6 @@ import {
export interface PlatformEntryCreationTypeModalProps {
isOpen: boolean;
isBusy: boolean;
- error: string | null;
entryConfig: CreationEntryConfig;
creationTypes: readonly PlatformCreationTypeCard[];
onClose: () => void;
@@ -94,7 +93,6 @@ function CreationTypeCard(props: {
export function PlatformEntryCreationTypeModal({
isOpen,
isBusy,
- error,
entryConfig,
creationTypes,
onClose,
@@ -172,11 +170,6 @@ export function PlatformEntryCreationTypeModal({
))}
- {error ? (
-
- {error}
-
- ) : null}
);
}
diff --git a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
index c6359f6b..a74239f8 100644
--- a/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
+++ b/src/components/platform-entry/PlatformEntryFlowShellImpl.tsx
@@ -38,6 +38,7 @@ import type {
BabyObjectMatchDraft,
CreateBabyObjectMatchDraftRequest,
} from '../../../packages/shared/src/contracts/edutainmentBabyObject';
+import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import type {
CreateMatch3DSessionRequest,
ExecuteMatch3DActionRequest,
@@ -154,13 +155,6 @@ import {
type CreationEntryConfig,
fetchCreationEntryConfig,
} from '../../services/creationEntryConfigService';
-import {
- cancelCreativeAgentSession,
- confirmCreativePuzzleTemplate,
- createCreativeAgentSession,
- streamCreativeAgentMessage,
- streamCreativeDraftEdit,
-} from '../../services/creative-agent';
import {
clearCreationUrlState,
type CreationUrlState,
@@ -169,11 +163,12 @@ import {
writeCreationUrlState,
} from '../../services/creationUrlState';
import {
- clearPuzzleRuntimeUrlState,
- readPuzzleRuntimeUrlState,
- writePuzzleRuntimeUrlState,
- type PuzzleRuntimeUrlState,
-} from '../../services/puzzleRuntimeUrlState';
+ cancelCreativeAgentSession,
+ confirmCreativePuzzleTemplate,
+ createCreativeAgentSession,
+ streamCreativeAgentMessage,
+ streamCreativeDraftEdit,
+} from '../../services/creative-agent';
import {
readCustomWorldAgentUiState,
shouldRestoreCustomWorldAgentUiState,
@@ -196,7 +191,6 @@ import {
JumpHopWorkProfileResponse,
JumpHopWorkspaceCreateRequest,
} from '../../services/jump-hop/jumpHopClient';
-import type { JumpHopWorkSummaryResponse } from '../../../packages/shared/src/contracts/jumpHop';
import { match3dCreationClient } from '../../services/match3d-creation';
import { createServerMatch3DRuntimeAdapter } from '../../services/match3d-runtime';
import {
@@ -287,6 +281,12 @@ import {
listPuzzleWorks,
updatePuzzleWork,
} from '../../services/puzzle-works';
+import {
+ clearPuzzleRuntimeUrlState,
+ type PuzzleRuntimeUrlState,
+ readPuzzleRuntimeUrlState,
+ writePuzzleRuntimeUrlState,
+} from '../../services/puzzleRuntimeUrlState';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import {
@@ -375,6 +375,7 @@ import {
mapVisualNovelWorkToPlatformGalleryCard,
mapWoodenFishWorkToPlatformGalleryCard,
type PlatformPublicGalleryCard,
+ resolvePlatformPublicWorkCode,
} from '../rpg-entry/rpgEntryWorldPresentation';
import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreationAgentOperationPolling';
import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld';
@@ -414,6 +415,7 @@ import {
PlatformEntryHomeView,
type PlatformHomeTab,
} from './PlatformEntryHomeView';
+import { usePlatformDesktopLayout } from './platformEntryResponsive';
import {
buildCreationHubFallbackItems,
resolveRpgCreationErrorMessage,
@@ -423,11 +425,14 @@ import type {
SelectionStage,
} from './platformEntryTypes';
import { PlatformEntryWorldDetailView } from './PlatformEntryWorldDetailView';
+import {
+ PlatformErrorDialog,
+ type PlatformErrorDialogPayload,
+} from './PlatformErrorDialog';
import { PlatformFeedbackView } from './PlatformFeedbackView';
import { PlatformWorkDetailView } from './PlatformWorkDetailView';
import { usePlatformCreationAgentFlowController } from './usePlatformCreationAgentFlowController';
import { usePlatformEntryBootstrap } from './usePlatformEntryBootstrap';
-import { usePlatformDesktopLayout } from './platformEntryResponsive';
import { usePlatformEntryLibraryDetail } from './usePlatformEntryLibraryDetail';
import { usePlatformEntryNavigation } from './usePlatformEntryNavigation';
@@ -2012,6 +2017,22 @@ function createPendingDraftShelfState(
};
}
+function normalizePlatformErrorMessage(message: string | null | undefined) {
+ const normalized = message?.trim();
+ return normalized ? normalized : null;
+}
+
+function formatPlatformErrorSource(label: string, id?: string | null) {
+ const normalizedId = id?.trim();
+ return normalizedId ? `${label} ${normalizedId}` : label;
+}
+
+function buildPlatformErrorDialogDismissKey(
+ error: (PlatformErrorDialogPayload & { key: string }) | null,
+) {
+ return error ? `${error.key}:${error.source}:${error.message}` : null;
+}
+
function createMiniGameDraftGenerationStateForRestoredDraft(
kind: MiniGameDraftGenerationKind,
metadata?: MiniGameDraftGenerationState['metadata'],
@@ -5767,6 +5788,336 @@ export function PlatformEntryFlowShellImpl({
isMiniGameDraftGenerating(
activePuzzleBackgroundCompileTask?.generationState ?? null,
);
+ const [dismissedPlatformErrorDialogKey, setDismissedPlatformErrorDialogKey] =
+ useState(null);
+ const currentPlatformErrorDialog = useMemo<
+ (PlatformErrorDialogPayload & { key: string }) | null
+ >(() => {
+ const candidates: Array<{
+ key: string;
+ source: string;
+ message: string | null | undefined;
+ }> = [
+ {
+ key: 'creation-entry-config',
+ source: '创作入å£é…ç½®',
+ message: creationEntryConfigError,
+ },
+ {
+ key: 'platform-bootstrap',
+ source: 'å¹³å°é¦–页',
+ message: platformBootstrap.platformError,
+ },
+ {
+ key: 'rpg-creation-type',
+ source: '创作入å£',
+ message: sessionController.creationTypeError,
+ },
+ {
+ key: 'rpg-restore',
+ source: 'åˆ›ä½œä½œå“æž¶',
+ message: sessionController.agentWorkspaceRestoreError,
+ },
+ {
+ key: 'rpg-result',
+ source: formatPlatformErrorSource(
+ 'RPG è‰ç¨¿',
+ sessionController.agentSession?.sessionId ??
+ sessionController.generatedCustomWorldProfile?.id,
+ ),
+ message: resultViewError,
+ },
+ {
+ key: 'public-work-detail',
+ source: formatPlatformErrorSource(
+ '作å“详情',
+ selectedPublicWorkDetail
+ ? resolvePlatformPublicWorkCode(selectedPublicWorkDetail)
+ : selectedDetailEntry?.profileId,
+ ),
+ message: publicWorkDetailError ?? detailNavigation.detailError,
+ },
+ {
+ key: 'big-fish',
+ source: formatPlatformErrorSource(
+ selectionStage === 'big-fish-runtime' ? '大鱼åƒå°é±¼æ¸¸çŽ©' : '大鱼è‰ç¨¿',
+ bigFishRun?.runId ?? bigFishSession?.sessionId,
+ ),
+ message: bigFishError,
+ },
+ {
+ key: 'match3d',
+ source: formatPlatformErrorSource(
+ selectionStage === 'match3d-runtime' ? '抓大鹅游玩' : '抓大鹅è‰ç¨¿',
+ match3dRun?.runId ??
+ match3dGenerationViewSession?.sessionId ??
+ match3dSession?.sessionId,
+ ),
+ message: match3dGenerationViewError ?? match3dError,
+ },
+ {
+ key: 'square-hole',
+ source: formatPlatformErrorSource(
+ selectionStage === 'square-hole-runtime'
+ ? '方洞挑战游玩'
+ : '方洞挑战è‰ç¨¿',
+ squareHoleRun?.runId ?? squareHoleSession?.sessionId,
+ ),
+ message: squareHoleError,
+ },
+ {
+ key: 'jump-hop',
+ source: formatPlatformErrorSource(
+ selectionStage === 'jump-hop-runtime' ? '跳一跳游玩' : '跳一跳è‰ç¨¿',
+ jumpHopRun?.runId ?? jumpHopSession?.sessionId,
+ ),
+ message: jumpHopError,
+ },
+ {
+ key: 'wooden-fish',
+ source: formatPlatformErrorSource(
+ selectionStage === 'wooden-fish-runtime'
+ ? '敲木鱼游玩'
+ : '敲木鱼è‰ç¨¿',
+ woodenFishRun?.runId ?? woodenFishSession?.sessionId,
+ ),
+ message: woodenFishError,
+ },
+ {
+ key: 'puzzle',
+ source: formatPlatformErrorSource(
+ selectionStage === 'puzzle-runtime' ? '拼图游玩' : '拼图è‰ç¨¿',
+ puzzleRun?.runId ??
+ puzzleGenerationViewSession?.sessionId ??
+ puzzleSession?.sessionId,
+ ),
+ message: puzzleGenerationViewError ?? puzzleCreationError ?? puzzleError,
+ },
+ {
+ key: 'puzzle-onboarding',
+ source: '拼图首次创作',
+ message: puzzleOnboardingError,
+ },
+ {
+ key: 'puzzle-shelf',
+ source: 'æ‹¼å›¾ä½œå“æž¶',
+ message: puzzleShelfError,
+ },
+ {
+ key: 'visual-novel',
+ source: formatPlatformErrorSource(
+ selectionStage === 'visual-novel-runtime'
+ ? '视觉å°è¯´æ¸¸çŽ©'
+ : '视觉å°è¯´è‰ç¨¿',
+ visualNovelRun?.runId ?? visualNovelSession?.sessionId,
+ ),
+ message: visualNovelError,
+ },
+ {
+ key: 'baby-object-match',
+ source: formatPlatformErrorSource(
+ selectionStage === 'baby-object-match-runtime'
+ ? 'å®è´è¯†ç‰©æ¸¸çŽ©'
+ : 'å®è´è¯†ç‰©è‰ç¨¿',
+ babyObjectMatchDraft?.profileId,
+ ),
+ message: babyObjectMatchError,
+ },
+ {
+ key: 'bark-battle',
+ source: formatPlatformErrorSource(
+ selectionStage === 'bark-battle-runtime'
+ ? '汪汪声浪游玩'
+ : '汪汪声浪è‰ç¨¿',
+ barkBattlePublishedConfig?.workId ?? barkBattleDraftConfig?.workId,
+ ),
+ message: barkBattleError,
+ },
+ {
+ key: 'creative-agent',
+ source: formatPlatformErrorSource(
+ '智能创作 Agent',
+ creativeAgentSession?.sessionId,
+ ),
+ message: creativeAgentError,
+ },
+ {
+ key: 'rpg-generation',
+ source: formatPlatformErrorSource(
+ 'RPG è‰ç¨¿ç”Ÿæˆ',
+ sessionController.agentSession?.sessionId,
+ ),
+ message: sessionController.activeGenerationError,
+ },
+ ];
+
+ for (const candidate of candidates) {
+ const message = normalizePlatformErrorMessage(candidate.message);
+ if (message) {
+ return {
+ key: candidate.key,
+ source: candidate.source,
+ message,
+ };
+ }
+ }
+
+ return null;
+ }, [
+ babyObjectMatchDraft?.profileId,
+ babyObjectMatchError,
+ barkBattleDraftConfig?.workId,
+ barkBattleError,
+ barkBattlePublishedConfig?.workId,
+ bigFishError,
+ bigFishRun?.runId,
+ bigFishSession?.sessionId,
+ creationEntryConfigError,
+ creativeAgentError,
+ creativeAgentSession?.sessionId,
+ detailNavigation.detailError,
+ jumpHopError,
+ jumpHopRun?.runId,
+ jumpHopSession?.sessionId,
+ match3dError,
+ match3dGenerationViewError,
+ match3dGenerationViewSession?.sessionId,
+ match3dRun?.runId,
+ match3dSession?.sessionId,
+ platformBootstrap.platformError,
+ publicWorkDetailError,
+ puzzleCreationError,
+ puzzleError,
+ puzzleGenerationViewError,
+ puzzleGenerationViewSession?.sessionId,
+ puzzleOnboardingError,
+ puzzleRun?.runId,
+ puzzleSession?.sessionId,
+ puzzleShelfError,
+ resultViewError,
+ selectedDetailEntry?.profileId,
+ selectedPublicWorkDetail,
+ selectionStage,
+ sessionController.activeGenerationError,
+ sessionController.agentSession?.sessionId,
+ sessionController.agentWorkspaceRestoreError,
+ sessionController.creationTypeError,
+ sessionController.generatedCustomWorldProfile?.id,
+ squareHoleError,
+ squareHoleRun?.runId,
+ squareHoleSession?.sessionId,
+ visualNovelError,
+ visualNovelRun?.runId,
+ visualNovelSession?.sessionId,
+ woodenFishError,
+ woodenFishRun?.runId,
+ woodenFishSession?.sessionId,
+ ]);
+ const activePlatformErrorDialogDismissKey =
+ buildPlatformErrorDialogDismissKey(currentPlatformErrorDialog);
+ const activePlatformErrorDialog =
+ activePlatformErrorDialogDismissKey &&
+ activePlatformErrorDialogDismissKey === dismissedPlatformErrorDialogKey
+ ? null
+ : currentPlatformErrorDialog;
+ const closePlatformErrorDialog = useCallback(() => {
+ if (!currentPlatformErrorDialog) {
+ return;
+ }
+
+ const dismissKey = buildPlatformErrorDialogDismissKey(
+ currentPlatformErrorDialog,
+ );
+ if (dismissKey) {
+ setDismissedPlatformErrorDialogKey(dismissKey);
+ }
+
+ if (currentPlatformErrorDialog.key === 'creation-entry-config') {
+ setCreationEntryConfigError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'platform-bootstrap') {
+ platformBootstrap.setPlatformError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'rpg-creation-type') {
+ sessionController.setCreationTypeError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'rpg-restore') {
+ return;
+ }
+ if (
+ currentPlatformErrorDialog.key === 'rpg-result' ||
+ currentPlatformErrorDialog.key === 'rpg-generation'
+ ) {
+ autosaveCoordinator.setCustomWorldAutoSaveError(null);
+ sessionController.setCustomWorldError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'public-work-detail') {
+ setPublicWorkDetailError(null);
+ detailNavigation.setDetailError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'big-fish') {
+ setBigFishError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'match3d') {
+ setMatch3DError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'square-hole') {
+ setSquareHoleError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'jump-hop') {
+ setJumpHopError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'wooden-fish') {
+ setWoodenFishError(null);
+ return;
+ }
+ if (
+ currentPlatformErrorDialog.key === 'puzzle' ||
+ currentPlatformErrorDialog.key === 'puzzle-onboarding' ||
+ currentPlatformErrorDialog.key === 'puzzle-shelf'
+ ) {
+ setPuzzleCreationError(null);
+ setPuzzleOnboardingError(null);
+ setPuzzleShelfError(null);
+ setPuzzleError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'visual-novel') {
+ setVisualNovelError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'baby-object-match') {
+ setBabyObjectMatchError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'bark-battle') {
+ setBarkBattleError(null);
+ return;
+ }
+ if (currentPlatformErrorDialog.key === 'creative-agent') {
+ setCreativeAgentError(null);
+ }
+ }, [
+ autosaveCoordinator,
+ currentPlatformErrorDialog,
+ detailNavigation,
+ platformBootstrap,
+ sessionController,
+ setBigFishError,
+ setMatch3DError,
+ setPuzzleError,
+ setSquareHoleError,
+ setVisualNovelError,
+ ]);
const shouldPollPuzzleGenerationSession =
selectionStage === 'puzzle-generating' &&
activePuzzleGenerationSessionId != null &&
@@ -14098,19 +14449,6 @@ export function PlatformEntryFlowShellImpl({
void refreshBabyObjectMatchShelf();
void refreshBarkBattleShelf();
}}
- createError={
- creationEntryConfigError ??
- sessionController.creationTypeError ??
- bigFishError ??
- match3dError ??
- (isSquareHoleCreationVisible ? squareHoleError : null) ??
- woodenFishError ??
- puzzleCreationError ??
- puzzleError ??
- (isVisualNovelCreationOpen ? visualNovelError : null) ??
- babyObjectMatchError ??
- barkBattleError
- }
createBusy={
!creationEntryConfig ||
sessionController.isCreatingAgentSession ||
@@ -15762,7 +16100,6 @@ export function PlatformEntryFlowShellImpl({
settingDescription={null}
progressTitle="拼图è‰ç¨¿ç”Ÿæˆè¿›åº¦"
activeBadgeLabel="è‰ç¨¿ç”Ÿæˆä¸"
- pausedBadgeLabel="è‰ç¨¿ç”Ÿæˆå·²æš‚åœ"
idleBadgeLabel="ç‰å¾…返回工作区"
hideBatchModule
/>
@@ -16420,7 +16757,7 @@ export function PlatformEntryFlowShellImpl({
{creationEntryConfig ? (
- {
@@ -16542,6 +16865,12 @@ export function PlatformEntryFlowShellImpl({
payload={publishSharePayload}
onClose={() => setPublishSharePayload(null)}
/>
+
({
+ copyTextToClipboard: vi.fn(),
+}));
+
+afterEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('PlatformErrorDialog', () => {
+ test('shows source, message, and copies the full error report', async () => {
+ vi.mocked(clipboardService.copyTextToClipboard).mockResolvedValue(true);
+
+ render(
+ {}}
+ />,
+ );
+
+ const dialog = screen.getByRole('dialog', { name: 'å‘生错误' });
+ expect(within(dialog).getByText('拼图è‰ç¨¿ puzzle-session-123')).toBeTruthy();
+ expect(within(dialog).getByText('图片生æˆå¤±è´¥ï¼Œè¯·ç¨åŽå†è¯•。')).toBeTruthy();
+
+ fireEvent.click(within(dialog).getByRole('button', { name: 'å¤åˆ¶æŠ¥é”™' }));
+
+ expect(clipboardService.copyTextToClipboard).toHaveBeenCalledWith(
+ ['æ¥æºï¼šæ‹¼å›¾è‰ç¨¿ puzzle-session-123', '错误:图片生æˆå¤±è´¥ï¼Œè¯·ç¨åŽå†è¯•。'].join(
+ '\n',
+ ),
+ );
+ await waitFor(() => {
+ expect(
+ within(dialog).getByRole('button', { name: 'å·²å¤åˆ¶' }),
+ ).toBeTruthy();
+ });
+ });
+
+ test('does not render when there is no active error', () => {
+ render( {}} />);
+
+ expect(screen.queryByRole('dialog', { name: 'å‘生错误' })).toBeNull();
+ });
+});
diff --git a/src/components/platform-entry/PlatformErrorDialog.tsx b/src/components/platform-entry/PlatformErrorDialog.tsx
new file mode 100644
index 00000000..794a6a5c
--- /dev/null
+++ b/src/components/platform-entry/PlatformErrorDialog.tsx
@@ -0,0 +1,120 @@
+import { Check, Copy } from 'lucide-react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+
+import { copyTextToClipboard } from '../../services/clipboard';
+import { UnifiedModal } from '../common/UnifiedModal';
+
+export type PlatformErrorDialogPayload = {
+ source: string;
+ message: string;
+};
+
+type PlatformErrorDialogProps = {
+ error: PlatformErrorDialogPayload | null;
+ onClose: () => void;
+ overlayClassName?: string;
+ panelClassName?: string;
+};
+
+function buildPlatformErrorReport(error: PlatformErrorDialogPayload) {
+ return [`æ¥æºï¼š${error.source}`, `错误:${error.message}`].join('\n');
+}
+
+export function PlatformErrorDialog({
+ error,
+ onClose,
+ overlayClassName = 'platform-theme platform-theme--light !items-center',
+ panelClassName = 'platform-remap-surface rounded-[1.5rem]',
+}: PlatformErrorDialogProps) {
+ const [copyState, setCopyState] = useState<'idle' | 'copied' | 'failed'>(
+ 'idle',
+ );
+ const resetTimerRef = useRef(null);
+ const reportText = useMemo(
+ () => (error ? buildPlatformErrorReport(error) : ''),
+ [error],
+ );
+
+ useEffect(
+ () => () => {
+ if (resetTimerRef.current !== null) {
+ window.clearTimeout(resetTimerRef.current);
+ }
+ },
+ [],
+ );
+
+ useEffect(() => {
+ setCopyState('idle');
+ }, [error?.source, error?.message]);
+
+ const copyError = () => {
+ if (!reportText) {
+ return;
+ }
+
+ void copyTextToClipboard(reportText).then((copied) => {
+ setCopyState(copied ? 'copied' : 'failed');
+ if (resetTimerRef.current !== null) {
+ window.clearTimeout(resetTimerRef.current);
+ }
+ resetTimerRef.current = window.setTimeout(() => {
+ resetTimerRef.current = null;
+ setCopyState('idle');
+ }, 1400);
+ });
+ };
+
+ return (
+
+ {copyState === 'copied' ? (
+
+ ) : (
+
+ )}
+ {copyState === 'copied'
+ ? 'å·²å¤åˆ¶'
+ : copyState === 'failed'
+ ? 'å¤åˆ¶å¤±è´¥'
+ : 'å¤åˆ¶æŠ¥é”™'}
+
+ }
+ >
+ {error ? (
+ <>
+
+
+ æ¥æº
+
+
+ {error.source}
+
+
+
+
+ 错误
+
+
+ {error.message}
+
+
+ >
+ ) : null}
+
+ );
+}
diff --git a/src/components/platform-entry/PlatformWorkDetailView.tsx b/src/components/platform-entry/PlatformWorkDetailView.tsx
index 7477d978..65d3c404 100644
--- a/src/components/platform-entry/PlatformWorkDetailView.tsx
+++ b/src/components/platform-entry/PlatformWorkDetailView.tsx
@@ -24,7 +24,6 @@ import {
formatPlatformWorldTime,
isBarkBattleGalleryEntry,
isEdutainmentGalleryEntry,
- isJumpHopGalleryEntry,
type PlatformPublicGalleryCard,
resolvePlatformPublicWorkCode,
resolvePlatformWorldCoverSlides,
@@ -36,7 +35,7 @@ export interface PlatformWorkDetailViewProps {
authorAvatarUrl?: string | null;
authorDisplayName?: string | null;
isBusy: boolean;
- error: string | null;
+ error?: string | null;
visibleCoverCount?: number;
onBack: () => void;
onLike: () => void;
@@ -89,7 +88,6 @@ export function PlatformWorkDetailView({
authorAvatarUrl,
authorDisplayName,
isBusy,
- error,
visibleCoverCount = 1,
onBack,
onLike,
@@ -432,9 +430,6 @@ export function PlatformWorkDetailView({
{shareState === 'copied' ? '分享内容已å¤åˆ¶' : '分享失败'}
) : null}
- {error ? (
- {error}
- ) : null}
diff --git a/src/components/puzzle-result/PuzzleResultView.tsx b/src/components/puzzle-result/PuzzleResultView.tsx
index 4b280ad5..093b529a 100644
--- a/src/components/puzzle-result/PuzzleResultView.tsx
+++ b/src/components/puzzle-result/PuzzleResultView.tsx
@@ -1819,12 +1819,7 @@ export function PuzzleResultView({
) : null}
- {error ? (
-
- {error}
-
- ) : null}
- {!error && autoSaveError ? (
+ {autoSaveError ? (
{autoSaveError}
diff --git a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
index f27b62c7..f336c6d6 100644
--- a/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
+++ b/src/components/rpg-entry/RpgEntryHomeView.recharge.test.tsx
@@ -1826,11 +1826,18 @@ test('non-wechat profile opens reward code from recharge-shaped entry', async ()
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
-test('profile daily task shortcut opens task center and claims reward', async () => {
+test('profile daily task shortcut reflects task progress and claim updates', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
renderProfileView(onRechargeSuccess);
+
+ const dailyTask = screen.getByRole('button', { name: /æ¯æ—¥ä»»åŠ¡/u });
+ await waitFor(() => {
+ expect(within(dailyTask).getByText('1 / 1')).toBeTruthy();
+ });
+ expect(within(dailyTask).getByText('领å–')).toBeTruthy();
+
await user.click(screen.getByRole('button', { name: /æ¯æ—¥ä»»åŠ¡/u }));
expect(await screen.findByText('æ¯æ—¥ç™»å½•')).toBeTruthy();
@@ -1847,6 +1854,7 @@ test('profile daily task shortcut opens task center and claims reward', async ()
expect(await screen.findByText('å·²é¢†å– 10 泥点')).toBeTruthy();
expect(screen.queryByRole('button', { name: '已领å–' })).toBeNull();
expect(screen.getByText('æš‚æ— ä»»åŠ¡')).toBeTruthy();
+ expect(within(dailyTask).getByText('已完æˆ')).toBeTruthy();
});
test('profile task center keeps only the highest priority actionable task', async () => {
@@ -1909,7 +1917,7 @@ test('profile task center keeps only the highest priority actionable task', asyn
expect(screen.queryByText('低优先级已完æˆ')).toBeNull();
});
-test('profile total play time card always uses hours', () => {
+test('profile total play time card always uses hours', async () => {
renderProfileView(vi.fn(), {
totalPlayTimeMs: 90 * 60 * 1000,
});
@@ -1920,9 +1928,10 @@ test('profile total play time card always uses hours', () => {
expect(within(playTimeCard).getByText('1.5å°æ—¶')).toBeTruthy();
expect(within(playTimeCard).queryByText('90分')).toBeNull();
+ await screen.findByText('1 / 1');
});
-test('profile played works card shows count unit', () => {
+test('profile played works card shows count unit', async () => {
renderProfileView(vi.fn(), {
playedWorldCount: 1,
});
@@ -1932,9 +1941,10 @@ test('profile played works card shows count unit', () => {
});
expect(within(playedCard).getByText('1个')).toBeTruthy();
+ await screen.findByText('1 / 1');
});
-test('profile stats cards are centered without update timestamp', () => {
+test('profile stats cards are centered without update timestamp', async () => {
renderProfileView(vi.fn(), {
updatedAt: '2026-05-03T08:01:00Z',
});
@@ -1950,6 +1960,7 @@ test('profile stats cards are centered without update timestamp', () => {
expect(card.className).toContain('text-center');
}
expect(screen.queryByText(/更新于/u)).toBeNull();
+ await screen.findByText('1 / 1');
});
test('mobile profile page matches the reference layout sections', async () => {
@@ -2007,7 +2018,7 @@ test('mobile profile page matches the reference layout sections', async () => {
expect(dailyTask.querySelector('.platform-profile-daily-task-card__desc')).toBeTruthy();
expect(dailyTask.querySelector('.platform-profile-daily-task-card__progress')).toBeTruthy();
expect(dailyTask.textContent).toContain('完æˆä»»åŠ¡å¯é¢†å– 10 泥点');
- expect(within(dailyTask).getByText('0 / 1')).toBeTruthy();
+ expect(await within(dailyTask).findByText('1 / 1')).toBeTruthy();
const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
expect(
@@ -2101,7 +2112,7 @@ test('profile scan action opens camera scanner instead of recharge panel', async
expect(mockGetRpgProfileRechargeCenter).not.toHaveBeenCalled();
});
-test('desktop account entry uses saved avatar image when available', () => {
+test('desktop account entry uses saved avatar image when available', async () => {
mockDesktopLayout();
const avatarUrl = 'data:image/png;base64,AAAA';
@@ -2111,6 +2122,7 @@ test('desktop account entry uses saved avatar image when available', () => {
const avatarImage = accountEntry.querySelector('img');
expect(avatarImage?.getAttribute('src')).toBe(avatarUrl);
expect(within(accountEntry).queryByText('测')).toBeNull();
+ await screen.findByText('1 / 1');
});
test('profile avatar upload uses the shared square crop tool', async () => {
@@ -2184,7 +2196,7 @@ test('profile invite shortcut shows reward subtitle and invited users', async ()
expect(screen.queryByText('今日')).toBeNull();
});
-test('profile redeem invite shortcut sits between invite and community for fresh accounts', async () => {
+test('profile page hides legacy redeem invite secondary shortcut for fresh accounts', async () => {
renderProfileView(
vi.fn(),
{},
@@ -2192,20 +2204,16 @@ test('profile redeem invite shortcut sits between invite and community for fresh
);
const inviteButton = screen.getByRole('button', { name: /邀请好å‹/u });
- const redeemButton = await screen.findByRole('button', {
- name: /填邀请ç /u,
- });
const communityButton = screen.getByRole('button', { name: /玩家社区/u });
- const secondaryShortcuts = screen.getByRole('region', {
- name: '次级入å£',
+
+ await waitFor(() => {
+ expect(mockGetRpgProfileReferralInviteCenter).toHaveBeenCalledTimes(1);
});
expect(inviteButton).toBeTruthy();
expect(communityButton).toBeTruthy();
- expect(
- within(secondaryShortcuts).getByRole('button', { name: /填邀请ç /u }),
- ).toBeTruthy();
- expect(within(redeemButton).getByText('新用户奖励')).toBeTruthy();
+ expect(screen.queryByRole('region', { name: '次级入å£' })).toBeNull();
+ expect(screen.queryByRole('button', { name: /填邀请ç /u })).toBeNull();
});
test('profile redeem invite shortcut hides after redeemed or one day old', async () => {
@@ -2226,6 +2234,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
expect(
within(firstShortcutRegion).queryByRole('button', { name: /填邀请ç /u }),
).toBeNull();
+ await screen.findByText('1 / 1');
unmount();
renderProfileView(vi.fn(), {}, { createdAt: '2026-04-01T00:00:00.000Z' });
@@ -2237,6 +2246,7 @@ test('profile redeem invite shortcut hides after redeemed or one day old', async
name: /填邀请ç /u,
}),
).toBeNull();
+ await screen.findByText('1 / 1');
});
test('invite query opens login modal for logged out users', async () => {
@@ -2269,9 +2279,10 @@ test('profile redeem invite modal reads query invite code after login', async ()
expect((input as HTMLInputElement).value).toBe('SPRING2026');
});
-test('profile redeem invite modal submits code and hides shortcut after success', async () => {
+test('profile redeem invite query modal submits code after login', async () => {
const user = userEvent.setup();
const onRechargeSuccess = vi.fn();
+ window.history.replaceState(null, '', '/?inviteCode=spring-2026');
renderProfileView(
onRechargeSuccess,
@@ -2279,9 +2290,7 @@ test('profile redeem invite modal submits code and hides shortcut after success'
{ createdAt: buildFreshProfileCreatedAt() },
);
- await user.click(await screen.findByRole('button', { name: /填邀请ç /u }));
- const input = await screen.findByLabelText('邀请ç ');
- await user.type(input, 'spring-2026');
+ expect(await screen.findByLabelText('邀请ç ')).toBeTruthy();
await user.click(screen.getByRole('button', { name: 'æäº¤' }));
await waitFor(() => {
@@ -2291,12 +2300,7 @@ test('profile redeem invite modal submits code and hides shortcut after success'
});
expect(onRechargeSuccess).toHaveBeenCalledTimes(1);
expect(await screen.findByText('已填写')).toBeTruthy();
- const shortcutRegion = screen.getByRole('region', { name: '常用功能' });
- expect(
- within(shortcutRegion).queryByRole('button', {
- name: /填邀请ç /u,
- }),
- ).toBeNull();
+ expect(screen.queryByRole('region', { name: '次级入å£' })).toBeNull();
});
test('opens reward code modal from profile action on mobile', async () => {
diff --git a/src/components/rpg-entry/RpgEntryHomeView.tsx b/src/components/rpg-entry/RpgEntryHomeView.tsx
index 506b9c01..ab961e97 100644
--- a/src/components/rpg-entry/RpgEntryHomeView.tsx
+++ b/src/components/rpg-entry/RpgEntryHomeView.tsx
@@ -255,18 +255,25 @@ const RECOMMEND_ENTRY_DRAG_LIMIT_PX = 160;
const WECHAT_JS_SDK_URL = 'https://res.wx.qq.com/open/js/jweixin-1.6.0.js';
const WECHAT_PAY_CONFIRM_RETRY_DELAYS_MS = [800, 1600, 3000] as const;
const WECHAT_NATIVE_PAY_QR_IMAGE_SIZE = 180;
-const PROFILE_TASK_STATUS_PRIORITY_RANK: Record = {
+const PROFILE_TASK_STATUS_PRIORITY_RANK: Record<
+ ProfileTaskItem['status'],
+ number
+> = {
claimable: 2,
incomplete: 1,
disabled: 0,
claimed: -1,
};
+const PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS = 10;
const PROFILE_QR_SCAN_INTERVAL_MS = 360;
function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
return tasks
.map((task, index) => ({ task, index }))
- .filter(({ task }) => task.status === 'claimable' || task.status === 'incomplete')
+ .filter(
+ ({ task }) =>
+ task.status === 'claimable' || task.status === 'incomplete',
+ )
.sort(
(left, right) =>
PROFILE_TASK_STATUS_PRIORITY_RANK[right.task.status] -
@@ -277,6 +284,37 @@ function selectProfileTaskCenterTasks(tasks: ProfileTaskItem[]) {
.map(({ task }) => task);
}
+function selectProfileTaskCardTask(tasks: ProfileTaskItem[]) {
+ return (
+ selectProfileTaskCenterTasks(tasks)[0] ??
+ tasks.find((task) => task.status === 'claimed') ??
+ tasks.find((task) => task.status !== 'disabled') ??
+ null
+ );
+}
+
+function buildProfileTaskCardSummary(center: ProfileTaskCenterResponse | null) {
+ const task = selectProfileTaskCardTask(center?.tasks ?? []);
+ const threshold = Math.max(1, task?.threshold ?? 1);
+ const progressCount = Math.min(task?.progressCount ?? 0, threshold);
+ const rewardPoints =
+ task?.rewardPoints ?? PROFILE_TASK_CARD_FALLBACK_REWARD_POINTS;
+ const actionLabel =
+ task?.status === 'claimable'
+ ? '领å–'
+ : task?.status === 'claimed'
+ ? '已完æˆ'
+ : '去完æˆ';
+
+ return {
+ actionLabel,
+ progressCount,
+ progressPercent: Math.round((progressCount / threshold) * 100),
+ rewardPoints,
+ threshold,
+ };
+}
+
type ProfileReferralPanel = 'invite' | 'redeem' | 'community';
type ProfilePopupPanel = ProfileReferralPanel | 'saveArchives';
type BarcodeDetectorLike = {
@@ -2449,42 +2487,6 @@ function ProfileSettingsRow({
);
}
-function ProfileSecondaryShortcutButton({
- label,
- subLabel,
- icon,
- onClick,
-}: {
- label: string;
- subLabel?: string;
- icon: ComponentType<{ className?: string }>;
- onClick: () => void;
-}) {
- const Icon = icon;
-
- return (
-
- );
-}
-
function ProfileLegalSection({
onOpenDocument,
}: {
@@ -4218,12 +4220,10 @@ export function RpgEntryHomeView({
profileDashboard?.totalPlayTimeMs ?? 0,
);
const playedWorkCount = profileDashboard?.playedWorldCount ?? 0;
- const canShowReferralRedeemShortcut =
- isAuthenticated &&
- isWithinProfileInviteRedeemWindow(authUi?.user?.createdAt) &&
- isReferralCenterInitialized &&
- Boolean(referralCenter) &&
- referralCenter?.hasRedeemedCode !== true;
+ const profileTaskCardSummary = useMemo(
+ () => buildProfileTaskCardSummary(taskCenter),
+ [taskCenter],
+ );
const tabIcons: Record<
PlatformHomeTab,
ComponentType<{ className?: string }>
@@ -4776,7 +4776,7 @@ export function RpgEntryHomeView({
document.removeEventListener('visibilitychange', handleResume);
};
}, [handleWechatPayResult]);
- const loadTaskCenter = () => {
+ const loadTaskCenter = useCallback(() => {
setTaskCenterError(null);
setIsLoadingTaskCenter(true);
void getRpgProfileTasks()
@@ -4788,11 +4788,24 @@ export function RpgEntryHomeView({
);
})
.finally(() => setIsLoadingTaskCenter(false));
- };
+ }, []);
+
+ useEffect(() => {
+ if (activeTab !== 'profile' || !isAuthenticated) {
+ setTaskCenter(null);
+ setTaskCenterError(null);
+ return;
+ }
+
+ loadTaskCenter();
+ }, [activeTab, isAuthenticated, loadTaskCenter]);
+
const openTaskCenterPanel = () => {
setIsTaskCenterOpen(true);
setTaskClaimSuccess(null);
- loadTaskCenter();
+ if (!taskCenter) {
+ loadTaskCenter();
+ }
};
const openQrScannerPanel = () => {
if (!authUi?.user) {
@@ -6185,14 +6198,24 @@ export function RpgEntryHomeView({
æ¯æ—¥ä»»åŠ¡
- 完æˆä»»åŠ¡å¯é¢†å– 10 泥点
+ 完æˆä»»åŠ¡å¯é¢†å–{' '}
+
+ {profileTaskCardSummary.rewardPoints}
+ {' '}
+ 泥点
- 0 / 1
+ {profileTaskCardSummary.progressCount} /{' '}
+ {profileTaskCardSummary.threshold}
-
+
@@ -6202,7 +6225,7 @@ export function RpgEntryHomeView({
className="platform-profile-daily-task-card__mascot"
/>
- 去完æˆ
+ {profileTaskCardSummary.actionLabel}
@@ -6267,20 +6290,6 @@ export function RpgEntryHomeView({
/>
- {canShowReferralRedeemShortcut ? (
-
- openProfilePopupPanel('redeem')}
- />
-
- ) : null}
-
>
) : (