460 lines
10 KiB
Vue
460 lines
10 KiB
Vue
<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'
|
||
import type { RoomItem, RoomPlayer, RoomStatus } from '@/api/modules/game'
|
||
|
||
const props = defineProps<{
|
||
room: RoomItem
|
||
}>()
|
||
|
||
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>
|