重构 WebSocket 推送服务:统一使用 /ws 路径和 channel 订阅模式
- 后端重构: - 统一 WebSocket 路径为 /ws,通过 channel 区分不同推送服务 - 实现 UnifiedWebSocketHandler 统一处理所有推送频道 - 实现 WebSocketSubscriptionService 管理订阅和推送 - 消息类型改为 int 类型(1:SUB, 2:UNSUB, 3:DATA, 4:SUB_ACK, 5:PING, 6:PONG) - status 字段改为 int 类型(0: success, 非0: error) - 移除旧的 /ws/positions 路由和 PositionWebSocketHandler - 修复首推数据:订阅 position 频道后立即发送全量数据 - 前端重构: - 实现全局 WebSocket 管理器(单例模式) - 应用启动时立即建立全局 WebSocket 连接 - 实现 useWebSocketSubscription hook 用于订阅频道 - PositionList 完全依赖 WebSocket 推送,移除 HTTP 轮询 - 添加连接状态显示和自动重连机制 - 实现心跳保活机制(PING/PONG) - 配置更新: - 添加 WebSocket 相关配置项 - 更新 vite.config.ts 添加 /ws 代理配置
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { useEffect } from 'react'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import { ConfigProvider } from 'antd'
|
||||
import zhCN from 'antd/locale/zh_CN'
|
||||
@@ -11,8 +12,20 @@ import LeaderAdd from './pages/LeaderAdd'
|
||||
import ConfigPage from './pages/ConfigPage'
|
||||
import PositionList from './pages/PositionList'
|
||||
import Statistics from './pages/Statistics'
|
||||
import { wsManager } from './services/websocket'
|
||||
|
||||
function App() {
|
||||
// 应用启动时立即建立全局 WebSocket 连接
|
||||
useEffect(() => {
|
||||
// 立即建立连接(如果还未连接)
|
||||
if (!wsManager.isConnected()) {
|
||||
wsManager.connect()
|
||||
}
|
||||
|
||||
// 注意:应用不会卸载,所以不需要在 cleanup 中断开连接
|
||||
// WebSocket 连接会在整个应用生命周期中保持,并自动重连
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<ConfigProvider locale={zhCN}>
|
||||
<BrowserRouter>
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import { useEffect, useState, useRef } from 'react'
|
||||
import { wsManager, SubscriptionCallback } from '../services/websocket'
|
||||
|
||||
/**
|
||||
* 使用 WebSocket 订阅
|
||||
*/
|
||||
export function useWebSocketSubscription<T = any>(
|
||||
channel: string,
|
||||
callback: SubscriptionCallback,
|
||||
payload?: any
|
||||
): { connected: boolean } {
|
||||
const [connected, setConnected] = useState(wsManager.isConnected())
|
||||
const callbackRef = useRef(callback)
|
||||
|
||||
// 更新回调引用
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback
|
||||
}, [callback])
|
||||
|
||||
useEffect(() => {
|
||||
// 订阅频道(连接已在 App.tsx 中全局建立,这里只需要订阅)
|
||||
const unsubscribe = wsManager.subscribe(channel, (data) => {
|
||||
callbackRef.current(data)
|
||||
}, payload)
|
||||
|
||||
// 监听连接状态(连接在 App.tsx 中全局管理,这里只监听状态变化)
|
||||
const removeConnectionListener = wsManager.onConnectionChange(setConnected)
|
||||
|
||||
// 初始化连接状态
|
||||
setConnected(wsManager.isConnected())
|
||||
|
||||
return () => {
|
||||
unsubscribe()
|
||||
removeConnectionListener()
|
||||
}
|
||||
}, [channel, payload])
|
||||
|
||||
return { connected }
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { useEffect, useState, useMemo } from 'react'
|
||||
import { Card, Table, Tag, message, Space, Input, Radio, Select, Button, Row, Col, Empty } from 'antd'
|
||||
import { SearchOutlined, AppstoreOutlined, UnorderedListOutlined, UpOutlined, DownOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
import type { AccountPosition, Account } from '../types'
|
||||
import type { AccountPosition, Account, PositionPushMessage } from '../types'
|
||||
import { getPositionKey } from '../types'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
import { useWebSocketSubscription } from '../hooks/useWebSocket'
|
||||
import { wsManager } from '../services/websocket'
|
||||
|
||||
type PositionFilter = 'current' | 'historical'
|
||||
type ViewMode = 'card' | 'list'
|
||||
@@ -20,12 +23,118 @@ const PositionList: React.FC = () => {
|
||||
const [selectedAccountId, setSelectedAccountId] = useState<number | undefined>(undefined)
|
||||
const [viewMode, setViewMode] = useState<ViewMode>(isMobile ? 'card' : 'list')
|
||||
const [expandedCards, setExpandedCards] = useState<Set<string>>(new Set())
|
||||
const [wsConnected, setWsConnected] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchAccounts()
|
||||
fetchPositions()
|
||||
// 完全依赖 WebSocket 推送,不主动请求接口
|
||||
// 连接建立后会立即收到全量数据推送
|
||||
setLoading(true) // 显示加载状态,等待 WebSocket 全量推送
|
||||
|
||||
// 监听连接状态(WebSocket 连接在 App.tsx 中全局初始化,全局共享)
|
||||
const removeListener = wsManager.onConnectionChange((connected) => {
|
||||
setWsConnected(connected)
|
||||
})
|
||||
|
||||
// 获取当前连接状态
|
||||
setWsConnected(wsManager.isConnected())
|
||||
|
||||
return () => {
|
||||
removeListener()
|
||||
}
|
||||
}, [])
|
||||
|
||||
// 订阅仓位推送
|
||||
const { connected: positionConnected } = useWebSocketSubscription<PositionPushMessage>(
|
||||
'position',
|
||||
(message) => {
|
||||
handlePositionPushMessage(message)
|
||||
}
|
||||
)
|
||||
|
||||
// 更新连接状态(使用订阅的连接状态)
|
||||
useEffect(() => {
|
||||
setWsConnected(positionConnected)
|
||||
}, [positionConnected])
|
||||
|
||||
/**
|
||||
* 处理仓位推送消息
|
||||
*/
|
||||
const handlePositionPushMessage = (message: PositionPushMessage) => {
|
||||
if (message.type === 'FULL') {
|
||||
// 全量推送:直接替换(这是首次连接时的数据,完全以推送数据为准)
|
||||
setCurrentPositions(message.currentPositions || [])
|
||||
setHistoryPositions(message.historyPositions || [])
|
||||
setLoading(false)
|
||||
console.log('收到仓位全量推送:', {
|
||||
current: message.currentPositions?.length || 0,
|
||||
history: message.historyPositions?.length || 0
|
||||
})
|
||||
} else if (message.type === 'INCREMENTAL') {
|
||||
// 增量推送:合并数据(始终以推送数据为准)
|
||||
setCurrentPositions(prev => mergePositions(prev, message.currentPositions || [], message.removedPositionKeys || []))
|
||||
setHistoryPositions(prev => mergePositions(prev, message.historyPositions || [], message.removedPositionKeys || []))
|
||||
console.log('收到仓位增量推送:', {
|
||||
current: message.currentPositions?.length || 0,
|
||||
history: message.historyPositions?.length || 0,
|
||||
removed: message.removedPositionKeys?.length || 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并仓位数据
|
||||
* 新增的仓位插入到列表顶部,更新的仓位更新现有数据并保持位置,删除的仓位从列表中移除
|
||||
*/
|
||||
const mergePositions = (
|
||||
prev: AccountPosition[],
|
||||
updates: AccountPosition[],
|
||||
removedKeys: string[]
|
||||
): AccountPosition[] => {
|
||||
// 创建现有仓位的键集合,用于快速判断是新增还是更新
|
||||
const existingKeys = new Set(prev.map(pos => getPositionKey(pos)))
|
||||
|
||||
// 区分新增和更新的仓位
|
||||
const newPositions: AccountPosition[] = []
|
||||
const updateMap = new Map<string, AccountPosition>()
|
||||
|
||||
updates.forEach(update => {
|
||||
const key = getPositionKey(update)
|
||||
if (existingKeys.has(key)) {
|
||||
// 已存在的仓位,记录更新
|
||||
updateMap.set(key, update)
|
||||
} else {
|
||||
// 新增的仓位,插入到顶部
|
||||
newPositions.push(update)
|
||||
}
|
||||
})
|
||||
|
||||
// 构建结果数组
|
||||
const result: AccountPosition[] = []
|
||||
|
||||
// 1. 先添加新增的仓位(在顶部)
|
||||
result.push(...newPositions)
|
||||
|
||||
// 2. 遍历原有仓位,应用更新或保持不变
|
||||
prev.forEach(pos => {
|
||||
const key = getPositionKey(pos)
|
||||
|
||||
// 如果被删除,跳过
|
||||
if (removedKeys.includes(key)) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果有更新,使用新数据;否则保持原数据
|
||||
if (updateMap.has(key)) {
|
||||
result.push(updateMap.get(key)!)
|
||||
} else {
|
||||
result.push(pos)
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
const fetchAccounts = async () => {
|
||||
setAccountsLoading(true)
|
||||
try {
|
||||
@@ -42,22 +151,7 @@ const PositionList: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const fetchPositions = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const response = await apiService.accounts.positionsList()
|
||||
if (response.data.code === 0 && response.data.data) {
|
||||
setCurrentPositions(response.data.data.currentPositions || [])
|
||||
setHistoryPositions(response.data.data.historyPositions || [])
|
||||
} else {
|
||||
message.error(response.data.msg || '获取仓位列表失败')
|
||||
}
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取仓位列表失败')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
// 已移除 fetchPositions 函数,完全依赖 WebSocket 推送更新数据
|
||||
|
||||
// 根据筛选器选择对应的仓位列表
|
||||
const basePositions = useMemo(() => {
|
||||
@@ -657,7 +751,25 @@ const PositionList: React.FC = () => {
|
||||
<div>
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', flexWrap: 'wrap', gap: '12px', marginBottom: '12px' }}>
|
||||
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<h2 style={{ margin: 0 }}>仓位管理</h2>
|
||||
{/* WebSocket 连接状态指示器 */}
|
||||
<Tag
|
||||
color={wsConnected ? 'green' : 'orange'}
|
||||
style={{ margin: 0 }}
|
||||
>
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: wsConnected ? '#52c41a' : '#fa8c16',
|
||||
marginRight: '6px',
|
||||
animation: wsConnected ? 'pulse 2s infinite' : 'pulse 1s infinite'
|
||||
}}></span>
|
||||
{wsConnected ? '实时更新' : '连接中...'}
|
||||
</Tag>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px', flex: isMobile ? '1 1 100%' : '0 0 auto', flexWrap: 'wrap' }}>
|
||||
<Input
|
||||
placeholder="搜索账户、市场、方向..."
|
||||
|
||||
@@ -0,0 +1,356 @@
|
||||
/**
|
||||
* WebSocket 消息类型(int 值)
|
||||
*/
|
||||
export enum WebSocketMessageType {
|
||||
SUB = 1, // 订阅
|
||||
UNSUB = 2, // 取消订阅
|
||||
DATA = 3, // 数据推送
|
||||
SUB_ACK = 4, // 订阅确认
|
||||
PING = 5, // 心跳
|
||||
PONG = 6 // 心跳响应
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 消息
|
||||
*/
|
||||
export interface WebSocketMessage {
|
||||
type: number // WebSocketMessageType 的 int 值(1:SUB, 2:UNSUB, 3:DATA, 4:SUB_ACK, 5:PING, 6:PONG)
|
||||
channel?: string
|
||||
payload?: any
|
||||
timestamp?: number
|
||||
status?: number // 0: success, 非0: error
|
||||
message?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅回调函数
|
||||
*/
|
||||
export type SubscriptionCallback = (data: any) => void
|
||||
|
||||
/**
|
||||
* 全局 WebSocket 管理器
|
||||
*/
|
||||
class WebSocketManager {
|
||||
private ws: WebSocket | null = null
|
||||
private reconnectTimer: NodeJS.Timeout | null = null
|
||||
private pingInterval: NodeJS.Timeout | null = null
|
||||
private isConnecting = false
|
||||
private isUnmounting = false
|
||||
|
||||
// 订阅管理:channel -> Set<callback>
|
||||
private subscriptions = new Map<string, Set<SubscriptionCallback>>()
|
||||
|
||||
// 订阅状态:channel -> boolean(是否已向后端订阅)
|
||||
private subscribedChannels = new Set<string>()
|
||||
|
||||
// 连接状态回调
|
||||
private connectionCallbacks: Set<(connected: boolean) => void> = new Set()
|
||||
|
||||
private reconnectDelay = 3000
|
||||
private pingIntervalTime = 30000
|
||||
|
||||
/**
|
||||
* 连接 WebSocket(全局共享连接)
|
||||
*/
|
||||
connect(): void {
|
||||
// 如果已经连接或正在连接,直接返回
|
||||
if (this.ws?.readyState === WebSocket.OPEN || this.isConnecting) {
|
||||
return
|
||||
}
|
||||
|
||||
// 如果正在卸载,不允许连接
|
||||
if (this.isUnmounting) {
|
||||
return
|
||||
}
|
||||
|
||||
this.isConnecting = true
|
||||
const wsUrl = this.getWebSocketUrl()
|
||||
console.log('[WebSocket] 正在连接:', wsUrl)
|
||||
|
||||
try {
|
||||
// 如果已经有连接(但状态不是 OPEN),先关闭
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close()
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
|
||||
const ws = new WebSocket(wsUrl)
|
||||
this.ws = ws
|
||||
|
||||
ws.onopen = () => {
|
||||
console.log('[WebSocket] 连接成功')
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(true)
|
||||
this.startPing()
|
||||
this.resubscribeAll() // 重新订阅所有频道
|
||||
}
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
this.handleMessage(event.data)
|
||||
}
|
||||
|
||||
ws.onerror = (error) => {
|
||||
console.error('[WebSocket] 连接错误:', error)
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
}
|
||||
|
||||
ws.onclose = () => {
|
||||
console.log('[WebSocket] 连接关闭')
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
this.stopPing()
|
||||
// 自动重连(除非正在卸载)
|
||||
if (!this.isUnmounting) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[WebSocket] 创建连接失败:', error)
|
||||
this.isConnecting = false
|
||||
this.notifyConnectionStatus(false)
|
||||
// 自动重连(除非正在卸载)
|
||||
if (!this.isUnmounting) {
|
||||
this.scheduleReconnect()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 断开连接(仅在应用完全卸载时调用)
|
||||
*/
|
||||
disconnect(): void {
|
||||
console.log('[WebSocket] 断开连接')
|
||||
this.isUnmounting = true
|
||||
this.stopPing()
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
this.reconnectTimer = null
|
||||
}
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close()
|
||||
} catch (e) {
|
||||
// 忽略关闭错误
|
||||
}
|
||||
this.ws = null
|
||||
}
|
||||
this.notifyConnectionStatus(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* 订阅频道
|
||||
*/
|
||||
subscribe(channel: string, callback: SubscriptionCallback, payload?: any): () => void {
|
||||
// 添加订阅者
|
||||
if (!this.subscriptions.has(channel)) {
|
||||
this.subscriptions.set(channel, new Set())
|
||||
}
|
||||
this.subscriptions.get(channel)!.add(callback)
|
||||
|
||||
// 如果还未向后端订阅,发送订阅消息
|
||||
if (!this.subscribedChannels.has(channel)) {
|
||||
this.sendSubscribe(channel, payload)
|
||||
}
|
||||
|
||||
// 返回取消订阅函数
|
||||
return () => {
|
||||
this.unsubscribe(channel, callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消订阅
|
||||
*/
|
||||
unsubscribe(channel: string, callback: SubscriptionCallback): void {
|
||||
const callbacks = this.subscriptions.get(channel)
|
||||
if (callbacks) {
|
||||
callbacks.delete(callback)
|
||||
|
||||
// 如果没有订阅者了,向后端取消订阅
|
||||
if (callbacks.size === 0) {
|
||||
this.subscriptions.delete(channel)
|
||||
this.sendUnsubscribe(channel)
|
||||
this.subscribedChannels.delete(channel)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送订阅消息
|
||||
*/
|
||||
private sendSubscribe(channel: string, payload?: any): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
const message: WebSocketMessage = {
|
||||
type: WebSocketMessageType.SUB,
|
||||
channel,
|
||||
payload
|
||||
}
|
||||
this.ws.send(JSON.stringify(message))
|
||||
this.subscribedChannels.add(channel)
|
||||
console.log('已订阅频道:', channel)
|
||||
} else {
|
||||
// 如果连接未建立,先连接
|
||||
this.connect()
|
||||
// 连接建立后会通过 resubscribeAll 自动订阅
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送取消订阅消息
|
||||
*/
|
||||
private sendUnsubscribe(channel: string): void {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
const message: WebSocketMessage = {
|
||||
type: WebSocketMessageType.UNSUB,
|
||||
channel
|
||||
}
|
||||
this.ws.send(JSON.stringify(message))
|
||||
console.log('已取消订阅频道:', channel)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理收到的消息
|
||||
*/
|
||||
private handleMessage(data: string): void {
|
||||
// 处理心跳
|
||||
if (data === 'PONG') {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const message: WebSocketMessage = JSON.parse(data)
|
||||
|
||||
if (message.type === WebSocketMessageType.DATA && message.channel) {
|
||||
// 数据推送:分发到订阅者
|
||||
const callbacks = this.subscriptions.get(message.channel)
|
||||
if (callbacks) {
|
||||
callbacks.forEach(callback => {
|
||||
try {
|
||||
callback(message.payload)
|
||||
} catch (error) {
|
||||
console.error(`频道 ${message.channel} 回调执行失败:`, error)
|
||||
}
|
||||
})
|
||||
}
|
||||
} else if (message.type === WebSocketMessageType.SUB_ACK) {
|
||||
// 订阅确认
|
||||
if (message.status !== undefined && message.status !== 0) {
|
||||
console.error(`订阅频道 ${message.channel} 失败:`, message.message)
|
||||
this.subscribedChannels.delete(message.channel || '')
|
||||
} else {
|
||||
console.log(`订阅频道 ${message.channel} 成功`)
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('解析 WebSocket 消息失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新订阅所有频道
|
||||
*/
|
||||
private resubscribeAll(): void {
|
||||
this.subscribedChannels.clear()
|
||||
this.subscriptions.forEach((callbacks, channel) => {
|
||||
if (callbacks.size > 0) {
|
||||
this.sendSubscribe(channel)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 安排重连
|
||||
*/
|
||||
private scheduleReconnect(): void {
|
||||
if (this.isUnmounting) {
|
||||
return
|
||||
}
|
||||
|
||||
if (this.reconnectTimer) {
|
||||
clearTimeout(this.reconnectTimer)
|
||||
}
|
||||
|
||||
this.reconnectTimer = setTimeout(() => {
|
||||
this.connect()
|
||||
}, this.reconnectDelay)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始心跳
|
||||
*/
|
||||
private startPing(): void {
|
||||
this.stopPing()
|
||||
|
||||
// 立即发送一次心跳
|
||||
const sendPing = () => {
|
||||
if (this.ws?.readyState === WebSocket.OPEN) {
|
||||
this.ws.send('PING')
|
||||
console.log('发送心跳: PING')
|
||||
}
|
||||
}
|
||||
|
||||
sendPing()
|
||||
|
||||
// 每30秒发送一次心跳
|
||||
this.pingInterval = setInterval(sendPing, this.pingIntervalTime)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止心跳
|
||||
*/
|
||||
private stopPing(): void {
|
||||
if (this.pingInterval) {
|
||||
clearInterval(this.pingInterval)
|
||||
this.pingInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 WebSocket URL
|
||||
*/
|
||||
private getWebSocketUrl(): string {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
const host = window.location.host
|
||||
return `${protocol}//${host}/ws`
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册连接状态回调
|
||||
*/
|
||||
onConnectionChange(callback: (connected: boolean) => void): () => void {
|
||||
this.connectionCallbacks.add(callback)
|
||||
return () => {
|
||||
this.connectionCallbacks.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 通知连接状态变化
|
||||
*/
|
||||
private notifyConnectionStatus(connected: boolean): void {
|
||||
this.connectionCallbacks.forEach(callback => {
|
||||
try {
|
||||
callback(connected)
|
||||
} catch (error) {
|
||||
console.error('连接状态回调执行失败:', error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取连接状态
|
||||
*/
|
||||
isConnected(): boolean {
|
||||
return this.ws?.readyState === WebSocket.OPEN
|
||||
}
|
||||
}
|
||||
|
||||
// 导出单例
|
||||
export const wsManager = new WebSocketManager()
|
||||
|
||||
@@ -30,3 +30,13 @@ body {
|
||||
}
|
||||
}
|
||||
|
||||
/* WebSocket 连接状态动画 */
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -187,3 +187,26 @@ export interface PositionListResponse {
|
||||
historyPositions: AccountPosition[]
|
||||
}
|
||||
|
||||
/**
|
||||
* 仓位推送消息类型
|
||||
*/
|
||||
export type PositionPushMessageType = 'FULL' | 'INCREMENTAL'
|
||||
|
||||
/**
|
||||
* 仓位推送消息
|
||||
*/
|
||||
export interface PositionPushMessage {
|
||||
type: PositionPushMessageType // 消息类型:FULL(全量)或 INCREMENTAL(增量)
|
||||
timestamp: number // 消息时间戳
|
||||
currentPositions?: AccountPosition[] // 当前仓位列表(全量或增量)
|
||||
historyPositions?: AccountPosition[] // 历史仓位列表(全量或增量)
|
||||
removedPositionKeys?: string[] // 已删除的仓位键(仅增量推送时使用)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仓位唯一键
|
||||
*/
|
||||
export function getPositionKey(position: AccountPosition): string {
|
||||
return `${position.accountId}-${position.marketId}-${position.side}`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user