migration: preserve frontend shell pages and styles
This commit is contained in:
@@ -0,0 +1,194 @@
|
||||
(function exposeApplicationShell(global) {
|
||||
"use strict";
|
||||
|
||||
const SIDEBAR_STORAGE_KEY = "xiaobai-sidebar-collapsed";
|
||||
|
||||
function create(options) {
|
||||
const state = options.state;
|
||||
const registry = options.pages;
|
||||
let initialized = false;
|
||||
|
||||
function toggleHeaderCommandMenu(force) {
|
||||
const menu = document.querySelector("#headerCommandGroup");
|
||||
const button = document.querySelector("#headerMenuButton");
|
||||
if (!menu || !button) return;
|
||||
const open = typeof force === "boolean" ? force : !menu.classList.contains("is-open");
|
||||
menu.classList.toggle("is-open", open);
|
||||
button.setAttribute("aria-expanded", String(open));
|
||||
}
|
||||
|
||||
function updateSidebarControl() {
|
||||
const button = document.querySelector("#sidebarCollapseButton");
|
||||
if (!button) return;
|
||||
const automaticallyCollapsed = global.innerWidth <= 1023 && global.innerWidth > 720;
|
||||
const collapsed = document.body.classList.contains("sidebar-collapsed") || automaticallyCollapsed;
|
||||
button.setAttribute("aria-expanded", String(!collapsed));
|
||||
button.setAttribute("aria-label", collapsed ? "展开侧栏" : "收起侧栏");
|
||||
button.title = collapsed ? "展开侧栏" : "收起侧栏";
|
||||
const label = button.querySelector("span");
|
||||
if (label) label.textContent = collapsed ? "展开侧栏" : "收起侧栏";
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
const collapsed = document.body.classList.toggle("sidebar-collapsed");
|
||||
try {
|
||||
global.localStorage.setItem(SIDEBAR_STORAGE_KEY, collapsed ? "1" : "0");
|
||||
} catch (_error) {
|
||||
// The shell remains usable when storage is unavailable.
|
||||
}
|
||||
updateSidebarControl();
|
||||
}
|
||||
|
||||
function syncNavigation(viewId) {
|
||||
const page = registry.get(viewId);
|
||||
const navigationId = page?.navigation_alias || viewId;
|
||||
const marketView = page?.group === "market";
|
||||
document.body.dataset.activeView = viewId;
|
||||
document.querySelectorAll(".module-tab").forEach((button) => {
|
||||
button.classList.toggle("active", button.dataset.view === navigationId);
|
||||
button.classList.toggle(
|
||||
"mobile-active",
|
||||
global.innerWidth <= 720
|
||||
&& marketView
|
||||
&& button.dataset.view === "limitPool"
|
||||
&& viewId !== "limitPool",
|
||||
);
|
||||
});
|
||||
const selector = document.querySelector("#mobileMarketSelector");
|
||||
const select = document.querySelector("#mobileMarketViewSelect");
|
||||
if (selector) selector.hidden = !marketView;
|
||||
if (select && marketView) select.value = viewId;
|
||||
toggleHeaderCommandMenu(false);
|
||||
options.onNavigationSync?.(viewId);
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
const status = document.querySelector("#statusText");
|
||||
if (status) status.textContent = text;
|
||||
}
|
||||
|
||||
function setPageStatus(viewId, tradeDate = "") {
|
||||
const page = registry.get(viewId);
|
||||
const label = page?.title || "小白复盘";
|
||||
setStatus(tradeDate && tradeDate !== "--" ? `${label} · 数据日期 ${tradeDate}` : `${label} · 等待数据`);
|
||||
}
|
||||
|
||||
function openModalDialog(dialog) {
|
||||
if (!(dialog instanceof HTMLDialogElement)) return;
|
||||
document.querySelectorAll("dialog[open]").forEach((openDialog) => {
|
||||
if (openDialog !== dialog) openDialog.close();
|
||||
});
|
||||
if (!dialog.open) dialog.showModal();
|
||||
}
|
||||
|
||||
function mount(viewId, mountOptions = {}) {
|
||||
const page = registry.get(viewId);
|
||||
const view = document.getElementById(viewId);
|
||||
if (!page || !view?.classList.contains("workspace-view")) return false;
|
||||
const previousView = state.activeView;
|
||||
options.onBeforeMount?.(viewId, previousView);
|
||||
state.activeView = viewId;
|
||||
document.querySelectorAll(".workspace-view").forEach((candidate) => {
|
||||
const active = candidate.id === viewId;
|
||||
candidate.classList.toggle("active-view", active);
|
||||
candidate.classList.remove("view-entering");
|
||||
if (active && options.motionEnabled?.()) {
|
||||
void candidate.offsetWidth;
|
||||
candidate.classList.add("view-entering");
|
||||
candidate.addEventListener(
|
||||
"animationend",
|
||||
() => candidate.classList.remove("view-entering"),
|
||||
{ once: true },
|
||||
);
|
||||
const body = candidate.querySelector("tbody");
|
||||
if (body) options.animateRows?.(body);
|
||||
}
|
||||
});
|
||||
syncNavigation(viewId);
|
||||
setPageStatus(viewId, options.tradeDate?.() || "");
|
||||
if (mountOptions.updateUrl !== false) {
|
||||
const url = new URL(global.location.href);
|
||||
url.searchParams.set("view", viewId);
|
||||
url.hash = "";
|
||||
global.history.replaceState(null, "", url);
|
||||
}
|
||||
global.scrollTo({ top: 0, behavior: "auto" });
|
||||
options.onAfterMount?.(viewId, previousView);
|
||||
return true;
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
if (initialized) return;
|
||||
initialized = true;
|
||||
let collapsed = false;
|
||||
try {
|
||||
collapsed = global.localStorage.getItem(SIDEBAR_STORAGE_KEY) === "1";
|
||||
} catch (_error) {
|
||||
collapsed = false;
|
||||
}
|
||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
updateSidebarControl();
|
||||
syncNavigation(state.activeView);
|
||||
document.querySelectorAll(".module-tab").forEach((button) => {
|
||||
button.addEventListener("click", () => options.onNavigate?.(button.dataset.view));
|
||||
});
|
||||
document.querySelectorAll("[data-open-view]").forEach((button) => {
|
||||
button.addEventListener("click", () => options.onNavigate?.(button.dataset.openView));
|
||||
});
|
||||
document.querySelector("#mobileMarketViewSelect")?.addEventListener("change", (event) => {
|
||||
options.onNavigate?.(event.target.value);
|
||||
});
|
||||
document.querySelector("#sidebarCollapseButton")?.addEventListener("click", toggleSidebar);
|
||||
document.querySelector("#headerMenuButton")?.addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
toggleHeaderCommandMenu();
|
||||
});
|
||||
document.querySelector("#headerCommandGroup")?.addEventListener("click", (event) => {
|
||||
if (event.target.closest("button") && !event.target.closest(".account-menu-shell")) {
|
||||
toggleHeaderCommandMenu(false);
|
||||
}
|
||||
});
|
||||
document.querySelector("#overviewToggle")?.addEventListener("click", (event) => {
|
||||
const overview = document.querySelector(".overview-strip");
|
||||
if (!overview) return;
|
||||
const expanded = overview.dataset.overviewExpanded !== "true";
|
||||
overview.dataset.overviewExpanded = String(expanded);
|
||||
event.currentTarget.setAttribute("aria-expanded", String(expanded));
|
||||
event.currentTarget.title = expanded ? "收起市场详情" : "展开市场详情";
|
||||
const label = event.currentTarget.querySelector("span");
|
||||
if (label) label.textContent = expanded ? "收起详情" : "展开详情";
|
||||
event.currentTarget.querySelector("i")?.setAttribute(
|
||||
"data-lucide",
|
||||
expanded ? "chevron-up" : "chevron-down",
|
||||
);
|
||||
options.refreshIcons?.();
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest(".header-actions")) toggleHeaderCommandMenu(false);
|
||||
});
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") toggleHeaderCommandMenu(false);
|
||||
});
|
||||
global.addEventListener("resize", () => {
|
||||
if (global.innerWidth > 720) toggleHeaderCommandMenu(false);
|
||||
updateSidebarControl();
|
||||
syncNavigation(state.activeView);
|
||||
});
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
initialize,
|
||||
mount,
|
||||
openModalDialog,
|
||||
page: (viewId) => registry.get(viewId),
|
||||
setPageStatus,
|
||||
setStatus,
|
||||
syncNavigation,
|
||||
toggleHeaderCommandMenu,
|
||||
toggleSidebar,
|
||||
updateSidebarControl,
|
||||
});
|
||||
}
|
||||
|
||||
global.XiaobaiShell = Object.freeze({ create });
|
||||
})(window);
|
||||
Reference in New Issue
Block a user