From 0fa3c4f1dfaaf82ba7248cc40e86ee584c54169f Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Thu, 26 Mar 2026 17:18:29 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E6=B8=B8?= =?UTF-8?q?=E6=88=8F=E5=87=86=E5=A4=87=E7=8A=B6=E6=80=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 SeatPlayerCard 组件中添加 isReady 属性用于显示准备状态 - 添加准备/取消准备按钮,支持玩家切换准备状态 - 实现 WebSocket 消息处理以同步玩家准备状态 - 添加 CSS 样式显示准备状态标签和准备按钮 - 优化用户 ID 解析逻辑,支持多种字段格式 - 修复座位索引计算逻辑,确保相对位置正确显示 - 添加认证会话管理功能,确保用户信息同步加载 - 实现房间玩家状态更新的消息处理机制 --- src/assets/images/icons/cancel.png | Bin 0 -> 11563 bytes src/assets/images/icons/read.png | Bin 0 -> 11304 bytes src/assets/styles/room.css | 67 +++++++ src/components/game/SeatPlayerCard.vue | 1 + src/components/game/seat-player-card.ts | 1 + src/views/ChengduGamePage.vue | 232 +++++++++++++++++++++++- 6 files changed, 292 insertions(+), 9 deletions(-) create mode 100755 src/assets/images/icons/cancel.png create mode 100755 src/assets/images/icons/read.png diff --git a/src/assets/images/icons/cancel.png b/src/assets/images/icons/cancel.png new file mode 100755 index 0000000000000000000000000000000000000000..f4bfb58e0c060cd3f30ec5ec67bae39892c31966 GIT binary patch literal 11563 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 literal 0 HcmV?d00001 diff --git a/src/assets/images/icons/read.png b/src/assets/images/icons/read.png new file mode 100755 index 0000000000000000000000000000000000000000..95a0b92740df6a0e89fe0714851fec3b3de70382 GIT binary patch literal 11304 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 literal 0 HcmV?d00001 diff --git a/src/assets/styles/room.css b/src/assets/styles/room.css index d86828c..5f512f3 100644 --- a/src/assets/styles/room.css +++ b/src/assets/styles/room.css @@ -429,6 +429,22 @@ color: #eef5ff; } +.picture-scene .player-meta .ready-chip { + display: inline-flex; + align-items: center; + justify-content: center; + margin-top: 4px; + padding: 1px 8px; + border-radius: 999px; + color: #f7f2dd; + font-size: 10px; + font-weight: 700; + background: linear-gradient(180deg, rgba(214, 173, 58, 0.82), rgba(125, 91, 20, 0.92)); + box-shadow: + inset 0 1px 0 rgba(255, 255, 255, 0.22), + 0 2px 6px rgba(0, 0, 0, 0.2); +} + .picture-scene .player-badge.seat-right .player-meta, .picture-scene .player-badge.seat-left .player-meta { display: flex; @@ -575,6 +591,57 @@ pointer-events: none; } +.ready-toggle { + position: absolute; + right: 120px; + bottom: 70px; + display: inline-flex; + align-items: center; + justify-content: center; + gap: 8px; + min-width: 138px; + height: 44px; + padding: 0 16px 0 14px; + border: 1px solid rgba(220, 191, 118, 0.24); + border-radius: 999px; + background: + linear-gradient(180deg, rgba(14, 55, 40, 0.92), rgba(8, 36, 27, 0.96)), + radial-gradient(circle at 20% 24%, rgba(237, 214, 157, 0.08), transparent 34%); + box-shadow: + inset 0 1px 0 rgba(255, 244, 214, 0.1), + inset 0 -1px 0 rgba(0, 0, 0, 0.22), + 0 8px 18px rgba(0, 0, 0, 0.2); + z-index: 4; + animation: ready-toggle-pop 180ms ease-out; +} + +.ready-toggle-label { + color: #e5c472; + font-size: 14px; + font-weight: 800; + letter-spacing: 0.5px; + text-shadow: + -1px 0 rgba(0, 0, 0, 0.38), + 0 1px rgba(0, 0, 0, 0.38), + 1px 0 rgba(0, 0, 0, 0.38), + 0 -1px rgba(0, 0, 0, 0.38); +} + +.ready-toggle:active { + transform: translateY(1px) scale(0.96); +} + +@keyframes ready-toggle-pop { + from { + opacity: 0; + transform: translateY(10px) scale(0.94); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + .center-desk { position: absolute; left: 50%; diff --git a/src/components/game/SeatPlayerCard.vue b/src/components/game/SeatPlayerCard.vue index 3d4e552..627fc4f 100644 --- a/src/components/game/SeatPlayerCard.vue +++ b/src/components/game/SeatPlayerCard.vue @@ -43,6 +43,7 @@ const resolvedAvatarUrl = computed(() => {

{{ player.name }}

+ 已准备
diff --git a/src/components/game/seat-player-card.ts b/src/components/game/seat-player-card.ts index a77f893..6be38c6 100644 --- a/src/components/game/seat-player-card.ts +++ b/src/components/game/seat-player-card.ts @@ -4,5 +4,6 @@ export interface SeatPlayerCardModel { name: string // 显示名称 dealer: boolean // 是否庄家 isTurn: boolean // 是否当前轮到该玩家 + isReady: boolean // 是否已准备 missingSuitLabel: string // 定缺花色(万/筒/条) } diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index adf2bf8..9c07271 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -23,9 +23,11 @@ import westWind from '../assets/images/direction/xi.png' import northWind from '../assets/images/direction/bei.png' import type {SeatPlayerCardModel} from '../components/game/seat-player-card' import type {SeatKey} from '../game/seat' -import type {GameAction} from '../game/actions' +import type {GameAction, RoomPlayerUpdatePayload} from '../game/actions' import {dispatchGameAction} from '../game/dispatcher' import {refreshAccessToken} from '../api/auth' +import {AuthExpiredError, type AuthSession} from '../api/authed-request' +import {getUserInfo} from '../api/user' import {clearAuth, readStoredAuth, writeStoredAuth} from '../utils/auth-storage' import type {WsStatus} from '../ws/client' import {wsClient} from '../ws/client' @@ -62,6 +64,7 @@ const wsMessages = ref([]) const wsError = ref('') const selectedTile = ref(null) const leaveRoomPending = ref(false) +const readyTogglePending = ref(false) let clockTimer: number | null = null let unsubscribe: (() => void) | null = null @@ -74,8 +77,13 @@ let refreshingWsToken = false let lastForcedRefreshAt = 0 const loggedInUserId = computed(() => { - const rawId = auth.value?.user?.id - if (typeof rawId === 'string') { + const source = auth.value?.user as Record | undefined + const rawId = + source?.id ?? + source?.userID ?? + source?.user_id + + if (typeof rawId === 'string' && rawId.trim()) { return rawId } if (typeof rawId === 'number') { @@ -170,7 +178,7 @@ const seatViews = computed(() => { const currentTurn = gameStore.currentTurn return players.slice(0, 4).map((player) => { - const relativeIndex = (player.seatIndex - selfSeatIndex + 4) % 4 + const relativeIndex = (selfSeatIndex - player.seatIndex + 4) % 4 const seatKey = tableOrder[relativeIndex] ?? 'top' return { key: seatKey, @@ -195,7 +203,7 @@ const seatWinds = computed>(() => { } for (let absoluteSeat = 0; absoluteSeat < 4; absoluteSeat += 1) { - const relativeIndex = (absoluteSeat - selfSeatIndex + 4) % 4 + const relativeIndex = (selfSeatIndex - absoluteSeat + 4) % 4 const seatKey = tableOrder[relativeIndex] ?? 'top' result[seatKey] = directionBySeatIndex[absoluteSeat] ?? northWind } @@ -226,6 +234,56 @@ const roomStatusText = computed(() => { return map[status] ?? status ?? '--' }) +const myReadyState = computed(() => { + return Boolean(myPlayer.value?.isReady) +}) + +const showReadyToggle = computed(() => { + return gameStore.phase === 'waiting' && Boolean(gameStore.roomId) +}) + +function applyPlayerReadyState(playerId: string, ready: boolean): void { + const player = gameStore.players[playerId] + if (player) { + player.isReady = ready + } + + const room = activeRoom.value + if (!room || room.roomId !== gameStore.roomId) { + return + } + + const roomPlayer = room.players.find((item) => item.playerId === playerId) + if (roomPlayer) { + roomPlayer.ready = ready + } +} + +function syncReadyStatesFromRoomUpdate(payload: RoomPlayerUpdatePayload): void { + if (!Array.isArray(payload.players)) { + return + } + + for (const item of payload.players) { + const playerId = + (typeof item.PlayerID === 'string' && item.PlayerID) || + (typeof item.player_id === 'string' && item.player_id) || + '' + const ready = + typeof item.Ready === 'boolean' + ? item.Ready + : typeof item.ready === 'boolean' + ? item.ready + : undefined + + if (!playerId || typeof ready !== 'boolean') { + continue + } + + applyPlayerReadyState(playerId, ready) + } +} + const networkLabel = computed(() => { const map: Record = { connected: '已连接', @@ -268,6 +326,7 @@ const seatDecor = computed>(() => { name: '空位', dealer: false, isTurn: false, + isReady: false, missingSuitLabel: defaultMissingSuitLabel, }) @@ -294,6 +353,7 @@ const seatDecor = computed>(() => { name: Array.from(seat.isSelf ? selfDisplayName : displayName).slice(0, 4).join(''), dealer: seat.player.seatIndex === dealerIndex, isTurn: seat.isTurn, + isReady: Boolean(seat.player.isReady), missingSuitLabel: missingSuitLabel(seat.player.missingSuit), } } @@ -439,6 +499,54 @@ function toGameAction(message: unknown): GameAction | null { } } +function handleReadyStateResponse(message: unknown): void { + if (!message || typeof message !== 'object') { + return + } + + const source = message as Record + if (typeof source.type !== 'string') { + return + } + + const type = source.type.replace(/[-\s]/g, '_').toUpperCase() + if (type !== 'SET_READY') { + return + } + + const payload = source.payload + if (!payload || typeof payload !== 'object') { + return + } + + const readyPayload = payload as Record + const roomId = + typeof readyPayload.room_id === 'string' + ? readyPayload.room_id + : typeof source.roomId === 'string' + ? source.roomId + : '' + const userId = + typeof readyPayload.user_id === 'string' + ? readyPayload.user_id + : typeof source.target === 'string' + ? source.target + : '' + const ready = readyPayload.ready + + if (roomId && roomId !== gameStore.roomId) { + return + } + + if (typeof ready === 'boolean' && userId) { + applyPlayerReadyState(userId, ready) + } + + if (userId && userId === loggedInUserId.value) { + readyTogglePending.value = false + } +} + function logoutToLogin(): void { clearAuth() auth.value = null @@ -446,6 +554,74 @@ function logoutToLogin(): void { void router.replace('/login') } +function currentSession(): AuthSession | null { + const current = auth.value + if (!current?.token) { + return null + } + + return { + token: current.token, + tokenType: current.tokenType, + refreshToken: current.refreshToken, + expiresIn: current.expiresIn, + } +} + +function syncAuthSession(next: AuthSession): void { + if (!auth.value) { + return + } + + auth.value = { + ...auth.value, + token: next.token, + tokenType: next.tokenType ?? auth.value.tokenType, + refreshToken: next.refreshToken ?? auth.value.refreshToken, + expiresIn: next.expiresIn, + } + writeStoredAuth(auth.value) +} + +async function ensureCurrentUserLoaded(): Promise { + if (loggedInUserId.value) { + return + } + + const currentAuth = auth.value + const session = currentSession() + if (!session) { + return + } + + try { + const userInfo = await getUserInfo(session, syncAuthSession) + const resolvedId = userInfo.userID ?? userInfo.user_id ?? userInfo.id ?? currentAuth?.user?.id + const nextUser = { + ...(currentAuth?.user ?? {}), + ...userInfo, + id: + typeof resolvedId === 'string' || typeof resolvedId === 'number' + ? resolvedId + : undefined, + } + + if (!currentAuth) { + return + } + + auth.value = { + ...currentAuth, + user: nextUser, + } + writeStoredAuth(auth.value) + } catch (error) { + if (error instanceof AuthExpiredError) { + logoutToLogin() + } + } +} + function decodeJwtExpMs(token: string): number | null { const parts = token.split('.') const payloadPart = parts[1] @@ -557,6 +733,27 @@ function backHall(): void { }) } +function toggleReadyState(): void { + if (readyTogglePending.value) { + return + } + + const nextReady = !myReadyState.value + readyTogglePending.value = true + console.log('[ready-toggle]', { + loggedInUserId: loggedInUserId.value, + myReadyState: myReadyState.value, + nextReady, + }) + sendWsMessage({ + type: 'set_ready', + roomId: gameStore.roomId, + payload: { + ready: nextReady, + }, + }) +} + function handleLeaveRoom(): void { menuOpen.value = false backHall() @@ -627,10 +824,12 @@ function hydrateFromActiveRoom(routeRoomId: string): void { onMounted(() => { const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : '' - hydrateFromActiveRoom(routeRoomId) - if (routeRoomId) { - gameStore.roomId = routeRoomId - } + void ensureCurrentUserLoaded().finally(() => { + hydrateFromActiveRoom(routeRoomId) + if (routeRoomId) { + gameStore.roomId = routeRoomId + } + }) const handler = (status: WsStatus) => { wsStatus.value = status @@ -639,9 +838,14 @@ onMounted(() => { wsClient.onMessage((msg: unknown) => { const text = typeof msg === 'string' ? msg : JSON.stringify(msg) wsMessages.value.push(`[server] ${text}`) + handleReadyStateResponse(msg) const gameAction = toGameAction(msg) if (gameAction) { dispatchGameAction(gameAction) + if (gameAction.type === 'ROOM_PLAYER_UPDATE') { + syncReadyStatesFromRoomUpdate(gameAction.payload) + readyTogglePending.value = false + } } }) wsClient.onError((message: string) => { @@ -796,6 +1000,16 @@ onBeforeUnmount(() => { + +