行情区:默认200根、增量刷新、振幅与TV式现价倒计时

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-02 11:30:43 +08:00
parent d942e5b088
commit 2f1ca6fb5a
4 changed files with 189 additions and 14 deletions
+133 -7
View File
@@ -3,6 +3,17 @@
*/
(function () {
const AUTO_REFRESH_MS = 60000;
const DEFAULT_VISIBLE_BARS = 200;
const RIGHT_OFFSET_BARS = 12;
const TF_MS = {
"1m": 60_000,
"5m": 5 * 60_000,
"15m": 15 * 60_000,
"1h": 60 * 60_000,
"4h": 4 * 60 * 60_000,
"1d": 24 * 60 * 60_000,
"1w": 7 * 24 * 60 * 60_000,
};
const chartHost = document.getElementById("market-chart");
if (!chartHost) return;
@@ -17,6 +28,10 @@
const elL = document.getElementById("mkt-l");
const elC = document.getElementById("mkt-c");
const elV = document.getElementById("mkt-v");
const elAmp = document.getElementById("mkt-amp");
const elPriceTag = document.getElementById("market-price-tag");
const elPriceTagValue = document.getElementById("market-price-tag-value");
const elPriceTagTime = document.getElementById("market-price-tag-time");
const elExLabel = document.getElementById("mkt-exchange-label");
const elExBadge = document.getElementById("market-exchange-badge");
const elSymLabel = document.getElementById("mkt-symbol-label");
@@ -35,6 +50,9 @@
let loadToken = 0;
let marketInited = false;
let refreshTimer = null;
let lastViewKey = "";
let currentTf = "1d";
let priceTagTimer = null;
function fmtVol(v) {
if (v == null || Number.isNaN(Number(v))) return "-";
@@ -89,10 +107,38 @@
updateExchangeDisplay();
}
function fmtAmplitude(bar) {
if (!bar) return "-";
const o = Number(bar.open);
const h = Number(bar.high);
const l = Number(bar.low);
if (!o || o <= 0 || !Number.isFinite(h) || !Number.isFinite(l)) return "-";
return (((h - l) / o) * 100).toFixed(2) + "%";
}
function barRemainMs(tf) {
const period = TF_MS[tf] || TF_MS["1d"];
const now = Date.now();
const barOpen = Math.floor(now / period) * period;
return Math.max(0, barOpen + period - now);
}
function fmtBarCountdown(ms) {
const total = Math.max(0, Math.floor(ms / 1000));
const h = Math.floor(total / 3600);
const m = Math.floor((total % 3600) / 60);
const s = total % 60;
const pad = function (n) {
return n < 10 ? "0" + n : String(n);
};
if (h > 0) return h + ":" + pad(m) + ":" + pad(s);
return pad(m) + ":" + pad(s);
}
function paintOhlcv(bar) {
if (!bar) {
["o", "h", "l", "c", "v"].forEach(function (k) {
const el = { o: elO, h: elH, l: elL, c: elC, v: elV }[k];
["o", "h", "l", "c", "v", "amp"].forEach(function (k) {
const el = { o: elO, h: elH, l: elL, c: elC, v: elV, amp: elAmp }[k];
if (el) el.textContent = "-";
});
return;
@@ -102,6 +148,7 @@
if (elL) elL.textContent = fmtPrice(bar.low);
if (elC) elC.textContent = fmtPrice(bar.close);
if (elV) elV.textContent = fmtVol(bar.volume);
if (elAmp) elAmp.textContent = fmtAmplitude(bar);
}
function latestCandle() {
@@ -110,6 +157,46 @@
function showLatestOhlcv() {
paintOhlcv(latestCandle());
updatePriceTag();
}
function updatePriceTag() {
if (!elPriceTag || !candleSeries || !chart) return;
const bar = latestCandle();
if (!bar || bar.close == null) {
elPriceTag.classList.add("hidden");
elPriceTag.setAttribute("aria-hidden", "true");
return;
}
let y = null;
try {
y = candleSeries.priceToCoordinate(Number(bar.close));
} catch (e) {
y = null;
}
const hostH = chartHost.clientHeight || 0;
if (y == null || y < 8 || y > hostH - 8) {
elPriceTag.classList.add("hidden");
elPriceTag.setAttribute("aria-hidden", "true");
return;
}
const up = Number(bar.close) >= Number(bar.open);
elPriceTag.classList.remove("hidden", "is-up", "is-down");
elPriceTag.classList.add(up ? "is-up" : "is-down");
elPriceTag.setAttribute("aria-hidden", "false");
elPriceTag.style.top = y + "px";
if (elPriceTagValue) elPriceTagValue.textContent = fmtPrice(bar.close);
if (elPriceTagTime) elPriceTagTime.textContent = fmtBarCountdown(barRemainMs(currentTf));
}
function startPriceTagTimer() {
stopPriceTagTimer();
priceTagTimer = setInterval(updatePriceTag, 1000);
}
function stopPriceTagTimer() {
if (priceTagTimer) clearInterval(priceTagTimer);
priceTagTimer = null;
}
function applyPriceAutoScale() {
@@ -157,7 +244,12 @@
horzLines: { visible: false },
},
rightPriceScale: { borderColor: "#2a4058", autoScale: true },
timeScale: { borderColor: "#2a4058", timeVisible: true, secondsVisible: false },
timeScale: {
borderColor: "#2a4058",
timeVisible: true,
secondsVisible: false,
rightOffset: RIGHT_OFFSET_BARS,
},
crosshair: {
mode: LightweightCharts.CrosshairMode
? LightweightCharts.CrosshairMode.Normal
@@ -171,6 +263,8 @@
borderVisible: false,
wickUpColor: "#00ff9d",
wickDownColor: "#ff4d6d",
lastValueVisible: false,
priceLineVisible: false,
};
if (typeof chart.addCandlestickSeries === "function") {
@@ -223,11 +317,13 @@
chart.timeScale().subscribeVisibleLogicalRangeChange(function () {
updateVisibleRangeMarkers();
updatePriceTag();
});
window.addEventListener("resize", function () {
if (!chart) return;
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
updatePriceTag();
});
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
return true;
@@ -242,6 +338,20 @@
rangeMarkers = [];
}
function viewKey(exKey, sym, tf) {
return (exKey || "") + "|" + (sym || "") + "|" + (tf || "");
}
function applyDefaultVisibleRange() {
if (!chart || !lastCandles.length) return;
const n = lastCandles.length;
const visible = Math.min(DEFAULT_VISIBLE_BARS, n);
const from = Math.max(0, n - visible);
const to = n - 1;
chart.timeScale().applyOptions({ rightOffset: RIGHT_OFFSET_BARS });
chart.timeScale().setVisibleLogicalRange({ from: from, to: to });
}
function updateVisibleRangeMarkers() {
clearMarkers();
if (!candleSeries || !chart || !lastCandles.length) return;
@@ -305,7 +415,7 @@
refreshTimer = setInterval(function () {
const page = document.getElementById("page-market");
if (!page || page.classList.contains("hidden")) return;
loadChart(false);
loadChart(false, { autoTick: true });
}, AUTO_REFRESH_MS);
}
@@ -330,11 +440,14 @@
updateExchangeDisplay();
}
async function loadChart(force) {
async function loadChart(force, options) {
options = options || {};
const autoTick = !!options.autoTick;
if (!ensureChart()) return;
const exKey = (elExchange && elExchange.value) || "";
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
const tf = (elTf && elTf.value) || "1d";
currentTf = tf;
if (!exKey || !sym) {
if (elStatus) {
elStatus.className = "market-status err";
@@ -343,7 +456,13 @@
return;
}
const myToken = ++loadToken;
if (elStatus) {
const vKey = viewKey(exKey, sym, tf);
const resetView = !!force || !autoTick || vKey !== lastViewKey;
let savedRange = null;
if (!resetView && chart) {
savedRange = chart.timeScale().getVisibleLogicalRange();
}
if (!autoTick && elStatus) {
elStatus.className = "market-status";
elStatus.textContent = "加载中…";
}
@@ -372,7 +491,12 @@
indexCandles(lastCandles);
candleSeries.setData(lastCandles);
volumeSeries.setData(buildVolumeData(lastCandles));
chart.timeScale().fitContent();
if (resetView) {
lastViewKey = vKey;
applyDefaultVisibleRange();
} else if (savedRange) {
chart.timeScale().setVisibleLogicalRange(savedRange);
}
applyPriceAutoScale();
updateVisibleRangeMarkers();
showLatestOhlcv();
@@ -449,6 +573,7 @@
bind();
}
startAutoRefresh();
startPriceTagTimer();
await loadChart(false);
},
reload: function (force) {
@@ -456,6 +581,7 @@
},
startAutoRefresh: startAutoRefresh,
stopAutoRefresh: stopAutoRefresh,
stopPriceTagTimer: stopPriceTagTimer,
};
if (