From 66834d8a7a28622f4232ed6254484b5292634aaf Mon Sep 17 00:00:00 2001
From: wsy182 <2392948297@qq.com>
Date: Wed, 25 Mar 2026 17:26:18 +0800
Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E6=88=BF?=
=?UTF-8?q?=E9=97=B4=E7=8E=A9=E5=AE=B6=E7=8A=B6=E6=80=81=E5=90=8C=E6=AD=A5?=
=?UTF-8?q?=E5=8A=9F=E8=83=BD?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- 定义 RoomPlayerUpdatePayload 接口用于处理房间状态更新
- 在游戏动作中新增 ROOM_PLAYER_UPDATE 类型支持
- 实现游戏状态管理器中的房间玩家更新逻辑
- 重构成都麻将页面以使用新的状态管理机制
- 添加从 WebSocket 消息转换为游戏动作的功能
- 更新房间离开时的 WebSocket 消息发送逻辑
- 优化玩家手牌显示和选择逻辑
- 调整房间状态显示逻辑以匹配新状态模型
- 修复座位索引计算和庄家标识逻辑
- 更新全局样式中的图标按钮样式
- 替换大厅页面的刷新图标为 SVG 图像
- 升级 pnpm 包管理器版本
- 扩展玩家状态类型定义以支持显示名称和缺门信息
---
package.json | 2 +-
src/assets/images/icons/avatar.svg | 1 +
src/assets/images/icons/refresh.svg | 1 +
src/assets/styles/global.css | 13 ++
src/game/actions.ts | 27 ++-
src/game/dispatcher.ts | 10 +-
src/store/gameStore.ts | 91 ++++++++-
src/types/state/playerState.ts | 2 +
src/views/ChengduGamePage.vue | 282 ++++++++++++++++++++--------
src/views/HallPage.vue | 5 +-
10 files changed, 352 insertions(+), 82 deletions(-)
create mode 100644 src/assets/images/icons/avatar.svg
create mode 100644 src/assets/images/icons/refresh.svg
diff --git a/package.json b/package.json
index 5f79224..ebd3a6f 100644
--- a/package.json
+++ b/package.json
@@ -3,7 +3,7 @@
"private": true,
"version": "0.0.0",
"type": "module",
- "packageManager": "pnpm@9.0.0",
+ "packageManager": "pnpm@10.28.2",
"scripts": {
"dev": "vite",
"build": "vue-tsc -b && vite build",
diff --git a/src/assets/images/icons/avatar.svg b/src/assets/images/icons/avatar.svg
new file mode 100644
index 0000000..f013996
--- /dev/null
+++ b/src/assets/images/icons/avatar.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/images/icons/refresh.svg b/src/assets/images/icons/refresh.svg
new file mode 100644
index 0000000..f809f38
--- /dev/null
+++ b/src/assets/images/icons/refresh.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css
index 95cadbd..e553a9d 100644
--- a/src/assets/styles/global.css
+++ b/src/assets/styles/global.css
@@ -335,6 +335,9 @@ button:disabled {
.icon-btn {
width: 34px;
height: 34px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
border: 1px solid rgba(176, 216, 194, 0.35);
border-radius: 8px;
color: #d3efdf;
@@ -342,6 +345,16 @@ button:disabled {
cursor: pointer;
}
+.icon-btn-image {
+ width: 18px;
+ height: 18px;
+ display: block;
+}
+
+.icon-btn:disabled .icon-btn-image {
+ opacity: 0.65;
+}
+
.room-list {
list-style: none;
margin: 0;
diff --git a/src/game/actions.ts b/src/game/actions.ts
index 21d7728..8c03851 100644
--- a/src/game/actions.ts
+++ b/src/game/actions.ts
@@ -1,6 +1,25 @@
import type {GameState, PendingClaimState} from "../types/state";
import type {Tile} from "../types/tile.ts";
+export interface RoomPlayerUpdatePayload {
+ room_id?: string
+ status?: string
+ player_count?: number
+ player_ids?: string[]
+ players?: Array<{
+ Index?: number
+ index?: number
+ PlayerID?: string
+ player_id?: string
+ PlayerName?: string
+ player_name?: string
+ Ready?: boolean
+ ready?: boolean
+ MissingSuit?: string | null
+ missing_suit?: string | null
+ }>
+}
+
/**
* 游戏动作定义(只描述“发生了什么”)
@@ -52,4 +71,10 @@ export type GameAction =
playerId: string
action: 'peng' | 'gang' | 'hu' | 'pass'
}
-}
\ No newline at end of file
+}
+
+ // 房间玩家更新(等待房间人数变化)
+ | {
+ type: 'ROOM_PLAYER_UPDATE'
+ payload: RoomPlayerUpdatePayload
+}
diff --git a/src/game/dispatcher.ts b/src/game/dispatcher.ts
index dd32025..1571eec 100644
--- a/src/game/dispatcher.ts
+++ b/src/game/dispatcher.ts
@@ -29,5 +29,13 @@ export function dispatchGameAction(action: GameAction) {
case 'CLAIM_RESOLVED':
store.clearPendingClaim()
break
+
+ case 'ROOM_PLAYER_UPDATE':
+ store.onRoomPlayerUpdate(action.payload)
+ break
+
+
+ default:
+ throw new Error('Invalid game action')
}
-}
\ No newline at end of file
+}
diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts
index 986aa93..484862c 100644
--- a/src/store/gameStore.ts
+++ b/src/store/gameStore.ts
@@ -4,6 +4,7 @@ import {
type GameState,
type PendingClaimState,
} from '../types/state'
+import type { RoomPlayerUpdatePayload } from '../game/actions'
import type { Tile } from '../types/tile'
@@ -91,6 +92,94 @@ export const useGameStore = defineStore('game', {
this.phase = GAME_PHASE.ACTION
},
+ onRoomPlayerUpdate(payload: RoomPlayerUpdatePayload) {
+ if (typeof payload.room_id === 'string' && payload.room_id) {
+ this.roomId = payload.room_id
+ }
+
+ if (typeof payload.status === 'string' && payload.status) {
+ const phaseMap: Record = {
+ waiting: GAME_PHASE.WAITING,
+ dealing: GAME_PHASE.DEALING,
+ playing: GAME_PHASE.PLAYING,
+ action: GAME_PHASE.ACTION,
+ settlement: GAME_PHASE.SETTLEMENT,
+ }
+ this.phase = phaseMap[payload.status] ?? this.phase
+ }
+
+ const hasPlayerList =
+ Array.isArray(payload.players) || Array.isArray(payload.player_ids)
+ if (!hasPlayerList) {
+ return
+ }
+
+ const nextPlayers: GameState['players'] = {}
+ const players = Array.isArray(payload.players) ? payload.players : []
+ const playerIds = Array.isArray(payload.player_ids) ? payload.player_ids : []
+
+ players.forEach((raw, index) => {
+ const playerId =
+ (typeof raw.PlayerID === 'string' && raw.PlayerID) ||
+ (typeof raw.player_id === 'string' && raw.player_id) ||
+ playerIds[index]
+ if (!playerId) {
+ return
+ }
+
+ const previous = this.players[playerId]
+ const seatRaw = raw.Index ?? raw.index ?? index
+ const seatIndex =
+ typeof seatRaw === 'number' && Number.isFinite(seatRaw) ? seatRaw : index
+ const readyRaw = raw.Ready ?? raw.ready
+ const displayNameRaw = raw.PlayerName ?? raw.player_name
+ const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit
+
+ nextPlayers[playerId] = {
+ playerId,
+ seatIndex,
+ displayName:
+ typeof displayNameRaw === 'string' && displayNameRaw
+ ? displayNameRaw
+ : previous?.displayName,
+ missingSuit:
+ typeof missingSuitRaw === 'string' || missingSuitRaw === null
+ ? missingSuitRaw
+ : previous?.missingSuit,
+ handTiles: previous?.handTiles ?? [],
+ melds: previous?.melds ?? [],
+ discardTiles: previous?.discardTiles ?? [],
+ score: previous?.score ?? 0,
+ isReady:
+ typeof readyRaw === 'boolean'
+ ? readyRaw
+ : (previous?.isReady ?? false),
+ }
+ })
+
+ if (players.length === 0) {
+ playerIds.forEach((playerId, index) => {
+ if (typeof playerId !== 'string' || !playerId) {
+ return
+ }
+ const previous = this.players[playerId]
+ nextPlayers[playerId] = {
+ playerId,
+ seatIndex: previous?.seatIndex ?? index,
+ displayName: previous?.displayName ?? playerId,
+ missingSuit: previous?.missingSuit,
+ handTiles: previous?.handTiles ?? [],
+ melds: previous?.melds ?? [],
+ discardTiles: previous?.discardTiles ?? [],
+ score: previous?.score ?? 0,
+ isReady: previous?.isReady ?? false,
+ }
+ })
+ }
+
+ this.players = nextPlayers
+ },
+
// 清理操作窗口
clearPendingClaim() {
this.pendingClaim = undefined
@@ -102,4 +191,4 @@ export const useGameStore = defineStore('game', {
return Object.keys(this.players)[0] || ''
},
},
-})
\ No newline at end of file
+})
diff --git a/src/types/state/playerState.ts b/src/types/state/playerState.ts
index a596e02..dd347fb 100644
--- a/src/types/state/playerState.ts
+++ b/src/types/state/playerState.ts
@@ -4,6 +4,8 @@ import type { MeldState } from './meldState.ts'
export interface PlayerState {
playerId: string
seatIndex: number
+ displayName?: string
+ missingSuit?: string | null
// 手牌(只有自己有完整数据,后端可控制)
handTiles: Tile[]
diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue
index 967a46e..19b23ff 100644
--- a/src/views/ChengduGamePage.vue
+++ b/src/views/ChengduGamePage.vue
@@ -1,6 +1,6 @@