Files
mp-xieyingeng/ARCHITECTURE_DIAGRAM.md
richarjiang 69c0986996 feat: 进入关卡时 toast 提示体力消耗,修复 StorageManager 接口位置和 WxSDK 访问级别
- 进入关卡成功后显示 toast 提示消耗体力及剩余体力
- 将 StorageManager 中 UserInfo 接口移至模块顶层,修复嵌套接口语法问题
- WxSDK.getWx() 改为 static 公开方法,便于外部调用
2026-04-10 10:10:19 +08:00

26 KiB

Architecture & Flow Diagrams

System Architecture

┌─────────────────────────────────────────────────────────────────┐
│                      Cocos Creator Runtime                       │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │              UI Layer (This Analysis)                    │   │
│  ├──────────────────────────────────────────────────────────┤   │
│  │                                                          │   │
│  │  ┌────────────────┐         ┌──────────────┐            │   │
│  │  │   main.ts      │────────▶│ ViewManager  │            │   │
│  │  │  (Bootstrap)   │         │ (Singleton)  │            │   │
│  │  └────────────────┘         └──────────────┘            │   │
│  │                                   │                     │   │
│  │    Pages (All extend BaseView):   │                     │   │
│  │  ┌─────────────────────────────────┼──────────────┐    │   │
│  │  │ • PageLoading (init data)       │              │    │   │
│  │  │ • PageHome (hub)                │              │    │   │
│  │  │ • PageLevel (game - complex)   ◀┘              │    │   │
│  │  │ • PagePreviewLevels (list)                     │    │   │
│  │  │ • PassModal (modal)                            │    │   │
│  │  └────────────────────────────────────────────────┘    │   │
│  │                                                          │   │
│  │  ┌────────────────┐         ┌──────────────┐            │   │
│  │  │ ToastManager   │────────▶│   Toast      │            │   │
│  │  │ (Singleton)    │         │ (Component)  │            │   │
│  │  └────────────────┘         └──────────────┘            │   │
│  │                                                          │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
│  ┌──────────────────────────────────────────────────────────┐   │
│  │          Data Layer (External - Not Analyzed)            │   │
│  ├──────────────────────────────────────────────────────────┤   │
│  │ • AuthManager          • LevelDataManager                │   │
│  │ • StorageManager       • UserAssetsManager               │   │
│  │ • ShareManager         • WxSDK                           │   │
│  └──────────────────────────────────────────────────────────┘   │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Page State Machine

                    ┌─────────────────────────────┐
                    │    Application Starts       │
                    └──────────────┬──────────────┘
                                   │
                                   ▼
                        ┌──────────────────────┐
                        │   main.onLoad()      │
                        │ Registers all pages  │
                        └──────────┬───────────┘
                                   │
                                   ▼
                    ┌──────────────────────────────┐
                    │   PageLoading displayed      │
                    │ - Load auth + levels         │
                    │ - Sync progress              │
                    │ - Check share code           │
                    └──────────────┬───────────────┘
                                   │
                    ┌──────────────┴──────────────┐
                    │ Normal Mode  │ Share Mode   │
                    ▼              ▼              │
        ┌──────────────────┐   ┌───────────────┐ │
        │   PageHome       │   │  PageLevel    │ │
        │ [Start] [PK]     │   │  (in share)   │ │
        └────┬─────┬───────┘   └───────┬───────┘ │
             │     │                   │         │
    ┌────────▼─┐   └────────────┬──────┴────────┘
    │           │               │
    ▼           ▼               ▼
PageLevel  PageWrite        (Repeat levels)
(game)     Levels              │
  │        (create)            ▼
  │          │            PassModal
  │          ▼            [Share]
  │      PagePreview         │
  │      Levels              ▼
  │      (verify)        Back to Home
  │          │
  │          ▼
  │      PassModal
  │      [Next/Share]
  │          │
  └──────────┼─────────┐
             │         │
        ┌────▼──┐  ┌───▼────┐
        │ Next  │  │ Share  │
        │ Loop  │  │ to WX  │
        └───────┘  └────────┘

Page Stack Visualization

Session Trace

INITIALIZATION:
┌────────────────────────────────────────────┐
│ Stack: []                                  │
│ (Nothing loaded yet)                       │
└────────────────────────────────────────────┘
         │
         ▼ main.onLoad()
┌────────────────────────────────────────────┐
│ Stack: []                                  │
│ ViewManager + pages registered in registry │
└────────────────────────────────────────────┘
         │
         ▼ PageLoading.start() → ViewManager.open('PageHome')
┌────────────────────────────────────────────┐
│ Stack: [PageHome]                          │
│ PageHome._doShow() called                  │
└────────────────────────────────────────────┘

USER CLICKS "START GAME":
         ▼ PageHome._onStartGameClick() → ViewManager.open('PageLevel')
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel]               │
│ PageHome._doHide() called                  │
│ PageLevel._doShow() called                 │
└────────────────────────────────────────────┘

COMPLETES LEVEL:
         ▼ PageLevel shows PassModal (not in stack, drawn on top)
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel]               │
│ UI Layer: [PageLevel, PassModal@z999]      │
│ PassModal shown with instantiate()         │
└────────────────────────────────────────────┘

CLICKS "NEXT LEVEL":
         ▼ PassModal closed, PageLevel.nextLevel()
┌────────────────────────────────────────────┐
│ Stack: [PageHome, PageLevel]               │
│ PageLevel loads next level config          │
│ PassModal destroyed                        │
└────────────────────────────────────────────┘

CLICKS BACK FROM PageLevel:
         ▼ PageLevel.onIconSettingClick() → ViewManager.back()
┌────────────────────────────────────────────┐
│ Stack: [PageHome]                          │
│ PageLevel._doHide() called                 │
│ PageLevel destroyed (cache=true, not kept) │
│ PageHome._doShow() called                  │
└────────────────────────────────────────────┘

Component Communication Diagram

                    ┌─────────────────────┐
                    │   ViewManager       │
                    │   (Singleton)       │
                    └──────────┬──────────┘
                               │
                ┌──────────────┼──────────────┐
                │              │              │
                ▼              ▼              ▼
            open()        back()        replace()
                │              │              │
                │              │              │
    ┌───────────┴────┐     ┌───┴────┐    ┌──┴───────┐
    │                │     │        │    │          │
    ▼                ▼     ▼        ▼    ▼          ▼
PageHome    PageLevel    PassModal     (back)   (new page)
  │            │             │
  │            ├─ Timer ────┐
  │            │            │
  │            ├─ Points    │
  │            │    ↑       │
  │            │    │       │
  ▼            ▼    │       ▼
ToastMgr    Hints   │    PassModal
             │      │       │
             ├──────┤       └──▶ Share to WX
             │      │
         Unlock   Earn Points
         Consume  (UserAssets)
         (check hasPoints)

Data Flow: Level Completion

USER ENTERS ANSWER:
┌─────────────────────────────────────────────────────────┐
│ PageLevel.onSubmitAnswer()                              │
└─────────────────────────────────────────────────────────┘
         │
         ├─ getAnswer() ──▶ [Retrieve from EditBox]
         │
         ├─ Compare with _currentConfig.answer
         │
         ▼
    ┌────────────────────────────────────────┐
    │ CORRECT ANSWER                         │
    └────────────────────────────────────────┘
         │
         ├─ _isTransitioning = true  [Prevent double-submit]
         │
         ├─ stopCountdown()
         │
         ├─ playSuccessSound()
         │
         ├─ Calculate: timeSpent = 60 - _countdown
         │
         ├─┐ Normal Mode:
         │ │
         │ └─▶ UserAssetsManager.earnPoint(levelId, timeSpent)
         │        │
         │        ▼
         │    └─ UPDATE SERVER + LOCAL
         │    └─ Get points amount
         │    └─ PageLevel.updatePointsLabel()
         │
         ├─┐ Share Mode:
         │ │
         │ └─▶ ShareManager.reportLevelProgress(...)
         │        │
         │        ▼
         │    └─ SEND TO SERVER (fire-and-forget)
         │
         ├─ _showPassModal()
         │    │
         │    ├─ instantiate(passModalPrefab)
         │    │
         │    ├─ Set z-index = 999 (topmost)
         │    │
         │    ├─ Add to Canvas
         │    │
         │    └─ Call passModal.onViewLoad() + onViewShow()
         │
         └─ WAIT FOR USER:
              │
              ├─ [Next Level] ──▶ PassModal closed
              │                  │
              │                  ▼
              │             nextLevel()
              │                  │
              │         ┌────────┴────────┐
              │         │ No More Levels  │ Next Level
              │         ▼                 ▼
              │      BACK HOME        Load & Display
              │                       Restart Timer
              │
              └─ [Share] ──▶ WxSDK.shareAppMessage()
                             │
                             └─ Modal stays open

Hint Unlocking Flow

USER CLICKS UNLOCK BUTTON:
┌──────────────────────────────────────────────┐
│ PageLevel.onUnlockClue(index: 2 or 3)        │
└──────────────────────────────────────────────┘
         │
         ├─ Check: _isUnlocking == true?
         │    └─ YES: Return immediately (prevent double-click)
         │    └─ NO: Continue
         │
         ├─ Check: StorageManager.hasPoints()?
         │    └─ NO: Show toast "积分不足!"
         │    └─ YES: Continue
         │
         ├─ Set: _isUnlocking = true
         │
         ├─ await UserAssetsManager.consumePoint(levelId, index)
         │    │
         │    ├─ ASYNC: Contact server/storage
         │    │
         │    └─ Returns: success boolean
         │
         ├─ If success:
         │    │
         │    ├─ updatePointsLabel()
         │    │
         │    ├─ playClickSound()
         │    │
         │    ├─ hideUnlockButton(index)
         │    │
         │    ├─ showClue(index)
         │    │
         │    └─ setClue(index, config.clueN)
         │
         ├─ If failed:
         │    │
         │    └─ Show toast "积分不足!"
         │
         └─ Finally: _isUnlocking = false

Timer Sequence Diagram

Timeline:  1s    2s    3s  ...                59s   60s   61s
           │     │     │                     │     │     │
           ▼     ▼     ▼                     ▼     ▼     ▼
countdown: 60  59    58                     1     0    (ended)
           │     │     │                     │     │
display:  "60s" "59s" "58s" ......... "1s"  "0s"
           │     │     │                     │     │
           ▼     ▼     ▼                     ▼     ▼

LEVEL LOAD:
 │
 └─ startCountdown()
      │
      ├─ _countdown = 60
      ├─ _isTimeUp = false
      └─ schedule(onCountdownTick, 1.0) [Repeat every 1 sec]
           │
           ▼ (Every 1 second)
      onCountdownTick()
           │
           ├─ if (_isTimeUp) return
           │
           ├─ _countdown-- (60→59→58...→0)
           │
           ├─ updateClockLabel() [Display: "59s"]
           │
           └─ if (_countdown <= 0):
                │
                ├─ _isTimeUp = true
                │
                ├─ stopCountdown()
                │   └─ unschedule(onCountdownTick)
                │
                └─ onTimeUp()
                     │
                     └─ playFailSound()

ANSWER SUBMITTED (Correct):
 │
 └─ showSuccess()
      │
      ├─ stopCountdown()
      │   └─ Timer stops, no more ticks
      │
      └─ _showPassModal()

OR:

ANSWER SUBMITTED (Wrong):
 │
 └─ Timer continues running...
      │
      └─ User keeps playing

Input Box Creation & Management

LEVEL LOADS:
 │
 └─ _applyLevelConfig(config)
      │
      └─ createSingleInput(config.answer.length)
           │
           ├─ clearInputNodes()
           │   │
           │   └─ Destroy old input nodes
           │       (If level changed)
           │
           ├─ Hide inputTemplate
           │
           ├─ instantiate(inputTemplate)
           │
           ├─ Set properties:
           │   │
           │   ├─ active = true
           │   ├─ name = 'singleInput'
           │   ├─ position = (0, 0, 0)
           │   │
           │   └─ Get EditBox component
           │       │
           │       ├─ placeholder = "(Xlength个字)"
           │       ├─ maxLength = answerLength
           │       ├─ string = ""
           │       │
           │       └─ Listen events:
           │           ├─ TEXT_CHANGED → onInputTextChanged()
           │           └─ EDITING_DID_ENDED → onInputEditingEnded()
           │
           ├─ Calculate width:
           │   └─ Math.min(600, Math.max(200, length*60+40))
           │
           ├─ Set UITransform contentSize to width x 100
           │
           ├─ Adjust underline width to match
           │
           ├─ Add to inputLayout
           │
           └─ Store in _inputNodes[0]

USER ENTERS TEXT:
 │
 ├─ EditBox updates string property
 └─ TEXT_CHANGED event fires

USER SUBMITS:
 │
 ├─ getAnswer()
 │    │
 │    └─ _inputNodes[0].getComponent(EditBox).string.trim()
 │
 └─ Compare with config.answer

USER NAVIGATES AWAY:
 │
 └─ onViewDestroy() → clearInputNodes()
      │
      └─ Destroy all input nodes

Share Challenge Mode Flow

LAUNCH FROM WeChat LINK:
 │
 └─ PageLoading.start()
      │
      └─ _startPreload()
           │
           ├─ Initialize auth + levels (parallel)
           │
           ├─ WxSDK.getShareCodeFromLaunch()
           │    │
           │    └─ Extract from wx.getLaunchOptionsSync().query
           │
           ├─ if shareCode exists AND loginSuccess:
           │    │
           │    └─ ShareManager.instance.joinShare(shareCode)
           │         │
           │         ├─ ASYNC: Fetch share session from server
           │         │
           │         ├─ Get shared level set
           │         │
           │         └─ Return: success boolean
           │
           ├─ if (joinSuccess):
           │    │
           │    ├─ Set progress = 1
           │    │
           │    ├─ ViewManager.open('PageLevel', {
           │    │    params: { shareMode: true }
           │    │  })
           │    │
           │    └─ Destroy self (PageLoading)
           │
           └─ else (normal flow)
                │
                └─ Preload PageHome → Open PageHome

IN SHARE MODE (PageLevel):
 │
 ├─ onViewLoad():
 │    │
 │    ├─ params = getParams()
 │    ├─ _isShareMode = params?.shareMode === true
 │    │
 │    ├─ if shareMode:
 │    │    └─ currentLevelIndex = 0 (start at first)
 │    │
 │    └─ initLevel() loads from ShareManager, not LevelDataManager
 │
 ├─ showSuccess():
 │    │
 │    ├─ if normal mode:
 │    │    └─ earn points
 │    │
 │    └─ if share mode:
 │         │
 │         └─ ShareManager.reportLevelProgress(levelId, true, timeSpent)
 │              │
 │              └─ FIRE-AND-FORGET (no await)
 │
 ├─ nextLevel():
 │    │
 │    ├─ if NOT shareMode:
 │    │    └─ StorageManager.onLevelCompleted() [Save progress]
 │    │
 │    ├─ Check level count:
 │    │    │
 │    │    ├─ Share: ShareManager.getShareLevelCount()
 │    │    └─ Normal: LevelDataManager.getLevelCount()
 │    │
 │    ├─ if (allLevelsComplete):
 │    │    │
 │    │    ├─ if shareMode:
 │    │    │    │
 │    │    │    ├─ ShareManager.clearShareMode()
 │    │    │    │
 │    │    │    └─ ViewManager.replace('PageHome')
 │    │    │
 │    │    └─ if normal:
 │    │         └─ ViewManager.back()
 │    │
 │    └─ else (more levels):
 │         └─ Load next level
 │
 └─ Back to PageHome (replaces, clears share state)

ViewManager Caching Mechanism

OPEN 'PageHome' (first time):
 │
 └─ ViewManager.open('PageHome')
      │
      ├─ Check cache: _viewCache.get('PageHome')
      │    └─ Cache miss (undefined)
      │
      ├─ _instantiateView('PageHome', prefab)
      │    │
      │    ├─ instantiate(prefab)
      │    │
      │    ├─ Get BaseView component
      │    │
      │    ├─ Set viewId, config, params
      │    │
      │    ├─ Add to container
      │    │
      │    ├─ Call view.onViewLoad() ◄─── Called once
      │    │
      │    ├─ if (config.cache === true):  ◄─── PageHome has cache:true
      │    │    └─ _viewCache.set('PageHome', view)
      │    │
      │    └─ _showView(view)
      │         │
      │         └─ view._doShow()
      │              └─ view.onViewShow() ◄─── Called now
      │
      └─ Stack: [PageHome]

OPEN 'PageLevel':
 │
 └─ ViewManager.open('PageLevel')
      │
      ├─ PageHome._doHide()
      │    └─ view.onViewHide() ◄─── First hide
      │
      └─ (Same as above for PageLevel)
         │
         └─ Stack: [PageHome, PageLevel]

BACK FROM 'PageLevel':
 │
 └─ ViewManager.back()
      │
      ├─ Pop stack: PageLevel
      │
      ├─ PageLevel._doHide()
      │    └─ view.onViewHide()
      │
      ├─ Decide: shouldDestroy?
      │    │
      │    └─ PageLevel has cache:true
      │        └─ shouldDestroy = false (keep cached)
      │
      ├─ Get current: PageHome
      │
      ├─ PageHome._doShow()
      │    └─ view.onViewShow() ◄─── Called again
      │
      └─ Stack: [PageHome]
         Caches: {PageHome, PageLevel}

OPEN 'PageLevel' AGAIN (from cache):
 │
 └─ ViewManager.open('PageLevel')
      │
      ├─ Check cache: _viewCache.get('PageLevel')
      │    └─ Cache hit! (PageLevel instance)
      │
      ├─ _showView(cachedView)
      │    │
      │    ├─ PageHome._doHide()
      │    │
      │    └─ cachedView._doShow()
      │         └─ view.onViewShow() ◄─── Called again (2nd time)
      │            onViewLoad() NOT called again ◄─── Important!
      │
      └─ Stack: [PageHome, PageLevel]
         Same instance reused

Error Handling Paths

LEVEL LOADING ERROR:
 │
 ├─ initLevel() 
 │    │
 │    └─ await LevelDataManager.ensureLevelReady(index)
 │         │
 │         └─ Returns null (error)
 │
 └─ Log warning, return early
      │
      └─ UI stays as-is (might be blank)

INSUFFICIENT POINTS:
 │
 └─ onUnlockClue()
      │
      ├─ StorageManager.hasPoints() → false
      │
      └─ ToastManager.show("积分不足!")
          │
          └─ User sees notification, unlock blocked

DOUBLE-SUBMIT PREVENTION:
 │
 ├─ onSubmitAnswer()
 │    │
 │    └─ if (_isTransitioning) return ◄─── Blocks multiple calls
 │
 └─ showSuccess()
      │
      └─ _isTransitioning = true ◄─── Set to block
           │
           └─ After modal shown/closed
                └─ (Would be reset on nextLevel)

DOUBLE-CLICK UNLOCK:
 │
 └─ onUnlockClue()
      │
      └─ if (_isUnlocking) return ◄─── Blocks rapid clicks
           │
           └─ _isUnlocking = true at start
                └─ Finally: _isUnlocking = false ◄─── Reset after async