Skip to content

Commit 117b862

Browse files
committed
站点卡片:用近 48 小时余额迷你走势图取代已用/累计进度条 (v1.3.1)
已用/累计充值是终身累计值,对监控没有行动价值。换成余额 sparkline: 陡降 = 消耗快、平线 = 闲置、跳升 = 充值,一眼可辨消耗节奏, 也是趋势弹窗的缩略预览。历史数据服务端等距抽样到约 40 个点, 控制 /api/stations 载荷
1 parent eaf30f7 commit 117b862

6 files changed

Lines changed: 44 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
## 功能
99

10-
- **总览面板**:总剩余余额、**今日总消耗**、日均消耗(估算)、低余额 / 耗尽、查询异常统计;**总余额趋势图**(全站合计、24 小时~30 天切换、悬停查看分站明细)与**日均消耗对比图**;每站余额、今日消耗、用量进度条、状态标签、查询延迟
10+
- **总览面板**:总剩余余额、**今日总消耗**、日均消耗(估算)、低余额 / 耗尽、查询异常统计;**总余额趋势图**(全站合计、24 小时~30 天切换、悬停查看分站明细)与**日均消耗对比图**;每站余额、今日消耗、**近 48 小时余额迷你走势图**、状态标签、查询延迟
1111
- **今日消耗与站点一致**:Sub2API 站点直接读取站点用户仪表盘同款接口(`/api/v1/usage/dashboard/stats``today_actual_cost`,即今日实际扣费),与站点页面显示的数值完全一致;其他类型按余额历史推算并以 ≈ 标注
1212
- **用量统计页**:分站点、分模型、分时段的 Token 消耗——今天(按小时)/ 近 24 小时(滚动窗口)/
1313
近 7 天 / 近 30 天,含消耗趋势柱状图、分模型条形图与明细表(请求数 / 输入输出 / 总 Tokens / 实际消耗,

lib/history.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,21 @@ export class History {
7575
return Math.round(used * 100) / 100;
7676
}
7777

78+
// 站点卡片迷你走势图用:等距抽样到 maxPoints 个点,保留最后一个点
79+
sparkline(stationId, hours = 48, maxPoints = 40) {
80+
const pts = this.points(stationId, hours);
81+
if (pts.length <= maxPoints) return pts.map((p) => [p[0], p[1]]);
82+
const step = pts.length / maxPoints;
83+
const out = [];
84+
for (let i = 0; i < maxPoints; i++) {
85+
const p = pts[Math.floor(i * step)];
86+
out.push([p[0], p[1]]);
87+
}
88+
const last = pts[pts.length - 1];
89+
if (out[out.length - 1][0] !== last[0]) out.push([last[0], last[1]]);
90+
return out;
91+
}
92+
7893
points(stationId, hours = 72) {
7994
const arr = this.data[stationId] || [];
8095
const cutoff = Date.now() - hours * 3600 * 1000;

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "relay-monitor",
3-
"version": "1.3.0",
3+
"version": "1.3.1",
44
"private": true,
55
"description": "监控 sub2api / new-api 中转站余额的网页面板",
66
"type": "module",

public/app.js

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -165,12 +165,30 @@ $("#logoutBtn").onclick = async () => {
165165
const PLATE = { newapi: "NA", "newapi-key": "KEY", sub2api: "S2", "sub2api-password": "S2" };
166166
const CH_PLATE = { telegram: "TG", dingtalk: "DT", wecom: "WC", feishu: "FS", bark: "BK", ntfy: "NF", serverchan: "SC", resend: "RS", smtp: "SM", webhook: "WH" };
167167

168+
// 站点卡片里的迷你余额走势(近 48 小时):陡降 = 消耗快,平线 = 闲置,跳升 = 充值
169+
function sparkSvg(pts) {
170+
if (!pts || pts.length < 2) return "";
171+
const W = 170, H = 30, P = 3;
172+
const t0 = pts[0][0], t1 = pts[pts.length - 1][0];
173+
let min = Infinity, max = -Infinity;
174+
for (const [, v] of pts) { if (v < min) min = v; if (v > max) max = v; }
175+
if (max - min < 1e-9) { min -= 1; max += 1; } // 余额没变化时画一条居中的平线
176+
const x = (t) => P + ((t - t0) / (t1 - t0 || 1)) * (W - 2 * P);
177+
const y = (v) => P + (1 - (v - min) / (max - min)) * (H - 2 * P);
178+
const line = pts.map((p, i) => `${i ? "L" : "M"}${x(p[0]).toFixed(1)},${y(p[1]).toFixed(1)}`).join("");
179+
const area = `${line}L${x(t1).toFixed(1)},${H - P}L${x(t0).toFixed(1)},${H - P}Z`;
180+
const last = pts[pts.length - 1];
181+
return `<svg class="spark" viewBox="0 0 ${W} ${H}" aria-hidden="true">
182+
<path class="spark-area" d="${area}"/><path class="spark-line" d="${line}"/>
183+
<circle class="spark-dot" cx="${x(last[0]).toFixed(1)}" cy="${y(last[1]).toFixed(1)}" r="2.5"/>
184+
</svg>`;
185+
}
186+
168187
function stationRow(s) {
169188
const st = statusOf(s);
170189
const b = s.balance;
171190
const rate = rateOf(s);
172191
const cls = st === "danger" || st === "error" ? "danger" : st === "warn" ? "warn" : "";
173-
const usedPct = b && b.ok && b.total > 0 ? Math.min(100, Math.round((b.used / b.total) * 100)) : 0;
174192
const amount = b && b.ok ? cny(b.remaining * rate) : "—";
175193
let meta;
176194
if (b && b.ok) {
@@ -187,8 +205,9 @@ function stationRow(s) {
187205
} else {
188206
meta = `${esc(typeLabel(s.type))} · 尚未查询`;
189207
}
190-
const bar = b && b.ok
191-
? `<div class="st-bar"><div class="progress"><i class="${cls}" style="width:${usedPct}%"></i></div><span class="st-usage">已用 ${cny(b.used * rate)} / ${cny(b.total * rate)}</span></div>`
208+
const spark = sparkSvg(s.spark);
209+
const bar = b && b.ok && spark
210+
? `<div class="st-bar" title="近 48 小时余额走势">${spark}<span class="st-usage">近 48h 余额</span></div>`
192211
: "";
193212
const eta = etaText(s.prediction, rate);
194213
const pieces = [];

public/styles.css

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -151,11 +151,10 @@ button { font-family: inherit; cursor: pointer; }
151151
}
152152
.st-meta { font-size: 11.5px; color: var(--text-tertiary); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
153153
.st-bar { display: flex; align-items: center; gap: 10px; margin-top: 7px; }
154-
.progress { height: 5px; border-radius: 3px; background: rgba(0,0,0,.06); overflow: hidden; flex: 1; max-width: 260px; }
155-
:root[data-theme="dark"] .progress { background: rgba(255,255,255,.08); }
156-
.progress > i { display: block; height: 100%; background: var(--primary); transition: width .3s; }
157-
.progress > i.warn { background: var(--text-warning); }
158-
.progress > i.danger { background: var(--text-danger); }
154+
.spark { width: 170px; height: 30px; flex-shrink: 0; }
155+
.spark-line { fill: none; stroke: var(--primary); stroke-width: 1.5; stroke-linejoin: round; stroke-linecap: round; }
156+
.spark-area { fill: var(--primary); opacity: .08; }
157+
.spark-dot { fill: var(--primary); stroke: var(--bg-surface); stroke-width: 1.5; }
159158
.st-usage { font-size: 11px; color: var(--text-tertiary); white-space: nowrap; }
160159

161160
.st-balance { text-align: right; flex-shrink: 0; min-width: 108px; }

server.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,7 @@ function redact(s) {
481481
? { expiresAt: s2Tokens.expiresAt || null, lastLoginAt: s2Tokens.lastLoginAt || null }
482482
: null,
483483
prediction: history.predict(s.id),
484+
spark: history.sparkline(s.id, 48),
484485
todayUsed: fromSite ?? history.usedSince(s.id, midnight.getTime()),
485486
todayIsEstimate: fromSite == null,
486487
todayTokens: s.balance?.todayTokens ?? null,

0 commit comments

Comments
 (0)