增加k线图

This commit is contained in:
dekun
2026-05-22 13:47:27 +08:00
parent ee621976db
commit 74f98af40d
13 changed files with 543 additions and 8 deletions
+171
View File
@@ -0,0 +1,171 @@
/** 迷你日 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 = `
<div class="chart-modal-inner">
<button type="button" class="chart-modal-close" aria-label="关闭">×</button>
<h3 id="chart-modal-title"></h3>
<canvas id="chart-modal-canvas" width="900" height="360"></canvas>
</div>`;
document.body.appendChild(modal);
modal.querySelector(".chart-modal-close").onclick = () =>
modal.classList.add("hidden");
modal.addEventListener("click", (e) => {
if (e.target === modal) modal.classList.add("hidden");
});
}
document.body.addEventListener("click", (e) => {
const box = e.target.closest(".mini-chart[data-symbol]");
if (!box || box.dataset.loaded !== "1") return;
const symbol = box.dataset.symbol;
const candles = chartDataCache.get(symbol);
if (!candles) return;
modal.classList.remove("hidden");
document.getElementById("chart-modal-title").textContent =
`${symbol} · 日K ${candles.length}`;
drawCandlestickChart(
document.getElementById("chart-modal-canvas"),
candles
);
});
}
setupChartModal();