From 9bd85a4e43d2268989219d2517edf44a79509af9 Mon Sep 17 00:00:00 2001
From: Azure <983547216@qq.com>
Date: Thu, 18 Jun 2026 13:10:09 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E6=95=B0=E6=8D=AEmock?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.env.development | 3 +
env.d.ts | 5 +
src/api/mock/game-list.ts | 33 +++
src/api/mock/game-room.ts | 156 ++++++++++
src/api/mock/index.ts | 101 +++++++
src/api/modules/game/index.ts | 3 +
src/api/modules/game/list.ts | 14 +
src/api/modules/game/room.ts | 18 ++
src/api/modules/game/types.ts | 74 +++++
src/api/modules/index.ts | 1 +
src/api/request/requestClient.ts | 4 +-
src/components/baseLayouts/GameList.vue | 70 ++---
src/components/baseLayouts/Lobby.vue | 349 ++---------------------
src/components/baseLayouts/TableCard.vue | 34 +--
src/main.ts | 4 +
15 files changed, 466 insertions(+), 403 deletions(-)
create mode 100644 .env.development
create mode 100644 src/api/mock/game-list.ts
create mode 100644 src/api/mock/game-room.ts
create mode 100644 src/api/mock/index.ts
create mode 100644 src/api/modules/game/index.ts
create mode 100644 src/api/modules/game/list.ts
create mode 100644 src/api/modules/game/room.ts
create mode 100644 src/api/modules/game/types.ts
diff --git a/.env.development b/.env.development
new file mode 100644
index 0000000..13e03a9
--- /dev/null
+++ b/.env.development
@@ -0,0 +1,3 @@
+# 开发环境启用 Mock 拦截器
+# 后端接口就绪后设为 false 即可切换到真实请求
+VITE_USE_MOCK=true
diff --git a/env.d.ts b/env.d.ts
index 11f02fe..0de63e8 100644
--- a/env.d.ts
+++ b/env.d.ts
@@ -1 +1,6 @@
///
+
+interface ImportMetaEnv {
+ /** 是否启用 Mock 拦截器 */
+ readonly VITE_USE_MOCK: string
+}
diff --git a/src/api/mock/game-list.ts b/src/api/mock/game-list.ts
new file mode 100644
index 0000000..943e357
--- /dev/null
+++ b/src/api/mock/game-list.ts
@@ -0,0 +1,33 @@
+import type { GameItem } from '../modules/game/types'
+
+/** 游戏列表 Mock 数据 */
+export const mockGameList: GameItem[] = [
+ {
+ id: 'snake',
+ icon: '🐍',
+ name: '贪吃蛇',
+ description: '经典贪吃蛇竞技,在狭小空间内争夺生存权',
+ gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。'
+ },
+ {
+ id: 'tetris',
+ icon: '🧊',
+ name: '俄罗斯方块',
+ description: '经典下落消除,与对手一较高下',
+ gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。'
+ },
+ {
+ id: 'gomoku',
+ icon: '⚫',
+ name: '五子棋',
+ description: '黑白对弈,先连成五子者胜',
+ gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。'
+ },
+ {
+ id: 'guess-number',
+ icon: '🔢',
+ name: '猜数字',
+ description: '逻辑推理对战,猜中对方的秘密数字',
+ gameplay: '每轮输入猜测的数字组合,系统会提示位置和数字正确的数量。率先猜中对手数字组合的玩家获胜。'
+ }
+]
diff --git a/src/api/mock/game-room.ts b/src/api/mock/game-room.ts
new file mode 100644
index 0000000..b7a0079
--- /dev/null
+++ b/src/api/mock/game-room.ts
@@ -0,0 +1,156 @@
+import type { RoomItem } from '../modules/game/types'
+
+/** 根据名字生成头像 URL */
+function avatarUrl(name: string): string {
+ return `https://api.dicebear.com/7.x/bottts-neutral/svg?seed=${encodeURIComponent(name)}`
+}
+
+/** 各游戏的房间 Mock 数据 */
+export const mockRoomList: Record = {
+ /* ---- 贪吃蛇 ---- */
+ 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: '神秘数字'
+ }
+ ]
+}
diff --git a/src/api/mock/index.ts b/src/api/mock/index.ts
new file mode 100644
index 0000000..550a655
--- /dev/null
+++ b/src/api/mock/index.ts
@@ -0,0 +1,101 @@
+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'
+
+/**
+ * Mock 处理器函数类型
+ *
+ * 接收 axios 配置,返回 mock 响应数据体(ApiResponse.data 部分)
+ */
+type MockHandler = (config: InternalAxiosRequestConfig) => unknown
+
+/** Mock 路由条目 */
+interface MockRoute {
+ /** 请求方法,小写 */
+ method: string
+ /** URL 路径(支持精确匹配和前缀匹配) */
+ url: string
+ /** 模拟网络延迟(毫秒),默认 150ms */
+ delay?: number
+ /** 处理器 */
+ handler: MockHandler
+}
+
+/** Mock 路由表 */
+const mockRoutes: MockRoute[] = [
+ {
+ method: 'get',
+ url: '/game/list',
+ delay: 120,
+ handler: () => ({ list: mockGameList })
+ },
+ {
+ method: 'get',
+ url: '/game/room/list',
+ delay: 150,
+ handler: (config) => {
+ const params = config.params as GetRoomListRequest | undefined
+ const gameId = params?.gameId ?? 'snake'
+ const statusFilter = params?.status
+ let rooms = mockRoomList[gameId] ?? []
+ if (statusFilter) {
+ rooms = rooms.filter((r) => r.status === statusFilter)
+ }
+ return { list: rooms }
+ }
+ }
+]
+
+/**
+ * 安装 Mock 拦截器
+ *
+ * 在 axios 请求拦截器链中注入 mock 逻辑:
+ * 当请求 URL + method 匹配 mock 路由表时,将请求适配器替换为 mock 处理器,
+ * 绕开真实 HTTP 请求,直接返回模拟数据。
+ *
+ * 仅在开发环境且 VITE_USE_MOCK=true 时启用。
+ */
+export function setupMock(): void {
+ // 仅在 mock 模式启用时注册拦截器
+ if (import.meta.env.VITE_USE_MOCK !== 'true') {
+ return
+ }
+
+ requestClient.instance.interceptors.request.use((config) => {
+ const method = (config.method ?? 'get').toLowerCase()
+ const url = config.url ?? ''
+
+ // 查找匹配的 mock 路由
+ const route = mockRoutes.find((r) => r.method === method && url === r.url)
+ if (!route) return config
+
+ // 替换适配器,使请求不发出 HTTP 调用,直接返回 mock 数据
+ config.adapter = async (adapterConfig: AxiosRequestConfig) => {
+ // 模拟网络延迟
+ const delay = route.delay ?? 150
+ await new Promise((resolve) => setTimeout(resolve, delay))
+
+ const mockData = route.handler(adapterConfig)
+
+ // 构造符合 ApiResponse 格式的响应
+ const response: AxiosResponse = {
+ data: { code: 0, message: 'ok', data: mockData },
+ status: 200,
+ statusText: 'OK',
+ headers: {},
+ config: adapterConfig as InternalAxiosRequestConfig,
+ request: {}
+ }
+ return response
+ }
+
+ return config
+ })
+}
+
+/**
+ * 重新导出 mock 数据,供开发调试使用
+ */
+export { mockGameList, mockRoomList }
diff --git a/src/api/modules/game/index.ts b/src/api/modules/game/index.ts
new file mode 100644
index 0000000..b0118a0
--- /dev/null
+++ b/src/api/modules/game/index.ts
@@ -0,0 +1,3 @@
+export * from './types'
+export * from './list'
+export * from './room'
diff --git a/src/api/modules/game/list.ts b/src/api/modules/game/list.ts
new file mode 100644
index 0000000..affbfd3
--- /dev/null
+++ b/src/api/modules/game/list.ts
@@ -0,0 +1,14 @@
+import { requestClient } from '@/api/request'
+import type { GetGameListResponse } from './types'
+
+/**
+ * 获取游戏列表
+ *
+ * 返回所有可用的游戏信息,包括图标、名称、描述和玩法介绍。
+ * 该接口无需登录即可调用。
+ *
+ * @returns 游戏列表
+ */
+export function getGameList() {
+ return requestClient.get('/game/list')
+}
diff --git a/src/api/modules/game/room.ts b/src/api/modules/game/room.ts
new file mode 100644
index 0000000..42ca986
--- /dev/null
+++ b/src/api/modules/game/room.ts
@@ -0,0 +1,18 @@
+import { requestClient } from '@/api/request'
+import type { GetRoomListRequest, GetRoomListResponse } from './types'
+
+/**
+ * 获取指定游戏的房间列表
+ *
+ * 返回某个游戏下所有房间(桌台)的信息,支持按状态筛选。
+ * 该接口无需登录即可调用。
+ *
+ * @param params - 查询参数,包含 gameId 和可选的 status 筛选
+ * @returns 房间列表
+ */
+export function getRoomList(params: GetRoomListRequest) {
+ return requestClient.get(
+ '/game/room/list',
+ params as unknown as Record
+ )
+}
diff --git a/src/api/modules/game/types.ts b/src/api/modules/game/types.ts
new file mode 100644
index 0000000..51775a5
--- /dev/null
+++ b/src/api/modules/game/types.ts
@@ -0,0 +1,74 @@
+/**
+ * 游戏大厅相关类型定义
+ *
+ * 此文件为前后端接口契约,后端应按此结构提供数据。
+ */
+
+/** 游戏信息 */
+export interface GameItem {
+ /** 游戏唯一标识 */
+ id: string
+ /** 图标(emoji) */
+ icon: string
+ /** 游戏名称 */
+ name: string
+ /** 简短描述 */
+ description: string
+ /** 玩法介绍 */
+ gameplay: string
+}
+
+/** 房间内玩家 */
+export interface RoomPlayer {
+ /** 头像 URL */
+ avatar: string
+ /** 昵称 */
+ nickname: string
+}
+
+/** 房间状态 */
+export type RoomStatus = 'waiting' | 'playing' | 'finished'
+
+/** 房间(桌台)信息 */
+export interface RoomItem {
+ /** 房间唯一标识 */
+ id: string
+ /** 桌号显示名,如 "桌 01" */
+ name: string
+ /** 游戏图标 emoji */
+ gameIcon: string
+ /** 游戏 ID */
+ gameId: string
+ /** 最大玩家数 */
+ maxPlayers: number
+ /** 已入座玩家列表 */
+ players: RoomPlayer[]
+ /** 房间状态 */
+ status: RoomStatus
+ /** 状态展示文字 */
+ statusText: string
+ /** 底注金额 */
+ stakes: number
+ /** 玩法模式标签 */
+ mode: string
+ /** 房主昵称 */
+ creatorName: string
+}
+
+/** 获取游戏列表 — 无请求参数 */
+export interface GetGameListResponse {
+ list: GameItem[]
+}
+
+/** 获取房间列表请求参数 */
+export interface GetRoomListRequest {
+ /** 游戏 ID */
+ gameId: string
+ /** 筛选状态 */
+ status?: RoomStatus
+}
+
+/** 获取房间列表响应 */
+export interface GetRoomListResponse {
+ list: RoomItem[]
+}
diff --git a/src/api/modules/index.ts b/src/api/modules/index.ts
index d381706..eaf2a4a 100644
--- a/src/api/modules/index.ts
+++ b/src/api/modules/index.ts
@@ -1,2 +1,3 @@
export * from './account/index.ts'
export * from './user/index.ts'
+export * from './game/index.ts'
diff --git a/src/api/request/requestClient.ts b/src/api/request/requestClient.ts
index 8949594..f7fb064 100644
--- a/src/api/request/requestClient.ts
+++ b/src/api/request/requestClient.ts
@@ -36,8 +36,8 @@ function resolvePathParams(url: string, pathParams?: Record