Fix turbo nav for settings and stats pages.

Extract settings.js, preserve inline scripts from raw HTML (DOMParser strips them), and load trade config via JSON script tag.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
dekun
2026-06-26 21:26:15 +08:00
parent 6d55a54946
commit 9613fb0737
7 changed files with 229 additions and 163 deletions
+126
View File
@@ -0,0 +1,126 @@
/* Copyright (c) 2025-2026 马建军. All rights reserved.
* 专有软件 — 未经授权禁止复制、传播、转售。
* 详见 LICENSE.zh-CN.txt
*/
(function () {
function bootSettingsPage() {
if (!document.querySelector('.settings-page')) return;
var sel = document.getElementById('position-sizing-mode');
var lotsField = document.getElementById('field-fixed-lots');
var amountField = document.getElementById('field-fixed-amount');
function syncSizingFields() {
if (!sel) return;
var isAmount = sel.value === 'amount';
if (lotsField) lotsField.hidden = isAmount;
if (amountField) amountField.hidden = !isAmount;
}
if (sel && !sel.dataset.settingsBound) {
sel.dataset.settingsBound = '1';
sel.addEventListener('change', syncSizingFields);
}
syncSizingFields();
var SETTINGS_FOLD_KEY = 'qihuo_settings_fold';
function setSettingsFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveSettingsFoldState() {
var state = {};
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
state[el.getAttribute('data-settings-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(SETTINGS_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadSettingsFoldState() {
try {
var raw = localStorage.getItem(SETTINGS_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
var key = el.getAttribute('data-settings-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setSettingsFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-fold-head').forEach(function (btn) {
if (btn.dataset.settingsBound) return;
btn.dataset.settingsBound = '1';
btn.addEventListener('click', function () {
var panel = btn.closest('[data-settings-fold]');
if (!panel) return;
setSettingsFold(panel, !panel.classList.contains('is-collapsed'));
saveSettingsFoldState();
});
});
loadSettingsFoldState();
var CTP_FOLD_KEY = 'qihuo_ctp_fold';
function setCtpFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-ctp-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveCtpFoldState() {
var state = {};
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
state[el.getAttribute('data-ctp-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(CTP_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadCtpFoldState() {
try {
var raw = localStorage.getItem(CTP_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
var key = el.getAttribute('data-ctp-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setCtpFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-ctp-fold-head').forEach(function (btn) {
if (btn.dataset.settingsBound) return;
btn.dataset.settingsBound = '1';
btn.addEventListener('click', function () {
var panel = btn.closest('[data-ctp-fold]');
if (!panel) return;
setCtpFold(panel, !panel.classList.contains('is-collapsed'));
saveCtpFoldState();
});
});
loadCtpFoldState();
var ctpForm = document.getElementById('ctp-settings-form');
if (ctpForm && !ctpForm.dataset.settingsBound) {
ctpForm.dataset.settingsBound = '1';
ctpForm.addEventListener('submit', function (ev) {
var ctpCard = document.querySelector('[data-settings-fold="ctp"]');
if (ctpCard) setSettingsFold(ctpCard, false);
var simnowFold = document.querySelector('[data-ctp-fold="simnow"]');
if (simnowFold) setCtpFold(simnowFold, false);
var pwd = document.getElementById('simnow_password');
var pwdVal = pwd && pwd.value ? pwd.value.trim() : '';
var pwdWasSet = ctpForm.getAttribute('data-simnow-pwd-set') === '1';
if (pwdWasSet && !pwdVal) {
var ok = window.confirm(
'SimNow 交易密码为空,保存后不会更新密码(仍用旧密码)。\n\n'
+ '若快期已改密,请取消后在「交易密码」框手打新密码再保存。\n\n仍要保存其他项?'
);
if (!ok) ev.preventDefault();
}
});
}
}
if (window.qihuoPageBoot) window.qihuoPageBoot(bootSettingsPage, '.settings-page');
else document.addEventListener('DOMContentLoaded', bootSettingsPage);
})();
+6 -2
View File
@@ -138,8 +138,11 @@
}
function loadStats() {
fetch('/api/stats')
.then(function (r) { return r.json(); })
fetch('/api/stats', { credentials: 'same-origin' })
.then(function (r) {
if (!r.ok) throw new Error('HTTP ' + r.status);
return r.json();
})
.then(applyData)
.catch(function () {
var updated = document.getElementById('stats-updated');
@@ -148,6 +151,7 @@
}
function bootStatsPage() {
if (!document.getElementById('stats-summary')) return;
var viewSel = document.getElementById('stats-view-select');
if (viewSel && !viewSel.dataset.statsBound) {
viewSel.dataset.statsBound = '1';
+22 -6
View File
@@ -3,8 +3,7 @@
* 详见 LICENSE.zh-CN.txt
*/
(function () {
var sizingMode = window.TRADE_SIZING_MODE || 'fixed';
if (sizingMode === 'risk') sizingMode = 'amount';
var sizingMode = 'fixed';
var list = document.getElementById('position-live-list');
var orderList = document.getElementById('order-live-list');
var syncBadge = document.getElementById('sync-badge');
@@ -33,7 +32,7 @@
var hasSlTpMonitoring = false;
var ctpConnected = false;
var ctpConnecting = false;
var ctpAutoConnectEnabled = window.CTP_AUTO_CONNECT !== false;
var ctpAutoConnectEnabled = true;
var positionsRendered = false;
var selectedMaxLots = null;
var recommendMaxByProduct = {};
@@ -45,10 +44,26 @@
var REC_SORT_CACHE = 'qihuo_rec_sort_v2';
var REC_INDUSTRY_CACHE = 'qihuo_rec_industry_v1';
var REC_COLSPAN = 18;
var marketNavEnabled = !!window.MARKET_NAV_ENABLED;
var productCategories = window.PRODUCT_CATEGORIES || [];
var marketNavEnabled = false;
var productCategories = [];
var POS_CACHE_KEY = 'qihuo_trading_live_v5';
function loadTradeConfig() {
var el = document.getElementById('trade-page-data');
if (!el) return;
try {
var cfg = JSON.parse(el.textContent);
sizingMode = cfg.sizing_mode || 'fixed';
if (sizingMode === 'risk') sizingMode = 'amount';
ctpAutoConnectEnabled = cfg.ctp_auto_connect !== false;
marketNavEnabled = !!cfg.market_nav_enabled;
productCategories = cfg.product_categories || [];
window.TRADE_FIXED_LOTS = cfg.fixed_lots;
window.TRADE_FIXED_AMOUNT = cfg.fixed_amount;
window.__RECOMMEND_ROWS__ = cfg.recommend_rows || [];
} catch (e) { /* ignore */ }
}
function fmtNum(v, digits) {
if (v === null || v === undefined) return '--';
return Number(v).toFixed(digits === undefined ? 2 : digits);
@@ -1607,6 +1622,7 @@
}
function bootTradePage() {
loadTradeConfig();
if (!list && !orderList) return;
updateCtpConnectButtonState();
setPriceType('limit');
@@ -1648,7 +1664,7 @@
}
});
if (window.qihuoPageBoot) window.qihuoPageBoot(bootTradePage, '#position-live-list, #order-live-list');
if (window.qihuoPageBoot) window.qihuoPageBoot(bootTradePage, '.trade-page');
else if (window.qihuoOnPageLoad) window.qihuoOnPageLoad(bootTradePage);
else bootTradePage();
if (window.qihuoOnPageLeave) window.qihuoOnPageLeave(cleanupTradePage);
+63 -35
View File
@@ -4,7 +4,7 @@
*/
(function () {
var PERMANENT_CSS = ['/static/css/base.css', '/static/css/tech.css', '/static/css/responsive.css'];
var CORE_SCRIPT_MARKERS = ['theme.js', 'symbol.js', 'page.js', 'nav.js', 'pwa.js', 'turbonav.js'];
var CORE_SCRIPT_RE = /theme\.js|symbol\.js|page\.js|nav\.js|pwa\.js|turbonav\.js/;
var pageCache = new Map();
var MAX_CACHE = 10;
var inflight = null;
@@ -21,12 +21,6 @@
return PERMANENT_CSS.some(function (p) { return href.indexOf(p) !== -1; });
}
function isCoreScript(el) {
var src = el.getAttribute('src') || '';
if (!src) return false;
return CORE_SCRIPT_MARKERS.some(function (m) { return src.indexOf(m) !== -1; });
}
function shouldTurboClick(e, link) {
if (!link || !link.href) return false;
if (e.defaultPrevented) return false;
@@ -55,13 +49,13 @@
if (!res.ok) throw new Error('HTTP ' + res.status);
return res.text();
}).then(function (html) {
var doc = parseHtml(html);
pageCache.set(key, doc);
var pack = { doc: parseHtml(html), html: html };
pageCache.set(key, pack);
if (pageCache.size > MAX_CACHE) {
var first = pageCache.keys().next().value;
pageCache.delete(first);
}
return doc;
return pack;
});
}
@@ -84,24 +78,42 @@
return fetchedMain.innerHTML;
}
function collectPageScripts(doc) {
var out = [];
var pastCore = false;
doc.body.querySelectorAll(':scope > script').forEach(function (el) {
if (isCoreScript(el)) {
if ((el.getAttribute('src') || '').indexOf('pwa.js') !== -1) pastCore = true;
return;
/** DOMParser strips inline script bodies — parse from raw HTML instead. */
function collectPageScripts(rawHtml) {
var scripts = [];
var bodyMatch = /<body[\s\S]*?>([\s\S]*)<\/body>/i.exec(rawHtml);
if (!bodyMatch) return scripts;
var body = bodyMatch[1];
var re = /<script(\s[^>]*)?>([\s\S]*?)<\/script>/gi;
var pastPwa = false;
var m;
while ((m = re.exec(body)) !== null) {
var attrs = m[1] || '';
var text = m[2];
var srcMatch = /\ssrc=["']([^"']+)["']/i.exec(attrs);
var typeMatch = /\stype=["']([^"']+)["']/i.exec(attrs);
var idMatch = /\sid=["']([^"']+)["']/i.exec(attrs);
var src = srcMatch ? srcMatch[1] : '';
var type = typeMatch ? typeMatch[1].toLowerCase() : 'text/javascript';
var id = idMatch ? idMatch[1] : '';
if (src) {
if (/pwa\.js/.test(src)) pastPwa = true;
if (CORE_SCRIPT_RE.test(src)) continue;
if (!pastPwa) continue;
scripts.push({ src: src, text: '', type: type, id: id });
continue;
}
if (!pastCore) return;
if (isCoreScript(el)) return;
out.push(el);
});
return out;
if (!pastPwa) continue;
scripts.push({ src: '', text: text, type: type, id: id });
}
return scripts;
}
function removePageAssets() {
document.querySelectorAll('[data-page-css]').forEach(function (el) { el.remove(); });
document.querySelectorAll('body script[data-page-js]').forEach(function (el) { el.remove(); });
document.querySelectorAll('body script[data-page-js], body script[data-page-data]').forEach(function (el) {
el.remove();
});
}
function applyPageCss(items) {
@@ -124,24 +136,37 @@
}
function runPageScripts(items) {
return items.reduce(function (chain, srcEl) {
return items.reduce(function (chain, item) {
return chain.then(function () {
return new Promise(function (resolve) {
var s = document.createElement('script');
s.setAttribute('data-page-js', '');
var src = srcEl.getAttribute('src');
if (src) {
var bust = (src.indexOf('?') >= 0 ? '&' : '?') + '_turbo=' + Date.now();
s.src = src + bust;
if (item.src) {
s.setAttribute('data-page-js', '');
var bust = (item.src.indexOf('?') >= 0 ? '&' : '?') + '_turbo=' + Date.now();
s.src = item.src + bust;
s.async = false;
s.onload = function () { resolve(); };
s.onerror = function () { resolve(); };
document.body.appendChild(s);
} else {
s.textContent = srcEl.textContent;
return;
}
if (item.type === 'application/json') {
s.type = 'application/json';
if (item.id) s.id = item.id;
s.setAttribute('data-page-data', '');
s.textContent = item.text;
document.body.appendChild(s);
resolve();
return;
}
if (item.type && item.type !== 'text/javascript' && item.type !== 'module') {
resolve();
return;
}
s.setAttribute('data-page-js', '');
s.textContent = item.text;
document.body.appendChild(s);
resolve();
});
});
}, Promise.resolve());
@@ -166,7 +191,9 @@
if (main) main.classList.toggle('nav-loading', on);
}
function applyDocument(doc, url, fromPopstate) {
function applyDocument(pack, url, fromPopstate) {
var doc = pack.doc;
var rawHtml = pack.html;
var main = document.querySelector('.main');
if (!main || !doc.querySelector('.main')) {
window.location.href = url;
@@ -178,12 +205,13 @@
var cssItems = collectPageCss(doc);
var mainHtml = prepareMainHtml(doc);
var scriptItems = collectPageScripts(rawHtml);
return applyPageCss(cssItems).then(function () {
main.innerHTML = mainHtml;
document.title = doc.title || document.title;
syncNavActive(doc, url);
return runPageScripts(collectPageScripts(doc));
return runPageScripts(scriptItems);
}).then(function () {
currentUrl = normalizeUrl(url);
if (!fromPopstate) {
@@ -205,10 +233,10 @@
inflight = ctrl;
setLoading(true);
return fetchPage(target, ctrl.signal).then(function (doc) {
return fetchPage(target, ctrl.signal).then(function (pack) {
if (ctrl.signal.aborted) return;
inflight = null;
return applyDocument(doc, target, !!opts.fromPopstate);
return applyDocument(pack, target, !!opts.fromPopstate);
}).catch(function () {
if (ctrl.signal.aborted) return;
inflight = null;
+1 -1
View File
@@ -2,7 +2,7 @@
* 专有软件 — 未经授权禁止复制、传播、转售。
* 详见 LICENSE.zh-CN.txt
*/
var CACHE_VERSION = 'qihuo-v7';
var CACHE_VERSION = 'qihuo-v8';
var STATIC_CACHE = CACHE_VERSION + '-static';
var STATIC_ASSETS = [
'/static/css/base.css',
+2 -110
View File
@@ -212,7 +212,7 @@
{% endif %}
</p>
<form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form">
<form action="{{ url_for('settings') }}" method="post" id="ctp-settings-form" data-simnow-pwd-set="{{ '1' if ctp_cfg.simnow_password_set else '0' }}">
<input type="hidden" name="action" value="ctp">
<div class="settings-ctp-auto card" style="margin-bottom:.85rem;padding:.75rem 1rem">
@@ -478,113 +478,5 @@
</div>
{% endblock %}
{% block extra_js %}
<script>
(function () {
var sel = document.getElementById('position-sizing-mode');
var lotsField = document.getElementById('field-fixed-lots');
var amountField = document.getElementById('field-fixed-amount');
function syncSizingFields() {
if (!sel) return;
var isAmount = sel.value === 'amount';
if (lotsField) lotsField.hidden = isAmount;
if (amountField) amountField.hidden = !isAmount;
}
if (sel) sel.addEventListener('change', syncSizingFields);
syncSizingFields();
var SETTINGS_FOLD_KEY = 'qihuo_settings_fold';
function setSettingsFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveSettingsFoldState() {
var state = {};
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
state[el.getAttribute('data-settings-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(SETTINGS_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadSettingsFoldState() {
try {
var raw = localStorage.getItem(SETTINGS_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-settings-fold]').forEach(function (el) {
var key = el.getAttribute('data-settings-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setSettingsFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-fold-head').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = btn.closest('[data-settings-fold]');
if (!panel) return;
setSettingsFold(panel, !panel.classList.contains('is-collapsed'));
saveSettingsFoldState();
});
});
loadSettingsFoldState();
var CTP_FOLD_KEY = 'qihuo_ctp_fold';
function setCtpFold(el, collapsed) {
if (!el) return;
el.classList.toggle('is-collapsed', collapsed);
var head = el.querySelector('.settings-ctp-fold-head');
if (head) head.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
}
function saveCtpFoldState() {
var state = {};
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
state[el.getAttribute('data-ctp-fold')] = el.classList.contains('is-collapsed');
});
try { localStorage.setItem(CTP_FOLD_KEY, JSON.stringify(state)); } catch (e) { /* ignore */ }
}
function loadCtpFoldState() {
try {
var raw = localStorage.getItem(CTP_FOLD_KEY);
if (!raw) return;
var state = JSON.parse(raw);
document.querySelectorAll('[data-ctp-fold]').forEach(function (el) {
var key = el.getAttribute('data-ctp-fold');
if (Object.prototype.hasOwnProperty.call(state, key)) {
setCtpFold(el, !!state[key]);
}
});
} catch (e) { /* ignore */ }
}
document.querySelectorAll('.settings-ctp-fold-head').forEach(function (btn) {
btn.addEventListener('click', function () {
var panel = btn.closest('[data-ctp-fold]');
if (!panel) return;
setCtpFold(panel, !panel.classList.contains('is-collapsed'));
saveCtpFoldState();
});
});
loadCtpFoldState();
var ctpForm = document.getElementById('ctp-settings-form');
if (ctpForm) {
ctpForm.addEventListener('submit', function (ev) {
var ctpCard = document.querySelector('[data-settings-fold="ctp"]');
if (ctpCard) setSettingsFold(ctpCard, false);
var simnowFold = document.querySelector('[data-ctp-fold="simnow"]');
if (simnowFold) setCtpFold(simnowFold, false);
var pwd = document.getElementById('simnow_password');
var pwdVal = pwd && pwd.value ? pwd.value.trim() : '';
var pwdWasSet = {{ 'true' if ctp_cfg.simnow_password_set else 'false' }};
if (pwdWasSet && !pwdVal) {
var ok = window.confirm(
'SimNow 交易密码为空,保存后不会更新密码(仍用旧密码)。\n\n'
+ '若快期已改密,请取消后在「交易密码」框手打新密码再保存。\n\n仍要保存其他项?'
);
if (!ok) ev.preventDefault();
}
});
}
})();
</script>
<script src="{{ url_for('static', filename='js/settings.js') }}"></script>
{% endblock %}
+9 -9
View File
@@ -227,14 +227,14 @@
</div>
{% endblock %}
{% block extra_js %}
<script>
window.TRADE_SIZING_MODE = {{ sizing_mode|tojson }};
window.MARKET_NAV_ENABLED = {{ nav_items.market|tojson }};
window.TRADE_FIXED_LOTS = {{ fixed_lots|tojson }};
window.TRADE_FIXED_AMOUNT = {{ fixed_amount|tojson }};
window.PRODUCT_CATEGORIES = {{ product_categories | default([]) | tojson }};
window.__RECOMMEND_ROWS__ = {{ recommend_rows | default([]) | tojson }};
window.CTP_AUTO_CONNECT = {{ ctp_auto_connect | tojson }};
</script>
<script type="application/json" id="trade-page-data">{{ {
'sizing_mode': sizing_mode,
'market_nav_enabled': nav_items.market,
'fixed_lots': fixed_lots,
'fixed_amount': fixed_amount,
'product_categories': product_categories | default([]),
'recommend_rows': recommend_rows | default([]),
'ctp_auto_connect': ctp_auto_connect
} | tojson }}</script>
<script src="{{ url_for('static', filename='js/trade.js') }}?v={{ asset_v }}"></script>
{% endblock %}