解决内存泄露问题
This commit is contained in:
parent
3f68c81bf1
commit
4dae5e5d57
@ -58,7 +58,7 @@ function upsertIntraday(
|
||||
const price = toNum(priceRaw)
|
||||
const avg = toNum(avgRaw)
|
||||
const totalVolume = toNum(totalVolumeRaw)
|
||||
const list = [...points]
|
||||
const list = points.slice()
|
||||
const idx = list.findIndex((p) => p.time === time)
|
||||
|
||||
const othersSum = list.reduce((s, p, i) => (i === idx ? s : s + p.volume), 0)
|
||||
@ -73,8 +73,11 @@ function upsertIntraday(
|
||||
return list
|
||||
}
|
||||
|
||||
/** Merge tick detailinfos into quote.trades (newest first, dedupe, cap 200). */
|
||||
export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): QuoteData {
|
||||
/**
|
||||
* Merge tick detailinfos into quote.trades (newest first, dedupe, cap 200).
|
||||
* Mutates quote in place so Vue only invalidates trade-dependent effects.
|
||||
*/
|
||||
export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): void {
|
||||
const incoming: TradeTick[] = [...(data.detailinfos ?? [])]
|
||||
.reverse()
|
||||
.map((t) => ({
|
||||
@ -93,26 +96,27 @@ export function applyWsTick(quote: QuoteData, data: BaiduWsTickData): QuoteData
|
||||
merged.push(t)
|
||||
if (merged.length >= MAX_TRADES) break
|
||||
}
|
||||
return { ...quote, trades: merged }
|
||||
quote.trades = merged
|
||||
}
|
||||
|
||||
/** Apply snapshot fields onto quote (price, book, pankou, intraday point). */
|
||||
export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): QuoteData {
|
||||
const next: QuoteData = { ...quote }
|
||||
|
||||
/**
|
||||
* Apply snapshot fields onto quote (price, book, pankou, intraday point).
|
||||
* Mutates quote in place — do not replace quote.value, or every tick rebuilds charts.
|
||||
*/
|
||||
export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): void {
|
||||
if (data.cur) {
|
||||
const c = data.cur
|
||||
if (c.price != null) next.last = toNum(c.price, next.last)
|
||||
if (c.increase != null) next.change = toNum(c.increase, next.change)
|
||||
if (c.ratio != null) next.changePercent = toNum(c.ratio, next.changePercent)
|
||||
if (c.avgPrice != null) next.avg = toNum(c.avgPrice, next.avg)
|
||||
if (c.status) next.status = c.status
|
||||
if (c.price != null) quote.last = toNum(c.price, quote.last)
|
||||
if (c.increase != null) quote.change = toNum(c.increase, quote.change)
|
||||
if (c.ratio != null) quote.changePercent = toNum(c.ratio, quote.changePercent)
|
||||
if (c.avgPrice != null) quote.avg = toNum(c.avgPrice, quote.avg)
|
||||
if (c.status) quote.status = c.status
|
||||
}
|
||||
|
||||
if (data.update) {
|
||||
if (data.update.text) next.updatedAt = data.update.text
|
||||
if (data.update.stockStatus) next.status = data.update.stockStatus
|
||||
else if (data.update.tradeStatusCN) next.status = data.update.tradeStatusCN
|
||||
if (data.update.text) quote.updatedAt = data.update.text
|
||||
if (data.update.stockStatus) quote.status = data.update.stockStatus
|
||||
else if (data.update.tradeStatusCN) quote.status = data.update.tradeStatusCN
|
||||
}
|
||||
|
||||
if (data.pankouinfos?.length) {
|
||||
@ -120,20 +124,20 @@ export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): Qu
|
||||
const num = (ename: string, fallback: number) =>
|
||||
by[ename] ? toNum(by[ename]!.originValue ?? by[ename]!.value, fallback) : fallback
|
||||
|
||||
next.open = num('open', next.open)
|
||||
next.high = num('high', next.high)
|
||||
next.low = num('low', next.low)
|
||||
next.prevClose = num('preClose', next.prevClose)
|
||||
next.volume = num('volume', next.volume)
|
||||
next.openInterest = num('holdingAmount', next.openInterest)
|
||||
next.amplitude = num('amplitudeRatio', next.amplitude)
|
||||
next.settlement = num('settlement', next.settlement)
|
||||
next.prevSettlement = num('prevSettlement', next.prevSettlement)
|
||||
next.outerVol = num('outside', next.outerVol)
|
||||
next.innerVol = num('inside', next.innerVol)
|
||||
if (by.avgPrice) next.avg = num('avgPrice', next.avg)
|
||||
quote.open = num('open', quote.open)
|
||||
quote.high = num('high', quote.high)
|
||||
quote.low = num('low', quote.low)
|
||||
quote.prevClose = num('preClose', quote.prevClose)
|
||||
quote.volume = num('volume', quote.volume)
|
||||
quote.openInterest = num('holdingAmount', quote.openInterest)
|
||||
quote.amplitude = num('amplitudeRatio', quote.amplitude)
|
||||
quote.settlement = num('settlement', quote.settlement)
|
||||
quote.prevSettlement = num('prevSettlement', quote.prevSettlement)
|
||||
quote.outerVol = num('outside', quote.outerVol)
|
||||
quote.innerVol = num('inside', quote.innerVol)
|
||||
if (by.avgPrice) quote.avg = num('avgPrice', quote.avg)
|
||||
if (by.amount) {
|
||||
next.amount = parseAmountYi(by.amount.value, by.amount.originValue)
|
||||
quote.amount = parseAmountYi(by.amount.value, by.amount.originValue)
|
||||
}
|
||||
if (by.amountDelta) {
|
||||
const raw = by.amountDelta.originValue
|
||||
@ -141,7 +145,7 @@ export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): Qu
|
||||
const unavailable =
|
||||
(raw == null || !Number.isFinite(raw)) && (!display || display === '--')
|
||||
if (!unavailable) {
|
||||
next.amountDelta = parseAmountDelta(display, raw)
|
||||
quote.amountDelta = parseAmountDelta(display, raw)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -166,20 +170,18 @@ export function applyWsSnapshot(quote: QuoteData, data: BaiduWsSnapshotData): Qu
|
||||
const bidVol = bids.reduce((s, b) => s + b.volume, 0)
|
||||
const askVol = asks.reduce((s, a) => s + a.volume, 0)
|
||||
const total = bidVol + askVol
|
||||
next.orderBook = { asks, bids }
|
||||
next.buyRatio = total > 0 ? Math.round((bidVol / total) * 100) : 50
|
||||
next.sellRatio = 100 - next.buyRatio
|
||||
quote.orderBook = { asks, bids }
|
||||
quote.buyRatio = total > 0 ? Math.round((bidVol / total) * 100) : 50
|
||||
quote.sellRatio = 100 - quote.buyRatio
|
||||
}
|
||||
|
||||
if (data.point) {
|
||||
next.intraday = upsertIntraday(
|
||||
next.intraday,
|
||||
quote.intraday = upsertIntraday(
|
||||
quote.intraday,
|
||||
data.point.price,
|
||||
data.point.avgPrice,
|
||||
data.point.time,
|
||||
data.point.totalVolume,
|
||||
)
|
||||
}
|
||||
|
||||
return next
|
||||
}
|
||||
|
||||
@ -6,12 +6,12 @@
|
||||
<el-tab-pane :label="TAB.week" name="week" />
|
||||
<el-tab-pane :label="TAB.month" name="month" />
|
||||
</el-tabs>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
<v-chart class="chart" :option="option" :update-options="updateOpts" autoresize />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref } from 'vue'
|
||||
import { onUnmounted, ref, watch } from 'vue'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
import { LineChart, BarChart, CandlestickChart } from 'echarts/charts'
|
||||
@ -23,7 +23,7 @@ import {
|
||||
} from 'echarts/components'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import VChart from 'vue-echarts'
|
||||
import type { Candle, QuoteData } from '../../types'
|
||||
import type { Candle, IntradayPoint, QuoteData } from '../../types'
|
||||
import type { KlinePeriod } from '../../stores/quota'
|
||||
|
||||
use([
|
||||
@ -37,7 +37,7 @@ use([
|
||||
LegendComponent,
|
||||
])
|
||||
|
||||
/** ASCII-safe unicode escapes ? avoids Windows encoding corruption */
|
||||
/** ASCII-safe unicode escapes — avoids Windows encoding corruption */
|
||||
const TAB = {
|
||||
intraday: '\u5206\u65f6',
|
||||
day: '\u65e5K',
|
||||
@ -53,6 +53,9 @@ interface ColorParam {
|
||||
dataIndex?: number
|
||||
}
|
||||
|
||||
/** Throttle chart option rebuilds under high-frequency WS snapshots. */
|
||||
const CHART_THROTTLE_MS = 400
|
||||
|
||||
const props = defineProps<{
|
||||
quote: QuoteData
|
||||
klineLoading?: boolean
|
||||
@ -60,27 +63,71 @@ const props = defineProps<{
|
||||
}>()
|
||||
|
||||
const period = ref<ChartPeriod>('intraday')
|
||||
const option = ref<EChartsOption>({})
|
||||
const updateOpts = { notMerge: true, lazyUpdate: true }
|
||||
|
||||
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pendingBuild = false
|
||||
|
||||
async function onTabChange(name: string | number) {
|
||||
if (name === 'day' || name === 'week' || name === 'month') {
|
||||
await props.loadKline(name)
|
||||
}
|
||||
flushOption(true)
|
||||
}
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
function buildOption(): EChartsOption {
|
||||
if (period.value === 'intraday') {
|
||||
return buildIntradayOption(props.quote)
|
||||
return buildIntradayOption(props.quote.intraday, props.quote.prevSettlement)
|
||||
}
|
||||
return buildCandleOption(props.quote.candles[period.value])
|
||||
}
|
||||
|
||||
function flushOption(immediate = false) {
|
||||
if (immediate) {
|
||||
if (throttleTimer) {
|
||||
clearTimeout(throttleTimer)
|
||||
throttleTimer = null
|
||||
}
|
||||
pendingBuild = false
|
||||
option.value = buildOption()
|
||||
return
|
||||
}
|
||||
if (throttleTimer) {
|
||||
pendingBuild = true
|
||||
return
|
||||
}
|
||||
option.value = buildOption()
|
||||
throttleTimer = setTimeout(() => {
|
||||
throttleTimer = null
|
||||
if (pendingBuild) {
|
||||
pendingBuild = false
|
||||
option.value = buildOption()
|
||||
}
|
||||
}, CHART_THROTTLE_MS)
|
||||
}
|
||||
|
||||
// Only depend on chart-relevant fields (not trades / orderBook).
|
||||
watch(
|
||||
() => {
|
||||
if (period.value === 'intraday') {
|
||||
return [period.value, props.quote.intraday, props.quote.prevSettlement] as const
|
||||
}
|
||||
return [period.value, props.quote.candles[period.value]] as const
|
||||
},
|
||||
() => flushOption(false),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (throttleTimer) clearTimeout(throttleTimer)
|
||||
})
|
||||
|
||||
function buildIntradayOption(quote: QuoteData): EChartsOption {
|
||||
const points = quote.intraday
|
||||
function buildIntradayOption(points: IntradayPoint[], base: number): EChartsOption {
|
||||
const times = points.map((p) => p.time)
|
||||
const prices = points.map((p) => p.price)
|
||||
const avgs = points.map((p) => p.avg)
|
||||
const vols = points.map((p) => p.volume)
|
||||
const base = quote.prevSettlement
|
||||
|
||||
return {
|
||||
animation: false,
|
||||
@ -121,7 +168,8 @@ function buildIntradayOption(quote: QuoteData): EChartsOption {
|
||||
position: 'right',
|
||||
axisLabel: {
|
||||
fontSize: 10,
|
||||
formatter: (v: number) => `${(((v - base) / base) * 100).toFixed(2)}%`,
|
||||
formatter: (v: number) =>
|
||||
base ? `${(((v - base) / base) * 100).toFixed(2)}%` : '0.00%',
|
||||
},
|
||||
splitLine: { show: false },
|
||||
gridIndex: 0,
|
||||
|
||||
@ -11,7 +11,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<v-chart class="chart" :option="option" autoresize />
|
||||
<v-chart class="chart" :option="option" :update-options="updateOpts" autoresize />
|
||||
<div class="legend right">
|
||||
<div v-for="item in sellItems" :key="item.key" class="legend-item">
|
||||
<span class="swatch" :style="{ background: item.color }" />
|
||||
@ -26,7 +26,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { computed, onUnmounted, ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { use } from 'echarts/core'
|
||||
import { CanvasRenderer } from 'echarts/renderers'
|
||||
@ -39,6 +39,8 @@ import type { QuoteData, TradeTick } from '../../types'
|
||||
|
||||
use([CanvasRenderer, PieChart, TooltipComponent])
|
||||
|
||||
const PIE_THROTTLE_MS = 500
|
||||
|
||||
const props = defineProps<{
|
||||
quote: QuoteData
|
||||
}>()
|
||||
@ -123,18 +125,23 @@ function toItems(
|
||||
return { buyItems, sellItems, data: [...buyItems, ...sellItems] }
|
||||
}
|
||||
|
||||
const stats = computed(() => toItems(aggregate(props.quote.trades, threshold.value)))
|
||||
const stats = ref(toItems(aggregate([], threshold.value)))
|
||||
const buyItems = computed(() => stats.value.buyItems)
|
||||
const sellItems = computed(() => stats.value.sellItems)
|
||||
const option = ref<EChartsOption>({})
|
||||
const updateOpts = { notMerge: true, lazyUpdate: true }
|
||||
|
||||
const option = computed((): EChartsOption => {
|
||||
const data = stats.value.data.map((d) => ({
|
||||
let throttleTimer: ReturnType<typeof setTimeout> | null = null
|
||||
let pending = false
|
||||
|
||||
function buildPieOption(items: ReturnType<typeof toItems>): EChartsOption {
|
||||
const data = items.data.map((d) => ({
|
||||
name: d.name,
|
||||
value: d.value,
|
||||
itemStyle: { color: d.color },
|
||||
}))
|
||||
|
||||
return {
|
||||
animation: false,
|
||||
tooltip: {
|
||||
trigger: 'item',
|
||||
formatter: '{b}<br/>手数: {c}<br/>占比: {d}%',
|
||||
@ -151,6 +158,35 @@ const option = computed((): EChartsOption => {
|
||||
},
|
||||
],
|
||||
}
|
||||
}
|
||||
|
||||
function flushStats() {
|
||||
const next = toItems(aggregate(props.quote.trades, threshold.value))
|
||||
stats.value = next
|
||||
option.value = buildPieOption(next)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => [props.quote.trades, threshold.value] as const,
|
||||
() => {
|
||||
if (throttleTimer) {
|
||||
pending = true
|
||||
return
|
||||
}
|
||||
flushStats()
|
||||
throttleTimer = setTimeout(() => {
|
||||
throttleTimer = null
|
||||
if (pending) {
|
||||
pending = false
|
||||
flushStats()
|
||||
}
|
||||
}, PIE_THROTTLE_MS)
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
onUnmounted(() => {
|
||||
if (throttleTimer) clearTimeout(throttleTimer)
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@ -108,8 +108,10 @@ export function useAiAdvice() {
|
||||
snapshot: quota.getAnalysisSnapshot(),
|
||||
}
|
||||
|
||||
console.log('[AI] 提交给 DeepSeek 的关键词', payload.keywords)
|
||||
console.log('[AI] 提交给 DeepSeek 的数据快照', payload.snapshot)
|
||||
// 勿 console.log 完整 snapshot:DevTools 会长期持有大对象导致内存暴涨
|
||||
if (import.meta.env.DEV) {
|
||||
console.log('[AI] 分析', payload.snapshot.contract.code, payload.keywords.slice(0, 80))
|
||||
}
|
||||
|
||||
const advice = await fetchDeepSeekAdvice(apiKey, payload)
|
||||
if (epoch !== analysisEpoch) return
|
||||
|
||||
@ -137,10 +137,11 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
const code = activeContract().code
|
||||
if (msg.data.code && msg.data.code !== code) return
|
||||
const product = msg.data.product
|
||||
// Mutate in place — replacing quote.value forces every chart/UI to rebuild.
|
||||
if (product === 'tick') {
|
||||
quote.value = applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
||||
applyWsTick(quote.value, msg.data as BaiduWsTickData)
|
||||
} else if (product === 'snapshot') {
|
||||
quote.value = applyWsSnapshot(quote.value, msg.data as BaiduWsSnapshotData)
|
||||
applyWsSnapshot(quote.value, msg.data as BaiduWsSnapshotData)
|
||||
}
|
||||
}
|
||||
|
||||
@ -181,10 +182,11 @@ export const useQuotaStore = defineStore('quota', () => {
|
||||
startSessionWatch()
|
||||
}
|
||||
|
||||
/** (Re)connect only while page wants WS and within trading session. */
|
||||
/** Connect only while page wants WS and within trading session; skip if already running. */
|
||||
function connectWs() {
|
||||
if (!wsDesired || !isWsTradingSession()) return
|
||||
wsInSession.value = true
|
||||
if (quoteWs?.isRunning()) return
|
||||
quoteWs?.disconnect()
|
||||
quoteWs = new BaiduQuoteWs({
|
||||
onMessage: handleWsMessage,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user