refactor: establish shared frontend shell
This commit is contained in:
+25
-166
@@ -201,6 +201,19 @@ window.XiaobaiAPI.configure({
|
||||
onUnauthorized: () => showAuthGate("登录状态已失效,请重新登录。"),
|
||||
});
|
||||
|
||||
const applicationShell = window.XiaobaiShell.create({
|
||||
state,
|
||||
pages: window.XiaobaiPages,
|
||||
motionEnabled,
|
||||
animateRows,
|
||||
refreshIcons,
|
||||
tradeDate: () => displayCompactDate(
|
||||
state.dashboard?.meta?.trade_date || document.querySelector("#tradeDate")?.value || "",
|
||||
),
|
||||
onNavigate: (viewId) => openView(viewId),
|
||||
onNavigationSync: () => toggleAccountDropdown(false),
|
||||
});
|
||||
|
||||
const elements = {
|
||||
tradeDate: document.querySelector("#tradeDate"),
|
||||
loading: document.querySelector("#loadingOverlay"),
|
||||
@@ -225,11 +238,7 @@ const elements = {
|
||||
};
|
||||
|
||||
function openModalDialog(dialog) {
|
||||
if (!(dialog instanceof HTMLDialogElement)) return;
|
||||
document.querySelectorAll("dialog[open]").forEach((openDialog) => {
|
||||
if (openDialog !== dialog) openDialog.close();
|
||||
});
|
||||
if (!dialog.open) dialog.showModal();
|
||||
applicationShell.openModalDialog(dialog);
|
||||
}
|
||||
|
||||
const metricAnimationFrames = new WeakMap();
|
||||
@@ -250,20 +259,6 @@ let heartDustAnimationFrame = 0;
|
||||
let heartDustParticles = [];
|
||||
let heartIncenseAnimation = null;
|
||||
const heartCoinRotations = [0, 0, 0];
|
||||
const MARKET_VIEWS = new Set([
|
||||
"limitPool",
|
||||
"brokenView",
|
||||
"downView",
|
||||
"yesterdayView",
|
||||
"performanceView",
|
||||
"sentimentCycleView",
|
||||
"auctionView",
|
||||
"ladderView",
|
||||
"rotationView",
|
||||
"themeLibraryView",
|
||||
"popularityView",
|
||||
"dragonView",
|
||||
]);
|
||||
let rowAnimationObserver = null;
|
||||
let stockPreviewOpenTimer = null;
|
||||
let stockPreviewCloseTimer = null;
|
||||
@@ -446,7 +441,7 @@ function toggleTheme() {
|
||||
async function initialize() {
|
||||
syncThemeControl();
|
||||
refreshIcons();
|
||||
initializeApplicationShell();
|
||||
applicationShell.initialize();
|
||||
elements.tradeDate.value = todayString();
|
||||
const initialUrl = new URL(window.location.href);
|
||||
if (initialUrl.searchParams.has("date")) {
|
||||
@@ -488,9 +483,12 @@ async function startAuthenticatedApp() {
|
||||
if (["trend", "fortune", "heart"].includes(requestedHeavenPanel)) {
|
||||
state.heavenPanel = requestedHeavenPanel;
|
||||
}
|
||||
const legacyViewAliases = { sectorView: "rotationView", breadthView: "limitPool" };
|
||||
const requestedView = legacyViewAliases[searchParams.get("view")] || searchParams.get("view");
|
||||
if (requestedView && document.getElementById(requestedView)?.classList.contains("workspace-view")) {
|
||||
const requestedView = window.XiaobaiPages.resolve(searchParams.get("view"));
|
||||
if (
|
||||
requestedView
|
||||
&& window.XiaobaiPages.has(requestedView)
|
||||
&& document.getElementById(requestedView)?.classList.contains("workspace-view")
|
||||
) {
|
||||
openView(requestedView, false);
|
||||
if (requestedView !== searchParams.get("view")) {
|
||||
const url = new URL(window.location.href);
|
||||
@@ -626,17 +624,6 @@ function bindEvents() {
|
||||
});
|
||||
});
|
||||
|
||||
document.querySelectorAll(".module-tab").forEach((button) => {
|
||||
button.addEventListener("click", () => openView(button.dataset.view));
|
||||
});
|
||||
document.querySelector("#mobileMarketViewSelect").addEventListener("change", (event) => {
|
||||
openView(event.target.value);
|
||||
});
|
||||
document.querySelector("#sidebarCollapseButton").addEventListener("click", toggleSidebar);
|
||||
document.querySelector("#headerMenuButton").addEventListener("click", (event) => {
|
||||
event.stopPropagation();
|
||||
toggleHeaderCommandMenu();
|
||||
});
|
||||
document.querySelector("#globalSearchButton").addEventListener("click", openGlobalSearch);
|
||||
document.querySelector("#themeToggle").addEventListener("click", toggleTheme);
|
||||
document.querySelector("#alertButton").addEventListener("click", openAlerts);
|
||||
@@ -669,33 +656,22 @@ function bindEvents() {
|
||||
const result = event.target.closest("[data-search-result-index]");
|
||||
if (result) openGlobalSearchResult(number(result.dataset.searchResultIndex));
|
||||
});
|
||||
document.querySelector("#headerCommandGroup").addEventListener("click", (event) => {
|
||||
if (event.target.closest("button") && !event.target.closest(".account-menu-shell")) toggleHeaderCommandMenu(false);
|
||||
});
|
||||
document.addEventListener("click", (event) => {
|
||||
if (!event.target.closest(".header-actions")) toggleHeaderCommandMenu(false);
|
||||
if (!event.target.closest(".account-menu-shell")) toggleAccountDropdown(false);
|
||||
});
|
||||
window.addEventListener("keydown", handleGlobalSearchShortcut);
|
||||
document.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Escape") {
|
||||
toggleHeaderCommandMenu(false);
|
||||
toggleAccountDropdown(false, true);
|
||||
toggleMentorDirectory(false);
|
||||
}
|
||||
handleAccountMenuKeydown(event);
|
||||
});
|
||||
window.addEventListener("resize", () => {
|
||||
if (window.innerWidth > 720) toggleHeaderCommandMenu(false);
|
||||
if (window.innerWidth > 720) toggleMentorDirectory(false);
|
||||
if (!elements.stockPreview.hidden) closeStockPreview();
|
||||
updateSidebarControl();
|
||||
syncNavigationState(state.activeView);
|
||||
if (state.activeView === "dragonView") layoutDragonCards();
|
||||
});
|
||||
document.querySelectorAll("[data-open-view]").forEach((button) => {
|
||||
button.addEventListener("click", () => openView(button.dataset.openView));
|
||||
});
|
||||
document.querySelectorAll("[data-open-account]").forEach((button) => {
|
||||
button.addEventListener("click", () => openSettings("membership"));
|
||||
});
|
||||
@@ -754,17 +730,6 @@ function bindEvents() {
|
||||
renderRotationHistory();
|
||||
});
|
||||
});
|
||||
document.querySelector("#overviewToggle").addEventListener("click", () => {
|
||||
const overview = document.querySelector(".overview-strip");
|
||||
const expanded = overview.dataset.overviewExpanded !== "true";
|
||||
overview.dataset.overviewExpanded = String(expanded);
|
||||
const toggle = document.querySelector("#overviewToggle");
|
||||
toggle.setAttribute("aria-expanded", String(expanded));
|
||||
toggle.title = expanded ? "收起市场详情" : "展开市场详情";
|
||||
toggle.querySelector("span").textContent = expanded ? "收起详情" : "展开详情";
|
||||
toggle.querySelector("i").setAttribute("data-lucide", expanded ? "chevron-up" : "chevron-down");
|
||||
refreshIcons();
|
||||
});
|
||||
document.querySelector("#sentimentExportButton").addEventListener("click", exportSentimentHistory);
|
||||
document.querySelectorAll("[data-sentiment-range]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
@@ -8456,35 +8421,15 @@ function applyMembershipAccess() {
|
||||
}
|
||||
|
||||
function openView(viewId, updateHash = true) {
|
||||
if (!applicationShell.page(viewId)) return;
|
||||
closeStockPreview();
|
||||
state.activeView = viewId;
|
||||
if (viewId !== "auctionView") clearAuctionTimer();
|
||||
if (viewId !== "heavenView") {
|
||||
stopQiFieldCanvas();
|
||||
stopHeartDust();
|
||||
cancelHeavenPerformance();
|
||||
}
|
||||
document.querySelectorAll(".workspace-view").forEach((view) => {
|
||||
const active = view.id === viewId;
|
||||
view.classList.toggle("active-view", active);
|
||||
view.classList.remove("view-entering");
|
||||
if (active && motionEnabled()) {
|
||||
void view.offsetWidth;
|
||||
view.classList.add("view-entering");
|
||||
view.addEventListener("animationend", () => view.classList.remove("view-entering"), { once: true });
|
||||
const body = view.querySelector("tbody");
|
||||
if (body) animateRows(body);
|
||||
}
|
||||
});
|
||||
syncNavigationState(viewId);
|
||||
setPageStatus(viewId);
|
||||
if (updateHash) {
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set("view", viewId);
|
||||
url.hash = "";
|
||||
history.replaceState(null, "", url);
|
||||
}
|
||||
window.scrollTo({ top: 0, behavior: "auto" });
|
||||
if (!applicationShell.mount(viewId, { updateUrl: updateHash })) return;
|
||||
applyMembershipAccess();
|
||||
if (viewId === "dragonView") loadDragonTiger();
|
||||
if (viewId === "reviewWorkspaceView") loadReviewWorkspace();
|
||||
@@ -9153,32 +9098,7 @@ function setLoading(loading, text = "正在加载复盘数据", context = "defau
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
setText("statusText", text);
|
||||
}
|
||||
|
||||
function setPageStatus(viewId) {
|
||||
const labels = {
|
||||
sentimentCycleView: "情绪周期",
|
||||
limitPool: "涨停池",
|
||||
brokenView: "炸板池",
|
||||
downView: "跌停板",
|
||||
yesterdayView: "昨日涨停",
|
||||
performanceView: "涨停表现",
|
||||
ladderView: "市场天梯",
|
||||
rotationView: "板块轮动",
|
||||
auctionView: "集合竞价",
|
||||
themeLibraryView: "题材库",
|
||||
popularityView: "人气热榜",
|
||||
dragonView: "龙虎榜",
|
||||
screenerView: "智能选股",
|
||||
screenerTrackingView: "策略持续跟踪",
|
||||
mentorView: "问师",
|
||||
heavenView: "问天",
|
||||
reviewWorkspaceView: "我的复盘",
|
||||
};
|
||||
const label = labels[viewId] || "小白复盘";
|
||||
const tradeDate = displayCompactDate(state.dashboard?.meta?.trade_date || elements.tradeDate?.value || "");
|
||||
setStatus(tradeDate && tradeDate !== "--" ? `${label} · 数据日期 ${tradeDate}` : `${label} · 等待数据`);
|
||||
applicationShell.setStatus(text);
|
||||
}
|
||||
|
||||
let toastTimer;
|
||||
@@ -9203,47 +9123,8 @@ function refreshIcons() {
|
||||
window.lucide.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
}
|
||||
|
||||
function initializeApplicationShell() {
|
||||
let collapsed = false;
|
||||
try {
|
||||
collapsed = window.localStorage.getItem("xiaobai-sidebar-collapsed") === "1";
|
||||
} catch (_error) {
|
||||
collapsed = false;
|
||||
}
|
||||
document.body.classList.toggle("sidebar-collapsed", collapsed);
|
||||
updateSidebarControl();
|
||||
syncNavigationState(state.activeView);
|
||||
}
|
||||
|
||||
function toggleSidebar() {
|
||||
const collapsed = document.body.classList.toggle("sidebar-collapsed");
|
||||
try {
|
||||
window.localStorage.setItem("xiaobai-sidebar-collapsed", collapsed ? "1" : "0");
|
||||
} catch (_error) {
|
||||
// The visual state still works when storage is unavailable.
|
||||
}
|
||||
updateSidebarControl();
|
||||
}
|
||||
|
||||
function updateSidebarControl() {
|
||||
const button = document.querySelector("#sidebarCollapseButton");
|
||||
if (!button) return;
|
||||
const automaticallyCollapsed = window.innerWidth <= 1023 && window.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 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));
|
||||
applicationShell.toggleHeaderCommandMenu(force);
|
||||
}
|
||||
|
||||
function toggleAccountDropdown(force, returnFocus = false) {
|
||||
@@ -9287,28 +9168,6 @@ function handleAccountMenuKeydown(event) {
|
||||
}
|
||||
}
|
||||
|
||||
function syncNavigationState(viewId) {
|
||||
const marketView = MARKET_VIEWS.has(viewId);
|
||||
document.body.dataset.activeView = viewId;
|
||||
document.querySelectorAll(".module-tab").forEach((button) => {
|
||||
button.classList.toggle(
|
||||
"active",
|
||||
button.dataset.view === viewId
|
||||
|| (viewId === "screenerTrackingView" && button.dataset.view === "screenerView"),
|
||||
);
|
||||
button.classList.toggle(
|
||||
"mobile-active",
|
||||
window.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);
|
||||
toggleAccountDropdown(false);
|
||||
}
|
||||
|
||||
function updateSentimentGauge(rawScore) {
|
||||
const gauge = document.querySelector("#sentimentGauge");
|
||||
if (!gauge) return;
|
||||
|
||||
Reference in New Issue
Block a user