fix(web): 开关缠论元素保留视窗;分周期 Trend 涨跌配色

本地重绘统一冻结视窗;次/次次周期 Trend 上涨下跌使用独立颜色。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
jackyu66git
2026-08-25 23:56:55 +08:00
co-authored by Cursor
parent 90499533fb
commit 97e77847d0
6 changed files with 130 additions and 84 deletions
+10 -5
View File
@@ -12,14 +12,19 @@ function updateChartDisplay() {
_lastKlinePeriod = curPeriod; _lastKlinePeriod = curPeriod;
// 保存当前的可见范围(周期切换时不保留,避免范围越界) // 保存当前的可见范围(周期切换时不保留,避免范围越界)
if (!periodChanged && typeof snapshotPendingChartViewport === 'function') { if (!periodChanged) {
snapshotPendingChartViewport(); 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('更新图表显示'); console.log('更新图表显示');
// 重新初始化图表(initTradingView 内部会在最终同步时读取 _pendingRestoreView
initTradingView($('#symbol').val(), $('#timeframe').val());
} }
} }
// 确保所有时间处理都使用UTC时间,包括表格数据显示 // 确保所有时间处理都使用UTC时间,包括表格数据显示
+26 -2
View File
@@ -66,7 +66,19 @@ function chartTvFinalize(ctx) {
const allChartsNow = [mainChart, volumeChart, atrChart] const allChartsNow = [mainChart, volumeChart, atrChart]
.concat(showMacd && macdChart ? [macdChart] : []) .concat(showMacd && macdChart ? [macdChart] : [])
.concat(showMacd && chanMacdChart ? [chanMacdChart] : []); .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线过多时重叠) // 显示最近 200 根K线而非全部挤压(避免K线过多时重叠)
if (totalBars > visibleBarsCount) { if (totalBars > visibleBarsCount) {
const rangeFrom = totalBars - visibleBarsCount; const rangeFrom = totalBars - visibleBarsCount;
@@ -77,9 +89,21 @@ function chartTvFinalize(ctx) {
} }
} }
// 立即同步其他图表到主图表的范围(无 pending 时) // 立即同步其他图表到主图表的范围
setTimeout(() => { setTimeout(() => {
if (pendingView) { 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; return;
} }
const logRange = mainChart.timeScale().getVisibleLogicalRange(); const logRange = mainChart.timeScale().getVisibleLogicalRange();
+52 -45
View File
@@ -48,6 +48,52 @@ function alignMarkersToCandles(markers, candles) {
return out; return out;
} }
/** 解析后端 KLC trendUP/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) { function safeOverlayLineSetData(series, points) {
if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return; if (!series || typeof series.setData !== 'function' || !Array.isArray(points) || points.length < 2) return;
try { try {
@@ -2229,20 +2275,8 @@ function chartTvRenderOverlays(ctx) {
klcTrendMarkers = currentData.klc_trend.map(t => { klcTrendMarkers = currentData.klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); 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 timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
let marker = { time: timeAligned, position: 'inBar', color: '#9E9E9E', shape: 'square', size: 0.8 }; return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE);
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;
}); });
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
} }
@@ -2270,12 +2304,8 @@ function chartTvRenderOverlays(ctx) {
}; };
const elementMarkers = currentData.element_klc_trend.map(t => { const elementMarkers = currentData.element_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE);
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 };
}); });
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
} }
@@ -2290,15 +2320,10 @@ function chartTvRenderOverlays(ctx) {
} }
return best; return best;
}; };
const subSubColor = '#00897b';
const subSubMarkers = currentData.sub_sub_klc_trend.map(t => { const subSubMarkers = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts); const timeAligned = candlesTimesSs.has(ts) ? ts : nearestTimeSs(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor, shape: 'arrowUp', size: 0.4 }; return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE);
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 };
}); });
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers);
} }
@@ -2369,17 +2394,8 @@ function chartTvRenderOverlays(ctx) {
}; };
klcTrendMarkers = currentData.klc_trend.map(t => { klcTrendMarkers = currentData.klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts); const timeAligned = seriesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') { return buildKlcTrendMarker(timeAligned, t.trend, MAIN_KLC_TREND_STYLE);
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 };
}
}); });
console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5)); console.log('KLC趋势标记(对齐后)示例:', klcTrendMarkers.slice(0, 5));
} }
@@ -2406,12 +2422,8 @@ function chartTvRenderOverlays(ctx) {
}; };
const elementMarkers = currentData.element_klc_trend.map(t => { const elementMarkers = currentData.element_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts); const timeAligned = candlesTimes.has(ts) ? ts : nearestTime(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: '#00C853', shape: 'arrowUp', size: 0.5 }; return buildKlcTrendMarker(timeAligned, t.trend, ELEMENT_KLC_TREND_STYLE);
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 };
}); });
trendMarkersToUse = trendMarkersToUse.concat(elementMarkers); trendMarkersToUse = trendMarkersToUse.concat(elementMarkers);
} }
@@ -2426,15 +2438,10 @@ function chartTvRenderOverlays(ctx) {
} }
return best; return best;
}; };
const subSubColor2 = '#00897b';
const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => { const subSubMarkers2 = currentData.sub_sub_klc_trend.map(t => {
const ts = Math.floor(new Date(t.time).getTime() / 1000); const ts = Math.floor(new Date(t.time).getTime() / 1000);
const trendRaw = (t.trend || '').toString().toUpperCase();
const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts); const timeAligned = candlesTimesSs2.has(ts) ? ts : nearestTimeSs2(ts);
if (trendRaw === 'UP') return { time: timeAligned, position: 'aboveBar', color: subSubColor2, shape: 'arrowUp', size: 0.4 }; return buildKlcTrendMarker(timeAligned, t.trend, SUB_SUB_KLC_TREND_STYLE);
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 };
}); });
trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2); trendMarkersToUse = trendMarkersToUse.concat(subSubMarkers2);
} }
+24
View File
@@ -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() { function freezeChartViewportBeforeRequest() {
try { try {
if (tvWidget && tvWidget.mainChart) { if (tvWidget && tvWidget.mainChart) {
+13 -27
View File
@@ -668,10 +668,6 @@ function mapTimeframeToInterval(timeframe) {
function redrawFractalElements() { function redrawFractalElements() {
if (!tvWidget || !tvWidget.mainChart) return; if (!tvWidget || !tvWidget.mainChart) return;
const mainChart = tvWidget.mainChart;
const logicalRange = mainChart.timeScale().getVisibleLogicalRange();
const visibleRange = mainChart.timeScale().getVisibleRange();
// 确保使用主周期的K线和MACD数据 // 确保使用主周期的K线和MACD数据
if (currentData.original_kline_data) { if (currentData.original_kline_data) {
currentData.kline_data = currentData.original_kline_data; currentData.kline_data = currentData.original_kline_data;
@@ -679,29 +675,14 @@ function redrawFractalElements() {
if (currentData.original_macd) { if (currentData.original_macd) {
currentData.macd = currentData.original_macd; currentData.macd = currentData.original_macd;
} }
// 清除冗余引用,帮助GC回收
delete currentData.original_kline_data; delete currentData.original_kline_data;
delete currentData.original_macd; delete currentData.original_macd;
initTradingView($('#symbol').val(), $('#timeframe').val()); if (typeof reinitTradingViewPreservingViewport === 'function') {
reinitTradingViewPreservingViewport();
setTimeout(() => { } else {
if (tvWidget && tvWidget.mainChart) { initTradingView($('#symbol').val(), $('#timeframe').val());
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);
} }
// 只更新分形元素(笔、线段、中枢)的表格数据 // 只更新分形元素(笔、线段、中枢)的表格数据
function updateFractalTables() { function updateFractalTables() {
@@ -818,10 +799,15 @@ function refreshChart(data, options) {
} }
} }
// 保存当前缩放(barSpacing)和滚动位置(scrollPosition)到 window
// tvWidget 会在 initTradingView 内被重建,所以必须存到 window 上
// 全量重建:优先用请求前冻结的视窗(分析按钮在请求发出时已 capture) // 全量重建:优先用请求前冻结的视窗(分析按钮在请求发出时已 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; window._pendingRestoreView = window._preserveViewOnRefresh;
console.log('📌 全量重建:使用请求前冻结视窗'); console.log('📌 全量重建:使用请求前冻结视窗');
} else if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) { } else if (!window._pendingRestoreView && tvWidget && tvWidget.mainChart) {
+5 -5
View File
@@ -1387,18 +1387,18 @@
<script defer src="{{ url_for('static', filename='js/app/state.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/trend.js') }}?v=20260808i"></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/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_format.js') }}?v=20260810a"></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_view.js') }}?v=20260810a"></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_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_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_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_chan.js') }}?v=20260808i"></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_overlays.js') }}?v=20260810c"></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_finalize.js') }}?v=20260810a"></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_tv.js') }}?v=20260808i"></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_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/chart_tables.js') }}?v=20260808i"></script>
<script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260809z"></script> <script defer src="{{ url_for('static', filename='js/app/ui.js') }}?v=20260810a"></script>
<script defer src="{{ url_for('static', filename='js/app/overlays.js') }}?v=20260808i"></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> <script defer src="{{ url_for('static', filename='js/app/main.js') }}?v=20260808i"></script>