/** * 四所实例主题:默认暗色;单独登录用 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 = ``; if (out.includes("")) { out = out.replace("", `${guard}`); } else { out = guard + out; } out = out.replace(/]*)>/i, (m, attrs) => { if (/data-theme=/i.test(attrs)) { return m.replace(/data-theme="[^"]*"/i, `data-theme="${t}"`); } return ``; }); const overlay = ``; if (/]*>/i.test(out)) { out = out.replace(/]*)>/i, `${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);