feat(game): 添加玩家头像显示功能

- 在环境配置中更新代理目标地址
- 扩展游戏动作类型定义以支持头像URL字段
- 添加头像URL缓存计算逻辑以从多种来源获取头像
- 修改座位玩家卡片数据模型将avatar替换为avatarUrl
- 实现头像图片加载并添加默认头像回退机制
- 更新CSS样式以正确显示头像图片
- 重构游戏状态管理中的玩家头像数据处理
- 优化游戏页面中的头像分配逻辑
This commit is contained in:
2026-03-25 21:07:49 +08:00
parent 66834d8a7a
commit 6168117eb2
7 changed files with 77 additions and 22 deletions

View File

@@ -844,6 +844,13 @@ button:disabled {
box-shadow: box-shadow:
inset 0 2px 4px rgba(255, 255, 255, 0.18), inset 0 2px 4px rgba(255, 255, 255, 0.18),
0 6px 14px rgba(0, 0, 0, 0.22); 0 6px 14px rgba(0, 0, 0, 0.22);
overflow: hidden;
}
.avatar-card img {
width: 100%;
height: 100%;
object-fit: cover;
} }
.player-meta p { .player-meta p {

View File

@@ -3,6 +3,7 @@ import { computed } from 'vue'
import wanIcon from '../../assets/images/flowerClolor/wan.png' import wanIcon from '../../assets/images/flowerClolor/wan.png'
import tongIcon from '../../assets/images/flowerClolor/tong.png' import tongIcon from '../../assets/images/flowerClolor/tong.png'
import tiaoIcon from '../../assets/images/flowerClolor/tiao.png' import tiaoIcon from '../../assets/images/flowerClolor/tiao.png'
import defaultAvatarIcon from '../../assets/images/icons/avatar.svg'
import type { SeatPlayerCardModel } from './seat-player-card' import type { SeatPlayerCardModel } from './seat-player-card'
const props = defineProps<{ const props = defineProps<{
@@ -22,6 +23,10 @@ const missingSuitIcon = computed(() => {
} }
return '' return ''
}) })
const resolvedAvatarUrl = computed(() => {
return props.player.avatarUrl || defaultAvatarIcon
})
</script> </script>
<template> <template>
@@ -30,7 +35,9 @@ const missingSuitIcon = computed(() => {
:class="[seatClass, { 'is-turn': player.isTurn }]" :class="[seatClass, { 'is-turn': player.isTurn }]"
> >
<div class="avatar-panel"> <div class="avatar-panel">
<div class="avatar-card">{{ player.avatar }}</div> <div class="avatar-card">
<img :src="resolvedAvatarUrl" :alt="`${player.name}头像`" />
</div>
<span v-if="player.dealer" class="dealer-mark"></span> <span v-if="player.dealer" class="dealer-mark"></span>
</div> </div>

View File

@@ -1,6 +1,6 @@
// 玩家卡片展示模型用于座位UI渲染 // 玩家卡片展示模型用于座位UI渲染
export interface SeatPlayerCardModel { export interface SeatPlayerCardModel {
avatar: string // 头像 avatarUrl: string // 头像 URL
name: string // 显示名称 name: string // 显示名称
dealer: boolean // 是否庄家 dealer: boolean // 是否庄家
isTurn: boolean // 是否当前轮到该玩家 isTurn: boolean // 是否当前轮到该玩家

View File

@@ -13,6 +13,8 @@ export interface RoomPlayerUpdatePayload {
player_id?: string player_id?: string
PlayerName?: string PlayerName?: string
player_name?: string player_name?: string
AvatarUrl?: string
avatar_url?: string
Ready?: boolean Ready?: boolean
ready?: boolean ready?: boolean
MissingSuit?: string | null MissingSuit?: string | null

View File

@@ -133,6 +133,7 @@ export const useGameStore = defineStore('game', {
typeof seatRaw === 'number' && Number.isFinite(seatRaw) ? seatRaw : index typeof seatRaw === 'number' && Number.isFinite(seatRaw) ? seatRaw : index
const readyRaw = raw.Ready ?? raw.ready const readyRaw = raw.Ready ?? raw.ready
const displayNameRaw = raw.PlayerName ?? raw.player_name const displayNameRaw = raw.PlayerName ?? raw.player_name
const avatarUrlRaw = raw.AvatarUrl ?? raw.avatar_url
const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit const missingSuitRaw = raw.MissingSuit ?? raw.missing_suit
nextPlayers[playerId] = { nextPlayers[playerId] = {
@@ -142,6 +143,10 @@ export const useGameStore = defineStore('game', {
typeof displayNameRaw === 'string' && displayNameRaw typeof displayNameRaw === 'string' && displayNameRaw
? displayNameRaw ? displayNameRaw
: previous?.displayName, : previous?.displayName,
avatarURL:
typeof avatarUrlRaw === 'string'
? avatarUrlRaw
: previous?.avatarURL,
missingSuit: missingSuit:
typeof missingSuitRaw === 'string' || missingSuitRaw === null typeof missingSuitRaw === 'string' || missingSuitRaw === null
? missingSuitRaw ? missingSuitRaw
@@ -167,6 +172,7 @@ export const useGameStore = defineStore('game', {
playerId, playerId,
seatIndex: previous?.seatIndex ?? index, seatIndex: previous?.seatIndex ?? index,
displayName: previous?.displayName ?? playerId, displayName: previous?.displayName ?? playerId,
avatarURL: previous?.avatarURL,
missingSuit: previous?.missingSuit, missingSuit: previous?.missingSuit,
handTiles: previous?.handTiles ?? [], handTiles: previous?.handTiles ?? [],
melds: previous?.melds ?? [], melds: previous?.melds ?? [],

View File

@@ -3,6 +3,7 @@ import type { MeldState } from './meldState.ts'
export interface PlayerState { export interface PlayerState {
playerId: string playerId: string
avatarURL?: string
seatIndex: number seatIndex: number
displayName?: string displayName?: string
missingSuit?: string | null missingSuit?: string | null

View File

@@ -41,6 +41,8 @@ type DisplayPlayer = PlayerState & {
missingSuit?: string | null missingSuit?: string | null
} }
type GameActionPayload<TType extends GameAction['type']> = Extract<GameAction, { type: TType }>['payload']
interface SeatViewModel { interface SeatViewModel {
key: SeatKey key: SeatKey
player?: DisplayPlayer player?: DisplayPlayer
@@ -78,6 +80,31 @@ const loggedInUserName = computed(() => {
return auth.value?.user?.nickname || auth.value?.user?.username || '' return auth.value?.user?.nickname || auth.value?.user?.username || ''
}) })
const localCachedAvatarUrl = computed(() => {
const source = auth.value?.user as Record<string, unknown> | 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(() => { const myPlayer = computed(() => {
return gameStore.players[loggedInUserId.value] return gameStore.players[loggedInUserId.value]
}) })
@@ -206,8 +233,8 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1 const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
const defaultMissingSuitLabel = missingSuitLabel(null) const defaultMissingSuitLabel = missingSuitLabel(null)
const emptySeat = (avatar: string): SeatPlayerCardModel => ({ const emptySeat = (): SeatPlayerCardModel => ({
avatar, avatarUrl: '',
name: '空位', name: '空位',
dealer: false, dealer: false,
isTurn: false, isTurn: false,
@@ -215,20 +242,24 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
}) })
const result: Record<SeatKey, SeatPlayerCardModel> = { const result: Record<SeatKey, SeatPlayerCardModel> = {
top: emptySeat('1'), top: emptySeat(),
right: emptySeat('2'), right: emptySeat(),
bottom: emptySeat('我'), bottom: emptySeat(),
left: emptySeat('4'), left: emptySeat(),
} }
for (const [index, seat] of seatViews.value.entries()) { for (const seat of seatViews.value) {
if (!seat.player) { if (!seat.player) {
continue continue
} }
const displayName = seat.player.displayName || `玩家${seat.player.seatIndex + 1}` const displayName = seat.player.displayName || `玩家${seat.player.seatIndex + 1}`
const avatarUrl = seat.isSelf
? (localCachedAvatarUrl.value || seat.player.avatarURL || '')
: (seat.player.avatarURL || '')
result[seat.key] = { result[seat.key] = {
avatar: seat.isSelf ? '我' : String(index + 1), avatarUrl,
name: seat.isSelf ? '你自己' : displayName, name: seat.isSelf ? '你自己' : displayName,
dealer: seat.player.seatIndex === dealerIndex, dealer: seat.player.seatIndex === dealerIndex,
isTurn: seat.isTurn, isTurn: seat.isTurn,
@@ -334,42 +365,42 @@ function toGameAction(message: unknown): GameAction | null {
switch (type) { switch (type) {
case 'GAME_INIT': case 'GAME_INIT':
if (payload && typeof payload === 'object') { 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 return null
case 'GAME_START': case 'GAME_START':
if (payload && typeof payload === 'object') { 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 return null
case 'DRAW_TILE': case 'DRAW_TILE':
if (payload && typeof payload === 'object') { 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 return null
case 'PLAY_TILE': case 'PLAY_TILE':
if (payload && typeof payload === 'object') { 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 return null
case 'PENDING_CLAIM': case 'PENDING_CLAIM':
if (payload && typeof payload === 'object') { 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 return null
case 'CLAIM_RESOLVED': case 'CLAIM_RESOLVED':
if (payload && typeof payload === 'object') { 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 return null
case 'ROOM_PLAYER_UPDATE': case 'ROOM_PLAYER_UPDATE':
if (payload && typeof payload === 'object') { 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 return null
case 'ROOM_MEMBER_JOINED': case 'ROOM_MEMBER_JOINED':
if (payload && typeof payload === 'object') { 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 return null
default: default:
@@ -470,6 +501,7 @@ function hydrateFromActiveRoom(routeRoomId: string): void {
playerId: player.playerId, playerId: player.playerId,
seatIndex: player.index, seatIndex: player.index,
displayName: player.displayName || player.playerId, displayName: player.displayName || player.playerId,
avatarURL: previous?.avatarURL,
missingSuit: player.missingSuit ?? previous?.missingSuit, missingSuit: player.missingSuit ?? previous?.missingSuit,
isReady: player.ready, isReady: player.ready,
handTiles: previous?.handTiles ?? [], handTiles: previous?.handTiles ?? [],