123 lines
4.9 KiB
JavaScript
123 lines
4.9 KiB
JavaScript
import { health } from "./api.js";
|
|
import { disposeAll } from "./charts.js";
|
|
import { escapeHtml } from "./format.js";
|
|
import { onRoute, 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" },
|
|
{ path: "/system", label: "系统", icon: "system" },
|
|
];
|
|
|
|
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: "候选池" },
|
|
{ pattern: /^\/system$/, view: renderSystem, title: "系统状态" },
|
|
];
|
|
|
|
function icon(name) {
|
|
const paths = {
|
|
home: '<path d="M4 10.5 12 4l8 6.5V20a1 1 0 0 1-1 1h-5v-6H10v6H5a1 1 0 0 1-1-1z"/>',
|
|
rank: '<path d="M5 20V10m7 10V4m7 16v-7"/>',
|
|
fans: '<path d="M16 20v-1a4 4 0 0 0-4-4H7a4 4 0 0 0-4 4v1"/><circle cx="9.5" cy="7" r="3"/><path d="M17 11a3 3 0 1 0 0-6m1 15v-1a4 4 0 0 0-3-3.8"/>',
|
|
candidate: '<path d="M9 11l2 2 4-4"/><rect x="4" y="4" width="16" height="16" rx="3"/>',
|
|
system: '<circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.7 1.7 0 0 0 .3 1.9l.1.1a2 2 0 1 1-2.8 2.8l-.1-.1a1.7 1.7 0 0 0-1.9-.3 1.7 1.7 0 0 0-1 1.5V21a2 2 0 1 1-4 0v-.1a1.7 1.7 0 0 0-1-1.6 1.7 1.7 0 0 0-1.9.3l-.1.1a2 2 0 1 1-2.8-2.8l.1-.1a1.7 1.7 0 0 0 .3-1.9 1.7 1.7 0 0 0-1.5-1H3a2 2 0 1 1 0-4h.1a1.7 1.7 0 0 0 1.6-1 1.7 1.7 0 0 0-.3-1.9l-.1-.1a2 2 0 1 1 2.8-2.8l.1.1a1.7 1.7 0 0 0 1.9.3H9a1.7 1.7 0 0 0 1-1.5V3a2 2 0 1 1 4 0v.1a1.7 1.7 0 0 0 1 1.5 1.7 1.7 0 0 0 1.9-.3l.1-.1a2 2 0 1 1 2.8 2.8l-.1.1a1.7 1.7 0 0 0-.3 1.9V9a1.7 1.7 0 0 0 1.5 1H21a2 2 0 1 1 0 4h-.1a1.7 1.7 0 0 0-1.5 1z"/>',
|
|
};
|
|
return `<svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor"
|
|
stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths[name] || ""}</svg>`;
|
|
}
|
|
|
|
function renderNav() {
|
|
const nav = document.querySelector("#bottom-nav");
|
|
nav.innerHTML = NAV_ITEMS.map((item) => `
|
|
<a class="nav-item" href="#${item.path}" data-nav="${item.path}">
|
|
${icon(item.icon)}<span class="nav-label">${item.label}</span>
|
|
</a>`).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() {
|
|
renderNav();
|
|
const banner = document.querySelector("#preview-banner");
|
|
const hint = document.querySelector("#topbar-hint");
|
|
try {
|
|
const payload = await health();
|
|
const data = payload?.data || payload;
|
|
if (data?.preview) {
|
|
banner.hidden = false;
|
|
if (hint) hint.hidden = false;
|
|
}
|
|
} catch {
|
|
toast("无法连接看板服务", "error");
|
|
}
|
|
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();
|