fix(web): 分析/自动刷新后保留 K 线视窗位置

拆分手动分析与自动刷新拉数路径;全量重建用 logical 优先恢复视窗,
增量 recent 用 scroll+barDelta;避免 barSpacing 重锚与重复冻结导致往右跳。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-25 23:38:20 +08:00
co-authored by Cursor
parent 8ee11317d3
commit 90499533fb
8 changed files with 449 additions and 310 deletions
+29 -34
View File
@@ -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));