fix: 路由bug修改
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取房间详情
|
* 获取房间详情
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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}`)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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('/')
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user