import { adminApi, ApiError, setCsrfToken } from "./api/client"; import type { AdminRole, AuthState } from "./api/types"; import { renderLoading, showToast } from "./components/ui"; import { escapeHtml } from "./lib/format"; import { renderAudit } from "./pages/audit"; import { renderPasswordLogin } from "./pages/auth"; import { renderCredits } from "./pages/credits"; import { renderOverview } from "./pages/overview"; import { renderReferrals } from "./pages/referrals"; import { renderSecurity } from "./pages/security"; import { renderUsers } from "./pages/users"; type Route = | "overview" | "referrals" | "users" | "credits" | "audit" | "security"; interface RouteDefinition { id: Route; label: string; icon: string; roles: AdminRole[]; } const ALL_ROLES: AdminRole[] = ["SUPER_ADMIN", "SUPPORT", "ANALYST"]; const SUPPORT_ROLES: AdminRole[] = ["SUPER_ADMIN", "SUPPORT"]; const SUPER_ADMIN_ONLY: AdminRole[] = ["SUPER_ADMIN"]; const routes: RouteDefinition[] = [ { id: "overview", label: "运营总览", roles: ALL_ROLES, icon: '', }, { id: "referrals", label: "裂变分析", roles: ALL_ROLES, icon: '', }, { id: "users", label: "用户查询", roles: SUPPORT_ROLES, icon: '', }, { id: "credits", label: "积分流水", roles: SUPPORT_ROLES, icon: '', }, { id: "audit", label: "审计日志", roles: SUPER_ADMIN_ONLY, icon: '', }, { id: "security", label: "安全中心", roles: SUPER_ADMIN_ONLY, icon: '', }, ]; export class AdminApp { private auth: AuthState = { status: "anonymous" }; private renderVersion = 0; constructor(private readonly root: HTMLElement) { window.addEventListener("hashchange", () => void this.renderRoute()); window.addEventListener("admin:unauthorized", () => { setCsrfToken(); this.auth = { status: "anonymous" }; this.render(); showToast("登录状态已失效,请重新登录", "error"); }); } async start(): Promise { renderLoading(this.root, "检查登录状态"); try { const session = await adminApi.session(); if (session.authenticated && session.operatorName && session.role) { this.auth = { status: "authenticated", operatorName: session.operatorName, role: session.role, }; } } catch (error) { if (!(error instanceof ApiError) || error.status !== 401) { showToast("暂时无法确认登录状态", "error"); } } this.render(); } private render(): void { if (this.auth.status === "anonymous") { renderPasswordLogin(this.root, { onChange: (state) => this.setAuth(state) }); return; } this.renderShell(); void this.renderRoute(); } private setAuth(state: AuthState): void { this.auth = state; this.render(); } private currentRoute(): Route { const value = window.location.hash.replace(/^#\/?/, "") as Route; return this.availableRoutes().some((route) => route.id === value) ? value : "overview"; } private availableRoutes(): RouteDefinition[] { if (this.auth.status !== "authenticated") return []; const role = this.auth.role; return routes.filter((route) => route.roles.includes(role)); } private renderShell(): void { if (this.auth.status !== "authenticated") return; this.root.innerHTML = `
运营总览
`; const sidebar = this.root.querySelector("[data-sidebar]"); const menuButton = this.root.querySelector("[data-menu]"); const mobileLayout = window.matchMedia("(max-width: 1023px)"); const syncSidebarState = () => { if (!sidebar) return; sidebar.inert = mobileLayout.matches && !sidebar.classList.contains("sidebar--open"); }; const closeMenu = (restoreFocus = false) => { sidebar?.classList.remove("sidebar--open"); menuButton?.setAttribute("aria-expanded", "false"); syncSidebarState(); if (restoreFocus) menuButton?.focus(); }; const openMenu = () => { sidebar?.classList.add("sidebar--open"); menuButton?.setAttribute("aria-expanded", "true"); syncSidebarState(); sidebar ?.querySelector('a[aria-current="page"], a') ?.focus(); }; menuButton?.addEventListener("click", openMenu); this.root .querySelector("[data-scrim]") ?.addEventListener("click", () => closeMenu(true)); this.root.querySelectorAll("[data-route]").forEach((link) => { link.addEventListener("click", () => closeMenu()); }); sidebar?.addEventListener("keydown", (event) => { if (event.key === "Escape") closeMenu(true); }); mobileLayout.addEventListener("change", syncSidebarState); syncSidebarState(); this.root .querySelector("[data-logout]") ?.addEventListener("click", () => void this.logout()); } private async renderRoute(): Promise { if (this.auth.status !== "authenticated") return; const route = this.currentRoute(); this.root.querySelectorAll("[data-route]").forEach((link) => { const active = link.dataset.route === route; link.classList.toggle("active", active); if (active) { link.setAttribute("aria-current", "page"); } else { link.removeAttribute("aria-current"); } }); const routeDefinition = routes.find((item) => item.id === route); document.title = `${routeDefinition?.label ?? "管理中心"} · OSG`; const mobileTitle = this.root.querySelector("[data-mobile-title]"); if (mobileTitle) mobileTitle.textContent = routeDefinition?.label ?? "管理中心"; const content = this.root.querySelector("[data-content]"); if (!content) return; const version = ++this.renderVersion; const stage = document.createElement("div"); stage.className = "page-stage"; renderLoading(content); if (route === "overview") await renderOverview(stage); if (route === "referrals") await renderReferrals(stage); if (route === "users") renderUsers(stage, this.auth.role); if (route === "credits") renderCredits(stage); if (route === "audit") await renderAudit(stage); if (route === "security") { await renderSecurity(stage, this.auth.operatorName); } if (version === this.renderVersion) { content.replaceChildren(stage); const heading = content.querySelector("h1"); if (heading) { heading.tabIndex = -1; heading.focus({ preventScroll: true }); } else { content.focus({ preventScroll: true }); } } } private async logout(): Promise { try { await adminApi.logout(); } catch { // 即使服务端退出失败,也立即清理前端认证状态。 } finally { setCsrfToken(); this.auth = { status: "anonymous" }; window.history.replaceState(null, "", window.location.pathname); this.render(); } } } function roleLabel(role: AdminRole): string { const labels: Record = { SUPER_ADMIN: "超级管理员", SUPPORT: "支持人员", ANALYST: "分析员", }; return labels[role]; }