增加K线
This commit is contained in:
+151
-2
@@ -46,6 +46,9 @@ let lwcModalCandles = [];
|
||||
let lwcModalInterval = "1d";
|
||||
let lwcModalPriceMeta = { tick_size: "0.01", price_precision: 2 };
|
||||
let lwcOnVisibleRangeChange = null;
|
||||
let lwcOnCrosshairMove = null;
|
||||
let lwcOnChartClick = null;
|
||||
let lwcPinnedCandleTime = null;
|
||||
|
||||
function cacheKey(symbol, interval) {
|
||||
return `${symbol}:${interval}`;
|
||||
@@ -109,6 +112,142 @@ function formatPrice(price, 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) {
|
||||
if (!meta?.tick_size) return null;
|
||||
const priceMeta = {
|
||||
@@ -410,8 +549,10 @@ async function loadMiniChart(box) {
|
||||
|
||||
function destroyLwcChart() {
|
||||
unbindVisibleRangeHighLow();
|
||||
unbindChartInteractions();
|
||||
clearHighLowAnnotations();
|
||||
lwcModalCandles = [];
|
||||
lwcPinnedCandleTime = null;
|
||||
if (lwcResizeObserver) {
|
||||
lwcResizeObserver.disconnect();
|
||||
lwcResizeObserver = null;
|
||||
@@ -561,6 +702,7 @@ function ensureLwcChart(container) {
|
||||
lwcResizeObserver.observe(container);
|
||||
|
||||
bindVisibleRangeHighLow();
|
||||
bindChartInteractions();
|
||||
|
||||
return lwcChart;
|
||||
}
|
||||
@@ -578,12 +720,16 @@ function renderLwcChart(candles, interval, priceMeta) {
|
||||
lwcModalCandles = candles;
|
||||
lwcModalInterval = interval;
|
||||
lwcModalPriceMeta = meta;
|
||||
lwcPinnedCandleTime = null;
|
||||
|
||||
const { ohlc, vol } = candlesToLwc(candles, interval);
|
||||
lwcCandleSeries.setData(ohlc);
|
||||
lwcVolumeSeries.setData(vol);
|
||||
lwcChart.timeScale().fitContent();
|
||||
requestAnimationFrame(() => updateHighLowForVisibleWindow());
|
||||
requestAnimationFrame(() => {
|
||||
updateHighLowForVisibleWindow();
|
||||
showLatestOhlcPanel();
|
||||
});
|
||||
}
|
||||
|
||||
function updateIntervalTabs() {
|
||||
@@ -599,7 +745,7 @@ function updateModalMeta(candles, source, interval) {
|
||||
title.textContent = `${chartModalSymbol} · ${interval.toUpperCase()} K线`;
|
||||
}
|
||||
if (hint) {
|
||||
hint.textContent = `${candles.length} 根 · ${sourceLabel(source)} · 最高/最低随可见窗口 · 滚轮缩放 · Esc 关闭`;
|
||||
hint.textContent = `${candles.length} 根 · ${sourceLabel(source)} · 十字线看当前 · 点击选中 · Esc 关闭`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -674,6 +820,8 @@ function setupChartModal() {
|
||||
</div>
|
||||
<p class="chart-modal-hint" id="chart-modal-hint"></p>
|
||||
<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>
|
||||
</div>`;
|
||||
@@ -694,6 +842,7 @@ function setupChartModal() {
|
||||
});
|
||||
|
||||
modal.querySelector(".chart-modal-close").onclick = closeChartModal;
|
||||
document.getElementById("chart-autoscale-btn")?.addEventListener("click", onChartAutoscale);
|
||||
modal.addEventListener("click", (e) => {
|
||||
if (e.target === modal) closeChartModal();
|
||||
});
|
||||
|
||||
@@ -501,12 +501,85 @@ button:hover {
|
||||
}
|
||||
|
||||
.chart-modal-canvas-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
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 {
|
||||
float: right;
|
||||
background: transparent;
|
||||
|
||||
Reference in New Issue
Block a user