From 7d2d6707b7d6cd675d2aaacc0ea0c8564eed1678 Mon Sep 17 00:00:00 2001 From: Azure <983547216@qq.com> Date: Mon, 22 Jun 2026 10:32:53 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20user-bar=20=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.development | 5 +- components.d.ts | 2 + src/api/mock/game-list.ts | 53 ---- src/api/mock/game-room.ts | 365 ---------------------- src/api/mock/index.ts | 145 --------- src/api/modules/user/info.ts | 2 + src/api/request/requestClient.ts | 2 +- src/components/baseLayouts/UserBar.vue | 406 ++++++++++++++++++++++++- src/main.ts | 4 - src/stores/modules/account.ts | 2 + 10 files changed, 400 insertions(+), 586 deletions(-) delete mode 100644 src/api/mock/game-list.ts delete mode 100644 src/api/mock/game-room.ts delete mode 100644 src/api/mock/index.ts diff --git a/.env.development b/.env.development index 13e03a9..e833169 100644 --- a/.env.development +++ b/.env.development @@ -1,3 +1,2 @@ -# 开发环境启用 Mock 拦截器 -# 后端接口就绪后设为 false 即可切换到真实请求 -VITE_USE_MOCK=true +# 已切换为真实后端接口 +VITE_USE_MOCK=false diff --git a/components.d.ts b/components.d.ts index c5e30db..461a595 100644 --- a/components.d.ts +++ b/components.d.ts @@ -26,6 +26,7 @@ declare module 'vue' { ElOption: typeof import('element-plus/es')['ElOption'] ElPopover: typeof import('element-plus/es')['ElPopover'] ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] 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'] @@ -52,6 +53,7 @@ declare global { 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 ElSwitch: typeof import('element-plus/es')['ElSwitch'] 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'] diff --git a/src/api/mock/game-list.ts b/src/api/mock/game-list.ts deleted file mode 100644 index 232fb2f..0000000 --- a/src/api/mock/game-list.ts +++ /dev/null @@ -1,53 +0,0 @@ -import type { GameItem } from '../modules/game/types' - -/** 游戏列表 Mock 数据 */ -export const mockGameList: GameItem[] = [ - { - id: 'snake', - icon: '🐍', - name: '贪吃蛇', - description: '经典贪吃蛇竞技,在狭小空间内争夺生存权', - gameplay: '方向键控制蛇的移动,吃到食物变长。撞墙或其他蛇的身体即死亡,最后存活的玩家获胜。', - config: { - maxPlayers: 4, - modes: ['经典模式', '道具模式', '加速模式'], - stakesOptions: [100, 200, 300, 400, 500] - } - }, - { - id: 'tetris', - icon: '🧊', - name: '俄罗斯方块', - description: '经典下落消除,与对手一较高下', - gameplay: '方向键移动和旋转方块,填满横排即可消除。同时向对手发送干扰行,坚持到最后的玩家获胜。', - config: { - maxPlayers: 2, - modes: ['1v1对战', '竞速模式'], - stakesOptions: [150, 200, 300, 400, 500] - } - }, - { - id: 'gomoku', - icon: '⚫', - name: '五子棋', - description: '黑白对弈,先连成五子者胜', - gameplay: '鼠标点击落子,黑白双方轮流在棋盘上放置棋子。先在横、竖、斜任意方向连成五子的一方获胜。', - config: { - maxPlayers: 2, - modes: ['无禁手', '有禁手'], - stakesOptions: [100, 200, 300, 400, 500] - } - }, - { - id: 'guess-number', - icon: '🔢', - name: '猜数字', - description: '逻辑推理对战,猜中对方的秘密数字', - gameplay: '每轮输入猜测的数字组合,系统会提示位置和数字正确的数量。率先猜中对手数字组合的玩家获胜。', - config: { - maxPlayers: 4, - modes: ['经典模式', '限时模式'], - stakesOptions: [100, 150, 200, 300, 500] - } - } -] diff --git a/src/api/mock/game-room.ts b/src/api/mock/game-room.ts deleted file mode 100644 index 55fd3e8..0000000 --- a/src/api/mock/game-room.ts +++ /dev/null @@ -1,365 +0,0 @@ -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: '神秘数字' - } - ] -} - -/** - * 初始化各游戏当前最大桌号计数器 - * - * 从现有 mock 数据中扫描每个游戏最大桌号作为初始值。 - * 后续创建房间时从该值 +1 开始递增。 - */ -function initRoomCounters(): Record { - const counters: Record = {} - 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 = 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')}` -} diff --git a/src/api/mock/index.ts b/src/api/mock/index.ts deleted file mode 100644 index 4e637a5..0000000 --- a/src/api/mock/index.ts +++ /dev/null @@ -1,145 +0,0 @@ -import type { AxiosRequestConfig, AxiosResponse, InternalAxiosRequestConfig } from 'axios' -import { requestClient } from '@/api/request' -import { mockGameList } from './game-list' -import { mockRoomList, nextRoomTableNumber } from './game-room' -import type { CreateRoomRequest, GetRoomListRequest, RoomItem } 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 } - } - }, - { - 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 } - } - } -] - -/** - * 安装 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 as InternalAxiosRequestConfig) - - // 构造符合 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/user/info.ts b/src/api/modules/user/info.ts index 8b14aa3..0dc091c 100644 --- a/src/api/modules/user/info.ts +++ b/src/api/modules/user/info.ts @@ -23,6 +23,8 @@ export type SexType = (typeof Sex)[keyof typeof Sex] * 所有字段均为可选,部分字段可能为 null */ export interface UserInfoResponse { + /** 用户唯一标识(UUID) */ + userId?: string /** 用户昵称,最长 16 位 */ nickName?: string /** 头像图片 URL 地址,可能为 null */ diff --git a/src/api/request/requestClient.ts b/src/api/request/requestClient.ts index f7fb064..52c800e 100644 --- a/src/api/request/requestClient.ts +++ b/src/api/request/requestClient.ts @@ -36,7 +36,7 @@ function resolvePathParams(url: string, pathParams?: Record
- + - I wish they all could be California girls + + +
+ +
+ +
+
{{ displayName }}
+
+ + {{ currentStatus.label }} +
+
+
+ + +
+ {{ signatureText }} +
+ +
+ + +
+
+ + 编辑个人资料 +
+ + +
+
+ + 在线状态 + {{ currentStatus.label }} + + + +
+ + + +
+
+ + {{ status.label }} + + + +
+
+
+
+
+ +
+ + +
+
+ + 复制账号ID +
+
+ + 退出登录 +
+
+
@@ -28,10 +122,111 @@ + + + diff --git a/src/main.ts b/src/main.ts index c9d5d4e..270959f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,14 +3,10 @@ import 'element-plus/dist/index.css' import { createApp } from 'vue' import { initPinia } from './stores' -import { setupMock } from './api/mock' import App from './App.vue' import router from './router' -/** 开发环境启用 Mock 数据拦截 */ -setupMock() - const app = createApp(App) app.use(initPinia()) diff --git a/src/stores/modules/account.ts b/src/stores/modules/account.ts index edbb6a0..1126887 100644 --- a/src/stores/modules/account.ts +++ b/src/stores/modules/account.ts @@ -11,6 +11,8 @@ import { getUserInfo } from '@/api/modules/user' /** 账户信息 */ export interface AccountState { + /** 用户唯一标识(UUID) */ + userId?: string /** 用户昵称 */ nickName?: string /** 头像 */