Files
36-character-flower/docs/36字花-游戏模块数据与界面分层第一阶段实施稿.md
JiaJun 6aaf90a6ac docs(game): 添加游戏模块数据
- 新增 useGameBoardVm 数据层实施说明文档
- 添加 36字花核心玩法与前端规则摘要
- 创建游戏模块数据与界面分层第一阶段实施稿
- 定义四层架构:api/dto、store、view-model hooks、ui层
- 规范 PC 与 Mobile 共享业务逻辑的改造方案
- 明确各层职责边界和组件改造顺序
2026-05-09 17:52:30 +08:00

23 KiB
Raw Blame History

36字花游戏模块数据与界面分层第一阶段实施稿

1. 目标

第一阶段的目标不是做一次彻底重构,而是先把当前项目中的“业务数据”和“界面渲染”拆出清晰边界,让同一套数据和交互逻辑可以同时服务 PC 界面与 Mobile 界面。

本阶段只解决以下问题:

  • 一套业务数据,供 PCMobile 共同消费
  • 统一业务交互逻辑,避免双端各自维护一份
  • 保持 PCMobile 布局、样式、视觉独立
  • 不大范围推翻现有组件,优先在现有代码上增量改造

本阶段暂不追求:

  • 所有组件完全抽象成跨端共享组件
  • 一次性去掉所有本地 useState
  • 一次性重写所有弹窗和表单
  • 大规模目录迁移导致整仓成本过高

2. 当前代码现状

当前仓库中已经存在一部分良好的基础设施:

当前主要问题有 4 个:

  1. Mobile 界面直接复用了 Desktop 组件
    例如 src/features/game/entry/mobile-entry.tsx 当前直接引用了 DesktopAnimalDesktopGameHistoryDesktopTitle。这会导致移动端无法形成独立布局层。

  2. 业务状态和界面状态混杂
    一些状态是业务性的,例如当前选中筹码、是否允许下注;另一些只是表现性的,例如按钮点击动画、过渡效果。当前这两种状态没有被清晰拆开。

  3. 组件层直接拼 store 数据或自己维护业务数据
    例如 src/features/game/components/desktop/desktop-control.tsx 内部自己维护了部分核心控制状态,导致未来 Mobile 难以复用。

  4. 弹窗与 UI 业务状态分散在各组件本地 useState
    比如 auto settingnoticeuser infoprocedures 等弹窗,当前都在各自文件内部自管 open 或 tab 状态,不利于双端共享和统一调度。


3. 第一阶段总体原则

第一阶段采用四层结构:

  1. api / dto / normalize
  2. store / domain actions
  3. view-model hooks
  4. pc uimobile ui

每层职责如下:

3.1 API 层

职责:

  • 调用后端接口
  • DTO 转前端领域模型
  • 不参与 UI 展示逻辑

保留现状:

3.2 Store 层

职责:

  • 保存当前页面共享的真实业务状态
  • 提供业务动作
  • 不直接处理视觉布局

3.3 View-model hooks 层

职责:

  • 从 store 中读取数据
  • 聚合派生字段
  • 封装业务事件
  • 返回给 PCMobile 可直接渲染的数据结构

这层是本次改造的核心。

3.4 UI 层

职责:

  • 只负责布局、视觉、交互表现
  • PCMobile 各自独立
  • 不直接拼底层 store

4. 第一阶段目录改造清单

建议目录结构如下。第一阶段以“新增”为主,不要求立刻大规模迁移原文件。

src/features/game/
  api/
    game-api.ts
    index.ts
    types.ts

  shared/
    constants.ts
    mock-data.ts
    selectors.ts
    types.ts

  hooks/
    use-game-header-vm.ts
    use-game-status-vm.ts
    use-game-board-vm.ts
    use-game-history-vm.ts
    use-game-control-vm.ts
    use-game-announcement-vm.ts
    use-game-auto-spin-vm.ts
    index.ts

  views/
    pc/
      pc-entry.tsx
    mobile/
      mobile-entry.tsx

  components/
    shared/
      game-board.tsx
      game-history-list.tsx
      game-status-bar.tsx
      game-control-panel.tsx
    desktop/
    mobile/

  modal/
    desktop/
    mobile/

Store 目录建议补齐成:

src/store/game/
  game-round-store.ts
  game-session-store.ts
  game-ui-store.ts
  index.ts

说明:

  • 第一阶段可以继续保留现有 components/desktopentry/ 目录
  • views/pcviews/mobile 可以先作为新目录开始承接之后的容器入口
  • components/shared 第一阶段只抽少量共用展示结构,不强行全部收拢

5. 第一阶段 Store 设计

5.1 game-round-store

文件:

该 store 保留为“回合与下注核心业务状态”。

现有字段

  • cells
  • chips
  • history
  • round
  • selections
  • trends
  • activeChipId

现有动作

  • hydrateRound
  • selectChip
  • placeBet
  • removeSelectionsForCell
  • clearSelections
  • setPhase
  • syncRound
  • upsertSelections

第一阶段建议新增动作

  • setActiveChip(chipId: string)
  • replaceHistory(history: HistoryEntry[])
  • replaceTrends(trends: TrendEntry[])
  • replaceCells(cells: GameCell[])

说明:

  • 如果不想增加重复动作,也可以只统一命名 selectChip
  • 新增 replaceHistory / replaceTrends / replaceCells 是为了后续轮询、增量同步更清晰

不应该放在这里的内容

  • 各类 modal 的打开关闭
  • 用户信息 tab
  • auto-spin 面板设置
  • 登录注册表单
  • 按钮 hover / 点击动画态

5.2 game-session-store

文件:

该 store 保留为“会话、连接、公告、面板摘要信息”。

现有字段

  • announcements
  • connection
  • dashboard

现有动作

  • hydrateSession
  • dismissAnnouncement
  • markAnnouncementRead
  • setConnectionLatency
  • setConnectionStatus
  • syncConnection
  • syncDashboard

第一阶段建议新增字段

  • serverTimeIso: string | null
  • lastBootstrapAt: string | null

第一阶段建议新增动作

  • setServerTime(serverTimeIso: string)
  • setLastBootstrapAt(iso: string)

说明:


5.3 新增 game-ui-store

新文件建议:

  • src/store/game/game-ui-store.ts

该 store 专门管理“共享的 UI 业务状态”。

建议字段

  • activeModal: GameModalType | null
  • modalPayload: Record<string, unknown> | null
  • selectedUserInfoTab: 'profile' | 'message'
  • isBgmEnabled: boolean
  • isMuted: boolean
  • autoSpinEnabled: boolean
  • autoSpinSettings
    • stopIfBalanceLowerThan: string
    • stopIfSingleWinExceeds: string
    • stopOnAnyJackpot: boolean
  • authForm
    • loginAccount: string
    • loginPassword: string
    • registerAccount: string
    • registerPassword: string
    • registerConfirmPassword: string
  • proceduresType: 'withdraw' | 'topup' | null

建议动作

  • openModal(type, payload?)
  • closeModal()
  • setSelectedUserInfoTab(tab)
  • toggleBgm()
  • setAutoSpinEnabled(enabled)
  • setAutoSpinSetting(key, value)
  • setAuthField(field, value)
  • setProceduresType(type)
  • resetAuthForm()
  • resetAutoSpinSettings()

第一阶段要接管的现有状态来源

不要放到 game-ui-store 的状态

  • confirmClicked
  • clickedId
  • hidingId

这些状态目前位于 src/features/game/components/desktop/desktop-control.tsx,属于纯动画与表现状态,继续留在本地组件即可。


6. 第一阶段 Hook 设计

本阶段重点不是“先抽 shared 组件”,而是先抽统一的 view-model hooks

这些 hook 的目标:

  • 统一封装 store 读取
  • 统一组织派生字段
  • 统一输出 PC/Mobile 都能消费的 props

6.1 useGameHeaderVm

建议文件:

  • src/features/game/hooks/use-game-header-vm.ts

服务组件:

建议暴露字段

  • latencyMs: number | null
  • latencyLabel: string
  • connectionStatus: 'connected' | 'reconnecting' | 'offline'
  • systemTimeLabel: string
  • username: string
  • balanceLabel: string
  • avatarUrl: string | null
  • unreadMessageCount: number
  • isBgmEnabled: boolean

建议暴露动作

  • onOpenRules()
  • onOpenMessages()
  • onToggleBgm()
  • onOpenProfile()

说明

  • DesktopHeader 未来不再直接读取 store 或写死文案来源
  • header 只消费“最终展示值”与“用户点击后的动作”

6.2 useGameStatusVm

建议文件:

  • src/features/game/hooks/use-game-status-vm.ts

服务组件:

建议暴露字段

  • roundId: string
  • oddsLabel: string
  • streakLabel: string
  • limitLabel: string
  • countdownSeconds: number
  • countdownMs: number
  • phase: RoundPhase
  • phaseLabel: string
  • phaseTone: 'open' | 'locked' | 'settled'
  • acceptingBets: boolean

说明

  • 倒计时展示逻辑、回合阶段文案逻辑统一在这里处理
  • PCMobile 不各自重复拼 countdown 与 round phase

6.3 useGameBoardVm

建议文件:

  • src/features/game/hooks/use-game-board-vm.ts

服务组件:

建议暴露字段

  • cells
    • id
    • imageUrl
    • status
    • isSelected
    • isWinningCell
    • selectionAmount
    • selectionCount
    • hitCount
    • currentStreak
  • activeCellId: number | null
  • canPlaceBets: boolean
  • totalSelectedCount: number
  • totalSelectedAmount: number

建议暴露动作

  • onCellPress(cellId: number)
  • onCellLongPress?(cellId: number)
  • onCellClear(cellId: number)

说明

  • 优先基于 src/features/game/shared/selectors.ts 中的:
    • buildGameCellViewModels
    • groupSelectionsByCell
    • getSelectionTotal
  • DesktopAnimal 不应该自己管理业务选择逻辑,只负责渲染 cell grid

6.4 useGameHistoryVm

建议文件:

  • src/features/game/hooks/use-game-history-vm.ts

服务组件:

建议暴露字段

  • items
    • orderNo
    • roundId
    • winningCellId
    • betAmountLabel
    • totalAmountLabel
    • winAmountLabel
    • statusLabel
    • createdAtLabel
  • isEmpty: boolean
  • emptyText: string
  • recentWinningCellIds: number[]

说明


6.5 useGameControlVm

建议文件:

  • src/features/game/hooks/use-game-control-vm.ts

服务组件:

建议暴露字段

  • chips
    • id
    • value
    • amount
    • color
    • isSelected
    • isDefault
  • selectedChipId: string | null
  • selectedChipAmountLabel: string
  • selectionTotalLabel: string
  • selectedCountLabel: string
  • actionButtons
    • id
    • label
    • disabled
  • canConfirm: boolean
  • canClear: boolean
  • canRepeat: boolean
  • canOpenAutoSpin: boolean

建议暴露动作

  • onChipSelect(chipId: string)
  • onAddChip()
  • onReduceChip()
  • onClearSelections()
  • onRepeatLastRound()
  • onOpenAutoSpin()
  • onConfirmBet()

说明


6.6 useGameAnnouncementVm

建议文件:

  • src/features/game/hooks/use-game-announcement-vm.ts

服务组件:

建议暴露字段

  • activeAnnouncement: AnnouncementItem | null
  • visibleAnnouncements: AnnouncementItem[]
  • unreadCount: number
  • hasUnread: boolean
  • isOpen: boolean

建议暴露动作

  • onOpenAnnouncement(id?: string)
  • onDismissAnnouncement(id: string)
  • onMarkRead(id: string)
  • onCloseAnnouncement()

说明


6.7 useGameAutoSpinVm

建议文件:

  • src/features/game/hooks/use-game-auto-spin-vm.ts

服务组件:

建议暴露字段

  • open: boolean
  • enabled: boolean
  • stopIfBalanceLowerThan: string
  • stopIfSingleWinExceeds: string
  • stopOnAnyJackpot: boolean
  • canSubmit: boolean

建议暴露动作

  • onOpen()
  • onClose()
  • onToggleEnabled(enabled: boolean)
  • onChangeBalanceThreshold(value: string)
  • onChangeSingleWinThreshold(value: string)
  • onToggleStopOnJackpot(enabled: boolean)
  • onSubmit()

说明

  • 这是最适合第一阶段打样的一类场景数据结构固定、PC/Mobile 表现不同、交互规则相同

7. 第一阶段共享展示组件建议

本阶段不要一口气把所有 Desktop 组件变成跨端共享。

建议只优先抽 2 到 3 个 shared 展示组件:

  • src/features/game/components/shared/game-board.tsx
  • src/features/game/components/shared/game-history-list.tsx
  • src/features/game/components/shared/game-status-bar.tsx

7.1 game-board

职责:

  • 只接收 cells
  • 只接收 onCellPress
  • 不直接读 store

这样:

  • PC 可以包一层桌面 grid 样式
  • Mobile 可以包一层移动端布局样式

7.2 game-history-list

职责:

  • 只负责历史列表渲染
  • 不关心历史数据怎么来

7.3 game-status-bar

职责:

  • 渲染赔率、回合、倒计时、当前状态
  • useGameStatusVm 驱动

不建议第一阶段就抽 shared 的内容:

  • header
  • control panel
  • 大多数 modal

因为这些区域 PC/Mobile 差异通常更大,先共用 hook 比先共用视觉组件更稳。


8. 第一阶段 UI 层改造边界

本阶段只定 3 条规则:

8.1 EntryPage 可以直接处理 hydration

文件:

保留它作为:

  • bootstrap 获取入口
  • store hydrate 入口
  • PC/Mobile 分流入口

8.2 容器组件可以调用 VM hooks

例如:

  • PcEntry
  • MobileEntry
  • 各 section 容器
  • modal host

这些组件可以:

  • useGameBoardVm()
  • useGameControlVm()
  • useGameStatusVm()

8.3 纯展示组件不直接碰 store

比如:

未来都应该尽量演进为:

  • 只吃 props
  • 不主动拼底层 store

9. 第一阶段实施顺序

建议严格按这个顺序执行,避免改动面过大。

Step 1新增 UI Store 与 Hooks 目录

新增:

  • src/store/game/game-ui-store.ts
  • src/features/game/hooks/

这一步只建壳子,不急着接业务。

Step 2实现 useGameBoardVmuseGameControlVm

这两块是主玩法交互核心,优先收益最大。

优先接的现有文件:

Step 3DesktopAnimal 只吃 props

目标:

  • 不直接读 store
  • 不自己处理业务选择逻辑

保留字段形态:

  • cells
  • activeId
  • onSelect

Step 4DesktopControl 业务数据来自 VM

目标:

  • 当前筹码列表、已选筹码、总下注额等来自 useGameControlVm
  • 点击动画状态仍然保留本地

Step 5停止 Mobile 直接复用 Desktop 组件

文件:

当前问题:

  • 直接 import DesktopAnimal
  • 直接 import DesktopGameHistory
  • 直接 import DesktopTitle

第一阶段目标:

  • 改成 MobileBoardSection
  • 改成 MobileHistorySection
  • 共享 hook不共享 Desktop 布局组件

Step 6实现 useGameStatusVmuseGameHeaderVm

接入文件:

Step 7实现 game-ui-store + useGameAutoSpinVm

先接一个最合适的 modal

跑通后,再接:

  • notice
  • user info
  • procedures
  • login/register

10. 第一阶段优先改造文件清单

第一阶段最值得优先改的 6 个文件如下:

  1. src/features/game/entry/entry-page.tsx
  2. src/features/game/entry/mobile-entry.tsx
  3. src/features/game/components/desktop/desktop-animal.tsx
  4. src/features/game/components/desktop/desktop-control.tsx
  5. src/features/game/components/desktop/desktop-status.tsx
  6. src/features/game/modal/desktop/desktop-auto-setting-modal.tsx

原因:

  • 它们覆盖了主玩法、主状态、移动端入口和共享 modal 模式
  • 先把这 6 个改通,数据与界面分层的骨架就建立起来了

11. 第一阶段完成后的验收标准

做到以下几点,即可视为第一阶段完成:

  • PCMobile 都不直接拼 API / store 的底层结构
  • Mobile 不再直接 import DesktopXxx
  • 至少 board / control / status 这 3 类区域中的 2 类已经改为 “VM 驱动 + 展示组件消费 props”
  • modal 开关与 tab 这类共享 UI 业务状态已进入 game-ui-store
  • 视觉动画状态与业务状态分层清晰

12. 风险与边界说明

12.1 不要在第一阶段做的事情

  • 不要一次性把所有桌面组件都改成共享组件
  • 不要同时把全部 modal 状态都收进 store
  • 不要同时重做 API 调用方式
  • 不要试图在第一阶段解决所有命名和目录历史包袱

12.2 第一阶段最容易犯的错误

  • 把所有状态都塞进 store
    结果动画、hover、点击反馈这些纯表现状态也被全局化反而变复杂

  • 先抽 shared UI而不是先抽 VM
    这样容易做出一堆仍然耦合 store 的“伪共享组件”

  • Mobile 继续复用 Desktop 组件
    这会让后面任何布局调整都越来越痛苦


13. 结论

本项目第一阶段的正确方向不是:

  • “PC 一套状态Mobile 一套状态”

而是:

  • 一套 domain state
  • 一套 domain actions
  • 一套 view-model hooks
  • 两套 presentation

按本实施稿推进后,后续才适合继续做:

  • 第二阶段shared UI 组件整理
  • 第三阶段modal host 统一
  • 第四阶段:实时数据同步、轮询或 websocket 接入