Use Sina-only market K-lines and editable admin login synced to .env.

Market page uses Sina for quotes and bars with an auto-follow toggle and incremental chart updates while panning. Settings lets users change username and password, persisting to the database and .env.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 13:53:12 +08:00
parent 6905373401
commit 382a9a0e14
9 changed files with 324 additions and 75 deletions
+102 -21
View File
@@ -20,9 +20,11 @@
var streamActive = false;
var reconnectTimer = null;
var lastData = null;
var lastRenderedPrepared = null;
var lastPrevClose = null;
var chartOpts = { prevClose: false, ma: false, gapDay: false };
var followingLatest = true;
var autoFollow = true;
var DEFAULT_VISIBLE_BARS = 80;
var PERIOD_SECONDS = {
@@ -163,6 +165,7 @@
ma55Series = null;
prevCloseLine = null;
currentChartMode = '';
lastRenderedPrepared = null;
}
function buildChart(mode) {
@@ -313,20 +316,58 @@
});
}
function renderChart(data, preserveRange) {
if (!chartEl || !window.LightweightCharts) return;
lastData = data;
if (data.prev_close != null) lastPrevClose = data.prev_close;
function shouldPreserveView() {
return !autoFollow || !followingLatest;
}
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
var mode = isLine ? 'line' : 'candle';
if (!chart || currentChartMode !== mode) buildChart(mode);
if (!chart) return;
function applyBarUpdate(bar, mode, prepared) {
if (mode === 'line') {
areaSeries.update({ time: bar.time, value: bar.close });
return;
}
candleSeries.update({
time: bar.time,
open: bar.open,
high: bar.high,
low: bar.low,
close: bar.close,
});
var up = bar.close >= bar.open;
var c = themeColors();
volumeSeries.update({
time: bar.time,
value: bar.volume,
color: up ? c.up : c.down,
});
if (chartOpts.ma && ma21Series && ma55Series && prepared && prepared.length) {
var ma21 = calcMA(21, prepared);
var ma55 = calcMA(55, prepared);
if (ma21.length) ma21Series.update(ma21[ma21.length - 1]);
if (ma55.length) ma55Series.update(ma55[ma55.length - 1]);
}
}
var prepared = prepareBars(data.bars || [], data.period || currentPeriod);
data.preparedBars = prepared;
if (!prepared.length) return;
function tryIncrementalUpdate(prepared, mode) {
if (!lastRenderedPrepared || !prepared.length) return false;
var prev = lastRenderedPrepared;
var prevLast = prev[prev.length - 1];
var newLast = prepared[prepared.length - 1];
if (prepared.length === prev.length && newLast.time === prevLast.time) {
applyBarUpdate(newLast, mode, prepared);
return true;
}
if (prepared.length === prev.length + 1 && prepared[prepared.length - 2].time === prevLast.time) {
applyBarUpdate(newLast, mode, prepared);
if (autoFollow && followingLatest) {
setVisibleRange(prepared, true);
}
return true;
}
return false;
}
function renderChartFull(prepared, data, mode, preserveRange) {
if (mode === 'line') {
areaSeries.setData(prepared.map(function (b) {
return { time: b.time, value: b.close };
@@ -350,8 +391,33 @@
}
applyPrevCloseLine(lastPrevClose != null ? lastPrevClose : data.prev_close);
}
if (!shouldPreserveView()) {
setVisibleRange(prepared, !!preserveRange);
}
}
setVisibleRange(prepared, !!preserveRange);
function renderChart(data, options) {
options = options || {};
if (!chartEl || !window.LightweightCharts) return;
lastData = data;
if (data.prev_close != null) lastPrevClose = data.prev_close;
var isLine = data.chart_type === 'line' || data.period === 'timeshare';
var mode = isLine ? 'line' : 'candle';
if (!chart || currentChartMode !== mode) buildChart(mode);
if (!chart) return;
var prepared = prepareBars(data.bars || [], data.period || currentPeriod);
data.preparedBars = prepared;
if (!prepared.length) return;
if (!options.forceFull && shouldPreserveView() && tryIncrementalUpdate(prepared, mode)) {
lastRenderedPrepared = prepared;
return;
}
renderChartFull(prepared, data, mode, options.preserveRange);
lastRenderedPrepared = prepared;
}
function periodLabel(key) {
@@ -388,8 +454,6 @@
}
function klineSourceLabel(src) {
if (src === 'ctp') return 'CTP';
if (src === 'ctp+remote') return '新浪+CTP';
if (src === 'local') return '本地缓存';
return '新浪';
}
@@ -414,9 +478,9 @@
src = ' · ' + klineSourceLabel(lastData.source);
}
if (isTradingSession()) {
el.textContent = 'TradingView 图表 · 交易中 SSE 推送' + src;
el.textContent = '新浪数据 · 交易中 SSE 推送' + src;
} else {
el.textContent = 'TradingView 图表 · 非交易时段低频刷新' + src;
el.textContent = '新浪数据 · 非交易时段低频刷新' + src;
}
}
@@ -495,7 +559,7 @@
var data = JSON.parse(e.data);
if (!data.bars || !data.bars.length) return;
hideEmptyOverlay();
renderChart(data, lastData !== null);
renderChart(data, { preserveRange: lastData !== null });
updateQuoteMeta(data);
if (data.prev_close != null) updatePrevCloseDisplay(data.prev_close);
updateRefreshHint(false);
@@ -522,7 +586,7 @@
if (data.count) parts.push('共 ' + data.count + ' 根 · ' + periodLabel(data.period));
if (data.source) parts.push('K线 ' + klineSourceLabel(data.source));
if (data.quote_source) {
parts.push('报价 ' + (data.quote_source === 'ctp' ? 'CTP' : '新浪'));
parts.push('报价 新浪');
}
meta.textContent = parts.join(' · ');
}
@@ -580,6 +644,21 @@
if (zoomReset) zoomReset.addEventListener('click', resetDataZoom);
}
function bindAutoButton() {
var btn = document.getElementById('market-auto-btn');
if (!btn) return;
btn.addEventListener('click', function () {
autoFollow = !autoFollow;
btn.classList.toggle('is-active', autoFollow);
if (autoFollow) {
followingLatest = true;
if (lastData && lastData.preparedBars) {
setVisibleRange(lastData.preparedBars, false);
}
}
});
}
function bindChartOptions() {
var prevCb = document.getElementById('chart-opt-prev-close');
var maCb = document.getElementById('chart-opt-ma');
@@ -597,7 +676,7 @@
chartOpts.ma = maCb.checked;
if (lastData) {
destroyChart();
renderChart(lastData, false);
renderChart(lastData, { forceFull: true });
}
});
}
@@ -605,7 +684,7 @@
gapCb.addEventListener('change', function () {
chartOpts.gapDay = gapCb.checked;
followingLatest = true;
if (lastData) renderChart(lastData, false);
if (lastData) renderChart(lastData, { forceFull: true });
});
}
}
@@ -617,13 +696,14 @@
}
bindPeriodTabs();
bindZoomButtons();
bindAutoButton();
bindChartOptions();
document.addEventListener('click', function (e) {
if (e.target.closest('[data-theme-pick]') && lastData) {
setTimeout(function () {
destroyChart();
renderChart(lastData, false);
renderChart(lastData, { forceFull: true });
}, 80);
}
});
@@ -640,6 +720,7 @@
input.addEventListener('symbol-selected', function () {
lastPrevClose = null;
lastData = null;
lastRenderedPrepared = null;
destroyChart();
updatePrevCloseDisplay(null);
loadKline(true);