feat(hub): add dark/light theme toggle with moon and sun icons
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,8 +1,14 @@
|
||||
:root {
|
||||
:root,
|
||||
html[data-theme="dark"] {
|
||||
--bg: #050810;
|
||||
--bg-elevated: #0a1018;
|
||||
--panel: rgba(12, 20, 32, 0.82);
|
||||
--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;
|
||||
--muted: #6b8aa8;
|
||||
--border: rgba(0, 212, 255, 0.22);
|
||||
@@ -19,6 +25,31 @@
|
||||
--display: "Orbitron", var(--font);
|
||||
--mono: var(--font);
|
||||
--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 {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
background: var(--nav-bg);
|
||||
padding: 4px;
|
||||
border-radius: var(--radius);
|
||||
border: 1px solid var(--border-soft);
|
||||
@@ -1335,8 +1407,8 @@ body.market-chart-fs-open {
|
||||
}
|
||||
|
||||
.hub-trend-plan-card.plan-position-card {
|
||||
background: #141a2a;
|
||||
border: 1px solid #2a3150;
|
||||
background: var(--panel-solid);
|
||||
border: 1px solid var(--panel-solid-border);
|
||||
border-radius: 12px;
|
||||
padding: 12px 14px;
|
||||
}
|
||||
@@ -2108,12 +2180,23 @@ button.btn-sm {
|
||||
/* —— 登录页 —— */
|
||||
body.login-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
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 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -2512,7 +2595,7 @@ body.login-page {
|
||||
min-height: 380px;
|
||||
border: 1px solid var(--border-soft);
|
||||
border-radius: var(--radius);
|
||||
background: #0a1018;
|
||||
background: var(--chart-surface);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@@ -2951,3 +3034,81 @@ body.login-page {
|
||||
border-color: rgba(0, 255, 157, 0.45);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2645,6 +2645,9 @@
|
||||
initInstanceFrame();
|
||||
initFullscreen();
|
||||
initMobileLayout();
|
||||
if (globalThis.HubTheme && typeof HubTheme.initToggleUI === "function") {
|
||||
HubTheme.initToggleUI();
|
||||
}
|
||||
|
||||
function initShellNav() {
|
||||
document.querySelectorAll(".top-nav a[href^='/']").forEach((a) => {
|
||||
|
||||
@@ -1286,13 +1286,58 @@
|
||||
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) {
|
||||
const p = chartThemePalette();
|
||||
return (candles || []).map(function (c) {
|
||||
const up = Number(c.close) >= Number(c.open);
|
||||
return {
|
||||
time: c.time,
|
||||
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;
|
||||
}
|
||||
const tp = chartThemePalette();
|
||||
chart = LightweightCharts.createChart(chartHost, {
|
||||
layout: { background: { color: "#0a1018" }, textColor: "#b8d4e8" },
|
||||
layout: { background: { color: tp.bg }, textColor: tp.text },
|
||||
grid: {
|
||||
vertLines: { visible: false },
|
||||
horzLines: { visible: false },
|
||||
},
|
||||
rightPriceScale: { borderColor: "#2a4058", autoScale: true },
|
||||
rightPriceScale: { borderColor: tp.border, autoScale: true },
|
||||
timeScale: {
|
||||
borderColor: "#2a4058",
|
||||
borderColor: tp.border,
|
||||
timeVisible: true,
|
||||
secondsVisible: false,
|
||||
rightOffset: RIGHT_OFFSET_BARS,
|
||||
@@ -1327,11 +1373,11 @@
|
||||
});
|
||||
|
||||
const candleOpts = {
|
||||
upColor: "#00ff9d",
|
||||
downColor: "#ff4d6d",
|
||||
upColor: tp.up,
|
||||
downColor: tp.down,
|
||||
borderVisible: false,
|
||||
wickUpColor: "#00ff9d",
|
||||
wickDownColor: "#ff4d6d",
|
||||
wickUpColor: tp.up,
|
||||
wickDownColor: tp.down,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
priceFormat: SAFE_PRICE_FORMAT,
|
||||
@@ -1757,6 +1803,10 @@
|
||||
stopPriceTagTimer: stopPriceTagTimer,
|
||||
};
|
||||
|
||||
document.addEventListener("hub-theme-change", function () {
|
||||
applyChartTheme();
|
||||
});
|
||||
|
||||
if (
|
||||
document.getElementById("page-market") &&
|
||||
!document.getElementById("page-market").classList.contains("hidden")
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<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="theme-color" content="#0b0e18" />
|
||||
<meta name="apple-mobile-web-app-title" content="中控" />
|
||||
@@ -14,7 +15,7 @@
|
||||
<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'" />
|
||||
<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>
|
||||
<body>
|
||||
<div class="app-bg" aria-hidden="true"></div>
|
||||
@@ -28,6 +29,19 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<nav class="top-nav">
|
||||
<a href="/monitor" id="nav-monitor">监控区</a>
|
||||
@@ -234,7 +248,7 @@
|
||||
|
||||
<div id="toast"></div>
|
||||
<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/app.js?v=20260604-hub-trend-2col"></script>
|
||||
<script src="/assets/chart.js?v=20260604-hub-theme"></script>
|
||||
<script src="/assets/app.js?v=20260604-hub-theme"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<html lang="zh-CN" data-theme="dark">
|
||||
<head>
|
||||
<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="theme-color" content="#0b0e18" />
|
||||
<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="manifest" href="/assets/icons/manifest.webmanifest" />
|
||||
<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>
|
||||
<body class="login-page">
|
||||
<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-brand">
|
||||
<span class="brand-mark"></span>
|
||||
@@ -143,6 +159,9 @@
|
||||
submitBtn.textContent = oldLabel;
|
||||
});
|
||||
})();
|
||||
if (window.HubTheme && typeof HubTheme.initToggleUI === "function") {
|
||||
HubTheme.initToggleUI();
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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);
|
||||
Reference in New Issue
Block a user