fix(web): 分析/自动刷新后保留 K 线视窗位置
拆分手动分析与自动刷新拉数路径;全量重建用 logical 优先恢复视窗, 增量 recent 用 scroll+barDelta;避免 barSpacing 重锚与重复冻结导致往右跳。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -12,12 +12,8 @@ function updateChartDisplay() {
|
||||
_lastKlinePeriod = curPeriod;
|
||||
|
||||
// 保存当前的可见范围(周期切换时不保留,避免范围越界)
|
||||
if (!periodChanged && tvWidget && tvWidget.mainChart) {
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
} catch (e) {
|
||||
window._pendingRestoreView = null;
|
||||
}
|
||||
if (!periodChanged && typeof snapshotPendingChartViewport === 'function') {
|
||||
snapshotPendingChartViewport();
|
||||
}
|
||||
|
||||
console.log('更新图表显示');
|
||||
|
||||
@@ -113,7 +113,6 @@ function updateTradingViewData(options) {
|
||||
});
|
||||
|
||||
const newBarCount = candles.length;
|
||||
const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0;
|
||||
const firstBarTime = newBarCount > 0 ? candles[0].time : null;
|
||||
const lastBarTime = newBarCount > 0 ? candles[newBarCount - 1].time : null;
|
||||
const clampedVisibleRange = clampVisibleRangeToBarTimes(savedVisibleRange, firstBarTime, lastBarTime);
|
||||
@@ -336,58 +335,23 @@ function updateTradingViewData(options) {
|
||||
].filter(Boolean);
|
||||
|
||||
const vr = clampedVisibleRange || savedVisibleRange;
|
||||
const lr = savedLogicalRange;
|
||||
const savedScroll = savedScrollPosition;
|
||||
const mainChartRef = tvWidget.mainChart;
|
||||
const viewSnap = {
|
||||
logicalRange: savedLogicalRange,
|
||||
visibleRange: vr,
|
||||
scrollPosition: savedScrollPosition
|
||||
};
|
||||
|
||||
const applyPosition = function (tag) {
|
||||
let ok = false;
|
||||
// scrollToPosition 最稳;setVisibleRange 的 to 常含右侧空白会锚到最右
|
||||
if (typeof savedScroll === 'number' && mainChartRef) {
|
||||
try {
|
||||
const pos = savedScroll + (barDelta || 0);
|
||||
mainChartRef.timeScale().scrollToPosition(pos, false);
|
||||
const lrNow = mainChartRef.timeScale().getVisibleLogicalRange();
|
||||
if (lrNow) {
|
||||
charts.forEach(c => {
|
||||
try { c.timeScale().setVisibleLogicalRange(lrNow); } catch (e) {}
|
||||
});
|
||||
ok = true;
|
||||
console.log('🔄 恢复位置 scroll' + (tag || '') + ':', pos);
|
||||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
if (!ok && lr && lr.from !== undefined && lr.to !== undefined && newBarCount > 0) {
|
||||
const span = Math.max(1, lr.to - lr.from);
|
||||
let to = lr.to;
|
||||
let from = lr.from;
|
||||
const maxTo = newBarCount - 1 + 8;
|
||||
if (to > maxTo) {
|
||||
to = maxTo;
|
||||
from = to - span;
|
||||
}
|
||||
if (from < -8) {
|
||||
from = -8;
|
||||
to = from + span;
|
||||
}
|
||||
const clamped = { from: from, to: to };
|
||||
charts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleLogicalRange(clamped);
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
if (ok) console.log('🔄 恢复位置 logical' + (tag || '') + ':', clamped);
|
||||
}
|
||||
if (!ok && vr && vr.from !== undefined && vr.to !== undefined) {
|
||||
charts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleRange(vr);
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
if (ok) console.log('🔄 恢复位置 time' + (tag || '') + ':', vr);
|
||||
}
|
||||
if (typeof restoreChartViewState !== 'function') return;
|
||||
restoreChartViewState(charts, viewSnap, {
|
||||
incremental: true,
|
||||
skipBarSpacing: true,
|
||||
oldBarCount: oldBarCount,
|
||||
newBarCount: newBarCount,
|
||||
firstBarTime: firstBarTime,
|
||||
lastBarTime: lastBarTime
|
||||
});
|
||||
if (tag) console.log('🔄 恢复位置' + tag);
|
||||
};
|
||||
|
||||
const finishPreserve = function () {
|
||||
@@ -404,8 +368,18 @@ function updateTradingViewData(options) {
|
||||
applyPosition('');
|
||||
setTimeout(function () { applyPosition('@0'); }, 0);
|
||||
setTimeout(function () { applyPosition('@50'); }, 50);
|
||||
// 增量 setData 常不触发可见时间范围回调,但价格轴会变:补刷分型竖边
|
||||
var bumpFxVert = function () {
|
||||
if (typeof window._redrawFxBoxVerticalOverlay === 'function') {
|
||||
window._redrawFxBoxVerticalOverlay();
|
||||
}
|
||||
};
|
||||
bumpFxVert();
|
||||
setTimeout(bumpFxVert, 0);
|
||||
setTimeout(bumpFxVert, 50);
|
||||
setTimeout(function () {
|
||||
applyPosition('@150');
|
||||
bumpFxVert();
|
||||
finishPreserve();
|
||||
}, 150);
|
||||
} else {
|
||||
|
||||
@@ -66,9 +66,7 @@ function chartTvFinalize(ctx) {
|
||||
const allChartsNow = [mainChart, volumeChart, atrChart]
|
||||
.concat(showMacd && macdChart ? [macdChart] : [])
|
||||
.concat(showMacd && chanMacdChart ? [chanMacdChart] : []);
|
||||
if (hasPendingRestoreView && pendingView) {
|
||||
restoreChartViewState(allChartsNow, pendingView, { preferTime: true });
|
||||
} else {
|
||||
if (!hasPendingRestoreView || !pendingView) {
|
||||
// 显示最近 200 根K线而非全部挤压(避免K线过多时重叠)
|
||||
if (totalBars > visibleBarsCount) {
|
||||
const rangeFrom = totalBars - visibleBarsCount;
|
||||
@@ -81,8 +79,7 @@ function chartTvFinalize(ctx) {
|
||||
|
||||
// 立即同步其他图表到主图表的范围(无 pending 时)
|
||||
setTimeout(() => {
|
||||
if (window._pendingRestoreView) {
|
||||
restoreChartViewState(allChartsNow, window._pendingRestoreView, { preferTime: true });
|
||||
if (pendingView) {
|
||||
return;
|
||||
}
|
||||
const logRange = mainChart.timeScale().getVisibleLogicalRange();
|
||||
@@ -189,6 +186,9 @@ function chartTvFinalize(ctx) {
|
||||
updateIndicatorPanel();
|
||||
|
||||
// 绑定同步事件
|
||||
if (hasPendingRestoreView && pendingView) {
|
||||
window._preserveViewDuringUpdate = true;
|
||||
}
|
||||
bindSyncEvents(mainChartContainer, volumeChartContainer, atrChartContainer, macdChartContainer, chanMacdChartContainer, mainChart, volumeChart, atrChart, macdChart, chanMacdChart, showMacd);
|
||||
|
||||
// 最终确保所有图表时间轴对齐(同时恢复刷新前保存的缩放/位置)
|
||||
@@ -198,13 +198,20 @@ function chartTvFinalize(ctx) {
|
||||
if (showMacd && chanMacdChart) allCharts.push(chanMacdChart);
|
||||
|
||||
// 检查是否有待恢复的视图(缩放 + 位置)
|
||||
const pending = window._pendingRestoreView;
|
||||
const pending = window._pendingRestoreView || pendingView;
|
||||
window._pendingRestoreView = null;
|
||||
|
||||
if (pending) {
|
||||
// 恢复刷新前的缩放和位置(时间范围优先,避免数据滑动后逻辑索引错位)
|
||||
const firstT = candles && candles.length ? candles[0].time : null;
|
||||
const lastT = candles && candles.length ? candles[candles.length - 1].time : null;
|
||||
console.log('📌 恢复图表视图:', JSON.stringify(pending));
|
||||
restoreChartViewState(allCharts, pending, { preferTime: true });
|
||||
restoreChartViewState(allCharts, pending, {
|
||||
firstBarTime: firstT,
|
||||
lastBarTime: lastT,
|
||||
oldBarCount: window._preserveViewBarCount || 0,
|
||||
newBarCount: totalBars
|
||||
});
|
||||
window._preserveViewBarCount = 0;
|
||||
} else {
|
||||
// 无保存视图,正常同步主图到子图
|
||||
const visibleRange = mainChart.timeScale().getVisibleRange();
|
||||
@@ -218,6 +225,7 @@ function chartTvFinalize(ctx) {
|
||||
});
|
||||
}
|
||||
}
|
||||
window._preserveViewDuringUpdate = false;
|
||||
console.log('🔧 最终时间轴对齐完成');
|
||||
}, 150);
|
||||
// 只有在时间输入框都为空时才设置图表默认时间范围
|
||||
|
||||
@@ -106,6 +106,32 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
}
|
||||
mainChartContainer.appendChild(canvas);
|
||||
}
|
||||
var lastSig = '';
|
||||
var watchRaf = null;
|
||||
var cleaned = false;
|
||||
var redrawPending = false;
|
||||
var quant = function (v) {
|
||||
if (v == null || !isFinite(Number(v))) return 'n';
|
||||
return String(Math.round(Number(v)));
|
||||
};
|
||||
// LWC 4 无 priceScale 订阅:采样坐标变化(含增量 setData 后自动缩放)
|
||||
var sampleSig = function () {
|
||||
var boxes = window._fxBoxVerticals || [];
|
||||
var series = getMainPriceSeries();
|
||||
if (!series || !boxes.length) return '0';
|
||||
var ts = mainChart.timeScale();
|
||||
var a = boxes[0];
|
||||
var b = boxes[boxes.length - 1];
|
||||
return [
|
||||
boxes.length,
|
||||
quant(ts.timeToCoordinate(a.time)),
|
||||
quant(series.priceToCoordinate(a.hi)),
|
||||
quant(series.priceToCoordinate(a.lo)),
|
||||
quant(ts.timeToCoordinate(b.time)),
|
||||
quant(series.priceToCoordinate(b.hi)),
|
||||
quant(series.priceToCoordinate(b.lo))
|
||||
].join('|');
|
||||
};
|
||||
var redraw = function () {
|
||||
var boxes = window._fxBoxVerticals || [];
|
||||
var series = getMainPriceSeries();
|
||||
@@ -119,7 +145,10 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
if (!ctx2) return;
|
||||
ctx2.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx2.clearRect(0, 0, rect.width, rect.height);
|
||||
if (!series || !boxes.length) return;
|
||||
if (!series || !boxes.length) {
|
||||
lastSig = sampleSig();
|
||||
return;
|
||||
}
|
||||
var ts = mainChart.timeScale();
|
||||
for (var i = 0; i < boxes.length; i++) {
|
||||
var box = boxes[i];
|
||||
@@ -136,25 +165,48 @@ function syncFxBoxVerticalOverlay(mainChart, mainChartContainer) {
|
||||
ctx2.stroke();
|
||||
}
|
||||
ctx2.setLineDash([]);
|
||||
lastSig = sampleSig();
|
||||
};
|
||||
var onRange = function () { requestAnimationFrame(redraw); };
|
||||
try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(onRange); } catch (e) {}
|
||||
try { mainChart.timeScale().subscribeVisibleTimeRangeChange(onRange); } catch (e) {}
|
||||
var scheduleRedraw = function () {
|
||||
if (cleaned || redrawPending) return;
|
||||
redrawPending = true;
|
||||
requestAnimationFrame(function () {
|
||||
redrawPending = false;
|
||||
if (!cleaned) redraw();
|
||||
});
|
||||
};
|
||||
var watch = function () {
|
||||
if (cleaned) return;
|
||||
watchRaf = requestAnimationFrame(watch);
|
||||
var sig = sampleSig();
|
||||
if (sig !== lastSig) scheduleRedraw();
|
||||
};
|
||||
try { mainChart.timeScale().subscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
|
||||
try { mainChart.timeScale().subscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
|
||||
var ro = null;
|
||||
if (typeof ResizeObserver !== 'undefined') {
|
||||
ro = new ResizeObserver(onRange);
|
||||
ro = new ResizeObserver(scheduleRedraw);
|
||||
ro.observe(mainChartContainer);
|
||||
}
|
||||
window._redrawFxBoxVerticalOverlay = scheduleRedraw;
|
||||
window._fxBoxOverlayCleanup = function () {
|
||||
try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(onRange); } catch (e) {}
|
||||
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(onRange); } catch (e) {}
|
||||
if (cleaned) return;
|
||||
cleaned = true;
|
||||
if (watchRaf != null) {
|
||||
try { cancelAnimationFrame(watchRaf); } catch (e) {}
|
||||
watchRaf = null;
|
||||
}
|
||||
window._redrawFxBoxVerticalOverlay = null;
|
||||
try { mainChart.timeScale().unsubscribeVisibleLogicalRangeChange(scheduleRedraw); } catch (e) {}
|
||||
try { mainChart.timeScale().unsubscribeVisibleTimeRangeChange(scheduleRedraw); } catch (e) {}
|
||||
if (ro) try { ro.disconnect(); } catch (e) {}
|
||||
try { if (canvas && canvas.parentNode) canvas.parentNode.removeChild(canvas); } catch (e) {}
|
||||
};
|
||||
if (!window._tvInitCleanups) window._tvInitCleanups = [];
|
||||
window._tvInitCleanups.push(window._fxBoxOverlayCleanup);
|
||||
requestAnimationFrame(redraw);
|
||||
setTimeout(redraw, 50);
|
||||
scheduleRedraw();
|
||||
setTimeout(scheduleRedraw, 50);
|
||||
watchRaf = requestAnimationFrame(watch);
|
||||
}
|
||||
|
||||
function chartTvRenderOverlays(ctx) {
|
||||
@@ -2137,28 +2189,11 @@ function chartTvRenderOverlays(ctx) {
|
||||
});
|
||||
safeOverlayLineSetData(bottomSeries, [{ time: startTs, value: boxLow }, { time: endTs, value: boxLow }]);
|
||||
|
||||
const leftSeries = mainChart.addLineSeries({
|
||||
color: boxColor,
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
});
|
||||
safeOverlayLineSetData(leftSeries, [{ time: startTs, value: boxLow }, { time: startTs, value: boxHigh }]);
|
||||
|
||||
const rightSeries = mainChart.addLineSeries({
|
||||
color: boxColor,
|
||||
lineWidth: 1,
|
||||
lineStyle: 2,
|
||||
lastValueVisible: false,
|
||||
priceLineVisible: false,
|
||||
crosshairMarkerVisible: false,
|
||||
});
|
||||
safeOverlayLineSetData(rightSeries, [{ time: endTs, value: boxLow }, { time: endTs, value: boxHigh }]);
|
||||
pushFxBoxVertical(startTs, boxLow, boxHigh, boxColor);
|
||||
pushFxBoxVertical(endTs, boxLow, boxHigh, boxColor);
|
||||
|
||||
if (!tvWidget.series.subSubKlcFxBoxSeries) tvWidget.series.subSubKlcFxBoxSeries = [];
|
||||
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries, leftSeries, rightSeries);
|
||||
tvWidget.series.subSubKlcFxBoxSeries.push(topSeries, bottomSeries);
|
||||
}
|
||||
}
|
||||
} catch (e) { console.error('绘制次次周期KLC分型标记出错:', e); }
|
||||
@@ -2461,4 +2496,11 @@ function chartTvRenderOverlays(ctx) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KLC 分型框竖边:canvas 真竖线(LWC 折线做不到不斜)
|
||||
try {
|
||||
syncFxBoxVerticalOverlay(mainChart, mainChartContainer);
|
||||
} catch (e) {
|
||||
console.warn('分型竖边 overlay 失败:', e);
|
||||
}
|
||||
}
|
||||
|
||||
+296
-172
@@ -1,4 +1,4 @@
|
||||
/* chart_view.js — split from chart.js */
|
||||
/* chart_view.js — 手动分析 / 自动刷新 两套独立拉数逻辑 */
|
||||
|
||||
/** 用尾部 N 根合并进已有 K 线(同 timestamp 覆盖,更新则追加) */
|
||||
function mergeKlineTail(existing, incoming) {
|
||||
@@ -41,12 +41,29 @@ function mergeKlineTail(existing, incoming) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function updateChart(options) {
|
||||
options = options || {};
|
||||
// 只显示旋转加载图标
|
||||
$('#refreshLoadingSpinner').show();
|
||||
|
||||
// 获取参数
|
||||
/** 实时基线是否过旧(仅自动刷新用来决定 recent vs 全量 live) */
|
||||
function isLiveBaselineStale(timeframe) {
|
||||
if (!currentData || !Array.isArray(currentData.kline_data) || !currentData.kline_data.length) {
|
||||
return true;
|
||||
}
|
||||
const last = currentData.kline_data[currentData.kline_data.length - 1];
|
||||
let lastMs;
|
||||
if (last.timestamp != null && last.timestamp !== '') {
|
||||
lastMs = Number(last.timestamp);
|
||||
} else {
|
||||
lastMs = new Date(last.date).getTime();
|
||||
}
|
||||
if (Number.isNaN(lastMs)) return true;
|
||||
const tfMs = (window.timeframeToMs && window.timeframeToMs(timeframe)) || (15 * 60 * 1000);
|
||||
return (Date.now() - lastMs) > tfMs * 3;
|
||||
}
|
||||
|
||||
/** 兼容旧名 */
|
||||
function isChartBaselineStale(timeframe) {
|
||||
return isLiveBaselineStale(timeframe);
|
||||
}
|
||||
|
||||
function readChartFormContext() {
|
||||
const dataSource = $('#dataSource').val() || 'crypto';
|
||||
let symbol;
|
||||
if (dataSource === 'crypto') {
|
||||
@@ -54,91 +71,194 @@ function updateChart(options) {
|
||||
} else {
|
||||
symbol = $('#astockSymbol').val() || '000001';
|
||||
}
|
||||
|
||||
const timeframe = $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h';
|
||||
const timezone = $('#timezone').val() || 'Asia/Shanghai';
|
||||
const elementTimeframe = $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m';
|
||||
const subSubTimeframe = $('#subSubTimeframe').val() || '';
|
||||
|
||||
// 确保时区参数有效
|
||||
console.log('更新图表使用时区:', timezone, 'reason:', options.reason || (options.fromAutoRefresh ? 'auto' : 'manual'));
|
||||
console.log('数据源:', dataSource, '交易对/股票:', symbol);
|
||||
|
||||
// 如果symbol为空,不发送请求
|
||||
if (!symbol) {
|
||||
return {
|
||||
dataSource: dataSource,
|
||||
symbol: symbol,
|
||||
timeframe: $('#timeframe').val() || window.DEFAULT_MAIN_TIMEFRAME || '4h',
|
||||
timezone: $('#timezone').val() || 'Asia/Shanghai',
|
||||
elementTimeframe: $('#elementTimeframe').val() || window.DEFAULT_ELEMENT_TIMEFRAME || '1m',
|
||||
subSubTimeframe: $('#subSubTimeframe').val() || '',
|
||||
startTimeMs: $('#start_time').val() ? new Date($('#start_time').val()).getTime() : null,
|
||||
endTimeMs: $('#end_time').val() ? new Date($('#end_time').val()).getTime() : null
|
||||
};
|
||||
}
|
||||
|
||||
/** 当前展示用 K 线序列根数(主/小/次次周期与 freeze 逻辑一致) */
|
||||
function getActiveKlineBarCount(data) {
|
||||
data = data || (typeof currentData !== 'undefined' ? currentData : null);
|
||||
if (!data) return 0;
|
||||
const series = ($('#subSubPeriodKline').is(':checked') && data.sub_sub_kline_data) ||
|
||||
($('#elementPeriodKline').is(':checked') && data.element_kline_data) ||
|
||||
data.kline_data;
|
||||
return Array.isArray(series) ? series.length : 0;
|
||||
}
|
||||
|
||||
/** 全量 init 前拍快照(周期切换等;不含 _preserveViewOnRefresh) */
|
||||
function snapshotPendingChartViewport() {
|
||||
if (!tvWidget || !tvWidget.mainChart || typeof captureChartViewState !== 'function') {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
window._preserveViewBarCount = getActiveKlineBarCount();
|
||||
} catch (e) {
|
||||
window._pendingRestoreView = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function freezeChartViewportBeforeRequest() {
|
||||
try {
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
const snap = captureChartViewState(tvWidget.mainChart);
|
||||
window._preserveViewOnRefresh = snap;
|
||||
// 全量 initTradingView 只认 _pendingRestoreView,须与增量冻结同步
|
||||
window._pendingRestoreView = snap;
|
||||
window._preserveViewBarCount = getActiveKlineBarCount();
|
||||
console.log('📌 刷新前冻结视窗 bars=', window._preserveViewBarCount, snap);
|
||||
}
|
||||
} catch (e) {
|
||||
window._preserveViewOnRefresh = null;
|
||||
window._pendingRestoreView = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function abortInFlightChartRequest() {
|
||||
if (window._analyzeXhr && window._analyzeXhr.readyState !== 4) {
|
||||
try { window._analyzeXhr.abort(); } catch (e) {}
|
||||
}
|
||||
}
|
||||
|
||||
function applyAnalyzeSuccess(data, symbol, options) {
|
||||
options = options || {};
|
||||
const prevSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || '';
|
||||
if (currentData) {
|
||||
delete currentData.original_kline_data;
|
||||
delete currentData.original_macd;
|
||||
}
|
||||
currentData = data;
|
||||
window._lastChartSymbol = symbol;
|
||||
window._lastFullAnalyzeAt = Date.now();
|
||||
if (typeof renderWyckoffCycleSummary === 'function') {
|
||||
renderWyckoffCycleSummary();
|
||||
}
|
||||
|
||||
const ready = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
|
||||
const structureZonesOn = $('#showMainStructureZone').is(':checked');
|
||||
const symbolChanged = !!(prevSymbol && prevSymbol !== symbol);
|
||||
let wantIncremental = options.incremental !== undefined ? !!options.incremental : ready;
|
||||
if (structureZonesOn || options.forceFullRebuild || symbolChanged || options.incremental === false) {
|
||||
wantIncremental = false;
|
||||
}
|
||||
refreshChart(data, { incremental: wantIncremental });
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动分析(按钮 / 首屏 / 切换参数)
|
||||
* - 严格使用表单 start_time / end_time
|
||||
* - 只走 /api/analyze,不做 recent 合并,不改结束时间为现在
|
||||
*/
|
||||
function analyzeChart(options) {
|
||||
options = options || {};
|
||||
$('#refreshLoadingSpinner').show();
|
||||
|
||||
const ctx = readChartFormContext();
|
||||
console.log('手动分析:', ctx.symbol, ctx.timeframe, 'range', ctx.startTimeMs, '→', ctx.endTimeMs, options.reason || '');
|
||||
|
||||
if (!ctx.symbol) {
|
||||
console.error('交易对/股票代码不能为空');
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`更新图表: symbol=${symbol}, timeframe=${timeframe}, elementTimeframe=${elementTimeframe}, timezone=${timezone}`);
|
||||
|
||||
// 获取开始和结束时间(如果已设置)
|
||||
let startTimeMs = null;
|
||||
let endTimeMs = null;
|
||||
|
||||
if ($('#start_time').val()) {
|
||||
startTimeMs = new Date($('#start_time').val()).getTime();
|
||||
}
|
||||
|
||||
if ($('#end_time').val()) {
|
||||
endTimeMs = new Date($('#end_time').val()).getTime();
|
||||
}
|
||||
|
||||
// 自动刷新:取消进行中的上一请求,避免响应堆积
|
||||
if (options.fromAutoRefresh && window._analyzeXhr && window._analyzeXhr.readyState !== 4) {
|
||||
try { window._analyzeXhr.abort(); } catch (e) {}
|
||||
}
|
||||
|
||||
// 请求发出前冻结视窗(与自动刷新同一套;避免等响应时/setData 后 logical 索引漂移)
|
||||
try {
|
||||
if (tvWidget && tvWidget.mainChart) {
|
||||
window._preserveViewOnRefresh = captureChartViewState(tvWidget.mainChart);
|
||||
const prev = currentData && (
|
||||
($('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data) ||
|
||||
($('#elementPeriodKline').is(':checked') && currentData.element_kline_data) ||
|
||||
currentData.kline_data
|
||||
);
|
||||
window._preserveViewBarCount = Array.isArray(prev) ? prev.length : 0;
|
||||
console.log('📌 刷新前冻结视窗 bars=', window._preserveViewBarCount, window._preserveViewOnRefresh);
|
||||
}
|
||||
} catch (e) {
|
||||
window._preserveViewOnRefresh = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
}
|
||||
|
||||
abortInFlightChartRequest();
|
||||
freezeChartViewportBeforeRequest();
|
||||
const requestId = ++lastRequestId;
|
||||
|
||||
window._analyzeXhr = $.ajax({
|
||||
url: '/api/analyze',
|
||||
data: {
|
||||
symbol: ctx.symbol,
|
||||
timeframe: ctx.timeframe,
|
||||
timezone: ctx.timezone,
|
||||
element_timeframe: ctx.elementTimeframe,
|
||||
sub_sub_timeframe: ctx.subSubTimeframe || undefined,
|
||||
start_time: ctx.startTimeMs,
|
||||
end_time: ctx.endTimeMs,
|
||||
elements_only: false,
|
||||
zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000,
|
||||
include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0
|
||||
},
|
||||
success: function(data) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (requestId !== lastRequestId) return;
|
||||
applyAnalyzeSuccess(data, ctx.symbol, {
|
||||
incremental: options.incremental !== undefined ? options.incremental : false,
|
||||
forceFullRebuild: true
|
||||
});
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (textStatus === 'abort') return;
|
||||
console.error('分析失败:', errorThrown);
|
||||
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动刷新(仅启用自动刷新时)
|
||||
* - 结束时间固定为当前时间(调用方先 updateEndTimeToNow)
|
||||
* - mode=recent:/api/klines/recent 增量合并(不重算缠论)
|
||||
* - mode=full:/api/analyze 全量 live(start 用表单,end=现在)
|
||||
*/
|
||||
function autoRefreshChart(options) {
|
||||
options = options || {};
|
||||
const mode = options.mode === 'full' ? 'full' : 'recent';
|
||||
$('#refreshLoadingSpinner').show();
|
||||
|
||||
const ctx = readChartFormContext();
|
||||
console.log('自动刷新:', mode, ctx.symbol, ctx.timeframe, 'end=', ctx.endTimeMs);
|
||||
|
||||
if (!ctx.symbol) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
return;
|
||||
}
|
||||
|
||||
abortInFlightChartRequest();
|
||||
freezeChartViewportBeforeRequest();
|
||||
const requestId = ++lastRequestId;
|
||||
|
||||
const chartsReady = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
|
||||
const hasBaseline = !!(currentData && Array.isArray(currentData.kline_data) && currentData.kline_data.length);
|
||||
const baselineSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || '';
|
||||
// 自动刷新常态:只拉最近 2 根;换币对后基线不一致则禁止尾部合并(否则会叠旧缠论)
|
||||
// fullAnalyze(约每 1 分钟)走全量 analyze 更新缠论
|
||||
const useRecentTail = !!(
|
||||
options.fromAutoRefresh &&
|
||||
!options.fullAnalyze &&
|
||||
const canRecent = !!(
|
||||
mode === 'recent' &&
|
||||
chartsReady &&
|
||||
hasBaseline &&
|
||||
baselineSymbol &&
|
||||
baselineSymbol === symbol
|
||||
baselineSymbol === ctx.symbol &&
|
||||
!isLiveBaselineStale(ctx.timeframe)
|
||||
);
|
||||
|
||||
if (useRecentTail) {
|
||||
if (canRecent) {
|
||||
console.log('自动刷新 → /api/klines/recent limit=2');
|
||||
window._analyzeXhr = $.ajax({
|
||||
url: '/api/klines/recent',
|
||||
data: {
|
||||
symbol: symbol,
|
||||
timeframe: timeframe,
|
||||
symbol: ctx.symbol,
|
||||
timeframe: ctx.timeframe,
|
||||
limit: 2,
|
||||
element_timeframe: elementTimeframe || undefined,
|
||||
sub_sub_timeframe: subSubTimeframe || undefined
|
||||
element_timeframe: ctx.elementTimeframe || undefined,
|
||||
sub_sub_timeframe: ctx.subSubTimeframe || undefined
|
||||
},
|
||||
success: function(partial) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (requestId !== lastRequestId) return;
|
||||
if (!partial || !Array.isArray(partial.kline_data)) {
|
||||
console.warn('recent 响应无效,回退全量 analyze');
|
||||
updateChart({ incremental: true, reason: 'recent-fallback' });
|
||||
console.warn('recent 无效,改走 live 全量');
|
||||
autoRefreshChart({ mode: 'full', reason: 'recent-fallback' });
|
||||
return;
|
||||
}
|
||||
currentData.kline_data = mergeKlineTail(currentData.kline_data, partial.kline_data);
|
||||
@@ -163,81 +283,60 @@ function updateChart(options) {
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (textStatus === 'abort') return;
|
||||
console.warn('recent 失败,回退全量 analyze:', errorThrown);
|
||||
updateChart({ incremental: true, reason: 'recent-error-fallback' });
|
||||
console.warn('recent 失败,改走 live 全量:', errorThrown);
|
||||
autoRefreshChart({ mode: 'full', reason: 'recent-error-fallback' });
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 手动 / 首拉:全量 analyze
|
||||
|
||||
console.log('自动刷新 → /api/analyze (live)');
|
||||
window._analyzeXhr = $.ajax({
|
||||
url: '/api/analyze',
|
||||
data: {
|
||||
symbol: symbol,
|
||||
timeframe: timeframe,
|
||||
timezone: timezone,
|
||||
element_timeframe: elementTimeframe,
|
||||
sub_sub_timeframe: subSubTimeframe || undefined,
|
||||
start_time: startTimeMs,
|
||||
end_time: endTimeMs,
|
||||
symbol: ctx.symbol,
|
||||
timeframe: ctx.timeframe,
|
||||
timezone: ctx.timezone,
|
||||
element_timeframe: ctx.elementTimeframe,
|
||||
sub_sub_timeframe: ctx.subSubTimeframe || undefined,
|
||||
start_time: ctx.startTimeMs,
|
||||
end_time: ctx.endTimeMs,
|
||||
elements_only: false,
|
||||
zone_kl_lines: parseInt($('#zoneKlLines').val()) || 1000,
|
||||
include_structure_zones: $('#showMainStructureZone').is(':checked') ? 1 : 0
|
||||
// 威科夫随主分析一并返回;开关仅控制绘制,不再传 include_wyckoff
|
||||
},
|
||||
success: function(data) {
|
||||
// 隐藏加载图标
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
|
||||
// 忽略过期响应
|
||||
if (requestId !== lastRequestId) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 保存当前数据
|
||||
const prevSymbol = (currentData && currentData.symbol) || window._lastChartSymbol || '';
|
||||
if (currentData) {
|
||||
// 覆盖前断开旧引用,帮助GC尽快回收
|
||||
delete currentData.original_kline_data;
|
||||
delete currentData.original_macd;
|
||||
}
|
||||
currentData = data;
|
||||
window._lastChartSymbol = symbol;
|
||||
window._lastFullAnalyzeAt = Date.now();
|
||||
if (typeof renderWyckoffCycleSummary === 'function') {
|
||||
renderWyckoffCycleSummary();
|
||||
}
|
||||
|
||||
// 有图则增量;笔/段/中枢/结构区只在全量 init 绘制
|
||||
// 换币对 / 手动分析 / 结构区:必须全量重建,否则会残留旧币对叠层
|
||||
const ready = !!(tvWidget && tvWidget.state && tvWidget.state.isInitialized && tvWidget.mainChart);
|
||||
const structureZonesOn = $('#showMainStructureZone').is(':checked');
|
||||
const symbolChanged = !!(prevSymbol && prevSymbol !== symbol);
|
||||
let wantIncremental = options.incremental !== undefined
|
||||
? !!options.incremental
|
||||
: (ready || !!options.fromAutoRefresh);
|
||||
if (structureZonesOn || options.fullAnalyze || symbolChanged || options.incremental === false) {
|
||||
wantIncremental = false;
|
||||
}
|
||||
refreshChart(data, { incremental: wantIncremental });
|
||||
if (requestId !== lastRequestId) return;
|
||||
applyAnalyzeSuccess(data, ctx.symbol, {
|
||||
incremental: options.incremental === true,
|
||||
forceFullRebuild: options.forceFullRebuild !== false && options.incremental !== true
|
||||
});
|
||||
},
|
||||
error: function(jqXHR, textStatus, errorThrown) {
|
||||
// 隐藏加载图标
|
||||
$('#refreshLoadingSpinner').hide();
|
||||
if (textStatus === 'abort') {
|
||||
return;
|
||||
}
|
||||
|
||||
// 显示错误信息
|
||||
console.error('加载数据失败:', errorThrown);
|
||||
// 自动刷新失败不弹窗打扰
|
||||
if (!options.fromAutoRefresh) {
|
||||
alert('加载数据失败: ' + (jqXHR.responseJSON?.error || errorThrown));
|
||||
}
|
||||
if (textStatus === 'abort') return;
|
||||
console.error('自动刷新分析失败:', errorThrown);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** 兼容旧调用:手动走 analyzeChart;fromAutoRefresh 转 autoRefreshChart */
|
||||
function updateChart(options) {
|
||||
options = options || {};
|
||||
if (options.fromAutoRefresh) {
|
||||
console.warn('updateChart(fromAutoRefresh) 已废弃,请改用 autoRefreshChart');
|
||||
autoRefreshChart({
|
||||
mode: options.fullAnalyze ? 'full' : 'recent',
|
||||
incremental: options.incremental,
|
||||
forceFullRebuild: options.incremental === false,
|
||||
reason: options.reason
|
||||
});
|
||||
return;
|
||||
}
|
||||
analyzeChart(options);
|
||||
}
|
||||
|
||||
function captureChartViewState(chart) {
|
||||
if (!chart || !chart.timeScale) return null;
|
||||
const ts = chart.timeScale();
|
||||
@@ -287,77 +386,102 @@ function applySeriesDataTail(series, points, tailOnly) {
|
||||
}
|
||||
|
||||
function restoreChartViewState(charts, viewState, options) {
|
||||
// 全量重建备用:先缩放,再位置;不要在位置前写 rightOffset(会右边缘锚定)
|
||||
if (!viewState || !Array.isArray(charts) || charts.length === 0) return;
|
||||
options = options || {};
|
||||
const preferTime = !!options.preferTime;
|
||||
const validCharts = charts.filter(c => c && c.timeScale);
|
||||
if (validCharts.length === 0) return;
|
||||
const mainChart = validCharts[0];
|
||||
const incremental = !!options.incremental;
|
||||
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
if (typeof viewState.barSpacing === 'number') {
|
||||
c.timeScale().applyOptions({ barSpacing: viewState.barSpacing });
|
||||
const oldBarCount = options.oldBarCount || window._preserveViewBarCount || 0;
|
||||
const newBarCount = options.newBarCount || 0;
|
||||
const barDelta = (oldBarCount > 0 && newBarCount > 0) ? (newBarCount - oldBarCount) : 0;
|
||||
|
||||
const applyLogical = function (lr) {
|
||||
if (!lr || lr.from === undefined || lr.to === undefined) return false;
|
||||
let from = lr.from;
|
||||
let to = lr.to;
|
||||
if (newBarCount > 0) {
|
||||
const span = Math.max(1, to - from);
|
||||
const maxTo = newBarCount - 1 + 8;
|
||||
if (to > maxTo) {
|
||||
to = maxTo;
|
||||
from = to - span;
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
if (from < -8) {
|
||||
from = -8;
|
||||
to = from + span;
|
||||
}
|
||||
lr = { from: from, to: to };
|
||||
}
|
||||
let ok = false;
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleLogicalRange(lr);
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
return ok;
|
||||
};
|
||||
|
||||
let restored = false;
|
||||
const applyVisible = function () {
|
||||
if (!viewState.visibleRange ||
|
||||
viewState.visibleRange.from === undefined ||
|
||||
viewState.visibleRange.to === undefined) {
|
||||
return false;
|
||||
}
|
||||
let vr = viewState.visibleRange;
|
||||
if (options.firstBarTime != null && options.lastBarTime != null) {
|
||||
vr = clampVisibleRangeToBarTimes(vr, options.firstBarTime, options.lastBarTime);
|
||||
}
|
||||
let ok = false;
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleRange(vr);
|
||||
ok = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
return ok;
|
||||
};
|
||||
|
||||
const syncFromMain = function () {
|
||||
const applyScroll = function () {
|
||||
if (typeof viewState.scrollPosition !== 'number') return false;
|
||||
try {
|
||||
const pos = viewState.scrollPosition + barDelta;
|
||||
mainChart.timeScale().scrollToPosition(pos, false);
|
||||
const lrNow = mainChart.timeScale().getVisibleLogicalRange();
|
||||
if (lrNow) {
|
||||
validCharts.forEach(c => {
|
||||
try { c.timeScale().setVisibleLogicalRange(lrNow); } catch (e) {}
|
||||
});
|
||||
restored = true;
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
return false;
|
||||
};
|
||||
|
||||
if (typeof viewState.scrollPosition === 'number') {
|
||||
try {
|
||||
mainChart.timeScale().scrollToPosition(viewState.scrollPosition, false);
|
||||
syncFromMain();
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
const tryVisibleRange = function () {
|
||||
if (!viewState.visibleRange || viewState.visibleRange.from === undefined || viewState.visibleRange.to === undefined) {
|
||||
return false;
|
||||
const restorePosition = function () {
|
||||
if (incremental) {
|
||||
// 尾部增量:scroll+barDelta 最稳;全量重建勿先 scroll(中间段会锚到最右)
|
||||
if (applyScroll()) return true;
|
||||
if (applyLogical(viewState.logicalRange)) return true;
|
||||
return applyVisible();
|
||||
}
|
||||
// 全量重建:logical → clamped time → scroll
|
||||
if (applyLogical(viewState.logicalRange)) return true;
|
||||
if (applyVisible()) return true;
|
||||
return applyScroll();
|
||||
};
|
||||
|
||||
restorePosition();
|
||||
|
||||
// barSpacing 写在位置之后会按右缘重锚,故放最后并再扳一次位置
|
||||
if (!options.skipBarSpacing && typeof viewState.barSpacing === 'number') {
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleRange(viewState.visibleRange);
|
||||
restored = true;
|
||||
c.timeScale().applyOptions({ barSpacing: viewState.barSpacing });
|
||||
} catch (e) {}
|
||||
});
|
||||
return restored;
|
||||
};
|
||||
|
||||
const tryLogicalRange = function () {
|
||||
if (!viewState.logicalRange || viewState.logicalRange.from === undefined || viewState.logicalRange.to === undefined) {
|
||||
return false;
|
||||
}
|
||||
validCharts.forEach(c => {
|
||||
try {
|
||||
c.timeScale().setVisibleLogicalRange(viewState.logicalRange);
|
||||
restored = true;
|
||||
} catch (e) {}
|
||||
});
|
||||
return restored;
|
||||
};
|
||||
|
||||
if (!restored) {
|
||||
if (preferTime) {
|
||||
tryVisibleRange();
|
||||
if (!restored) tryLogicalRange();
|
||||
} else {
|
||||
tryLogicalRange();
|
||||
if (!restored) tryVisibleRange();
|
||||
}
|
||||
restorePosition();
|
||||
}
|
||||
}
|
||||
// 初始化图表
|
||||
|
||||
@@ -37,7 +37,7 @@ function saveMacdConfig() {
|
||||
data: JSON.stringify({ fast: fast, slow: slow, signal: signal }),
|
||||
success: function() {
|
||||
hideMacdConfig();
|
||||
updateChart();
|
||||
analyzeChart({ reason: 'macd-config-saved' });
|
||||
},
|
||||
error: function() {
|
||||
alert('保存MACD参数失败');
|
||||
@@ -87,7 +87,7 @@ $(document).on('change', '#showMainStructureZone', function() {
|
||||
console.log('结构区切换为:', on);
|
||||
// 勾选后才向服务器请求多周期结构区数据;结构区叠层只在全量 init 里绘制,必须 incremental:false
|
||||
if (on) {
|
||||
updateChart({ incremental: false });
|
||||
analyzeChart({ incremental: false, reason: 'structure-zone-on' });
|
||||
} else {
|
||||
updateChartDisplay();
|
||||
}
|
||||
|
||||
+29
-34
@@ -339,7 +339,7 @@ $(document).ready(function() {
|
||||
startAStockStatusUpdater();
|
||||
// A 股:metadata 完成后再拉数(下方不再重复 updateChart)
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
analyzeChart({ reason: 'astock-init' });
|
||||
}, 300);
|
||||
});
|
||||
}
|
||||
@@ -378,10 +378,10 @@ $(document).ready(function() {
|
||||
// 尝试加载更多交易对
|
||||
loadSymbols();
|
||||
|
||||
// 初始化图表:默认加密货币延迟拉取;若首屏为 A 股则在 chart_metadata 完成后再 updateChart
|
||||
// 初始化图表:默认加密货币延迟拉取;若首屏为 A 股则在 chart_metadata 完成后再 analyze
|
||||
if (initialDataSource !== 'a_stock') {
|
||||
setTimeout(function() {
|
||||
updateChart();
|
||||
analyzeChart({ reason: 'crypto-init' });
|
||||
}, 500);
|
||||
}
|
||||
|
||||
@@ -533,30 +533,35 @@ function startAutoRefresh() {
|
||||
|
||||
// 启动定时器
|
||||
autoRefreshTick = 0;
|
||||
|
||||
// 进入实时模式:结束时间=现在,立刻走自动刷新全量(视窗由 autoRefreshChart 内 freeze 冻结)
|
||||
updateEndTimeToNow();
|
||||
autoRefreshChart({
|
||||
mode: 'full',
|
||||
incremental: false,
|
||||
forceFullRebuild: true,
|
||||
reason: 'auto-refresh-start'
|
||||
});
|
||||
|
||||
autoRefreshTimer = setInterval(function() {
|
||||
// 更新结束时间显示(仅 UI)
|
||||
// 自动刷新专用:结束时间推进到现在
|
||||
updateEndTimeToNow();
|
||||
|
||||
autoRefreshTick += 1;
|
||||
const now = Date.now();
|
||||
const lastFull = window._lastFullAnalyzeAt || 0;
|
||||
const needFullAnalyze = !lastFull || (now - lastFull >= AUTO_FULL_ANALYZE_MS);
|
||||
// 常态:/api/klines/recent 合并尾部 K;满 1 分钟:全量 /api/analyze 刷新缠论
|
||||
if (needFullAnalyze) {
|
||||
console.log('自动刷新 → 全量缠论 analyze(距上次', lastFull ? Math.round((now - lastFull) / 1000) + 's' : '首次', ')');
|
||||
updateChart({
|
||||
fromAutoRefresh: true,
|
||||
fullAnalyze: true,
|
||||
incremental: true
|
||||
});
|
||||
const tf = $('#timeframe').val() || '4h';
|
||||
const needFull = !lastFull || (now - lastFull >= AUTO_FULL_ANALYZE_MS) ||
|
||||
(typeof isLiveBaselineStale === 'function' && isLiveBaselineStale(tf));
|
||||
|
||||
if (needFull) {
|
||||
console.log('自动刷新 tick → live 全量');
|
||||
autoRefreshChart({ mode: 'full', incremental: false, forceFullRebuild: true });
|
||||
} else {
|
||||
updateChart({
|
||||
fromAutoRefresh: true,
|
||||
incremental: true
|
||||
});
|
||||
console.log('自动刷新 tick → recent 尾部');
|
||||
autoRefreshChart({ mode: 'recent' });
|
||||
}
|
||||
|
||||
// 更新下次刷新时间
|
||||
nextRefreshTime = new Date(Date.now() + intervalMs);
|
||||
updateNextRefreshTimeDisplay();
|
||||
}, intervalMs);
|
||||
@@ -798,21 +803,7 @@ function refreshChart(data, options) {
|
||||
// 自动刷新:增量更新,避免每次销毁/重建 Lightweight Charts
|
||||
if (preferIncremental && chartsReady) {
|
||||
try {
|
||||
// 数据到达后再冻结视窗(比请求发出时更准;避免用到过期 scroll)
|
||||
if (tvWidget.mainChart && typeof captureChartViewState === 'function') {
|
||||
try {
|
||||
window._preserveViewOnRefresh = captureChartViewState(tvWidget.mainChart);
|
||||
const prev = currentData && (
|
||||
($('#subSubPeriodKline').is(':checked') && currentData.sub_sub_kline_data) ||
|
||||
($('#elementPeriodKline').is(':checked') && currentData.element_kline_data) ||
|
||||
currentData.kline_data
|
||||
);
|
||||
window._preserveViewBarCount = Array.isArray(prev) ? prev.length : 0;
|
||||
} catch (e) {
|
||||
window._preserveViewOnRefresh = null;
|
||||
window._preserveViewBarCount = 0;
|
||||
}
|
||||
}
|
||||
// 视窗已在请求发出时 freezeChartViewportBeforeRequest 冻结,勿在此重拍(会弄错 barCount)
|
||||
updateTradingViewData({ tailOnly: !!options.skipTables });
|
||||
// recent-tail 刷新结构未变,跳过表格重绘以提速
|
||||
if (!options.skipTables) {
|
||||
@@ -829,7 +820,11 @@ function refreshChart(data, options) {
|
||||
|
||||
// 保存当前缩放(barSpacing)和滚动位置(scrollPosition)到 window
|
||||
// tvWidget 会在 initTradingView 内被重建,所以必须存到 window 上
|
||||
if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) {
|
||||
// 全量重建:优先用请求前冻结的视窗(分析按钮在请求发出时已 capture)
|
||||
if (window._preserveViewOnRefresh) {
|
||||
window._pendingRestoreView = window._preserveViewOnRefresh;
|
||||
console.log('📌 全量重建:使用请求前冻结视窗');
|
||||
} else if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) {
|
||||
try {
|
||||
window._pendingRestoreView = captureChartViewState(tvWidget.mainChart);
|
||||
console.log('📌 保存图表视图:', JSON.stringify(window._pendingRestoreView));
|
||||
|
||||
@@ -982,7 +982,7 @@
|
||||
<input type="datetime-local" id="end_time" class="form-control">
|
||||
</div>
|
||||
<div class="col-md-1">
|
||||
<button class="btn btn-primary w-100" onclick="updateEndTimeToNow(); updateChart({ incremental: false, fullAnalyze: true })" style="padding: 8px 6px; font-size: 14px;">
|
||||
<button class="btn btn-primary w-100" onclick="analyzeChart()" style="padding: 8px 6px; font-size: 14px;" title="按开始/结束时间拉取并分析(与自动刷新无关)">
|
||||
分析
|
||||
</button>
|
||||
</div>
|
||||
@@ -1386,19 +1386,19 @@
|
||||
<script defer src="{{ url_for('static', filename='js/app/api_client.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/state.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/trend.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260808j"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260809r"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/macd_ui.js') }}?v=20260809t"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_format.js') }}?v=20260809y"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_view.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_lifecycle.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_shell.js') }}?v=20260809j"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_indicators.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_chan.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260809n"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260809d"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_overlays.js') }}?v=20260809s"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv_finalize.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tv.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809r"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_sync.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/chart_tables.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260809r"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260809z"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}?v=20260808i"></script>
|
||||
<script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260808i"></script>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user