feat(game): 添加玩家头像显示功能
- 在环境配置中更新代理目标地址 - 扩展游戏动作类型定义以支持头像URL字段 - 添加头像URL缓存计算逻辑以从多种来源获取头像 - 修改座位玩家卡片数据模型将avatar替换为avatarUrl - 实现头像图片加载并添加默认头像回退机制 - 更新CSS样式以正确显示头像图片 - 重构游戏状态管理中的玩家头像数据处理 - 优化游戏页面中的头像分配逻辑
This commit is contained in:
@@ -41,6 +41,8 @@ type DisplayPlayer = PlayerState & {
|
||||
missingSuit?: string | null
|
||||
}
|
||||
|
||||
type GameActionPayload<TType extends GameAction['type']> = Extract<GameAction, { type: TType }>['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<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(() => {
|
||||
return gameStore.players[loggedInUserId.value]
|
||||
})
|
||||
@@ -206,8 +233,8 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
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<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
})
|
||||
|
||||
const result: Record<SeatKey, SeatPlayerCardModel> = {
|
||||
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 ?? [],
|
||||
|
||||
Reference in New Issue
Block a user