K线后台自动刷新并通过SSE推送到前端,移除轮询

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-15 17:33:31 +08:00
parent b804bd19a7
commit 65992eb35e
5 changed files with 327 additions and 134 deletions
+93 -112
View File
@@ -4,12 +4,10 @@
var wrapEl = chartEl && chartEl.parentElement;
var chart = null;
var currentPeriod = '15m';
var quoteTimer = null;
var klineTimer = null;
var klineSource = null;
var streamActive = false;
var reconnectTimer = null;
var lastData = null;
var klineLoading = false;
var FAST_PERIODS = ['timeshare', '1m', '2m', '5m', '15m', '1h', '2h', '4h'];
function getSymbol() {
var hidden = document.getElementById('market-symbol-hidden');
@@ -59,19 +57,6 @@
return false;
}
function klinePollMs() {
if (!isTradingSession()) return 0;
if (currentPeriod === 'timeshare' || FAST_PERIODS.indexOf(currentPeriod) >= 0) {
return 1000;
}
if (currentPeriod === 'd' || currentPeriod === 'w') return 30000;
return 5000;
}
function quotePollMs() {
return isTradingSession() ? 1000 : 10000;
}
function initChart() {
if (!chartEl || !window.echarts) return;
chart = echarts.init(chartEl);
@@ -254,9 +239,7 @@
}
function hideEmptyOverlay() {
if (emptyEl) {
emptyEl.style.display = '';
}
if (emptyEl) emptyEl.style.display = '';
if (wrapEl) wrapEl.classList.add('has-data');
}
@@ -272,66 +255,106 @@
var btn = document.getElementById('market-load-btn');
if (btn) {
btn.disabled = on;
btn.textContent = on ? '加载中…' : '查看';
}
if (on) {
showEmptyOverlay('加载中…');
} else if (lastData) {
hideEmptyOverlay();
btn.textContent = on ? '连接中…' : '查看';
}
if (on) showEmptyOverlay('连接中…');
else if (lastData) hideEmptyOverlay();
}
function updateRefreshHint() {
function updateRefreshHint(disconnected) {
var el = document.getElementById('market-refresh-hint');
if (!el) return;
if (!getSymbol()) {
el.textContent = '';
return;
}
if (disconnected) {
el.textContent = 'SSE 连接中断,正在重连…';
return;
}
if (!streamActive) {
el.textContent = '';
return;
}
var src = lastData && lastData.source === 'local' ? ' · 本地缓存' : '';
if (isTradingSession()) {
var ms = klinePollMs();
var src = lastData && lastData.source === 'local' ? ' · 本地缓存' : '';
el.textContent = ms === 1000
? '交易中 · 1秒刷新' + src
: '交易中 · 自动刷新' + src;
el.textContent = '交易中 · 后台刷新 · SSE 推送(约1秒)' + src;
} else {
el.textContent = '非交易时段 · 暂停高频刷新';
el.textContent = 'SSE 推送 · 非交易时段低频刷新' + src;
}
}
function loadKline(silent) {
function applyQuote(data) {
var priceEl = document.getElementById('market-quote-price');
var nameEl = document.getElementById('market-quote-name');
if (nameEl && data.name) nameEl.textContent = data.name + ' ' + (data.symbol || '');
if (priceEl) {
priceEl.textContent = data.price != null ? Number(data.price).toFixed(2) : '—';
}
}
function stopKlineStream() {
streamActive = false;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (klineSource) {
klineSource.close();
klineSource = null;
}
}
function scheduleReconnect() {
if (reconnectTimer) return;
updateRefreshHint(true);
reconnectTimer = setTimeout(function () {
reconnectTimer = null;
if (getSymbol()) startKlineStream(false);
}, 3000);
}
function startKlineStream(showLoading) {
stopKlineStream();
var symbol = getSymbol();
if (!symbol) {
if (!silent) alert('请先选择或输入合约代码');
alert('请先选择或输入合约代码');
return;
}
if (klineLoading) return;
klineLoading = true;
if (!silent) setLoading(true);
if (showLoading) setLoading(true);
var url = '/api/kline?symbol=' + encodeURIComponent(symbol) + '&period=' + encodeURIComponent(currentPeriod);
fetch(url)
.then(function (r) {
return r.json().then(function (j) { return { ok: r.ok, data: j }; });
})
.then(function (res) {
if (!res.ok) throw new Error(res.data.error || '加载失败');
var codes = getMarketCodes();
var q = 'symbol=' + encodeURIComponent(symbol) +
'&period=' + encodeURIComponent(currentPeriod);
if (codes.market_code) q += '&market_code=' + encodeURIComponent(codes.market_code);
if (codes.sina_code) q += '&sina_code=' + encodeURIComponent(codes.sina_code);
klineSource = new EventSource('/api/kline/stream?' + q);
streamActive = true;
updateRefreshHint(false);
klineSource.addEventListener('kline', function (e) {
try {
var data = JSON.parse(e.data);
if (!data.bars || !data.bars.length) return;
hideEmptyOverlay();
renderChart(res.data, silent);
updateQuoteMeta(res.data);
updateRefreshHint();
if (!quoteTimer) startQuotePoll();
if (!klineTimer) startKlinePoll();
})
.catch(function (err) {
if (!silent) {
showEmptyOverlay(err.message || '加载失败');
}
})
.finally(function () {
klineLoading = false;
if (!silent) setLoading(false);
});
renderChart(data, lastData !== null);
updateQuoteMeta(data);
updateRefreshHint(false);
setLoading(false);
} catch (err) { /* ignore */ }
});
klineSource.addEventListener('quote', function (e) {
try {
applyQuote(JSON.parse(e.data));
} catch (err) { /* ignore */ }
});
klineSource.onerror = function () {
stopKlineStream();
scheduleReconnect();
};
}
function updateQuoteMeta(data) {
@@ -341,52 +364,13 @@
}
var nameEl = document.getElementById('market-quote-name');
var hiddenName = document.getElementById('market-symbol-name');
if (nameEl) {
if (nameEl && !(nameEl.textContent && nameEl.textContent.trim())) {
nameEl.textContent = (hiddenName && hiddenName.value) || data.symbol || '—';
}
}
function loadQuote() {
var codes = getMarketCodes();
if (!codes.symbol) return;
var q = 'symbol=' + encodeURIComponent(codes.symbol);
if (codes.market_code) q += '&market_code=' + encodeURIComponent(codes.market_code);
if (codes.sina_code) q += '&sina_code=' + encodeURIComponent(codes.sina_code);
fetch('/api/market_quote?' + q)
.then(function (r) { return r.json(); })
.then(function (data) {
var priceEl = document.getElementById('market-quote-price');
var nameEl = document.getElementById('market-quote-name');
if (nameEl && data.name) nameEl.textContent = data.name + ' ' + (data.symbol || '');
if (priceEl) {
priceEl.textContent = data.price != null ? Number(data.price).toFixed(2) : '—';
}
})
.catch(function () { /* ignore */ });
}
function startQuotePoll() {
if (quoteTimer) clearInterval(quoteTimer);
loadQuote();
var ms = quotePollMs();
if (ms > 0) quoteTimer = setInterval(loadQuote, ms);
}
function startKlinePoll() {
if (klineTimer) clearInterval(klineTimer);
var ms = klinePollMs();
if (ms > 0 && getSymbol()) {
klineTimer = setInterval(function () {
loadKline(true);
updateRefreshHint();
}, ms);
}
}
function restartPollers() {
startQuotePoll();
startKlinePoll();
updateRefreshHint();
function loadKline(showLoading) {
startKlineStream(showLoading);
}
function shiftDataZoom(delta) {
@@ -421,8 +405,7 @@
tabs.querySelectorAll('.period-tab').forEach(function (el) { el.classList.remove('active'); });
btn.classList.add('active');
currentPeriod = btn.getAttribute('data-period') || '15m';
restartPollers();
if (getSymbol()) loadKline(false);
if (getSymbol()) loadKline(true);
});
}
@@ -444,19 +427,17 @@
if (active) currentPeriod = active.getAttribute('data-period') || '15m';
var loadBtn = document.getElementById('market-load-btn');
if (loadBtn) loadBtn.addEventListener('click', function () {
restartPollers();
loadKline(false);
});
if (loadBtn) loadBtn.addEventListener('click', function () { loadKline(true); });
var hidden = document.getElementById('market-symbol-hidden');
var input = document.getElementById('market-symbol-input');
if (hidden && hidden.value) {
if (input && !input.value) input.value = hidden.value;
restartPollers();
loadKline(false);
loadKline(true);
} else {
updateRefreshHint();
updateRefreshHint(false);
}
window.addEventListener('beforeunload', stopKlineStream);
});
})();