const servicesEl = document.querySelector("#services"); const statusBar = document.querySelector("#statusBar"); const searchInput = document.querySelector("#searchInput"); const refreshButton = document.querySelector("#refreshButton"); const groupTemplate = document.querySelector("#groupTemplate"); const cardTemplate = document.querySelector("#cardTemplate"); const appBrand = document.querySelector("#appBrand"); const appTitle = document.querySelector("#appTitle"); const appSubtitle = document.querySelector("#appSubtitle"); let services = []; let settings = {}; function formatTime(value) { if (!value) return "尚未更新"; const date = new Date(value); if (Number.isNaN(date.getTime())) return value; return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(date); } function setStatus(message, isError = false) { statusBar.textContent = message; statusBar.classList.toggle("error", isError); } function applySettings(nextSettings = {}) { settings = nextSettings; document.title = settings.browserTitle || "家中服务导航"; appBrand.textContent = settings.brand || "STUN NAV"; appTitle.textContent = settings.title || "家中服务"; appSubtitle.textContent = settings.subtitle || ""; appSubtitle.hidden = !appSubtitle.textContent.trim(); } function serviceMatches(service, keyword) { if (!keyword) return true; const text = `${service.name} ${service.group} ${service.note} ${service.url}`.toLowerCase(); return text.includes(keyword.toLowerCase()); } function groupServices(items) { return items.reduce((groups, service) => { const group = service.group || "默认"; if (!groups.has(group)) groups.set(group, []); groups.get(group).push(service); return groups; }, new Map()); } async function copyUrl(url, button) { await navigator.clipboard.writeText(url); const oldTitle = button.title; button.title = "已复制"; setTimeout(() => { button.title = oldTitle; }, 1200); } function render() { const keyword = searchInput.value.trim(); const filtered = services.filter((service) => serviceMatches(service, keyword)); servicesEl.replaceChildren(); if (!filtered.length) { setStatus(services.length ? "没有匹配的服务" : "还没有收到 Lucky 推送的地址"); return; } setStatus(`${filtered.length} 个服务可用,最近刷新 ${formatTime(new Date().toISOString())}`); for (const [group, items] of groupServices(filtered)) { const groupNode = groupTemplate.content.firstElementChild.cloneNode(true); groupNode.querySelector("h2").textContent = group; const grid = groupNode.querySelector(".grid"); for (const service of items) { const card = cardTemplate.content.firstElementChild.cloneNode(true); card.querySelector(".service-mark").dataset.initial = (service.name || service.id || "?").trim().slice(0, 1).toUpperCase(); card.querySelector("h3").textContent = service.name; card.querySelector(".note").textContent = service.note || ""; card.querySelector(".meta").textContent = `${service.url} · ${formatTime(service.updatedAt)}`; const openButton = card.querySelector(".open-button"); openButton.href = service.url; const copyButton = card.querySelector(".copy-button"); copyButton.addEventListener("click", () => copyUrl(service.url, copyButton)); grid.append(card); } servicesEl.append(groupNode); } } async function loadServices() { try { setStatus("正在读取最新地址..."); const response = await fetch("/api/services", { cache: "no-store" }); if (!response.ok) throw new Error(`读取失败:${response.status}`); const data = await response.json(); services = Array.isArray(data.services) ? data.services : []; applySettings(data.settings || {}); render(); } catch (error) { setStatus(error.message || "读取失败", true); } } searchInput.addEventListener("input", render); refreshButton.addEventListener("click", loadServices); loadServices(); setInterval(loadServices, 30000);