diff --git a/.serena/cache/java/document_symbols_cache_v23-06-25.pkl b/.serena/cache/java/document_symbols_cache_v23-06-25.pkl index 6e3cf27..761610e 100644 Binary files a/.serena/cache/java/document_symbols_cache_v23-06-25.pkl and b/.serena/cache/java/document_symbols_cache_v23-06-25.pkl differ diff --git a/.serena/memories/blood_battle_scoring_v1.md b/.serena/memories/blood_battle_scoring_v1.md index 9b403b0..f9eed6c 100644 --- a/.serena/memories/blood_battle_scoring_v1.md +++ b/.serena/memories/blood_battle_scoring_v1.md @@ -2,15 +2,16 @@ - 已从工程占位分切换到“最小可扩展正式版”计分骨架。 - 当前已支持的规则主线包括:七对、对对胡、金钩钓、清一色、根、抢杠胡、杠上花、杠上炮、海底捞月、海底炮、明杠/补杠/暗杠、退税、查叫、一炮多响、最小正式版过水不胡。 - 当前胡牌计分口径:`paymentScore = 1 << totalFan`。 -- 一炮多响:只对 `HU` 开放多赢家同窗裁决,`PENG/GANG` 仍单赢家。 +- 一炮多响:只对 `HU` 开放多赢家同窗裁决,`PENG / GANG` 仍单赢家。 - 过水不胡:玩家在响应窗口里本可 `HU` 但选择 `PASS` 后,直到自己下一次真正摸牌前,不能再做响应胡;不影响碰、杠、自摸胡。 -- 后端最小验证:`cd backend && mvn test` 通过,当前 53 个测试通过。 -- 前端 H5 对局页已经完成两轮联调: - - 已区分“当前回合动作”和“响应动作”两种动作面板。 - - `DISCARD` 继续通过点击手牌执行,`GANG/HU` 等额外动作走统一面板。 - - 响应动作面板会展示 `sourceSeatNo`、`triggerTile`、`triggerEventType`、`windowId`。 - - 已支持在对局页切换玩家视角,并自动刷新对应 `GameStateResponse` 与重连该玩家私有 WebSocket 主题。 - - 公共事件接收已改为统一入口处理,会在 `RESPONSE_WINDOW_CLOSED`、`TURN_SWITCHED`、`TILE_DISCARDED`、`GAME_PHASE_CHANGED` 等场景下清理已失效的私有动作面板,避免旧窗口残留。 - - 公共事件时间线已支持中文摘要文案、时间展示和原始载荷折叠查看,便于联调时同时看“业务含义”和“真实 payload”。 -- 注释约定继续有效:后端复杂规则、前端复杂交互和后续数据库脚本都要补适当偏多的中文注释。 -- 前端验证:`cd frontend && npm run build` 通过。 \ No newline at end of file +- 教学建议链路已扩展为:`recommendedAction`、`explanation`、`candidates`,其中 `candidates` 透传到私有 WebSocket 教学消息,前端已支持展示候选牌、评分和原因标签中文映射。 +- 前端 H5 对局页已完成正式动作面板、响应动作面板、玩家视角切换、公共事件时间线、最近结算卡片、教学推荐高亮等联调。 +- 2026-03-20 当日新增一轮前端结构收口: + - 新增 `docs/H5_GAME_PAGE_ARCHITECTURE.md`,完成 `S1-08` 页面信息架构与拆分方案。 + - 前端共享类型已抽到 `frontend/src/types/game.ts`。 + - UI 标签映射与事件/结算格式化函数已抽到 `frontend/src/utils/gameUi.ts`。 + - 已拆出展示组件:`GameActionDock.vue`、`GameMessageStack.vue`、`PublicEventTimeline.vue`。 + - `App.vue` 当前定位已经收敛为“页面容器 + 请求/订阅协调层”,便于下一轮继续拆 `RoomPage / GamePage / ReviewPage`。 +- 2026-03-20 当日还修复了 `GameSessionServiceTest` 中两条“过水不胡”测试的脆弱构造,改为显式控制响应胡候选与后续安全弃牌,避免依赖随机初始牌与中间牌路。 +- 当前验证状态:`cd backend && mvn test` 通过;`cd frontend && npm run build` 通过。 +- 注释约定继续有效:后端复杂规则、前端复杂交互、后续数据库脚本和迁移都要补适当偏多的中文注释,尤其说明规则判断、状态切换、消息边界与字段语义。 \ No newline at end of file diff --git a/backend/src/main/java/com/xuezhanmaster/game/service/GameSessionService.java b/backend/src/main/java/com/xuezhanmaster/game/service/GameSessionService.java index 5523a40..491925f 100644 --- a/backend/src/main/java/com/xuezhanmaster/game/service/GameSessionService.java +++ b/backend/src/main/java/com/xuezhanmaster/game/service/GameSessionService.java @@ -301,7 +301,8 @@ public class GameSessionService { currentSeat.getPlayerId(), advice.teachingMode(), advice.recommendedAction(), - advice.explanation() + advice.explanation(), + advice.candidates() ); } diff --git a/backend/src/main/java/com/xuezhanmaster/ws/dto/PrivateTeachingMessage.java b/backend/src/main/java/com/xuezhanmaster/ws/dto/PrivateTeachingMessage.java index a31ebc7..766f520 100644 --- a/backend/src/main/java/com/xuezhanmaster/ws/dto/PrivateTeachingMessage.java +++ b/backend/src/main/java/com/xuezhanmaster/ws/dto/PrivateTeachingMessage.java @@ -1,11 +1,15 @@ package com.xuezhanmaster.ws.dto; +import com.xuezhanmaster.teaching.dto.CandidateAdviceItem; + +import java.util.List; + public record PrivateTeachingMessage( String gameId, String userId, String teachingMode, String recommendedAction, - String explanation + String explanation, + List candidates ) { } - diff --git a/backend/src/main/java/com/xuezhanmaster/ws/service/GameMessagePublisher.java b/backend/src/main/java/com/xuezhanmaster/ws/service/GameMessagePublisher.java index 5cbffc0..c77a9c9 100644 --- a/backend/src/main/java/com/xuezhanmaster/ws/service/GameMessagePublisher.java +++ b/backend/src/main/java/com/xuezhanmaster/ws/service/GameMessagePublisher.java @@ -3,6 +3,7 @@ package com.xuezhanmaster.ws.service; import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate; import com.xuezhanmaster.game.domain.ResponseActionWindow; import com.xuezhanmaster.game.event.GameEvent; +import com.xuezhanmaster.teaching.dto.CandidateAdviceItem; import com.xuezhanmaster.ws.dto.PrivateActionCandidate; import com.xuezhanmaster.ws.dto.PrivateActionMessage; import com.xuezhanmaster.ws.dto.PrivateTeachingMessage; @@ -97,10 +98,18 @@ public class GameMessagePublisher { ); } - public void publishPrivateTeaching(String gameId, String userId, String teachingMode, String recommendedAction, String explanation) { + public void publishPrivateTeaching( + String gameId, + String userId, + String teachingMode, + String recommendedAction, + String explanation, + List candidates + ) { messagingTemplate.convertAndSend( "/topic/users/" + userId + "/teaching", - new PrivateTeachingMessage(gameId, userId, teachingMode, recommendedAction, explanation) + // 教学消息需要同时携带推荐结果与备选列表,前端才能把“为什么是这张牌”解释完整。 + new PrivateTeachingMessage(gameId, userId, teachingMode, recommendedAction, explanation, candidates) ); } diff --git a/backend/src/test/java/com/xuezhanmaster/game/service/GameSessionServiceTest.java b/backend/src/test/java/com/xuezhanmaster/game/service/GameSessionServiceTest.java index abdf656..e7976ef 100644 --- a/backend/src/test/java/com/xuezhanmaster/game/service/GameSessionServiceTest.java +++ b/backend/src/test/java/com/xuezhanmaster/game/service/GameSessionServiceTest.java @@ -786,8 +786,9 @@ class GameSessionServiceTest { prepareWinningHand(winnerSeat); session.getTable().setCurrentSeatNo(2); ensureSeatHasMatchingTiles(session.getTable().getSeats().get(2), "9筒", 1); - ensureSeatHasMatchingTiles(session.getTable().getSeats().get(3), "9筒", 1); - removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2, 3); + // 这里只需要保证 seat 1 能对 seat 2 的 9筒形成“可胡后选择 PASS”的场景。 + // 不再额外给 seat 3 补 9筒,避免它也意外成为同一响应窗里的第二个胡牌候选,导致测试依赖随机手牌分布。 + removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2); gameSessionService.performAction( started.gameId(), @@ -801,9 +802,12 @@ class GameSessionServiceTest { assertThat(winnerSeat.isPassedHuBlocked()).isTrue(); assertThat(afterPass.currentSeatNo()).isEqualTo(3); + // 这里取 seat 3 当前手里的安全牌继续推进牌局,只验证“未摸到自己下一张牌前仍然不能响应胡”。 + String seatThreeSafeDiscard = session.getTable().getSeats().get(3).getHandTiles().get(0).getDisplayName(); + removeMatchingTilesFromOtherSeats(session, seatThreeSafeDiscard, 3); GameStateResponse afterSecondDiscard = gameSessionService.performAction( started.gameId(), - new GameActionRequest("player-4", "DISCARD", "9筒", null) + new GameActionRequest("player-4", "DISCARD", seatThreeSafeDiscard, null) ); assertThat(session.getPendingResponseActionWindow()).isNull(); @@ -863,12 +867,15 @@ class GameSessionServiceTest { assertThat(afterSeatZeroDiscard.currentSeatNo()).isEqualTo(1); assertThat(winnerSeat.isPassedHuBlocked()).isFalse(); - removeMatchingTilesFromOtherSeats(session, "8万", 1); + // 解锁完成后,重新稳定构造一个“seat 2 打出 9筒,seat 1 可以响应胡”的窗口, + // 避免继续依赖中间多轮摸打与随机初始牌造成的测试波动。 + prepareWinningHand(winnerSeat); + GameSeat sourceSeat = session.getTable().getSeats().get(2); + sourceSeat.getHandTiles().clear(); + sourceSeat.receiveTile(parseTile("9筒")); + session.getTable().setCurrentSeatNo(2); + removeMatchingTilesFromOtherSeats(session, "9筒", 1, 2); - gameSessionService.performAction( - started.gameId(), - new GameActionRequest("player-2", "DISCARD", "8万", null) - ); GameStateResponse afterHu = gameSessionService.performAction( started.gameId(), new GameActionRequest("player-3", "DISCARD", "9筒", null) diff --git a/backend/src/test/java/com/xuezhanmaster/ws/service/GameMessagePublisherTest.java b/backend/src/test/java/com/xuezhanmaster/ws/service/GameMessagePublisherTest.java index aacf6f0..500078d 100644 --- a/backend/src/test/java/com/xuezhanmaster/ws/service/GameMessagePublisherTest.java +++ b/backend/src/test/java/com/xuezhanmaster/ws/service/GameMessagePublisherTest.java @@ -4,8 +4,10 @@ import com.xuezhanmaster.game.domain.ActionType; import com.xuezhanmaster.game.domain.ResponseActionOption; import com.xuezhanmaster.game.domain.ResponseActionSeatCandidate; import com.xuezhanmaster.game.domain.ResponseActionWindow; +import com.xuezhanmaster.teaching.dto.CandidateAdviceItem; import com.xuezhanmaster.ws.dto.PrivateActionCandidate; import com.xuezhanmaster.ws.dto.PrivateActionMessage; +import com.xuezhanmaster.ws.dto.PrivateTeachingMessage; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -84,4 +86,29 @@ class GameMessagePublisherTest { .extracting(candidate -> candidate.actionType()) .containsExactly("PENG", "PASS"); } + + @Test + void shouldPublishStructuredTeachingMessageWithCandidateList() { + gameMessagePublisher.publishPrivateTeaching( + "game-1", + "user-1", + "BRIEF", + "9筒", + "建议先打孤张。", + List.of( + new CandidateAdviceItem("9筒", 90, List.of("ISOLATED_TILE")), + new CandidateAdviceItem("1万", 70, List.of("EDGE_TILE")) + ) + ); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PrivateTeachingMessage.class); + verify(messagingTemplate).convertAndSend(org.mockito.ArgumentMatchers.eq("/topic/users/user-1/teaching"), captor.capture()); + + PrivateTeachingMessage message = captor.getValue(); + assertThat(message.teachingMode()).isEqualTo("BRIEF"); + assertThat(message.recommendedAction()).isEqualTo("9筒"); + assertThat(message.candidates()).hasSize(2); + assertThat(message.candidates().get(0).tile()).isEqualTo("9筒"); + assertThat(message.candidates().get(0).reasonTags()).containsExactly("ISOLATED_TILE"); + } } diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md index 059e9e6..68213e4 100644 --- a/docs/DEVELOPMENT_PLAN.md +++ b/docs/DEVELOPMENT_PLAN.md @@ -278,12 +278,21 @@ ws WebSocket 配置与消息发布 ### 6.2 当前前端结构 -当前前端仍以单页面原型为主,但已经承担三类职责: +当前前端仍以 `App.vue` 作为页面容器,但已经从“单文件原型”开始向“可继续拆页的正式骨架”演进,当前承担三类职责: - H5 房间流操作 - 对局状态展示 - WebSocket 消息接收与展示 +同时,已完成第一轮最小拆分准备: + +- 共享类型:`frontend/src/types/game.ts` +- UI 格式化工具:`frontend/src/utils/gameUi.ts` +- 展示组件: + - `frontend/src/components/GameActionDock.vue` + - `frontend/src/components/GameMessageStack.vue` + - `frontend/src/components/PublicEventTimeline.vue` + 后续建议按页面拆分为: - `HomePage` @@ -337,11 +346,15 @@ ws WebSocket 配置与消息发布 - WebSocket 私有动作订阅 - WebSocket 私有教学订阅 - `/api` 与 `/ws` 代理配置 +- 正式动作面板与响应动作面板联调 +- 公共事件时间线与最近结算卡片 +- 教学推荐高亮与候选建议列表 +- 对局页信息架构文档与最小组件拆分基础 ### 7.3 当前进行中 - 动作系统从“定缺 + 出牌”扩展到真正的麻将动作系统 -- H5 原型页向正式对局页演进的规划和拆解 +- H5 页面容器继续向 `RoomPage / GamePage / ReviewPage` 拆分 ### 7.4 当前已完成的文档治理 diff --git a/docs/H5_GAME_PAGE_ARCHITECTURE.md b/docs/H5_GAME_PAGE_ARCHITECTURE.md new file mode 100644 index 0000000..a684ae8 --- /dev/null +++ b/docs/H5_GAME_PAGE_ARCHITECTURE.md @@ -0,0 +1,321 @@ +# H5 对局页信息架构与页面拆分方案 + +当前状态快照日期:`2026-03-20` + +--- + +## 1. 文档目标 + +这份文档用于落实 `S1-08`,为 H5 原型页向正式页面演进提供可以直接执行的拆分输入。 + +它要解决的不是“最终视觉长什么样”,而是先把下面几件事固定下来: + +- 页面边界怎么切 +- 正式对局页内部区域怎么分 +- 公共消息和私有消息分别落在哪个区 +- 哪些状态应该留在页面容器,哪些应该沉到子组件 +- 下一轮前端继续拆页时,如何避免再次把逻辑堆回 `App.vue` + +这符合: + +- `KISS`:先拆职责边界,不先追求完整视觉重写 +- `YAGNI`:当前不引入路由级复杂壳层和状态库 +- `SOLID`:页面容器、消息面板、动作面板、公共时间线分层明确 +- `DRY`:共享类型与格式化函数抽离,避免多组件重复维护 + +--- + +## 2. 当前现状判断 + +当前 `frontend/src/App.vue` 已经承担了四类职责: + +1. 房间流操作 +2. 对局页状态与动作提交 +3. WebSocket 订阅与消息清理 +4. 消息卡片、结算卡片、时间线等展示 + +这说明它已经不再是“临时原型脚本”,而是一个事实上的页面容器。 + +当前如果继续在 `App.vue` 里叠加: + +- 断线重连提示 +- 教学开关 +- 复盘入口 +- 页面级切换 +- 更多正式规则结算展示 + +后续维护成本会快速上升。 + +--- + +## 3. 正式页面拆分建议 + +### 3.1 页面层级 + +建议前端逐步演进为以下页面: + +- `HomePage` + - 负责产品入口、快速建房、邀请码输入、最近房间入口 +- `RoomPage` + - 负责建房后、开局前的房间管理与玩家准备 +- `GamePage` + - 负责正式对局中的牌桌、动作、教学、公共事件 +- `ReviewPage` + - 负责局后个人复盘、关键失误与建议回看 + +当前阶段不要求一次性引入完整路由,但组件组织和文档命名要按这个终态准备。 + +### 3.2 当前这一轮的落地点 + +本轮先聚焦 `GamePage`。 + +原因: + +- 房间流已经基本够用 +- 当前复杂度主要集中在对局页 +- 动作消息、教学消息、结算时间线都已经真实接入 +- 正式血战计分规则后续迭代也主要会继续压在对局页 + +--- + +## 4. GamePage 信息分区 + +### 4.1 顶部概览区 + +职责: + +- 显示对局阶段 +- 显示当前视角 +- 显示剩余牌墙 +- 显示当前系统提示 + +说明: + +- 顶部概览区只做“当前局势总览” +- 不承载具体动作按钮 +- 不放长文教学解释,避免遮挡主桌面 + +### 4.2 自己手牌与当前动作区 + +职责: + +- 展示自己的手牌与副露 +- 承接定缺、主动回合动作 +- 处理“点击手牌出牌”的高频交互 +- 在手牌区局部高亮教学推荐牌 + +说明: + +- 出牌仍保留“点击手牌直接触发” +- 杠、自摸胡等低频动作通过统一动作面板承接 +- 这是为了保留 H5 的高频单手操作效率 + +### 4.3 响应动作区 + +职责: + +- 响应窗口出现时展示 `PENG / GANG / HU / PASS` +- 清晰显示来源座位、目标牌、触发事件类型 + +说明: + +- 该区域与“当前回合动作区”共用一个动作容器 +- 但视觉上必须区分当前回合与响应窗口 +- 当前项目已经实现统一动作消息结构,因此前端只需按 `actionScope` 切换视图 + +### 4.4 私有教学区 + +职责: + +- 展示推荐动作 +- 展示解释文案 +- 展示候选牌、评分、原因标签 + +说明: + +- 私有教学区必须与公共区域显著分离 +- 教学候选列表属于私有信息,不得混入公共事件流 +- 移动端下建议紧贴动作区,但与牌桌保持卡片边界 + +### 4.5 公共桌面区 + +职责: + +- 展示其他座位的公开信息 +- 展示弃牌、副露、积分、是否已胡 + +说明: + +- 公共桌面区不显示其他玩家私有教学 +- 这是消息边界在 UI 层的直接体现 + +### 4.6 公共事件与最近结算区 + +职责: + +- 展示公共事件时间线 +- 展示最近一次结算卡片 +- 展示最近结算对应的分数变化 + +说明: + +- 公共事件是调试和正式观感都需要保留的区域 +- 但在正式页面中,它应当是“次主区”,不是最上方首屏主区 + +--- + +## 5. 组件拆分建议 + +### 5.1 页面容器组件 + +- `GamePageContainer` + - 负责请求、WebSocket、页面级状态聚合 + - 负责“谁是当前视角用户”和“当前牌局状态”这两个页面主状态 + +当前阶段由 `App.vue` 暂代这个角色。 + +### 5.2 已建议先拆出的展示组件 + +本轮已经先按最小风险拆出三块: + +- `GameActionDock.vue` + - 只负责主动动作 / 响应动作展示与按钮提交 +- `GameMessageStack.vue` + - 只负责私有动作摘要卡与私有教学卡 +- `PublicEventTimeline.vue` + - 只负责最近结算、分数变化与公共事件时间线 + +这样做的目的不是“拆得越碎越好”,而是先把最容易继续膨胀的三个展示区从页面容器中拔出来。 + +### 5.3 下一轮建议继续拆的组件 + +- `RoomControlPanel` + - 建房、入房、准备、开局入口 +- `SelfHandPanel` + - 手牌、副露、推荐牌高亮 +- `PublicSeatBoard` + - 其他玩家公开桌面 +- `ViewSwitchPanel` + - 多玩家视角切换 + +--- + +## 6. 状态归属建议 + +### 6.1 必须保留在页面容器的状态 + +- `room` +- `game` +- `currentUserId` +- `publicEvents` +- `privateAction` +- `privateTeaching` +- `wsStatus` +- `busy` +- `error` + +原因: + +- 它们同时被多个区域消费 +- 其中 `privateAction` 与公共事件清理之间有联动 +- `currentUserId` 变化会触发重拉状态与重连私有 WebSocket + +### 6.2 可下沉为共享工具的内容 + +- 接口响应类型 +- 私有/公共消息 DTO +- UI 标签映射 +- 事件摘要格式化函数 +- 结算详情解析函数 + +本轮已经落地为: + +- `frontend/src/types/game.ts` +- `frontend/src/utils/gameUi.ts` + +### 6.3 当前不建议引入的东西 + +- Pinia 等全局状态库 +- 过早的页面级缓存层 +- 复杂消息归档服务 + +原因: + +- 当前页面仍是单条主链,复杂状态库会提升理解成本 +- 现阶段优先保证规则联调和 H5 可操作性 + +--- + +## 7. 移动端布局建议 + +### 7.1 布局优先级 + +移动端从上到下建议为: + +1. 顶部提示与对局摘要 +2. 自己手牌区 +3. 动作区 +4. 私有教学区 +5. 公共桌面区 +6. 公共事件时间线 + +### 7.2 关键原因 + +- 手牌和动作区必须靠前,保证高频操作路径最短 +- 私有教学紧跟动作区,减少视线切换 +- 公共事件时间线放在靠后位置,避免压缩手牌点击区域 + +### 7.3 H5 交互要求回落 + +- 关键按钮保持大点击热区 +- 不依赖 hover +- 响应窗口出现时动作按钮必须明显聚焦 +- 私有教学不能遮住手牌 + +--- + +## 8. 本轮已经落地的最小结构调整 + +为降低后续继续拆页的成本,本轮已经完成以下基础整理: + +- 抽离共享类型文件,减少组件间类型重复 +- 抽离 UI 格式化工具,减少多处重复的事件/结算渲染逻辑 +- 抽离动作面板组件 +- 抽离私有消息栈组件 +- 抽离公共事件时间线组件 + +这一步仍然保持: + +- `App.vue` 作为页面容器 +- 行为逻辑不变 +- WebSocket 订阅逻辑不变 +- 动作提交链路不变 + +--- + +## 9. 下一轮前端任务建议 + +建议按以下顺序继续: + +1. 把 `App.vue` 演进为 `GamePageContainer` +2. 把房间流区域抽成 `RoomPage` 或 `RoomControlPanel` +3. 把手牌区和公共桌面区拆成独立组件 +4. 再决定是否引入路由和页面级导航 + +如果下一轮要优先做体验而不是结构,建议优先加: + +- 教学开关入口 +- WebSocket 断线提示 +- 对局结束后的复盘入口占位 + +--- + +## 10. 验收结论 + +`S1-08` 完成的标准不是“页面完全重写”,而是: + +- 正式对局页的职责边界已经明确 +- 私有区、公共区、动作区层级已经固定 +- 下一轮前端可以按文档和当前代码骨架继续拆,不必重新讨论页面结构 + +当前结论:已满足。 diff --git a/docs/SPRINT_01_ISSUES_BOARD.md b/docs/SPRINT_01_ISSUES_BOARD.md index c8f3a1a..ee9501b 100644 --- a/docs/SPRINT_01_ISSUES_BOARD.md +++ b/docs/SPRINT_01_ISSUES_BOARD.md @@ -44,52 +44,7 @@ Sprint 目标: ## 2. 待做 -### S1-08 [H5] 对局页信息架构与页面拆分方案 - -## 背景 - -当前 `App.vue` 是原型操作台,已经能跑主流程,但信息和状态都堆在单页里。后续如果不先定义页面结构,动作系统一扩展,前端会迅速失控。 - -## 目标 - -输出 H5 正式对局页拆分方案,明确: - -- 房间页、对局页、复盘页的职责 -- 对局页内的信息分区 -- 私有教学面板和动作面板层级 -- 公共事件区与私有区边界 - -## 范围 - -- 页面职责定义 -- 组件拆分建议 -- 状态归属建议 -- 移动端布局区块说明 - -## 非范围 - -- 不在本任务里实现完整 UI -- 不在本任务里处理所有视觉细节 - -## 依赖 - -- `S1-05` - -## 产出物 - -- 页面拆分文档 -- 信息架构草图说明 -- 下一轮前端任务拆分建议 - -## 验收标准 - -- 下一轮前端改造可以按本文档直接开始 -- 公共区、私有区、动作区职责清楚 - -## 验证方式 - -- 文档评审 -- 与当前 H5 要求、周计划和阶段看板一致 +当前无新增待做项。下一轮若继续前端方向,建议从 `GamePageContainer / RoomPage` 拆分继续推进。 --- @@ -293,6 +248,35 @@ Sprint 目标: - 当前定缺和出牌流程未被破坏 - `npm run build` 已通过 +### S1-08 [H5] 对局页信息架构与页面拆分方案 + +## 已完成内容 + +- 新增文档 `docs/H5_GAME_PAGE_ARCHITECTURE.md` +- 明确了 `HomePage / RoomPage / GamePage / ReviewPage` 的职责边界 +- 明确了正式对局页内的六个信息分区: + - 顶部概览区 + - 自己手牌与当前动作区 + - 响应动作区 + - 私有教学区 + - 公共桌面区 + - 公共事件与最近结算区 +- 明确了页面容器与子组件的状态归属边界 +- 前端已完成最小拆分准备: + - 共享类型抽到 `frontend/src/types/game.ts` + - UI 格式化工具抽到 `frontend/src/utils/gameUi.ts` + - 展示组件拆出: + - `GameActionDock.vue` + - `GameMessageStack.vue` + - `PublicEventTimeline.vue` +- 当前重构保持原有动作提交流程、WebSocket 订阅逻辑和 H5 操作路径不变 + +## 验收结果 + +- 下一轮前端可以按文档与当前骨架继续拆页 +- 公共区、私有区、动作区职责已经固定 +- `npm run build` 已通过 + ### S1-06 [研究] 响应优先级裁决规则澄清 ## 已完成内容 diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 3e4a048..43955d2 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,105 +1,31 @@