diff --git a/app/static/app.js b/app/static/app.js
index b74b5d2..ba605d0 100644
--- a/app/static/app.js
+++ b/app/static/app.js
@@ -14,6 +14,7 @@
const checkingIds = new Set();
let historyCache = {};
let hasRendered = false; // 首次渲染保留进入动画,自动刷新静默更新
+ const cardCharts = {}; // {accountId: Chart} 卡片迷你图实例
const accountWindows = {}; // {accountId: 'all'|'1h'|'1d'|'1w'|'1mo'|'1yr'}
const windowCache = {}; // {'aid:win': [...]}
@@ -208,7 +209,6 @@
} else {
hist = windowCache[a.id + ":" + win] || historyCache[a.id] || [];
}
- const spark = sparkline(hist, a.threshold, st.cls);
const winBtns = WINDOW_KEYS.map((k) =>
``
).join("");
@@ -231,10 +231,10 @@
: `—`}
${th !== null ? `/ 阈值 ${th}` : ""}
- ${spark
+ ${hist.length > 0
? `
余额历史 · ${WINDOW_LABELS[win]}(${hist.length} 条${th !== null ? `,虚线阈值 ${th}` : ""})
- ${spark}
+
${winBtns}
`
: ""}
@@ -274,37 +274,55 @@
}
}
- /* 余额历史迷你折线图(SVG)。含阈值虚线;线色跟随状态。 */
- function sparkline(hist, threshold, statusCls) {
- const vals = (hist || []).map((h) => Number(h.balance)).filter(Number.isFinite);
- if (vals.length === 0) return "";
- const W = 100, H = 30, PAD = 3;
- const max = Math.max(...vals, threshold > 0 ? threshold : 0);
- const min = Math.min(...vals, threshold > 0 ? threshold : 0);
- const range = max - min || 1;
- const yOf = (v) => (H - PAD - ((v - min) / range) * (H - PAD * 2)).toFixed(1);
- const lineColor = statusCls === "warn" ? "var(--warn)"
- : statusCls === "err" ? "var(--text-3)"
- : statusCls === "disabled" ? "var(--text-3)"
- : "var(--ok)";
- let thLine = "";
+ /* 销毁所有卡片迷你图实例(重绘前调用,避免 canvas 替换后泄漏) */
+ function destroyCardCharts() {
+ Object.values(cardCharts).forEach((c) => { try { c.destroy(); } catch (_) { /* 忽略 */ } });
+ for (const k in cardCharts) delete cardCharts[k];
+ }
+
+ /* 卡片迷你折线图(Chart.js):线色跟随状态,阈值画虚线,无坐标轴 */
+ function createCardChart(canvas, hist, threshold, statusCls) {
+ const css = getComputedStyle(document.documentElement);
+ const lineColor = statusCls === "warn" ? (css.getPropertyValue("--warn").trim() || "#d97706")
+ : statusCls === "err" || statusCls === "disabled" ? (css.getPropertyValue("--text-3").trim() || "#9aa1ad")
+ : (css.getPropertyValue("--accent").trim() || "#3b6ef6");
+ const thColor = css.getPropertyValue("--text-3").trim() || "#9aa1ad";
+ const datasets = [{
+ label: "余额",
+ data: hist.map((h) => h.balance),
+ borderColor: lineColor,
+ backgroundColor: lineColor + "1f",
+ fill: true,
+ tension: 0.35,
+ pointRadius: 0,
+ borderWidth: 1.5,
+ }];
if (threshold > 0) {
- const y = yOf(threshold);
- thLine = ``;
+ datasets.push({
+ label: "阈值",
+ data: [threshold, threshold],
+ borderColor: thColor,
+ borderDash: [4, 4],
+ pointRadius: 0,
+ borderWidth: 1,
+ });
}
- if (vals.length === 1) {
- const x = W / 2, y = yOf(vals[0]);
- return ``;
- }
- const pts = vals.map((v, i) => {
- const x = (i / (vals.length - 1)) * W;
- return `${x.toFixed(1)},${yOf(v)}`;
+ return new Chart(canvas, {
+ type: "line",
+ data: { labels: hist.map((h) => h.checked_at), datasets },
+ options: {
+ responsive: true,
+ maintainAspectRatio: false,
+ animation: false, // 自动刷新时不重播动画,避免闪烁
+ plugins: { legend: { display: false }, tooltip: { enabled: false } },
+ scales: { x: { display: false }, y: { display: false } },
+ },
});
- return ``;
}
function renderAccounts(animate = true) {
const grid = document.getElementById("account-grid");
+ destroyCardCharts();
const stats = { total: accounts.length, ok: 0, below: 0, error: 0, disabled: 0, pending: 0 };
accounts.forEach((a) => {
const st = statusInfo(a);
@@ -340,6 +358,18 @@
list = [...list].sort((x, y) => STATUS_ORDER[statusInfo(x).cls] - STATUS_ORDER[statusInfo(y).cls]);
grid.innerHTML = list.map((a, i) => accountCard(a, i, animate)).join("");
+ // 为每张卡片的 canvas 创建 Chart.js 迷你图
+ grid.querySelectorAll("canvas.card-chart").forEach((canvas) => {
+ const aid = Number(canvas.dataset.aid);
+ const acc = accounts.find((a) => a.id === aid);
+ if (!acc) return;
+ const win = accountWindows[aid] || "all";
+ const hist = win === "all"
+ ? (historyCache[aid] || [])
+ : (windowCache[aid + ":" + win] || historyCache[aid] || []);
+ if (hist.length === 0) return;
+ cardCharts[aid] = createCardChart(canvas, hist, acc.threshold, statusInfo(acc).cls);
+ });
const hint = document.getElementById("empty-hint");
if (accounts.length > 0) {
hint.classList.add("hidden");
@@ -392,11 +422,12 @@
/* ---------- 模态框 ---------- */
- function openModal(html, onMount) {
+ function openModal(html, onMount, onClose) {
const root = document.getElementById("modal-root");
const mask = document.createElement("div");
mask.className = "modal-mask";
mask.innerHTML = html;
+ if (onClose) mask._onClose = onClose;
mask.addEventListener("click", (e) => { if (e.target === mask) closeModal(mask); });
document.addEventListener("keydown", (e) => { if (e.key === "Escape") closeModal(mask); }, { once: true });
root.appendChild(mask);
@@ -405,7 +436,9 @@
}
function closeModal(mask) {
- if (mask) mask.remove();
+ if (!mask) return;
+ if (mask._onClose) mask._onClose();
+ mask.remove();
}
function confirmDialog(text, onOk) {
@@ -513,7 +546,7 @@
});
}
- /* 历史弹窗 */
+ /* 历史弹窗:纯表格(时间 + 余额),无图表 */
async function showHistory(accountId) {
const acc = accounts.find((a) => a.id === accountId);
if (!acc) return;
@@ -530,12 +563,15 @@
body.innerHTML = `暂无记录(检查成功后自动记录)
`;
return;
}
- const max = Math.max(...hist.map((h) => h.balance), 1e-9);
- const bars = hist.map((h) =>
- ``).join("");
- const items = [...hist].reverse().map((h) =>
- `${escapeHtml(h.checked_at)}${fmtBalance(h.balance)} ${escapeHtml(plat.currency || "")}
`).join("");
- body.innerHTML = `${bars}
${items}`;
+ const rows = [...hist].reverse().map((h) =>
+ `| ${escapeHtml(h.checked_at)} | ${fmtBalance(h.balance)} ${escapeHtml(plat.currency || "")} |
`).join("");
+ body.innerHTML = `
+ `;
} catch (e) {
mask.querySelector("#hist-body").textContent = "加载失败:" + e.message;
}
diff --git a/app/static/index.html b/app/static/index.html
index 3a76e39..a356566 100644
--- a/app/static/index.html
+++ b/app/static/index.html
@@ -139,6 +139,7 @@
+