feat: 游戏大厅

This commit is contained in:
2026-06-18 12:20:11 +08:00
parent dd8157b723
commit a2057da8fa
6 changed files with 973 additions and 50 deletions

View File

@@ -0,0 +1,489 @@
<template>
<div
class="table-card"
:class="{
'table-card--playing': room.status === 'playing',
'table-card--full': room.players.length >= room.maxPlayers
}"
@click="handleClick"
>
<!-- 桌子+椅子可视区域 -->
<div class="table-card__stage">
<!-- 椅子以圆形环绕桌子排布 -->
<div
v-for="(pos, idx) in chairPositions"
:key="idx"
class="table-card__chair"
:class="{
'table-card__chair--occupied': idx < room.players.length,
'table-card__chair--empty': idx >= room.players.length
}"
:style="{ top: pos.top, left: pos.left }"
@click.stop="handleChairClick(idx)"
>
<!-- 有人头像 + tooltip -->
<el-tooltip
v-if="room.players[idx]"
:content="chairTooltip(idx)"
placement="top"
:show-after="400"
>
<div class="table-card__chair-seat">
<img
:src="room.players[idx].avatar"
:alt="room.players[idx].nickname"
class="table-card__chair-avatar"
/>
</div>
</el-tooltip>
<!-- 空椅子 -->
<div v-else class="table-card__chair-seat">
<span class="table-card__chair-plus"></span>
</div>
<!-- 房主皇冠标识 -->
<span v-if="isHost(idx)" class="table-card__chair-crown" title="房主">👑</span>
</div>
<!-- 圆桌 -->
<div class="table-card__table">
<span class="table-card__table-icon">{{ room.gameIcon }}</span>
<span class="table-card__table-name">{{ room.name }}</span>
</div>
</div>
<!-- 底部信息区 -->
<div class="table-card__info">
<div class="table-card__info-row">
<span class="table-card__player-count">
<span class="table-card__dot" :class="statusDotClass"></span>
{{ room.players.length }}/{{ room.maxPlayers }}
</span>
<span class="table-card__status" :class="statusLabelClass">{{ room.statusText }}</span>
</div>
<div class="table-card__info-row table-card__info-row--secondary">
<span>底注 {{ room.stakes }}</span>
<span>{{ room.mode }}</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
/** 房间内玩家 */
interface RoomPlayer {
avatar: string
nickname: string
}
/** 房间数据 */
export interface Room {
id: string
/** 桌号显示名,如 "桌 01" */
name: string
/** 游戏图标 emoji */
gameIcon: string
/** 游戏 ID */
gameId: string
/** 最大玩家数 */
maxPlayers: number
/** 已入座玩家列表 */
players: RoomPlayer[]
/** 房间状态 */
status: 'waiting' | 'playing' | 'finished'
/** 状态展示文字 */
statusText: string
/** 底注金额 */
stakes: number
/** 玩法模式标签 */
mode: string
/** 房主昵称 */
creatorName: string
}
const props = defineProps<{
room: Room
}>()
const emit = defineEmits<{
(e: 'join', roomId: string): void
(e: 'join-seat', roomId: string, seatIndex: number): void
(e: 'spectate', roomId: string): void
}>()
/** 舞台中心坐标px */
const STAGE_CENTER_X = 124 // 卡片宽度 248px 的一半
const STAGE_CENTER_Y = 105 // 舞台高度 210px 的一半
/** 椅子环绕半径px */
const ORBIT_RADIUS = 76
/**
* 按角度环绕计算椅子位置
* 角度从顶部12 点钟方向)开始,顺时针递增
* @returns 各椅子 {top, left} 像素值数组
*/
const chairPositions = computed(() => {
const n = props.room.maxPlayers
const positions: Array<{ top: string; left: string }> = []
for (let i = 0; i < n; i++) {
// 从顶部开始,顺时针均分
const angleDeg = (360 / n) * i
const angleRad = (angleDeg * Math.PI) / 180
const left = STAGE_CENTER_X + ORBIT_RADIUS * Math.sin(angleRad)
const top = STAGE_CENTER_Y - ORBIT_RADIUS * Math.cos(angleRad)
positions.push({
left: `${left}px`,
top: `${top}px`,
})
}
return positions
})
/** 状态指示点样式 */
const statusDotClass = computed(() => {
switch (props.room.status) {
case 'waiting':
return 'table-card__dot--waiting'
case 'playing':
return 'table-card__dot--playing'
case 'finished':
return 'table-card__dot--finished'
default:
return 'table-card__dot--waiting'
}
})
/** 状态标签样式 */
const statusLabelClass = computed(() => {
switch (props.room.status) {
case 'waiting':
return 'table-card__status--waiting'
case 'playing':
return 'table-card__status--playing'
default:
return ''
}
})
/** 点击卡片 → 加入或观战 */
function handleClick() {
if (props.room.status === 'playing') {
emit('spectate', props.room.id)
} else {
emit('join', props.room.id)
}
}
/** 点击空椅子 → 指定座位加入 */
function handleChairClick(idx: number) {
if (idx >= props.room.players.length && props.room.status === 'waiting') {
emit('join-seat', props.room.id, idx)
}
}
/** 判断指定座位是否为房主 */
function isHost(idx: number): boolean {
const player = props.room.players[idx]
return !!player && player.nickname === props.room.creatorName
}
/** 椅子 hover 提示文字 */
function chairTooltip(idx: number): string {
const player = props.room.players[idx]
if (!player) return ''
return player.nickname === props.room.creatorName
? `${player.nickname}(房主)`
: player.nickname
}
</script>
<style scoped lang="scss">
/* ===== 卡片容器 ===== */
.table-card {
position: relative;
display: flex;
flex-direction: column;
width: 248px;
background-color: var(--bg-surface);
border-radius: var(--radius-md);
border: 1px solid var(--border-default);
box-shadow: var(--shadow-sm);
cursor: pointer;
overflow: hidden;
transition:
transform 0.2s ease,
box-shadow 0.2s ease;
user-select: none;
&:hover {
transform: translateY(-3px);
box-shadow: var(--shadow-md);
}
/* 进行中的房间 — 暖色左边条 */
&--playing {
border-left: 3px solid var(--color-warning);
}
/* 满员 — 降低饱和度 */
&--full {
opacity: 0.6;
&:hover {
opacity: 0.82;
}
}
}
/* ===== 舞台区域 ===== */
.table-card__stage {
position: relative;
width: 100%;
height: 210px;
flex-shrink: 0;
/* 中心微光,突出桌子 */
background: radial-gradient(
ellipse 65% 55% at 50% 50%,
rgba(79, 70, 229, 0.028) 0%,
transparent 100%
);
}
/* ===== 椅子(圆形环绕) ===== */
.table-card__chair {
position: absolute;
width: 32px;
height: 32px;
transform: translate(-50%, -50%);
z-index: 2;
}
.table-card__chair-seat {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
border-radius: 50%;
transition:
border-color 0.2s ease,
background-color 0.2s ease,
transform 0.2s ease,
box-shadow 0.2s ease;
}
/* ---- 空椅子 ---- */
.table-card__chair--empty .table-card__chair-seat {
border: 2px dashed var(--border-default);
background-color: transparent;
}
.table-card__chair-plus {
/* 使用 CSS 伪元素画十字,保证几何居中 */
position: relative;
width: 12px;
height: 12px;
&::before,
&::after {
content: '';
position: absolute;
background-color: var(--text-tertiary);
border-radius: 1px;
transition: background-color 0.2s ease;
}
/* 横线 */
&::before {
top: 50%;
left: 0;
width: 100%;
height: 2px;
transform: translateY(-50%);
}
/* 竖线 */
&::after {
top: 0;
left: 50%;
width: 2px;
height: 100%;
transform: translateX(-50%);
}
}
.table-card__chair--empty:hover .table-card__chair-seat {
transform: scale(1.25);
border-style: solid;
border-color: var(--color-primary);
background-color: var(--color-primary-light);
box-shadow: 0 0 0 5px rgba(79, 70, 229, 0.1);
}
.table-card__chair--empty:hover .table-card__chair-plus {
&::before,
&::after {
background-color: var(--color-primary);
}
}
/* ---- 有人椅子 ---- */
.table-card__chair--occupied .table-card__chair-seat {
border: 2px solid var(--color-primary-light);
background-color: var(--bg-surface);
overflow: hidden;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
}
.table-card__chair-avatar {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
/* ---- 房主皇冠 ---- */
.table-card__chair-crown {
position: absolute;
bottom: -4px;
right: -4px;
z-index: 5;
display: flex;
align-items: center;
justify-content: center;
width: 15px;
height: 15px;
font-size: 9px;
line-height: 1;
background-color: var(--bg-surface);
border-radius: 50%;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.18);
pointer-events: none;
}
/* ===== 圆桌(居中) ===== */
.table-card__table {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1px;
width: 76px;
height: 76px;
border-radius: 50%;
/* 木质桌面 */
background: linear-gradient(
145deg,
#e8d5b0 0%,
#d4a574 40%,
#c49060 100%
);
box-shadow:
0 3px 12px rgba(0, 0, 0, 0.15),
inset 0 1px 2px rgba(255, 255, 255, 0.4);
}
.table-card__table-icon {
font-size: 26px;
line-height: 1;
filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.12));
}
.table-card__table-name {
font-size: 10px;
font-weight: 700;
color: rgba(255, 255, 255, 0.85);
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.25);
letter-spacing: 0.3px;
}
/* ===== 底部信息区 ===== */
.table-card__info {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 14px 12px;
border-top: 1px solid var(--border-light);
}
.table-card__info-row {
display: flex;
align-items: center;
justify-content: space-between;
&--secondary {
font-size: 12px;
color: var(--text-tertiary);
}
}
.table-card__player-count {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: var(--text-primary);
}
/* ---- 状态指示点 ---- */
.table-card__dot {
width: 7px;
height: 7px;
border-radius: 50%;
flex-shrink: 0;
&--waiting {
background-color: var(--color-success);
}
&--playing {
background-color: var(--color-warning);
animation: dot-pulse 2s ease-in-out infinite;
}
&--finished {
background-color: var(--text-tertiary);
}
}
/* ---- 状态标签 ---- */
.table-card__status {
font-size: 11px;
font-weight: 500;
padding: 2px 8px;
border-radius: 10px;
&--waiting {
color: var(--color-success);
background-color: var(--color-success-light);
}
&--playing {
color: var(--color-warning);
background-color: var(--color-warning-light);
}
}
@keyframes dot-pulse {
0%,
100% {
opacity: 1;
}
50% {
opacity: 0.3;
}
}
</style>