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

4
components.d.ts vendored
View File

@@ -24,8 +24,10 @@ declare module 'vue' {
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default'] GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink'] RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView'] RouterView: typeof import('vue-router')['RouterView']
TableCard: typeof import('./src/components/baseLayouts/TableCard.vue')['default']
UserBar: typeof import('./src/components/baseLayouts/UserBar.vue')['default'] UserBar: typeof import('./src/components/baseLayouts/UserBar.vue')['default']
} }
} }
@@ -44,7 +46,9 @@ declare global {
const ElPopover: typeof import('element-plus/es')['ElPopover'] const ElPopover: typeof import('element-plus/es')['ElPopover']
const ElTooltip: typeof import('element-plus/es')['ElTooltip'] const ElTooltip: typeof import('element-plus/es')['ElTooltip']
const GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default'] const GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
const Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']
const RouterLink: typeof import('vue-router')['RouterLink'] const RouterLink: typeof import('vue-router')['RouterLink']
const RouterView: typeof import('vue-router')['RouterView'] const RouterView: typeof import('vue-router')['RouterView']
const TableCard: typeof import('./src/components/baseLayouts/TableCard.vue')['default']
const UserBar: typeof import('./src/components/baseLayouts/UserBar.vue')['default'] const UserBar: typeof import('./src/components/baseLayouts/UserBar.vue')['default']
} }

View File

@@ -54,46 +54,32 @@ interface GameInfo {
/** 游戏列表数据 */ /** 游戏列表数据 */
const games: GameInfo[] = [ const games: GameInfo[] = [
{ {
id: 'basketball', id: 'snake',
icon: '🏀', icon: '🐍',
name: '篮球对决', name: '贪吃蛇',
description: '实时篮球对战游戏,与全球玩家同场竞技', description: '经典贪吃蛇竞技,在狭小空间内争夺生存权',
gameplay: '使用方向键移动球员空格键投篮。每场比赛5分钟得分多者获胜。支持1v1、3v3模式。' gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。'
}, },
{ {
id: 'football', id: 'tetris',
icon: '', icon: '🧊',
name: '足球竞技', name: '俄罗斯方块',
description: '热血足球比赛,组建你的梦之队', description: '经典下落消除,与对手一较高下',
gameplay: 'WASD控制球员移动J键传球K键射门。11人制比赛配合战术赢得胜利。' gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。'
}, },
{ {
id: 'racing', id: 'gomoku',
icon: '🏎️',
name: '极速赛车',
description: '极限竞速,漂移过弯超越对手',
gameplay: '方向键控制方向Shift键加速漂移。多种赛道可选挑战最快圈速记录。'
},
{
id: 'chess',
icon: '♟️',
name: '策略棋牌',
description: '经典棋牌合集,运筹帷幄决胜千里',
gameplay: '包含围棋、象棋、五子棋等多种棋类。支持人机对战和在线匹配两种模式。'
},
{
id: 'shooting',
icon: '🎯', icon: '🎯',
name: '射击战场', name: '五子棋',
description: '团队战术射击,配合队友夺取胜利', description: '黑白对弈,先连成五子者胜',
gameplay: '鼠标瞄准左键射击R键换弹。多种武器和地图5v5团队竞技模式。' gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。'
}, },
{ {
id: 'arena', id: 'guess-number',
icon: '🏆', icon: '🔢',
name: '竞技场', name: '猜数字',
description: '综合竞技平台,参与锦标赛赢取奖励', description: '逻辑推理对战,猜中对方的秘密数字',
gameplay: '每日举办多场锦标赛,涵盖各类型游戏。积累积分提升段位,赢取丰厚奖励。' gameplay: '每轮输入猜测的数字组合,系统会提示位置和数字正确的数量。率先猜中对手数字组合的玩家获胜。'
} }
] ]

View File

@@ -0,0 +1,449 @@
<template>
<div class="lobby">
<!-- 顶部操作栏 -->
<div class="lobby__toolbar">
<div class="lobby__filters">
<button
v-for="f in statusFilters"
:key="f.key"
class="lobby__filter-btn"
:class="{ 'lobby__filter-btn--active': activeFilter === f.key }"
@click="activeFilter = f.key"
>
{{ f.label }}
</button>
</div>
<button class="lobby__create-btn" @click="handleCreateRoom">
<span class="lobby__create-icon">+</span>
创建房间
</button>
</div>
<!-- 桌子网格 -->
<div class="lobby__grid">
<TableCard
v-for="room in filteredRooms"
:key="room.id"
:room="room"
@join="handleJoin"
@join-seat="handleJoinSeat"
@spectate="handleSpectate"
/>
</div>
<!-- 空状态提示 -->
<div v-if="filteredRooms.length === 0" class="lobby__empty">
<span class="lobby__empty-icon">🪑</span>
<p class="lobby__empty-text">暂无符合条件的房间</p>
<p class="lobby__empty-hint">换个筛选条件或者自己创建一个房间吧</p>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import TableCard from './TableCard.vue'
import type { Room } from './TableCard.vue'
import { useBaseLayoutStore } from '@/stores'
import { storeToRefs } from 'pinia'
/** 状态筛选选项 */
const statusFilters = [
{ key: 'all', label: '全部' },
{ key: 'waiting', label: '等待中' },
{ key: 'playing', label: '进行中' },
]
const activeFilter = ref('all')
const baseLayoutStore = useBaseLayoutStore()
const { activeGameId } = storeToRefs(baseLayoutStore)
/** 切换游戏时重置筛选 */
watch(activeGameId, () => {
activeFilter.value = 'all'
})
/** 根据名字生成头像 URL */
function avatarUrl(name: string): string {
return `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${encodeURIComponent(name)}`
}
/**
* 各游戏的模拟房间数据
* key 对应 GameList 中的 gameId
*/
const ALL_ROOMS: Record<string, Room[]> = {
/* ---- 贪吃蛇 ---- */
snake: [
{
id: 'snake-001', name: '桌 01', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('小龙'), nickname: '小龙' },
{ avatar: avatarUrl('阿蟒'), nickname: '阿蟒' },
],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '经典模式',
creatorName: '小龙',
},
{
id: 'snake-002', name: '桌 02', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('闪电'), nickname: '闪电' },
{ avatar: avatarUrl('疾风'), nickname: '疾风' },
{ avatar: avatarUrl('游侠'), nickname: '游侠' },
{ avatar: avatarUrl('幻影'), nickname: '幻影' },
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '道具模式',
creatorName: '闪电',
},
{
id: 'snake-003', name: '桌 03', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('青蛇'), nickname: '青蛇' },
],
status: 'waiting', statusText: '等待中', stakes: 200, mode: '加速模式',
creatorName: '青蛇',
},
{
id: 'snake-004', name: '桌 04', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4,
players: [],
status: 'waiting', statusText: '等待中', stakes: 100, mode: '经典模式',
creatorName: '毒牙',
},
{
id: 'snake-005', name: '桌 05', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('巨蟒'), nickname: '巨蟒' },
{ avatar: avatarUrl('灵蛇'), nickname: '灵蛇' },
{ avatar: avatarUrl('毒牙'), nickname: '毒牙' },
],
status: 'waiting', statusText: '等待中', stakes: 400, mode: '道具模式',
creatorName: '巨蟒',
},
],
/* ---- 俄罗斯方块 ---- */
tetris: [
{
id: 'tetris-001', name: '桌 01', gameIcon: '🧊', gameId: 'tetris',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('方块大师'), nickname: '方块大师' },
],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '1v1 对战',
creatorName: '方块大师',
},
{
id: 'tetris-002', name: '桌 02', gameIcon: '🧊', gameId: 'tetris',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('极速下落'), nickname: '极速下落' },
{ avatar: avatarUrl('消行高手'), nickname: '消行高手' },
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '竞速模式',
creatorName: '极速下落',
},
{
id: 'tetris-003', name: '桌 03', gameIcon: '🧊', gameId: 'tetris',
maxPlayers: 2,
players: [],
status: 'waiting', statusText: '等待中', stakes: 150, mode: '1v1 对战',
creatorName: '堆叠王者',
},
{
id: 'tetris-004', name: '桌 04', gameIcon: '🧊', gameId: 'tetris',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('T旋大师'), nickname: 'T旋大师' },
],
status: 'waiting', statusText: '等待中', stakes: 200, mode: '1v1 对战',
creatorName: 'T旋大师',
},
{
id: 'tetris-005', name: '桌 05', gameIcon: '🧊', gameId: 'tetris',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('连击王'), nickname: '连击王' },
{ avatar: avatarUrl('消除者'), nickname: '消除者' },
],
status: 'playing', statusText: '进行中', stakes: 400, mode: '竞速模式',
creatorName: '连击王',
},
],
/* ---- 五子棋 ---- */
gomoku: [
{
id: 'gomoku-001', name: '桌 01', gameIcon: '🎯', gameId: 'gomoku',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('棋圣'), nickname: '棋圣' },
],
status: 'waiting', statusText: '等待中', stakes: 200, mode: '无禁手',
creatorName: '棋圣',
},
{
id: 'gomoku-002', name: '桌 02', gameIcon: '🎯', gameId: 'gomoku',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('黑白子'), nickname: '黑白子' },
{ avatar: avatarUrl('五连珠'), nickname: '五连珠' },
],
status: 'playing', statusText: '进行中', stakes: 400, mode: '有禁手',
creatorName: '黑白子',
},
{
id: 'gomoku-003', name: '桌 03', gameIcon: '🎯', gameId: 'gomoku',
maxPlayers: 2,
players: [],
status: 'waiting', statusText: '等待中', stakes: 100, mode: '无禁手',
creatorName: '落子无悔',
},
{
id: 'gomoku-004', name: '桌 04', gameIcon: '🎯', gameId: 'gomoku',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('天元'), nickname: '天元' },
],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '有禁手',
creatorName: '天元',
},
{
id: 'gomoku-005', name: '桌 05', gameIcon: '🎯', gameId: 'gomoku',
maxPlayers: 2,
players: [
{ avatar: avatarUrl('先手必胜'), nickname: '先手必胜' },
{ avatar: avatarUrl('后手无敌'), nickname: '后手无敌' },
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '有禁手',
creatorName: '先手必胜',
},
],
/* ---- 猜数字 ---- */
'guess-number': [
{
id: 'guess-001', name: '桌 01', gameIcon: '🔢', gameId: 'guess-number',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('逻辑怪'), nickname: '逻辑怪' },
{ avatar: avatarUrl('推理王'), nickname: '推理王' },
],
status: 'waiting', statusText: '等待中', stakes: 200, mode: '经典模式',
creatorName: '逻辑怪',
},
{
id: 'guess-002', name: '桌 02', gameIcon: '🔢', gameId: 'guess-number',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('秒猜'), nickname: '秒猜' },
{ avatar: avatarUrl('心算'), nickname: '心算' },
{ avatar: avatarUrl('直觉'), nickname: '直觉' },
],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '限时模式',
creatorName: '秒猜',
},
{
id: 'guess-003', name: '桌 03', gameIcon: '🔢', gameId: 'guess-number',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('数字猎人'), nickname: '数字猎人' },
],
status: 'waiting', statusText: '等待中', stakes: 150, mode: '经典模式',
creatorName: '数字猎人',
},
{
id: 'guess-004', name: '桌 04', gameIcon: '🔢', gameId: 'guess-number',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('密码破解'), nickname: '密码破解' },
{ avatar: avatarUrl('枚举大师'), nickname: '枚举大师' },
{ avatar: avatarUrl('二分法'), nickname: '二分法' },
{ avatar: avatarUrl('穷举王'), nickname: '穷举王' },
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '限时模式',
creatorName: '密码破解',
},
{
id: 'guess-005', name: '桌 05', gameIcon: '🔢', gameId: 'guess-number',
maxPlayers: 4,
players: [],
status: 'waiting', statusText: '等待中', stakes: 100, mode: '经典模式',
creatorName: '神秘数字',
},
],
}
/** 当前游戏的房间列表 */
const rooms = computed(() => ALL_ROOMS[activeGameId.value] ?? [])
/** 筛选后的房间列表 */
const filteredRooms = computed(() => {
const list = rooms.value
if (activeFilter.value === 'all') return list
if (activeFilter.value === 'waiting') return list.filter((r) => r.status === 'waiting')
if (activeFilter.value === 'playing') return list.filter((r) => r.status === 'playing')
return list
})
/** 加入房间 */
function handleJoin(roomId: string) {
// eslint-disable-next-line no-console
console.log('加入房间:', roomId)
}
/** 加入指定座位 */
function handleJoinSeat(roomId: string, seatIndex: number) {
// eslint-disable-next-line no-console
console.log('加入房间:', roomId, '座位:', seatIndex)
}
/** 观战 */
function handleSpectate(roomId: string) {
// eslint-disable-next-line no-console
console.log('观战房间:', roomId)
}
/** 创建房间 */
function handleCreateRoom() {
// eslint-disable-next-line no-console
console.log('创建房间')
}
</script>
<style scoped lang="scss">
.lobby {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
padding: var(--space-md);
gap: var(--space-md);
/* 深色底板模拟地板,让白色卡片"浮"在上面 */
background:
radial-gradient(ellipse at 50% 0%, rgba(79, 70, 229, 0.03) 0%, transparent 60%),
linear-gradient(180deg, #e8ecf1 0%, #e2e6ed 100%);
overflow-y: auto;
}
/* ===== 顶部操作栏 ===== */
.lobby__toolbar {
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
}
.lobby__filters {
display: flex;
gap: 4px;
padding: 4px;
background-color: var(--bg-surface);
border-radius: var(--radius-sm);
border: 1px solid var(--border-default);
}
.lobby__filter-btn {
padding: 6px 16px;
border: none;
border-radius: 4px;
background: transparent;
color: var(--text-secondary);
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition:
background-color 0.15s ease,
color 0.15s ease;
&:hover {
color: var(--text-primary);
background-color: var(--bg-surface-hover);
}
&--active {
color: var(--text-inverse);
background-color: var(--color-primary);
box-shadow: var(--shadow-sm);
&:hover {
color: var(--text-inverse);
background-color: var(--color-primary-hover);
}
}
}
.lobby__create-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 18px;
border: none;
border-radius: var(--radius-sm);
background-color: var(--color-primary);
color: var(--text-inverse);
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition:
background-color 0.15s ease,
transform 0.15s ease;
&:hover {
background-color: var(--color-primary-hover);
transform: scale(1.03);
}
&:active {
transform: scale(0.97);
}
}
.lobby__create-icon {
font-size: 16px;
font-weight: 400;
line-height: 1;
}
/* ===== 桌子网格 ===== */
.lobby__grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(252px, 1fr));
gap: var(--space-md);
justify-items: center;
align-content: start;
}
/* ===== 空状态 ===== */
.lobby__empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
gap: 8px;
color: var(--text-tertiary);
}
.lobby__empty-icon {
font-size: 56px;
opacity: 0.5;
margin-bottom: 8px;
}
.lobby__empty-text {
font-size: 15px;
font-weight: 500;
color: var(--text-secondary);
margin: 0;
}
.lobby__empty-hint {
font-size: 13px;
margin: 0;
}
</style>

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>

View File

@@ -27,7 +27,9 @@
</div> </div>
<!-- 主内容区域 --> <!-- 主内容区域 -->
<div class="base-layout__content"></div> <div class="base-layout__content">
<Lobby />
</div>
</div> </div>
</div> </div>
</template> </template>
@@ -35,6 +37,7 @@
<script setup lang="ts"> <script setup lang="ts">
import UserBar from '@/components/baseLayouts/UserBar.vue' import UserBar from '@/components/baseLayouts/UserBar.vue'
import GameList from '@/components/baseLayouts/GameList.vue' import GameList from '@/components/baseLayouts/GameList.vue'
import Lobby from '@/components/baseLayouts/Lobby.vue'
import { useBaseLayoutStore } from '@/stores' import { useBaseLayoutStore } from '@/stores'
import { storeToRefs } from 'pinia' import { storeToRefs } from 'pinia'
@@ -95,7 +98,8 @@ const { activeGameIcon, activeGameName } = storeToRefs(baseLayoutStore)
display: flex; display: flex;
flex: 1; flex: 1;
width: 100%; width: 100%;
height: 100%; min-height: 0; /* flex 子元素必须设置,允许收缩到内容以下 */
overflow: hidden;
} }
.base-layout__sidebar { .base-layout__sidebar {
@@ -157,21 +161,11 @@ const { activeGameIcon, activeGameName } = storeToRefs(baseLayoutStore)
right: 0; right: 0;
cursor: ew-resize; cursor: ew-resize;
z-index: 10; z-index: 10;
/* 默认微弱可见,提示可拖拽 */ background: transparent;
background: linear-gradient(
90deg,
transparent 0%,
transparent 40%,
var(--border-default) 40%,
var(--border-default) 60%,
transparent 60%,
transparent 100%
);
transition: background-color 0.2s ease; transition: background-color 0.2s ease;
&:hover { &:hover {
background-color: var(--color-primary-light); background-color: var(--color-primary-light);
background: none;
} }
&:active { &:active {
@@ -183,7 +177,8 @@ const { activeGameIcon, activeGameName } = storeToRefs(baseLayoutStore)
display: flex; display: flex;
flex: 1; flex: 1;
width: 100%; width: 100%;
height: 100%; min-height: 0; /* flex 子元素必须设置,允许收缩 */
overflow: hidden;
background-color: var(--bg-body); background-color: var(--bg-body);
} }
</style> </style>

View File

@@ -7,11 +7,11 @@ export const useBaseLayoutStore = defineStore(
/** 顶部导航栏标题 */ /** 顶部导航栏标题 */
const topbarTitle = ref<string>('') const topbarTitle = ref<string>('')
/** 当前选中的游戏 ID */ /** 当前选中的游戏 ID */
const activeGameId = ref<string>('basketball') const activeGameId = ref<string>('snake')
/** 当前选中的游戏图标 */ /** 当前选中的游戏图标 */
const activeGameIcon = ref<string>('🏀') const activeGameIcon = ref<string>('🐍')
/** 当前选中的游戏名称 */ /** 当前选中的游戏名称 */
const activeGameName = ref<string>('篮球对决') const activeGameName = ref<string>('贪吃蛇')
/** /**
* 设置顶部导航栏标题 * 设置顶部导航栏标题