/** 迷你日 K 线图(Canvas) + 限速队列 */ const chartDataCache = new Map(); const chartQueue = []; let chartQueueRunning = false; const CHART_FETCH_GAP_MS = 120; function enqueueCharts(root) { root.querySelectorAll(".mini-chart[data-symbol]").forEach((box) => { const symbol = box.dataset.symbol; if (!symbol || box.dataset.loaded === "1" || box.dataset.loading === "1") return; chartQueue.push(box); }); runChartQueue(); } async function runChartQueue() { if (chartQueueRunning) return; chartQueueRunning = true; while (chartQueue.length) { const box = chartQueue.shift(); if (!box || !box.isConnected) continue; await loadMiniChart(box); await sleep(CHART_FETCH_GAP_MS); } chartQueueRunning = false; } function sleep(ms) { return new Promise((r) => setTimeout(r, ms)); } async function loadMiniChart(box) { const symbol = box.dataset.symbol; if (!symbol) return; box.dataset.loading = "1"; const canvas = box.querySelector("canvas"); const status = box.querySelector(".chart-status"); if (status) status.textContent = "加载…"; try { let candles = chartDataCache.get(symbol); let source = "cache"; if (!candles) { const res = await fetch(`/api/chart/${symbol}/daily?limit=300`); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.detail || res.statusText); } const data = await res.json(); candles = data.candles || []; source = data.source || "db"; chartDataCache.set(symbol, candles); } if (!candles.length) throw new Error("无K线数据"); drawCandlestickChart(canvas, candles); box.dataset.loaded = "1"; const srcLabel = source === "db" ? "本地" : source === "db_stale" ? "本地(旧)" : source === "cache" ? "缓存" : "同步"; if (status) status.textContent = `${candles.length}日·${srcLabel}`; box.title = `${symbol} 最近${candles.length}根日K (${srcLabel})`; } catch (e) { if (status) status.textContent = "—"; box.title = `${symbol}: ${e.message}`; drawEmptyChart(canvas); } finally { box.dataset.loading = "0"; } } function drawEmptyChart(canvas) { if (!canvas) return; const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; ctx.clearRect(0, 0, w, h); ctx.fillStyle = "#3a4558"; ctx.fillRect(0, 0, w, h); ctx.fillStyle = "#8b9cb3"; ctx.font = "11px sans-serif"; ctx.fillText("暂无", 8, h / 2 + 4); } function drawCandlestickChart(canvas, candles) { if (!canvas || !candles.length) return; const ctx = canvas.getContext("2d"); const w = canvas.width; const h = canvas.height; const pad = { t: 4, r: 4, b: 4, l: 4 }; const plotW = w - pad.l - pad.r; const plotH = h - pad.t - pad.b; let min = Infinity; let max = -Infinity; for (const c of candles) { min = Math.min(min, c.low); max = Math.max(max, c.high); } const range = max - min || 1; const n = candles.length; const step = plotW / n; const bodyW = Math.max(1, step * 0.65); ctx.clearRect(0, 0, w, h); ctx.fillStyle = "#121820"; ctx.fillRect(0, 0, w, h); const yOf = (price) => pad.t + plotH * (1 - (price - min) / range); for (let i = 0; i < n; i++) { const c = candles[i]; const up = c.close >= c.open; const x = pad.l + i * step + step / 2; const yHigh = yOf(c.high); const yLow = yOf(c.low); const yOpen = yOf(c.open); const yClose = yOf(c.close); const color = up ? "#0ecb81" : "#f6465d"; ctx.strokeStyle = color; ctx.lineWidth = 1; ctx.beginPath(); ctx.moveTo(x, yHigh); ctx.lineTo(x, yLow); ctx.stroke(); const top = Math.min(yOpen, yClose); const bodyH = Math.max(1, Math.abs(yClose - yOpen)); ctx.fillStyle = color; ctx.fillRect(x - bodyW / 2, top, bodyW, bodyH); } } /** 点击放大 */ function setupChartModal() { let modal = document.getElementById("chart-modal"); if (!modal) { modal = document.createElement("div"); modal.id = "chart-modal"; modal.className = "chart-modal hidden"; modal.innerHTML = `