119 lines
4.0 KiB
JavaScript
119 lines
4.0 KiB
JavaScript
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);
|