```
feat(game): add player cards and topbar styling for Chengdu Mahjong game - Add new CSS classes for topbar layout including .topbar-left, .topbar-back-btn, .topbar-room-meta, .eyebrow, and .topbar-room-name - Create dedicated player card components for each seat position (top, right, bottom, left) - Refactor seatDecor computed property to use SeatPlayerCardModel interface with proper typing - Replace inline player badge rendering with reusable player card components - Update game header layout to use new topbar structure with back button and room metadata - Adjust spacing and font sizes in game header elements ```
This commit is contained in:
@@ -459,6 +459,39 @@ button:disabled {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-back-btn {
|
||||
flex: 0 0 auto;
|
||||
min-width: 108px;
|
||||
}
|
||||
|
||||
.topbar-room-meta {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
color: #f7e4b0;
|
||||
font-size: 12px;
|
||||
letter-spacing: 2px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.topbar-room-name {
|
||||
margin-top: 4px;
|
||||
color: #f6edd5;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.game-header h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 800;
|
||||
@@ -467,9 +500,9 @@ button:disabled {
|
||||
}
|
||||
|
||||
.game-header .sub-title {
|
||||
margin-top: 6px;
|
||||
margin-top: 4px;
|
||||
color: #d7eadf;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
@@ -1195,6 +1228,10 @@ button:disabled {
|
||||
justify-items: stretch;
|
||||
}
|
||||
|
||||
.topbar-left {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.topbar-center,
|
||||
.topbar-right {
|
||||
justify-content: flex-start;
|
||||
|
||||
12
src/components/game/BottomPlayerCard.vue
Normal file
12
src/components/game/BottomPlayerCard.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import SeatPlayerCard from './SeatPlayerCard.vue'
|
||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||
|
||||
defineProps<{
|
||||
player: SeatPlayerCardModel
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SeatPlayerCard seat-class="seat-bottom" :player="player" />
|
||||
</template>
|
||||
12
src/components/game/LeftPlayerCard.vue
Normal file
12
src/components/game/LeftPlayerCard.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import SeatPlayerCard from './SeatPlayerCard.vue'
|
||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||
|
||||
defineProps<{
|
||||
player: SeatPlayerCardModel
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SeatPlayerCard seat-class="seat-left" :player="player" />
|
||||
</template>
|
||||
12
src/components/game/RightPlayerCard.vue
Normal file
12
src/components/game/RightPlayerCard.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import SeatPlayerCard from './SeatPlayerCard.vue'
|
||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||
|
||||
defineProps<{
|
||||
player: SeatPlayerCardModel
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SeatPlayerCard seat-class="seat-right" :player="player" />
|
||||
</template>
|
||||
23
src/components/game/SeatPlayerCard.vue
Normal file
23
src/components/game/SeatPlayerCard.vue
Normal file
@@ -0,0 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||
|
||||
defineProps<{
|
||||
seatClass: string
|
||||
player: SeatPlayerCardModel
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<article
|
||||
class="player-badge"
|
||||
:class="[seatClass, { 'is-turn': player.isTurn, offline: !player.isOnline }]"
|
||||
>
|
||||
<div class="avatar-card">{{ player.avatar }}</div>
|
||||
<div class="player-meta">
|
||||
<p>{{ player.name }}</p>
|
||||
<strong>{{ player.money }}</strong>
|
||||
</div>
|
||||
<span v-if="player.dealer" class="dealer-mark">庄</span>
|
||||
<span class="missing-mark">{{ player.missingSuitLabel }}</span>
|
||||
</article>
|
||||
</template>
|
||||
12
src/components/game/TopPlayerCard.vue
Normal file
12
src/components/game/TopPlayerCard.vue
Normal file
@@ -0,0 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import SeatPlayerCard from './SeatPlayerCard.vue'
|
||||
import type { SeatPlayerCardModel } from './seat-player-card'
|
||||
|
||||
defineProps<{
|
||||
player: SeatPlayerCardModel
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<SeatPlayerCard seat-class="seat-top" :player="player" />
|
||||
</template>
|
||||
9
src/components/game/seat-player-card.ts
Normal file
9
src/components/game/seat-player-card.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface SeatPlayerCardModel {
|
||||
avatar: string
|
||||
name: string
|
||||
money: string
|
||||
dealer: boolean
|
||||
isTurn: boolean
|
||||
isOnline: boolean
|
||||
missingSuitLabel: string
|
||||
}
|
||||
@@ -6,9 +6,14 @@ import topBackImage from '../assets/images/tiles/top/tbgs_2.png'
|
||||
import rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
||||
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
||||
import leftBackImage from '../assets/images/tiles/left/tbgs_3.png'
|
||||
import TopPlayerCard from '../components/game/TopPlayerCard.vue'
|
||||
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
||||
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
||||
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
|
||||
import type { AuthSession } from '../api/authed-request'
|
||||
import { refreshAccessToken } from '../api/auth'
|
||||
import { getUserInfo } from '../api/user'
|
||||
import type { SeatPlayerCardModel } from '../components/game/seat-player-card'
|
||||
import {
|
||||
DEFAULT_MAX_PLAYERS,
|
||||
activeRoomState,
|
||||
@@ -213,25 +218,66 @@ const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
||||
}
|
||||
})
|
||||
|
||||
const seatDecor = computed(() => {
|
||||
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
const scoreMap = roomState.value.game?.state?.scores ?? {}
|
||||
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
|
||||
|
||||
return seatViews.value.map((seat, index) => {
|
||||
return seatViews.value.reduce(
|
||||
(acc, seat, index) => {
|
||||
const playerId = seat.player?.playerId ?? ''
|
||||
const score = playerId ? scoreMap[playerId] : undefined
|
||||
|
||||
return {
|
||||
seat: seat.key,
|
||||
avatar: seat.isSelf ? '我' : String(index + 1),
|
||||
name: seat.player ? (seat.isSelf ? '你' : playerId) : '空位',
|
||||
money: typeof score === 'number' ? `${score}` : '--',
|
||||
dealer: seat.player?.index === dealerIndex,
|
||||
isTurn: seat.isTurn,
|
||||
isOnline: Boolean(seat.player),
|
||||
missingSuit: null as string | null,
|
||||
}
|
||||
})
|
||||
acc[seat.key] = {
|
||||
avatar: seat.isSelf ? '我' : String(index + 1),
|
||||
name: seat.player ? (seat.isSelf ? '你' : playerId) : '空位',
|
||||
money: typeof score === 'number' ? `${score}` : '--',
|
||||
dealer: seat.player?.index === dealerIndex,
|
||||
isTurn: seat.isTurn,
|
||||
isOnline: Boolean(seat.player),
|
||||
missingSuitLabel: missingSuitLabel(null),
|
||||
}
|
||||
|
||||
return acc
|
||||
},
|
||||
{
|
||||
top: {
|
||||
avatar: '1',
|
||||
name: '空位',
|
||||
money: '--',
|
||||
dealer: false,
|
||||
isTurn: false,
|
||||
isOnline: false,
|
||||
missingSuitLabel: missingSuitLabel(null),
|
||||
},
|
||||
right: {
|
||||
avatar: '2',
|
||||
name: '空位',
|
||||
money: '--',
|
||||
dealer: false,
|
||||
isTurn: false,
|
||||
isOnline: false,
|
||||
missingSuitLabel: missingSuitLabel(null),
|
||||
},
|
||||
bottom: {
|
||||
avatar: '我',
|
||||
name: '空位',
|
||||
money: '--',
|
||||
dealer: false,
|
||||
isTurn: false,
|
||||
isOnline: false,
|
||||
missingSuitLabel: missingSuitLabel(null),
|
||||
},
|
||||
left: {
|
||||
avatar: '4',
|
||||
name: '空位',
|
||||
money: '--',
|
||||
dealer: false,
|
||||
isTurn: false,
|
||||
isOnline: false,
|
||||
missingSuitLabel: missingSuitLabel(null),
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
const centerTimer = computed(() => {
|
||||
@@ -905,14 +951,15 @@ onBeforeUnmount(() => {
|
||||
<template>
|
||||
<section class="hall-page game-page">
|
||||
<header class="hall-header game-header">
|
||||
<div>
|
||||
<h1>成都麻将对局</h1>
|
||||
<p v-if="loggedInUserName" class="sub-title">玩家:{{ loggedInUserName }}</p>
|
||||
</div>
|
||||
<div class="header-actions">
|
||||
<button class="ghost-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
|
||||
<div class="topbar-left">
|
||||
<button class="ghost-btn topbar-back-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
|
||||
{{ leaveRoomPending ? '退出中...' : '返回大厅' }}
|
||||
</button>
|
||||
<div class="topbar-room-meta">
|
||||
<p class="eyebrow">成都麻将对局</p>
|
||||
<p class="topbar-room-name">{{ roomState.name || roomName || '未命名房间' }}</p>
|
||||
<p v-if="loggedInUserName" class="sub-title">玩家:{{ loggedInUserName }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="topbar-center">
|
||||
@@ -972,20 +1019,10 @@ onBeforeUnmount(() => {
|
||||
<small>底注 6 番 · 封顶 32 倍</small>
|
||||
</div>
|
||||
|
||||
<article
|
||||
v-for="player in seatDecor"
|
||||
:key="player.seat"
|
||||
class="player-badge"
|
||||
:class="[`seat-${player.seat}`, { 'is-turn': player.isTurn, offline: !player.isOnline }]"
|
||||
>
|
||||
<div class="avatar-card">{{ player.avatar }}</div>
|
||||
<div class="player-meta">
|
||||
<p>{{ player.name }}</p>
|
||||
<strong>{{ player.money }}</strong>
|
||||
</div>
|
||||
<span v-if="player.dealer" class="dealer-mark">庄</span>
|
||||
<span class="missing-mark">{{ missingSuitLabel(player.missingSuit) }}</span>
|
||||
</article>
|
||||
<TopPlayerCard :player="seatDecor.top" />
|
||||
<RightPlayerCard :player="seatDecor.right" />
|
||||
<BottomPlayerCard :player="seatDecor.bottom" />
|
||||
<LeftPlayerCard :player="seatDecor.left" />
|
||||
|
||||
<div class="wall wall-top">
|
||||
<img v-for="key in wallBacks.top" :key="key" :src="getBackImage('top')" alt="" />
|
||||
|
||||
Reference in New Issue
Block a user