Files
tick-stock-panel/frontend/src/lib/theme.ts
T
Gundy 8abf7e338a feat(theme): 亮色主题基础设施 + 全部图表主题化
- lib/theme.ts: localStorage 持久化 + useTheme hook (本页/跨标签页
  同步) + ChartTheme 图表调色板 (ECharts/lightweight-charts 画布
  不吃 CSS 变量, 统一从这里取色)
- index.html: 去掉写死的 class=dark, 换预渲染内联脚本 (默认暗色,
  切过亮色则保持), 避免首屏闪烁
- Layout: 侧边栏底部亮/暗切换按钮 (设置入口上方)
- 图表主题化 (切换即时生效, 无需刷新):
  · EChartsIntraday / EChartsCandlestick / CandlestickChart
    (lightweight-charts applyOptions) / AnalysisKChart
  · 回测 4 图 (StrategyNav/FactorIC/FactorGroupNav/ReturnDistribution)
  · Sentiment 页双图
  序列颜色 (红涨绿跌/MA线) 双主题通用保持不变; 轴/网格/十字线/
  tooltip/信息栏背景按主题切换

CSS 变量亮色板 index.css 里本就有 (:root), 首次真正启用
2026-07-03 13:51:17 +08:00

122 lines
3.7 KiB
TypeScript

// 主题管理 — 暗色(默认) / 亮色切换
//
// 机制:
// - 状态存 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())
}