feat: 数据mock

This commit is contained in:
2026-06-22 09:28:15 +08:00
parent 676f4d5e2d
commit 0b7a89c06b
10 changed files with 613 additions and 73 deletions

8
components.d.ts vendored
View File

@@ -12,16 +12,20 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
CreateRoomDialog: typeof import('./src/components/baseLayouts/CreateRoomDialog.vue')['default']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElBadge: typeof import('element-plus/es')['ElBadge']
ElButton: typeof import('element-plus/es')['ElButton']
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputOtp: typeof import('element-plus/es')['ElInputOtp']
ElOption: typeof import('element-plus/es')['ElOption']
ElPopover: typeof import('element-plus/es')['ElPopover']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElTooltip: typeof import('element-plus/es')['ElTooltip']
GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']
@@ -34,16 +38,20 @@ declare module 'vue' {
// For TSX support
declare global {
const CreateRoomDialog: typeof import('./src/components/baseLayouts/CreateRoomDialog.vue')['default']
const ElAvatar: typeof import('element-plus/es')['ElAvatar']
const ElBadge: typeof import('element-plus/es')['ElBadge']
const ElButton: typeof import('element-plus/es')['ElButton']
const ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
const ElDialog: typeof import('element-plus/es')['ElDialog']
const ElForm: typeof import('element-plus/es')['ElForm']
const ElFormItem: typeof import('element-plus/es')['ElFormItem']
const ElIcon: typeof import('element-plus/es')['ElIcon']
const ElInput: typeof import('element-plus/es')['ElInput']
const ElInputOtp: typeof import('element-plus/es')['ElInputOtp']
const ElOption: typeof import('element-plus/es')['ElOption']
const ElPopover: typeof import('element-plus/es')['ElPopover']
const ElSelect: typeof import('element-plus/es')['ElSelect']
const ElTooltip: typeof import('element-plus/es')['ElTooltip']
const GameList: typeof import('./src/components/baseLayouts/GameList.vue')['default']
const Lobby: typeof import('./src/components/baseLayouts/Lobby.vue')['default']

View File

@@ -7,27 +7,47 @@ export const mockGameList: GameItem[] = [
icon: '🐍',
name: '贪吃蛇',
description: '经典贪吃蛇竞技,在狭小空间内争夺生存权',
gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。'
gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。',
config: {
maxPlayers: 4,
modes: ['经典模式', '道具模式', '加速模式'],
stakesOptions: [100, 200, 300, 400, 500]
}
},
{
id: 'tetris',
icon: '🧊',
name: '俄罗斯方块',
description: '经典下落消除,与对手一较高下',
gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。'
gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。',
config: {
maxPlayers: 2,
modes: ['1v1对战', '竞速模式'],
stakesOptions: [150, 200, 300, 400, 500]
}
},
{
id: 'gomoku',
icon: '⚫',
name: '五子棋',
description: '黑白对弈,先连成五子者胜',
gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。'
gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。',
config: {
maxPlayers: 2,
modes: ['无禁手', '有禁手'],
stakesOptions: [100, 200, 300, 400, 500]
}
},
{
id: 'guess-number',
icon: '🔢',
name: '猜数字',
description: '逻辑推理对战,猜中对方的秘密数字',
gameplay: '每轮输入猜测的数字组合,系统会提示位置和数字正确的数量。率先猜中对手数字组合的玩家获胜。'
gameplay: '每轮输入猜测的数字组合,系统会提示位置和数字正确的数量。率先猜中对手数字组合的玩家获胜。',
config: {
maxPlayers: 4,
modes: ['经典模式', '限时模式'],
stakesOptions: [100, 150, 200, 300, 500]
}
}
]

View File

@@ -10,134 +10,287 @@ export const mockRoomList: Record<string, RoomItem[]> = {
/* ---- 贪吃蛇 ---- */
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',
id: 'snake-001',
name: '桌 01',
gameIcon: '🐍',
gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('闪电'), nickname: '闪电' }, { avatar: avatarUrl('疾风'), nickname: '疾风' },
{ avatar: avatarUrl('游侠'), nickname: '游侠' }, { avatar: avatarUrl('幻影'), nickname: '幻影' }
{ avatar: avatarUrl('小龙'), nickname: '小龙' },
{ avatar: avatarUrl('阿蟒'), nickname: '阿蟒' }
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '道具模式', creatorName: '闪电'
status: 'waiting',
statusText: '等待中',
stakes: 300,
mode: '经典模式',
creatorName: '小龙'
},
{
id: 'snake-003', name: '桌 03', gameIcon: '🐍', gameId: 'snake',
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: '青蛇'
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-004',
name: '桌 04',
gameIcon: '🐍',
gameId: 'snake',
maxPlayers: 4,
players: [],
status: 'waiting',
statusText: '等待中',
stakes: 100,
mode: '经典模式',
creatorName: '毒牙'
},
{
id: 'snake-005', name: '桌 05', gameIcon: '🐍', gameId: 'snake',
id: 'snake-005',
name: '桌 05',
gameIcon: '🐍',
gameId: 'snake',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('巨蟒'), nickname: '巨蟒' }, { avatar: avatarUrl('灵蛇'), nickname: '灵蛇' },
{ avatar: avatarUrl('巨蟒'), nickname: '巨蟒' },
{ avatar: avatarUrl('灵蛇'), nickname: '灵蛇' },
{ avatar: avatarUrl('毒牙'), nickname: '毒牙' }
],
status: 'waiting', statusText: '等待中', stakes: 400, mode: '道具模式', creatorName: '巨蟒'
status: 'waiting',
statusText: '等待中',
stakes: 400,
mode: '道具模式',
creatorName: '巨蟒'
}
],
/* ---- 俄罗斯方块 ---- */
tetris: [
{
id: 'tetris-001', name: '桌 01', gameIcon: '🧊', gameId: 'tetris',
id: 'tetris-001',
name: '桌 01',
gameIcon: '🧊',
gameId: 'tetris',
maxPlayers: 2,
players: [{ avatar: avatarUrl('方块大师'), nickname: '方块大师' }],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '1v1 对战', creatorName: '方块大师'
status: 'waiting',
statusText: '等待中',
stakes: 300,
mode: '1v1 对战',
creatorName: '方块大师'
},
{
id: 'tetris-002', name: '桌 02', gameIcon: '🧊', gameId: 'tetris',
id: 'tetris-002',
name: '桌 02',
gameIcon: '🧊',
gameId: 'tetris',
maxPlayers: 2,
players: [{ avatar: avatarUrl('极速下落'), nickname: '极速下落' }, { avatar: avatarUrl('消行高手'), nickname: '消行高手' }],
status: 'playing', statusText: '进行中', stakes: 500, mode: '竞速模式', creatorName: '极速下落'
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-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',
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旋大师'
status: 'waiting',
statusText: '等待中',
stakes: 200,
mode: '1v1 对战',
creatorName: 'T旋大师'
},
{
id: 'tetris-005', name: '桌 05', gameIcon: '🧊', gameId: 'tetris',
id: 'tetris-005',
name: '桌 05',
gameIcon: '🧊',
gameId: 'tetris',
maxPlayers: 2,
players: [{ avatar: avatarUrl('连击王'), nickname: '连击王' }, { avatar: avatarUrl('消除者'), nickname: '消除者' }],
status: 'playing', statusText: '进行中', stakes: 400, mode: '竞速模式', creatorName: '连击王'
players: [
{ avatar: avatarUrl('连击王'), nickname: '连击王' },
{ avatar: avatarUrl('消除者'), nickname: '消除者' }
],
status: 'playing',
statusText: '进行中',
stakes: 400,
mode: '竞速模式',
creatorName: '连击王'
}
],
/* ---- 五子棋 ---- */
gomoku: [
{
id: 'gomoku-001', name: '桌 01', gameIcon: '⚫', gameId: 'gomoku',
id: 'gomoku-001',
name: '桌 01',
gameIcon: '⚫',
gameId: 'gomoku',
maxPlayers: 2,
players: [{ avatar: avatarUrl('棋圣'), nickname: '棋圣' }],
status: 'waiting', statusText: '等待中', stakes: 200, mode: '无禁手', creatorName: '棋圣'
status: 'waiting',
statusText: '等待中',
stakes: 200,
mode: '无禁手',
creatorName: '棋圣'
},
{
id: 'gomoku-002', name: '桌 02', gameIcon: '⚫', gameId: 'gomoku',
id: 'gomoku-002',
name: '桌 02',
gameIcon: '⚫',
gameId: 'gomoku',
maxPlayers: 2,
players: [{ avatar: avatarUrl('黑白子'), nickname: '黑白子' }, { avatar: avatarUrl('五连珠'), nickname: '五连珠' }],
status: 'playing', statusText: '进行中', stakes: 400, mode: '有禁手', creatorName: '黑白子'
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-003',
name: '桌 03',
gameIcon: '',
gameId: 'gomoku',
maxPlayers: 2,
players: [],
status: 'waiting',
statusText: '等待中',
stakes: 100,
mode: '无禁手',
creatorName: '落子无悔'
},
{
id: 'gomoku-004', name: '桌 04', gameIcon: '⚫', gameId: 'gomoku',
id: 'gomoku-004',
name: '桌 04',
gameIcon: '⚫',
gameId: 'gomoku',
maxPlayers: 2,
players: [{ avatar: avatarUrl('天元'), nickname: '天元' }],
status: 'waiting', statusText: '等待中', stakes: 300, mode: '有禁手', creatorName: '天元'
status: 'waiting',
statusText: '等待中',
stakes: 300,
mode: '有禁手',
creatorName: '天元'
},
{
id: 'gomoku-005', name: '桌 05', gameIcon: '⚫', gameId: 'gomoku',
id: 'gomoku-005',
name: '桌 05',
gameIcon: '⚫',
gameId: 'gomoku',
maxPlayers: 2,
players: [{ avatar: avatarUrl('先手必胜'), nickname: '先手必胜' }, { avatar: avatarUrl('后手无敌'), nickname: '后手无敌' }],
status: 'playing', statusText: '进行中', stakes: 500, mode: '有禁手', creatorName: '先手必胜'
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',
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: '逻辑怪'
players: [
{ avatar: avatarUrl('逻辑怪'), nickname: '逻辑怪' },
{ avatar: avatarUrl('推理王'), nickname: '推理王' }
],
status: 'waiting',
statusText: '等待中',
stakes: 200,
mode: '经典模式',
creatorName: '逻辑怪'
},
{
id: 'guess-002', name: '桌 02', gameIcon: '🔢', gameId: 'guess-number',
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: '秒猜'
status: 'waiting',
statusText: '等待中',
stakes: 300,
mode: '限时模式',
creatorName: '秒猜'
},
{
id: 'guess-003', name: '桌 03', gameIcon: '🔢', gameId: 'guess-number',
id: 'guess-003',
name: '桌 03',
gameIcon: '🔢',
gameId: 'guess-number',
maxPlayers: 4,
players: [{ avatar: avatarUrl('数字猎人'), nickname: '数字猎人' }],
status: 'waiting', statusText: '等待中', stakes: 150, mode: '经典模式', creatorName: '数字猎人'
status: 'waiting',
statusText: '等待中',
stakes: 150,
mode: '经典模式',
creatorName: '数字猎人'
},
{
id: 'guess-004', name: '桌 04', gameIcon: '🔢', gameId: 'guess-number',
id: 'guess-004',
name: '桌 04',
gameIcon: '🔢',
gameId: 'guess-number',
maxPlayers: 4,
players: [
{ avatar: avatarUrl('密码破解'), nickname: '密码破解' },
@@ -145,12 +298,68 @@ export const mockRoomList: Record<string, RoomItem[]> = {
{ avatar: avatarUrl('二分法'), nickname: '二分法' },
{ avatar: avatarUrl('穷举王'), nickname: '穷举王' }
],
status: 'playing', statusText: '进行中', stakes: 500, mode: '限时模式', creatorName: '密码破解'
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: '神秘数字'
id: 'guess-005',
name: '桌 05',
gameIcon: '🔢',
gameId: 'guess-number',
maxPlayers: 4,
players: [],
status: 'waiting',
statusText: '等待中',
stakes: 100,
mode: '经典模式',
creatorName: '神秘数字'
}
]
}
/**
* 初始化各游戏当前最大桌号计数器
*
* 从现有 mock 数据中扫描每个游戏最大桌号作为初始值。
* 后续创建房间时从该值 +1 开始递增。
*/
function initRoomCounters(): Record<string, number> {
const counters: Record<string, number> = {}
for (const [gameId, rooms] of Object.entries(mockRoomList)) {
let max = 0
for (const room of rooms) {
const match = room.name.match(/^桌\s*(\d+)$/)
if (match) {
// @ts-ignore
const num = parseInt(match[1], 10)
if (num > max) max = num
}
}
counters[gameId] = max
}
return counters
}
/** 各游戏当前最大桌号计数器 */
export const roomCounters: Record<string, number> = initRoomCounters()
/**
* 获取下一个可用桌号
*
* 格式:"桌 01"、"桌 02" … 两位数补零。
* 每次调用后计数器自增。
*
* @param gameId - 游戏 ID
* @returns 格式化的桌号字符串,如 "桌 06"
*/
export function nextRoomTableNumber(gameId: string): string {
if (!roomCounters[gameId]) {
roomCounters[gameId] = 0
}
roomCounters[gameId]++
const num = roomCounters[gameId]
return `${String(num).padStart(2, '0')}`
}

View File

@@ -1,8 +1,8 @@
import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { requestClient } from '@/api/request'
import { mockGameList } from './game-list'
import { mockRoomList } from './game-room'
import type { GetRoomListRequest } from '../modules/game/types'
import { mockRoomList, nextRoomTableNumber } from './game-room'
import type { CreateRoomRequest, GetRoomListRequest, RoomItem } from '../modules/game/types'
/**
* Mock 处理器函数类型
@@ -45,6 +45,50 @@ const mockRoutes: MockRoute[] = [
}
return { list: rooms }
}
},
{
method: 'post',
url: '/game/room/create',
delay: 200,
handler: (config) => {
const body = JSON.parse(config.data as string) as CreateRoomRequest
const { gameId, name, maxPlayers, mode, stakes } = body
const tableName = nextRoomTableNumber(gameId)
// 从现有最大 ID 编号 +1 生成新 ID
const existingRooms = mockRoomList[gameId] ?? []
let maxIdNum = 0
for (const room of existingRooms) {
const match = room.id.match(/-(\d+)$/)
if (match) {
const num = parseInt(match[1], 10)
if (num > maxIdNum) maxIdNum = num
}
}
const newId = `${gameId}-${String(maxIdNum + 1).padStart(3, '0')}`
const newRoom: RoomItem = {
id: newId,
name: name || tableName,
gameIcon: mockGameList.find((g) => g.id === gameId)?.icon ?? '🎮',
gameId,
maxPlayers,
players: [],
status: 'waiting',
statusText: '等待中',
stakes,
mode,
creatorName: '当前玩家'
}
// 将新房间插入到列表头部
if (!mockRoomList[gameId]) {
mockRoomList[gameId] = []
}
mockRoomList[gameId].unshift(newRoom)
return { room: newRoom }
}
}
]

View File

@@ -1,5 +1,10 @@
import { requestClient } from '@/api/request'
import type { GetRoomListRequest, GetRoomListResponse } from './types'
import type {
CreateRoomRequest,
CreateRoomResponse,
GetRoomListRequest,
GetRoomListResponse
} from './types'
/**
* 获取指定游戏的房间列表
@@ -16,3 +21,17 @@ export function getRoomList(params: GetRoomListRequest) {
params as unknown as Record<string, unknown>
)
}
/**
* 创建房间
*
* 根据当前游戏和选定的参数创建新房间。
* 房间名称留空时由后端自动生成"桌 XX"编号。
* 密码留空时房间为公开房间。
*
* @param data - 创建房间参数
* @returns 新建的房间信息
*/
export function createRoom(data: CreateRoomRequest) {
return requestClient.post<CreateRoomResponse>('/game/room/create', data)
}

View File

@@ -16,6 +16,8 @@ export interface GameItem {
description: string
/** 玩法介绍 */
gameplay: string
/** 游戏配置(模式、人数、底注等选项) */
config: GameConfig
}
/** 房间内玩家 */
@@ -26,6 +28,16 @@ export interface RoomPlayer {
nickname: string
}
/** 游戏配置(随游戏列表接口一起返回) */
export interface GameConfig {
/** 最大玩家数 */
maxPlayers: number
/** 可选玩法模式列表 */
modes: string[]
/** 可选底注档位列表 */
stakesOptions: number[]
}
/** 房间状态 */
export type RoomStatus = 'waiting' | 'playing' | 'finished'
@@ -72,3 +84,25 @@ export interface GetRoomListRequest {
export interface GetRoomListResponse {
list: RoomItem[]
}
/** 创建房间请求参数 */
export interface CreateRoomRequest {
/** 游戏 ID */
gameId: string
/** 房间名称(可选,留空由后端自动生成) */
name?: string
/** 房间密码(可选,留空为公开房间) */
password?: string
/** 玩法模式 */
mode: string
/** 底注金额 */
stakes: number
/** 最大玩家数 */
maxPlayers: number
}
/** 创建房间响应 */
export interface CreateRoomResponse {
/** 新建的房间信息 */
room: RoomItem
}

View File

@@ -0,0 +1,186 @@
<template>
<el-dialog
v-model="visible"
:title="dialogTitle"
width="420px"
:close-on-click-modal="false"
@closed="handleClosed"
>
<el-form
ref="formRef"
:model="form"
:rules="rules"
label-width="72px"
label-position="left"
@submit.prevent="handleSubmit"
>
<!-- 房间名称可选 -->
<el-form-item label="房间名" prop="name">
<el-input
v-model="form.name"
placeholder="留空自动生成桌号"
maxlength="20"
show-word-limit
clearable
/>
<div class="create-room-dialog__hint">留空将自动生成"桌 XX"编号</div>
</el-form-item>
<!-- 游戏模式 -->
<el-form-item label="模式" prop="mode">
<el-select v-model="form.mode" placeholder="请选择模式">
<el-option v-for="m in gameConfig?.modes ?? []" :key="m" :label="m" :value="m" />
</el-select>
</el-form-item>
<!-- 底注金额 -->
<el-form-item label="底注" prop="stakes">
<el-select v-model="form.stakes" placeholder="请选择底注">
<el-option
v-for="s in gameConfig?.stakesOptions ?? []"
:key="s"
:label="`${s} 金币`"
:value="s"
/>
</el-select>
</el-form-item>
<!-- 房间密码可选 -->
<el-form-item label="密码" prop="password">
<el-input
v-model="form.password"
type="password"
placeholder="留空为公开房间"
maxlength="16"
show-password
clearable
/>
<div class="create-room-dialog__hint">设置密码后其他玩家需输入密码才能加入</div>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="submitting" @click="handleSubmit"> 确认创建 </el-button>
</template>
</el-dialog>
</template>
<script setup lang="ts">
import { ref, reactive, computed, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useBaseLayoutStore } from '@/stores'
import { storeToRefs } from 'pinia'
import type { GameConfig } from '@/api/modules/game'
import { createRoom } from '@/api/modules/game'
/** 创建房间表单数据 */
interface CreateRoomForm {
name: string
mode: string
stakes: number | undefined
password: string
}
const props = defineProps<{
modelValue: boolean
}>()
const emit = defineEmits<{
(e: 'update:modelValue', value: boolean): void
(e: 'created'): void
}>()
const baseLayoutStore = useBaseLayoutStore()
const { activeGameName, activeGameConfig } = storeToRefs(baseLayoutStore)
const formRef = ref<FormInstance>()
const submitting = ref(false)
/** 对话框可见性(双向绑定) */
const visible = computed({
get: () => props.modelValue,
set: (val: boolean) => emit('update:modelValue', val)
})
/** 对话框标题 */
const dialogTitle = computed(() => `创建房间 — ${activeGameName.value}`)
/** 当前游戏配置(只读) */
const gameConfig = computed<GameConfig | null>(() => activeGameConfig.value)
/** 表单数据 */
const form = reactive<CreateRoomForm>({
name: '',
mode: '',
stakes: undefined,
password: ''
})
/** 初始化默认值:取当前游戏配置的第一个选项 */
function initDefaults() {
if (gameConfig.value) {
form.mode = gameConfig.value.modes[0] ?? ''
form.stakes = gameConfig.value.stakesOptions[0]
}
form.name = ''
form.password = ''
}
/** 弹窗打开时设置默认值 */
watch(
() => props.modelValue,
(val) => {
if (val) initDefaults()
}
)
/** 切换游戏时重置默认值 */
watch(activeGameConfig, () => {
if (props.modelValue) initDefaults()
})
const rules: FormRules = {
mode: [{ required: true, message: '请选择游戏模式', trigger: 'change' }],
stakes: [{ required: true, message: '请选择底注金额', trigger: 'change' }],
name: [{ max: 20, message: '房间名不超过 20 个字符', trigger: 'blur' }],
password: [{ max: 16, message: '密码不超过 16 个字符', trigger: 'blur' }]
}
/** 提交创建房间 */
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
submitting.value = true
try {
await createRoom({
gameId: baseLayoutStore.activeGameId,
name: form.name.trim() || undefined,
password: form.password || undefined,
mode: form.mode,
stakes: form.stakes!,
maxPlayers: gameConfig.value?.maxPlayers ?? 0
})
visible.value = false
emit('created')
} finally {
submitting.value = false
}
}
/** 弹窗关闭动画结束后重置表单 */
function handleClosed() {
initDefaults()
formRef.value?.resetFields()
}
</script>
<style scoped lang="scss">
.create-room-dialog__hint {
font-size: 12px;
color: var(--text-tertiary);
margin-top: 4px;
line-height: 1.4;
}
</style>

View File

@@ -53,11 +53,18 @@ async function fetchGameList() {
try {
const res = await getGameList()
games.value = res.data.list
// 默认选第一个游戏
if (games.value.length > 0 && !activeGameId.value) {
// 优先恢复已选中的游戏,否则默认选第一个
if (games.value.length > 0) {
const storedGame = games.value.find((g) => g.id === activeGameId.value)
if (storedGame) {
// 已有选中的游戏,同步最新配置到 store
baseLayoutStore.setActiveGame(storedGame.id, storedGame.icon, storedGame.name, storedGame.config)
} else {
// 没有匹配的已选游戏,默认选第一个
const first = games.value[0]
if (first) {
baseLayoutStore.setActiveGame(first.id, first.icon, first.name)
baseLayoutStore.setActiveGame(first.id, first.icon, first.name, first.config)
}
}
}
} finally {
@@ -79,7 +86,7 @@ const activeGame = computed(() => games.value.find((g) => g.id === activeGameId.
function selectGame(id: string) {
const game = games.value.find((g) => g.id === id)
if (game) {
baseLayoutStore.setActiveGame(game.id, game.icon, game.name)
baseLayoutStore.setActiveGame(game.id, game.icon, game.name, game.config)
}
}
</script>
@@ -113,7 +120,9 @@ function selectGame(id: string) {
height: 48px;
border-radius: var(--radius-sm);
cursor: pointer;
transition: background-color 0.15s ease, transform 0.15s ease;
transition:
background-color 0.15s ease,
transform 0.15s ease;
&:hover {
background-color: var(--bg-surface-hover);

View File

@@ -37,12 +37,16 @@
<p class="lobby__empty-text">暂无符合条件的房间</p>
<p class="lobby__empty-hint">换个筛选条件或者自己创建一个房间吧</p>
</div>
<!-- 创建房间弹窗 -->
<CreateRoomDialog v-model="showCreateDialog" @created="fetchRoomList" />
</div>
</template>
<script setup lang="ts">
import { ref, computed, watch } from 'vue'
import TableCard from './TableCard.vue'
import CreateRoomDialog from './CreateRoomDialog.vue'
import type { RoomItem } from '@/api/modules/game'
import { useBaseLayoutStore } from '@/stores'
import { storeToRefs } from 'pinia'
@@ -111,10 +115,12 @@ function handleSpectate(roomId: string) {
console.log('观战房间:', roomId)
}
/** 创建房间对话框可见性 */
const showCreateDialog = ref(false)
/** 创建房间 */
function handleCreateRoom() {
// eslint-disable-next-line no-console
console.log('创建房间')
showCreateDialog.value = true
}
</script>

View File

@@ -1,5 +1,6 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { GameConfig } from '@/api/modules/game'
/** 侧边栏最小宽度 */
export const MIN_SIDEBAR_WIDTH = 200
@@ -19,6 +20,8 @@ export const useBaseLayoutStore = defineStore(
const activeGameIcon = ref<string>('🐍')
/** 当前选中的游戏名称 */
const activeGameName = ref<string>('贪吃蛇')
/** 当前选中游戏的配置(模式、底注、人数等选项) */
const activeGameConfig = ref<GameConfig | null>(null)
/** 侧边栏当前宽度 */
const sidebarWidth = ref<number>(DEFAULT_SIDEBAR_WIDTH)
@@ -36,10 +39,11 @@ export const useBaseLayoutStore = defineStore(
* @param icon - 游戏图标
* @param name - 游戏名称
*/
const setActiveGame = (id: string, icon: string, name: string): void => {
const setActiveGame = (id: string, icon: string, name: string, config: GameConfig): void => {
activeGameId.value = id
activeGameIcon.value = icon
activeGameName.value = name
activeGameConfig.value = config
}
/**
@@ -55,6 +59,7 @@ export const useBaseLayoutStore = defineStore(
activeGameId,
activeGameIcon,
activeGameName,
activeGameConfig,
sidebarWidth,
setTopbarTitle,
setActiveGame,