Initial stun navigation app
This commit is contained in:
118
public/app.js
Normal file
118
public/app.js
Normal file
@@ -0,0 +1,118 @@
|
||||
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);
|
||||
Reference in New Issue
Block a user