feat(hub): add dark/light theme toggle with moon and sun icons

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-04 12:03:48 +08:00
parent b394e495ca
commit d1914df46f
6 changed files with 327 additions and 19 deletions
+166 -5
View File
@@ -1,8 +1,14 @@
:root { :root,
html[data-theme="dark"] {
--bg: #050810; --bg: #050810;
--bg-elevated: #0a1018; --bg-elevated: #0a1018;
--panel: rgba(12, 20, 32, 0.82); --panel: rgba(12, 20, 32, 0.82);
--panel-hover: rgba(18, 28, 44, 0.9); --panel-hover: rgba(18, 28, 44, 0.9);
--panel-solid: #141a2a;
--panel-solid-border: #2a3150;
--nav-bg: rgba(0, 0, 0, 0.35);
--overlay: rgba(0, 0, 0, 0.45);
--chart-surface: #0a1018;
--text: #e8f4ff; --text: #e8f4ff;
--muted: #6b8aa8; --muted: #6b8aa8;
--border: rgba(0, 212, 255, 0.22); --border: rgba(0, 212, 255, 0.22);
@@ -19,6 +25,31 @@
--display: "Orbitron", var(--font); --display: "Orbitron", var(--font);
--mono: var(--font); --mono: var(--font);
--layout-max: 1520px; --layout-max: 1520px;
color-scheme: dark;
}
html[data-theme="light"] {
--bg: #e8eef5;
--bg-elevated: #ffffff;
--panel: rgba(255, 255, 255, 0.94);
--panel-hover: rgba(248, 252, 255, 0.98);
--panel-solid: #ffffff;
--panel-solid-border: #c8d4e4;
--nav-bg: rgba(255, 255, 255, 0.88);
--overlay: rgba(15, 35, 60, 0.35);
--chart-surface: #f4f7fb;
--text: #152030;
--muted: #5a6f85;
--border: rgba(0, 120, 170, 0.28);
--border-soft: rgba(0, 90, 140, 0.14);
--green: #0a8f5c;
--red: #c93552;
--accent: #007aa8;
--accent-2: #5b4fc7;
--accent-dim: rgba(0, 122, 168, 0.1);
--glow: 0 0 18px rgba(0, 122, 168, 0.12);
--shadow: 0 8px 28px rgba(20, 50, 90, 0.1);
color-scheme: light;
} }
* { * {
@@ -171,10 +202,51 @@ a:hover {
} }
} }
.theme-toggle {
display: inline-flex;
align-items: center;
gap: 2px;
padding: 3px;
border-radius: var(--radius);
border: 1px solid var(--border-soft);
background: var(--nav-bg);
backdrop-filter: blur(8px);
}
.theme-toggle-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 34px;
height: 32px;
padding: 0;
border: none;
border-radius: 7px;
background: transparent;
color: var(--muted);
cursor: pointer;
transition: background 0.15s, color 0.15s, box-shadow 0.15s;
}
.theme-toggle-btn:hover {
color: var(--text);
background: var(--panel-hover);
}
.theme-toggle-btn.is-active {
color: var(--accent);
background: var(--accent-dim);
box-shadow: inset 0 0 0 1px var(--border);
}
.theme-toggle-btn .theme-icon {
display: block;
}
.top-nav { .top-nav {
display: flex; display: flex;
gap: 4px; gap: 4px;
background: rgba(0, 0, 0, 0.35); background: var(--nav-bg);
padding: 4px; padding: 4px;
border-radius: var(--radius); border-radius: var(--radius);
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
@@ -1335,8 +1407,8 @@ body.market-chart-fs-open {
} }
.hub-trend-plan-card.plan-position-card { .hub-trend-plan-card.plan-position-card {
background: #141a2a; background: var(--panel-solid);
border: 1px solid #2a3150; border: 1px solid var(--panel-solid-border);
border-radius: 12px; border-radius: 12px;
padding: 12px 14px; padding: 12px 14px;
} }
@@ -2108,12 +2180,23 @@ button.btn-sm {
/* —— 登录页 —— */ /* —— 登录页 —— */
body.login-page { body.login-page {
display: flex; display: flex;
flex-direction: column;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
min-height: 100vh; min-height: 100vh;
padding: 24px; padding: 24px;
} }
.login-theme-bar {
position: relative;
z-index: 2;
width: 100%;
max-width: 400px;
display: flex;
justify-content: flex-end;
margin-bottom: 10px;
}
.login-panel { .login-panel {
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -2512,7 +2595,7 @@ body.login-page {
min-height: 380px; min-height: 380px;
border: 1px solid var(--border-soft); border: 1px solid var(--border-soft);
border-radius: var(--radius); border-radius: var(--radius);
background: #0a1018; background: var(--chart-surface);
overflow: hidden; overflow: hidden;
} }
@@ -2951,3 +3034,81 @@ body.login-page {
border-color: rgba(0, 255, 157, 0.45); border-color: rgba(0, 255, 157, 0.45);
background: rgba(0, 255, 157, 0.1); background: rgba(0, 255, 157, 0.1);
} }
/* —— 亮色主题:背景与局部硬编码色 —— */
html[data-theme="light"] .app-bg,
html[data-theme="light"] .login-bg {
background:
linear-gradient(rgba(0, 100, 150, 0.06) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 100, 150, 0.06) 1px, transparent 1px),
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(0, 140, 200, 0.1), transparent),
radial-gradient(ellipse 60% 40% at 100% 100%, rgba(90, 80, 200, 0.06), transparent);
background-size: 48px 48px, 48px 48px, auto, auto;
}
html[data-theme="light"] .app-bg::after,
html[data-theme="light"] .login-bg::after {
opacity: 0.15;
}
html[data-theme="light"] .market-ind-options {
background: rgba(255, 255, 255, 0.98);
box-shadow: var(--shadow);
}
html[data-theme="light"] .market-price-auto {
background: rgba(255, 255, 255, 0.95);
}
html[data-theme="light"] .market-price-auto.is-on {
color: var(--green);
background: rgba(10, 143, 92, 0.12);
}
html[data-theme="light"] .hub-trend-plan-card .plan-card-meta,
html[data-theme="light"] .hub-trend-plan-card .plan-cell .lbl,
html[data-theme="light"] .hub-trend-plan-card .plan-dca-title {
color: var(--muted);
}
html[data-theme="light"] .hub-trend-plan-card .plan-card-meta .accent {
color: var(--accent);
}
html[data-theme="light"] .hub-trend-plan-card .plan-cell .val {
color: var(--text);
}
html[data-theme="light"] .hub-trend-plan-card .plan-dca-table th {
color: var(--muted);
}
html[data-theme="light"] .hub-trend-plan-card .plan-dca-table th,
html[data-theme="light"] .hub-trend-plan-card .plan-dca-table td {
border-bottom-color: var(--border-soft);
}
html[data-theme="light"] .hub-trend-plan-card .hub-plan-be-input {
background: var(--bg-elevated);
border-color: var(--border-soft);
color: var(--text);
}
html[data-theme="light"] .exchange-fullscreen-panel,
html[data-theme="light"] .modal-panel {
box-shadow: var(--shadow);
}
html[data-theme="light"] input,
html[data-theme="light"] select,
html[data-theme="light"] textarea {
background: var(--bg-elevated);
color: var(--text);
border-color: var(--border-soft);
}
html[data-theme="light"] .hub-tile,
html[data-theme="light"] .hub-pos-card,
html[data-theme="light"] .settings-row {
box-shadow: 0 2px 12px rgba(20, 50, 90, 0.06);
}
+3
View File
@@ -2645,6 +2645,9 @@
initInstanceFrame(); initInstanceFrame();
initFullscreen(); initFullscreen();
initMobileLayout(); initMobileLayout();
if (globalThis.HubTheme && typeof HubTheme.initToggleUI === "function") {
HubTheme.initToggleUI();
}
function initShellNav() { function initShellNav() {
document.querySelectorAll(".top-nav a[href^='/']").forEach((a) => { document.querySelectorAll(".top-nav a[href^='/']").forEach((a) => {
+58 -8
View File
@@ -1286,13 +1286,58 @@
return candleByTime[t] || null; return candleByTime[t] || null;
} }
function chartThemePalette() {
const light = document.documentElement.getAttribute("data-theme") === "light";
return light
? {
bg: "#f4f7fb",
text: "#5a6f85",
border: "#c8d4e4",
up: "#0a8f5c",
down: "#c93552",
volUp: "rgba(10, 143, 92, 0.45)",
volDown: "rgba(201, 53, 82, 0.45)",
}
: {
bg: "#0a1018",
text: "#b8d4e8",
border: "#2a4058",
up: "#00ff9d",
down: "#ff4d6d",
volUp: "rgba(0, 255, 157, 0.5)",
volDown: "rgba(255, 77, 109, 0.5)",
};
}
function applyChartTheme() {
if (!chart) return;
const p = chartThemePalette();
chart.applyOptions({
layout: { background: { color: p.bg }, textColor: p.text },
rightPriceScale: { borderColor: p.border },
timeScale: { borderColor: p.border },
});
if (candleSeries) {
candleSeries.applyOptions({
upColor: p.up,
downColor: p.down,
wickUpColor: p.up,
wickDownColor: p.down,
});
}
if (volumeSeries && lastCandles.length) {
volumeSeries.setData(buildVolumeData(lastCandles));
}
}
function buildVolumeData(candles) { function buildVolumeData(candles) {
const p = chartThemePalette();
return (candles || []).map(function (c) { return (candles || []).map(function (c) {
const up = Number(c.close) >= Number(c.open); const up = Number(c.close) >= Number(c.open);
return { return {
time: c.time, time: c.time,
value: Number(c.volume) || 0, value: Number(c.volume) || 0,
color: up ? "rgba(0, 255, 157, 0.5)" : "rgba(255, 77, 109, 0.5)", color: up ? p.volUp : p.volDown,
}; };
}); });
} }
@@ -1306,15 +1351,16 @@
} }
return false; return false;
} }
const tp = chartThemePalette();
chart = LightweightCharts.createChart(chartHost, { chart = LightweightCharts.createChart(chartHost, {
layout: { background: { color: "#0a1018" }, textColor: "#b8d4e8" }, layout: { background: { color: tp.bg }, textColor: tp.text },
grid: { grid: {
vertLines: { visible: false }, vertLines: { visible: false },
horzLines: { visible: false }, horzLines: { visible: false },
}, },
rightPriceScale: { borderColor: "#2a4058", autoScale: true }, rightPriceScale: { borderColor: tp.border, autoScale: true },
timeScale: { timeScale: {
borderColor: "#2a4058", borderColor: tp.border,
timeVisible: true, timeVisible: true,
secondsVisible: false, secondsVisible: false,
rightOffset: RIGHT_OFFSET_BARS, rightOffset: RIGHT_OFFSET_BARS,
@@ -1327,11 +1373,11 @@
}); });
const candleOpts = { const candleOpts = {
upColor: "#00ff9d", upColor: tp.up,
downColor: "#ff4d6d", downColor: tp.down,
borderVisible: false, borderVisible: false,
wickUpColor: "#00ff9d", wickUpColor: tp.up,
wickDownColor: "#ff4d6d", wickDownColor: tp.down,
lastValueVisible: false, lastValueVisible: false,
priceLineVisible: false, priceLineVisible: false,
priceFormat: SAFE_PRICE_FORMAT, priceFormat: SAFE_PRICE_FORMAT,
@@ -1757,6 +1803,10 @@
stopPriceTagTimer: stopPriceTagTimer, stopPriceTagTimer: stopPriceTagTimer,
}; };
document.addEventListener("hub-theme-change", function () {
applyChartTheme();
});
if ( if (
document.getElementById("page-market") && document.getElementById("page-market") &&
!document.getElementById("page-market").classList.contains("hidden") !document.getElementById("page-market").classList.contains("hidden")
+18 -4
View File
@@ -1,7 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="/assets/theme.js?v=20260604-hub-theme"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b0e18" /> <meta name="theme-color" content="#0b0e18" />
<meta name="apple-mobile-web-app-title" content="中控" /> <meta name="apple-mobile-web-app-title" content="中控" />
@@ -14,7 +15,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin /> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" /> <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript> <noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
<link rel="stylesheet" href="/assets/app.css?v=20260604-hub-trend-2col" /> <link rel="stylesheet" href="/assets/app.css?v=20260604-hub-theme" />
</head> </head>
<body> <body>
<div class="app-bg" aria-hidden="true"></div> <div class="app-bg" aria-hidden="true"></div>
@@ -28,6 +29,19 @@
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
<span id="sys-status" class="sys-pill" title="系统状态">SYNC</span> <span id="sys-status" class="sys-pill" title="系统状态">SYNC</span>
<nav class="top-nav"> <nav class="top-nav">
<a href="/monitor" id="nav-monitor">监控区</a> <a href="/monitor" id="nav-monitor">监控区</a>
@@ -234,7 +248,7 @@
<div id="toast"></div> <div id="toast"></div>
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script> <script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
<script src="/assets/chart.js?v=20260604-hub-trend-plan"></script> <script src="/assets/chart.js?v=20260604-hub-theme"></script>
<script src="/assets/app.js?v=20260604-hub-trend-2col"></script> <script src="/assets/app.js?v=20260604-hub-theme"></script>
</body> </body>
</html> </html>
+21 -2
View File
@@ -1,7 +1,8 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="zh-CN"> <html lang="zh-CN" data-theme="dark">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<script src="/assets/theme.js?v=20260604-hub-theme"></script>
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover" />
<meta name="theme-color" content="#0b0e18" /> <meta name="theme-color" content="#0b0e18" />
<meta name="apple-mobile-web-app-title" content="中控" /> <meta name="apple-mobile-web-app-title" content="中控" />
@@ -10,10 +11,25 @@
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png" /> <link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png" />
<link rel="manifest" href="/assets/icons/manifest.webmanifest" /> <link rel="manifest" href="/assets/icons/manifest.webmanifest" />
<title>登录 · 复盘系统中控</title> <title>登录 · 复盘系统中控</title>
<link rel="stylesheet" href="/assets/app.css?v=20260530-hub-embed-login" /> <link rel="stylesheet" href="/assets/app.css?v=20260604-hub-theme" />
</head> </head>
<body class="login-page"> <body class="login-page">
<div class="login-bg" aria-hidden="true"></div> <div class="login-bg" aria-hidden="true"></div>
<div class="login-theme-bar">
<div class="theme-toggle" role="group" aria-label="界面主题">
<button type="button" class="theme-toggle-btn is-active" data-theme-value="dark" aria-pressed="true" title="暗色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true">
<path fill="currentColor" d="M12.1 3a9 9 0 1 0 8.9 11 6.5 6.5 0 1 1-8.9-11z"/>
</svg>
</button>
<button type="button" class="theme-toggle-btn" data-theme-value="light" aria-pressed="false" title="亮色主题">
<svg class="theme-icon" viewBox="0 0 24 24" width="18" height="18" aria-hidden="true" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4"/>
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/>
</svg>
</button>
</div>
</div>
<div class="login-panel"> <div class="login-panel">
<div class="login-brand"> <div class="login-brand">
<span class="brand-mark"></span> <span class="brand-mark"></span>
@@ -143,6 +159,9 @@
submitBtn.textContent = oldLabel; submitBtn.textContent = oldLabel;
}); });
})(); })();
if (window.HubTheme && typeof HubTheme.initToggleUI === "function") {
HubTheme.initToggleUI();
}
</script> </script>
</body> </body>
</html> </html>
+61
View File
@@ -0,0 +1,61 @@
/** 中控主题:暗色(默认)/ 亮色,localStorage hub-theme */
(function (global) {
const KEY = "hub-theme";
const META = { dark: "#0b0e18", light: "#e8eef5" };
function normalize(theme) {
return theme === "light" ? "light" : "dark";
}
function get() {
try {
return normalize(localStorage.getItem(KEY));
} catch (_) {
return "dark";
}
}
function apply(theme) {
const t = normalize(theme);
const root = document.documentElement;
root.setAttribute("data-theme", t);
try {
localStorage.setItem(KEY, t);
} catch (_) {}
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
document.dispatchEvent(new CustomEvent("hub-theme-change", { detail: { theme: t } }));
return t;
}
function toggle() {
return apply(get() === "dark" ? "light" : "dark");
}
function syncToggleUI(root) {
const scope = root || document;
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
const on = btn.getAttribute("data-theme-value") === get();
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
});
}
function initToggleUI(root) {
const scope = root || document;
syncToggleUI(scope);
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
if (btn.dataset.themeBound === "1") return;
btn.dataset.themeBound = "1";
btn.addEventListener("click", () => {
apply(btn.getAttribute("data-theme-value"));
syncToggleUI(scope);
});
});
document.addEventListener("hub-theme-change", () => syncToggleUI(scope));
}
apply(get());
global.HubTheme = { KEY, get, apply, toggle, syncToggleUI, initToggleUI };
})(typeof window !== "undefined" ? window : globalThis);