feat(game): 实现游戏房间状态管理和WebSocket连接功能
- 添加路由参数解析和房间状态初始化逻辑 - 实现房间玩家座位视图计算和状态映射 - 集成WebSocket客户端连接管理和重连机制 - 添加房间数据持久化存储功能 - 实现游戏界面状态显示和用户交互控制 - 更新WS代理目标地址配置 - 重构房间状态管理模块分离到独立store
This commit is contained in:
@@ -148,7 +148,7 @@
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
left: 58px;
|
||||
min-width: 124px;
|
||||
min-width: 128px;
|
||||
padding: 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
|
||||
45
src/store/index.ts
Normal file
45
src/store/index.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { ref } from 'vue'
|
||||
import type {
|
||||
ActiveRoomState,
|
||||
ActiveRoomSelectionInput,
|
||||
} from './state'
|
||||
import { readActiveRoomSnapshot, saveActiveRoom } from './storage'
|
||||
|
||||
const activeRoom = ref<ActiveRoomState | null>(readActiveRoomSnapshot())
|
||||
|
||||
function normalizeRoom(input: ActiveRoomSelectionInput): ActiveRoomState {
|
||||
return {
|
||||
roomId: input.roomId,
|
||||
roomName: input.roomName ?? '',
|
||||
gameType: input.gameType ?? 'chengdu',
|
||||
ownerId: input.ownerId ?? '',
|
||||
maxPlayers: input.maxPlayers ?? 4,
|
||||
playerCount: input.playerCount ?? input.players?.length ?? 0,
|
||||
status: input.status ?? 'waiting',
|
||||
createdAt: input.createdAt ?? '',
|
||||
updatedAt: input.updatedAt ?? '',
|
||||
players: input.players ?? [],
|
||||
myHand: [],
|
||||
game: {
|
||||
state: {
|
||||
wall: [],
|
||||
scores: {},
|
||||
dealerIndex: -1,
|
||||
currentTurn: -1,
|
||||
phase: 'waiting',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前房间
|
||||
export function setActiveRoom(input: ActiveRoomSelectionInput) {
|
||||
const next = normalizeRoom(input)
|
||||
activeRoom.value = next
|
||||
saveActiveRoom(next)
|
||||
}
|
||||
|
||||
// 使用房间状态
|
||||
export function useActiveRoomState() {
|
||||
return activeRoom
|
||||
}
|
||||
49
src/store/state.ts
Normal file
49
src/store/state.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// 房间玩家状态
|
||||
export interface RoomPlayerState {
|
||||
index: number
|
||||
playerId: string
|
||||
displayName?: string
|
||||
missingSuit?: string | null
|
||||
ready: boolean
|
||||
hand: string[]
|
||||
melds: string[]
|
||||
outTiles: string[]
|
||||
hasHu: boolean
|
||||
}
|
||||
|
||||
// 房间整体状态
|
||||
export interface ActiveRoomState {
|
||||
roomId: string
|
||||
roomName: string
|
||||
gameType: string
|
||||
ownerId: string
|
||||
maxPlayers: number
|
||||
playerCount: number
|
||||
status: string
|
||||
createdAt: string
|
||||
updatedAt: string
|
||||
players: RoomPlayerState[]
|
||||
myHand: string[]
|
||||
game?: {
|
||||
state?: {
|
||||
wall?: string[]
|
||||
scores?: Record<string, number>
|
||||
dealerIndex?: number
|
||||
currentTurn?: number
|
||||
phase?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface ActiveRoomSelectionInput {
|
||||
roomId: string
|
||||
roomName?: string
|
||||
gameType?: string
|
||||
ownerId?: string
|
||||
maxPlayers?: number
|
||||
playerCount?: number
|
||||
status?: string
|
||||
createdAt?: string
|
||||
updatedAt?: string
|
||||
players?: RoomPlayerState[]
|
||||
}
|
||||
20
src/store/storage.ts
Normal file
20
src/store/storage.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import type { ActiveRoomState } from './state'
|
||||
|
||||
const KEY = 'mahjong_active_room'
|
||||
|
||||
// 读取缓存
|
||||
export function readActiveRoomSnapshot(): ActiveRoomState | null {
|
||||
const raw = localStorage.getItem(KEY)
|
||||
if (!raw) return null
|
||||
|
||||
try {
|
||||
return JSON.parse(raw)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
// 写入缓存
|
||||
export function saveActiveRoom(state: ActiveRoomState) {
|
||||
localStorage.setItem(KEY, JSON.stringify(state))
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {Tile} from "../../models";
|
||||
import type { Tile } from '../tile'
|
||||
|
||||
export type Meld =
|
||||
| {
|
||||
@@ -14,4 +14,4 @@ export type Meld =
|
||||
| {
|
||||
type: 'an_gang'
|
||||
tiles: Tile[]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Tile} from "../../models";
|
||||
import type {Meld} from "./meld.ts";
|
||||
import type { Tile } from '../tile'
|
||||
import type { Meld } from './meld'
|
||||
|
||||
export interface Player{
|
||||
playerId: string
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import {computed, onBeforeUnmount, onMounted, ref} from 'vue'
|
||||
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
import deskImage from '../assets/images/desk/desk_01.png'
|
||||
import wanIcon from '../assets/images/flowerClolor/wan.png'
|
||||
import tongIcon from '../assets/images/flowerClolor/tong.png'
|
||||
@@ -15,11 +16,63 @@ import TopPlayerCard from '../components/game/TopPlayerCard.vue'
|
||||
import RightPlayerCard from '../components/game/RightPlayerCard.vue'
|
||||
import BottomPlayerCard from '../components/game/BottomPlayerCard.vue'
|
||||
import LeftPlayerCard from '../components/game/LeftPlayerCard.vue'
|
||||
import type {SeatPlayerCardModel} from '../components/game/seat-player-card'
|
||||
import type {WsStatus} from "../ws/client.ts";
|
||||
import {wsClient} from "../ws/client.ts";
|
||||
import type { SeatPlayerCardModel } from '../components/game/seat-player-card'
|
||||
import type { SeatKey } from '../game/seat'
|
||||
import { readStoredAuth } from '../utils/auth-storage'
|
||||
import type { WsStatus } from '../ws/client'
|
||||
import { wsClient } from '../ws/client'
|
||||
import { buildWsUrl } from '../ws/url'
|
||||
import type {ActiveRoomState, RoomPlayerState} from "../store/state.ts";
|
||||
import {readActiveRoomSnapshot} from "../store/storage.ts";
|
||||
|
||||
|
||||
interface SeatViewModel {
|
||||
key: SeatKey
|
||||
player?: RoomPlayerState
|
||||
isSelf: boolean
|
||||
isTurn: boolean
|
||||
}
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const auth = ref(readStoredAuth())
|
||||
|
||||
function createFallbackRoomState(): ActiveRoomState {
|
||||
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
||||
const routeRoomName = typeof route.query.roomName === 'string' ? route.query.roomName : ''
|
||||
|
||||
return {
|
||||
roomId: routeRoomId,
|
||||
roomName: routeRoomName,
|
||||
gameType: 'chengdu',
|
||||
ownerId: '',
|
||||
maxPlayers: 4,
|
||||
playerCount: 0,
|
||||
status: 'waiting',
|
||||
createdAt: '',
|
||||
updatedAt: '',
|
||||
players: [],
|
||||
myHand: [],
|
||||
game: {
|
||||
state: {
|
||||
wall: [],
|
||||
scores: {},
|
||||
dealerIndex: -1,
|
||||
currentTurn: -1,
|
||||
phase: 'waiting',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const activeRoom = ref<ActiveRoomState>(readActiveRoomSnapshot() ?? createFallbackRoomState())
|
||||
|
||||
const now = ref(Date.now())
|
||||
const wsStatus = ref<WsStatus>('idle')
|
||||
const wsMessages = ref<string[]>([])
|
||||
const wsError = ref('')
|
||||
const selectedTile = ref<string | null>(null)
|
||||
const leaveRoomPending = ref(false)
|
||||
let clockTimer: number | null = null
|
||||
let unsubscribe: (() => void) | null = null
|
||||
|
||||
@@ -29,9 +82,76 @@ const menuTriggerActive = ref(false)
|
||||
let menuTriggerTimer: number | null = null
|
||||
let menuOpenTimer: number | null = null
|
||||
|
||||
const loggedInUserId = computed(() => {
|
||||
const rawId = auth.value?.user?.id
|
||||
if (typeof rawId === 'string') {
|
||||
return rawId
|
||||
}
|
||||
if (typeof rawId === 'number') {
|
||||
return String(rawId)
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const loggedInUserName = computed(() => {
|
||||
return auth.value?.user?.nickname || auth.value?.user?.username || ''
|
||||
})
|
||||
|
||||
const roomName = computed(() => {
|
||||
const queryRoomName = typeof route.query.roomName === 'string' ? route.query.roomName : ''
|
||||
return queryRoomName || activeRoom.value.roomName || ''
|
||||
})
|
||||
|
||||
const roomState = computed(() => {
|
||||
return {
|
||||
...activeRoom.value,
|
||||
name: activeRoom.value.roomName,
|
||||
}
|
||||
})
|
||||
|
||||
const seatViews = computed<SeatViewModel[]>(() => {
|
||||
const players = roomState.value.players ?? []
|
||||
const tableOrder: SeatKey[] = ['bottom', 'right', 'top', 'left']
|
||||
const selfIndex = players.findIndex((player) => player.playerId === loggedInUserId.value)
|
||||
const currentTurn = roomState.value.game?.state?.currentTurn
|
||||
|
||||
return players.slice(0, 4).map((player, index) => {
|
||||
const relativeIndex = selfIndex >= 0 ? (index - selfIndex + 4) % 4 : index
|
||||
const seatKey = tableOrder[relativeIndex] ?? 'top'
|
||||
return {
|
||||
key: seatKey,
|
||||
player,
|
||||
isSelf: player.playerId === loggedInUserId.value,
|
||||
isTurn: typeof currentTurn === 'number' && player.index === currentTurn,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const rightMessages = computed(() => wsMessages.value.slice(-16).reverse())
|
||||
|
||||
const currentPhaseText = computed(() => {
|
||||
const phase = roomState.value.game?.state?.phase
|
||||
const map: Record<string, string> = {
|
||||
waiting: '等待中',
|
||||
dealing: '发牌中',
|
||||
playing: '对局中',
|
||||
finished: '已结束',
|
||||
}
|
||||
return phase ? (map[phase] ?? phase) : '等待中'
|
||||
})
|
||||
|
||||
const roomStatusText = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
waiting: '等待玩家',
|
||||
playing: '游戏中',
|
||||
finished: '已结束',
|
||||
}
|
||||
const status = roomState.value.status
|
||||
return map[status] ?? status ?? '--'
|
||||
})
|
||||
|
||||
const networkLabel = computed(() => {
|
||||
const map: Record<string, string> = {
|
||||
const map: Record<WsStatus, string> = {
|
||||
connected: '已连接',
|
||||
connecting: '连接中',
|
||||
error: '连接异常',
|
||||
@@ -52,7 +172,7 @@ const formattedClock = computed(() => {
|
||||
})
|
||||
|
||||
const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
||||
const wallSize = roomState.value.game?.state?.wall.length ?? 0
|
||||
const wallSize = roomState.value.game?.state?.wall?.length ?? 0
|
||||
const perSide = Math.max(6, Math.ceil((wallSize || 48) / 4 / 2))
|
||||
|
||||
return {
|
||||
@@ -64,7 +184,6 @@ const wallBacks = computed<Record<SeatKey, string[]>>(() => {
|
||||
})
|
||||
|
||||
const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
const scoreMap = roomState.value.game?.state?.scores ?? {}
|
||||
const dealerIndex = roomState.value.game?.state?.dealerIndex ?? -1
|
||||
const defaultMissingSuitLabel = missingSuitLabel(null)
|
||||
|
||||
@@ -88,15 +207,12 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
continue
|
||||
}
|
||||
|
||||
const playerId = seat.player.playerId
|
||||
const score = scoreMap[playerId]
|
||||
const displayName = seat.player.displayName || `玩家${seat.player.index + 1}`
|
||||
result[seat.key] = {
|
||||
avatar: seat.isSelf ? '我' : String(index + 1),
|
||||
name: seat.isSelf ? '你自己' : seat.player.displayName || `玩家${seat.player.index + 1}`,
|
||||
money: typeof score === 'number' ? String(score) : '--',
|
||||
name: seat.isSelf ? '你自己' : displayName,
|
||||
dealer: seat.player.index === dealerIndex,
|
||||
isTurn: seat.isTurn,
|
||||
isOnline: true,
|
||||
missingSuitLabel: missingSuitLabel(seat.player.missingSuit),
|
||||
}
|
||||
}
|
||||
@@ -104,8 +220,6 @@ const seatDecor = computed<Record<SeatKey, SeatPlayerCardModel>>(() => {
|
||||
return result
|
||||
})
|
||||
|
||||
const rightMessages = computed(() => wsMessages.value.slice(-16).reverse())
|
||||
|
||||
const floatingMissingSuit = computed(() => {
|
||||
const suitMap: Record<string, string> = {
|
||||
万: wanIcon,
|
||||
@@ -113,10 +227,14 @@ const floatingMissingSuit = computed(() => {
|
||||
条: tiaoIcon,
|
||||
}
|
||||
|
||||
const topLabel = seatDecor.value.top?.missingSuitLabel ?? ''
|
||||
const leftLabel = seatDecor.value.left?.missingSuitLabel ?? ''
|
||||
const rightLabel = seatDecor.value.right?.missingSuitLabel ?? ''
|
||||
|
||||
return {
|
||||
top: suitMap[seatDecor.value.top.missingSuitLabel] ?? '',
|
||||
left: suitMap[seatDecor.value.left.missingSuitLabel] ?? '',
|
||||
right: suitMap[seatDecor.value.right.missingSuitLabel] ?? '',
|
||||
top: suitMap[topLabel] ?? '',
|
||||
left: suitMap[leftLabel] ?? '',
|
||||
right: suitMap[rightLabel] ?? '',
|
||||
}
|
||||
})
|
||||
|
||||
@@ -127,6 +245,9 @@ function missingSuitLabel(value: string | null | undefined): string {
|
||||
tiao: '条',
|
||||
}
|
||||
|
||||
if (!value) {
|
||||
return ''
|
||||
}
|
||||
return suitMap[value] ?? value
|
||||
}
|
||||
|
||||
@@ -170,6 +291,40 @@ function toggleTrustMode(): void {
|
||||
menuOpen.value = false
|
||||
}
|
||||
|
||||
function selectTile(tile: string): void {
|
||||
selectedTile.value = selectedTile.value === tile ? null : tile
|
||||
}
|
||||
|
||||
function ensureWsConnected(): void {
|
||||
const token = auth.value?.token
|
||||
if (!token) {
|
||||
wsError.value = '未找到登录凭证,无法建立连接'
|
||||
return
|
||||
}
|
||||
|
||||
wsError.value = ''
|
||||
wsClient.connect(buildWsUrl(token), token)
|
||||
}
|
||||
|
||||
function reconnectWs(): void {
|
||||
const token = auth.value?.token
|
||||
if (!token) {
|
||||
wsError.value = '未找到登录凭证,无法建立连接'
|
||||
return
|
||||
}
|
||||
|
||||
wsError.value = ''
|
||||
wsClient.reconnect(buildWsUrl(token), token)
|
||||
}
|
||||
|
||||
function backHall(): void {
|
||||
leaveRoomPending.value = true
|
||||
wsClient.close()
|
||||
void router.push('/hall').finally(() => {
|
||||
leaveRoomPending.value = false
|
||||
})
|
||||
}
|
||||
|
||||
function handleLeaveRoom(): void {
|
||||
menuOpen.value = false
|
||||
backHall()
|
||||
@@ -196,12 +351,35 @@ function handleGlobalEsc(event: KeyboardEvent): void {
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const handler = (status: any) => {
|
||||
WsStatus.value = status
|
||||
const routeRoomId = typeof route.params.roomId === 'string' ? route.params.roomId : ''
|
||||
if (routeRoomId && activeRoom.value.roomId !== routeRoomId) {
|
||||
activeRoom.value = {
|
||||
...activeRoom.value,
|
||||
roomId: routeRoomId,
|
||||
}
|
||||
}
|
||||
if (!activeRoom.value.roomName && typeof route.query.roomName === 'string') {
|
||||
activeRoom.value = {
|
||||
...activeRoom.value,
|
||||
roomName: route.query.roomName,
|
||||
}
|
||||
}
|
||||
|
||||
// 保存取消订阅函数
|
||||
const handler = (status: WsStatus) => {
|
||||
wsStatus.value = status
|
||||
}
|
||||
|
||||
wsClient.onMessage((msg: unknown) => {
|
||||
const text = typeof msg === 'string' ? msg : JSON.stringify(msg)
|
||||
wsMessages.value.push(`[server] ${text}`)
|
||||
})
|
||||
wsClient.onError((message: string) => {
|
||||
wsError.value = message
|
||||
wsMessages.value.push(`[error] ${message}`)
|
||||
})
|
||||
|
||||
unsubscribe = wsClient.onStatusChange(handler)
|
||||
ensureWsConnected()
|
||||
|
||||
clockTimer = window.setInterval(() => {
|
||||
now.value = Date.now()
|
||||
@@ -212,7 +390,6 @@ onMounted(() => {
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
// 取消 ws 订阅
|
||||
if (unsubscribe) {
|
||||
unsubscribe()
|
||||
unsubscribe = null
|
||||
@@ -286,7 +463,7 @@ onBeforeUnmount(() => {
|
||||
</div>
|
||||
<div class="left-counter">
|
||||
<span class="counter-light"></span>
|
||||
<strong>{{ roomState.game?.state?.wall.length ?? 48 }}</strong>
|
||||
<strong>{{ roomState.game?.state?.wall?.length ?? 48 }}</strong>
|
||||
</div>
|
||||
<span v-if="isTrustMode" class="trust-chip">托管中</span>
|
||||
</div>
|
||||
@@ -356,7 +533,7 @@ onBeforeUnmount(() => {
|
||||
<p class="sidebar-title">WebSocket 消息</p>
|
||||
<small>{{ networkLabel }} · {{ loggedInUserName || '未登录昵称' }}</small>
|
||||
</div>
|
||||
<button class="sidebar-btn" type="button" @click="connectWs">重连</button>
|
||||
<button class="sidebar-btn" type="button" @click="reconnectWs">重连</button>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-stats">
|
||||
|
||||
@@ -4,10 +4,12 @@ import { useRouter } from 'vue-router'
|
||||
import { AuthExpiredError, type AuthSession } from '../api/authed-request'
|
||||
import { createRoom, joinRoom, listRooms, type RoomItem } from '../api/mahjong'
|
||||
import { getUserInfo, type UserInfo } from '../api/user'
|
||||
import { hydrateActiveRoomFromSelection } from '../store/active-room-store'
|
||||
import type { RoomPlayerState } from '../store/active-room-store'
|
||||
import { setActiveRoom } from '../store'
|
||||
import type { RoomPlayerState } from '../store/state'
|
||||
import type { StoredAuth } from '../types/session'
|
||||
import { clearAuth, readStoredAuth, writeStoredAuth } from '../utils/auth-storage'
|
||||
import { wsClient } from '../ws/client'
|
||||
import { buildWsUrl } from '../ws/url'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
@@ -176,6 +178,14 @@ function currentSession(): AuthSession | null {
|
||||
return toSession(auth.value)
|
||||
}
|
||||
|
||||
function connectGameWs(): void {
|
||||
const token = auth.value?.token
|
||||
if (!token) {
|
||||
return
|
||||
}
|
||||
wsClient.connect(buildWsUrl(token), token)
|
||||
}
|
||||
|
||||
async function refreshRooms(): Promise<void> {
|
||||
const session = currentSession()
|
||||
if (!session) {
|
||||
@@ -236,7 +246,7 @@ async function submitCreateRoom(): Promise<void> {
|
||||
)
|
||||
|
||||
createdRoom.value = room
|
||||
hydrateActiveRoomFromSelection({
|
||||
setActiveRoom({
|
||||
roomId: room.room_id,
|
||||
roomName: room.name,
|
||||
gameType: room.game_type,
|
||||
@@ -282,7 +292,7 @@ async function handleJoinRoom(room?: { roomId?: string; roomName?: string }): Pr
|
||||
roomSubmitting.value = true
|
||||
try {
|
||||
const joinedRoom = await joinRoom(session, { roomId: targetRoomId }, syncAuth)
|
||||
hydrateActiveRoomFromSelection({
|
||||
setActiveRoom({
|
||||
roomId: joinedRoom.room_id,
|
||||
roomName: joinedRoom.name,
|
||||
gameType: joinedRoom.game_type,
|
||||
@@ -297,6 +307,7 @@ async function handleJoinRoom(room?: { roomId?: string; roomName?: string }): Pr
|
||||
quickJoinRoomId.value = joinedRoom.room_id
|
||||
successMessage.value = `已加入房间:${joinedRoom.room_id}`
|
||||
await refreshRooms()
|
||||
connectGameWs()
|
||||
await router.push({
|
||||
path: `/game/chengdu/${joinedRoom.room_id}`,
|
||||
query: joinedRoom.name ? { roomName: joinedRoom.name } : undefined,
|
||||
@@ -335,7 +346,7 @@ async function enterCreatedRoom(): Promise<void> {
|
||||
}
|
||||
|
||||
showCreatedModal.value = false
|
||||
hydrateActiveRoomFromSelection({
|
||||
setActiveRoom({
|
||||
roomId: createdRoom.value.room_id,
|
||||
roomName: createdRoom.value.name,
|
||||
gameType: createdRoom.value.game_type,
|
||||
@@ -347,6 +358,7 @@ async function enterCreatedRoom(): Promise<void> {
|
||||
updatedAt: createdRoom.value.updated_at,
|
||||
players: mapRoomPlayers(createdRoom.value),
|
||||
})
|
||||
connectGameWs()
|
||||
await router.push({
|
||||
path: `/game/chengdu/${createdRoom.value.room_id}`,
|
||||
query: { roomName: createdRoom.value.name },
|
||||
|
||||
@@ -31,6 +31,7 @@ class WsClient {
|
||||
|
||||
private reconnectTimer: number | null = null
|
||||
private reconnectDelay = 2000 // 重连间隔(毫秒)
|
||||
private manualClosing = false
|
||||
|
||||
|
||||
// 构造带 token 的 URL
|
||||
@@ -46,8 +47,8 @@ class WsClient {
|
||||
|
||||
// 建立连接
|
||||
connect(url: string, token?: string) {
|
||||
// 已连接则不重复连接
|
||||
if (this.ws && this.status === 'connected') {
|
||||
// 已连接或连接中则不重复连接
|
||||
if (this.ws && (this.status === 'connected' || this.status === 'connecting')) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -57,6 +58,7 @@ class WsClient {
|
||||
}
|
||||
|
||||
this.setStatus('connecting')
|
||||
this.manualClosing = false
|
||||
|
||||
const finalUrl = this.buildUrl()
|
||||
this.ws = new WebSocket(finalUrl)
|
||||
@@ -85,7 +87,10 @@ class WsClient {
|
||||
// 连接关闭
|
||||
this.ws.onclose = () => {
|
||||
this.setStatus('closed')
|
||||
this.tryReconnect()
|
||||
if (!this.manualClosing) {
|
||||
this.tryReconnect()
|
||||
}
|
||||
this.manualClosing = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +105,7 @@ class WsClient {
|
||||
|
||||
// 手动关闭
|
||||
close() {
|
||||
this.manualClosing = true
|
||||
if (this.ws) {
|
||||
this.ws.close()
|
||||
this.ws = null
|
||||
@@ -107,6 +113,11 @@ class WsClient {
|
||||
this.clearReconnect()
|
||||
}
|
||||
|
||||
reconnect(url: string, token?: string) {
|
||||
this.close()
|
||||
this.connect(url, token)
|
||||
}
|
||||
|
||||
|
||||
// 订阅消息
|
||||
onMessage(handler: MessageHandler) {
|
||||
@@ -159,4 +170,4 @@ class WsClient {
|
||||
}
|
||||
|
||||
// 单例
|
||||
export const wsClient = new WsClient()
|
||||
export const wsClient = new WsClient()
|
||||
|
||||
Reference in New Issue
Block a user