|
|
|
@@ -281,7 +281,6 @@
|
|
|
|
|
|
|
|
|
|
function apply() {
|
|
|
|
|
if (!mq.matches) return;
|
|
|
|
|
if (consumeSoftNavPending()) return;
|
|
|
|
|
document.querySelectorAll(".top-nav").forEach(scrollActiveTab);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -330,257 +329,28 @@
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function remapInlineStylesInHtml(html) {
|
|
|
|
|
if (!html) return html;
|
|
|
|
|
return html.replace(/\sstyle=(["'])([\s\S]*?)\1/gi, (full, q, style) => {
|
|
|
|
|
return ` style=${q}${remapInlineStyle(style, "light")}${q}`;
|
|
|
|
|
});
|
|
|
|
|
/** 仅中控 iframe 内:fetch + document.write 换页,避免 iframe 整页卸载白屏。单独打开实例仍走浏览器正常跳转。 */
|
|
|
|
|
function initHubEmbedInFrameNav() {
|
|
|
|
|
if (!isHubLinked()) return;
|
|
|
|
|
|
|
|
|
|
let navToken = 0;
|
|
|
|
|
|
|
|
|
|
function isSoftNavLink(a) {
|
|
|
|
|
if (!a || !a.getAttribute) return false;
|
|
|
|
|
return !!a.closest(".top-nav, .strategy-subnav");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function prepareSoftNavHtml(html, theme) {
|
|
|
|
|
const t = normalize(theme || get());
|
|
|
|
|
const bg = META[t];
|
|
|
|
|
let out = html || "";
|
|
|
|
|
if (t === "light") {
|
|
|
|
|
out = remapInlineStylesInHtml(out);
|
|
|
|
|
}
|
|
|
|
|
const guard = `<style id="inst-nav-guard">html,body{background:${bg}!important;color-scheme:${t};}#inst-nav-overlay{position:fixed;inset:0;z-index:2147483647;background:${bg};pointer-events:none;opacity:1;}</style>`;
|
|
|
|
|
if (out.includes("</head>")) {
|
|
|
|
|
out = out.replace("</head>", `${guard}</head>`);
|
|
|
|
|
} else {
|
|
|
|
|
out = guard + out;
|
|
|
|
|
}
|
|
|
|
|
out = out.replace(/<html([^>]*)>/i, (m, attrs) => {
|
|
|
|
|
if (/data-theme=/i.test(attrs)) {
|
|
|
|
|
return m.replace(/data-theme="[^"]*"/i, `data-theme="${t}"`);
|
|
|
|
|
}
|
|
|
|
|
return `<html${attrs} data-theme="${t}">`;
|
|
|
|
|
});
|
|
|
|
|
const overlay = `<div id="inst-nav-overlay" aria-hidden="true" style="position:fixed;inset:0;z-index:2147483647;background:${bg};pointer-events:none;opacity:1"></div>`;
|
|
|
|
|
if (/<body[^>]*>/i.test(out)) {
|
|
|
|
|
out = out.replace(/<body([^>]*)>/i, `<body$1>${overlay}`);
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function ensureNavOverlay(theme) {
|
|
|
|
|
const t = normalize(theme || get());
|
|
|
|
|
const bg = META[t];
|
|
|
|
|
let el = document.getElementById("inst-nav-overlay");
|
|
|
|
|
if (!el) {
|
|
|
|
|
el = document.createElement("div");
|
|
|
|
|
el.id = "inst-nav-overlay";
|
|
|
|
|
el.setAttribute("aria-hidden", "true");
|
|
|
|
|
(document.body || document.documentElement).appendChild(el);
|
|
|
|
|
}
|
|
|
|
|
el.style.cssText = `position:fixed;inset:0;z-index:2147483647;background:${bg};pointer-events:none;opacity:1;`;
|
|
|
|
|
return el;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function dismissNavOverlay() {
|
|
|
|
|
const el = document.getElementById("inst-nav-overlay");
|
|
|
|
|
if (!el) return;
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
requestAnimationFrame(() => {
|
|
|
|
|
el.style.transition = "opacity 90ms ease";
|
|
|
|
|
el.style.opacity = "0";
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
async function navigateInFrame(href, opts) {
|
|
|
|
|
const token = ++navToken;
|
|
|
|
|
try {
|
|
|
|
|
el.remove();
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}, 120);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function markSoftNavPending() {
|
|
|
|
|
try {
|
|
|
|
|
sessionStorage.setItem("inst-soft-nav", "1");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function consumeSoftNavPending() {
|
|
|
|
|
try {
|
|
|
|
|
if (sessionStorage.getItem("inst-soft-nav") === "1") {
|
|
|
|
|
sessionStorage.removeItem("inst-soft-nav");
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const PAGE_CACHE_PREFIX = "inst-pc:v1:";
|
|
|
|
|
const PAGE_CACHE_DAYS_KEY = "inst-page-cache-days";
|
|
|
|
|
const PAGE_CACHE_INDEX_KEY = "inst-page-cache-index";
|
|
|
|
|
const DEFAULT_PAGE_CACHE_DAYS = 7;
|
|
|
|
|
const MAX_PAGE_CACHE_ENTRIES = 16;
|
|
|
|
|
const CACHE_REVALIDATE_KEY = "inst-cache-revalidate";
|
|
|
|
|
|
|
|
|
|
function getPageCacheMaxAgeMs() {
|
|
|
|
|
try {
|
|
|
|
|
const days = parseInt(localStorage.getItem(PAGE_CACHE_DAYS_KEY) || "", 10);
|
|
|
|
|
if (Number.isFinite(days) && days > 0 && days <= 30) {
|
|
|
|
|
return days * 86400000;
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return DEFAULT_PAGE_CACHE_DAYS * 86400000;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setPageCacheDays(days) {
|
|
|
|
|
const d = parseInt(days, 10);
|
|
|
|
|
if (!Number.isFinite(d) || d <= 0 || d > 30) return DEFAULT_PAGE_CACHE_DAYS;
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(PAGE_CACHE_DAYS_KEY, String(d));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return d;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pageCacheStorageKey(cacheKey) {
|
|
|
|
|
return PAGE_CACHE_PREFIX + cacheKey;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function pageCacheKey(href) {
|
|
|
|
|
try {
|
|
|
|
|
const u = new URL(href, location.href);
|
|
|
|
|
return `${u.pathname}${u.search}|t=${get()}`;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return `${href}|t=${get()}`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readPageCacheIndex() {
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(PAGE_CACHE_INDEX_KEY);
|
|
|
|
|
const data = raw ? JSON.parse(raw) : null;
|
|
|
|
|
return Array.isArray(data) ? data : [];
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function writePageCacheIndex(rows) {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(PAGE_CACHE_INDEX_KEY, JSON.stringify(rows.slice(0, MAX_PAGE_CACHE_ENTRIES)));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function prunePageCache(aggressive) {
|
|
|
|
|
const maxAge = getPageCacheMaxAgeMs();
|
|
|
|
|
const now = Date.now();
|
|
|
|
|
let index = readPageCacheIndex().filter((row) => {
|
|
|
|
|
if (!row || !row.key || !row.savedAt) return false;
|
|
|
|
|
if (now - row.savedAt > maxAge) {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(pageCacheStorageKey(row.key));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return true;
|
|
|
|
|
});
|
|
|
|
|
index.sort((a, b) => (b.savedAt || 0) - (a.savedAt || 0));
|
|
|
|
|
if (aggressive || index.length > MAX_PAGE_CACHE_ENTRIES) {
|
|
|
|
|
index.slice(MAX_PAGE_CACHE_ENTRIES).forEach((row) => {
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(pageCacheStorageKey(row.key));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
});
|
|
|
|
|
index = index.slice(0, MAX_PAGE_CACHE_ENTRIES);
|
|
|
|
|
}
|
|
|
|
|
writePageCacheIndex(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function readPageCache(cacheKey) {
|
|
|
|
|
prunePageCache(false);
|
|
|
|
|
try {
|
|
|
|
|
const raw = localStorage.getItem(pageCacheStorageKey(cacheKey));
|
|
|
|
|
if (!raw) return null;
|
|
|
|
|
const entry = JSON.parse(raw);
|
|
|
|
|
if (!entry || typeof entry.html !== "string" || !entry.savedAt) return null;
|
|
|
|
|
if (Date.now() - entry.savedAt > getPageCacheMaxAgeMs()) {
|
|
|
|
|
localStorage.removeItem(pageCacheStorageKey(cacheKey));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return entry;
|
|
|
|
|
} catch (_) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function savePageCache(cacheKey, html) {
|
|
|
|
|
if (!html) return;
|
|
|
|
|
const savedAt = Date.now();
|
|
|
|
|
const payload = JSON.stringify({ html, savedAt, key: cacheKey });
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(pageCacheStorageKey(cacheKey), payload);
|
|
|
|
|
} catch (_) {
|
|
|
|
|
prunePageCache(true);
|
|
|
|
|
try {
|
|
|
|
|
localStorage.setItem(pageCacheStorageKey(cacheKey), payload);
|
|
|
|
|
} catch (e2) {
|
|
|
|
|
const r = await fetch(href, { credentials: "same-origin" });
|
|
|
|
|
if (token !== navToken) return;
|
|
|
|
|
if (!r.ok) {
|
|
|
|
|
location.href = href;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
let index = readPageCacheIndex().filter((row) => row && row.key !== cacheKey);
|
|
|
|
|
index.unshift({ key: cacheKey, savedAt });
|
|
|
|
|
writePageCacheIndex(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function clearPageCache() {
|
|
|
|
|
readPageCacheIndex().forEach((row) => {
|
|
|
|
|
if (!row || !row.key) return;
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(pageCacheStorageKey(row.key));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
localStorage.removeItem(PAGE_CACHE_INDEX_KEY);
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function markCacheRevalidatePending() {
|
|
|
|
|
try {
|
|
|
|
|
sessionStorage.setItem(CACHE_REVALIDATE_KEY, "1");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function consumeCacheRevalidatePending() {
|
|
|
|
|
try {
|
|
|
|
|
if (sessionStorage.getItem(CACHE_REVALIDATE_KEY) === "1") {
|
|
|
|
|
sessionStorage.removeItem(CACHE_REVALIDATE_KEY);
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function refreshInstanceLiveData() {
|
|
|
|
|
const fns = [
|
|
|
|
|
"refreshAccountSnapshot",
|
|
|
|
|
"refreshPriceSnapshotConditional",
|
|
|
|
|
"refreshPriceSnapshot",
|
|
|
|
|
"refreshOrderDefaults",
|
|
|
|
|
"loadJournals",
|
|
|
|
|
"loadReviews",
|
|
|
|
|
];
|
|
|
|
|
fns.forEach((name) => {
|
|
|
|
|
const fn = global[name];
|
|
|
|
|
if (typeof fn === "function") {
|
|
|
|
|
try {
|
|
|
|
|
fn();
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
try {
|
|
|
|
|
document.dispatchEvent(new CustomEvent("instance-page-revalidate"));
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scheduleLiveDataRefresh() {
|
|
|
|
|
window.setTimeout(refreshInstanceLiveData, 0);
|
|
|
|
|
window.setTimeout(refreshInstanceLiveData, 350);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateSoftNavHistory(href, opts) {
|
|
|
|
|
const html = await r.text();
|
|
|
|
|
if (token !== navToken) return;
|
|
|
|
|
let path = href;
|
|
|
|
|
try {
|
|
|
|
|
const u = new URL(href, location.href);
|
|
|
|
@@ -588,73 +358,11 @@
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
if (opts && opts.replace) history.replaceState(null, "", path);
|
|
|
|
|
else history.pushState(null, "", path);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function paintSoftNavDocument(html, href, opts) {
|
|
|
|
|
updateSoftNavHistory(href, opts);
|
|
|
|
|
document.open();
|
|
|
|
|
document.write(html);
|
|
|
|
|
document.close();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function seedCurrentPageCache() {
|
|
|
|
|
if (global.__instPageCacheSeeded) return;
|
|
|
|
|
global.__instPageCacheSeeded = true;
|
|
|
|
|
const href = location.pathname + location.search;
|
|
|
|
|
const cacheKey = pageCacheKey(href);
|
|
|
|
|
if (readPageCache(cacheKey)) return;
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
fetch(href, { credentials: "same-origin", cache: "no-store" })
|
|
|
|
|
.then((r) => (r.ok ? r.text() : null))
|
|
|
|
|
.then((html) => {
|
|
|
|
|
if (html) savePageCache(cacheKey, html);
|
|
|
|
|
})
|
|
|
|
|
.catch(() => {});
|
|
|
|
|
}, 1500);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function initInstanceTopNavSoft() {
|
|
|
|
|
prunePageCache(false);
|
|
|
|
|
let navToken = 0;
|
|
|
|
|
|
|
|
|
|
function isSoftNavLink(a) {
|
|
|
|
|
if (!a || !a.getAttribute) return false;
|
|
|
|
|
if (a.hasAttribute("download") || a.target === "_blank") return false;
|
|
|
|
|
return !!a.closest(".top-nav, .strategy-subnav");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function navigateInFrame(href, opts) {
|
|
|
|
|
const token = ++navToken;
|
|
|
|
|
const themeNow = get();
|
|
|
|
|
const cacheKey = pageCacheKey(href);
|
|
|
|
|
ensureNavOverlay(themeNow);
|
|
|
|
|
markSoftNavPending();
|
|
|
|
|
|
|
|
|
|
const cached = readPageCache(cacheKey);
|
|
|
|
|
let paintedFromCache = false;
|
|
|
|
|
if (cached && cached.html) {
|
|
|
|
|
markCacheRevalidatePending();
|
|
|
|
|
paintSoftNavDocument(prepareSoftNavHtml(cached.html, themeNow), href, opts);
|
|
|
|
|
paintedFromCache = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch(href, { credentials: "same-origin", cache: "no-store" });
|
|
|
|
|
if (token !== navToken) return;
|
|
|
|
|
if (!r.ok) {
|
|
|
|
|
if (!paintedFromCache) location.href = href;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const rawHtml = await r.text();
|
|
|
|
|
savePageCache(cacheKey, rawHtml);
|
|
|
|
|
if (token !== navToken) return;
|
|
|
|
|
if (!paintedFromCache) {
|
|
|
|
|
paintSoftNavDocument(prepareSoftNavHtml(rawHtml, themeNow), href, opts);
|
|
|
|
|
} else {
|
|
|
|
|
scheduleLiveDataRefresh();
|
|
|
|
|
}
|
|
|
|
|
} catch (_) {
|
|
|
|
|
if (!paintedFromCache && token === navToken) location.href = href;
|
|
|
|
|
if (token === navToken) location.href = href;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -686,18 +394,36 @@
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function purgeLegacySoftNavCache() {
|
|
|
|
|
try {
|
|
|
|
|
for (let i = localStorage.length - 1; i >= 0; i -= 1) {
|
|
|
|
|
const key = localStorage.key(i);
|
|
|
|
|
if (!key) continue;
|
|
|
|
|
if (
|
|
|
|
|
key.startsWith("inst-pc:") ||
|
|
|
|
|
key === "inst-page-cache-index" ||
|
|
|
|
|
key === "inst-page-cache-days"
|
|
|
|
|
) {
|
|
|
|
|
localStorage.removeItem(key);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
sessionStorage.removeItem("inst-soft-nav");
|
|
|
|
|
sessionStorage.removeItem("inst-cache-revalidate");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function boot() {
|
|
|
|
|
purgeLegacySoftNavCache();
|
|
|
|
|
if (isHubLinked()) {
|
|
|
|
|
apply(get(), { skipStore: true });
|
|
|
|
|
window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
|
|
|
|
|
initHubEmbedInFrameNav();
|
|
|
|
|
try {
|
|
|
|
|
window.parent.postMessage({ type: "instance-theme-ready" }, "*");
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
} else {
|
|
|
|
|
apply(getStandalone());
|
|
|
|
|
}
|
|
|
|
|
initInstanceTopNavSoft();
|
|
|
|
|
seedCurrentPageCache();
|
|
|
|
|
|
|
|
|
|
function observeDynamicLists() {
|
|
|
|
|
["journal-list", "review-list"].forEach((id) => {
|
|
|
|
@@ -721,20 +447,6 @@
|
|
|
|
|
syncInlineStyles(get());
|
|
|
|
|
patchHubNavLinks(get());
|
|
|
|
|
observeDynamicLists();
|
|
|
|
|
if (consumeCacheRevalidatePending()) {
|
|
|
|
|
scheduleLiveDataRefresh();
|
|
|
|
|
}
|
|
|
|
|
if (document.getElementById("inst-nav-overlay")) {
|
|
|
|
|
dismissNavOverlay();
|
|
|
|
|
window.setTimeout(() => {
|
|
|
|
|
const el = document.getElementById("inst-nav-overlay");
|
|
|
|
|
if (el) {
|
|
|
|
|
try {
|
|
|
|
|
el.remove();
|
|
|
|
|
} catch (_) {}
|
|
|
|
|
}
|
|
|
|
|
}, 800);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
if (document.readyState === "loading") {
|
|
|
|
|
document.addEventListener("DOMContentLoaded", onReady);
|
|
|
|
@@ -765,9 +477,5 @@
|
|
|
|
|
mergeHubQueryIntoHref,
|
|
|
|
|
syncReviewEditButtons,
|
|
|
|
|
initReviewEditModeSync,
|
|
|
|
|
setPageCacheDays,
|
|
|
|
|
clearPageCache,
|
|
|
|
|
getPageCacheMaxAgeMs,
|
|
|
|
|
refreshInstanceLiveData,
|
|
|
|
|
};
|
|
|
|
|
})(typeof window !== "undefined" ? window : globalThis);
|
|
|
|
|