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 @@