```
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;
|
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 {
|
.game-header h1 {
|
||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
@@ -467,9 +500,9 @@ button:disabled {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.game-header .sub-title {
|
.game-header .sub-title {
|
||||||
margin-top: 6px;
|
margin-top: 4px;
|
||||||
color: #d7eadf;
|
color: #d7eadf;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
@@ -1195,6 +1228,10 @@ button:disabled {
|
|||||||
justify-items: stretch;
|
justify-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.topbar-left {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
.topbar-center,
|
.topbar-center,
|
||||||
.topbar-right {
|
.topbar-right {
|
||||||
justify-content: flex-start;
|
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 rightBackImage from '../assets/images/tiles/right/tbgs_1.png'
|
||||||
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
import bottomBackImage from '../assets/images/tiles/bottom/tdbgs_4.png'
|
||||||
import leftBackImage from '../assets/images/tiles/left/tbgs_3.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 type { AuthSession } from '../api/authed-request'
|
||||||
import { refreshAccessToken } from '../api/auth'
|
import { refreshAccessToken } from '../api/auth'
|
||||||
import { getUserInfo } from '../api/user'
|
import { getUserInfo } from '../api/user'
|
||||||
|
import type { SeatPlayerCardModel } from '../components/game/seat-player-card'
|
||||||
import {
|
import {
|
||||||
DEFAULT_MAX_PLAYERS,
|
DEFAULT_MAX_PLAYERS,
|
||||||
activeRoomState,
|
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 scoreMap = roomState.value.game?.state?.scores ?? {}
|
||||||
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
|
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 playerId = seat.player?.playerId ?? ''
|
||||||
const score = playerId ? scoreMap[playerId] : undefined
|
const score = playerId ? scoreMap[playerId] : undefined
|
||||||
|
|
||||||
return {
|
acc[seat.key] = {
|
||||||
seat: seat.key,
|
|
||||||
avatar: seat.isSelf ? '我' : String(index + 1),
|
avatar: seat.isSelf ? '我' : String(index + 1),
|
||||||
name: seat.player ? (seat.isSelf ? '你' : playerId) : '空位',
|
name: seat.player ? (seat.isSelf ? '你' : playerId) : '空位',
|
||||||
money: typeof score === 'number' ? `${score}` : '--',
|
money: typeof score === 'number' ? `${score}` : '--',
|
||||||
dealer: seat.player?.index === dealerIndex,
|
dealer: seat.player?.index === dealerIndex,
|
||||||
isTurn: seat.isTurn,
|
isTurn: seat.isTurn,
|
||||||
isOnline: Boolean(seat.player),
|
isOnline: Boolean(seat.player),
|
||||||
missingSuit: null as string | null,
|
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(() => {
|
const centerTimer = computed(() => {
|
||||||
@@ -905,14 +951,15 @@ onBeforeUnmount(() => {
|
|||||||
<template>
|
<template>
|
||||||
<section class="hall-page game-page">
|
<section class="hall-page game-page">
|
||||||
<header class="hall-header game-header">
|
<header class="hall-header game-header">
|
||||||
<div>
|
<div class="topbar-left">
|
||||||
<h1>成都麻将对局</h1>
|
<button class="ghost-btn topbar-back-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
|
||||||
<p v-if="loggedInUserName" class="sub-title">玩家:{{ loggedInUserName }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="header-actions">
|
|
||||||
<button class="ghost-btn" type="button" :disabled="leaveRoomPending" @click="backHall">
|
|
||||||
{{ leaveRoomPending ? '退出中...' : '返回大厅' }}
|
{{ leaveRoomPending ? '退出中...' : '返回大厅' }}
|
||||||
</button>
|
</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>
|
||||||
|
|
||||||
<div class="topbar-center">
|
<div class="topbar-center">
|
||||||
@@ -972,20 +1019,10 @@ onBeforeUnmount(() => {
|
|||||||
<small>底注 6 番 · 封顶 32 倍</small>
|
<small>底注 6 番 · 封顶 32 倍</small>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<article
|
<TopPlayerCard :player="seatDecor.top" />
|
||||||
v-for="player in seatDecor"
|
<RightPlayerCard :player="seatDecor.right" />
|
||||||
:key="player.seat"
|
<BottomPlayerCard :player="seatDecor.bottom" />
|
||||||
class="player-badge"
|
<LeftPlayerCard :player="seatDecor.left" />
|
||||||
: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>
|
|
||||||
|
|
||||||
<div class="wall wall-top">
|
<div class="wall wall-top">
|
||||||
<img v-for="key in wallBacks.top" :key="key" :src="getBackImage('top')" alt="" />
|
<img v-for="key in wallBacks.top" :key="key" :src="getBackImage('top')" alt="" />
|
||||||
|
|||||||
Reference in New Issue
Block a user