fix: 路由bug修改

This commit is contained in:
2026-06-23 12:52:53 +08:00
parent 88ab464d6a
commit f34dbeccb2
5 changed files with 168 additions and 38 deletions

View File

@@ -4,7 +4,9 @@ import type {
CreateRoomResponse, CreateRoomResponse,
GetRoomListRequest, GetRoomListRequest,
GetRoomListResponse, GetRoomListResponse,
RoomDetailResponse JoinRoomRequest,
RoomDetailResponse,
RoomItem
} from './types' } from './types'
/** /**
@@ -37,6 +39,19 @@ export function createRoom(data: CreateRoomRequest) {
return requestClient.post<CreateRoomResponse>('/game/room/create', data) return requestClient.post<CreateRoomResponse>('/game/room/create', data)
} }
/**
* 加入房间
*
* 玩家通过此接口加入指定房间,成为房间内的玩家。
* 公开房间无需传密码。加入成功后需通过 WebSocket 进行后续交互。
*
* @param data - 加入房间参数roomId 和可选的 password
* @returns 更新后的房间信息
*/
export function joinRoom(data: JoinRoomRequest) {
return requestClient.post<RoomItem>('/game/room/join', data)
}
/** /**
* 获取房间详情 * 获取房间详情
* *

View File

@@ -151,6 +151,14 @@ export interface MoveItem {
col: number col: number
} }
/** 加入房间请求参数 */
export interface JoinRoomRequest {
/** 房间 ID */
roomId: string
/** 房间密码(可选,公开房间留空) */
password?: string
}
/** WebSocket 消息通用封装 */ /** WebSocket 消息通用封装 */
export interface WsMessage<T = unknown> { export interface WsMessage<T = unknown> {
type: string type: string

View File

@@ -1,25 +1,40 @@
import type { WsMessage } from '@/api/modules/game' import type { WsMessage } from '@/api/modules/game'
/** 重连配置 */
const RECONNECT_MAX_RETRIES = 5
/** 重连基础延迟ms指数退避1s, 2s, 4s, 8s, 16s */
const RECONNECT_BASE_DELAY = 1000
/** 重连最大延迟ms */
const RECONNECT_MAX_DELAY = 16000
/** WebSocket 连接回调 */ /** WebSocket 连接回调 */
export interface RoomSocketCallbacks { export interface RoomSocketCallbacks {
onOpen?: () => void onOpen?: () => void
onClose?: (event: CloseEvent) => void onClose?: (event: CloseEvent) => void
onError?: (event: Event) => void onError?: (event: Event) => void
onMessage?: (message: WsMessage) => void onMessage?: (message: WsMessage) => void
/** 自动重连成功回调(重连后需调用方重新拉取房间状态) */
onReconnect?: () => void
/** 重连次数耗尽回调 */
onReconnectFailed?: () => void
} }
/** WebSocket 客户端封装 */ /** WebSocket 客户端封装 */
export interface RoomSocket { export interface RoomSocket {
/** 发送消息 */ /** 发送消息 */
send: (type: string, data?: unknown) => void send: (type: string, data?: unknown) => void
/** 关闭连接 */ /** 关闭连接(主动关闭不触发重连) */
close: () => void close: () => void
/** 连接状态 */ /** 连接状态 */
isOpen: () => boolean isOpen: () => boolean
} }
/** /**
* 创建房间 WebSocket 连接 * 创建房间 WebSocket 连接(含自动重连)
*
* 连接断开时自动指数退避重连,最多重试 5 次。
* 主动调用 close() 或重连耗尽时触发 onClose 回调。
* 重连成功后触发 onReconnect 回调,调用方应在此重新拉取房间状态。
* *
* @param roomId 房间 ID * @param roomId 房间 ID
* @param token 用户认证 token * @param token 用户认证 token
@@ -33,45 +48,113 @@ export function createRoomSocket(
): RoomSocket { ): RoomSocket {
const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/room?roomId=${encodeURIComponent(roomId)}&token=${encodeURIComponent(token)}` const wsUrl = `${window.location.protocol === 'https:' ? 'wss:' : 'ws:'}//${window.location.host}/ws/room?roomId=${encodeURIComponent(roomId)}&token=${encodeURIComponent(token)}`
const ws = new WebSocket(wsUrl) /** 当前 WebSocket 实例 */
let ws: WebSocket | null = null
/** 当前重试次数 */
let retryCount = 0
/** 是否主动关闭(主动关闭不重连) */
let intentionalClose = false
/** 重连定时器 ID用于组件卸载时取消 */
let reconnectTimer: ReturnType<typeof setTimeout> | null = null
ws.onopen = () => { /** 建立 WebSocket 连接 */
// eslint-disable-next-line no-console function connect(): void {
console.log('[WS] 房间连接已建立, roomId=', roomId) // 如果已有旧的重连定时器,取消它
callbacks.onOpen?.() if (reconnectTimer) {
} clearTimeout(reconnectTimer)
reconnectTimer = null
}
ws.onclose = (event) => { ws = new WebSocket(wsUrl)
// eslint-disable-next-line no-console
console.log('[WS] 房间连接已关闭, roomId=', roomId, 'code=', event.code)
callbacks.onClose?.(event)
}
ws.onerror = (event) => { ws.onopen = () => {
console.error('[WS] 房间连接错误, roomId=', roomId, event) // eslint-disable-next-line no-console
callbacks.onError?.(event) console.log('[WS] 房间连接已建立, roomId=', roomId)
} if (retryCount > 0) {
// 这是重连成功
// eslint-disable-next-line no-console
console.log('[WS] 重连成功, roomId=', roomId, '重试次数=', retryCount)
retryCount = 0
callbacks.onReconnect?.()
} else {
callbacks.onOpen?.()
}
}
ws.onmessage = (event) => { ws.onclose = (event) => {
try { // 主动关闭,不重连
const message: WsMessage = JSON.parse(event.data as string) if (intentionalClose) {
callbacks.onMessage?.(message) // eslint-disable-next-line no-console
} catch (e) { console.log('[WS] 主动关闭连接, roomId=', roomId, 'code=', event.code)
console.error('[WS] 消息解析失败:', e) callbacks.onClose?.(event)
return
}
// 已达最大重试次数
if (retryCount >= RECONNECT_MAX_RETRIES) {
// eslint-disable-next-line no-console
console.log('[WS] 重连次数耗尽, roomId=', roomId, 'retries=', retryCount)
callbacks.onReconnectFailed?.()
callbacks.onClose?.(event)
return
}
// 指数退避重连
retryCount++
const delay = Math.min(
RECONNECT_BASE_DELAY * Math.pow(2, retryCount - 1),
RECONNECT_MAX_DELAY
)
// eslint-disable-next-line no-console
console.log(
`[WS] 连接断开,${delay / 1000}s 后重连 (${retryCount}/${RECONNECT_MAX_RETRIES}), roomId=`,
roomId
)
reconnectTimer = setTimeout(() => {
reconnectTimer = null
connect()
}, delay)
}
ws.onerror = (event) => {
console.error('[WS] 连接错误, roomId=', roomId, event)
callbacks.onError?.(event)
}
ws.onmessage = (event) => {
try {
const message: WsMessage = JSON.parse(event.data as string)
callbacks.onMessage?.(message)
} catch (e) {
console.error('[WS] 消息解析失败:', e)
}
} }
} }
// 初始连接
connect()
return { return {
send(type: string, data?: unknown) { send(type: string, data?: unknown) {
if (ws.readyState === WebSocket.OPEN) { if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type, data })) ws.send(JSON.stringify({ type, data }))
} }
}, },
close() { close() {
ws.close(1000, '用户离开') intentionalClose = true
// 取消进行中的重连定时器
if (reconnectTimer) {
clearTimeout(reconnectTimer)
reconnectTimer = null
}
if (ws) {
ws.close(1000, '用户离开')
ws = null
}
}, },
isOpen() { isOpen() {
return ws.readyState === WebSocket.OPEN return ws?.readyState === WebSocket.OPEN
} }
} }
} }

View File

@@ -51,8 +51,9 @@ import { useRouter } from 'vue-router'
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'
import { getRoomList } from '@/api/modules/game' import { getRoomList, joinRoom } from '@/api/modules/game'
import { sseBus } from '@/stores/sseBus' import { sseBus } from '@/stores/sseBus'
import { ElMessage } from 'element-plus'
/** 状态筛选选项 */ /** 状态筛选选项 */
const statusFilters = [ const statusFilters = [
@@ -129,17 +130,26 @@ const filteredRooms = computed(() => {
return list return list
}) })
/** 加入房间 */ /** 加入房间(需先调用后端 join API 成为玩家) */
function handleJoin(roomId: string) { async function handleJoin(roomId: string) {
router.push(`/room/${roomId}`) try {
await joinRoom({ roomId })
router.push(`/room/${roomId}`)
} catch (e: unknown) {
// 后端业务错误通过 ApiResponse.message 传递,需兼容多种 reject 类型
const msg =
(e as { message?: string })?.message ||
(e instanceof Error ? e.message : '加入房间失败,请稍后重试')
ElMessage.error(msg)
}
} }
/** 加入指定座位 */ /** 加入指定座位(后端自动分配座位,调用 join API 即可) */
function handleJoinSeat(roomId: string, seatIndex: number) { async function handleJoinSeat(roomId: string, _seatIndex: number) {
router.push({ path: `/room/${roomId}`, query: { seat: String(seatIndex) } }) await handleJoin(roomId)
} }
/** 观战 */ /** 观战(不调用 join API仅建立 WebSocket 连接接收对局数据) */
function handleSpectate(roomId: string) { function handleSpectate(roomId: string) {
router.push(`/room/${roomId}`) router.push(`/room/${roomId}`)
} }

View File

@@ -76,12 +76,26 @@ onMounted(async () => {
const res = await getRoomDetail(roomId) const res = await getRoomDetail(roomId)
roomStore.initFromDetail(res.data, userId) roomStore.initFromDetail(res.data, userId)
// 2. 建立 WebSocket 连接 // 2. 建立 WebSocket 连接(含自动重连)
const token = accountStore.accessToken const token = accountStore.accessToken
socket = createRoomSocket(roomId, token, { socket = createRoomSocket(roomId, token, {
onMessage: handleWsMessage, onMessage: handleWsMessage,
onClose: () => { /**
// 连接关闭,提示并返回大厅 * 重连成功 → 重新拉取房间详情恢复完整状态(棋盘、轮次、玩家列表等)
*/
onReconnect: async () => {
try {
const res = await getRoomDetail(roomId)
roomStore.initFromDetail(res.data, userId)
} catch {
// 重连后拉取房间状态失败(房间可能已结束),返回大厅
router.push('/')
}
},
/**
* 重连次数耗尽 → 返回大厅
*/
onReconnectFailed: () => {
router.push('/') router.push('/')
} }
}) })