- 新增 useGameBoardVm 数据层实施说明文档 - 添加 36字花核心玩法与前端规则摘要 - 创建游戏模块数据与界面分层第一阶段实施稿 - 定义四层架构:api/dto、store、view-model hooks、ui层 - 规范 PC 与 Mobile 共享业务逻辑的改造方案 - 明确各层职责边界和组件改造顺序
23 KiB
36字花游戏模块数据与界面分层第一阶段实施稿
1. 目标
第一阶段的目标不是做一次彻底重构,而是先把当前项目中的“业务数据”和“界面渲染”拆出清晰边界,让同一套数据和交互逻辑可以同时服务 PC 界面与 Mobile 界面。
本阶段只解决以下问题:
- 一套业务数据,供
PC与Mobile共同消费 - 统一业务交互逻辑,避免双端各自维护一份
- 保持
PC与Mobile布局、样式、视觉独立 - 不大范围推翻现有组件,优先在现有代码上增量改造
本阶段暂不追求:
- 所有组件完全抽象成跨端共享组件
- 一次性去掉所有本地
useState - 一次性重写所有弹窗和表单
- 大规模目录迁移导致整仓成本过高
2. 当前代码现状
当前仓库中已经存在一部分良好的基础设施:
- 数据请求与 DTO 归一化:
- 共享领域类型与派生逻辑:
- 回合与会话状态:
- 页面入口已统一在:
当前主要问题有 4 个:
-
Mobile界面直接复用了Desktop组件
例如 src/features/game/entry/mobile-entry.tsx 当前直接引用了DesktopAnimal、DesktopGameHistory、DesktopTitle。这会导致移动端无法形成独立布局层。 -
业务状态和界面状态混杂
一些状态是业务性的,例如当前选中筹码、是否允许下注;另一些只是表现性的,例如按钮点击动画、过渡效果。当前这两种状态没有被清晰拆开。 -
组件层直接拼 store 数据或自己维护业务数据
例如 src/features/game/components/desktop/desktop-control.tsx 内部自己维护了部分核心控制状态,导致未来Mobile难以复用。 -
弹窗与 UI 业务状态分散在各组件本地
useState
比如auto setting、notice、user info、procedures等弹窗,当前都在各自文件内部自管open或 tab 状态,不利于双端共享和统一调度。
3. 第一阶段总体原则
第一阶段采用四层结构:
api / dto / normalizestore / domain actionsview-model hookspc ui与mobile ui
每层职责如下:
3.1 API 层
职责:
- 调用后端接口
- DTO 转前端领域模型
- 不参与 UI 展示逻辑
保留现状:
3.2 Store 层
职责:
- 保存当前页面共享的真实业务状态
- 提供业务动作
- 不直接处理视觉布局
3.3 View-model hooks 层
职责:
- 从 store 中读取数据
- 聚合派生字段
- 封装业务事件
- 返回给
PC与Mobile可直接渲染的数据结构
这层是本次改造的核心。
3.4 UI 层
职责:
- 只负责布局、视觉、交互表现
PC与Mobile各自独立- 不直接拼底层 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/desktop与entry/目录 views/pc与views/mobile可以先作为新目录开始承接之后的容器入口components/shared第一阶段只抽少量共用展示结构,不强行全部收拢
5. 第一阶段 Store 设计
5.1 game-round-store
文件:
该 store 保留为“回合与下注核心业务状态”。
现有字段
cellschipshistoryroundselectionstrendsactiveChipId
现有动作
hydrateRoundselectChipplaceBetremoveSelectionsForCellclearSelectionssetPhasesyncRoundupsertSelections
第一阶段建议新增动作
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 保留为“会话、连接、公告、面板摘要信息”。
现有字段
announcementsconnectiondashboard
现有动作
hydrateSessiondismissAnnouncementmarkAnnouncementReadsetConnectionLatencysetConnectionStatussyncConnectionsyncDashboard
第一阶段建议新增字段
serverTimeIso: string | nulllastBootstrapAt: string | null
第一阶段建议新增动作
setServerTime(serverTimeIso: string)setLastBootstrapAt(iso: string)
说明:
- src/features/game/components/desktop/desktop-header.tsx 当前展示系统时间,后续建议统一从这里拿
lastBootstrapAt方便后续做重连、刷新、数据同步判断
5.3 新增 game-ui-store
新文件建议:
src/store/game/game-ui-store.ts
该 store 专门管理“共享的 UI 业务状态”。
建议字段
activeModal: GameModalType | nullmodalPayload: Record<string, unknown> | nullselectedUserInfoTab: 'profile' | 'message'isBgmEnabled: booleanisMuted: booleanautoSpinEnabled: booleanautoSpinSettingsstopIfBalanceLowerThan: stringstopIfSingleWinExceeds: stringstopOnAnyJackpot: boolean
authFormloginAccount: stringloginPassword: stringregisterAccount: stringregisterPassword: stringregisterConfirmPassword: string
proceduresType: 'withdraw' | 'topup' | null
建议动作
openModal(type, payload?)closeModal()setSelectedUserInfoTab(tab)toggleBgm()setAutoSpinEnabled(enabled)setAutoSpinSetting(key, value)setAuthField(field, value)setProceduresType(type)resetAuthForm()resetAutoSpinSettings()
第一阶段要接管的现有状态来源
- src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
- src/features/game/modal/desktop/desktop-notice-modal.tsx
- src/features/game/modal/desktop/desktop-userInfo-modal.tsx
- src/features/game/modal/desktop/desktop-procedures-modal.tsx
- src/features/game/modal/desktop/desktop-login-modal.tsx
- src/features/game/modal/desktop/desktop-register-modal.tsx
- src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx
不要放到 game-ui-store 的状态
confirmClickedclickedIdhidingId
这些状态目前位于 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
服务组件:
- src/features/game/components/desktop/desktop-header.tsx
- 未来的
mobile-header.tsx
建议暴露字段
latencyMs: number | nulllatencyLabel: stringconnectionStatus: 'connected' | 'reconnecting' | 'offline'systemTimeLabel: stringusername: stringbalanceLabel: stringavatarUrl: string | nullunreadMessageCount: numberisBgmEnabled: boolean
建议暴露动作
onOpenRules()onOpenMessages()onToggleBgm()onOpenProfile()
说明
DesktopHeader未来不再直接读取 store 或写死文案来源- header 只消费“最终展示值”与“用户点击后的动作”
6.2 useGameStatusVm
建议文件:
src/features/game/hooks/use-game-status-vm.ts
服务组件:
建议暴露字段
roundId: stringoddsLabel: stringstreakLabel: stringlimitLabel: stringcountdownSeconds: numbercountdownMs: numberphase: RoundPhasephaseLabel: stringphaseTone: 'open' | 'locked' | 'settled'acceptingBets: boolean
说明
- 倒计时展示逻辑、回合阶段文案逻辑统一在这里处理
PC和Mobile不各自重复拼 countdown 与 round phase
6.3 useGameBoardVm
建议文件:
src/features/game/hooks/use-game-board-vm.ts
服务组件:
- src/features/game/components/desktop/desktop-animal.tsx
- 未来
mobile-board.tsx
建议暴露字段
cellsidimageUrlstatusisSelectedisWinningCellselectionAmountselectionCounthitCountcurrentStreak
activeCellId: number | nullcanPlaceBets: booleantotalSelectedCount: numbertotalSelectedAmount: number
建议暴露动作
onCellPress(cellId: number)onCellLongPress?(cellId: number)onCellClear(cellId: number)
说明
- 优先基于 src/features/game/shared/selectors.ts 中的:
buildGameCellViewModelsgroupSelectionsByCellgetSelectionTotal
DesktopAnimal不应该自己管理业务选择逻辑,只负责渲染 cell grid
6.4 useGameHistoryVm
建议文件:
src/features/game/hooks/use-game-history-vm.ts
服务组件:
- src/features/game/components/desktop/desktop-game-history.tsx
- 未来
mobile-history.tsx
建议暴露字段
itemsorderNoroundIdwinningCellIdbetAmountLabeltotalAmountLabelwinAmountLabelstatusLabelcreatedAtLabel
isEmpty: booleanemptyText: stringrecentWinningCellIds: number[]
说明
- 当前 src/features/game/components/desktop/desktop-game-history.tsx 里还是本地 fake data,第一阶段应切换为 VM 数据
PC与Mobile历史列表的数据格式统一,只是布局不同
6.5 useGameControlVm
建议文件:
src/features/game/hooks/use-game-control-vm.ts
服务组件:
- src/features/game/components/desktop/desktop-control.tsx
- 未来
mobile-control.tsx
建议暴露字段
chipsidvalueamountcolorisSelectedisDefault
selectedChipId: string | nullselectedChipAmountLabel: stringselectionTotalLabel: stringselectedCountLabel: stringactionButtonsidlabeldisabled
canConfirm: booleancanClear: booleancanRepeat: booleancanOpenAutoSpin: boolean
建议暴露动作
onChipSelect(chipId: string)onAddChip()onReduceChip()onClearSelections()onRepeatLastRound()onOpenAutoSpin()onConfirmBet()
说明
- src/features/game/components/desktop/desktop-control.tsx 内部仍可保留点击动画和短暂视觉反馈
- 但“当前选中筹码、是否可确认、操作是否禁用”等必须来自 VM,而不是继续散在组件内部
6.6 useGameAnnouncementVm
建议文件:
src/features/game/hooks/use-game-announcement-vm.ts
服务组件:
- src/features/game/components/shared/game-announcement-modal.tsx
- header 消息入口
- notice modal
建议暴露字段
activeAnnouncement: AnnouncementItem | nullvisibleAnnouncements: AnnouncementItem[]unreadCount: numberhasUnread: booleanisOpen: boolean
建议暴露动作
onOpenAnnouncement(id?: string)onDismissAnnouncement(id: string)onMarkRead(id: string)onCloseAnnouncement()
说明
- 这样公告逻辑不会散落在 src/features/game/entry/entry-page.tsx 和多个具体 modal 中
6.7 useGameAutoSpinVm
建议文件:
src/features/game/hooks/use-game-auto-spin-vm.ts
服务组件:
- src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
- 未来
mobile-auto-setting-modal.tsx
建议暴露字段
open: booleanenabled: booleanstopIfBalanceLowerThan: stringstopIfSingleWinExceeds: stringstopOnAnyJackpot: booleancanSubmit: 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.tsxsrc/features/game/components/shared/game-history-list.tsxsrc/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
例如:
PcEntryMobileEntry- 各 section 容器
- modal host
这些组件可以:
useGameBoardVm()useGameControlVm()useGameStatusVm()
8.3 纯展示组件不直接碰 store
比如:
- src/features/game/components/desktop/desktop-animal.tsx
- src/features/game/components/desktop/desktop-game-history.tsx
- src/features/game/components/desktop/desktop-status.tsx
- src/features/game/components/desktop/desktop-header.tsx
未来都应该尽量演进为:
- 只吃 props
- 不主动拼底层 store
9. 第一阶段实施顺序
建议严格按这个顺序执行,避免改动面过大。
Step 1:新增 UI Store 与 Hooks 目录
新增:
src/store/game/game-ui-store.tssrc/features/game/hooks/
这一步只建壳子,不急着接业务。
Step 2:实现 useGameBoardVm 与 useGameControlVm
这两块是主玩法交互核心,优先收益最大。
优先接的现有文件:
- src/features/game/components/desktop/desktop-animal.tsx
- src/features/game/components/desktop/desktop-control.tsx
Step 3:让 DesktopAnimal 只吃 props
目标:
- 不直接读 store
- 不自己处理业务选择逻辑
保留字段形态:
cellsactiveIdonSelect
Step 4:让 DesktopControl 业务数据来自 VM
目标:
- 当前筹码列表、已选筹码、总下注额等来自
useGameControlVm - 点击动画状态仍然保留本地
Step 5:停止 Mobile 直接复用 Desktop 组件
文件:
当前问题:
- 直接 import
DesktopAnimal - 直接 import
DesktopGameHistory - 直接 import
DesktopTitle
第一阶段目标:
- 改成
MobileBoardSection - 改成
MobileHistorySection - 共享 hook,不共享
Desktop布局组件
Step 6:实现 useGameStatusVm 与 useGameHeaderVm
接入文件:
- src/features/game/components/desktop/desktop-status.tsx
- src/features/game/components/desktop/desktop-header.tsx
Step 7:实现 game-ui-store + useGameAutoSpinVm
先接一个最合适的 modal:
跑通后,再接:
- notice
- user info
- procedures
- login/register
10. 第一阶段优先改造文件清单
第一阶段最值得优先改的 6 个文件如下:
- src/features/game/entry/entry-page.tsx
- src/features/game/entry/mobile-entry.tsx
- src/features/game/components/desktop/desktop-animal.tsx
- src/features/game/components/desktop/desktop-control.tsx
- src/features/game/components/desktop/desktop-status.tsx
- src/features/game/modal/desktop/desktop-auto-setting-modal.tsx
原因:
- 它们覆盖了主玩法、主状态、移动端入口和共享 modal 模式
- 先把这 6 个改通,数据与界面分层的骨架就建立起来了
11. 第一阶段完成后的验收标准
做到以下几点,即可视为第一阶段完成:
PC与Mobile都不直接拼 API / store 的底层结构Mobile不再直接 importDesktopXxx- 至少
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 接入