diff --git a/frontend/index.html b/frontend/index.html index 7f38b92..94fcdb5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -1,7 +1,20 @@ - + + diff --git a/frontend/src/components/CandlestickChart.tsx b/frontend/src/components/CandlestickChart.tsx index 8652938..9b766fb 100644 --- a/frontend/src/components/CandlestickChart.tsx +++ b/frontend/src/components/CandlestickChart.tsx @@ -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(null) const candleRef = useRef | null>(null) const volRef = useRef | 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, }, diff --git a/frontend/src/components/EChartsCandlestick.tsx b/frontend/src/components/EChartsCandlestick.tsx index f7bd977..20b8daa 100644 --- a/frontend/src/components/EChartsCandlestick.tsx +++ b/frontend/src/components/EChartsCandlestick.tsx @@ -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(data.length - 1) @@ -819,14 +823,14 @@ export function EChartsCandlestick({ const turnoverRate = floatShares && d.volume ? (d.volume * 100 / floatShares * 100) : null let html = `
` - html += `${d.date}` - html += `` + html += `${d.date}` + html += `` html += `${d.open.toFixed(2)}` - html += `` + html += `` html += `${d.high.toFixed(2)}` - html += `` + html += `` html += `${d.low.toFixed(2)}` - html += `` + html += `` html += `${d.close.toFixed(2)}` // 涨跌幅 (收盘后, 换手前; 和收间隔一些距离) if (prev) { @@ -834,8 +838,8 @@ export function EChartsCandlestick({ html += `${isUp ? '+' : ''}${chgPct.toFixed(2)}%` } if (turnoverRate != null) { - html += `换手` - html += `${turnoverRate.toFixed(2)}%` + html += `换手` + html += `${turnoverRate.toFixed(2)}%` } html += `
` @@ -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 = `
` - html += `${d.date}` - html += `` + html += `${d.date}` + html += `` html += `${d.open.toFixed(2)}` - html += `` + html += `` html += `${d.high.toFixed(2)}` - html += `` + html += `` html += `${d.low.toFixed(2)}` - html += `` + html += `` const prevClose0 = data[idx-1]?.close ?? d.close const clr0 = d.close >= prevClose0 ? THEME.bull : THEME.bear html += `${d.close.toFixed(2)}` @@ -1065,8 +1069,8 @@ export function EChartsCandlestick({ html += `${chgPct0 >= 0 ? '+' : ''}${chgPct0.toFixed(2)}%` } if (turnoverRate != null) { - html += `换手` - html += `${turnoverRate.toFixed(2)}%` + html += `换手` + html += `${turnoverRate.toFixed(2)}%` } html += `
` if (showMA) { @@ -1088,7 +1092,7 @@ export function EChartsCandlestick({
{/* 主图信息栏 — 内容由 JS 直接操作 innerHTML */} {showInfoBar && ( -
)} diff --git a/frontend/src/components/EChartsIntraday.tsx b/frontend/src/components/EChartsIntraday.tsx index 65bb6ca..812ef13 100644 --- a/frontend/src/components/EChartsIntraday.tsx +++ b/frontend/src/components/EChartsIntraday.tsx @@ -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('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
} -
+
{/* 第一行: 日期 + OHLC */}
{!d && } diff --git a/frontend/src/components/ExtDimensionAnalysis.tsx b/frontend/src/components/ExtDimensionAnalysis.tsx index 30b04cf..7d5de5e 100644 --- a/frontend/src/components/ExtDimensionAnalysis.tsx +++ b/frontend/src/components/ExtDimensionAnalysis.tsx @@ -296,8 +296,8 @@ export function ExtDimensionAnalysis({ 页面按分析菜单配置读取扩展数据源,使用分组字段生成榜单,并严格按照 detail_columns 渲染明细列。

-
- +
+
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index e46dbbe..1473efb 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -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 ( + + ) +} + 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() {
+ diff --git a/frontend/src/components/stock-analysis/AnalysisKChart.tsx b/frontend/src/components/stock-analysis/AnalysisKChart.tsx index 4cde2bd..0759757 100644 --- a/frontend/src/components/stock-analysis/AnalysisKChart.tsx +++ b/frontend/src/components/stock-analysis/AnalysisKChart.tsx @@ -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(null) const chartInstRef = useRef(null) + // 主题: buildOption 内部用 CT() 动态取色, 这里只负责切换时触发重建 + const theme = useTheme() const [activeTypes, setActiveTypes] = useState>(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 (
diff --git a/frontend/src/lib/theme.ts b/frontend/src/lib/theme.ts new file mode 100644 index 0000000..6252ed5 --- /dev/null +++ b/frontend/src/lib/theme.ts @@ -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(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()) +} diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index c5e8ee4..9dd9235 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -325,24 +325,25 @@ function EmotionRadar({ radar, score }: { radar: OverviewMarket['radar']; score: + {/* 中心/网格用 CSS 变量取色, 亮暗主题自动切换 (SVG 属性支持 hsl(var(--x))) */} - - - + + + {gridPolygons.map(g => ( ))} - {points.map(p => )} + {points.map(p => )} - {points.map(p => )} + {points.map(p => )} {score} {points.map(p => ( diff --git a/frontend/src/pages/LimitUpLadder.tsx b/frontend/src/pages/LimitUpLadder.tsx index 9ff5cfa..f924c0a 100644 --- a/frontend/src/pages/LimitUpLadder.tsx +++ b/frontend/src/pages/LimitUpLadder.tsx @@ -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 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,
{groupConceptStats.length > 0 && (
- 概念 + 概念 {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 && (
- 行业 + 行业 {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} diff --git a/frontend/src/pages/backtest/StrategyBacktest.tsx b/frontend/src/pages/backtest/StrategyBacktest.tsx index bc951c5..30f89b1 100644 --- a/frontend/src/pages/backtest/StrategyBacktest.tsx +++ b/frontend/src/pages/backtest/StrategyBacktest.tsx @@ -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?
{label}
{value} diff --git a/frontend/src/pages/backtest/charts/FactorGroupNavChart.tsx b/frontend/src/pages/backtest/charts/FactorGroupNavChart.tsx index 98e618b..5bae619 100644 --- a/frontend/src/pages/backtest/charts/FactorGroupNavChart.tsx +++ b/frontend/src/pages/backtest/charts/FactorGroupNavChart.tsx @@ -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 = `
${date}
` + let html = `
${date}
` for (const p of params) { if (p.value == null) continue html += `
@@ -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 diff --git a/frontend/src/pages/backtest/charts/FactorICChart.tsx b/frontend/src/pages/backtest/charts/FactorICChart.tsx index c66b161..05691f9 100644 --- a/frontend/src/pages/backtest/charts/FactorICChart.tsx +++ b/frontend/src/pages/backtest/charts/FactorICChart.tsx @@ -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 = `
${date}
` + let html = `
${date}
` for (const p of params) { if (p.value == null) continue html += `
@@ -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
} diff --git a/frontend/src/pages/backtest/charts/ReturnDistributionChart.tsx b/frontend/src/pages/backtest/charts/ReturnDistributionChart.tsx index 382fcb3..25132ea 100644 --- a/frontend/src/pages/backtest/charts/ReturnDistributionChart.tsx +++ b/frontend/src/pages/backtest/charts/ReturnDistributionChart.tsx @@ -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(() => { 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
} diff --git a/frontend/src/pages/backtest/charts/StrategyNavChart.tsx b/frontend/src/pages/backtest/charts/StrategyNavChart.tsx index a6c808e..30a998b 100644 --- a/frontend/src/pages/backtest/charts/StrategyNavChart.tsx +++ b/frontend/src/pages/backtest/charts/StrategyNavChart.tsx @@ -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 = `
${date}
` + let html = `
${date}
` 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 (