Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
274 lines
11 KiB
JavaScript
274 lines
11 KiB
JavaScript
(function exposeApplicationShell(global) {
|
|
"use strict";
|
|
|
|
const SIDEBAR_STORAGE_KEY = "xiaobai-sidebar-collapsed";
|
|
const MOBILE_BREAKPOINT = 767;
|
|
|
|
function isMobileViewport() {
|
|
return global.matchMedia(`(max-width: ${MOBILE_BREAKPOINT}px)`).matches;
|
|
}
|
|
|
|
function create(options) {
|
|
const state = options.state;
|
|
const registry = options.pages;
|
|
let initialized = false;
|
|
|
|
function syncViewportMode() {
|
|
document.body.classList.toggle("mobile-shell", isMobileViewport());
|
|
}
|
|
|
|
function toggleHeaderCommandMenu(force) {
|
|
const menu = document.querySelector("#headerCommandGroup");
|
|
const button = document.querySelector("#headerMenuButton");
|
|
const backdrop = document.querySelector("#mobileCommandBackdrop");
|
|
if (!menu || !button) return;
|
|
if (!isMobileViewport()) {
|
|
menu.classList.remove("is-open");
|
|
button.setAttribute("aria-expanded", "false");
|
|
document.body.classList.remove("mobile-command-open");
|
|
if (backdrop) backdrop.hidden = true;
|
|
return;
|
|
}
|
|
const open = typeof force === "boolean" ? force : !menu.classList.contains("is-open");
|
|
menu.classList.toggle("is-open", open);
|
|
button.setAttribute("aria-expanded", String(open));
|
|
document.body.classList.toggle("mobile-command-open", open);
|
|
if (backdrop) backdrop.hidden = !open;
|
|
if (open) {
|
|
global.requestAnimationFrame(() => menu.querySelector("button:not([hidden])")?.focus({ preventScroll: true }));
|
|
} else if (force === false && document.activeElement && menu.contains(document.activeElement)) {
|
|
button.focus({ preventScroll: true });
|
|
}
|
|
}
|
|
|
|
function updateSidebarControl() {
|
|
const button = document.querySelector("#sidebarCollapseButton");
|
|
if (!button) return;
|
|
const automaticallyCollapsed = global.innerWidth <= 1023 && !isMobileViewport();
|
|
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 pageGroupLabel(page) {
|
|
return page?.group === "market"
|
|
? "市场复盘"
|
|
: page?.group === "personal" ? "个人" : "智能工具";
|
|
}
|
|
|
|
function writePageSubtitle(viewId) {
|
|
const pageSubtitle = document.querySelector("#currentPageSubtitle");
|
|
if (!pageSubtitle) return;
|
|
const page = registry.get(viewId);
|
|
const dateText = options.tradeDate?.() || "";
|
|
const hasDate = Boolean(dateText) && dateText !== "--";
|
|
if (viewId === "mentorView") {
|
|
pageSubtitle.textContent = hasDate
|
|
? `与不同交易思维模型持续对话 · 数据日期 ${dateText}`
|
|
: "与不同交易思维模型持续对话 · 数据日期 --";
|
|
return;
|
|
}
|
|
const groupLabel = pageGroupLabel(page);
|
|
pageSubtitle.textContent = hasDate ? `${groupLabel} · ${dateText}` : groupLabel;
|
|
}
|
|
|
|
function syncNavigation(viewId) {
|
|
const page = registry.get(viewId);
|
|
const navigationId = page?.navigation_alias || viewId;
|
|
const marketView = page?.group === "market";
|
|
document.body.dataset.activeView = viewId;
|
|
const mobileGroup = document.querySelector("#mobilePageGroup");
|
|
const mobileTitle = document.querySelector("#mobilePageTitle");
|
|
if (mobileGroup) {
|
|
mobileGroup.textContent = page?.group === "market"
|
|
? "行情"
|
|
: page?.group === "personal" ? "复盘" : "工具";
|
|
}
|
|
if (mobileTitle) mobileTitle.textContent = page?.title || "小白复盘";
|
|
const pageTitle = document.querySelector("#currentPageTitle");
|
|
if (pageTitle) pageTitle.textContent = page?.title || "小白复盘";
|
|
writePageSubtitle(viewId);
|
|
document.querySelectorAll(".module-tab").forEach((button) => {
|
|
button.classList.toggle("active", button.dataset.view === navigationId);
|
|
button.classList.toggle(
|
|
"mobile-active",
|
|
isMobileViewport()
|
|
&& 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);
|
|
syncViewportMode();
|
|
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("#mobileCommandBackdrop")?.addEventListener("click", () => {
|
|
toggleHeaderCommandMenu(false);
|
|
});
|
|
document.querySelectorAll("[data-mobile-command-target]").forEach((button) => {
|
|
button.addEventListener("click", () => {
|
|
const target = document.getElementById(button.dataset.mobileCommandTarget || "");
|
|
toggleHeaderCommandMenu(false);
|
|
target?.click();
|
|
});
|
|
});
|
|
document.querySelector("#headerCommandGroup")?.addEventListener("click", (event) => {
|
|
if (event.target.closest("button") && !event.target.closest(".account-menu-shell")) {
|
|
toggleHeaderCommandMenu(false);
|
|
}
|
|
});
|
|
function setOverviewExpanded(expanded) {
|
|
const overview = document.querySelector(".overview-strip");
|
|
const button = document.querySelector("#overviewToggle");
|
|
if (!overview || !button) return;
|
|
overview.dataset.overviewExpanded = String(expanded);
|
|
button.setAttribute("aria-expanded", String(expanded));
|
|
button.title = expanded ? "收起市场详情" : "展开市场详情";
|
|
const label = button.querySelector("span");
|
|
if (label) label.textContent = "详情";
|
|
button.querySelector("i")?.setAttribute("data-lucide", expanded ? "chevron-up" : "chevron-down");
|
|
options.refreshIcons?.();
|
|
}
|
|
|
|
document.querySelector("#overviewToggle")?.addEventListener("click", (event) => {
|
|
event.stopPropagation();
|
|
const overview = document.querySelector(".overview-strip");
|
|
if (!overview) return;
|
|
const expanded = overview.dataset.overviewExpanded !== "true";
|
|
setOverviewExpanded(expanded);
|
|
if (expanded) toggleHeaderCommandMenu(false);
|
|
});
|
|
document.addEventListener("click", (event) => {
|
|
if (!event.target.closest("#headerCommandGroup, #headerMenuButton")) toggleHeaderCommandMenu(false);
|
|
if (!event.target.closest(".overview-strip")) setOverviewExpanded(false);
|
|
});
|
|
document.addEventListener("keydown", (event) => {
|
|
if (event.key !== "Escape") return;
|
|
toggleHeaderCommandMenu(false);
|
|
setOverviewExpanded(false);
|
|
});
|
|
global.addEventListener("resize", () => {
|
|
toggleHeaderCommandMenu(false);
|
|
syncViewportMode();
|
|
updateSidebarControl();
|
|
syncNavigation(state.activeView);
|
|
});
|
|
}
|
|
|
|
return Object.freeze({
|
|
initialize,
|
|
mount,
|
|
openModalDialog,
|
|
page: (viewId) => registry.get(viewId),
|
|
setPageStatus,
|
|
setStatus,
|
|
syncNavigation,
|
|
syncViewportMode,
|
|
toggleHeaderCommandMenu,
|
|
toggleSidebar,
|
|
updateSidebarControl,
|
|
});
|
|
}
|
|
|
|
global.XiaobaiShell = Object.freeze({ create });
|
|
})(window);
|