增加K线

This commit is contained in:
dekun
2026-05-30 11:00:41 +08:00
parent 61b5e7424a
commit aa0103c634
2 changed files with 224 additions and 2 deletions
+151 -2
View File
@@ -46,6 +46,9 @@ let lwcModalCandles = [];
let lwcModalInterval = "1d"; let lwcModalInterval = "1d";
let lwcModalPriceMeta = { tick_size: "0.01", price_precision: 2 }; let lwcModalPriceMeta = { tick_size: "0.01", price_precision: 2 };
let lwcOnVisibleRangeChange = null; let lwcOnVisibleRangeChange = null;
let lwcOnCrosshairMove = null;
let lwcOnChartClick = null;
let lwcPinnedCandleTime = null;
function cacheKey(symbol, interval) { function cacheKey(symbol, interval) {
return `${symbol}:${interval}`; return `${symbol}:${interval}`;
@@ -109,6 +112,142 @@ function formatPrice(price, precision) {
return Number(price).toFixed(precision); return Number(price).toFixed(precision);
} }
function formatVolume(val) {
const v = Number(val);
if (!Number.isFinite(v)) return "—";
if (v >= 1e8) return `${(v / 1e8).toFixed(2)}亿`;
if (v >= 1e4) return `${(v / 1e4).toFixed(2)}`;
return v.toFixed(2);
}
function calcAmplitude(candle) {
const open = Number(candle.open);
const high = Number(candle.high);
const low = Number(candle.low);
if (!open) return "—";
return `${(((high - low) / open) * 100).toFixed(2)}%`;
}
function lwcTimeEquals(a, b) {
if (a == null || b == null) return false;
if (typeof a === "number" && typeof b === "number") return a === b;
if (typeof a === "object" && typeof b === "object") {
return a.year === b.year && a.month === b.month && a.day === b.day;
}
return false;
}
function findCandleByLwcTime(time) {
if (time == null) return null;
for (const c of lwcModalCandles) {
if (lwcTimeEquals(toLwcTime(c.time, lwcModalInterval), time)) return c;
}
return null;
}
function formatCandleTimeLabel(ms, interval) {
const d = new Date(ms);
if (interval === "1d" || interval === "1w") {
return d.toLocaleDateString("zh-CN");
}
return d.toLocaleString("zh-CN", {
month: "2-digit",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
hour12: false,
});
}
function renderOhlcPanel(candle, modeLabel) {
const panel = document.getElementById("chart-ohlc-panel");
if (!panel) return;
if (!candle) {
panel.innerHTML = '<span class="chart-ohlc-empty">—</span>';
return;
}
const precision = lwcModalPriceMeta.price_precision ?? 2;
const up = Number(candle.close) >= Number(candle.open);
const closeCls = up ? "up" : "down";
const vol = formatVolume(candle.quote_volume || candle.volume);
const timeLabel = formatCandleTimeLabel(candle.time, lwcModalInterval);
panel.innerHTML = `
<div class="chart-ohlc-head">
<span class="chart-ohlc-mode">${modeLabel}</span>
<span class="chart-ohlc-time">${timeLabel}</span>
</div>
<div class="chart-ohlc-row">
<span>开 <b>${formatPrice(candle.open, precision)}</b></span>
<span>高 <b class="up">${formatPrice(candle.high, precision)}</b></span>
<span>低 <b class="down">${formatPrice(candle.low, precision)}</b></span>
<span>收 <b class="${closeCls}">${formatPrice(candle.close, precision)}</b></span>
<span>量 <b>${vol}</b></span>
<span>振幅 <b>${calcAmplitude(candle)}</b></span>
</div>`;
}
function showLatestOhlcPanel() {
const last = lwcModalCandles[lwcModalCandles.length - 1];
renderOhlcPanel(last, "最新");
}
function updateOhlcFromTime(time, modeLabel) {
const candle = findCandleByLwcTime(time);
if (candle) renderOhlcPanel(candle, modeLabel);
}
function onChartAutoscale() {
if (!lwcChart) return;
lwcPinnedCandleTime = null;
lwcChart.timeScale().fitContent();
lwcChart.priceScale("right").applyOptions({ autoScale: true });
requestAnimationFrame(() => {
updateHighLowForVisibleWindow();
showLatestOhlcPanel();
});
}
function unbindChartInteractions() {
if (lwcChart && lwcOnCrosshairMove) {
lwcChart.unsubscribeCrosshairMove(lwcOnCrosshairMove);
}
if (lwcChart && lwcOnChartClick) {
lwcChart.unsubscribeClick(lwcOnChartClick);
}
lwcOnCrosshairMove = null;
lwcOnChartClick = null;
}
function bindChartInteractions() {
if (!lwcChart) return;
unbindChartInteractions();
lwcOnCrosshairMove = (param) => {
if (lwcPinnedCandleTime != null) return;
if (param.time) {
updateOhlcFromTime(param.time, "当前");
return;
}
showLatestOhlcPanel();
};
lwcOnChartClick = (param) => {
if (!param.time) return;
if (lwcPinnedCandleTime != null && lwcTimeEquals(lwcPinnedCandleTime, param.time)) {
lwcPinnedCandleTime = null;
updateOhlcFromTime(param.time, "当前");
return;
}
lwcPinnedCandleTime = param.time;
updateOhlcFromTime(param.time, "选中");
};
lwcChart.subscribeCrosshairMove(lwcOnCrosshairMove);
lwcChart.subscribeClick(lwcOnChartClick);
}
function rememberPriceMeta(symbol, meta) { function rememberPriceMeta(symbol, meta) {
if (!meta?.tick_size) return null; if (!meta?.tick_size) return null;
const priceMeta = { const priceMeta = {
@@ -410,8 +549,10 @@ async function loadMiniChart(box) {
function destroyLwcChart() { function destroyLwcChart() {
unbindVisibleRangeHighLow(); unbindVisibleRangeHighLow();
unbindChartInteractions();
clearHighLowAnnotations(); clearHighLowAnnotations();
lwcModalCandles = []; lwcModalCandles = [];
lwcPinnedCandleTime = null;
if (lwcResizeObserver) { if (lwcResizeObserver) {
lwcResizeObserver.disconnect(); lwcResizeObserver.disconnect();
lwcResizeObserver = null; lwcResizeObserver = null;
@@ -561,6 +702,7 @@ function ensureLwcChart(container) {
lwcResizeObserver.observe(container); lwcResizeObserver.observe(container);
bindVisibleRangeHighLow(); bindVisibleRangeHighLow();
bindChartInteractions();
return lwcChart; return lwcChart;
} }
@@ -578,12 +720,16 @@ function renderLwcChart(candles, interval, priceMeta) {
lwcModalCandles = candles; lwcModalCandles = candles;
lwcModalInterval = interval; lwcModalInterval = interval;
lwcModalPriceMeta = meta; lwcModalPriceMeta = meta;
lwcPinnedCandleTime = null;
const { ohlc, vol } = candlesToLwc(candles, interval); const { ohlc, vol } = candlesToLwc(candles, interval);
lwcCandleSeries.setData(ohlc); lwcCandleSeries.setData(ohlc);
lwcVolumeSeries.setData(vol); lwcVolumeSeries.setData(vol);
lwcChart.timeScale().fitContent(); lwcChart.timeScale().fitContent();
requestAnimationFrame(() => updateHighLowForVisibleWindow()); requestAnimationFrame(() => {
updateHighLowForVisibleWindow();
showLatestOhlcPanel();
});
} }
function updateIntervalTabs() { function updateIntervalTabs() {
@@ -599,7 +745,7 @@ function updateModalMeta(candles, source, interval) {
title.textContent = `${chartModalSymbol} · ${interval.toUpperCase()} K线`; title.textContent = `${chartModalSymbol} · ${interval.toUpperCase()} K线`;
} }
if (hint) { if (hint) {
hint.textContent = `${candles.length} 根 · ${sourceLabel(source)} · 最高/最低随可见窗口 · 滚轮缩放 · Esc 关闭`; hint.textContent = `${candles.length} 根 · ${sourceLabel(source)} · 十字线看当前 · 点击选中 · Esc 关闭`;
} }
} }
@@ -674,6 +820,8 @@ function setupChartModal() {
</div> </div>
<p class="chart-modal-hint" id="chart-modal-hint"></p> <p class="chart-modal-hint" id="chart-modal-hint"></p>
<div class="chart-modal-canvas-wrap"> <div class="chart-modal-canvas-wrap">
<div id="chart-ohlc-panel" class="chart-ohlc-panel"></div>
<button type="button" id="chart-autoscale-btn" class="chart-autoscale-btn">自动</button>
<div id="chart-modal-container" class="chart-lwc-container"></div> <div id="chart-modal-container" class="chart-lwc-container"></div>
</div> </div>
</div>`; </div>`;
@@ -694,6 +842,7 @@ function setupChartModal() {
}); });
modal.querySelector(".chart-modal-close").onclick = closeChartModal; modal.querySelector(".chart-modal-close").onclick = closeChartModal;
document.getElementById("chart-autoscale-btn")?.addEventListener("click", onChartAutoscale);
modal.addEventListener("click", (e) => { modal.addEventListener("click", (e) => {
if (e.target === modal) closeChartModal(); if (e.target === modal) closeChartModal();
}); });
+73
View File
@@ -501,12 +501,85 @@ button:hover {
} }
.chart-modal-canvas-wrap { .chart-modal-canvas-wrap {
position: relative;
overflow: hidden; overflow: hidden;
border-radius: 8px; border-radius: 8px;
border: 1px solid var(--border); border: 1px solid var(--border);
background: #0d1118; background: #0d1118;
} }
.chart-ohlc-panel {
position: absolute;
top: 10px;
left: 12px;
z-index: 5;
pointer-events: none;
padding: 0.45rem 0.65rem;
border-radius: 6px;
background: rgba(13, 17, 24, 0.82);
border: 1px solid var(--border);
font-size: 0.82rem;
color: var(--text);
line-height: 1.45;
max-width: calc(100% - 120px);
}
.chart-ohlc-head {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.2rem;
color: var(--muted);
font-size: 0.75rem;
}
.chart-ohlc-mode {
color: var(--accent);
font-weight: 600;
}
.chart-ohlc-row {
display: flex;
flex-wrap: wrap;
gap: 0.65rem 1rem;
}
.chart-ohlc-row b {
font-weight: 600;
color: var(--text);
}
.chart-ohlc-row b.up {
color: #0ecb81;
}
.chart-ohlc-row b.down {
color: #f6465d;
}
.chart-ohlc-empty {
color: var(--muted);
}
.chart-autoscale-btn {
position: absolute;
right: 58px;
bottom: 28px;
z-index: 5;
padding: 0.2rem 0.55rem;
font-size: 0.75rem;
border: 1px solid var(--border);
border-radius: 4px;
background: rgba(13, 17, 24, 0.88);
color: var(--muted);
cursor: pointer;
}
.chart-autoscale-btn:hover {
color: var(--text);
border-color: var(--accent);
}
.chart-modal-close { .chart-modal-close {
float: right; float: right;
background: transparent; background: transparent;