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:
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user