feat: circular fund account cards with fullscreen detail view

Show per-exchange balances as clickable circles with mini curves; open a fullscreen panel for equity history and drawdown.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-10 17:04:21 +08:00
parent ba629ea0ee
commit ec8607932b
3 changed files with 403 additions and 89 deletions
+189 -42
View File
@@ -4488,68 +4488,121 @@ body.hub-page-ai #page-ai {
overflow: hidden; overflow: hidden;
} }
.funds-section-title { .funds-section-title {
margin: 0 0 10px; margin: 0 0 4px;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
} }
.funds-accounts { .funds-section-hint {
display: grid; margin: 0 0 14px;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); font-size: 0.75rem;
gap: 12px; color: var(--muted);
} }
.funds-ac-card { .funds-accounts {
background: var(--panel); display: flex;
border: 1px solid var(--border-soft); flex-wrap: wrap;
border-radius: var(--radius); gap: 18px 22px;
padding: 12px 14px; justify-content: flex-start;
padding: 4px 0 8px;
}
.funds-ac-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px;
}
.funds-ac-card.is-off {
opacity: 0.72;
}
.funds-ac-head {
display: flex;
align-items: center; align-items: center;
justify-content: space-between;
gap: 8px; gap: 8px;
width: 118px;
flex: 0 0 auto;
} }
.funds-ac-head h3 { .funds-ac-circle {
margin: 0; position: relative;
font-size: 0.92rem; width: 112px;
height: 112px;
padding: 0;
border: 2px solid var(--border-soft);
border-radius: 50%;
background: var(--panel);
cursor: pointer;
overflow: hidden;
transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s;
} }
.funds-ac-badge { .funds-ac-circle:hover:not(:disabled) {
font-size: 0.68rem; border-color: var(--accent, #3b82f6);
padding: 2px 8px; box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
border-radius: 999px; transform: translateY(-1px);
border: 1px solid var(--border-soft);
color: var(--muted);
} }
.funds-ac-stats { .funds-ac-circle:focus-visible {
display: grid; outline: 2px solid var(--accent, #3b82f6);
grid-template-columns: 1fr 1fr; outline-offset: 3px;
gap: 6px 10px;
font-size: 0.78rem;
} }
.funds-ac-stats .k { .funds-ac-circle.is-off {
color: var(--muted); opacity: 0.62;
margin-right: 6px; cursor: default;
} }
.funds-ac-stats .v { .funds-ac-circle.is-off:hover {
font-variant-numeric: tabular-nums; transform: none;
box-shadow: none;
border-color: var(--border-soft);
} }
.funds-ac-chart { .funds-ac-circle-chart {
height: 72px; position: absolute;
min-height: 72px; inset: 6px;
border-radius: 6px; border-radius: 50%;
overflow: hidden;
background: var(--inset-surface); background: var(--inset-surface);
font-size: 0.72rem; font-size: 0.68rem;
color: var(--muted); color: var(--muted);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
text-align: center;
line-height: 1.25;
padding: 4px;
}
.funds-ac-circle-overlay {
position: absolute;
inset: 0;
border-radius: 50%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
padding: 10px;
background: radial-gradient(circle at 50% 55%, rgba(11, 14, 24, 0.55) 0%, rgba(11, 14, 24, 0.82) 68%);
pointer-events: none;
}
html[data-theme="light"] .funds-ac-circle-overlay {
background: radial-gradient(circle at 50% 55%, rgba(240, 244, 249, 0.45) 0%, rgba(240, 244, 249, 0.88) 68%);
}
.funds-ac-circle-name {
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.02em;
max-width: 88px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.funds-ac-circle-amt {
font-size: 0.78rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
color: var(--text);
}
.funds-ac-circle-badge {
font-size: 0.66rem;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid var(--border-soft);
color: var(--muted);
text-align: center;
max-width: 100%;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.funds-ac-circle-badge.is-ok {
color: var(--green);
border-color: rgba(34, 197, 94, 0.35);
} }
.funds-empty { .funds-empty {
color: var(--muted); color: var(--muted);
@@ -4557,6 +4610,100 @@ body.hub-page-ai #page-ai {
padding: 12px 0; padding: 12px 0;
} }
.funds-fullscreen {
position: fixed;
inset: 0;
z-index: 160;
background: var(--fs-scrim);
backdrop-filter: blur(6px);
overflow: auto;
padding: 16px 20px 24px;
}
.funds-fullscreen.hidden {
display: none !important;
}
.funds-fs-backdrop {
position: fixed;
inset: 0;
z-index: 0;
border: none;
padding: 0;
margin: 0;
background: transparent;
cursor: pointer;
}
.funds-fs-panel {
position: relative;
z-index: 1;
max-width: min(1200px, 96vw);
margin: 0 auto;
background: var(--panel);
border: 1px solid var(--border-soft);
border-radius: var(--radius);
padding: 16px 18px 20px;
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.28);
}
.funds-fs-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px solid var(--border-soft);
}
.funds-fs-title {
margin: 0;
font-size: 1.15rem;
font-weight: 600;
}
.funds-fs-sub {
margin: 4px 0 0;
font-size: 0.76rem;
color: var(--muted);
}
.funds-fs-summary {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
gap: 10px;
margin-bottom: 14px;
}
.funds-fs-stat {
background: var(--inset-surface);
border: 1px solid var(--border-soft);
border-radius: 8px;
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 4px;
}
.funds-fs-stat .k {
font-size: 0.72rem;
color: var(--muted);
}
.funds-fs-stat .v {
font-size: 1rem;
font-weight: 600;
font-variant-numeric: tabular-nums;
}
.funds-fs-stat .v.pos {
color: var(--green);
}
.funds-fs-stat .v.neg {
color: var(--red);
}
.funds-fs-chart-host {
height: min(52vh, 420px);
min-height: 260px;
border: 1px solid var(--border-soft);
border-radius: var(--radius);
background: var(--chart-surface, var(--panel));
overflow: hidden;
}
body.funds-fullscreen-open {
overflow: hidden;
}
/* —— 币种档案 —— */ /* —— 币种档案 —— */
.archive-toolbar { .archive-toolbar {
flex-wrap: wrap; flex-wrap: wrap;
+192 -47
View File
@@ -15,10 +15,26 @@
const elAccounts = document.getElementById("funds-accounts"); const elAccounts = document.getElementById("funds-accounts");
const elBtnRefresh = document.getElementById("funds-btn-refresh"); const elBtnRefresh = document.getElementById("funds-btn-refresh");
const elFs = document.getElementById("funds-fullscreen");
const elFsBackdrop = document.getElementById("funds-fs-backdrop");
const elFsClose = document.getElementById("funds-fs-close");
const elFsTitle = document.getElementById("funds-fs-title");
const elFsSub = document.getElementById("funds-fs-sub");
const elFsTotal = document.getElementById("funds-fs-total");
const elFsFunding = document.getElementById("funds-fs-funding");
const elFsTrading = document.getElementById("funds-fs-trading");
const elFsDelta = document.getElementById("funds-fs-delta");
const elFsDd = document.getElementById("funds-fs-dd");
const elFsChartHost = document.getElementById("funds-fs-chart");
let chart = null; let chart = null;
let lineSeries = null; let lineSeries = null;
let fsChart = null;
let fsLineSeries = null;
let inited = false; let inited = false;
let loading = false; let loading = false;
let lastOverview = null;
let fsAccountKey = "";
function fmt(n, d) { function fmt(n, d) {
if (n == null || n === "" || !Number.isFinite(Number(n))) return "—"; if (n == null || n === "" || !Number.isFinite(Number(n))) return "—";
@@ -64,6 +80,15 @@
if (elChartHost) elChartHost.innerHTML = ""; if (elChartHost) elChartHost.innerHTML = "";
} }
function destroyFsChart() {
if (fsChart) {
fsChart.remove();
fsChart = null;
fsLineSeries = null;
}
if (elFsChartHost) elFsChartHost.innerHTML = "";
}
function chartPalette() { function chartPalette() {
const light = document.documentElement.getAttribute("data-theme") === "light"; const light = document.documentElement.getAttribute("data-theme") === "light";
return light return light
@@ -71,11 +96,9 @@
: { bg: "#0b0e18", text: "#9aa4b8", border: "#2a3348", line: "#3b82f6" }; : { bg: "#0b0e18", text: "#9aa4b8", border: "#2a3348", line: "#3b82f6" };
} }
function ensureChart() { function createAreaChart(host) {
if (!elChartHost || !window.LightweightCharts) return;
if (chart) return;
const p = chartPalette(); const p = chartPalette();
chart = LightweightCharts.createChart(elChartHost, { const c = LightweightCharts.createChart(host, {
layout: { background: { color: p.bg }, textColor: p.text }, layout: { background: { color: p.bg }, textColor: p.text },
grid: { grid: {
vertLines: { color: p.border, visible: true }, vertLines: { color: p.border, visible: true },
@@ -87,7 +110,7 @@
handleScroll: { mouseWheel: true, pressedMouseMove: true }, handleScroll: { mouseWheel: true, pressedMouseMove: true },
handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true }, handleScale: { axisPressedMouseMove: true, mouseWheel: true, pinch: true },
}); });
lineSeries = chart.addAreaSeries({ const s = c.addAreaSeries({
lineColor: p.line, lineColor: p.line,
topColor: p.line + "44", topColor: p.line + "44",
bottomColor: p.line + "08", bottomColor: p.line + "08",
@@ -95,11 +118,28 @@
priceFormat: { type: "price", precision: 2, minMove: 0.01 }, priceFormat: { type: "price", precision: 2, minMove: 0.01 },
}); });
new ResizeObserver(function () { new ResizeObserver(function () {
if (chart && elChartHost) { if (c && host) {
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight }); c.applyOptions({ width: host.clientWidth, height: host.clientHeight });
} }
}).observe(elChartHost); }).observe(host);
chart.applyOptions({ width: elChartHost.clientWidth, height: elChartHost.clientHeight }); c.applyOptions({ width: host.clientWidth, height: host.clientHeight });
return { chart: c, series: s };
}
function ensureChart() {
if (!elChartHost || !window.LightweightCharts) return;
if (chart) return;
const built = createAreaChart(elChartHost);
chart = built.chart;
lineSeries = built.series;
}
function ensureFsChart() {
if (!elFsChartHost || !window.LightweightCharts) return;
if (fsChart) return;
const built = createAreaChart(elFsChartHost);
fsChart = built.chart;
fsLineSeries = built.series;
} }
function renderMiniChart(host, series) { function renderMiniChart(host, series) {
@@ -120,7 +160,12 @@
handleScroll: false, handleScroll: false,
handleScale: false, handleScale: false,
}); });
const s = mini.addLineSeries({ color: p.line, lineWidth: 1.5 }); const s = mini.addAreaSeries({
lineColor: p.line,
topColor: p.line + "55",
bottomColor: p.line + "05",
lineWidth: 1.5,
});
s.setData(data); s.setData(data);
mini.timeScale().fitContent(); mini.timeScale().fitContent();
const w = host.clientWidth; const w = host.clientWidth;
@@ -128,6 +173,21 @@
if (w > 0 && h > 0) mini.applyOptions({ width: w, height: h }); if (w > 0 && h > 0) mini.applyOptions({ width: w, height: h });
} }
function accountStatus(ac) {
if (!ac || !ac.monitored) return { text: "未监控", cls: "" };
if (ac.data_ok) return { text: "已监控", cls: "is-ok" };
return { text: "余额未齐", cls: "" };
}
function monitoredLabel(ac) {
return ac && ac.monitored ? "暂无曲线" : "未参与";
}
function shortAmt(ac) {
if (!ac || !ac.monitored || !ac.data_ok) return "—";
return fmt(ac.total_usdt, 0) + " U";
}
function renderAccounts(accounts) { function renderAccounts(accounts) {
if (!elAccounts) return; if (!elAccounts) return;
if (!accounts || !accounts.length) { if (!accounts || !accounts.length) {
@@ -137,66 +197,139 @@
elAccounts.innerHTML = accounts elAccounts.innerHTML = accounts
.map(function (ac) { .map(function (ac) {
const monitored = !!ac.monitored; const monitored = !!ac.monitored;
const cls = monitored ? "" : " is-off"; const offCls = monitored ? "" : " is-off";
const total = monitored && ac.data_ok ? fmt(ac.total_usdt, 2) + " U" : "—"; const st = accountStatus(ac);
const funding = monitored && ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) : "—"; const clickable = monitored ? "" : ' disabled aria-disabled="true"';
const trading = monitored && ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) : "—";
const dd = ac.drawdown || {};
const ddU = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—";
const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—";
const status = monitored ? (ac.data_ok ? "已监控" : "余额未齐") : "未监控";
return ( return (
'<article class="funds-ac-card' + '<div class="funds-ac-item">' +
cls + '<button type="button" class="funds-ac-circle' +
offCls +
'" data-key="' + '" data-key="' +
(ac.key || "") + (ac.key || "") +
'"' +
clickable +
' title="' +
(monitored ? "点击查看资金曲线" : "未监控,不参与合计") +
'">' + '">' +
'<div class="funds-ac-head">' + '<div class="funds-ac-circle-chart" aria-hidden="true"></div>' +
'<h3>' + '<div class="funds-ac-circle-overlay">' +
'<span class="funds-ac-circle-name">' +
(ac.name || ac.key || "—") + (ac.name || ac.key || "—") +
"</h3>" + "</span>" +
'<span class="funds-ac-badge">' + '<span class="funds-ac-circle-amt">' +
status + shortAmt(ac) +
"</span>" + "</span>" +
"</div>" + "</div>" +
'<div class="funds-ac-stats">' + "</button>" +
'<div><span class="k">总资金</span><span class="v">' + '<span class="funds-ac-circle-badge ' +
total + st.cls +
"</span></div>" + '">' +
'<div><span class="k">资金户</span><span class="v">' + st.text +
funding + "</span>" +
"</span></div>" + "</div>"
'<div><span class="k">交易户</span><span class="v">' +
trading +
"</span></div>" +
'<div><span class="k">最大回撤</span><span class="v">' +
ddU +
" / " +
ddPct +
"</span></div>" +
"</div>" +
'<div class="funds-ac-chart" aria-hidden="true"></div>' +
"</article>"
); );
}) })
.join(""); .join("");
elAccounts.querySelectorAll(".funds-ac-card").forEach(function (card, idx) { elAccounts.querySelectorAll(".funds-ac-circle").forEach(function (btn, idx) {
const ac = accounts[idx]; const ac = accounts[idx];
const host = card.querySelector(".funds-ac-chart"); const host = btn.querySelector(".funds-ac-circle-chart");
if (ac && ac.monitored && host) { if (ac && ac.monitored && host) {
renderMiniChart(host, ac.series || []); renderMiniChart(host, ac.series || []);
} else if (host) { } else if (host) {
host.textContent = monitoredLabel(ac); host.textContent = monitoredLabel(ac);
} }
if (ac && ac.monitored) {
btn.addEventListener("click", function () {
openAccountFullscreen(ac.key);
});
}
}); });
} }
function monitoredLabel(ac) { function findAccount(key) {
return ac && ac.monitored ? "暂无曲线" : "未参与合计"; const accounts = (lastOverview && lastOverview.accounts) || [];
return accounts.find(function (ac) {
return String(ac.key || "") === String(key || "");
});
}
function closeAccountFullscreen() {
fsAccountKey = "";
destroyFsChart();
if (elFs) {
elFs.classList.add("hidden");
elFs.setAttribute("aria-hidden", "true");
}
document.body.classList.remove("funds-fullscreen-open");
}
function openAccountFullscreen(key) {
const ac = findAccount(key);
if (!ac || !ac.monitored) return;
fsAccountKey = String(key || "");
const dd = ac.drawdown || {};
const meta = lastOverview || {};
if (elFsTitle) elFsTitle.textContent = ac.name || ac.key || "—";
if (elFsSub) {
const parts = [
"资金户 + 交易户(不含浮盈)",
"交易日 " + (meta.trading_day || "—"),
"自 " + (meta.history_start_day || "2026-06-09") + " 起",
];
elFsSub.textContent = parts.join(" · ");
}
if (elFsTotal) {
elFsTotal.textContent =
ac.data_ok && ac.total_usdt != null ? fmt(ac.total_usdt, 2) + " U" : "—";
}
if (elFsFunding) {
elFsFunding.textContent =
ac.funding_usdt != null ? fmt(ac.funding_usdt, 2) + " U" : "—";
}
if (elFsTrading) {
elFsTrading.textContent =
ac.trading_usdt != null ? fmt(ac.trading_usdt, 2) + " U" : "—";
}
if (elFsDelta) {
elFsDelta.textContent = fmtDelta(ac.day_delta_usdt);
elFsDelta.className = "v " + deltaClass(ac.day_delta_usdt);
}
if (elFsDd) {
const ddU = dd.max_drawdown_u != null ? fmt(dd.max_drawdown_u, 2) + " U" : "—";
const ddPct = dd.max_drawdown_pct != null ? fmt(dd.max_drawdown_pct, 2) + "%" : "—";
elFsDd.textContent = ddU + " / " + ddPct;
}
if (elFs) {
elFs.classList.remove("hidden");
elFs.setAttribute("aria-hidden", "false");
document.body.classList.add("funds-fullscreen-open");
}
destroyFsChart();
const pts = seriesToChartData(ac.series || []);
if (pts.length) {
ensureFsChart();
if (fsLineSeries) {
fsLineSeries.setData(pts);
fsChart.timeScale().fitContent();
}
requestAnimationFrame(function () {
if (fsChart && elFsChartHost) {
fsChart.applyOptions({
width: elFsChartHost.clientWidth,
height: elFsChartHost.clientHeight,
});
fsChart.timeScale().fitContent();
}
});
} else if (elFsChartHost) {
elFsChartHost.innerHTML =
'<p class="funds-empty">暂无历史曲线,请保持监控板运行以积累快照</p>';
}
} }
function renderOverview(data) { function renderOverview(data) {
lastOverview = data;
const totals = data.totals || {}; const totals = data.totals || {};
const dd = totals.drawdown || {}; const dd = totals.drawdown || {};
if (elTotal) { if (elTotal) {
@@ -235,6 +368,11 @@
} }
} }
renderAccounts(data.accounts || []); renderAccounts(data.accounts || []);
if (fsAccountKey) {
const ac = findAccount(fsAccountKey);
if (ac && ac.monitored) openAccountFullscreen(fsAccountKey);
else closeAccountFullscreen();
}
} }
async function load() { async function load() {
@@ -259,8 +397,14 @@
function bind() { function bind() {
if (elBtnRefresh) elBtnRefresh.addEventListener("click", load); if (elBtnRefresh) elBtnRefresh.addEventListener("click", load);
if (elFsBackdrop) elFsBackdrop.addEventListener("click", closeAccountFullscreen);
if (elFsClose) elFsClose.addEventListener("click", closeAccountFullscreen);
document.addEventListener("keydown", function (ev) {
if (ev.key === "Escape" && fsAccountKey) closeAccountFullscreen();
});
document.addEventListener("hub-theme-change", function () { document.addEventListener("hub-theme-change", function () {
destroyChart(); destroyChart();
destroyFsChart();
load(); load();
}); });
} }
@@ -275,6 +419,7 @@
} }
function destroy() { function destroy() {
closeAccountFullscreen();
destroyChart(); destroyChart();
} }
+22
View File
@@ -357,9 +357,31 @@
<p id="funds-meta" class="funds-meta"></p> <p id="funds-meta" class="funds-meta"></p>
<div id="funds-chart-total" class="funds-chart-host"></div> <div id="funds-chart-total" class="funds-chart-host"></div>
<h2 class="funds-section-title">分户资金</h2> <h2 class="funds-section-title">分户资金</h2>
<p class="funds-section-hint">圆形为各交易所快照,点击查看资金曲线与回撤</p>
<div id="funds-accounts" class="funds-accounts"></div> <div id="funds-accounts" class="funds-accounts"></div>
</div> </div>
<div id="funds-fullscreen" class="funds-fullscreen hidden" aria-hidden="true">
<button type="button" id="funds-fs-backdrop" class="funds-fs-backdrop" aria-label="关闭全屏"></button>
<div class="funds-fs-panel">
<div class="funds-fs-head">
<div>
<h2 id="funds-fs-title" class="funds-fs-title"></h2>
<p id="funds-fs-sub" class="funds-fs-sub"></p>
</div>
<button type="button" id="funds-fs-close" class="ghost">关闭</button>
</div>
<section class="funds-fs-summary">
<div class="funds-fs-stat"><span class="k">总资金</span><span id="funds-fs-total" class="v"></span></div>
<div class="funds-fs-stat"><span class="k">资金户</span><span id="funds-fs-funding" class="v"></span></div>
<div class="funds-fs-stat"><span class="k">交易户</span><span id="funds-fs-trading" class="v"></span></div>
<div class="funds-fs-stat"><span class="k">较昨日</span><span id="funds-fs-delta" class="v"></span></div>
<div class="funds-fs-stat"><span class="k">最大回撤</span><span id="funds-fs-dd" class="v"></span></div>
</section>
<div id="funds-fs-chart" class="funds-fs-chart-host"></div>
</div>
</div>
<div id="page-ai" class="page hidden"> <div id="page-ai" class="page hidden">
<div class="page-head"> <div class="page-head">
<h1><span class="head-tag">AI</span> 教练</h1> <h1><span class="head-tag">AI</span> 教练</h1>