import { session } from "./api.js"; import { disposeAll } from "./charts.js"; import { onRoute, parseLocation, startRouter } from "./router.js"; import { renderCandidates } from "./views/candidates.js"; import { renderFans } from "./views/fans.js"; import { renderHome } from "./views/home.js"; import { renderRankings } from "./views/rankings.js"; import { renderSystem } from "./views/system.js"; import { renderUp } from "./views/up.js"; import { renderVideo } from "./views/video.js"; const NAV_ITEMS = [ { path: "/home", label: "首页", icon: "home" }, { path: "/rank", label: "榜单", icon: "rank" }, { path: "/fans", label: "粉丝", icon: "fans" }, { path: "/candidates", label: "候选", icon: "candidate", protected: true }, { path: "/system", label: "系统", icon: "system", protected: true }, ]; const ROUTES = [ { pattern: /^\/home$/, view: renderHome, title: "最新动态" }, { pattern: /^\/rank$/, view: renderRankings, title: "榜单" }, { pattern: /^\/video\/(\d+)$/, view: renderVideo, title: "视频详情" }, { pattern: /^\/up\/(\d+)$/, view: renderUp, title: "UP 详情" }, { pattern: /^\/fans$/, view: renderFans, title: "粉丝" }, { pattern: /^\/candidates$/, view: renderCandidates, title: "候选池", beforeEnter: requireAuth, }, { pattern: /^\/system$/, view: renderSystem, title: "系统状态", beforeEnter: requireAuth, }, ]; let authState = { authEnabled: false, authenticated: true }; function canAccessProtected() { return !authState.authEnabled || authState.authenticated; } function requireAuth() { if (canAccessProtected()) return true; const { path, query } = parseLocation(); const search = query.toString(); window.location.assign( `/login?next=${encodeURIComponent(`${path}${search ? `?${search}` : ""}`)}`, ); return false; } function icon(name) { const paths = { home: '', rank: '', fans: '', candidate: '', system: '', }; return ``; } function renderNav() { const nav = document.querySelector("#bottom-nav"); const items = NAV_ITEMS.filter( (item) => !item.protected || canAccessProtected(), ); nav.innerHTML = items.map((item) => ` ${icon(item.icon)}${item.label} `).join(""); } function activeNav(path) { let active = "/home"; if (path.startsWith("/rank") || path.startsWith("/video/") || path.startsWith("/up/")) active = "/rank"; else if (path.startsWith("/fans")) active = "/fans"; else if (path.startsWith("/candidates")) active = "/candidates"; else if (path.startsWith("/system")) active = "/system"; document.querySelectorAll("[data-nav]").forEach((item) => { item.classList.toggle("active", item.dataset.nav === active); }); } function setTitle(route, path) { const title = route?.title || "看板"; document.title = `${title} · 咻咻满 Reaction`; const element = document.querySelector("#topbar-title"); if (element) element.textContent = title; activeNav(path); } function toast(message, kind = "info") { const element = document.querySelector("#toast"); if (!element) return; element.textContent = message; element.className = `toast show ${kind}`; window.clearTimeout(toast.timer); toast.timer = window.setTimeout(() => { element.className = "toast"; }, 2600); } window.addEventListener("error", (event) => { const target = event.target; if (target instanceof HTMLImageElement) { target.classList.add("image-error"); const wrapper = target.closest(".video-thumb, .hero-cover, .heat-thumb, .candidate-thumb"); wrapper?.classList.add("cover-placeholder"); } }, true); async function boot() { const banner = document.querySelector("#preview-banner"); const hint = document.querySelector("#topbar-hint"); const authLink = document.querySelector("#topbar-auth"); try { const data = await session(); authState = { authEnabled: Boolean(data?.auth_enabled), authenticated: data?.authenticated !== false, }; if (data?.preview) { banner.hidden = false; if (hint) hint.hidden = false; } if (authLink && authState.authEnabled && !authState.authenticated) { authLink.hidden = false; } } catch { toast("无法连接看板服务", "error"); } renderNav(); onRoute(({ path, route }) => { disposeAll(); setTitle(route, path); }); startRouter({ root: document.querySelector("#app"), outlet: document.querySelector("#view"), routes: ROUTES, }); } window.addEventListener("unhandledrejection", (event) => { const reason = event.reason; if (reason?.name === "ApiError") { toast(reason.message, "error"); } }); boot();