feat: 游戏大厅
This commit is contained in:
489
src/components/baseLayouts/TableCard.vue
Normal file
489
src/components/baseLayouts/TableCard.vue
Normal 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>
|
||||
Reference in New Issue
Block a user