mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 14:24:15 +08:00
feat(kline): show since-prev-close change and period count on hover
悬停某根K线时, 图表自带信息栏额外显示「至今」(该K线昨收到最新收盘的涨跌幅) 与「周期 N」(到最新K线共多少根); 鼠标移出图表区仅隐藏该字段, 其余保持。 - updateAxisPointer 重写: 区分 悬停K线变化 与 竖虚线显隐, 分别控制信息栏重绘与副图 graphic - 信息栏行高 height → min-height, 容纳「至今」字段不被裁剪 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react'
|
||||
import { chartTheme, getTheme, useTheme } from '@/lib/theme'
|
||||
import { fmtPct } from '@/lib/format'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
|
||||
@@ -832,6 +833,8 @@ export function EChartsCandlestick({
|
||||
const infoIdxRef = useRef<number>(data.length - 1)
|
||||
const compactRef = useRef(false)
|
||||
const userZoomRef = useRef<{ start: number; end: number } | null>(null)
|
||||
// 竖虚线(crosshair)是否可见: 控制信息栏「至今」字段的显隐。鼠标移出图表区即 false。
|
||||
const hoverActiveRef = useRef(false)
|
||||
|
||||
// 需要在闭包中访问最新值的变量 — 先声明占位,后面赋值
|
||||
const activeIndicatorsRef = useRef(activeIndicators)
|
||||
@@ -911,7 +914,7 @@ export function EChartsCandlestick({
|
||||
const floatShares = stockInfo?.float_shares
|
||||
const turnoverRate = floatShares && d.volume ? (d.volume * 100 / floatShares * 100) : null
|
||||
|
||||
let html = `<div style="display:flex;align-items:center;gap:6px;padding:0 8px;font:11px 'JetBrains Mono',monospace;select:none;height:20px;flex-wrap:wrap">`
|
||||
let html = `<div style="display:flex;align-items:center;gap:6px;padding:0 8px;font:11px 'JetBrains Mono',monospace;select:none;min-height:20px;flex-wrap:wrap">`
|
||||
html += `<span style="color:${CT().text}">${d.date}</span>`
|
||||
html += `<span style="color:${CT().text}">开</span>`
|
||||
html += `<span style="color:${d.open >= d.close ? THEME.bear : THEME.bull}">${d.open.toFixed(2)}</span>`
|
||||
@@ -930,11 +933,25 @@ export function EChartsCandlestick({
|
||||
html += `<span style="color:${CT().text}">换手</span>`
|
||||
html += `<span style="color:${CT().text}">${turnoverRate.toFixed(2)}%</span>`
|
||||
}
|
||||
// 至今: 仅当竖虚线(crosshair)在图上且鼠标悬停某根 K 线时显示。
|
||||
// 最新价取最后一根K线收盘 (后端 _maybe_inject_live_candle 盘中注入实时价, 收盘后即最近收盘)。
|
||||
// 基准取该K线昨收(前一日收盘), 与同花顺及全市场涨幅口径一致; 数据第一根K线无昨收则跳过。
|
||||
if (hoverActiveRef.current && prev && Number.isFinite(prev.close) && prev.close > 0) {
|
||||
const latestPrice = data[data.length - 1].close
|
||||
if (Number.isFinite(latestPrice)) {
|
||||
const sinceRatio = (latestPrice - prev.close) / prev.close
|
||||
const sinceClr = sinceRatio >= 0 ? THEME.bull : THEME.bear
|
||||
html += `<span style="color:${CT().text}">至今</span>`
|
||||
html += `<span style="color:${sinceClr}">${fmtPct(sinceRatio)}</span>`
|
||||
// 周期数: 从该K线(含)到最新一根K线共多少根; 悬停最后一根时为 1
|
||||
html += `<span style="color:${CT().text}">周期 ${data.length - idx}</span>`
|
||||
}
|
||||
}
|
||||
html += `</div>`
|
||||
|
||||
// 第二行: MA + BOLL
|
||||
if (showMA) {
|
||||
html += `<div style="display:flex;align-items:center;gap:10px;padding:0 8px;font:11px 'JetBrains Mono',monospace;select:none;height:20px;flex-wrap:wrap">`
|
||||
html += `<div style="display:flex;align-items:center;gap:10px;padding:0 8px;font:11px 'JetBrains Mono',monospace;select:none;min-height:20px;flex-wrap:wrap">`
|
||||
if (d.ma5 != null) html += `<span style="color:${THEME.ma5}">MA5:${Number(d.ma5).toFixed(2)}</span>`
|
||||
if (d.ma10 != null) html += `<span style="color:${THEME.ma10}">MA10:${Number(d.ma10).toFixed(2)}</span>`
|
||||
if (d.ma20 != null) html += `<span style="color:${THEME.ma20}">MA20:${Number(d.ma20).toFixed(2)}</span>`
|
||||
@@ -954,6 +971,8 @@ export function EChartsCandlestick({
|
||||
infoIdxRef.current = data.length - 1
|
||||
compactRef.current = false
|
||||
userZoomRef.current = null
|
||||
// 新数据无悬停上下文, 隐藏「至今」; 下次鼠标移动时由 updateAxisPointer 重新置位
|
||||
hoverActiveRef.current = false
|
||||
}, [data.length])
|
||||
|
||||
// ===== 初始化 chart (只在 chartHeight 变化时重建) =====
|
||||
@@ -965,32 +984,36 @@ export function EChartsCandlestick({
|
||||
chartRef.current = chart
|
||||
|
||||
// 鼠标移动 → 只更新 ref + DOM,不触发 React re-render
|
||||
// 设计原则: 找不到有效数据时保持上次显示,永远不清空信息栏
|
||||
// 设计原则: 找不到有效数据时保持上次显示,永远不清空信息栏; 鼠标移出时仅隐藏「至今」。
|
||||
chart.on('updateAxisPointer', (event: any) => {
|
||||
const axesInfo = event.axesInfo
|
||||
if (!axesInfo) return // 鼠标移出图表区域,保持当前显示
|
||||
for (const info of Object.values(axesInfo)) {
|
||||
const val = (info as any)?.value
|
||||
if (val == null) continue
|
||||
const d = dataRef.current
|
||||
const idx = typeof val === 'number' ? val : d.findIndex(x => x.date === val)
|
||||
if (idx >= 0 && idx < d.length) {
|
||||
if (infoIdxRef.current === idx) return
|
||||
infoIdxRef.current = idx
|
||||
|
||||
// 直接更新信息栏 DOM (通过 ref 读取最新的生成函数)
|
||||
const infoEl = infoBarRef.current
|
||||
if (infoEl) {
|
||||
const html = getInfoBarHTMLRef.current()
|
||||
if (html) infoEl.innerHTML = html // 只在有内容时更新
|
||||
}
|
||||
|
||||
// 更新子图 graphic
|
||||
triggerInfoBarUpdate()
|
||||
return
|
||||
const d = dataRef.current
|
||||
// 竖虚线是否正落在某根有效 K 线上 (鼠标在图表数据区内)
|
||||
let foundIdx = -1
|
||||
if (axesInfo) {
|
||||
for (const info of Object.values(axesInfo)) {
|
||||
const val = (info as any)?.value
|
||||
if (val == null) continue
|
||||
const idx = typeof val === 'number' ? val : d.findIndex(x => x.date === val)
|
||||
if (idx >= 0 && idx < d.length) { foundIdx = idx; break }
|
||||
}
|
||||
}
|
||||
// 没有找到有效数据 — 不做任何操作,保持上次显示
|
||||
const active = foundIdx >= 0
|
||||
const idxChanged = foundIdx >= 0 && infoIdxRef.current !== foundIdx
|
||||
const visChanged = active !== hoverActiveRef.current
|
||||
hoverActiveRef.current = active
|
||||
if (idxChanged) infoIdxRef.current = foundIdx
|
||||
// 竖虚线显隐或悬停 K 线变化 → 重绘一次信息栏 (控制「至今」字段显隐 + 当前 K 线数据)
|
||||
if (visChanged || idxChanged) {
|
||||
const infoEl = infoBarRef.current
|
||||
if (infoEl) {
|
||||
const html = getInfoBarHTMLRef.current()
|
||||
if (html) infoEl.innerHTML = html // 只在有内容时更新
|
||||
}
|
||||
}
|
||||
if (foundIdx < 0) return
|
||||
// 更新子图 graphic (仅悬停 K 线变化时; 纯显隐切换不影响副图)
|
||||
if (idxChanged) triggerInfoBarUpdate()
|
||||
})
|
||||
|
||||
chart.on('click', (params: any) => {
|
||||
@@ -1159,7 +1182,7 @@ export function EChartsCandlestick({
|
||||
if (!d) return ''
|
||||
const floatShares = stockInfo?.float_shares
|
||||
const turnoverRate = floatShares && d.volume ? (d.volume * 100 / floatShares * 100) : null
|
||||
let html = `<div style="display:flex;align-items:center;gap:6px;padding:0 8px;font:11px 'JetBrains Mono',monospace;height:20px;flex-wrap:wrap">`
|
||||
let html = `<div style="display:flex;align-items:center;gap:6px;padding:0 8px;font:11px 'JetBrains Mono',monospace;min-height:20px;flex-wrap:wrap">`
|
||||
html += `<span style="color:${CT().text}">${d.date}</span>`
|
||||
html += `<span style="color:${CT().text}">开</span>`
|
||||
html += `<span style="color:${d.open >= d.close ? THEME.bear : THEME.bull}">${d.open.toFixed(2)}</span>`
|
||||
@@ -1182,7 +1205,7 @@ export function EChartsCandlestick({
|
||||
}
|
||||
html += `</div>`
|
||||
if (showMA) {
|
||||
html += `<div style="display:flex;align-items:center;gap:10px;padding:0 8px;font:11px 'JetBrains Mono',monospace;height:20px;flex-wrap:wrap">`
|
||||
html += `<div style="display:flex;align-items:center;gap:10px;padding:0 8px;font:11px 'JetBrains Mono',monospace;min-height:20px;flex-wrap:wrap">`
|
||||
if (d.ma5 != null) html += `<span style="color:${THEME.ma5}">MA5:${Number(d.ma5).toFixed(2)}</span>`
|
||||
if (d.ma10 != null) html += `<span style="color:${THEME.ma10}">MA10:${Number(d.ma10).toFixed(2)}</span>`
|
||||
if (d.ma20 != null) html += `<span style="color:${THEME.ma20}">MA20:${Number(d.ma20).toFixed(2)}</span>`
|
||||
|
||||
Reference in New Issue
Block a user