Files
webgame-frontend/src/components/baseLayouts/TableCard.vue
2026-06-18 13:10:09 +08:00

460 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>