fix(webui): align coin chart data with AP timeline

This commit is contained in:
wess09 2026-05-11 21:42:45 +08:00
parent a1f5bdb334
commit 86ecf38ebe
3 changed files with 109 additions and 55 deletions

View File

@ -503,6 +503,16 @@ class Cl1Database:
} }
snapshots = data.get('coins_snapshots', []) snapshots = data.get('coins_snapshots', [])
if snapshots:
last = snapshots[-1]
try:
same_yellow = int(last.get('yellow_coins', -1)) == snapshot['yellow_coins']
same_purple = int(last.get('purple_coins', -1)) == snapshot['purple_coins']
if same_yellow and same_purple and last.get('source') == source:
return
except (TypeError, ValueError):
pass
snapshots.append(snapshot) snapshots.append(snapshot)
# 保留最近 500 条记录,避免数据过大 # 保留最近 500 条记录,避免数据过大
if len(snapshots) > 500: if len(snapshots) > 500:

View File

@ -470,6 +470,7 @@ class AlasGUI(Frame):
counts = [] counts = []
ap_list = [] ap_list = []
detail_sources = [] detail_sources = []
chart_points = []
is_detail_mode = False is_detail_mode = False
today = _dt.now().date() today = _dt.now().date()
@ -486,11 +487,13 @@ class AlasGUI(Frame):
labels.append(p['dt'].strftime('%H:%M')) labels.append(p['dt'].strftime('%H:%M'))
ap_list.append(p['ap']) ap_list.append(p['ap'])
detail_sources.append(p.get('source', '-')) detail_sources.append(p.get('source', '-'))
chart_points.append(p)
view_title = t("Gui.Stat.DetailChartTitle") view_title = t("Gui.Stat.DetailChartTitle")
else: else:
for p in raw_points: for p in raw_points:
labels.append(p['dt'].strftime('%m-%d %H:%M')) labels.append(p['dt'].strftime('%m-%d %H:%M'))
ap_list.append(p['ap']) ap_list.append(p['ap'])
chart_points.append(p)
view_title = t("Gui.Stat.ViewTitleLine") view_title = t("Gui.Stat.ViewTitleLine")
is_detail_mode = False is_detail_mode = False
current_view = 'line' current_view = 'line'
@ -498,6 +501,7 @@ class AlasGUI(Frame):
for p in raw_points: for p in raw_points:
labels.append(p['dt'].strftime('%m-%d %H:%M')) labels.append(p['dt'].strftime('%m-%d %H:%M'))
ap_list.append(p['ap']) ap_list.append(p['ap'])
chart_points.append(p)
view_title = t("Gui.Stat.ViewTitleLine") view_title = t("Gui.Stat.ViewTitleLine")
else: else:
from collections import OrderedDict from collections import OrderedDict
@ -561,8 +565,7 @@ class AlasGUI(Frame):
coins_stats_html = '' coins_stats_html = ''
coins_legend_html = '' coins_legend_html = ''
if coins_timeline and current_view in ('line', 'detail'): if coins_timeline and chart_points and current_view in ('line', 'detail'):
show_coins = True
coins_raw_points = [] coins_raw_points = []
for pt in coins_timeline: for pt in coins_timeline:
ts_raw = pt.get('ts', '') ts_raw = pt.get('ts', '')
@ -579,29 +582,42 @@ class AlasGUI(Frame):
if coins_raw_points: if coins_raw_points:
coins_raw_points.sort(key=lambda p: p['dt']) coins_raw_points.sort(key=lambda p: p['dt'])
for p in coins_raw_points: coins_idx = 0
yellow_coins_list.append(p['yellow_coins']) coins_last = len(coins_raw_points) - 1
purple_coins_list.append(p['purple_coins']) for p in chart_points:
coins_sources_list.append(p.get('source', '-')) while coins_idx < coins_last:
cur_delta = abs((coins_raw_points[coins_idx]['dt'] - p['dt']).total_seconds())
next_delta = abs((coins_raw_points[coins_idx + 1]['dt'] - p['dt']).total_seconds())
if next_delta > cur_delta:
break
coins_idx += 1
coins_point = coins_raw_points[coins_idx]
yellow_coins_list.append(coins_point['yellow_coins'])
purple_coins_list.append(coins_point['purple_coins'])
coins_sources_list.append(coins_point.get('source', '-'))
if yellow_coins_list: valid_yellow_coins = [v for v in yellow_coins_list if v is not None]
yc_cur = yellow_coins_list[-1] valid_purple_coins = [v for v in purple_coins_list if v is not None]
yc_change = yellow_coins_list[-1] - yellow_coins_list[0] if len(yellow_coins_list) >= 2 else 0 show_coins = bool(valid_yellow_coins or valid_purple_coins)
if valid_yellow_coins:
yc_cur = valid_yellow_coins[-1]
yc_change = valid_yellow_coins[-1] - valid_yellow_coins[0] if len(valid_yellow_coins) >= 2 else 0
yc_change_color = '#ef5350' if yc_change >= 0 else '#26a69a' yc_change_color = '#ef5350' if yc_change >= 0 else '#26a69a'
yc_change_sign = '+' if yc_change >= 0 else '' yc_change_sign = '+' if yc_change >= 0 else ''
yc_max = max(yellow_coins_list) yc_max = max(valid_yellow_coins)
yc_min = min(yellow_coins_list) yc_min = min(valid_yellow_coins)
coins_stats_html += f'<div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:4px; font-size:12px; color:#aaa;"><span>黄币: <b style="color:#ffd54f">{yc_cur}</b></span><span>变化: <b style="color:{yc_change_color}">{yc_change_sign}{yc_change}</b></span><span>最高: <b style="color:#ef5350">{yc_max}</b></span><span>最低: <b style="color:#26a69a">{yc_min}</b></span></div>' coins_stats_html += f'<div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:4px; font-size:12px; color:#aaa;"><span>黄币: <b style="color:#ffd54f">{yc_cur}</b></span><span>变化: <b style="color:{yc_change_color}">{yc_change_sign}{yc_change}</b></span><span>最高: <b style="color:#ef5350">{yc_max}</b></span><span>最低: <b style="color:#26a69a">{yc_min}</b></span></div>'
coins_legend_html += '<span style="display:flex; align-items:center; gap:4px;"><span style="width:12px; height:2px; background:#ffd54f; border-radius:1px; border-top:1px dashed #ffd54f;"></span>黄币</span>' coins_legend_html += '<span style="display:flex; align-items:center; gap:4px;"><span style="width:12px; height:2px; background:#ffd54f; border-radius:1px; border-top:1px dashed #ffd54f;"></span>黄币</span>'
if purple_coins_list: if valid_purple_coins:
pc_cur = purple_coins_list[-1] pc_cur = valid_purple_coins[-1]
pc_change = purple_coins_list[-1] - purple_coins_list[0] if len(purple_coins_list) >= 2 else 0 pc_change = valid_purple_coins[-1] - valid_purple_coins[0] if len(valid_purple_coins) >= 2 else 0
pc_change_color = '#ef5350' if pc_change >= 0 else '#26a69a' pc_change_color = '#ef5350' if pc_change >= 0 else '#26a69a'
pc_change_sign = '+' if pc_change >= 0 else '' pc_change_sign = '+' if pc_change >= 0 else ''
pc_max = max(purple_coins_list) pc_max = max(valid_purple_coins)
pc_min = min(purple_coins_list) pc_min = min(valid_purple_coins)
coins_stats_html += f'<div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:4px; font-size:12px; color:#aaa;"><span>紫币: <b style="color:#ce93d8">{pc_cur}</b></span><span>变化: <b style="color:{pc_change_color}">{pc_change_sign}{pc_change}</b></span><span>最高: <b style="color:#ef5350">{pc_max}</b></span><span>最低: <b style="color:#26a69a">{pc_min}</b></span></div>' coins_stats_html += f'<div style="display:flex; flex-wrap:wrap; gap:12px; margin-bottom:4px; font-size:12px; color:#aaa;"><span>紫币: <b style="color:#ce93d8">{pc_cur}</b></span><span>变化: <b style="color:{pc_change_color}">{pc_change_sign}{pc_change}</b></span><span>最高: <b style="color:#ef5350">{pc_max}</b></span><span>最低: <b style="color:#26a69a">{pc_min}</b></span></div>'
coins_legend_html += '<span style="display:flex; align-items:center; gap:4px;"><span style="width:12px; height:2px; background:#ce93d8; border-radius:1px; border-top:1px dashed #ce93d8;"></span>紫币</span>' coins_legend_html += '<span style="display:flex; align-items:center; gap:4px;"><span style="width:12px; height:2px; background:#ce93d8; border-radius:1px; border-top:1px dashed #ce93d8;"></span>紫币</span>'

View File

@ -96,12 +96,15 @@
var coinsMin = Infinity, coinsMax = -Infinity; var coinsMin = Infinity, coinsMax = -Infinity;
var yellowCoinsLen = yellowCoins ? yellowCoins.length : 0; var yellowCoinsLen = yellowCoins ? yellowCoins.length : 0;
var purpleCoinsLen = purpleCoins ? purpleCoins.length : 0; var purpleCoinsLen = purpleCoins ? purpleCoins.length : 0;
var hasCoins = showCoins && chartType === 'line' && (yellowCoinsLen > 0 || purpleCoinsLen > 0);
if (showCoins && chartType === 'line') { if (showCoins && chartType === 'line') {
for (var i = 0; i < yellowCoinsLen; i++) { for (var i = 0; i < yellowCoinsLen; i++) {
if (yellowCoins[i] === null || yellowCoins[i] === undefined) continue;
if (yellowCoins[i] < coinsMin) coinsMin = yellowCoins[i]; if (yellowCoins[i] < coinsMin) coinsMin = yellowCoins[i];
if (yellowCoins[i] > coinsMax) coinsMax = yellowCoins[i]; if (yellowCoins[i] > coinsMax) coinsMax = yellowCoins[i];
} }
for (var i = 0; i < purpleCoinsLen; i++) { for (var i = 0; i < purpleCoinsLen; i++) {
if (purpleCoins[i] === null || purpleCoins[i] === undefined) continue;
if (purpleCoins[i] < coinsMin) coinsMin = purpleCoins[i]; if (purpleCoins[i] < coinsMin) coinsMin = purpleCoins[i];
if (purpleCoins[i] > coinsMax) coinsMax = purpleCoins[i]; if (purpleCoins[i] > coinsMax) coinsMax = purpleCoins[i];
} }
@ -115,6 +118,47 @@
function xOfLine(i) { return pad.l + (i / Math.max(nn - 1, 1)) * gW; } function xOfLine(i) { return pad.l + (i / Math.max(nn - 1, 1)) * gW; }
function yOf(v) { return pad.t + gH - (v - allMin) / (allMax - allMin) * gH; } function yOf(v) { return pad.t + gH - (v - allMin) / (allMax - allMin) * gH; }
function yOfCoins(v) { return pad.t + gH - (v - coinsMin) / (coinsMax - coinsMin) * gH; } function yOfCoins(v) { return pad.t + gH - (v - coinsMin) / (coinsMax - coinsMin) * gH; }
function drawCoinsLine(xOf, start, end) {
if (!hasCoins) return;
ctx.lineWidth = 1.5;
ctx.lineJoin = "round";
ctx.setLineDash([4, 2]);
if (yellowCoinsLen > 0) {
ctx.beginPath();
ctx.strokeStyle = "#ffd54f";
var startedYellowCoins = false;
for (var i = start; i < end && i < yellowCoinsLen; i++) {
if (yellowCoins[i] === null || yellowCoins[i] === undefined) {
startedYellowCoins = false;
continue;
}
var x = xOf(i), y = yOfCoins(yellowCoins[i]);
if (!startedYellowCoins) { ctx.moveTo(x, y); startedYellowCoins = true; }
else ctx.lineTo(x, y);
}
ctx.stroke();
}
if (purpleCoinsLen > 0) {
ctx.beginPath();
ctx.strokeStyle = "#ce93d8";
var startedPurpleCoins = false;
for (var i = start; i < end && i < purpleCoinsLen; i++) {
if (purpleCoins[i] === null || purpleCoins[i] === undefined) {
startedPurpleCoins = false;
continue;
}
var x = xOf(i), y = yOfCoins(purpleCoins[i]);
if (!startedPurpleCoins) { ctx.moveTo(x, y); startedPurpleCoins = true; }
else ctx.lineTo(x, y);
}
ctx.stroke();
}
ctx.setLineDash([]);
}
var candleSpace = gW / nn; var candleSpace = gW / nn;
var candleW = Math.max(3, Math.min(candleSpace * 0.6, 30)); var candleW = Math.max(3, Math.min(candleSpace * 0.6, 30));
@ -136,8 +180,8 @@
ctx.fillText(Math.round(v), pad.l - 8, y); ctx.fillText(Math.round(v), pad.l - 8, y);
} }
if (showCoins && chartType === 'line' && yellowCoinsLen > 0) { if (hasCoins) {
ctx.fillStyle = "#ffd54f"; ctx.fillStyle = "#999";
ctx.textAlign = "left"; ctx.textAlign = "left";
for (var i = 0; i <= 5; i++) { for (var i = 0; i <= 5; i++) {
var v = coinsMin + (coinsMax - coinsMin) * (i / 5); var v = coinsMin + (coinsMax - coinsMin) * (i / 5);
@ -278,35 +322,7 @@
drawMA(10, "#e91e63"); drawMA(10, "#e91e63");
} }
if (showCoins && chartType === 'line' && yellowCoinsLen > 0) { drawCoinsLine(xOfLine, 0, nn);
ctx.lineWidth = 1.5;
ctx.lineJoin = "round";
ctx.setLineDash([4, 2]);
if (yellowCoinsLen > 0) {
ctx.beginPath();
ctx.strokeStyle = "#ffd54f";
for (var i = 0; i < yellowCoinsLen; i++) {
var x = xOfLine(i), y = yOfCoins(yellowCoins[i]);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
if (purpleCoinsLen > 0) {
ctx.beginPath();
ctx.strokeStyle = "#ce93d8";
for (var i = 0; i < purpleCoinsLen; i++) {
var x = xOfLine(i), y = yOfCoins(purpleCoins[i]);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
ctx.setLineDash([]);
}
cv.addEventListener("mousemove", function(e) { cv.addEventListener("mousemove", function(e) {
var rect = cv.getBoundingClientRect(); var rect = cv.getBoundingClientRect();
@ -375,17 +391,17 @@
{ parts: [{ type: 'text', value: "来源: " }, { type: 'bold', value: source, style: { color: sourceColor } }] } { parts: [{ type: 'text', value: "来源: " }, { type: 'bold', value: source, style: { color: sourceColor } }] }
]; ];
if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen) { if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) {
var yc = yellowCoins[idx]; var yc = yellowCoins[idx];
var ycDiff = idx > 0 && idx < yellowCoinsLen ? (yc - yellowCoins[idx - 1]) : 0; var ycDiff = idx > 0 && yellowCoins[idx - 1] !== null && yellowCoins[idx - 1] !== undefined ? (yc - yellowCoins[idx - 1]) : 0;
var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a"; var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a";
var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff; var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] }); tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] });
} }
if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) { if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) {
var pc = purpleCoins[idx]; var pc = purpleCoins[idx];
var pcDiff = idx > 0 && idx < purpleCoinsLen ? (pc - purpleCoins[idx - 1]) : 0; var pcDiff = idx > 0 && purpleCoins[idx - 1] !== null && purpleCoins[idx - 1] !== undefined ? (pc - purpleCoins[idx - 1]) : 0;
var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a"; var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a";
var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff; var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] }); tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] });
@ -423,17 +439,17 @@
{ parts: [{ type: 'text', value: "单次变化: " }, { type: 'bold', value: ds, style: { color: dc } }] } { parts: [{ type: 'text', value: "单次变化: " }, { type: 'bold', value: ds, style: { color: dc } }] }
]; ];
if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen) { if (showCoins && yellowCoinsLen > 0 && idx < yellowCoinsLen && yellowCoins[idx] !== null && yellowCoins[idx] !== undefined) {
var yc = yellowCoins[idx]; var yc = yellowCoins[idx];
var ycDiff = idx > 0 && idx < yellowCoinsLen ? (yc - yellowCoins[idx - 1]) : 0; var ycDiff = idx > 0 && yellowCoins[idx - 1] !== null && yellowCoins[idx - 1] !== undefined ? (yc - yellowCoins[idx - 1]) : 0;
var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a"; var ycColor = ycDiff >= 0 ? "#ef5350" : "#26a69a";
var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff; var ycDiffStr = (ycDiff >= 0 ? "+" : "") + ycDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] }); tooltipRows.push({ parts: [{ type: 'text', value: "黄币: " }, { type: 'bold', value: String(yc), style: { color: "#ffd54f" } }, { type: 'text', value: " (" + ycDiffStr + ")", style: { color: ycColor } }] });
} }
if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) { if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) {
var pc = purpleCoins[idx]; var pc = purpleCoins[idx];
var pcDiff = idx > 0 && idx < purpleCoinsLen ? (pc - purpleCoins[idx - 1]) : 0; var pcDiff = idx > 0 && purpleCoins[idx - 1] !== null && purpleCoins[idx - 1] !== undefined ? (pc - purpleCoins[idx - 1]) : 0;
var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a"; var pcColor = pcDiff >= 0 ? "#ef5350" : "#26a69a";
var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff; var pcDiffStr = (pcDiff >= 0 ? "+" : "") + pcDiff;
tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] }); tooltipRows.push({ parts: [{ type: 'text', value: "紫币: " }, { type: 'bold', value: String(pc), style: { color: "#ce93d8" } }, { type: 'text', value: " (" + pcDiffStr + ")", style: { color: pcColor } }] });
@ -552,6 +568,16 @@
ctx.fillText(Math.round(v), pad.l - 8, y); ctx.fillText(Math.round(v), pad.l - 8, y);
} }
if (hasCoins) {
ctx.fillStyle = "#999";
ctx.textAlign = "left";
for (var i = 0; i <= 5; i++) {
var v = coinsMin + (coinsMax - coinsMin) * (i / 5);
var y = yOfCoins(v);
ctx.fillText(Math.round(v), W - pad.r + 8, y);
}
}
ctx.fillStyle = "#666"; ctx.fillStyle = "#666";
ctx.font = "10px -apple-system, sans-serif"; ctx.font = "10px -apple-system, sans-serif";
ctx.textAlign = "center"; ctx.textAlign = "center";
@ -594,6 +620,8 @@
ctx.fill(); ctx.fill();
} }
drawCoinsLine(dxOf, visibleStart, visibleEnd);
var labelInterval = Math.max(1, Math.floor(visibleNn / 8)); var labelInterval = Math.max(1, Math.floor(visibleNn / 8));
for (var i = visibleStart; i < visibleEnd; i += labelInterval) { for (var i = visibleStart; i < visibleEnd; i += labelInterval) {
var lx = dxOf(i); var lx = dxOf(i);