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

View File

@@ -7,27 +7,47 @@ export const mockGameList: GameItem[] = [
icon: '🐍', icon: '🐍',
name: '贪吃蛇', name: '贪吃蛇',
description: '经典贪吃蛇竞技,在狭小空间内争夺生存权', description: '经典贪吃蛇竞技,在狭小空间内争夺生存权',
gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。' gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。',
config: {
maxPlayers: 4,
modes: ['经典模式', '道具模式', '加速模式'],
stakesOptions: [100, 200, 300, 400, 500]
}
}, },
{ {
id: 'tetris', id: 'tetris',
icon: '🧊', icon: '🧊',
name: '俄罗斯方块', name: '俄罗斯方块',
description: '经典下落消除,与对手一较高下', description: '经典下落消除,与对手一较高下',
gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。' gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。',
config: {
maxPlayers: 2,
modes: ['1v1对战', '竞速模式'],
stakesOptions: [150, 200, 300, 400, 500]
}
}, },
{ {
id: 'gomoku', id: 'gomoku',
icon: '⚫', icon: '⚫',
name: '五子棋', name: '五子棋',
description: '黑白对弈,先连成五子者胜', description: '黑白对弈,先连成五子者胜',
gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。' gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。',
config: {
maxPlayers: 2,
modes: ['无禁手', '有禁手'],
stakesOptions: [100, 200, 300, 400, 500]
}
}, },
{ {
id: 'guess-number', id: 'guess-number',
icon: '🔢', icon: '🔢',
name: '猜数字', name: '猜数字',
description: '逻辑推理对战,猜中对方的秘密数字', 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: [ snake: [
{ {
id: 'snake-001', name: '桌 01', gameIcon: '🐍', gameId: 'snake', id: 'snake-001',
maxPlayers: 4, name: '桌 01',
players: [{ avatar: avatarUrl('小龙'), nickname: '小龙' }, { avatar: avatarUrl('阿蟒'), nickname: '阿蟒' }], gameIcon: '🐍',
status: 'waiting', statusText: '等待中', stakes: 300, mode: '经典模式', creatorName: '小龙' gameId: 'snake',
},
{
id: 'snake-002', name: '桌 02', gameIcon: '🐍', gameId: 'snake',
maxPlayers: 4, maxPlayers: 4,
players: [ 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, maxPlayers: 4,
players: [{ avatar: avatarUrl('青蛇'), nickname: '青蛇' }], 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', id: 'snake-004',
maxPlayers: 4, players: [], name: '桌 04',
status: 'waiting', statusText: '等待中', stakes: 100, mode: '经典模式', creatorName: '毒牙' 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, maxPlayers: 4,
players: [ players: [
{ avatar: avatarUrl('巨蟒'), nickname: '巨蟒' }, { 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: [ tetris: [
{ {
id: 'tetris-001', name: '桌 01', gameIcon: '🧊', gameId: 'tetris', id: 'tetris-001',
name: '桌 01',
gameIcon: '🧊',
gameId: 'tetris',
maxPlayers: 2, maxPlayers: 2,
players: [{ avatar: avatarUrl('方块大师'), nickname: '方块大师' }], 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('极速下落'), nickname: '极速下落' }, { avatar: avatarUrl('消行高手'), nickname: '消行高手' }], players: [
status: 'playing', statusText: '进行中', stakes: 500, mode: '竞速模式', creatorName: '极速下落' { avatar: avatarUrl('极速下落'), nickname: '极速下落' },
{ avatar: avatarUrl('消行高手'), nickname: '消行高手' }
],
status: 'playing',
statusText: '进行中',
stakes: 500,
mode: '竞速模式',
creatorName: '极速下落'
}, },
{ {
id: 'tetris-003', name: '桌 03', gameIcon: '🧊', gameId: 'tetris', id: 'tetris-003',
maxPlayers: 2, players: [], name: '桌 03',
status: 'waiting', statusText: '等待中', stakes: 150, mode: '1v1 对战', creatorName: '堆叠王者' 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('T旋大师'), nickname: 'T旋大师' }], 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('连击王'), nickname: '连击王' }, { avatar: avatarUrl('消除者'), nickname: '消除者' }], players: [
status: 'playing', statusText: '进行中', stakes: 400, mode: '竞速模式', creatorName: '连击王' { avatar: avatarUrl('连击王'), nickname: '连击王' },
{ avatar: avatarUrl('消除者'), nickname: '消除者' }
],
status: 'playing',
statusText: '进行中',
stakes: 400,
mode: '竞速模式',
creatorName: '连击王'
} }
], ],
/* ---- 五子棋 ---- */ /* ---- 五子棋 ---- */
gomoku: [ gomoku: [
{ {
id: 'gomoku-001', name: '桌 01', gameIcon: '⚫', gameId: 'gomoku', id: 'gomoku-001',
name: '桌 01',
gameIcon: '⚫',
gameId: 'gomoku',
maxPlayers: 2, maxPlayers: 2,
players: [{ avatar: avatarUrl('棋圣'), nickname: '棋圣' }], 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('黑白子'), nickname: '黑白子' }, { avatar: avatarUrl('五连珠'), nickname: '五连珠' }], players: [
status: 'playing', statusText: '进行中', stakes: 400, mode: '有禁手', creatorName: '黑白子' { avatar: avatarUrl('黑白子'), nickname: '黑白子' },
{ avatar: avatarUrl('五连珠'), nickname: '五连珠' }
],
status: 'playing',
statusText: '进行中',
stakes: 400,
mode: '有禁手',
creatorName: '黑白子'
}, },
{ {
id: 'gomoku-003', name: '桌 03', gameIcon: '⚫', gameId: 'gomoku', id: 'gomoku-003',
maxPlayers: 2, players: [], name: '桌 03',
status: 'waiting', statusText: '等待中', stakes: 100, mode: '无禁手', creatorName: '落子无悔' 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('天元'), nickname: '天元' }], 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, maxPlayers: 2,
players: [{ avatar: avatarUrl('先手必胜'), nickname: '先手必胜' }, { avatar: avatarUrl('后手无敌'), nickname: '后手无敌' }], players: [
status: 'playing', statusText: '进行中', stakes: 500, mode: '有禁手', creatorName: '先手必胜' { avatar: avatarUrl('先手必胜'), nickname: '先手必胜' },
{ avatar: avatarUrl('后手无敌'), nickname: '后手无敌' }
],
status: 'playing',
statusText: '进行中',
stakes: 500,
mode: '有禁手',
creatorName: '先手必胜'
} }
], ],
/* ---- 猜数字 ---- */ /* ---- 猜数字 ---- */
'guess-number': [ 'guess-number': [
{ {
id: 'guess-001', name: '桌 01', gameIcon: '🔢', gameId: 'guess-number', id: 'guess-001',
name: '桌 01',
gameIcon: '🔢',
gameId: 'guess-number',
maxPlayers: 4, maxPlayers: 4,
players: [{ avatar: avatarUrl('逻辑怪'), nickname: '逻辑怪' }, { avatar: avatarUrl('推理王'), nickname: '推理王' }], players: [
status: 'waiting', statusText: '等待中', stakes: 200, mode: '经典模式', creatorName: '逻辑怪' { 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, maxPlayers: 4,
players: [ players: [
{ avatar: avatarUrl('秒猜'), nickname: '秒猜' }, { avatar: avatarUrl('秒猜'), nickname: '秒猜' },
{ avatar: avatarUrl('心算'), nickname: '心算' }, { 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, maxPlayers: 4,
players: [{ avatar: avatarUrl('数字猎人'), nickname: '数字猎人' }], 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, maxPlayers: 4,
players: [ players: [
{ avatar: avatarUrl('密码破解'), nickname: '密码破解' }, { avatar: avatarUrl('密码破解'), nickname: '密码破解' },
@@ -145,12 +298,68 @@ export const mockRoomList: Record<string, RoomItem[]> = {
{ avatar: avatarUrl('二分法'), nickname: '二分法' }, { avatar: avatarUrl('二分法'), nickname: '二分法' },
{ 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', id: 'guess-005',
maxPlayers: 4, players: [], name: '桌 05',
status: 'waiting', statusText: '等待中', stakes: 100, mode: '经典模式', creatorName: '神秘数字' 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 type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios'
import { requestClient } from '@/api/request' import { requestClient } from '@/api/request'
import { mockGameList } from './game-list' import { mockGameList } from './game-list'
import { mockRoomList } from './game-room' import { mockRoomList, nextRoomTableNumber } from './game-room'
import type { GetRoomListRequest } from '../modules/game/types' import type { CreateRoomRequest, GetRoomListRequest, RoomItem } from '../modules/game/types'
/** /**
* Mock 处理器函数类型 * Mock 处理器函数类型
@@ -45,6 +45,50 @@ const mockRoutes: MockRoute[] = [
} }
return { list: rooms } 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 { 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> 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 description: string
/** 玩法介绍 */ /** 玩法介绍 */
gameplay: string gameplay: string
/** 游戏配置(模式、人数、底注等选项) */
config: GameConfig
} }
/** 房间内玩家 */ /** 房间内玩家 */
@@ -26,6 +28,16 @@ export interface RoomPlayer {
nickname: string nickname: string
} }
/** 游戏配置(随游戏列表接口一起返回) */
export interface GameConfig {
/** 最大玩家数 */
maxPlayers: number
/** 可选玩法模式列表 */
modes: string[]
/** 可选底注档位列表 */
stakesOptions: number[]
}
/** 房间状态 */ /** 房间状态 */
export type RoomStatus = 'waiting' | 'playing' | 'finished' export type RoomStatus = 'waiting' | 'playing' | 'finished'
@@ -72,3 +84,25 @@ export interface GetRoomListRequest {
export interface GetRoomListResponse { export interface GetRoomListResponse {
list: RoomItem[] 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 { try {
const res = await getGameList() const res = await getGameList()
games.value = res.data.list games.value = res.data.list
// 默认选第一个游戏 // 优先恢复已选中的游戏,否则默认选第一个
if (games.value.length > 0 && !activeGameId.value) { if (games.value.length > 0) {
const first = games.value[0] const storedGame = games.value.find((g) => g.id === activeGameId.value)
if (first) { if (storedGame) {
baseLayoutStore.setActiveGame(first.id, first.icon, first.name) // 已有选中的游戏,同步最新配置到 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, first.config)
}
} }
} }
} finally { } finally {
@@ -79,7 +86,7 @@ const activeGame = computed(() => games.value.find((g) => g.id === activeGameId.
function selectGame(id: string) { function selectGame(id: string) {
const game = games.value.find((g) => g.id === id) const game = games.value.find((g) => g.id === id)
if (game) { if (game) {
baseLayoutStore.setActiveGame(game.id, game.icon, game.name) baseLayoutStore.setActiveGame(game.id, game.icon, game.name, game.config)
} }
} }
</script> </script>
@@ -113,7 +120,9 @@ function selectGame(id: string) {
height: 48px; height: 48px;
border-radius: var(--radius-sm); border-radius: var(--radius-sm);
cursor: pointer; cursor: pointer;
transition: background-color 0.15s ease, transform 0.15s ease; transition:
background-color 0.15s ease,
transform 0.15s ease;
&:hover { &:hover {
background-color: var(--bg-surface-hover); background-color: var(--bg-surface-hover);

View File

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

View File

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