From 6168117eb2bd37aef48ddf9eb4d0cd45eaf4eeb9 Mon Sep 17 00:00:00 2001 From: wsy182 <2392948297@qq.com> Date: Wed, 25 Mar 2026 21:07:49 +0800 Subject: [PATCH] =?UTF-8?q?feat(game):=20=E6=B7=BB=E5=8A=A0=E7=8E=A9?= =?UTF-8?q?=E5=AE=B6=E5=A4=B4=E5=83=8F=E6=98=BE=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在环境配置中更新代理目标地址 - 扩展游戏动作类型定义以支持头像URL字段 - 添加头像URL缓存计算逻辑以从多种来源获取头像 - 修改座位玩家卡片数据模型将avatar替换为avatarUrl - 实现头像图片加载并添加默认头像回退机制 - 更新CSS样式以正确显示头像图片 - 重构游戏状态管理中的玩家头像数据处理 - 优化游戏页面中的头像分配逻辑 --- src/assets/styles/global.css | 7 +++ src/components/game/SeatPlayerCard.vue | 11 ++++- src/components/game/seat-player-card.ts | 4 +- src/game/actions.ts | 2 + src/store/gameStore.ts | 6 +++ src/types/state/playerState.ts | 5 +- src/views/ChengduGamePage.vue | 64 ++++++++++++++++++------- 7 files changed, 77 insertions(+), 22 deletions(-) diff --git a/src/assets/styles/global.css b/src/assets/styles/global.css index e553a9d..0f384c7 100644 --- a/src/assets/styles/global.css +++ b/src/assets/styles/global.css @@ -844,6 +844,13 @@ button:disabled { box-shadow: inset 0 2px 4px rgba(255, 255, 255, 0.18), 0 6px 14px rgba(0, 0, 0, 0.22); + overflow: hidden; +} + +.avatar-card img { + width: 100%; + height: 100%; + object-fit: cover; } .player-meta p { diff --git a/src/components/game/SeatPlayerCard.vue b/src/components/game/SeatPlayerCard.vue index 848e7c9..3d4e552 100644 --- a/src/components/game/SeatPlayerCard.vue +++ b/src/components/game/SeatPlayerCard.vue @@ -3,6 +3,7 @@ import { computed } from 'vue' import wanIcon from '../../assets/images/flowerClolor/wan.png' import tongIcon from '../../assets/images/flowerClolor/tong.png' import tiaoIcon from '../../assets/images/flowerClolor/tiao.png' +import defaultAvatarIcon from '../../assets/images/icons/avatar.svg' import type { SeatPlayerCardModel } from './seat-player-card' const props = defineProps<{ @@ -22,6 +23,10 @@ const missingSuitIcon = computed(() => { } return '' }) + +const resolvedAvatarUrl = computed(() => { + return props.player.avatarUrl || defaultAvatarIcon +}) \ No newline at end of file + diff --git a/src/components/game/seat-player-card.ts b/src/components/game/seat-player-card.ts index 2f8971f..a77f893 100644 --- a/src/components/game/seat-player-card.ts +++ b/src/components/game/seat-player-card.ts @@ -1,8 +1,8 @@ // 玩家卡片展示模型(用于座位UI渲染) export interface SeatPlayerCardModel { - avatar: string // 头像 + avatarUrl: string // 头像 URL name: string // 显示名称 dealer: boolean // 是否庄家 isTurn: boolean // 是否当前轮到该玩家 missingSuitLabel: string // 定缺花色(万/筒/条) -} \ No newline at end of file +} diff --git a/src/game/actions.ts b/src/game/actions.ts index 8c03851..f061995 100644 --- a/src/game/actions.ts +++ b/src/game/actions.ts @@ -13,6 +13,8 @@ export interface RoomPlayerUpdatePayload { player_id?: string PlayerName?: string player_name?: string + AvatarUrl?: string + avatar_url?: string Ready?: boolean ready?: boolean MissingSuit?: string | null diff --git a/src/store/gameStore.ts b/src/store/gameStore.ts index 484862c..08f108d 100644 --- a/src/store/gameStore.ts +++ b/src/store/gameStore.ts @@ -133,6 +133,7 @@ export const useGameStore = defineStore('game', { typeof seatRaw === 'number' && Number.isFinite(seatRaw) ? seatRaw : index const readyRaw = raw.Ready ?? raw.ready const displayNameRaw = raw.PlayerName ?? raw.player_name + const avatarUrlRaw = raw.AvatarUrl ?? raw.avatar_url const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit nextPlayers[playerId] = { @@ -142,6 +143,10 @@ export const useGameStore = defineStore('game', { typeof displayNameRaw === 'string' && displayNameRaw ? displayNameRaw : previous?.displayName, + avatarURL: + typeof avatarUrlRaw === 'string' + ? avatarUrlRaw + : previous?.avatarURL, missingSuit: typeof missingSuitRaw === 'string' || missingSuitRaw === null ? missingSuitRaw @@ -167,6 +172,7 @@ export const useGameStore = defineStore('game', { playerId, seatIndex: previous?.seatIndex ?? index, displayName: previous?.displayName ?? playerId, + avatarURL: previous?.avatarURL, missingSuit: previous?.missingSuit, handTiles: previous?.handTiles ?? [], melds: previous?.melds ?? [], diff --git a/src/types/state/playerState.ts b/src/types/state/playerState.ts index dd347fb..54791b9 100644 --- a/src/types/state/playerState.ts +++ b/src/types/state/playerState.ts @@ -1,8 +1,9 @@ -import type { Tile } from '../tile' -import type { MeldState } from './meldState.ts' +import type {Tile} from '../tile' +import type {MeldState} from './meldState.ts' export interface PlayerState { playerId: string + avatarURL?: string seatIndex: number displayName?: string missingSuit?: string | null diff --git a/src/views/ChengduGamePage.vue b/src/views/ChengduGamePage.vue index 19b23ff..4a96f8b 100644 --- a/src/views/ChengduGamePage.vue +++ b/src/views/ChengduGamePage.vue @@ -41,6 +41,8 @@ type DisplayPlayer = PlayerState & { missingSuit?: string | null } +type GameActionPayload = Extract['payload'] + interface SeatViewModel { key: SeatKey player?: DisplayPlayer @@ -78,6 +80,31 @@ const loggedInUserName = computed(() => { return auth.value?.user?.nickname || auth.value?.user?.username || '' }) +const localCachedAvatarUrl = computed(() => { + const source = auth.value?.user as Record | undefined + if (!source) { + return '' + } + + const avatarCandidates = [ + source.avatar, + source.avatar_url, + source.avatarUrl, + source.head_img, + source.headImg, + source.profile_image, + source.profileImage, + ] + + for (const candidate of avatarCandidates) { + if (typeof candidate === 'string' && candidate.trim()) { + return candidate + } + } + + return '' +}) + const myPlayer = computed(() => { return gameStore.players[loggedInUserId.value] }) @@ -206,8 +233,8 @@ const seatDecor = computed>(() => { const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1 const defaultMissingSuitLabel = missingSuitLabel(null) - const emptySeat = (avatar: string): SeatPlayerCardModel => ({ - avatar, + const emptySeat = (): SeatPlayerCardModel => ({ + avatarUrl: '', name: '空位', dealer: false, isTurn: false, @@ -215,20 +242,24 @@ const seatDecor = computed>(() => { }) const result: Record = { - top: emptySeat('1'), - right: emptySeat('2'), - bottom: emptySeat('我'), - left: emptySeat('4'), + top: emptySeat(), + right: emptySeat(), + bottom: emptySeat(), + left: emptySeat(), } - for (const [index, seat] of seatViews.value.entries()) { + for (const seat of seatViews.value) { if (!seat.player) { continue } const displayName = seat.player.displayName || `玩家${seat.player.seatIndex + 1}` + const avatarUrl = seat.isSelf + ? (localCachedAvatarUrl.value || seat.player.avatarURL || '') + : (seat.player.avatarURL || '') + result[seat.key] = { - avatar: seat.isSelf ? '我' : String(index + 1), + avatarUrl, name: seat.isSelf ? '你自己' : displayName, dealer: seat.player.seatIndex === dealerIndex, isTurn: seat.isTurn, @@ -334,42 +365,42 @@ function toGameAction(message: unknown): GameAction | null { switch (type) { case 'GAME_INIT': if (payload && typeof payload === 'object') { - return {type: 'GAME_INIT', payload: payload as GameAction['payload']} + return {type: 'GAME_INIT', payload: payload as GameActionPayload<'GAME_INIT'>} } return null case 'GAME_START': if (payload && typeof payload === 'object') { - return {type: 'GAME_START', payload: payload as GameAction['payload']} + return {type: 'GAME_START', payload: payload as GameActionPayload<'GAME_START'>} } return null case 'DRAW_TILE': if (payload && typeof payload === 'object') { - return {type: 'DRAW_TILE', payload: payload as GameAction['payload']} + return {type: 'DRAW_TILE', payload: payload as GameActionPayload<'DRAW_TILE'>} } return null case 'PLAY_TILE': if (payload && typeof payload === 'object') { - return {type: 'PLAY_TILE', payload: payload as GameAction['payload']} + return {type: 'PLAY_TILE', payload: payload as GameActionPayload<'PLAY_TILE'>} } return null case 'PENDING_CLAIM': if (payload && typeof payload === 'object') { - return {type: 'PENDING_CLAIM', payload: payload as GameAction['payload']} + return {type: 'PENDING_CLAIM', payload: payload as GameActionPayload<'PENDING_CLAIM'>} } return null case 'CLAIM_RESOLVED': if (payload && typeof payload === 'object') { - return {type: 'CLAIM_RESOLVED', payload: payload as GameAction['payload']} + return {type: 'CLAIM_RESOLVED', payload: payload as GameActionPayload<'CLAIM_RESOLVED'>} } return null case 'ROOM_PLAYER_UPDATE': if (payload && typeof payload === 'object') { - return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameAction['payload']} + return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameActionPayload<'ROOM_PLAYER_UPDATE'>} } return null case 'ROOM_MEMBER_JOINED': if (payload && typeof payload === 'object') { - return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameAction['payload']} + return {type: 'ROOM_PLAYER_UPDATE', payload: payload as GameActionPayload<'ROOM_PLAYER_UPDATE'>} } return null default: @@ -470,6 +501,7 @@ function hydrateFromActiveRoom(routeRoomId: string): void { playerId: player.playerId, seatIndex: player.index, displayName: player.displayName || player.playerId, + avatarURL: previous?.avatarURL, missingSuit: player.missingSuit ?? previous?.missingSuit, isReady: player.ready, handTiles: previous?.handTiles ?? [],