From 86ecf38ebe7c2496984a606f3d161132548a6bb2 Mon Sep 17 00:00:00 2001 From: wess09 Date: Mon, 11 May 2026 21:42:45 +0800 Subject: [PATCH] fix(webui): align coin chart data with AP timeline --- module/statistics/cl1_database.py | 10 +++ module/webui/app.py | 48 +++++++++----- webapp/ap_chart.js | 106 +++++++++++++++++++----------- 3 files changed, 109 insertions(+), 55 deletions(-) diff --git a/module/statistics/cl1_database.py b/module/statistics/cl1_database.py index 2b484f979..42410778f 100644 --- a/module/statistics/cl1_database.py +++ b/module/statistics/cl1_database.py @@ -503,6 +503,16 @@ class Cl1Database: } 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) # 保留最近 500 条记录,避免数据过大 if len(snapshots) > 500: diff --git a/module/webui/app.py b/module/webui/app.py index 467a2c5c6..8e0e3eedb 100644 --- a/module/webui/app.py +++ b/module/webui/app.py @@ -470,6 +470,7 @@ class AlasGUI(Frame): counts = [] ap_list = [] detail_sources = [] + chart_points = [] is_detail_mode = False today = _dt.now().date() @@ -486,11 +487,13 @@ class AlasGUI(Frame): labels.append(p['dt'].strftime('%H:%M')) ap_list.append(p['ap']) detail_sources.append(p.get('source', '-')) + chart_points.append(p) view_title = t("Gui.Stat.DetailChartTitle") else: for p in raw_points: labels.append(p['dt'].strftime('%m-%d %H:%M')) ap_list.append(p['ap']) + chart_points.append(p) view_title = t("Gui.Stat.ViewTitleLine") is_detail_mode = False current_view = 'line' @@ -498,6 +501,7 @@ class AlasGUI(Frame): for p in raw_points: labels.append(p['dt'].strftime('%m-%d %H:%M')) ap_list.append(p['ap']) + chart_points.append(p) view_title = t("Gui.Stat.ViewTitleLine") else: from collections import OrderedDict @@ -561,8 +565,7 @@ class AlasGUI(Frame): coins_stats_html = '' coins_legend_html = '' - if coins_timeline and current_view in ('line', 'detail'): - show_coins = True + if coins_timeline and chart_points and current_view in ('line', 'detail'): coins_raw_points = [] for pt in coins_timeline: ts_raw = pt.get('ts', '') @@ -579,29 +582,42 @@ class AlasGUI(Frame): if coins_raw_points: coins_raw_points.sort(key=lambda p: p['dt']) - for p in coins_raw_points: - yellow_coins_list.append(p['yellow_coins']) - purple_coins_list.append(p['purple_coins']) - coins_sources_list.append(p.get('source', '-')) + coins_idx = 0 + coins_last = len(coins_raw_points) - 1 + for p in chart_points: + 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: - yc_cur = yellow_coins_list[-1] - yc_change = yellow_coins_list[-1] - yellow_coins_list[0] if len(yellow_coins_list) >= 2 else 0 + valid_yellow_coins = [v for v in yellow_coins_list if v is not None] + valid_purple_coins = [v for v in purple_coins_list if v is not None] + 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_sign = '+' if yc_change >= 0 else '' - yc_max = max(yellow_coins_list) - yc_min = min(yellow_coins_list) + yc_max = max(valid_yellow_coins) + yc_min = min(valid_yellow_coins) coins_stats_html += f'
黄币: {yc_cur}变化: {yc_change_sign}{yc_change}最高: {yc_max}最低: {yc_min}
' coins_legend_html += '黄币' - if purple_coins_list: - pc_cur = purple_coins_list[-1] - pc_change = purple_coins_list[-1] - purple_coins_list[0] if len(purple_coins_list) >= 2 else 0 + if valid_purple_coins: + pc_cur = valid_purple_coins[-1] + 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_sign = '+' if pc_change >= 0 else '' - pc_max = max(purple_coins_list) - pc_min = min(purple_coins_list) + pc_max = max(valid_purple_coins) + pc_min = min(valid_purple_coins) coins_stats_html += f'
紫币: {pc_cur}变化: {pc_change_sign}{pc_change}最高: {pc_max}最低: {pc_min}
' coins_legend_html += '紫币' diff --git a/webapp/ap_chart.js b/webapp/ap_chart.js index 57970ac1c..48d993a5d 100644 --- a/webapp/ap_chart.js +++ b/webapp/ap_chart.js @@ -96,12 +96,15 @@ var coinsMin = Infinity, coinsMax = -Infinity; var yellowCoinsLen = yellowCoins ? yellowCoins.length : 0; var purpleCoinsLen = purpleCoins ? purpleCoins.length : 0; + var hasCoins = showCoins && chartType === 'line' && (yellowCoinsLen > 0 || purpleCoinsLen > 0); if (showCoins && chartType === 'line') { 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] > coinsMax) coinsMax = yellowCoins[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] > coinsMax) coinsMax = purpleCoins[i]; } @@ -115,6 +118,47 @@ 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 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 candleW = Math.max(3, Math.min(candleSpace * 0.6, 30)); @@ -136,8 +180,8 @@ ctx.fillText(Math.round(v), pad.l - 8, y); } - if (showCoins && chartType === 'line' && yellowCoinsLen > 0) { - ctx.fillStyle = "#ffd54f"; + if (hasCoins) { + ctx.fillStyle = "#999"; ctx.textAlign = "left"; for (var i = 0; i <= 5; i++) { var v = coinsMin + (coinsMax - coinsMin) * (i / 5); @@ -278,35 +322,7 @@ drawMA(10, "#e91e63"); } - if (showCoins && chartType === 'line' && yellowCoinsLen > 0) { - 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([]); - } + drawCoinsLine(xOfLine, 0, nn); cv.addEventListener("mousemove", function(e) { var rect = cv.getBoundingClientRect(); @@ -375,17 +391,17 @@ { 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 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 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 } }] }); } - if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) { + if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) { 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 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 } }] }); @@ -423,17 +439,17 @@ { 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 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 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 } }] }); } - if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen) { + if (showCoins && purpleCoinsLen > 0 && idx < purpleCoinsLen && purpleCoins[idx] !== null && purpleCoins[idx] !== undefined) { 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 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 } }] }); @@ -552,6 +568,16 @@ 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.font = "10px -apple-system, sans-serif"; ctx.textAlign = "center"; @@ -594,6 +620,8 @@ ctx.fill(); } + drawCoinsLine(dxOf, visibleStart, visibleEnd); + var labelInterval = Math.max(1, Math.floor(visibleNn / 8)); for (var i = visibleStart; i < visibleEnd; i += labelInterval) { var lx = dxOf(i);