docs(game): 添加游戏模块数据

- 新增 useGameBoardVm 数据层实施说明文档
- 添加 36字花核心玩法与前端规则摘要
- 创建游戏模块数据与界面分层第一阶段实施稿
- 定义四层架构:api/dto、store、view-model hooks、ui层
- 规范 PC 与 Mobile 共享业务逻辑的改造方案
- 明确各层职责边界和组件改造顺序
This commit is contained in:
JiaJun
2026-05-09 17:52:30 +08:00
parent 7622d4121f
commit 6aaf90a6ac
28 changed files with 2635 additions and 258 deletions

View File

@@ -0,0 +1,248 @@
# 36字花 useGameBoardVm 数据层实施说明
## 1. 目标
本说明只服务下一步开发:实现 `useGameBoardVm.ts`
当前目标很单一:
- 让桌面选号盘从 store 读取真实业务数据
- 让点击格子时真正触发下注动作
- 不在 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 内写业务逻辑
- 不同时改造 mobile
本阶段不做:
- 不改造 mobile
- 不新增 `game-ui-store`
- 不重写 `DesktopAnimal` 整个展示结构
- 不处理 auto-spin / modal
---
## 2. 相关文件
本次只围绕以下文件展开:
- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
- [src/features/game/entry/pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx)
- [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts)
- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts)
将新增:
- `src/features/game/hooks/use-game-board-vm.ts`
---
## 3. 当前问题
当前 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) 里虽然已经挂载了 `DesktopAnimal`,但还没有把桌面主玩法接到业务链路。
当前状态:
- `DesktopAnimal` 只是展示组件
- `DesktopAnimal` 支持 `activeId``onSelect`
- `PcEntry` 直接渲染 `<DesktopAnimal />`
- 点击格子不会写入真实下注状态
结果是:
- 控制栏虽然已经接入了部分业务数据
- 状态栏、历史区也开始接入 store
- 但桌面最核心的“点击动物下注”链路还没有打通
---
## 4. useGameBoardVm 的职责
`useGameBoardVm` 只做 3 件事:
1.`game-round-store` 读取选号盘需要的业务数据
2. 组织出桌面选号盘可以直接消费的 view-model
3. 暴露点击格子的业务动作
它不负责:
- 直接渲染 UI
- 做 hover / 动画状态
- 控制 modal
- 处理移动端布局
---
## 5. 数据来源
`useGameBoardVm` 第一版只从 [game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts) 读取:
- `cells`
- `round`
- `selections`
- `trends`
- `placeBet`
可复用的派生逻辑来自 [selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts)
- `buildGameCellViewModels`
---
## 6. 第一版输出字段
第一版不追求一步到位,输出保持最小可用。
建议 `useGameBoardVm` 返回:
```ts
{
cells: GameCellViewModel[]
activeId: number | null
canPlaceBets: boolean
onCellPress: (cellId: number) => void
}
```
### 字段说明
#### `cells`
来源:
- `buildGameCellViewModels({ cells, round, selections, trends })`
作用:
- 给未来第二版 board 组件升级时使用
- 即使第一版 `DesktopAnimal` 还没完全吃它,也应该先在 hook 里产出来
#### `activeId`
第一版定义:
- 当前有下注的最后一个格子 id
- 如果没有任何下注,则为 `null`
作用:
- 兼容当前 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 现有接口
- 因为这个组件当前只支持单个 `activeId`,还不支持多个已选格子
#### `canPlaceBets`
定义:
- `round.phase === 'betting'`
作用:
- 控制点击是否真正触发下注
- 也为后续 UI 禁用态预留
#### `onCellPress`
定义:
-`canPlaceBets === true` 时,调用 `placeBet(cellId)`
- 否则不执行
---
## 7. 第一版实现规则
### 7.1 不在 DesktopAnimal 内直接读 store
[desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx) 必须继续保持展示组件定位。
不应该在里面直接写:
- `useGameRoundStore`
- `placeBet`
- `buildGameCellViewModels`
- `round.phase` 判断
### 7.2 业务写在 hookUI 只接 props
推荐接线方式:
在 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx) 中:
```tsx
const { activeId, onCellPress } = useGameBoardVm()
<DesktopAnimal activeId={activeId} onSelect={onCellPress} />
```
### 7.3 第一版先兼容现有 DesktopAnimal 接口
当前 `DesktopAnimal` 只支持:
- `activeId?: number | null`
- `onSelect?: (animalId: number) => void`
所以第一版不要强行重做它的 props 结构。
先兼容现状,把业务链路打通即可。
---
## 8. 第一版文件改动范围
本次改动建议控制在 2 到 3 个文件:
1. 新增 `src/features/game/hooks/use-game-board-vm.ts`
2. 修改 [pc-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/pc-entry.tsx)
3. 如有必要,微调 [desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
其中第 3 项不是必须,优先争取只改前两个文件。
---
## 9. 第一版完成标准
完成后应满足:
- `PcEntry` 不再裸挂 `<DesktopAnimal />`
- `PcEntry` 通过 `useGameBoardVm` 把选号盘接到 store
- 点击格子会真实调用下注动作
- 控制栏中的总下注额会随点击变化
- `DesktopAnimal` 仍然保持展示组件定位
---
## 10. 第二版演进方向
第一版做完后,下一版再考虑升级 `DesktopAnimal` 接口。
当前限制:
- `DesktopAnimal` 只能高亮一个 `activeId`
第二版建议升级为:
```ts
{
items: GameCellViewModel[]
onSelect: (cellId: number) => void
}
```
这样就可以支持:
- 多个已下注格子同时高亮
- 依据 `status` 区分 `betting / selected / won / lost`
- PC 和 Mobile 共用同一份 board view-model
但这些都不属于当前这一步。
---
## 11. 结论
当前最正确的操作顺序是:
1. 先写 `useGameBoardVm.ts`
2. 再在 `pc-entry.tsx` 中接入
3. 暂时不把业务写进 `desktop-animal.tsx`
这样可以在最小改动范围内把桌面端最核心的“点击格子下注”链路打通并继续保持“数据层在外、UI 层在内”的改造方向。

View File

@@ -0,0 +1,396 @@
# 36字花核心玩法与前端规则摘要
## 1. 项目定位
36字花是一个**单期开奖结果、单期号循环运行**的实时开奖类游戏。
前端的核心界面是:
- 36 宫格下注盘
- 倒计时与当前期状态栏
- 筹码与确认下注区
- 开奖历史与走势信息
- 公告、规则、自动托管、充值提现等外围模块
产品运行原则:
- 全平台共享同一局数据
- `PC``Mobile` 只分界面,不分玩法、不分对局
- 前端必须以服务端状态为准,不能靠本地时间自行判断开奖或封盘
---
## 2. 核心玩法
### 2.1 基本盘面
- 游戏共有 **36 个号码格子**
- 每个格子代表一个“字花编号”
- 编号范围为 **1-36**
- 当前前端盘面布局为 **6 x 6**
相关代码与约束:
- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts)
- `GAME_GRID_ROWS = 6`
- `GAME_GRID_COLUMNS = 6`
- `GAME_TOTAL_CELLS = 36`
### 2.2 开奖规则
- 每一期只会开出 **1 个中奖号码**
- 开奖号码属于 1-36 中的一个
- 用户只要下注号码集合中包含该号码,即视为中奖
接口口径见:
- [docs/36字花-移动端接口设计草案.md](/Users/jiaunun/Desktop/36-character-flower/docs/36字花-移动端接口设计草案.md)
- `POST /api/game/placeBet`
文档原意:
- 玩家提交的是“号码集合 + 单注金额”
- 系统按“单注金额 × 号码数量”计算本笔总扣款
- 开奖后只出一个号码
- 若该号码命中玩家所选集合,则该笔下注中奖
---
## 3. 下注模型
### 3.1 单注结构
一笔下注至少包含:
- `period_no`:下注目标期号
- `numbers`:本次下注号码集合
- `single_bet_amount`:单个号码的下注金额
说明:
- `numbers` 是一个号码集合,而不是单个号码
- 多选号码时,总扣款 = `single_bet_amount × numbers数量`
- 重复号码应去重
### 3.2 前端当前数据模型
当前前端 store 中的单笔选择数据为:
- `cellId`
- `chipId`
- `amount`
- `placedAt`
- `source`
定义见:
- [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts)
- `BetSelection`
当前本地 mock 与 store 落地方式是:
- 每点击一个格子,会追加一条 `selection`
- 每条 `selection` 对应一个格子和一个筹码金额
这与接口文档中的“号码集合提交”并不完全一致。
因此当前前端可以先采用如下理解:
- UI 交互阶段:按“逐格选择”记录本地状态
- 提交到后端阶段:再把本地多个格子聚合成 `numbers`
这是后续 `confirm bet` 需要承担的转换逻辑。
---
## 4. 回合状态机
### 4.1 当前状态枚举
当前项目中定义的回合阶段为:
- `waiting`
- `betting`
- `locked`
- `revealing`
- `settled`
定义见:
- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts)
接口文档中的状态口径包括:
- `betting`
- `locked`
- `settling`
- `finished`
- `void`
说明:
- 当前前端内部状态和接口文档状态还未完全统一
- 第一阶段前端可继续沿用内部状态机
- 后续真实联调时,要补一层接口状态 -> 前端状态的映射
### 4.2 各阶段前端规则
#### `BETTING`
- 允许选格子
- 允许切换筹码
- 允许清除当前选择
- 允许重复上一注
- 允许确认下注
- 允许开启自动托管
#### `LOCKED`
- 已封盘
- 前端必须立即停止下注交互
- 不应等待后端返回后才禁用点击
#### `REVEALING / SETTLING`
- 等待开奖与派彩
- 展示开奖结果、跑马灯、中奖态
#### `SETTLED / FINISHED`
- 本期结束
- 准备进入下一轮
- 历史、走势、余额等应刷新
#### `VOID`
- 本期作废
- 需要退款待开奖本金
- 前端要清理本期下注态并正确提示
---
## 5. 封盘与倒计时规则
### 5.1 核心原则
- 前端必须以服务端返回的时间和阶段为准
- 到达封盘时间点时,前端应立即锁盘
- 即使网络延迟,也不能继续允许下注交互
这点在需求文档中是明确要求:
- [docs/frontend-baseline-requirements.md](/Users/jiaunun/Desktop/36-character-flower/docs/frontend-baseline-requirements.md)
### 5.2 当前前端倒计时模型
当前前端使用:
- `round.bettingClosesAt`
- `round.revealingAt`
- `round.settledAt`
并通过:
- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts)
- `getRoundCountdownMs(round)`
来计算当前倒计时毫秒数。
规则如下:
- `waiting / betting`:倒计时到 `bettingClosesAt`
- `locked / revealing`:倒计时到 `revealingAt`
- 其余:倒计时到 `settledAt`
### 5.3 Mock 数据口径
当前 mock 数据中:
- 开始后约 18 秒封盘
- 约 24 秒开奖
- 约 30 秒结算
来源:
- [src/features/game/shared/mock-data.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/mock-data.ts)
这只是前端演示节奏,不代表最终真实服务端配置。
---
## 6. 赔率、金额与筹码规则
### 6.1 当前赔率
当前 mock 中每个格子赔率固定为:
- `36`
来源:
- [src/features/game/shared/mock-data.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/mock-data.ts)
- `createGameCells()`
### 6.2 当前默认筹码
当前默认筹码面额为:
- `10`
- `25`
- `50`
- `100`
- `200`
- `500`
来源:
- [src/features/game/shared/constants.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/constants.ts)
- `DEFAULT_GAME_CHIP_AMOUNTS`
### 6.3 下注限制
从接口文档与基线需求中可得出以下前端规则:
- 单次下注号码数量不得超过 `pick_max_number_count`
- 单号码下注金额不得超过 `single_number_max_bet`
- 总下注额不得超过余额
- 封盘后不能继续下注
文档来源:
- [docs/36字花-移动端接口设计草案.md](/Users/jiaunun/Desktop/36-character-flower/docs/36字花-移动端接口设计草案.md)
- [docs/frontend-baseline-requirements.md](/Users/jiaunun/Desktop/36-character-flower/docs/frontend-baseline-requirements.md)
---
## 7. 中奖、历史与走势
### 7.1 历史记录
每期开奖后应产生一条历史记录,至少包括:
- `roundId`
- `winningCellId`
- `settledAt`
- `payoutMultiplier`
- `totalPoolAmount`
定义见:
- [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts)
- `HistoryEntry`
### 7.2 走势
前端会基于历史数据派生走势信息,包括:
- `currentStreak`
- `hitCount`
- `missCount`
- `direction`
- `lastHitRoundId`
派生逻辑见:
- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts)
- `deriveTrendEntries(history)`
说明:
- 走势属于纯派生展示数据
- 不应由 UI 组件自己重复计算
---
## 8. 公告、维护与运行开关
### 8.1 运行开关
接口文档中存在:
- `runtime_enabled`
含义:
- `true`:游戏正常运行
- `false`:后台维护中,禁止下注
规则:
- 维护中不允许新下注
- 当前已开盘的一局仍可正常开奖和派彩
- 前端应禁用下注入口并提示维护状态
### 8.2 公告
公告是前端大厅的一部分,但不属于主玩法。
当前前端已存在公告模型:
- `AnnouncementState`
- `AnnouncementItem`
它们属于会话层状态,不应混入下注与回合逻辑。
---
## 9. 自动托管
接口文档中已定义自动托管能力:
- `POST /api/game/autoSpin`
入参包括:
- `action`
- `period_no`
- `numbers`
- `single_bet_amount`
- `rounds`
说明:
- 自动托管属于建立在主玩法之上的扩展能力
- 它依赖同一套选号与下注规则
- 当前前端可以先保留 UI 壳层,不需要在主玩法没走通前抢先落业务
---
## 10. 前端当前最应该优先走通的主玩法链路
基于上述规则,当前前端最核心、最应该优先走通的是:
1. 状态栏拿到当前期状态与倒计时
2. 控制栏拿到当前筹码与总下注额
3. 选号盘点击格子后写入本地下注选择
4. 总下注额、选中数量、选号高亮联动刷新
5. 后续再补确认下注请求与开奖结果回写
这也是为什么当前下一步应优先实现:
- `useGameBoardVm`
而不是优先改 `mobile` 或外围弹窗。
---
## 11. 总结
36字花这个项目的核心不是“36 张图摆出来”,而是下面这条实时对局链路:
- 同一局
- 同一倒计时
- 同一开奖结果
- 36 格可选号码
- 用户用统一筹码模型下注
- 封盘、开奖、派奖按服务端状态推进
前端实现时必须坚持两点:
1. **玩法规则统一**
- PC 和 Mobile 只能换壳,不能换规则
2. **服务端状态优先**
- 前端可以先做本地交互反馈
- 但回合状态、封盘、开奖、派彩都必须最终以服务端为准

View File

@@ -0,0 +1,846 @@
# 36字花游戏模块数据与界面分层第一阶段实施稿
## 1. 目标
第一阶段的目标不是做一次彻底重构,而是先把当前项目中的“业务数据”和“界面渲染”拆出清晰边界,让同一套数据和交互逻辑可以同时服务 `PC` 界面与 `Mobile` 界面。
本阶段只解决以下问题:
- 一套业务数据,供 `PC``Mobile` 共同消费
- 统一业务交互逻辑,避免双端各自维护一份
- 保持 `PC``Mobile` 布局、样式、视觉独立
- 不大范围推翻现有组件,优先在现有代码上增量改造
本阶段暂不追求:
- 所有组件完全抽象成跨端共享组件
- 一次性去掉所有本地 `useState`
- 一次性重写所有弹窗和表单
- 大规模目录迁移导致整仓成本过高
## 2. 当前代码现状
当前仓库中已经存在一部分良好的基础设施:
- 数据请求与 DTO 归一化:
- [src/features/game/api/game-api.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/game-api.ts)
- 共享领域类型与派生逻辑:
- [src/features/game/shared/types.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/types.ts)
- [src/features/game/shared/selectors.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts)
- 回合与会话状态:
- [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts)
- [src/store/game/game-session-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-session-store.ts)
- 页面入口已统一在:
- [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx)
当前主要问题有 4 个:
1. `Mobile` 界面直接复用了 `Desktop` 组件
例如 [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx) 当前直接引用了 `DesktopAnimal``DesktopGameHistory``DesktopTitle`。这会导致移动端无法形成独立布局层。
2. 业务状态和界面状态混杂
一些状态是业务性的,例如当前选中筹码、是否允许下注;另一些只是表现性的,例如按钮点击动画、过渡效果。当前这两种状态没有被清晰拆开。
3. 组件层直接拼 store 数据或自己维护业务数据
例如 [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx) 内部自己维护了部分核心控制状态,导致未来 `Mobile` 难以复用。
4. 弹窗与 UI 业务状态分散在各组件本地 `useState`
比如 `auto setting``notice``user info``procedures` 等弹窗,当前都在各自文件内部自管 `open` 或 tab 状态,不利于双端共享和统一调度。
---
## 3. 第一阶段总体原则
第一阶段采用四层结构:
1. `api / dto / normalize`
2. `store / domain actions`
3. `view-model hooks`
4. `pc ui``mobile ui`
每层职责如下:
### 3.1 API 层
职责:
- 调用后端接口
- DTO 转前端领域模型
- 不参与 UI 展示逻辑
保留现状:
- [src/features/game/api/game-api.ts](/Users/jiaunun/Desktop/36-character-flower/src/features/game/api/game-api.ts)
### 3.2 Store 层
职责:
- 保存当前页面共享的真实业务状态
- 提供业务动作
- 不直接处理视觉布局
### 3.3 View-model hooks 层
职责:
- 从 store 中读取数据
- 聚合派生字段
- 封装业务事件
- 返回给 `PC``Mobile` 可直接渲染的数据结构
这层是本次改造的核心。
### 3.4 UI 层
职责:
- 只负责布局、视觉、交互表现
- `PC``Mobile` 各自独立
- 不直接拼底层 store
---
## 4. 第一阶段目录改造清单
建议目录结构如下。第一阶段以“新增”为主,不要求立刻大规模迁移原文件。
```text
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 目录建议补齐成:
```text
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`
文件:
- [src/store/game/game-round-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-round-store.ts)
该 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`
文件:
- [src/store/game/game-session-store.ts](/Users/jiaunun/Desktop/36-character-flower/src/store/game/game-session-store.ts)
该 store 保留为“会话、连接、公告、面板摘要信息”。
#### 现有字段
- `announcements`
- `connection`
- `dashboard`
#### 现有动作
- `hydrateSession`
- `dismissAnnouncement`
- `markAnnouncementRead`
- `setConnectionLatency`
- `setConnectionStatus`
- `syncConnection`
- `syncDashboard`
#### 第一阶段建议新增字段
- `serverTimeIso: string | null`
- `lastBootstrapAt: string | null`
#### 第一阶段建议新增动作
- `setServerTime(serverTimeIso: string)`
- `setLastBootstrapAt(iso: string)`
说明:
- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/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 | 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()`
#### 第一阶段要接管的现有状态来源
- [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx)
- [src/features/game/modal/desktop/desktop-notice-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-notice-modal.tsx)
- [src/features/game/modal/desktop/desktop-userInfo-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-userInfo-modal.tsx)
- [src/features/game/modal/desktop/desktop-procedures-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-procedures-modal.tsx)
- [src/features/game/modal/desktop/desktop-login-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-login-modal.tsx)
- [src/features/game/modal/desktop/desktop-register-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-register-modal.tsx)
- [src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-withdraw-topup-modal.tsx)
#### 不要放到 `game-ui-store` 的状态
- `confirmClicked`
- `clickedId`
- `hidingId`
这些状态目前位于 [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx)
- 未来的 `mobile-header.tsx`
#### 建议暴露字段
- `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`
服务组件:
- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx)
#### 建议暴露字段
- `roundId: string`
- `oddsLabel: string`
- `streakLabel: string`
- `limitLabel: string`
- `countdownSeconds: number`
- `countdownMs: number`
- `phase: RoundPhase`
- `phaseLabel: string`
- `phaseTone: '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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
- 未来 `mobile-board.tsx`
#### 建议暴露字段
- `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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/shared/selectors.ts) 中的:
- `buildGameCellViewModels`
- `groupSelectionsByCell`
- `getSelectionTotal`
- `DesktopAnimal` 不应该自己管理业务选择逻辑,只负责渲染 cell grid
---
### 6.4 `useGameHistoryVm`
建议文件:
- `src/features/game/hooks/use-game-history-vm.ts`
服务组件:
- [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-history.tsx)
- 未来 `mobile-history.tsx`
#### 建议暴露字段
- `items`
- `orderNo`
- `roundId`
- `winningCellId`
- `betAmountLabel`
- `totalAmountLabel`
- `winAmountLabel`
- `statusLabel`
- `createdAtLabel`
- `isEmpty: boolean`
- `emptyText: string`
- `recentWinningCellIds: number[]`
#### 说明
- 当前 [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx)
- 未来 `mobile-control.tsx`
#### 建议暴露字段
- `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()`
#### 说明
- [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/shared/game-announcement-modal.tsx)
- header 消息入口
- notice modal
#### 建议暴露字段
- `activeAnnouncement: AnnouncementItem | null`
- `visibleAnnouncements: AnnouncementItem[]`
- `unreadCount: number`
- `hasUnread: boolean`
- `isOpen: boolean`
#### 建议暴露动作
- `onOpenAnnouncement(id?: string)`
- `onDismissAnnouncement(id: string)`
- `onMarkRead(id: string)`
- `onCloseAnnouncement()`
#### 说明
- 这样公告逻辑不会散落在 [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/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](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx)
- 未来 `mobile-auto-setting-modal.tsx`
#### 建议暴露字段
- `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
文件:
- [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx)
保留它作为:
- bootstrap 获取入口
- store hydrate 入口
- PC/Mobile 分流入口
### 8.2 容器组件可以调用 VM hooks
例如:
- `PcEntry`
- `MobileEntry`
- 各 section 容器
- modal host
这些组件可以:
- `useGameBoardVm()`
- `useGameControlVm()`
- `useGameStatusVm()`
### 8.3 纯展示组件不直接碰 store
比如:
- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
- [src/features/game/components/desktop/desktop-game-history.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-game-history.tsx)
- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx)
- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx)
未来都应该尽量演进为:
- 只吃 props
- 不主动拼底层 store
---
## 9. 第一阶段实施顺序
建议严格按这个顺序执行,避免改动面过大。
### Step 1新增 UI Store 与 Hooks 目录
新增:
- `src/store/game/game-ui-store.ts`
- `src/features/game/hooks/`
这一步只建壳子,不急着接业务。
### Step 2实现 `useGameBoardVm` 与 `useGameControlVm`
这两块是主玩法交互核心,优先收益最大。
优先接的现有文件:
- [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
- [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx)
### Step 3让 `DesktopAnimal` 只吃 props
目标:
- 不直接读 store
- 不自己处理业务选择逻辑
保留字段形态:
- `cells`
- `activeId`
- `onSelect`
### Step 4让 `DesktopControl` 业务数据来自 VM
目标:
- 当前筹码列表、已选筹码、总下注额等来自 `useGameControlVm`
- 点击动画状态仍然保留本地
### Step 5停止 `Mobile` 直接复用 `Desktop` 组件
文件:
- [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx)
当前问题:
- 直接 import `DesktopAnimal`
- 直接 import `DesktopGameHistory`
- 直接 import `DesktopTitle`
第一阶段目标:
- 改成 `MobileBoardSection`
- 改成 `MobileHistorySection`
- 共享 hook不共享 `Desktop` 布局组件
### Step 6实现 `useGameStatusVm` 与 `useGameHeaderVm`
接入文件:
- [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx)
- [src/features/game/components/desktop/desktop-header.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-header.tsx)
### Step 7实现 `game-ui-store` + `useGameAutoSpinVm`
先接一个最合适的 modal
- [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx)
跑通后,再接:
- notice
- user info
- procedures
- login/register
---
## 10. 第一阶段优先改造文件清单
第一阶段最值得优先改的 6 个文件如下:
1. [src/features/game/entry/entry-page.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/entry-page.tsx)
2. [src/features/game/entry/mobile-entry.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/entry/mobile-entry.tsx)
3. [src/features/game/components/desktop/desktop-animal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-animal.tsx)
4. [src/features/game/components/desktop/desktop-control.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-control.tsx)
5. [src/features/game/components/desktop/desktop-status.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/components/desktop/desktop-status.tsx)
6. [src/features/game/modal/desktop/desktop-auto-setting-modal.tsx](/Users/jiaunun/Desktop/36-character-flower/src/features/game/modal/desktop/desktop-auto-setting-modal.tsx)
原因:
- 它们覆盖了主玩法、主状态、移动端入口和共享 modal 模式
- 先把这 6 个改通,数据与界面分层的骨架就建立起来了
---
## 11. 第一阶段完成后的验收标准
做到以下几点,即可视为第一阶段完成:
- `PC``Mobile` 都不直接拼 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 接入