From 97e77847d02ada5686b4979e7892b3ab0191b198 Mon Sep 17 00:00:00 2001 From: jackyu66git Date: Tue, 25 Aug 2026 23:56:55 +0800 Subject: [PATCH] =?UTF-8?q?fix(web):=20=E5=BC=80=E5=85=B3=E7=BC=A0?= =?UTF-8?q?=E8=AE=BA=E5=85=83=E7=B4=A0=E4=BF=9D=E7=95=99=E8=A7=86=E7=AA=97?= =?UTF-8?q?=EF=BC=9B=E5=88=86=E5=91=A8=E6=9C=9F=20Trend=20=E6=B6=A8?= =?UTF-8?q?=E8=B7=8C=E9=85=8D=E8=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 本地重绘统一冻结视窗;次/次次周期 Trend 上涨下跌使用独立颜色。 Co-authored-by: Cursor --- web/static/js/app/chart_format.js | 15 ++-- web/static/js/app/chart_tv_finalize.js | 28 +++++++- web/static/js/app/chart_tv_overlays.js | 97 ++++++++++++++------------ web/static/js/app/chart_view.js | 24 +++++++ web/static/js/app/ui.js | 40 ++++------- web/templates/index.html | 10 +-- 6 files changed, 130 insertions(+), 84 deletions(-) diff --git a/web/static/js/app/chart_format.js b/web/static/js/app/chart_format.js index 9cbdc30..cdc34c5 100644 --- a/web/static/js/app/chart_format.js +++ b/web/static/js/app/chart_format.js @@ -12,14 +12,19 @@ function updateChartDisplay() { _lastKlinePeriod = curPeriod; // 保存当前的可见范围(周期切换时不保留,避免范围越界) - if (!periodChanged && typeof snapshotPendingChartViewport === 'function') { - snapshotPendingChartViewport(); + if (!periodChanged) { + if (typeof reinitTradingViewPreservingViewport === 'function') { + reinitTradingViewPreservingViewport(); + } else { + initTradingView($('#symbol').val(), $('#timeframe').val()); + } + } else { + window._pendingRestoreView = null; + window._preserveViewBarCount = 0; + initTradingView($('#symbol').val(), $('#timeframe').val()); } console.log('更新图表显示'); - - // 重新初始化图表(initTradingView 内部会在最终同步时读取 _pendingRestoreView) - initTradingView($('#symbol').val(), $('#timeframe').val()); } } // 确保所有时间处理都使用UTC时间,包括表格数据显示 diff --git a/web/static/js/app/chart_tv_finalize.js b/web/static/js/app/chart_tv_finalize.js index 6ac0aa2..c9dc84f 100644 --- a/web/static/js/app/chart_tv_finalize.js +++ b/web/static/js/app/chart_tv_finalize.js @@ -66,7 +66,19 @@ function chartTvFinalize(ctx) { const allChartsNow = [mainChart, volumeChart, atrChart] .concat(showMacd && macdChart ? [macdChart] : []) .concat(showMacd && chanMacdChart ? [chanMacdChart] : []); - if (!hasPendingRestoreView || !pendingView) { + const restoreOpts = function () { + const firstT = candles && candles.length ? candles[0].time : null; + const lastT = candles && candles.length ? candles[candles.length - 1].time : null; + return { + firstBarTime: firstT, + lastBarTime: lastT, + oldBarCount: window._preserveViewBarCount || 0, + newBarCount: totalBars + }; + }; + if (hasPendingRestoreView && pendingView) { + restoreChartViewState(allChartsNow, pendingView, restoreOpts()); + } else { // 显示最近 200 根K线而非全部挤压(避免K线过多时重叠) if (totalBars > visibleBarsCount) { const rangeFrom = totalBars - visibleBarsCount; @@ -77,9 +89,21 @@ function chartTvFinalize(ctx) { } } - // 立即同步其他图表到主图表的范围(无 pending 时) + // 立即同步其他图表到主图表的范围 setTimeout(() => { if (pendingView) { + restoreChartViewState(allChartsNow, pendingView, restoreOpts()); + const logRange = mainChart.timeScale().getVisibleLogicalRange(); + if (logRange) { + volumeChart.timeScale().setVisibleLogicalRange(logRange); + atrChart.timeScale().setVisibleLogicalRange(logRange); + if (showMacd && macdChart) { + macdChart.timeScale().setVisibleLogicalRange(logRange); + } + if (showMacd && chanMacdChart) { + chanMacdChart.timeScale().setVisibleLogicalRange(logRange); + } + } return; } const logRange = mainChart.timeScale().getVisibleLogicalRange(); diff --git a/web/static/js/app/chart_tv_overlays.js b/web/static/js/app/chart_tv_overlays.js index be114e9..3815e90 100644 --- a/web/static/js/app/chart_tv_overlays.js +++ b/web/static/js/app/chart_tv_overlays.js @@ -48,6 +48,52 @@ function alignMarkersToCandles(markers, candles) { return out; } +/** 解析后端 KLC trend(UP/DOWN/FLAT 或枚举名/数字) */ +function normalizeKlcTrendRaw(trend) { + var raw = (trend == null ? '' : String(trend)).trim(); + if (!raw) return 'UNKNOWN'; + var upper = raw.toUpperCase(); + if (upper === 'UP' || raw === '1' || upper.indexOf('.UP') >= 0 || upper.endsWith('UP')) return 'UP'; + if (upper === 'DOWN' || raw === '2' || upper.indexOf('.DOWN') >= 0 || upper.endsWith('DOWN')) return 'DOWN'; + if (upper === 'FLAT' || raw === '3' || upper.indexOf('.FLAT') >= 0 || upper.endsWith('FLAT')) return 'FLAT'; + return upper; +} + +var MAIN_KLC_TREND_STYLE = { + UP: { position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }, + DOWN: { position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }, + FLAT: { position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }, + UNKNOWN: { position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 } +}; + +/** 次周期 Trend:橙涨 / 靛蓝跌,与主周期绿红区分 */ +var ELEMENT_KLC_TREND_STYLE = { + UP: { position: 'aboveBar', color: '#e67e22', shape: 'arrowUp', size: 0.5 }, + DOWN: { position: 'belowBar', color: '#5c6bc0', shape: 'arrowDown', size: 0.5 }, + FLAT: { position: 'inBar', color: '#bdbdbd', shape: 'circle', size: 0.8 }, + UNKNOWN: { position: 'inBar', color: '#8e44ad', shape: 'square', size: 0.8 } +}; + +/** 次次周期 Trend:青绿涨 / 灰蓝跌(略小标记) */ +var SUB_SUB_KLC_TREND_STYLE = { + UP: { position: 'aboveBar', color: '#00b894', shape: 'arrowUp', size: 0.4 }, + DOWN: { position: 'belowBar', color: '#546e7a', shape: 'arrowDown', size: 0.4 }, + FLAT: { position: 'inBar', color: '#00897b', shape: 'circle', size: 0.5 }, + UNKNOWN: { position: 'inBar', color: '#004d40', shape: 'square', size: 0.5 } +}; + +function buildKlcTrendMarker(timeAligned, trendRaw, palette) { + var kind = normalizeKlcTrendRaw(trendRaw); + var style = palette[kind] || palette.UNKNOWN || palette.FLAT; + return { + time: timeAligned, + position: style.position, + color: style.color, + shape: style.shape, + size: style.size + }; +} + function safeOverlayLineSetData(series, points) { if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return; try { @@ -2229,20 +2275,8 @@ function chartTvRenderOverlays(ctx) { klcTrendMarkers = currentData.klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); let timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); - let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 }; - if (trendRaw === 'UP') { - marker = { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - } else if (trendRaw === 'DOWN') { - marker = { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - } else if (trendRaw === 'FLAT') { - marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - } else { - // UNKNOWN 或其他 - marker = { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - } - return marker; + return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE); }); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); } @@ -2270,12 +2304,8 @@ function chartTvRenderOverlays(ctx) { }; const elementMarkers = currentData.element_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE); }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } @@ -2290,15 +2320,10 @@ function chartTvRenderOverlays(ctx) { } return best; }; - const subSubColor = '#00897b'; const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor, shape: 'arrowDown', size: 0.4 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'circle', size: 0.5 }; - return { time: timeAligned, position: 'inBar', color: subSubColor, shape: 'square', size: 0.5 }; + return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE); }); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); } @@ -2369,17 +2394,8 @@ function chartTvRenderOverlays(ctx) { }; klcTrendMarkers = currentData.klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') { - return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - } else if (trendRaw === 'DOWN') { - return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - } else if (trendRaw === 'FLAT') { - return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - } else { - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; - } + return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE); }); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); } @@ -2406,12 +2422,8 @@ function chartTvRenderOverlays(ctx) { }; const elementMarkers = currentData.element_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: '#D32F2F', shape: 'arrowDown', size: 0.5 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'circle', size: 0.8 }; - return { time: timeAligned, position: 'inBar', color: '#2196F3', shape: 'square', size: 0.8 }; + return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE); }); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); } @@ -2426,15 +2438,10 @@ function chartTvRenderOverlays(ctx) { } return best; }; - const subSubColor2 = '#00897b'; const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { const ts = Math.floor(new Date(t.time).getTime() / 1000); - const trendRaw = (t.trend || '').toString().toUpperCase(); const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); - if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; - if (trendRaw === 'DOWN') return { time: timeAligned, position: 'belowBar', color: subSubColor2, shape: 'arrowDown', size: 0.4 }; - if (trendRaw === 'FLAT') return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'circle', size: 0.5 }; - return { time: timeAligned, position: 'inBar', color: subSubColor2, shape: 'square', size: 0.5 }; + return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE); }); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); } diff --git a/web/static/js/app/chart_view.js b/web/static/js/app/chart_view.js index 0491e58..412ae07 100644 --- a/web/static/js/app/chart_view.js +++ b/web/static/js/app/chart_view.js @@ -107,6 +107,30 @@ function snapshotPendingChartViewport() { } } +/** 本地重绘(开关缠论元素 / K 线类型等):先冻结视窗再 init */ +function reinitTradingViewPreservingViewport() { + snapshotPendingChartViewport(); + initTradingView($('#symbol').val(), $('#timeframe').val()); +} + +/** 全量 init 前确保 _pendingRestoreView 与 bar 数齐全(refreshChart 等路径) */ +function ensurePendingChartViewportBeforeInit() { + if (window._preserveViewOnRefresh) { + window._pendingRestoreView = window._preserveViewOnRefresh; + if (!window._preserveViewBarCount) { + window._preserveViewBarCount = getActiveKlineBarCount(); + } + return; + } + if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) { + snapshotPendingChartViewport(); + return; + } + if (window._pendingRestoreView && !window._preserveViewBarCount) { + window._preserveViewBarCount = getActiveKlineBarCount(); + } +} + function freezeChartViewportBeforeRequest() { try { if (tvWidget && tvWidget.mainChart) { diff --git a/web/static/js/app/ui.js b/web/static/js/app/ui.js index a91ba19..ecc7591 100644 --- a/web/static/js/app/ui.js +++ b/web/static/js/app/ui.js @@ -668,10 +668,6 @@ function mapTimeframeToInterval(timeframe) { function redrawFractalElements() { if (!tvWidget || !tvWidget.mainChart) return; - const mainChart = tvWidget.mainChart; - const logicalRange = mainChart.timeScale().getVisibleLogicalRange(); - const visibleRange = mainChart.timeScale().getVisibleRange(); - // 确保使用主周期的K线和MACD数据 if (currentData.original_kline_data) { currentData.kline_data = currentData.original_kline_data; @@ -679,29 +675,14 @@ function redrawFractalElements() { if (currentData.original_macd) { currentData.macd = currentData.original_macd; } - // 清除冗余引用,帮助GC回收 delete currentData.original_kline_data; delete currentData.original_macd; - initTradingView($('#symbol').val(), $('#timeframe').val()); - - setTimeout(() => { - if (tvWidget && tvWidget.mainChart) { - if (logicalRange) { - tvWidget.mainChart.timeScale().setVisibleLogicalRange(logicalRange); - if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleLogicalRange(logicalRange); - if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleLogicalRange(logicalRange); - if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleLogicalRange(logicalRange); - if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleLogicalRange(logicalRange); - } else if (visibleRange) { - tvWidget.mainChart.timeScale().setVisibleRange(visibleRange); - if (tvWidget.volumeChart) tvWidget.volumeChart.timeScale().setVisibleRange(visibleRange); - if (tvWidget.atrChart) tvWidget.atrChart.timeScale().setVisibleRange(visibleRange); - if (tvWidget.macdChart) tvWidget.macdChart.timeScale().setVisibleRange(visibleRange); - if (tvWidget.chanMacdChart) tvWidget.chanMacdChart.timeScale().setVisibleRange(visibleRange); - } - } - }, 200); + if (typeof reinitTradingViewPreservingViewport === 'function') { + reinitTradingViewPreservingViewport(); + } else { + initTradingView($('#symbol').val(), $('#timeframe').val()); + } } // 只更新分形元素(笔、线段、中枢)的表格数据 function updateFractalTables() { @@ -818,10 +799,15 @@ function refreshChart(data, options) { } } - // 保存当前缩放(barSpacing)和滚动位置(scrollPosition)到 window - // tvWidget 会在 initTradingView 内被重建,所以必须存到 window 上 // 全量重建:优先用请求前冻结的视窗(分析按钮在请求发出时已 capture) - if (window._preserveViewOnRefresh) { + if (typeof ensurePendingChartViewportBeforeInit === 'function') { + ensurePendingChartViewportBeforeInit(); + if (window._preserveViewOnRefresh) { + console.log('📌 全量重建:使用请求前冻结视窗'); + } else if (window._pendingRestoreView) { + console.log('📌 使用已保存图表视图'); + } + } else if (window._preserveViewOnRefresh) { window._pendingRestoreView = window._preserveViewOnRefresh; console.log('📌 全量重建:使用请求前冻结视窗'); } else if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) { diff --git a/web/templates/index.html b/web/templates/index.html index d6c762e..b48eca0 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1387,18 +1387,18 @@ - - + + - - + + - +