行情区:默认200根、增量刷新、振幅与TV式现价倒计时
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2051,10 +2051,53 @@ body.login-page {
|
|||||||
margin-right: 4px;
|
margin-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.market-price-tag {
|
||||||
|
position: absolute;
|
||||||
|
right: 52px;
|
||||||
|
z-index: 6;
|
||||||
|
pointer-events: none;
|
||||||
|
padding: 4px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-family: var(--font);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1.2;
|
||||||
|
text-align: center;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
min-width: 76px;
|
||||||
|
box-shadow: 0 1px 6px rgba(0, 0, 0, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-price-tag.hidden {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-price-tag.is-up {
|
||||||
|
background: #00ff9d;
|
||||||
|
color: #0a1018;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-price-tag.is-down {
|
||||||
|
background: #ff4d6d;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-price-tag-value {
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
}
|
||||||
|
|
||||||
|
.market-price-tag-time {
|
||||||
|
margin-top: 2px;
|
||||||
|
font-size: 0.68rem;
|
||||||
|
font-weight: 500;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
opacity: 0.95;
|
||||||
|
}
|
||||||
|
|
||||||
.market-price-auto {
|
.market-price-auto {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 6px;
|
right: 8px;
|
||||||
top: 42%;
|
bottom: 10px;
|
||||||
z-index: 5;
|
z-index: 5;
|
||||||
padding: 4px 8px;
|
padding: 4px 8px;
|
||||||
font-size: 0.68rem;
|
font-size: 0.68rem;
|
||||||
|
|||||||
@@ -262,8 +262,9 @@
|
|||||||
if (page === "settings") loadSettingsUI();
|
if (page === "settings") loadSettingsUI();
|
||||||
if (page === "market" && window.hubMarketChart) {
|
if (page === "market" && window.hubMarketChart) {
|
||||||
window.hubMarketChart.init();
|
window.hubMarketChart.init();
|
||||||
} else if (window.hubMarketChart && window.hubMarketChart.stopAutoRefresh) {
|
} else if (window.hubMarketChart) {
|
||||||
window.hubMarketChart.stopAutoRefresh();
|
if (window.hubMarketChart.stopAutoRefresh) window.hubMarketChart.stopAutoRefresh();
|
||||||
|
if (window.hubMarketChart.stopPriceTagTimer) window.hubMarketChart.stopPriceTagTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,17 @@
|
|||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
const AUTO_REFRESH_MS = 60000;
|
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");
|
const chartHost = document.getElementById("market-chart");
|
||||||
if (!chartHost) return;
|
if (!chartHost) return;
|
||||||
|
|
||||||
@@ -17,6 +28,10 @@
|
|||||||
const elL = document.getElementById("mkt-l");
|
const elL = document.getElementById("mkt-l");
|
||||||
const elC = document.getElementById("mkt-c");
|
const elC = document.getElementById("mkt-c");
|
||||||
const elV = document.getElementById("mkt-v");
|
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 elExLabel = document.getElementById("mkt-exchange-label");
|
||||||
const elExBadge = document.getElementById("market-exchange-badge");
|
const elExBadge = document.getElementById("market-exchange-badge");
|
||||||
const elSymLabel = document.getElementById("mkt-symbol-label");
|
const elSymLabel = document.getElementById("mkt-symbol-label");
|
||||||
@@ -35,6 +50,9 @@
|
|||||||
let loadToken = 0;
|
let loadToken = 0;
|
||||||
let marketInited = false;
|
let marketInited = false;
|
||||||
let refreshTimer = null;
|
let refreshTimer = null;
|
||||||
|
let lastViewKey = "";
|
||||||
|
let currentTf = "1d";
|
||||||
|
let priceTagTimer = null;
|
||||||
|
|
||||||
function fmtVol(v) {
|
function fmtVol(v) {
|
||||||
if (v == null || Number.isNaN(Number(v))) return "-";
|
if (v == null || Number.isNaN(Number(v))) return "-";
|
||||||
@@ -89,10 +107,38 @@
|
|||||||
updateExchangeDisplay();
|
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) {
|
function paintOhlcv(bar) {
|
||||||
if (!bar) {
|
if (!bar) {
|
||||||
["o", "h", "l", "c", "v"].forEach(function (k) {
|
["o", "h", "l", "c", "v", "amp"].forEach(function (k) {
|
||||||
const el = { o: elO, h: elH, l: elL, c: elC, v: elV }[k];
|
const el = { o: elO, h: elH, l: elL, c: elC, v: elV, amp: elAmp }[k];
|
||||||
if (el) el.textContent = "-";
|
if (el) el.textContent = "-";
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -102,6 +148,7 @@
|
|||||||
if (elL) elL.textContent = fmtPrice(bar.low);
|
if (elL) elL.textContent = fmtPrice(bar.low);
|
||||||
if (elC) elC.textContent = fmtPrice(bar.close);
|
if (elC) elC.textContent = fmtPrice(bar.close);
|
||||||
if (elV) elV.textContent = fmtVol(bar.volume);
|
if (elV) elV.textContent = fmtVol(bar.volume);
|
||||||
|
if (elAmp) elAmp.textContent = fmtAmplitude(bar);
|
||||||
}
|
}
|
||||||
|
|
||||||
function latestCandle() {
|
function latestCandle() {
|
||||||
@@ -110,6 +157,46 @@
|
|||||||
|
|
||||||
function showLatestOhlcv() {
|
function showLatestOhlcv() {
|
||||||
paintOhlcv(latestCandle());
|
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() {
|
function applyPriceAutoScale() {
|
||||||
@@ -157,7 +244,12 @@
|
|||||||
horzLines: { visible: false },
|
horzLines: { visible: false },
|
||||||
},
|
},
|
||||||
rightPriceScale: { borderColor: "#2a4058", autoScale: true },
|
rightPriceScale: { borderColor: "#2a4058", autoScale: true },
|
||||||
timeScale: { borderColor: "#2a4058", timeVisible: true, secondsVisible: false },
|
timeScale: {
|
||||||
|
borderColor: "#2a4058",
|
||||||
|
timeVisible: true,
|
||||||
|
secondsVisible: false,
|
||||||
|
rightOffset: RIGHT_OFFSET_BARS,
|
||||||
|
},
|
||||||
crosshair: {
|
crosshair: {
|
||||||
mode: LightweightCharts.CrosshairMode
|
mode: LightweightCharts.CrosshairMode
|
||||||
? LightweightCharts.CrosshairMode.Normal
|
? LightweightCharts.CrosshairMode.Normal
|
||||||
@@ -171,6 +263,8 @@
|
|||||||
borderVisible: false,
|
borderVisible: false,
|
||||||
wickUpColor: "#00ff9d",
|
wickUpColor: "#00ff9d",
|
||||||
wickDownColor: "#ff4d6d",
|
wickDownColor: "#ff4d6d",
|
||||||
|
lastValueVisible: false,
|
||||||
|
priceLineVisible: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (typeof chart.addCandlestickSeries === "function") {
|
if (typeof chart.addCandlestickSeries === "function") {
|
||||||
@@ -223,11 +317,13 @@
|
|||||||
|
|
||||||
chart.timeScale().subscribeVisibleLogicalRangeChange(function () {
|
chart.timeScale().subscribeVisibleLogicalRangeChange(function () {
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
|
updatePriceTag();
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("resize", function () {
|
window.addEventListener("resize", function () {
|
||||||
if (!chart) return;
|
if (!chart) return;
|
||||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||||
|
updatePriceTag();
|
||||||
});
|
});
|
||||||
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
chart.applyOptions({ width: chartHost.clientWidth, height: chartHost.clientHeight });
|
||||||
return true;
|
return true;
|
||||||
@@ -242,6 +338,20 @@
|
|||||||
rangeMarkers = [];
|
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() {
|
function updateVisibleRangeMarkers() {
|
||||||
clearMarkers();
|
clearMarkers();
|
||||||
if (!candleSeries || !chart || !lastCandles.length) return;
|
if (!candleSeries || !chart || !lastCandles.length) return;
|
||||||
@@ -305,7 +415,7 @@
|
|||||||
refreshTimer = setInterval(function () {
|
refreshTimer = setInterval(function () {
|
||||||
const page = document.getElementById("page-market");
|
const page = document.getElementById("page-market");
|
||||||
if (!page || page.classList.contains("hidden")) return;
|
if (!page || page.classList.contains("hidden")) return;
|
||||||
loadChart(false);
|
loadChart(false, { autoTick: true });
|
||||||
}, AUTO_REFRESH_MS);
|
}, AUTO_REFRESH_MS);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -330,11 +440,14 @@
|
|||||||
updateExchangeDisplay();
|
updateExchangeDisplay();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadChart(force) {
|
async function loadChart(force, options) {
|
||||||
|
options = options || {};
|
||||||
|
const autoTick = !!options.autoTick;
|
||||||
if (!ensureChart()) return;
|
if (!ensureChart()) return;
|
||||||
const exKey = (elExchange && elExchange.value) || "";
|
const exKey = (elExchange && elExchange.value) || "";
|
||||||
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
const sym = (elSymbol && elSymbol.value.trim().toUpperCase()) || "";
|
||||||
const tf = (elTf && elTf.value) || "1d";
|
const tf = (elTf && elTf.value) || "1d";
|
||||||
|
currentTf = tf;
|
||||||
if (!exKey || !sym) {
|
if (!exKey || !sym) {
|
||||||
if (elStatus) {
|
if (elStatus) {
|
||||||
elStatus.className = "market-status err";
|
elStatus.className = "market-status err";
|
||||||
@@ -343,7 +456,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const myToken = ++loadToken;
|
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.className = "market-status";
|
||||||
elStatus.textContent = "加载中…";
|
elStatus.textContent = "加载中…";
|
||||||
}
|
}
|
||||||
@@ -372,7 +491,12 @@
|
|||||||
indexCandles(lastCandles);
|
indexCandles(lastCandles);
|
||||||
candleSeries.setData(lastCandles);
|
candleSeries.setData(lastCandles);
|
||||||
volumeSeries.setData(buildVolumeData(lastCandles));
|
volumeSeries.setData(buildVolumeData(lastCandles));
|
||||||
chart.timeScale().fitContent();
|
if (resetView) {
|
||||||
|
lastViewKey = vKey;
|
||||||
|
applyDefaultVisibleRange();
|
||||||
|
} else if (savedRange) {
|
||||||
|
chart.timeScale().setVisibleLogicalRange(savedRange);
|
||||||
|
}
|
||||||
applyPriceAutoScale();
|
applyPriceAutoScale();
|
||||||
updateVisibleRangeMarkers();
|
updateVisibleRangeMarkers();
|
||||||
showLatestOhlcv();
|
showLatestOhlcv();
|
||||||
@@ -449,6 +573,7 @@
|
|||||||
bind();
|
bind();
|
||||||
}
|
}
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
|
startPriceTagTimer();
|
||||||
await loadChart(false);
|
await loadChart(false);
|
||||||
},
|
},
|
||||||
reload: function (force) {
|
reload: function (force) {
|
||||||
@@ -456,6 +581,7 @@
|
|||||||
},
|
},
|
||||||
startAutoRefresh: startAutoRefresh,
|
startAutoRefresh: startAutoRefresh,
|
||||||
stopAutoRefresh: stopAutoRefresh,
|
stopAutoRefresh: stopAutoRefresh,
|
||||||
|
stopPriceTagTimer: stopPriceTagTimer,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" media="print" onload="this.media='all'" />
|
||||||
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
<noscript><link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600&family=Orbitron:wght@500;600;700&display=swap" rel="stylesheet" /></noscript>
|
||||||
<link rel="stylesheet" href="/assets/app.css?v=20260528-hub-market3" />
|
<link rel="stylesheet" href="/assets/app.css?v=20260528-hub-market5" />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="app-bg" aria-hidden="true"></div>
|
<div class="app-bg" aria-hidden="true"></div>
|
||||||
@@ -110,8 +110,13 @@
|
|||||||
<span class="ohlcv-item"><span class="k">低</span><span id="mkt-l">—</span></span>
|
<span class="ohlcv-item"><span class="k">低</span><span id="mkt-l">—</span></span>
|
||||||
<span class="ohlcv-item"><span class="k">收</span><span id="mkt-c">—</span></span>
|
<span class="ohlcv-item"><span class="k">收</span><span id="mkt-c">—</span></span>
|
||||||
<span class="ohlcv-item market-vol"><span class="k">量</span><span id="mkt-v">—</span></span>
|
<span class="ohlcv-item market-vol"><span class="k">量</span><span id="mkt-v">—</span></span>
|
||||||
|
<span class="ohlcv-item"><span class="k">振幅</span><span id="mkt-amp">—</span></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="market-price-tag" class="market-price-tag hidden" aria-hidden="true">
|
||||||
|
<div id="market-price-tag-value" class="market-price-tag-value"></div>
|
||||||
|
<div id="market-price-tag-time" class="market-price-tag-time"></div>
|
||||||
|
</div>
|
||||||
<button type="button" id="market-price-auto" class="market-price-auto is-on" title="价格轴自动缩放">自动</button>
|
<button type="button" id="market-price-auto" class="market-price-auto is-on" title="价格轴自动缩放">自动</button>
|
||||||
<div id="market-chart" class="market-chart-host"></div>
|
<div id="market-chart" class="market-chart-host"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -182,7 +187,7 @@
|
|||||||
|
|
||||||
<div id="toast"></div>
|
<div id="toast"></div>
|
||||||
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
<script src="https://unpkg.com/lightweight-charts@4.2.0/dist/lightweight-charts.standalone.production.js"></script>
|
||||||
<script src="/assets/chart.js?v=20260528-hub-market3"></script>
|
<script src="/assets/chart.js?v=20260528-hub-market5"></script>
|
||||||
<script src="/assets/app.js?v=20260528-hub-market3"></script>
|
<script src="/assets/app.js?v=20260528-hub-market5"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user