feat: user-bar 修改
This commit is contained in:
@@ -1,3 +1,2 @@
|
|||||||
# 开发环境启用 Mock 拦截器
|
# 已切换为真实后端接口
|
||||||
# 后端接口就绪后设为 false 即可切换到真实请求
|
VITE_USE_MOCK=false
|
||||||
VITE_USE_MOCK=true
|
|
||||||
|
|||||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -26,6 +26,7 @@ declare module 'vue' {
|
|||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
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']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
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']
|
||||||
@@ -52,6 +53,7 @@ declare global {
|
|||||||
const ElOption: typeof import('element-plus/es')['ElOption']
|
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 ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
const ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
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']
|
||||||
|
|||||||
@@ -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]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@@ -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<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',
|
|
||||||
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<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')}`
|
|
||||||
}
|
|
||||||
@@ -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 }
|
|
||||||
@@ -23,6 +23,8 @@ export type SexType = (typeof Sex)[keyof typeof Sex]
|
|||||||
* 所有字段均为可选,部分字段可能为 null
|
* 所有字段均为可选,部分字段可能为 null
|
||||||
*/
|
*/
|
||||||
export interface UserInfoResponse {
|
export interface UserInfoResponse {
|
||||||
|
/** 用户唯一标识(UUID) */
|
||||||
|
userId?: string
|
||||||
/** 用户昵称,最长 16 位 */
|
/** 用户昵称,最长 16 位 */
|
||||||
nickName?: string
|
nickName?: string
|
||||||
/** 头像图片 URL 地址,可能为 null */
|
/** 头像图片 URL 地址,可能为 null */
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ function resolvePathParams(url: string, pathParams?: Record<string, string | num
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
class RequestClient {
|
class RequestClient {
|
||||||
/** axios 实例(暴露给 mock 等外部模块使用) */
|
/** axios 实例 */
|
||||||
public instance: AxiosInstance
|
public instance: AxiosInstance
|
||||||
|
|
||||||
/** 是否正在刷新 token */
|
/** 是否正在刷新 token */
|
||||||
|
|||||||
@@ -1,23 +1,117 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="user-bar">
|
<div class="user-bar">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<el-popover trigger="click" :width="260">
|
<el-popover
|
||||||
|
trigger="click"
|
||||||
|
:width="280"
|
||||||
|
:show-arrow="false"
|
||||||
|
:offset="14"
|
||||||
|
placement="top"
|
||||||
|
popper-class="user-popover"
|
||||||
|
>
|
||||||
<template #reference>
|
<template #reference>
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
<el-badge is-dot :offset="[-4, 20]" class="user-badge">
|
<el-badge is-dot :offset="[-4, 20]" class="user-badge">
|
||||||
<el-avatar
|
<el-avatar :size="28" shape="circle" :src="userAvatar" />
|
||||||
:size="28"
|
|
||||||
shape="circle"
|
|
||||||
src="https://07akioni.oss-cn-beijing.aliyuncs.com/07akioni.jpeg"
|
|
||||||
/>
|
|
||||||
</el-badge>
|
</el-badge>
|
||||||
<div class="user-name">
|
<div class="user-name">
|
||||||
<div class="user-title">MarchGlow</div>
|
<div class="user-title">{{ displayName }}</div>
|
||||||
<div class="user-state">在线</div>
|
<div class="user-state">{{ currentStatus.label }}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<span>I wish they all could be California girls</span>
|
|
||||||
|
<!-- 弹出面板内容 -->
|
||||||
|
<div class="user-panel">
|
||||||
|
<!-- 顶部:大头像 + 昵称 + 在线状态 -->
|
||||||
|
<div class="user-panel__header">
|
||||||
|
<el-avatar :size="56" shape="circle" :src="userAvatar" class="user-panel__avatar" />
|
||||||
|
<div class="user-panel__identity">
|
||||||
|
<div class="user-panel__nickname">{{ displayName }}</div>
|
||||||
|
<div class="user-panel__status">
|
||||||
|
<span
|
||||||
|
class="user-panel__status-dot"
|
||||||
|
:style="{ backgroundColor: currentStatus.color }"
|
||||||
|
></span>
|
||||||
|
<span>{{ currentStatus.label }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 个性签名 -->
|
||||||
|
<div class="user-panel__signature">
|
||||||
|
{{ signatureText }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-panel__divider"></div>
|
||||||
|
|
||||||
|
<!-- 详细设置区:编辑个人资料 + 在线状态 -->
|
||||||
|
<div class="user-panel__section">
|
||||||
|
<div class="user-panel__item" @click="handleEditProfile">
|
||||||
|
<el-icon :size="18"><EditPen /></el-icon>
|
||||||
|
<span>编辑个人资料</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 在线状态(父项 + 二级菜单) -->
|
||||||
|
<div class="user-panel__status-group">
|
||||||
|
<div class="user-panel__item" @click="toggleStatusMenu">
|
||||||
|
<span
|
||||||
|
class="user-panel__status-dot"
|
||||||
|
:style="{ backgroundColor: currentStatus.color }"
|
||||||
|
></span>
|
||||||
|
<span>在线状态</span>
|
||||||
|
<span class="user-panel__status-current">{{ currentStatus.label }}</span>
|
||||||
|
<el-icon
|
||||||
|
:size="14"
|
||||||
|
class="user-panel__chevron"
|
||||||
|
:class="{ 'is-open': showStatusMenu }"
|
||||||
|
>
|
||||||
|
<ArrowRight />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 二级菜单:状态选项列表 -->
|
||||||
|
<Transition name="status-menu">
|
||||||
|
<div v-show="showStatusMenu" class="user-panel__status-submenu">
|
||||||
|
<div
|
||||||
|
v-for="status in statusOptions"
|
||||||
|
:key="status.key"
|
||||||
|
class="user-panel__status-option"
|
||||||
|
:class="{ 'is-active': currentStatus.key === status.key }"
|
||||||
|
@click="selectStatus(status)"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="user-panel__status-dot"
|
||||||
|
:style="{ backgroundColor: status.color }"
|
||||||
|
></span>
|
||||||
|
<span>{{ status.label }}</span>
|
||||||
|
<el-icon
|
||||||
|
v-if="currentStatus.key === status.key"
|
||||||
|
:size="14"
|
||||||
|
class="user-panel__check"
|
||||||
|
>
|
||||||
|
<Check />
|
||||||
|
</el-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="user-panel__divider"></div>
|
||||||
|
|
||||||
|
<!-- 操作区:复制账号ID + 退出登录 -->
|
||||||
|
<div class="user-panel__section">
|
||||||
|
<div class="user-panel__item" @click="handleCopyUserId">
|
||||||
|
<el-icon :size="18"><DocumentCopy /></el-icon>
|
||||||
|
<span>复制账号ID</span>
|
||||||
|
</div>
|
||||||
|
<div class="user-panel__item user-panel__item--danger" @click="handleLogout">
|
||||||
|
<el-icon :size="18"><SwitchButton /></el-icon>
|
||||||
|
<span>退出登录</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</el-popover>
|
</el-popover>
|
||||||
|
|
||||||
<div class="feature">
|
<div class="feature">
|
||||||
@@ -28,10 +122,111 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Setting } from '@element-plus/icons-vue'
|
import { computed, ref } from 'vue'
|
||||||
|
import { storeToRefs } from 'pinia'
|
||||||
|
import { ElMessage } from 'element-plus'
|
||||||
|
import {
|
||||||
|
Setting,
|
||||||
|
EditPen,
|
||||||
|
ArrowRight,
|
||||||
|
Check,
|
||||||
|
DocumentCopy,
|
||||||
|
SwitchButton
|
||||||
|
} from '@element-plus/icons-vue'
|
||||||
|
import { useAccountStore } from '@/stores'
|
||||||
|
|
||||||
|
/** 在线状态选项 */
|
||||||
|
interface StatusOption {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
color: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const STATUS_OPTIONS: StatusOption[] = [
|
||||||
|
{ key: 'online', label: '在线', color: 'var(--color-success)' },
|
||||||
|
{ key: 'away', label: '离开', color: 'var(--color-warning)' },
|
||||||
|
{ key: 'dnd', label: '请勿打扰', color: 'var(--color-danger)' },
|
||||||
|
{ key: 'invisible', label: '隐身', color: 'var(--text-tertiary)' }
|
||||||
|
]
|
||||||
|
|
||||||
|
const accountStore = useAccountStore()
|
||||||
|
const { accountInfo } = storeToRefs(accountStore)
|
||||||
|
|
||||||
|
/** 当前在线状态(默认:在线) */
|
||||||
|
const currentStatus = ref<StatusOption>(STATUS_OPTIONS[0]!)
|
||||||
|
/** 状态二级菜单是否展开 */
|
||||||
|
const showStatusMenu = ref(false)
|
||||||
|
/** 在线状态选项列表 */
|
||||||
|
const statusOptions = STATUS_OPTIONS
|
||||||
|
|
||||||
|
/** 用户头像(优先使用 store 中的头像,否则使用默认占位) */
|
||||||
|
const userAvatar = computed(() => {
|
||||||
|
return accountInfo.value?.avatar || ''
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 显示昵称(未登录或未加载时显示默认文案) */
|
||||||
|
const displayName = computed(() => {
|
||||||
|
return accountInfo.value?.nickName || '未登录'
|
||||||
|
})
|
||||||
|
|
||||||
|
/** 个性签名(无签名时显示默认文案) */
|
||||||
|
const signatureText = computed(() => {
|
||||||
|
return accountInfo.value?.signature || '这个人很懒,什么都没写...'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 编辑个人资料
|
||||||
|
* 预留功能入口,后续可弹出编辑弹窗或跳转到设置页
|
||||||
|
*/
|
||||||
|
const handleEditProfile = () => {
|
||||||
|
ElMessage.info('编辑个人资料功能开发中')
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 展开/收起在线状态二级菜单 */
|
||||||
|
const toggleStatusMenu = () => {
|
||||||
|
showStatusMenu.value = !showStatusMenu.value
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择一个在线状态
|
||||||
|
* @param status - 选中的状态选项
|
||||||
|
*/
|
||||||
|
const selectStatus = (status: StatusOption) => {
|
||||||
|
currentStatus.value = status
|
||||||
|
showStatusMenu.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 复制账号ID到剪贴板
|
||||||
|
* 复制用户的 UUID 标识符
|
||||||
|
*/
|
||||||
|
const handleCopyUserId = async () => {
|
||||||
|
const userId = accountInfo.value?.userId
|
||||||
|
if (!userId) {
|
||||||
|
ElMessage.warning('账号ID获取失败,请刷新后重试')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(userId)
|
||||||
|
ElMessage.success('账号ID已复制到剪贴板')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('复制失败,请手动复制')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 退出登录
|
||||||
|
* 调用 store 清除认证状态,后续可跳转至登录页
|
||||||
|
*/
|
||||||
|
const handleLogout = () => {
|
||||||
|
accountStore.logout()
|
||||||
|
ElMessage.success('已退出登录')
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped lang="scss">
|
<style scoped lang="scss">
|
||||||
|
/* ========== user-bar 容器 ========== */
|
||||||
.user-bar {
|
.user-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -47,9 +242,8 @@ import { Setting } from '@element-plus/icons-vue'
|
|||||||
width: 12px;
|
width: 12px;
|
||||||
height: 12px;
|
height: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user-badge :deep(.el-badge__content.is-dot) {
|
.user-badge :deep(.el-badge__content.is-dot) {
|
||||||
background-color: var(--color-success);
|
background-color: v-bind('currentStatus.color');
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
@@ -80,7 +274,6 @@ import { Setting } from '@element-plus/icons-vue'
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 2px;
|
gap: 2px;
|
||||||
/* 默认内容宽度,超长时上限 160px 触发省略 */
|
|
||||||
max-width: 160px;
|
max-width: 160px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
@@ -122,16 +315,199 @@ import { Setting } from '@element-plus/icons-vue'
|
|||||||
background-color: var(--bg-surface-hover);
|
background-color: var(--bg-surface-hover);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
|
|
||||||
/* 移入时缓动旋转 180°,完成后保持 */
|
|
||||||
:deep(.el-icon) {
|
:deep(.el-icon) {
|
||||||
transition: transform 0.6s cubic-bezier(0.6, 0, 1, 1);
|
transition: transform 0.6s cubic-bezier(0.6, 0, 1, 1);
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 移出时无过渡,瞬间复位 */
|
|
||||||
:deep(.el-icon) {
|
:deep(.el-icon) {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ========== 弹出面板 ========== */
|
||||||
|
.user-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 顶部头像区 ---- */
|
||||||
|
.user-panel__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 4px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__avatar {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__identity {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__nickname {
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.3;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 签名区 ---- */
|
||||||
|
.user-panel__signature {
|
||||||
|
padding: 8px 0;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 分隔线 ---- */
|
||||||
|
.user-panel__divider {
|
||||||
|
height: 1px;
|
||||||
|
background-color: var(--border-light);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 操作区 ---- */
|
||||||
|
.user-panel__section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-panel__item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 10px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 危险操作(退出登录) */
|
||||||
|
.user-panel__item--danger {
|
||||||
|
color: var(--color-danger);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-danger-light);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 在线状态组(父项 + 二级菜单) ---- */
|
||||||
|
.user-panel__status-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前选中状态标签(显示在父项右侧) */
|
||||||
|
.user-panel__status-current {
|
||||||
|
margin-left: auto;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 父项中的箭头图标 */
|
||||||
|
.user-panel__chevron {
|
||||||
|
color: var(--text-tertiary);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 二级菜单容器 ---- */
|
||||||
|
.user-panel__status-submenu {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 2px 0 4px 28px; /* 左侧缩进,与父项文字对齐 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 二级菜单单项 */
|
||||||
|
.user-panel__status-option {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 13px;
|
||||||
|
color: var(--text-primary);
|
||||||
|
transition: background-color 0.12s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--bg-surface-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 当前选中项的对勾图标 */
|
||||||
|
.user-panel__check {
|
||||||
|
margin-left: auto;
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- 二级菜单展开/收起动画 ---- */
|
||||||
|
.status-menu-enter-active {
|
||||||
|
transition:
|
||||||
|
max-height 0.25s ease,
|
||||||
|
opacity 0.2s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.status-menu-leave-active {
|
||||||
|
transition:
|
||||||
|
max-height 0.2s ease,
|
||||||
|
opacity 0.15s ease;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.status-menu-enter-from,
|
||||||
|
.status-menu-leave-to {
|
||||||
|
max-height: 0;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
.status-menu-enter-to,
|
||||||
|
.status-menu-leave-from {
|
||||||
|
max-height: 200px;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- 覆盖 el-popover 默认样式(非 scoped,因为 popover 渲染在 body 下) -->
|
||||||
|
<style lang="scss">
|
||||||
|
.user-popover {
|
||||||
|
padding: 8px 12px !important;
|
||||||
|
border-radius: var(--radius-md) !important;
|
||||||
|
box-shadow: var(--shadow-lg) !important;
|
||||||
|
border: 1px solid var(--border-default) !important;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ import 'element-plus/dist/index.css'
|
|||||||
|
|
||||||
import { createApp } from 'vue'
|
import { createApp } from 'vue'
|
||||||
import { initPinia } from './stores'
|
import { initPinia } from './stores'
|
||||||
import { setupMock } from './api/mock'
|
|
||||||
|
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import router from './router'
|
import router from './router'
|
||||||
|
|
||||||
/** 开发环境启用 Mock 数据拦截 */
|
|
||||||
setupMock()
|
|
||||||
|
|
||||||
const app = createApp(App)
|
const app = createApp(App)
|
||||||
|
|
||||||
app.use(initPinia())
|
app.use(initPinia())
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import { getUserInfo } from '@/api/modules/user'
|
|||||||
|
|
||||||
/** 账户信息 */
|
/** 账户信息 */
|
||||||
export interface AccountState {
|
export interface AccountState {
|
||||||
|
/** 用户唯一标识(UUID) */
|
||||||
|
userId?: string
|
||||||
/** 用户昵称 */
|
/** 用户昵称 */
|
||||||
nickName?: string
|
nickName?: string
|
||||||
/** 头像 */
|
/** 头像 */
|
||||||
|
|||||||
Reference in New Issue
Block a user