mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 17:54:15 +08:00
Merge PR #45: feat 亮色主题 — 明暗切换 + 全部图表主题化
This commit is contained in:
+14
-1
@@ -1,7 +1,20 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN" class="dark">
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<script>
|
||||
// 预渲染主题脚本: 首屏前设置 html.dark, 避免亮暗闪烁 (FOUC)。
|
||||
// 默认暗色; 用户切过亮色后 (localStorage tf-theme=light) 保持亮色。
|
||||
(function () {
|
||||
try {
|
||||
if (localStorage.getItem('tf-theme') !== 'light') {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
} catch (e) {
|
||||
document.documentElement.classList.add('dark')
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="theme-color" content="#8B5CF6" />
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type CandlestickData,
|
||||
type HistogramData,
|
||||
} from 'lightweight-charts'
|
||||
import { useChartTheme } from '@/lib/theme'
|
||||
|
||||
export interface OHLC {
|
||||
date: string
|
||||
@@ -24,11 +25,9 @@ export function fmtBigNum(v: number): string {
|
||||
return v.toFixed(0)
|
||||
}
|
||||
|
||||
// 序列颜色 (双主题通用); 画布文字/网格/边框主题相关色走 ChartTheme
|
||||
const THEME = {
|
||||
background: 'transparent',
|
||||
textColor: '#A1A1AA',
|
||||
gridColor: 'rgba(255,255,255,0.04)',
|
||||
borderColor: '#27272A',
|
||||
bull: '#F04438',
|
||||
bear: '#12B76A',
|
||||
volBull: 'rgba(240,68,56,0.4)',
|
||||
@@ -45,6 +44,19 @@ export function CandlestickChart({ data, height = 480 }: Props) {
|
||||
const chartRef = useRef<IChartApi | null>(null)
|
||||
const candleRef = useRef<ISeriesApi<'Candlestick'> | null>(null)
|
||||
const volRef = useRef<ISeriesApi<'Histogram'> | null>(null)
|
||||
const ct = useChartTheme()
|
||||
const ctRef = useRef(ct)
|
||||
ctRef.current = ct
|
||||
|
||||
// 主题切换: 无需重建 chart, applyOptions 即时生效
|
||||
useEffect(() => {
|
||||
chartRef.current?.applyOptions({
|
||||
layout: { textColor: ct.text },
|
||||
grid: { vertLines: { color: ct.grid }, horzLines: { color: ct.grid } },
|
||||
rightPriceScale: { borderColor: ct.border },
|
||||
timeScale: { borderColor: ct.border },
|
||||
})
|
||||
}, [ct])
|
||||
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return
|
||||
@@ -55,22 +67,22 @@ export function CandlestickChart({ data, height = 480 }: Props) {
|
||||
height,
|
||||
layout: {
|
||||
background: { color: THEME.background },
|
||||
textColor: THEME.textColor,
|
||||
textColor: ctRef.current.text,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: 11,
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: THEME.gridColor },
|
||||
horzLines: { color: THEME.gridColor },
|
||||
vertLines: { color: ctRef.current.grid },
|
||||
horzLines: { color: ctRef.current.grid },
|
||||
},
|
||||
crosshair: {
|
||||
mode: CrosshairMode.Normal,
|
||||
vertLine: { labelVisible: false },
|
||||
horzLine: { labelVisible: false },
|
||||
},
|
||||
rightPriceScale: { borderColor: THEME.borderColor },
|
||||
rightPriceScale: { borderColor: ctRef.current.border },
|
||||
timeScale: {
|
||||
borderColor: THEME.borderColor,
|
||||
borderColor: ctRef.current.border,
|
||||
timeVisible: false,
|
||||
secondsVisible: false,
|
||||
},
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useCallback, useMemo } from 'react'
|
||||
import { chartTheme, getTheme, useTheme } from '@/lib/theme'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
|
||||
@@ -293,6 +294,7 @@ interface Props {
|
||||
activeIndicators?: string[]
|
||||
}
|
||||
|
||||
// 序列颜色 (双主题通用); 画布轴/网格/文字等主题相关色走 CT() 动态取
|
||||
const THEME = {
|
||||
bull: '#C74040',
|
||||
bear: '#2D9B65',
|
||||
@@ -302,12 +304,12 @@ const THEME = {
|
||||
ma10: '#3B82F6',
|
||||
ma20: '#F97316',
|
||||
ma60: '#8B5CF6',
|
||||
text: '#A1A1AA',
|
||||
grid: 'rgba(255,255,255,0.04)',
|
||||
border: '#27272A',
|
||||
bg: 'transparent',
|
||||
}
|
||||
|
||||
/** 当前主题的图表调色板 (buildOption/信息栏在渲染时调用; 主题切换由组件 effect 触发重建)。 */
|
||||
const CT = () => chartTheme(getTheme())
|
||||
|
||||
/** 可见蜡烛超过此数量时,涨停/炸板标签切换为小圆点。 */
|
||||
const COMPACT_THRESHOLD = 60
|
||||
|
||||
@@ -429,7 +431,7 @@ function buildOption(
|
||||
const isSell = m.kind === 'sell'
|
||||
|
||||
if (m.above) {
|
||||
const dotColor = m.color ?? (isBuy ? '#FACC15' : THEME.text)
|
||||
const dotColor = m.color ?? (isBuy ? '#FACC15' : CT().text)
|
||||
if (compact) {
|
||||
markPointData.push({
|
||||
name: m.date, coord: [m.date, d.high],
|
||||
@@ -457,11 +459,11 @@ function buildOption(
|
||||
symbol: 'arrow', symbolSize: 12,
|
||||
symbolRotate: isBuy ? 0 : 180,
|
||||
symbolOffset: isBuy ? [0, '60%'] : [0, '-60%'],
|
||||
itemStyle: { color: isBuy ? THEME.bull : isSell ? THEME.bear : THEME.text },
|
||||
itemStyle: { color: isBuy ? THEME.bull : isSell ? THEME.bear : CT().text },
|
||||
label: {
|
||||
show: !!m.label, formatter: m.label ?? '',
|
||||
position: isBuy ? 'bottom' : 'top', distance: 8,
|
||||
color: THEME.text, fontSize: 10,
|
||||
color: CT().text, fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
},
|
||||
})
|
||||
@@ -497,8 +499,8 @@ function buildOption(
|
||||
grids.push({ left, right, top: topPad, height: candleAvail })
|
||||
xAxes.push({
|
||||
type: 'category', data: dates, boundaryGap: true,
|
||||
axisLine: { lineStyle: { color: THEME.border } },
|
||||
axisLabel: { color: THEME.text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' },
|
||||
axisLine: { lineStyle: { color: CT().border } },
|
||||
axisLabel: { color: CT().text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' },
|
||||
axisTick: { show: false },
|
||||
splitLine: { show: false },
|
||||
})
|
||||
@@ -508,8 +510,8 @@ function buildOption(
|
||||
boundaryGap: [0.03, 0.03],
|
||||
splitArea: { show: false },
|
||||
axisLine: { show: false }, axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: THEME.grid } },
|
||||
axisLabel: { color: THEME.text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' },
|
||||
splitLine: { lineStyle: { color: CT().grid } },
|
||||
axisLabel: { color: CT().text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' },
|
||||
})
|
||||
xAxisIndices.push(0)
|
||||
|
||||
@@ -524,8 +526,8 @@ function buildOption(
|
||||
show: !!r.label,
|
||||
position: 'insideTop',
|
||||
distance: 8,
|
||||
color: '#DBEAFE',
|
||||
backgroundColor: 'rgba(15,23,42,0.72)',
|
||||
color: CT().tooltipText,
|
||||
backgroundColor: CT().tooltipBg,
|
||||
borderColor: 'rgba(59,130,246,0.35)',
|
||||
borderWidth: 1,
|
||||
borderRadius: 4,
|
||||
@@ -541,7 +543,7 @@ function buildOption(
|
||||
.filter(line => Number.isFinite(line.value))
|
||||
.map(line => {
|
||||
const lineStyle = {
|
||||
color: line.color ?? THEME.text,
|
||||
color: line.color ?? CT().text,
|
||||
type: 'dashed' as const,
|
||||
width: 1,
|
||||
opacity: 0.92,
|
||||
@@ -550,8 +552,8 @@ function buildOption(
|
||||
show: !!line.label,
|
||||
formatter: line.label ?? '',
|
||||
position: 'insideEndTop' as const,
|
||||
color: line.color ?? THEME.text,
|
||||
backgroundColor: 'rgba(15,23,42,0.72)',
|
||||
color: line.color ?? CT().text,
|
||||
backgroundColor: CT().tooltipBg,
|
||||
borderRadius: 4,
|
||||
padding: [2, 6],
|
||||
fontSize: 10,
|
||||
@@ -577,7 +579,7 @@ function buildOption(
|
||||
color: '#3B82F6',
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
backgroundColor: 'rgba(24,24,27,0.85)',
|
||||
backgroundColor: CT().tooltipBg,
|
||||
borderColor: '#3B82F6',
|
||||
borderWidth: 1,
|
||||
padding: [1, 4],
|
||||
@@ -642,7 +644,7 @@ function buildOption(
|
||||
top: chartTop,
|
||||
height: def.height,
|
||||
show: true,
|
||||
borderColor: 'rgba(255,255,255,0.06)',
|
||||
borderColor: CT().grid,
|
||||
borderWidth: 1,
|
||||
})
|
||||
|
||||
@@ -660,9 +662,9 @@ function buildOption(
|
||||
gridIndex: gridIdx,
|
||||
splitNumber: 2,
|
||||
axisLine: { show: false }, axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: THEME.grid } },
|
||||
splitLine: { lineStyle: { color: CT().grid } },
|
||||
axisLabel: {
|
||||
show: true, color: THEME.text, fontSize: 9,
|
||||
show: true, color: CT().text, fontSize: 9,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
},
|
||||
})
|
||||
@@ -686,7 +688,7 @@ function buildOption(
|
||||
backgroundColor: THEME.bg,
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
axisPointer: { type: 'cross', crossStyle: { color: '#555' } },
|
||||
axisPointer: { type: 'cross', crossStyle: { color: CT().crosshair } },
|
||||
backgroundColor: 'transparent',
|
||||
borderWidth: 0,
|
||||
textStyle: { fontSize: 0 },
|
||||
@@ -695,7 +697,7 @@ function buildOption(
|
||||
axisPointer: {
|
||||
link: [{ xAxisIndex: 'all' }],
|
||||
label: {
|
||||
backgroundColor: '#333',
|
||||
backgroundColor: CT().crosshairLabelBg,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
fontSize: 10,
|
||||
},
|
||||
@@ -742,6 +744,8 @@ export function EChartsCandlestick({
|
||||
dataRef.current = data
|
||||
const onDateClickRef = useRef(onDateClick)
|
||||
onDateClickRef.current = onDateClick
|
||||
// 主题: buildOption/信息栏内部通过 CT() 动态取调色板, 这里只负责切换时触发重建
|
||||
const theme = useTheme()
|
||||
|
||||
// --- 全部用 ref,避免高频交互触发 React 重渲染 ---
|
||||
const infoIdxRef = useRef<number>(data.length - 1)
|
||||
@@ -819,14 +823,14 @@ export function EChartsCandlestick({
|
||||
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">`
|
||||
html += `<span style="color:${THEME.text}">${d.date}</span>`
|
||||
html += `<span style="color:${THEME.text}">开</span>`
|
||||
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>`
|
||||
html += `<span style="color:${THEME.text}">高</span>`
|
||||
html += `<span style="color:${CT().text}">高</span>`
|
||||
html += `<span style="color:${THEME.bull}">${d.high.toFixed(2)}</span>`
|
||||
html += `<span style="color:${THEME.text}">低</span>`
|
||||
html += `<span style="color:${CT().text}">低</span>`
|
||||
html += `<span style="color:${THEME.bear}">${d.low.toFixed(2)}</span>`
|
||||
html += `<span style="color:${THEME.text}">收</span>`
|
||||
html += `<span style="color:${CT().text}">收</span>`
|
||||
html += `<span style="color:${clr};font-weight:600">${d.close.toFixed(2)}</span>`
|
||||
// 涨跌幅 (收盘后, 换手前; 和收间隔一些距离)
|
||||
if (prev) {
|
||||
@@ -834,8 +838,8 @@ export function EChartsCandlestick({
|
||||
html += `<span style="color:${clr};margin-left:8px">${isUp ? '+' : ''}${chgPct.toFixed(2)}%</span>`
|
||||
}
|
||||
if (turnoverRate != null) {
|
||||
html += `<span style="color:${THEME.text}">换手</span>`
|
||||
html += `<span style="color:${THEME.text}">${turnoverRate.toFixed(2)}%</span>`
|
||||
html += `<span style="color:${CT().text}">换手</span>`
|
||||
html += `<span style="color:${CT().text}">${turnoverRate.toFixed(2)}%</span>`
|
||||
}
|
||||
html += `</div>`
|
||||
|
||||
@@ -961,7 +965,7 @@ export function EChartsCandlestick({
|
||||
const isBuy = m.kind === 'buy'
|
||||
const isSell = m.kind === 'sell'
|
||||
if (m.above) {
|
||||
const dotColor = m.color ?? (isBuy ? '#FACC15' : THEME.text)
|
||||
const dotColor = m.color ?? (isBuy ? '#FACC15' : CT().text)
|
||||
if (compact) {
|
||||
markPointData.push({
|
||||
name: m.date, coord: [m.date, d.high],
|
||||
@@ -989,11 +993,11 @@ export function EChartsCandlestick({
|
||||
symbol: 'arrow', symbolSize: 12,
|
||||
symbolRotate: isBuy ? 0 : 180,
|
||||
symbolOffset: isBuy ? [0, '60%'] : [0, '-60%'],
|
||||
itemStyle: { color: isBuy ? THEME.bull : isSell ? THEME.bear : THEME.text },
|
||||
itemStyle: { color: isBuy ? THEME.bull : isSell ? THEME.bear : CT().text },
|
||||
label: {
|
||||
show: !!m.label, formatter: m.label ?? '',
|
||||
position: isBuy ? 'bottom' : 'top', distance: 8,
|
||||
color: THEME.text, fontSize: 10,
|
||||
color: CT().text, fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
},
|
||||
})
|
||||
@@ -1038,7 +1042,7 @@ export function EChartsCandlestick({
|
||||
if (infoEl) {
|
||||
infoEl.innerHTML = getInfoBarHTML()
|
||||
}
|
||||
}, [data, markers, ranges, priceLines, linkedPrice, showMA, showMarkersProp, activeIndicators, chartHeight, dates, dateIndexMap, initialZoom, getInfoBarHTML])
|
||||
}, [data, markers, ranges, priceLines, linkedPrice, showMA, showMarkersProp, activeIndicators, chartHeight, dates, dateIndexMap, initialZoom, getInfoBarHTML, theme])
|
||||
|
||||
// 渲染信息栏容器 (内容由 JS 直接写入)
|
||||
const initialHTML = useMemo(() => {
|
||||
@@ -1048,14 +1052,14 @@ 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;height:20px;flex-wrap:wrap">`
|
||||
html += `<span style="color:${THEME.text}">${d.date}</span>`
|
||||
html += `<span style="color:${THEME.text}">开</span>`
|
||||
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>`
|
||||
html += `<span style="color:${THEME.text}">高</span>`
|
||||
html += `<span style="color:${CT().text}">高</span>`
|
||||
html += `<span style="color:${THEME.bull}">${d.high.toFixed(2)}</span>`
|
||||
html += `<span style="color:${THEME.text}">低</span>`
|
||||
html += `<span style="color:${CT().text}">低</span>`
|
||||
html += `<span style="color:${THEME.bear}">${d.low.toFixed(2)}</span>`
|
||||
html += `<span style="color:${THEME.text}">收</span>`
|
||||
html += `<span style="color:${CT().text}">收</span>`
|
||||
const prevClose0 = data[idx-1]?.close ?? d.close
|
||||
const clr0 = d.close >= prevClose0 ? THEME.bull : THEME.bear
|
||||
html += `<span style="color:${clr0};font-weight:600">${d.close.toFixed(2)}</span>`
|
||||
@@ -1065,8 +1069,8 @@ export function EChartsCandlestick({
|
||||
html += `<span style="color:${clr0};margin-left:8px">${chgPct0 >= 0 ? '+' : ''}${chgPct0.toFixed(2)}%</span>`
|
||||
}
|
||||
if (turnoverRate != null) {
|
||||
html += `<span style="color:${THEME.text}">换手</span>`
|
||||
html += `<span style="color:${THEME.text}">${turnoverRate.toFixed(2)}%</span>`
|
||||
html += `<span style="color:${CT().text}">换手</span>`
|
||||
html += `<span style="color:${CT().text}">${turnoverRate.toFixed(2)}%</span>`
|
||||
}
|
||||
html += `</div>`
|
||||
if (showMA) {
|
||||
@@ -1088,7 +1092,7 @@ export function EChartsCandlestick({
|
||||
<div className="w-full">
|
||||
{/* 主图信息栏 — 内容由 JS 直接操作 innerHTML */}
|
||||
{showInfoBar && (
|
||||
<div ref={infoBarRef} style={{ backgroundColor: 'rgba(39,39,42,0.6)' }}
|
||||
<div ref={infoBarRef} style={{ backgroundColor: CT().infoBarBg }}
|
||||
dangerouslySetInnerHTML={{ __html: initialHTML }} />
|
||||
)}
|
||||
|
||||
|
||||
@@ -2,19 +2,17 @@ import { useEffect, useMemo, useRef, useState } from 'react'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
import type { MinuteKlineRow } from '@/lib/api'
|
||||
import { useChartTheme, type ChartTheme } from '@/lib/theme'
|
||||
|
||||
type YMode = 'adaptive' | 'limit'
|
||||
|
||||
// 序列颜色 (双主题通用); 画布轴/网格/十字线等主题相关色走 ChartTheme
|
||||
const THEME = {
|
||||
line: '#3B82F6',
|
||||
areaFill: 'rgba(59,130,246,0.40)',
|
||||
avgLine: '#F59E0B',
|
||||
refLine: 'rgba(255,255,255,0.25)',
|
||||
volUp: 'rgba(240,68,56,0.6)',
|
||||
volDown: 'rgba(18,183,106,0.6)',
|
||||
text: '#A1A1AA',
|
||||
grid: 'rgba(255,255,255,0.04)',
|
||||
border: '#27272A',
|
||||
}
|
||||
|
||||
interface Props {
|
||||
@@ -108,7 +106,7 @@ function getLimitPrices(prevClose: number, symbol?: string): {
|
||||
return { limitUp, limitDown, upPct, downPct }
|
||||
}
|
||||
|
||||
function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgPrices: number[], lineColor: string, areaColor: string, yMode: YMode, symbol?: string, showLimitLines = true, showAvgLine = true): EChartsOption {
|
||||
function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgPrices: number[], lineColor: string, areaColor: string, yMode: YMode, ct: ChartTheme, symbol?: string, showLimitLines = true, showAvgLine = true): EChartsOption {
|
||||
// 将数据映射到全天时间轴上的正确位置
|
||||
const timeIndexMap = new Map(FULL_DAY_TIMES.map((t, i) => [t, i]))
|
||||
const closes = new Array(FULL_DAY_TIMES.length).fill(null) as (number | null)[]
|
||||
@@ -150,7 +148,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
if (prevClose != null) {
|
||||
markLineData.push({
|
||||
yAxis: prevClose,
|
||||
lineStyle: { color: THEME.refLine, type: 'dashed', width: 1 },
|
||||
lineStyle: { color: ct.crosshair, type: 'dashed', width: 1 },
|
||||
label: { show: false },
|
||||
symbol: 'none',
|
||||
})
|
||||
@@ -237,16 +235,16 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
type: 'cross',
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(39,39,42,0.9)',
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: ct.tooltipBg,
|
||||
borderColor: ct.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
padding: [2, 5],
|
||||
color: '#A1A1AA',
|
||||
color: ct.tooltipText,
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
},
|
||||
crossStyle: { color: 'rgba(255,255,255,0.2)', type: 'dashed', width: 1 },
|
||||
lineStyle: { color: 'rgba(255,255,255,0.2)', type: 'dashed', width: 1 },
|
||||
crossStyle: { color: ct.crosshair, type: 'dashed', width: 1 },
|
||||
lineStyle: { color: ct.crosshair, type: 'dashed', width: 1 },
|
||||
},
|
||||
},
|
||||
axisPointer: {
|
||||
@@ -263,14 +261,14 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
boundaryGap: false,
|
||||
axisPointer: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(255,255,255,0.2)', type: 'dashed', width: 1 },
|
||||
lineStyle: { color: ct.crosshair, type: 'dashed', width: 1 },
|
||||
label: {
|
||||
show: true,
|
||||
backgroundColor: 'rgba(39,39,42,0.9)',
|
||||
borderColor: 'rgba(255,255,255,0.1)',
|
||||
backgroundColor: ct.tooltipBg,
|
||||
borderColor: ct.tooltipBorder,
|
||||
borderWidth: 1,
|
||||
padding: [2, 4],
|
||||
color: '#A1A1AA',
|
||||
color: ct.tooltipText,
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
formatter: (params: any) => {
|
||||
@@ -280,7 +278,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
},
|
||||
axisLine: { show: false },
|
||||
axisLabel: {
|
||||
color: THEME.text,
|
||||
color: ct.text,
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
formatter: xAxisLabelFormatter,
|
||||
@@ -289,7 +287,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
axisTick: { show: false },
|
||||
splitLine: {
|
||||
show: true,
|
||||
lineStyle: { color: 'rgba(255,255,255,0.04)' },
|
||||
lineStyle: { color: ct.grid },
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -312,7 +310,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
splitArea: { show: false },
|
||||
axisLine: { show: false },
|
||||
axisTick: { show: false },
|
||||
splitLine: { lineStyle: { color: THEME.grid } },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
axisPointer: {
|
||||
label: {
|
||||
formatter: (params: any) => {
|
||||
@@ -322,7 +320,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: THEME.text,
|
||||
color: ct.text,
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
formatter: (v: number) => v.toFixed(2),
|
||||
@@ -360,7 +358,7 @@ function buildOption(data: MinuteKlineRow[], prevClose: number | undefined, avgP
|
||||
},
|
||||
},
|
||||
axisLabel: {
|
||||
color: THEME.text,
|
||||
color: ct.text,
|
||||
fontSize: 10,
|
||||
fontFamily: 'JetBrains Mono, monospace',
|
||||
formatter: (v: number) => {
|
||||
@@ -420,6 +418,7 @@ export function EChartsIntraday({ data, height = 320, prevClose, date, symbol, o
|
||||
|
||||
const [infoIdx, setInfoIdx] = useState(data.length - 1)
|
||||
const [yMode, setYMode] = useState<YMode>('adaptive')
|
||||
const ct = useChartTheme()
|
||||
const avgPrices = useMemo(() => computeAvgPrice(data), [data])
|
||||
|
||||
// 分时线颜色:基于最新价 vs 昨收
|
||||
@@ -494,11 +493,11 @@ export function EChartsIntraday({ data, height = 320, prevClose, date, symbol, o
|
||||
}
|
||||
fullDayToDataIdx.current = mapping
|
||||
|
||||
chart.setOption(buildOption(data, prevClose, avgPrices, lineColor, areaFill, yMode, symbol, showLimitLines, showAvgLine), true)
|
||||
chart.setOption(buildOption(data, prevClose, avgPrices, lineColor, areaFill, yMode, ct, symbol, showLimitLines, showAvgLine), true)
|
||||
} else {
|
||||
chart.clear()
|
||||
}
|
||||
}, [data, prevClose, height, lineColor, areaFill, yMode, symbol, showLimitLines, showAvgLine])
|
||||
}, [data, prevClose, height, lineColor, areaFill, yMode, ct, symbol, showLimitLines, showAvgLine])
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
@@ -548,7 +547,7 @@ export function EChartsIntraday({ data, height = 320, prevClose, date, symbol, o
|
||||
</button>
|
||||
</div>
|
||||
</div>}
|
||||
<div style={{ backgroundColor: 'rgba(39,39,42,0.6)' }}>
|
||||
<div style={{ backgroundColor: ct.infoBarBg }}>
|
||||
{/* 第一行: 日期 + OHLC */}
|
||||
<div className="flex items-center gap-x-2 px-2 font-mono text-[11px] select-none flex-wrap" style={{ height: 20 }}>
|
||||
{!d && <span className="text-muted">—</span>}
|
||||
|
||||
@@ -296,8 +296,8 @@ export function ExtDimensionAnalysis({
|
||||
页面按分析菜单配置读取扩展数据源,使用分组字段生成榜单,并严格按照 detail_columns 渲染明细列。
|
||||
</p>
|
||||
</div>
|
||||
<div className="absolute right-8 top-6 hidden h-28 w-28 rounded-full border border-white/10 bg-white/[0.03] lg:flex items-center justify-center">
|
||||
<BarChart3 className="h-12 w-12 text-white/20" />
|
||||
<div className="absolute right-8 top-6 hidden h-28 w-28 rounded-full border border-foreground/10 bg-foreground/[0.03] lg:flex items-center justify-center">
|
||||
<BarChart3 className="h-12 w-12 text-foreground/20" />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
||||
@@ -43,11 +43,14 @@ import {
|
||||
CheckCircle2,
|
||||
BookOpenCheck,
|
||||
ExternalLink,
|
||||
Sun,
|
||||
Moon,
|
||||
X,
|
||||
} from 'lucide-react'
|
||||
import { Logo } from './Logo'
|
||||
import { api, type IndexQuote } from '@/lib/api'
|
||||
import { cn } from '@/lib/cn'
|
||||
import { toggleTheme, useTheme } from '@/lib/theme'
|
||||
import { setCurrentTotal as setAlertTotal, useUnreadAlerts } from '@/lib/monitorBadge'
|
||||
|
||||
// 品牌色 — 只用于 logo / brand 区域,不影响功能语义色
|
||||
@@ -80,6 +83,22 @@ const nav = [
|
||||
{ to: '/data', label: '数据', icon: Database },
|
||||
] as const
|
||||
|
||||
/** 亮/暗主题切换 — 状态存 localStorage, 生效见 lib/theme.ts */
|
||||
function ThemeToggle() {
|
||||
const theme = useTheme()
|
||||
const dark = theme === 'dark'
|
||||
return (
|
||||
<button
|
||||
onClick={() => toggleTheme()}
|
||||
className="flex w-full items-center gap-3 rounded-btn px-3 py-2 text-sm text-foreground/80 transition-colors duration-150 ease-smooth hover:bg-elevated hover:text-foreground cursor-pointer"
|
||||
title={dark ? '切换到亮色模式' : '切换到暗色模式'}
|
||||
>
|
||||
{dark ? <Sun className="h-4 w-4 shrink-0" /> : <Moon className="h-4 w-4 shrink-0" />}
|
||||
<span>{dark ? '亮色模式' : '暗色模式'}</span>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
function fmtIndexValue(v: number | null | undefined) {
|
||||
if (v == null || Number.isNaN(Number(v))) return '--'
|
||||
return Number(v).toFixed(2)
|
||||
@@ -547,6 +566,7 @@ export function Layout() {
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border px-2 py-3 space-y-0.5 shrink-0">
|
||||
<ThemeToggle />
|
||||
<NavLink
|
||||
to="/settings"
|
||||
className={({ isActive }) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef, useMemo, useState } from 'react'
|
||||
import { chartTheme, getTheme, useTheme } from '@/lib/theme'
|
||||
import * as echarts from 'echarts'
|
||||
import type { ECharts, EChartsOption } from 'echarts'
|
||||
import type { KlineRow, LevelSeries } from '@/lib/api'
|
||||
@@ -18,16 +19,17 @@ import type { KlineRow, LevelSeries } from '@/lib/api'
|
||||
* - 指标副图: 后续如需 MACD/KDJ,按 SUB_CHARTS 模式扩展
|
||||
*/
|
||||
|
||||
// ===== 配色(与主图一致的红涨绿跌,深色背景) =====
|
||||
// ===== 配色(红涨绿跌, 双主题通用); 画布轴/网格主题相关色走 CT() =====
|
||||
const THEME = {
|
||||
bull: '#C74040',
|
||||
bear: '#2D9B65',
|
||||
text: '#A1A1AA',
|
||||
grid: 'rgba(255,255,255,0.04)',
|
||||
volUp: 'rgba(240,68,56,0.5)',
|
||||
volDown: 'rgba(18,183,106,0.5)',
|
||||
}
|
||||
|
||||
/** 当前主题的图表调色板 (buildOption 渲染时调用; 切换由组件 effect 触发重建)。 */
|
||||
const CT = () => chartTheme(getTheme())
|
||||
|
||||
// ===== 价位类型(与后端 levels.py 的 LEVEL_TYPES 对齐) =====
|
||||
export type LevelType = 'sr' | 'pivot' | 'extreme' | 'boll' | 'keltner_s' | 'keltner_m' | 'keltner_l' | 'atr_stop' | 'gap' | 'fib' | 'round'
|
||||
|
||||
@@ -123,6 +125,8 @@ export function AnalysisKChart({
|
||||
}: Props) {
|
||||
const chartRef = useRef<HTMLDivElement>(null)
|
||||
const chartInstRef = useRef<ECharts | null>(null)
|
||||
// 主题: buildOption 内部用 CT() 动态取色, 这里只负责切换时触发重建
|
||||
const theme = useTheme()
|
||||
const [activeTypes, setActiveTypes] = useState<Set<LevelType>>(new Set(defaultLevelTypes))
|
||||
/** 枢轴点显示到第几档:1=只P+R1/S1, 2=到R2/S2, 3=全档(R3/S3) */
|
||||
const [pivotRank, setPivotRank] = useState<1 | 2 | 3>(1)
|
||||
@@ -289,8 +293,8 @@ export function AnalysisKChart({
|
||||
xAxis: [
|
||||
{
|
||||
type: 'category', data: dates, boundaryGap: true,
|
||||
axisLine: { lineStyle: { color: THEME.grid } },
|
||||
axisLabel: { color: THEME.text, fontSize: 10 },
|
||||
axisLine: { lineStyle: { color: CT().grid } },
|
||||
axisLabel: { color: CT().text, fontSize: 10 },
|
||||
splitLine: { show: false },
|
||||
axisPointer: { show: true, label: { show: false } },
|
||||
},
|
||||
@@ -300,19 +304,19 @@ export function AnalysisKChart({
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
{ scale: true, splitLine: { lineStyle: { color: THEME.grid } },
|
||||
axisLabel: { color: THEME.text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' } },
|
||||
{ scale: true, splitLine: { lineStyle: { color: CT().grid } },
|
||||
axisLabel: { color: CT().text, fontSize: 10, fontFamily: 'JetBrains Mono, monospace' } },
|
||||
{ scale: true, gridIndex: 1, splitNumber: 2,
|
||||
// 成交量区不画背景横线
|
||||
splitLine: { show: false },
|
||||
axisLabel: { color: THEME.text, fontSize: 9, fontFamily: 'JetBrains Mono, monospace',
|
||||
axisLabel: { color: CT().text, fontSize: 9, fontFamily: 'JetBrains Mono, monospace',
|
||||
formatter: (v: number) => fmtVol(v) } },
|
||||
],
|
||||
dataZoom: [
|
||||
{ type: 'inside', xAxisIndex: [0, 1], start: zoomStart, end: 100 },
|
||||
{ type: 'slider', xAxisIndex: [0, 1], bottom: sliderBottom, height: SLIDER_H, start: zoomStart, end: 100,
|
||||
borderColor: 'transparent', fillerColor: 'rgba(255,255,255,0.06)',
|
||||
handleStyle: { color: '#52525B' }, textStyle: { color: THEME.text, fontSize: 10 } },
|
||||
borderColor: 'transparent', fillerColor: CT().zoomFill,
|
||||
handleStyle: { color: '#52525B' }, textStyle: { color: CT().text, fontSize: 10 } },
|
||||
],
|
||||
// 不弹 hover tooltip(用户要求);但保留十字线 axisPointer 作为缩放/定位参照
|
||||
tooltip: { show: false },
|
||||
@@ -335,7 +339,7 @@ export function AnalysisKChart({
|
||||
}
|
||||
chartInstRef.current.setOption(buildOption(), true)
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [rows, levels, series, seriesDates, activeTypes, pivotRank, markers, ranges, height])
|
||||
}, [rows, levels, series, seriesDates, activeTypes, pivotRank, markers, ranges, height, theme])
|
||||
|
||||
// resize
|
||||
useEffect(() => {
|
||||
@@ -464,7 +468,7 @@ function LevelOverview({
|
||||
}
|
||||
|
||||
const Row = ({ p }: { p: PriceLevel }) => {
|
||||
const color = LEVEL_GROUPS.find(g => g.key === p.type)?.color ?? THEME.text
|
||||
const color = LEVEL_GROUPS.find(g => g.key === p.type)?.color ?? CT().text
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-0.5">
|
||||
<span className="h-1.5 w-1.5 rounded-full shrink-0" style={{ backgroundColor: color }} />
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
// 主题管理 — 暗色(默认) / 亮色切换
|
||||
//
|
||||
// 机制:
|
||||
// - 状态存 localStorage('tf-theme'), 默认 dark (保持老用户体验不变)
|
||||
// - 生效方式: html.dark class (index.css 的 CSS variables + Tailwind darkMode:class)
|
||||
// - index.html 里有预渲染内联脚本, 首屏前就设好 class, 避免闪烁 (FOUC)
|
||||
// - UI token (bg-surface/text-foreground 等) 自动跟随;
|
||||
// 图表画布不吃 CSS 变量, 统一走 useChartTheme() 取调色板
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
const KEY = 'tf-theme'
|
||||
const EVENT = 'tf-theme-change'
|
||||
|
||||
export type Theme = 'dark' | 'light'
|
||||
|
||||
export function getTheme(): Theme {
|
||||
try {
|
||||
return localStorage.getItem(KEY) === 'light' ? 'light' : 'dark'
|
||||
} catch {
|
||||
return 'dark'
|
||||
}
|
||||
}
|
||||
|
||||
export function setTheme(theme: Theme) {
|
||||
try { localStorage.setItem(KEY, theme) } catch { /* ignore */ }
|
||||
document.documentElement.classList.toggle('dark', theme === 'dark')
|
||||
window.dispatchEvent(new CustomEvent(EVENT, { detail: theme }))
|
||||
}
|
||||
|
||||
export function toggleTheme(): Theme {
|
||||
const next: Theme = getTheme() === 'dark' ? 'light' : 'dark'
|
||||
setTheme(next)
|
||||
return next
|
||||
}
|
||||
|
||||
/** 订阅当前主题 (本页切换 + 其他标签页切换均同步)。 */
|
||||
export function useTheme(): Theme {
|
||||
const [theme, set] = useState<Theme>(getTheme)
|
||||
useEffect(() => {
|
||||
const onChange = () => set(getTheme())
|
||||
window.addEventListener(EVENT, onChange)
|
||||
window.addEventListener('storage', onChange) // 跨标签页同步
|
||||
return () => {
|
||||
window.removeEventListener(EVENT, onChange)
|
||||
window.removeEventListener('storage', onChange)
|
||||
}
|
||||
}, [])
|
||||
return theme
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// 图表调色板 — ECharts / lightweight-charts 画布不吃 CSS 变量,
|
||||
// 所有图表组件统一从这里取色, 主题切换时依赖 useTheme 重建 option。
|
||||
// bull/bear/accent 等语义色双主题一致, 不在此重复定义。
|
||||
// ================================================================
|
||||
|
||||
export interface ChartTheme {
|
||||
/** 轴刻度/图例等常规文字 */
|
||||
text: string
|
||||
/** 信息条/图例里的强调文字 */
|
||||
textStrong: string
|
||||
/** 网格线 */
|
||||
grid: string
|
||||
/** 轴线/边框 */
|
||||
border: string
|
||||
/** 十字光标线 */
|
||||
crosshair: string
|
||||
/** 十字光标轴标签背景 */
|
||||
crosshairLabelBg: string
|
||||
/** tooltip 背景 */
|
||||
tooltipBg: string
|
||||
/** tooltip 边框 */
|
||||
tooltipBorder: string
|
||||
/** tooltip 文字 */
|
||||
tooltipText: string
|
||||
/** 半透明信息条背景 (K线图左上角 OHLC 条) */
|
||||
infoBarBg: string
|
||||
/** dataZoom 滑块填充 */
|
||||
zoomFill: string
|
||||
/** 分时图均价线以外的弱填充 */
|
||||
fillSubtle: string
|
||||
}
|
||||
|
||||
const DARK: ChartTheme = {
|
||||
text: '#A1A1AA',
|
||||
textStrong: '#E4E4E7',
|
||||
grid: 'rgba(255,255,255,0.06)',
|
||||
border: '#27272A',
|
||||
crosshair: 'rgba(255,255,255,0.25)',
|
||||
crosshairLabelBg: '#333',
|
||||
tooltipBg: 'rgba(24,24,27,0.95)',
|
||||
tooltipBorder: 'rgba(255,255,255,0.1)',
|
||||
tooltipText: '#E4E4E7',
|
||||
infoBarBg: 'rgba(39,39,42,0.6)',
|
||||
zoomFill: 'rgba(255,255,255,0.06)',
|
||||
fillSubtle: 'rgba(255,255,255,0.04)',
|
||||
}
|
||||
|
||||
const LIGHT: ChartTheme = {
|
||||
text: '#71717A',
|
||||
textStrong: '#27272A',
|
||||
grid: 'rgba(0,0,0,0.06)',
|
||||
border: '#E4E4E7',
|
||||
crosshair: 'rgba(0,0,0,0.3)',
|
||||
crosshairLabelBg: '#52525B',
|
||||
tooltipBg: 'rgba(255,255,255,0.97)',
|
||||
tooltipBorder: 'rgba(0,0,0,0.1)',
|
||||
tooltipText: '#27272A',
|
||||
infoBarBg: 'rgba(244,244,245,0.85)',
|
||||
zoomFill: 'rgba(0,0,0,0.06)',
|
||||
fillSubtle: 'rgba(0,0,0,0.04)',
|
||||
}
|
||||
|
||||
export function chartTheme(theme: Theme): ChartTheme {
|
||||
return theme === 'dark' ? DARK : LIGHT
|
||||
}
|
||||
|
||||
/** hook: 当前主题的图表调色板 (主题切换自动触发重渲染)。 */
|
||||
export function useChartTheme(): ChartTheme {
|
||||
return chartTheme(useTheme())
|
||||
}
|
||||
@@ -325,24 +325,25 @@ function EmotionRadar({ radar, score }: { radar: OverviewMarket['radar']; score:
|
||||
<stop offset="0%" stopColor={`${color}57`} />
|
||||
<stop offset="100%" stopColor={`${color}1f`} />
|
||||
</radialGradient>
|
||||
{/* 中心/网格用 CSS 变量取色, 亮暗主题自动切换 (SVG 属性支持 hsl(var(--x))) */}
|
||||
<radialGradient id="emotionRadarCenter" cx="50%" cy="50%" r="55%">
|
||||
<stop offset="0%" stopColor="rgba(15,23,42,0.92)" />
|
||||
<stop offset="68%" stopColor="rgba(15,23,42,0.70)" />
|
||||
<stop offset="100%" stopColor="rgba(15,23,42,0)" />
|
||||
<stop offset="0%" stopColor="hsl(var(--surface) / 0.92)" />
|
||||
<stop offset="68%" stopColor="hsl(var(--surface) / 0.70)" />
|
||||
<stop offset="100%" stopColor="hsl(var(--surface) / 0)" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
{gridPolygons.map(g => (
|
||||
<polygon
|
||||
key={g.level}
|
||||
points={g.points}
|
||||
fill={g.idx % 2 === 0 ? 'rgba(30,41,59,0.26)' : 'rgba(15,23,42,0.16)'}
|
||||
stroke={g.level === 1 ? 'rgba(148,163,184,0.22)' : 'rgba(148,163,184,0.12)'}
|
||||
fill={g.idx % 2 === 0 ? 'hsl(var(--elevated) / 0.55)' : 'hsl(var(--elevated) / 0.3)'}
|
||||
stroke={g.level === 1 ? 'hsl(var(--border) / 0.9)' : 'hsl(var(--border) / 0.5)'}
|
||||
strokeWidth={g.level === 1 ? 1.2 : 0.8}
|
||||
/>
|
||||
))}
|
||||
{points.map(p => <line key={p.key} x1={cx} y1={cy} x2={p.gx} y2={p.gy} stroke="rgba(148,163,184,0.08)" />)}
|
||||
{points.map(p => <line key={p.key} x1={cx} y1={cy} x2={p.gx} y2={p.gy} stroke="hsl(var(--border) / 0.4)" />)}
|
||||
<polygon points={polygon} fill="url(#emotionRadarFill)" stroke={color} strokeWidth="2" />
|
||||
{points.map(p => <circle key={p.key} cx={p.x} cy={p.y} r="2.8" fill={color} stroke="rgba(15,23,42,0.9)" strokeWidth="1" />)}
|
||||
{points.map(p => <circle key={p.key} cx={p.x} cy={p.y} r="2.8" fill={color} stroke="hsl(var(--surface) / 0.9)" strokeWidth="1" />)}
|
||||
<circle cx={cx} cy={cy} r="29" fill="url(#emotionRadarCenter)" />
|
||||
<text x={cx} y={cy + 7} textAnchor="middle" className="fill-foreground font-mono text-[24px] font-bold">{score}</text>
|
||||
{points.map(p => (
|
||||
|
||||
@@ -10,6 +10,7 @@ import { storage } from '@/lib/storage'
|
||||
import { fmtPct, priceColorClass } from '@/lib/format'
|
||||
import { PageHeader } from '@/components/PageHeader'
|
||||
import { EmptyState } from '@/components/EmptyState'
|
||||
import { useTheme } from '@/lib/theme'
|
||||
import { useCapabilities, usePreferences } from '@/lib/useSharedQueries'
|
||||
import { SealedBadge } from '@/components/SealedBadge'
|
||||
import type { ExtColumnDisplayConfig } from '@/lib/watchlist-columns'
|
||||
@@ -149,7 +150,8 @@ const STATUS_STYLE: Record<string, { bg: string; bar: string; nameCls: string; c
|
||||
limit_up: {
|
||||
bg: '',
|
||||
bar: 'border-l-2 border-bull/50',
|
||||
nameCls: 'text-rose-50 text-[13px]',
|
||||
// 亮色用深酒红, 暗色保持近白 — 卡片底是淡红渐变, 双主题都要有对比度
|
||||
nameCls: 'text-rose-900 dark:text-rose-50 text-[13px]',
|
||||
codeCls: 'text-muted/80',
|
||||
badge: '',
|
||||
badgeText: '',
|
||||
@@ -162,7 +164,7 @@ const STATUS_STYLE: Record<string, { bg: string; bar: string; nameCls: string; c
|
||||
limit_down: {
|
||||
bg: '',
|
||||
bar: 'border-l-2 border-bear/50',
|
||||
nameCls: 'text-green-50 text-[13px]',
|
||||
nameCls: 'text-emerald-900 dark:text-green-50 text-[13px]',
|
||||
codeCls: 'text-muted/80',
|
||||
badge: '',
|
||||
badgeText: '',
|
||||
@@ -778,12 +780,14 @@ function TagStats({ title, tiers, extFields, fieldKey, color, selectedTag, onSel
|
||||
tiers: LimitLadderTier[]
|
||||
extFields: ExtFieldConfig
|
||||
fieldKey: 'concept' | 'industry'
|
||||
color: { text: [number, number, number]; bg: [number, number, number] }
|
||||
/** text=暗色文字, textLight=亮色文字 (亮底需要更深的色阶), bg=底色 */
|
||||
color: { text: [number, number, number]; textLight: [number, number, number]; bg: [number, number, number] }
|
||||
selectedTag: { fieldKey: 'concept' | 'industry'; tag: string } | null
|
||||
onSelect: (sel: { fieldKey: 'concept' | 'industry'; tag: string } | null) => void
|
||||
direction: Direction
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false)
|
||||
const isDark = useTheme() === 'dark'
|
||||
const mainStatus = direction === 'down' ? 'limit_down' : 'limit_up'
|
||||
|
||||
const stats = useMemo(() => {
|
||||
@@ -805,7 +809,7 @@ function TagStats({ title, tiers, extFields, fieldKey, color, selectedTag, onSel
|
||||
if (stats.length === 0) return null
|
||||
|
||||
const maxCount = stats[0]?.[1] ?? 1
|
||||
const [r, g, b] = color.text
|
||||
const [r, g, b] = isDark ? color.text : color.textLight
|
||||
const [br, bg, bb] = color.bg
|
||||
const needsExpand = stats.length > 10
|
||||
|
||||
@@ -839,10 +843,13 @@ function TagStats({ title, tiers, extFields, fieldKey, color, selectedTag, onSel
|
||||
onClick={() => onSelect(isSelected ? null : { fieldKey, tag: name })}
|
||||
className="text-[11px] px-2 py-1 rounded-sm whitespace-nowrap cursor-pointer hover:brightness-110 transition-all"
|
||||
style={{
|
||||
color: isSelected ? '#fff' : `rgba(${r},${g},${b},${0.6 + intensity * 0.4})`,
|
||||
// 亮色: 深色阶文字 + 更淡的底; 选中态不用白字 (黄底白字在亮色下不可读)
|
||||
color: isSelected
|
||||
? (isDark ? '#fff' : `rgb(${r},${g},${b})`)
|
||||
: `rgba(${r},${g},${b},${isDark ? 0.6 + intensity * 0.4 : 0.75 + intensity * 0.25})`,
|
||||
backgroundColor: isSelected
|
||||
? `rgba(${br},${bg},${bb},0.7)`
|
||||
: `rgba(${br},${bg},${bb},${intensity * 0.2})`,
|
||||
? `rgba(${br},${bg},${bb},${isDark ? 0.7 : 0.28})`
|
||||
: `rgba(${br},${bg},${bb},${intensity * (isDark ? 0.2 : 0.14)})`,
|
||||
outline: isSelected ? `1px solid rgba(${r},${g},${b},0.8)` : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
@@ -883,6 +890,7 @@ function TierGroup({ tier, defaultOpen, extFields, filterKeys, bf, onStockClick,
|
||||
onMonitorChange: () => void
|
||||
hasDepth: boolean
|
||||
}) {
|
||||
const isDarkTheme = useTheme() === 'dark'
|
||||
const [open, setOpen] = useState(defaultOpen)
|
||||
const cfg = { ...DEFAULT_BF, ...bf }
|
||||
const mainStatus = direction === 'down' ? 'limit_down' : 'limit_up'
|
||||
@@ -963,7 +971,7 @@ function TierGroup({ tier, defaultOpen, extFields, filterKeys, bf, onStockClick,
|
||||
<div className="px-3 pt-1 pb-2 space-y-1">
|
||||
{groupConceptStats.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 items-center">
|
||||
<span className="text-[9px] tracking-wider text-yellow-400/70 mr-0.5">概念</span>
|
||||
<span className="text-[9px] tracking-wider text-yellow-700/80 dark:text-yellow-400/70 mr-0.5">概念</span>
|
||||
{groupConceptStats.slice(0, 20).map(([name, count]) => {
|
||||
const isSelected = selectedTag?.fieldKey === 'concept' && selectedTag?.tag === name
|
||||
return (
|
||||
@@ -972,9 +980,13 @@ function TierGroup({ tier, defaultOpen, extFields, filterKeys, bf, onStockClick,
|
||||
onClick={() => onSelectTag(isSelected ? null : { fieldKey: 'concept', tag: name })}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-sm whitespace-nowrap cursor-pointer hover:brightness-110 transition-all"
|
||||
style={{
|
||||
color: isSelected ? '#fff' : 'rgba(250,204,21,0.8)',
|
||||
backgroundColor: isSelected ? 'rgba(234,179,8,0.7)' : 'rgba(234,179,8,0.12)',
|
||||
outline: isSelected ? '1px solid rgba(250,204,21,0.8)' : 'none',
|
||||
color: isSelected
|
||||
? (isDarkTheme ? '#fff' : 'rgb(161,98,7)')
|
||||
: (isDarkTheme ? 'rgba(250,204,21,0.8)' : 'rgba(161,98,7,0.9)'),
|
||||
backgroundColor: isSelected
|
||||
? `rgba(234,179,8,${isDarkTheme ? 0.7 : 0.28})`
|
||||
: `rgba(234,179,8,${isDarkTheme ? 0.12 : 0.1})`,
|
||||
outline: isSelected ? `1px solid rgba(${isDarkTheme ? '250,204,21' : '161,98,7'},0.8)` : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
>
|
||||
@@ -986,7 +998,7 @@ function TierGroup({ tier, defaultOpen, extFields, filterKeys, bf, onStockClick,
|
||||
)}
|
||||
{groupIndustryStats.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 items-center">
|
||||
<span className="text-[9px] tracking-wider text-blue-400/70 mr-0.5">行业</span>
|
||||
<span className="text-[9px] tracking-wider text-blue-700/80 dark:text-blue-400/70 mr-0.5">行业</span>
|
||||
{groupIndustryStats.slice(0, 20).map(([name, count]) => {
|
||||
const isSelected = selectedTag?.fieldKey === 'industry' && selectedTag?.tag === name
|
||||
return (
|
||||
@@ -995,9 +1007,13 @@ function TierGroup({ tier, defaultOpen, extFields, filterKeys, bf, onStockClick,
|
||||
onClick={() => onSelectTag(isSelected ? null : { fieldKey: 'industry', tag: name })}
|
||||
className="text-[10px] px-1.5 py-0.5 rounded-sm whitespace-nowrap cursor-pointer hover:brightness-110 transition-all"
|
||||
style={{
|
||||
color: isSelected ? '#fff' : 'rgba(96,165,250,0.8)',
|
||||
backgroundColor: isSelected ? 'rgba(59,130,246,0.7)' : 'rgba(59,130,246,0.12)',
|
||||
outline: isSelected ? '1px solid rgba(96,165,250,0.8)' : 'none',
|
||||
color: isSelected
|
||||
? (isDarkTheme ? '#fff' : 'rgb(29,78,216)')
|
||||
: (isDarkTheme ? 'rgba(96,165,250,0.8)' : 'rgba(29,78,216,0.9)'),
|
||||
backgroundColor: isSelected
|
||||
? `rgba(59,130,246,${isDarkTheme ? 0.7 : 0.22})`
|
||||
: `rgba(59,130,246,${isDarkTheme ? 0.12 : 0.08})`,
|
||||
outline: isSelected ? `1px solid rgba(${isDarkTheme ? '96,165,250' : '29,78,216'},0.8)` : 'none',
|
||||
outlineOffset: 1,
|
||||
}}
|
||||
>
|
||||
@@ -1627,7 +1643,7 @@ export function LimitUpLadder() {
|
||||
tiers={tiers}
|
||||
extFields={resolveExtFields(extFields, showConcept, showIndustry)}
|
||||
fieldKey="concept"
|
||||
color={{ text: [250, 204, 21], bg: [234, 179, 8] }}
|
||||
color={{ text: [250, 204, 21], textLight: [161, 98, 7], bg: [234, 179, 8] }}
|
||||
selectedTag={selectedTag}
|
||||
onSelect={handleSelectTag}
|
||||
direction={direction}
|
||||
@@ -1640,7 +1656,7 @@ export function LimitUpLadder() {
|
||||
tiers={tiers}
|
||||
extFields={resolveExtFields(extFields, showConcept, showIndustry)}
|
||||
fieldKey="industry"
|
||||
color={{ text: [96, 165, 250], bg: [59, 130, 246] }}
|
||||
color={{ text: [96, 165, 250], textLight: [29, 78, 216], bg: [59, 130, 246] }}
|
||||
selectedTag={selectedTag}
|
||||
onSelect={handleSelectTag}
|
||||
direction={direction}
|
||||
|
||||
@@ -186,7 +186,8 @@ const fmtLots = (v: number | null | undefined) => {
|
||||
}
|
||||
|
||||
const statValueColor = (v: number | null | undefined) => {
|
||||
if (v == null || Number.isNaN(v) || v === 0) return '#f8fafc'
|
||||
// 中性值继承页面前景色 (亮暗主题都可读), 不再写死近白色
|
||||
if (v == null || Number.isNaN(v) || v === 0) return 'inherit'
|
||||
return v > 0 ? '#f87171' : '#34d399'
|
||||
}
|
||||
|
||||
@@ -383,7 +384,7 @@ function Stat({ label, value, color }: { label: ReactNode; value: string; color?
|
||||
<div className="text-[11px] text-secondary">{label}</div>
|
||||
<div
|
||||
className="mt-1 break-words text-sm font-mono font-semibold leading-tight tracking-tight num xl:text-base"
|
||||
style={{ color: color ?? '#f8fafc' }}
|
||||
style={{ color: color ?? 'inherit' }}
|
||||
title={value}
|
||||
>
|
||||
{value}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useECharts } from './useECharts'
|
||||
import type { FactorBacktestResult } from '@/lib/api'
|
||||
import { useChartTheme } from '@/lib/theme'
|
||||
|
||||
const GROUP_COLORS = [
|
||||
'#6366f1', // Q1 indigo
|
||||
@@ -20,6 +21,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export function FactorGroupNavChart({ result }: Props) {
|
||||
const ct = useChartTheme()
|
||||
const option = useMemo(() => {
|
||||
if (!result.group_nav.length) return null
|
||||
|
||||
@@ -58,12 +60,12 @@ export function FactorGroupNavChart({ result }: Props) {
|
||||
grid: { left: 56, right: 16, top: 12, bottom: 28 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(15,23,42,0.95)',
|
||||
borderColor: 'rgba(148,163,184,0.2)',
|
||||
textStyle: { color: '#e2e8f0', fontSize: 12 },
|
||||
backgroundColor: ct.tooltipBg,
|
||||
borderColor: ct.tooltipBorder,
|
||||
textStyle: { color: ct.tooltipText, fontSize: 12 },
|
||||
formatter: (params: any) => {
|
||||
const date = params[0]?.axisValue ?? ''
|
||||
let html = `<div style="font-size:11px;color:#94a3b8;margin-bottom:4px">${date}</div>`
|
||||
let html = `<div style="font-size:11px;color:${ct.text};margin-bottom:4px">${date}</div>`
|
||||
for (const p of params) {
|
||||
if (p.value == null) continue
|
||||
html += `<div style="display:flex;justify-content:space-between;gap:16px">
|
||||
@@ -80,22 +82,22 @@ export function FactorGroupNavChart({ result }: Props) {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: { color: '#64748b', fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisLine: { lineStyle: { color: '#334155' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisLine: { lineStyle: { color: ct.border } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
scale: true,
|
||||
axisLabel: { color: '#64748b', fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: '#1e293b' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
axisLine: { show: false },
|
||||
},
|
||||
series,
|
||||
} as any
|
||||
}, [result.group_nav, result.long_short_nav, result.run_id])
|
||||
}, [result.group_nav, result.long_short_nav, result.run_id, ct])
|
||||
|
||||
const chartRef = useECharts(option, [result.run_id])
|
||||
const chartRef = useECharts(option, [result.run_id, ct])
|
||||
|
||||
// 图例
|
||||
const groupCols = result.group_nav.length > 0
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useECharts } from './useECharts'
|
||||
import type { FactorBacktestResult } from '@/lib/api'
|
||||
import { useChartTheme } from '@/lib/theme'
|
||||
|
||||
interface Props {
|
||||
result: FactorBacktestResult
|
||||
}
|
||||
|
||||
export function FactorICChart({ result }: Props) {
|
||||
const ct = useChartTheme()
|
||||
const option = useMemo(() => {
|
||||
if (!result.ic_series.length) return null
|
||||
|
||||
@@ -26,12 +28,12 @@ export function FactorICChart({ result }: Props) {
|
||||
grid: { left: 50, right: 16, top: 16, bottom: 28 },
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(15,23,42,0.95)',
|
||||
borderColor: 'rgba(148,163,184,0.2)',
|
||||
textStyle: { color: '#e2e8f0', fontSize: 12 },
|
||||
backgroundColor: ct.tooltipBg,
|
||||
borderColor: ct.tooltipBorder,
|
||||
textStyle: { color: ct.tooltipText, fontSize: 12 },
|
||||
formatter: (params: any) => {
|
||||
const date = params[0]?.axisValue ?? ''
|
||||
let html = `<div style="font-size:11px;color:#94a3b8;margin-bottom:4px">${date}</div>`
|
||||
let html = `<div style="font-size:11px;color:${ct.text};margin-bottom:4px">${date}</div>`
|
||||
for (const p of params) {
|
||||
if (p.value == null) continue
|
||||
html += `<div style="display:flex;justify-content:space-between;gap:16px">
|
||||
@@ -45,14 +47,14 @@ export function FactorICChart({ result }: Props) {
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: dates,
|
||||
axisLabel: { color: '#64748b', fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisLine: { lineStyle: { color: '#334155' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisLine: { lineStyle: { color: ct.border } },
|
||||
axisTick: { show: false },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#64748b', fontSize: 10, formatter: (v: number) => `${(v * 100).toFixed(0)}%` },
|
||||
splitLine: { lineStyle: { color: '#1e293b' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10, formatter: (v: number) => `${(v * 100).toFixed(0)}%` },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
axisLine: { show: false },
|
||||
},
|
||||
series: [
|
||||
@@ -80,9 +82,9 @@ export function FactorICChart({ result }: Props) {
|
||||
},
|
||||
],
|
||||
} as any
|
||||
}, [result.ic_series])
|
||||
}, [result.ic_series, ct])
|
||||
|
||||
const chartRef = useECharts(option, [result.run_id])
|
||||
const chartRef = useECharts(option, [result.run_id, ct])
|
||||
|
||||
return <div ref={chartRef} className="h-[200px]" />
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useECharts } from './useECharts'
|
||||
import type { EChartsOption } from 'echarts'
|
||||
import { useChartTheme } from '@/lib/theme'
|
||||
|
||||
interface DistBin {
|
||||
range: string
|
||||
@@ -13,6 +14,7 @@ interface DistBin {
|
||||
* 柱子颜色按收益正负区分(正红负绿),零轴居中。
|
||||
*/
|
||||
export function ReturnDistributionChart({ distribution }: { distribution: DistBin[] }) {
|
||||
const ct = useChartTheme()
|
||||
const option = useMemo<EChartsOption>(() => {
|
||||
const cats = distribution.map(d => d.range)
|
||||
const vals = distribution.map(d => d.count)
|
||||
@@ -20,7 +22,7 @@ export function ReturnDistributionChart({ distribution }: { distribution: DistBi
|
||||
const colors = distribution.map(d => {
|
||||
const lo = parseFloat(d.range)
|
||||
// 中心档(跨 0) 用中性色
|
||||
if (lo < 0 && parseFloat(d.range.split('~')[1]) > 0) return '#a1a1aa'
|
||||
if (lo < 0 && parseFloat(d.range.split('~')[1]) > 0) return ct.text
|
||||
return lo >= 0 ? '#ef4444' : '#22c55e'
|
||||
})
|
||||
|
||||
@@ -39,13 +41,13 @@ export function ReturnDistributionChart({ distribution }: { distribution: DistBi
|
||||
xAxis: {
|
||||
type: 'category',
|
||||
data: cats,
|
||||
axisLabel: { color: '#a1a1aa', fontSize: 10, rotate: 45, interval: 1 },
|
||||
axisLine: { lineStyle: { color: '#3f3f46' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10, rotate: 45, interval: 1 },
|
||||
axisLine: { lineStyle: { color: ct.border } },
|
||||
},
|
||||
yAxis: {
|
||||
type: 'value',
|
||||
axisLabel: { color: '#a1a1aa', fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: '#27272a' } },
|
||||
axisLabel: { color: ct.text, fontSize: 10 },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
},
|
||||
series: [
|
||||
{
|
||||
@@ -55,9 +57,9 @@ export function ReturnDistributionChart({ distribution }: { distribution: DistBi
|
||||
},
|
||||
],
|
||||
}
|
||||
}, [distribution])
|
||||
}, [distribution, ct])
|
||||
|
||||
const chartRef = useECharts(option, [distribution])
|
||||
const chartRef = useECharts(option, [distribution, ct])
|
||||
|
||||
return <div ref={chartRef} className="h-48 w-full" />
|
||||
}
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { useMemo } from 'react'
|
||||
import { useECharts } from './useECharts'
|
||||
import type { StrategyBacktestResult } from '@/lib/api'
|
||||
import { useChartTheme } from '@/lib/theme'
|
||||
|
||||
interface Props {
|
||||
result: StrategyBacktestResult
|
||||
}
|
||||
|
||||
export function StrategyNavChart({ result }: Props) {
|
||||
const ct = useChartTheme()
|
||||
const option = useMemo(() => {
|
||||
if (!result.equity_curve.length) return null
|
||||
|
||||
@@ -28,7 +30,7 @@ export function StrategyNavChart({ result }: Props) {
|
||||
animation: false,
|
||||
axisPointer: {
|
||||
link: [{ xAxisIndex: 'all' }],
|
||||
label: { backgroundColor: '#334155' },
|
||||
label: { backgroundColor: ct.crosshairLabelBg },
|
||||
},
|
||||
grid: [
|
||||
{ left: 64, right: hasBenchmark ? 64 : 16, top: 14, bottom: '40%' },
|
||||
@@ -39,14 +41,14 @@ export function StrategyNavChart({ result }: Props) {
|
||||
type: 'category', data: dates, gridIndex: 0,
|
||||
axisLabel: { show: false }, axisTick: { show: false },
|
||||
axisPointer: { show: true, type: 'line' },
|
||||
axisLine: { lineStyle: { color: '#334155' } },
|
||||
axisLine: { lineStyle: { color: ct.border } },
|
||||
},
|
||||
{
|
||||
type: 'category', data: dates, gridIndex: 1,
|
||||
axisLabel: { color: '#64748b', fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisLabel: { color: ct.text, fontSize: 10, interval: Math.floor(dates.length / 6) },
|
||||
axisTick: { show: false },
|
||||
axisPointer: { show: true, type: 'line' },
|
||||
axisLine: { lineStyle: { color: '#334155' } },
|
||||
axisLine: { lineStyle: { color: ct.border } },
|
||||
},
|
||||
],
|
||||
yAxis: [
|
||||
@@ -54,13 +56,13 @@ export function StrategyNavChart({ result }: Props) {
|
||||
type: 'value', gridIndex: 0,
|
||||
scale: true,
|
||||
name: hasBenchmark ? '上证点位' : '策略资金',
|
||||
nameTextStyle: { color: hasBenchmark ? 'rgba(148,163,184,0.55)' : '#64748b', fontSize: 10, padding: [0, 0, 4, 0] },
|
||||
nameTextStyle: { color: hasBenchmark ? ct.text : ct.text, fontSize: 10, padding: [0, 0, 4, 0] },
|
||||
axisLabel: {
|
||||
color: hasBenchmark ? 'rgba(148,163,184,0.55)' : '#64748b',
|
||||
color: hasBenchmark ? ct.text : ct.text,
|
||||
fontSize: 10,
|
||||
formatter: hasBenchmark ? ((v: number) => v.toFixed(0)) : axisMoneyFmt,
|
||||
},
|
||||
splitLine: { lineStyle: { color: '#1e293b' } },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
axisLine: { show: false },
|
||||
},
|
||||
{
|
||||
@@ -68,10 +70,10 @@ export function StrategyNavChart({ result }: Props) {
|
||||
position: 'right',
|
||||
scale: true,
|
||||
name: hasBenchmark ? '策略资金' : '',
|
||||
nameTextStyle: { color: '#64748b', fontSize: 10, padding: [0, 0, 4, 0] },
|
||||
nameTextStyle: { color: ct.text, fontSize: 10, padding: [0, 0, 4, 0] },
|
||||
axisLabel: {
|
||||
show: hasBenchmark,
|
||||
color: '#64748b',
|
||||
color: ct.text,
|
||||
fontSize: 10,
|
||||
formatter: axisMoneyFmt,
|
||||
},
|
||||
@@ -83,10 +85,10 @@ export function StrategyNavChart({ result }: Props) {
|
||||
position: 'right',
|
||||
max: 0,
|
||||
axisLabel: {
|
||||
color: '#64748b', fontSize: 10,
|
||||
color: ct.text, fontSize: 10,
|
||||
formatter: (v: number) => `${v.toFixed(1)}%`,
|
||||
},
|
||||
splitLine: { lineStyle: { color: '#1e293b' } },
|
||||
splitLine: { lineStyle: { color: ct.grid } },
|
||||
axisLine: { show: false },
|
||||
},
|
||||
],
|
||||
@@ -105,22 +107,22 @@ export function StrategyNavChart({ result }: Props) {
|
||||
filterMode: 'filter',
|
||||
height: 16,
|
||||
bottom: 10,
|
||||
borderColor: 'rgba(148,163,184,0.18)',
|
||||
backgroundColor: 'rgba(15,23,42,0.55)',
|
||||
borderColor: ct.border,
|
||||
backgroundColor: ct.zoomFill,
|
||||
fillerColor: 'rgba(59,130,246,0.18)',
|
||||
handleStyle: { color: '#64748b', borderColor: '#94a3b8' },
|
||||
textStyle: { color: '#64748b', fontSize: 10 },
|
||||
handleStyle: { color: ct.text, borderColor: '#94a3b8' },
|
||||
textStyle: { color: ct.text, fontSize: 10 },
|
||||
brushSelect: false,
|
||||
},
|
||||
],
|
||||
tooltip: {
|
||||
trigger: 'axis',
|
||||
backgroundColor: 'rgba(15,23,42,0.95)',
|
||||
borderColor: 'rgba(148,163,184,0.2)',
|
||||
textStyle: { color: '#e2e8f0', fontSize: 12 },
|
||||
backgroundColor: ct.tooltipBg,
|
||||
borderColor: ct.tooltipBorder,
|
||||
textStyle: { color: ct.tooltipText, fontSize: 12 },
|
||||
formatter: (params: any) => {
|
||||
const date = params[0]?.axisValue ?? ''
|
||||
let html = `<div style="font-size:11px;color:#94a3b8;margin-bottom:4px">${date}</div>`
|
||||
let html = `<div style="font-size:11px;color:${ct.text};margin-bottom:4px">${date}</div>`
|
||||
for (const p of params) {
|
||||
if (p.value == null) continue
|
||||
const isDrawdown = p.seriesName === '回撤'
|
||||
@@ -180,9 +182,9 @@ export function StrategyNavChart({ result }: Props) {
|
||||
},
|
||||
],
|
||||
} as any
|
||||
}, [result.equity_curve, result.drawdown_curve, result.benchmark_curve, result.run_id])
|
||||
}, [result.equity_curve, result.drawdown_curve, result.benchmark_curve, result.run_id, ct])
|
||||
|
||||
const chartRef = useECharts(option, [result.run_id])
|
||||
const chartRef = useECharts(option, [result.run_id, ct])
|
||||
|
||||
return (
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user