feat(game): 添加玩家头像显示功能
- 在环境配置中更新代理目标地址 - 扩展游戏动作类型定义以支持头像URL字段 - 添加头像URL缓存计算逻辑以从多种来源获取头像 - 修改座位玩家卡片数据模型将avatar替换为avatarUrl - 实现头像图片加载并添加默认头像回退机制 - 更新CSS样式以正确显示头像图片 - 重构游戏状态管理中的玩家头像数据处理 - 优化游戏页面中的头像分配逻辑
This commit is contained in:
@@ -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 {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 // 是否当前轮到该玩家
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ?? [],
|
||||||
|
|||||||
Reference in New Issue
Block a user