Appearance
最流畅的更新
本节目标:理解“最流畅的更新”的核心思路,并能把它用于实际开发或面试表达。
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
import WebSocketService from '@/services/websocketService'
import { getToken } from '@/utils/auth'
export interface SymbolSvg {
svg: string
svgb: string
}
export interface ForexSymbol {
id: number
symbol: string
contact_size: string
digits: string
description: string
type: string
add: number
symbol_svg: SymbolSvg
is_favorite: boolean
sort: string
}
export interface TradeForm {
number: string //数量
type: 'lots' | 'shares' //交易类型,假设可能的值为 'lots' 或
'shares'
pending: boolean //是否挂单
pending_number: string //挂单数量
take: boolean //是否设置止盈
take_number: string //止盈数量
take_type: 'price' | 'percent' //止盈类型,假设可能的值为 'price' 或
'percent'
stop: boolean //是否设置止损
stop_number: string //止损数量
stop_type: 'price' | 'percent' //止损类型,假设可能的值为 'price' 或
'percent'
ask: number //买价格
bid: number //卖价
status: 'buy' | 'sell' //交易状态
}
export interface PricePayload {
ask: number
bid: number
ts: number
dayOpen: number
dayHigh: number
dayLow: number
preClose: number
}
const useTradeViewStore = defineStore(
'tradeView',
() => {
const tabs = ref<ForexSymbol[]>([])
const currentTab = ref<ForexSymbol | null>(null)
const wsSubList = ref<string[]>([])
const cData = ref<unknown>(null)
const tradeForm = ref<TradeForm>({
number: '',
type: 'lots',
pending: false,
pending_number: '',
take: false, //止盈
take_number: '',
take_type: 'price',
stop: false, //止损
stop_number: '',
stop_type: 'price',
ask: 0, //买价格
bid: 0, //卖价
status: 'buy', // buy / sell
})
const rowMap = computed(() => {
const _map = new Map<string, ForexSymbol>()
for (const item of fullDataList.value) {
_map.set(item.symbol, item)
}
return _map
})
const fullDataList = ref<ForexSymbol[]>([])
const priceQueue = new Map<string, PricePayload[]>()
let rafId: number | null = null
function scheduleFlush() {
if (rafId === null) rafId = requestAnimationFrame(flushQueue)
}
function flushQueue() {
priceQueue.forEach((queue, symbol) => {
const last = queue[queue.length - 1] //只取最新一条
queue.length = 0 //清空队列
const row = rowMap.value.get(symbol)
if (!row) return
//对比旧值 ── 只改动真的发生变化的字段
if (last.bid !== row.bid) {
row.bidTrend = last.bid > row.bid ? 'up' : 'down'
row.bid = last.bid
clearTrendLater(row, 'bidTrend')
}
if (last.ask !== row.ask) {
row.askTrend = last.ask > row.ask ? 'up' : 'down'
row.ask = last.ask
clearTrendLater(row, 'askTrend')
}
/* ===如果还需写入 dayOpen/dayHigh 等,可同理赋值
=== */
})
rafId = null
}
/* ======== 500 ms后自动移除闪烁类
======== */
function clearTrendLater(row: TableRow, key: 'bidTrend' | 'askTrend') {
setTimeout(() => {
row[key] = null
}, 500)
}
async function initNavTabs(list: ForexSymbol[], favoritesTop4: ForexSymbol[]) {
if (!tabs.value.length) {
if (favoritesTop4.length) {
tabs.value = favoritesTop4
} else {
tabs.value = list.slice(0, 4)
}
currentTab.value =
tabs.value.find((t) => t.symbol === currentTab.value?.symbol) ?? tabs.value[0] ?? null
}
}
function initWebSocket(host: string, srv: string, loginid: string) {
const token = getToken()
// const url = `${host}?auth=${token}&servername=${srv}&loginid=${loginid}`
const url = `ws://122.51.27.47:8099/ws?auth=${token}&servername=${srv}&loginid=${loginid}`
const svc = WebSocketService.getInstance()
svc.connect(url)
svc.addEventListener('message', handleIncomingData)
}
function handleIncomingData(raw: unknown) {
let txt = ''
if (typeof raw === 'string') txt = raw.trim()
else if (raw instanceof ArrayBuffer) txt = new TextDecoder().decode(raw)
else return //非文本忽略
const parts = txt.split(/\s+/)
if (parts.length !== 8) return //非行情消息忽略
const [symbol, askStr, bidStr, tsStr, open, high, low, pre] = parts
const payload: PricePayload = {
ask: +askStr,
bid: +bidStr,
ts: +tsStr,
dayOpen: +open,
dayHigh: +high,
dayLow: +low,
preClose: +pre,
}
if (!priceQueue.has(symbol)) priceQueue.set(symbol, [])
priceQueue.get(symbol)!.push(payload)
scheduleFlush()
}
//订阅或更新订阅:mode='append' 追加,'update' 覆盖
function subscribe(symbols: string[], mode: 'append' | 'update' = 'update') {
const svc = WebSocketService.getInstance()
const currentSubscribedSymbols = wsSubList.value //当前已订阅的列表
const currentSubscribedSet = new Set(currentSubscribedSymbols) //当前已订阅的 Set 形式,用于高效查找
const effectiveTargetSymbolsSet = new Set(symbols)
const currentActiveSymbol = currentTab.value?.symbol
if (currentActiveSymbol) {
effectiveTargetSymbolsSet.add(currentActiveSymbol)
}
let symbolsToActuallySubscribe: string[] = []
let symbolsToActuallyUnsubscribe: string[] = []
if (mode === 'append') {
symbolsToActuallySubscribe = Array.from(effectiveTargetSymbolsSet).filter(
(s) => !currentSubscribedSet.has(s),
)
if (symbolsToActuallySubscribe.length > 0) {
wsSubList.value.push(...symbolsToActuallySubscribe)
}
} else {
symbolsToActuallyUnsubscribe = currentSubscribedSymbols.filter(
(s) => !effectiveTargetSymbolsSet.has(s),
)
symbolsToActuallySubscribe = Array.from(effectiveTargetSymbolsSet).filter(
(s) => !currentSubscribedSet.has(s),
)
wsSubList.value = Array.from(effectiveTargetSymbolsSet)
}
if (symbolsToActuallyUnsubscribe.length > 0) {
svc.send(`unsub:${symbolsToActuallyUnsubscribe.join(';')};`)
}
if (symbolsToActuallySubscribe.length > 0) {
svc.send(`sub:${symbolsToActuallySubscribe.join(';')};`)
}
if (symbolsToActuallyUnsubscribe.length === 0 && symbolsToActuallySubscribe.length === 0) {
}
}
//取消所有订阅
function unsubscribeAll() {
WebSocketService.getInstance().send('unsuball')
wsSubList.value = []
}
//更新当前激活的标签
function updateCurrentTab(tab: ForexSymbol) {
currentTab.value = tab
}
function delTab(tab: ForexSymbol) {
tabs.value = tabs.value.filter((t) => t.symbol !== tab.symbol)
}
//更新标签数组并持久化
function updateTradingTabs(tabsOrTab: ForexSymbol | ForexSymbol[]) {
const arr = Array.isArray(tabsOrTab) ? tabsOrTab : [tabsOrTab]
tabs.value = arr
}
return {
tabs,
currentTab,
wsSubList,
cData,
tradeForm,
fullDataList,
initNavTabs,
initWebSocket,
handleIncomingData,
subscribe,
unsubscribeAll,
updateCurrentTab,
updateTradingTabs,
delTab,
}
},
{
persist: {
key: 'trade-view', // localStorage键
pick: ['tabs', 'currentTab', 'tradeForm'], //只持久化这两个字段
debug: import.meta.env.DEV, //调试环境下输出错误
},
},
)
export default useTradeViewStore