Files
crypto_monitor/static/instance_theme.js
T
2026-06-23 23:52:28 +08:00

774 lines
22 KiB
JavaScript

/**
* 四所实例主题:默认暗色;单独登录用 instance-theme;中控 iframe/SSO 随 hub-theme 联动。
*/
(function (global) {
const STANDALONE_KEY = "instance-theme";
const HUB_LINKED_THEME_KEY = "hub-linked-theme";
const META = { dark: "#0b0d14", light: "#d8e2ec" };
function normalize(theme) {
return theme === "light" ? "light" : "dark";
}
function isHubLinked() {
try {
const p = new URLSearchParams(location.search);
if (p.get("embed") === "1") return true;
const ht = p.get("hub_theme");
if (ht === "light" || ht === "dark") return true;
} catch (_) {}
try {
if (window.self !== window.top) return true;
} catch (_) {
return true;
}
return false;
}
function themeFromUrl() {
try {
const t = new URLSearchParams(location.search).get("hub_theme");
if (t === "light" || t === "dark") return t;
} catch (_) {}
return null;
}
function readLinkedThemeStorage() {
try {
const t = sessionStorage.getItem(HUB_LINKED_THEME_KEY);
if (t === "light" || t === "dark") return t;
} catch (_) {}
return null;
}
function writeLinkedThemeStorage(theme) {
if (!isHubLinked()) return;
try {
sessionStorage.setItem(HUB_LINKED_THEME_KEY, normalize(theme));
} catch (_) {}
}
function getStandalone() {
try {
return normalize(localStorage.getItem(STANDALONE_KEY));
} catch (_) {
return "dark";
}
}
function setStandalone(theme) {
try {
localStorage.setItem(STANDALONE_KEY, normalize(theme));
} catch (_) {}
}
let _linkedTheme = null;
let _appliedTheme = null;
function get() {
if (isHubLinked()) {
return themeFromUrl() || _linkedTheme || readLinkedThemeStorage() || "dark";
}
return getStandalone();
}
/** 模板内联暗色 → 亮色(切换时重写 style 属性) */
const INLINE_HEX_LIGHT = {
"#cfd3ef": "#1a2838",
"#8892b0": "#4a6078",
"#9aa3c4": "#4a6078",
"#8b95a8": "#4a6078",
"#8b95b8": "#4a6078",
"#6a7598": "#4a6078",
"#7d8799": "#4a6078",
"#6d7689": "#4a6078",
"#dbe4ff": "#142232",
"#f0f2ff": "#142232",
"#e8ecf4": "#142232",
"#c5cce0": "#4a6078",
"#b8c4ff": "#142232",
"#8fc8ff": "#006e9a",
"#6ab8ff": "#006e9a",
"#6eb5ff": "#006e9a",
"#101522": "#ffffff",
"#121726": "#ffffff",
"#141423": "#ffffff",
"#24243b": "#b8c8d8",
"#252a45": "#b8c8d8",
"#252538": "#eef3f8",
"#1a1a29": "#f6f9fc",
"#2e2e45": "#b8c8d8",
"#2b2b43": "#d0dae4",
"#151a2a": "#eef3f8",
"#141a2a": "#ffffff",
"#141923": "#ffffff",
"#141a2e": "#ffffff",
"#0f1424": "#f6f9fc",
"#0f1420": "#f6f9fc",
"#0f1117": "#d8e2ec",
"#1a2034": "#eef3f8",
"#1a2030": "#ffffff",
"#1f3a5a": "#e8eef5",
"#2f2f44": "#dde5ec",
"#2a3f6c": "rgba(0,110,154,0.14)",
"#304164": "rgba(0,95,140,0.22)",
"#2a3150": "#b8c8d8",
"#2a3152": "#b8c8d8",
"#3a5a8a": "rgba(0,95,140,0.35)",
"#2a3348": "#b8c8d8",
"#243050": "rgba(0,75,115,0.16)",
"#2a3558": "#d0dae4",
"#3a4468": "#c8d4e0",
"#3a4a66": "#b8c8d8",
"#3a3f52": "#dde5ec",
"#3d4659": "#b8c8d8",
"#1f2740": "#eef3f8",
"#1f2a44": "rgba(0,110,154,0.1)",
"#1f4a3a": "#e8f5ef",
"#2a4a7a": "#e8eef5",
"#3a3048": "#eef3f8",
"#d4b8ff": "#5b4fc7",
"#e6e8ef": "#1a2838",
};
function remapInlineStyle(style, theme) {
if (!style) return style;
if (theme !== "light") return style;
const hadSecondaryBtnBg = /#1f3a5a/i.test(style);
let out = style;
for (const [from, to] of Object.entries(INLINE_HEX_LIGHT)) {
out = out.replace(new RegExp(from.replace("#", "\\#"), "gi"), to);
}
if (hadSecondaryBtnBg && !/color\s*:/i.test(style)) {
out = `${out.replace(/;+\s*$/, "")};color:#006e9a`;
}
return out;
}
function syncInlineStyles(theme, root) {
const scope = root || document;
scope.querySelectorAll("[style]").forEach((el) => {
const raw = el.getAttribute("style");
if (!raw) return;
if (!el.dataset.instStyleBase) {
el.dataset.instStyleBase = raw;
}
const base = el.dataset.instStyleBase;
el.setAttribute("style", theme === "light" ? remapInlineStyle(base, "light") : base);
});
}
function mergeHubQueryIntoHref(href, theme) {
if (!href || href.startsWith("#") || href.startsWith("javascript:")) return href;
try {
const u = new URL(href, location.origin);
if (u.origin !== location.origin) return href;
if (isHubLinked()) {
u.searchParams.set("embed", "1");
if (theme === "light" || theme === "dark") {
u.searchParams.set("hub_theme", theme);
}
}
return u.pathname + u.search + u.hash;
} catch (_) {
return href;
}
}
function patchHubNavLinks(theme) {
if (!isHubLinked()) return;
const t = normalize(theme || get());
document
.querySelectorAll(".top-nav a[href], .strategy-subnav a[href]")
.forEach((a) => {
const href = a.getAttribute("href");
if (!href) return;
const next = mergeHubQueryIntoHref(href, t);
if (next !== href) a.setAttribute("href", next);
});
}
function apply(theme, opts) {
const options = opts || {};
const linked = isHubLinked();
const t = normalize(theme);
const root = document.documentElement;
const unchanged =
!options.force &&
_appliedTheme === t &&
root.getAttribute("data-theme") === t;
if (unchanged) {
return t;
}
_appliedTheme = t;
if (linked) {
_linkedTheme = t;
writeLinkedThemeStorage(t);
root.setAttribute("data-hub-linked", "1");
} else {
root.removeAttribute("data-hub-linked");
}
if (!linked && !options.skipStore) {
setStandalone(t);
}
root.setAttribute("data-theme", t);
const meta = document.querySelector('meta[name="theme-color"]');
if (meta) meta.setAttribute("content", META[t]);
root.style.colorScheme = t;
if (document.body) {
syncInlineStyles(t);
patchHubNavLinks(t);
} else {
document.addEventListener(
"DOMContentLoaded",
function onDom() {
syncInlineStyles(t);
patchHubNavLinks(t);
},
{ once: true }
);
}
syncToggleUI();
document.dispatchEvent(
new CustomEvent("instance-theme-change", { detail: { theme: t, hubLinked: linked } })
);
return t;
}
function syncToggleUI(root) {
const scope = root || document;
const linked = isHubLinked();
const toggle = scope.querySelector(".instance-theme-toggle");
if (toggle) {
toggle.classList.toggle("is-hub-linked", linked);
toggle.setAttribute("aria-hidden", linked ? "true" : "false");
}
if (linked) return;
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
const on = btn.getAttribute("data-theme-value") === getStandalone();
btn.classList.toggle("is-active", on);
btn.setAttribute("aria-pressed", on ? "true" : "false");
});
}
function initToggleUI(root) {
const scope = root || document;
syncToggleUI(scope);
scope.querySelectorAll(".theme-toggle-btn[data-theme-value]").forEach((btn) => {
if (btn.dataset.themeBound === "1") return;
btn.dataset.themeBound = "1";
btn.addEventListener("click", () => {
if (isHubLinked()) return;
apply(btn.getAttribute("data-theme-value"));
});
});
}
function initMobileTopNav() {
const mq = window.matchMedia("(max-width: 720px)");
function scrollActiveTab(nav) {
const active = nav.querySelector("a.active");
if (!active) return;
requestAnimationFrame(() => {
try {
active.scrollIntoView({ inline: "center", block: "nearest", behavior: "instant" });
} catch (_) {
active.scrollIntoView(false);
}
});
}
function apply() {
if (!mq.matches) return;
if (consumeSoftNavPending()) return;
document.querySelectorAll(".top-nav").forEach(scrollActiveTab);
}
apply();
mq.addEventListener("change", apply);
window.addEventListener("resize", apply);
window.addEventListener("orientationchange", apply);
}
function initFromHubMessage(data) {
if (!data || data.type !== "hub-theme-sync") return;
if (!isHubLinked()) return;
apply(data.theme, { skipStore: true });
}
/** 交易记录页:核对开关与按钮 disabled 保持同步(iframe 软导航/表单恢复后不触发 change) */
function syncReviewEditButtons() {
const toggle = document.getElementById("review-mode-toggle");
if (!toggle) return;
const on = !!toggle.checked;
document.querySelectorAll(".review-edit-btn").forEach((btn) => {
btn.disabled = !on;
});
}
function initReviewEditModeSync() {
const toggle = document.getElementById("review-mode-toggle");
if (!toggle) return;
if (toggle.dataset.instReviewModeBound !== "1") {
toggle.dataset.instReviewModeBound = "1";
toggle.addEventListener("input", () => {
if (typeof global.toggleReviewMode === "function") global.toggleReviewMode();
else syncReviewEditButtons();
});
}
const run = () => {
if (typeof global.toggleReviewMode === "function") global.toggleReviewMode();
else syncReviewEditButtons();
};
run();
requestAnimationFrame(run);
setTimeout(run, 0);
if (!global.__instReviewModePageshowBound) {
global.__instReviewModePageshowBound = true;
window.addEventListener("pageshow", run);
}
}
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}`;
});
}
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(() => {
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) {
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) {
let path = href;
try {
const u = new URL(href, location.href);
path = u.pathname + u.search + u.hash;
} 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;
}
}
document.addEventListener(
"click",
(ev) => {
const a = ev.target.closest("a[href]");
if (!a || !isSoftNavLink(a) || ev.defaultPrevented) return;
if (ev.button !== 0 || ev.ctrlKey || ev.metaKey || ev.shiftKey || ev.altKey) return;
const rawHref = a.getAttribute("href");
if (!rawHref || rawHref.startsWith("#") || rawHref.startsWith("javascript:")) return;
let target;
try {
target = new URL(rawHref, location.href);
} catch (_) {
return;
}
if (target.origin !== location.origin) return;
const nextHref = target.pathname + target.search + target.hash;
if (target.pathname === location.pathname && target.search === location.search) return;
ev.preventDefault();
void navigateInFrame(nextHref);
},
true
);
window.addEventListener("popstate", () => {
void navigateInFrame(location.pathname + location.search + location.hash, { replace: true });
});
}
function boot() {
if (isHubLinked()) {
apply(get(), { skipStore: true });
window.addEventListener("message", (ev) => initFromHubMessage(ev.data));
try {
window.parent.postMessage({ type: "instance-theme-ready" }, "*");
} catch (_) {}
} else {
apply(getStandalone());
}
initInstanceTopNavSoft();
seedCurrentPageCache();
function observeDynamicLists() {
["journal-list", "review-list"].forEach((id) => {
const el = document.getElementById(id);
if (!el || el.dataset.instThemeObserved === "1") return;
el.dataset.instThemeObserved = "1";
new MutationObserver(() => {
syncInlineStyles(get());
patchHubNavLinks(get());
}).observe(el, {
childList: true,
subtree: true,
});
});
}
const onReady = () => {
initToggleUI();
initMobileTopNav();
initReviewEditModeSync();
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);
} else {
onReady();
}
document.addEventListener("instance-theme-change", (ev) => {
const t = ev.detail && ev.detail.theme;
if (t) {
syncInlineStyles(t);
patchHubNavLinks(t);
}
});
}
boot();
global.InstanceTheme = {
STANDALONE_KEY,
HUB_LINKED_THEME_KEY,
isHubLinked,
get,
apply,
initToggleUI,
syncToggleUI,
syncInlineStyles,
patchHubNavLinks,
mergeHubQueryIntoHref,
syncReviewEditButtons,
initReviewEditModeSync,
setPageCacheDays,
clearPageCache,
getPageCacheMaxAgeMs,
refreshInstanceLiveData,
};
})(typeof window !== "undefined" ? window : globalThis);