import { session, visit } from "./api.js";
import { disposeAll } from "./charts.js";
import { onRoute, parseLocation, startRouter } from "./router.js";
import { canAccessProtected, setSession } from "./session.js";
import { renderCandidates } from "./views/candidates.js";
import { renderFans } from "./views/fans.js";
import { renderHome } from "./views/home.js";
import { renderUps, renderVideos } from "./views/rankings.js";
import { renderSystem } from "./views/system.js";
import { renderUp } from "./views/up.js";
import { renderVideo } from "./views/video.js";
import { renderAbout } from "./views/about.js";
const NAV_ITEMS = [
{ path: "/home", label: "首页", icon: "home" },
{ path: "/videos", label: "视频", icon: "video" },
{ path: "/ups", label: "UP主", icon: "up" },
{ path: "/fans", label: "粉丝", icon: "fans" },
{ path: "/about", label: "说明", icon: "info" },
];
// 登录后的「候选 / 系统」入口放在顶栏右侧(未登录显示「登录」),
// 以减轻移动端底部主导航的拥挤。
const TOPBAR_ITEMS = [
{ path: "/candidates", label: "候选" },
{ path: "/system", label: "系统" },
];
const ROUTES = [
{ pattern: /^\/home$/, view: renderHome, title: "最新动态" },
{ pattern: /^\/videos$/, view: renderVideos, title: "视频" },
{ pattern: /^\/ups$/, view: renderUps, title: "UP主" },
{ pattern: /^\/rank$/, view: renderLegacyRank, title: "榜单" },
{ pattern: /^\/video\/(\d+)$/, view: renderVideo, title: "视频详情" },
{ pattern: /^\/up\/(\d+)$/, view: renderUp, title: "UP 详情" },
{ pattern: /^\/fans$/, view: renderFans, title: "粉丝" },
{ pattern: /^\/about$/, view: renderAbout, title: "数据说明" },
{
pattern: /^\/candidates$/, view: renderCandidates, title: "候选池",
beforeEnter: requireAuth,
},
{
pattern: /^\/system$/, view: renderSystem, title: "系统状态",
beforeEnter: requireAuth,
},
];
function renderLegacyRank(root, context) {
// 旧链接 /rank?scope=up 仍可用,但不再在页面内混排视频与 UP。
return context.query.get("scope") === "up"
? renderUps(root, context)
: renderVideos(root, context);
}
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: '',
video: '',
up: '',
fans: '',
candidate: '',
system: '',
info: '',
};
return ``;
}
function renderNav() {
const nav = document.querySelector("#nav-items");
nav.innerHTML = NAV_ITEMS.map((item) => `
${icon(item.icon)}${item.label}
`).join("");
// 未登录显示「登录」;可访问受保护页面时显示「候选 | 系统」。
const loggedIn = canAccessProtected();
document.querySelectorAll(".topbar-auth").forEach((element) => {
element.hidden = loggedIn;
});
document.querySelectorAll("[data-topbar-links]").forEach((element) => {
element.hidden = !loggedIn;
element.innerHTML = loggedIn
? TOPBAR_ITEMS.map((item) => `
${item.label}`).join("")
: "";
});
}
function activeNav(path) {
let active = "/home";
if (path.startsWith("/videos") || path.startsWith("/video/") || path.startsWith("/rank")) active = "/videos";
else if (path.startsWith("/ups") || path.startsWith("/up/")) active = "/ups";
else if (path.startsWith("/fans")) active = "/fans";
else if (path.startsWith("/candidates")) active = "/candidates";
else if (path.startsWith("/system")) active = "/system";
else if (path.startsWith("/about")) active = "/about";
document.querySelectorAll("[data-nav]").forEach((item) => {
item.classList.toggle("active", item.dataset.nav === active);
});
document.querySelectorAll("[data-topbar-nav]").forEach((item) => {
item.classList.toggle("active", item.dataset.topbarNav === active);
});
}
function setTitle(route, path) {
const title = route?.title || "看板";
document.title = `${title} · 咻咻满 Reaction`;
document.querySelectorAll(".brand-subtitle").forEach((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, .media-thumb, .candidate-thumb");
wrapper?.classList.add("cover-placeholder");
}
}, true);
async function boot() {
const banner = document.querySelector("#preview-banner");
try {
const data = await session();
setSession(data);
if (data?.preview) {
banner.hidden = false;
}
} catch {
toast("无法连接看板服务", "error");
}
renderNav();
let lastVisitPath = null;
onRoute(({ path, route }) => {
disposeAll();
setTitle(route, path);
// 访问统计:仅在路由路径变化时上报(排序/筛选等同路径重渲染不计 PV)。
if (path !== lastVisitPath) {
lastVisitPath = path;
visit(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();