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