(() => { "use strict"; const grid = document.querySelector("#project-grid"); const refreshButton = document.querySelector("#refresh-projects"); const status = document.querySelector("#refresh-status"); const projectCounts = document.querySelectorAll("[data-project-count]"); const liveStatus = document.querySelector("#live-status"); const liveStatusLabel = document.querySelector("#live-status-label"); if (!grid || !refreshButton || !status || !liveStatus || !liveStatusLabel) { return; } document.body.classList.add("is-enhanced"); const createProjectCard = (project) => { if ( typeof project.name !== "string" || typeof project.route_path !== "string" || !project.route_path.startsWith("/") || project.route_path.startsWith("//") ) { return null; } const link = document.createElement("a"); link.className = "site-item"; link.href = project.route_path; link.title = project.name; const cardTop = document.createElement("div"); cardTop.className = "site-card-top"; const avatar = document.createElement("span"); avatar.className = "site-avatar"; avatar.setAttribute("aria-hidden", "true"); avatar.textContent = Array.from(project.name)[0]?.toUpperCase() || "A"; const open = document.createElement("span"); open.className = "site-open"; open.setAttribute("aria-hidden", "true"); open.innerHTML = ` `; cardTop.append(avatar, open); const info = document.createElement("div"); info.className = "site-info"; const name = document.createElement("h3"); name.className = "site-name"; name.textContent = project.name; const description = document.createElement("span"); description.className = "site-desc"; description.textContent = "本地静态应用"; info.append(name, description); const footer = document.createElement("div"); footer.className = "site-card-footer"; const path = document.createElement("span"); path.className = "site-path"; path.textContent = project.route_path; const ready = document.createElement("span"); ready.className = "site-ready"; ready.textContent = "READY"; footer.append(path, ready); link.append(cardTop, info, footer); return link; }; let renderedSignature = JSON.stringify( Array.from(grid.querySelectorAll(".site-item")).map((link) => [ link.title, link.getAttribute("href"), ]), ); const projectsSignature = (projects) => JSON.stringify( projects.map((project) => [project.name, project.route_path]), ); const renderProjects = (projects) => { const nextSignature = projectsSignature(projects); if (nextSignature === renderedSignature) { return false; } grid.replaceChildren(); if (projects.length === 0) { const emptyState = document.createElement("div"); emptyState.className = "empty-state"; emptyState.innerHTML = `

这里还没有项目

在托管根目录下创建子目录,并放入 index.html,刷新后即可在这里看到它。

`; grid.append(emptyState); renderedSignature = nextSignature; return true; } const cards = projects.map(createProjectCard).filter(Boolean); grid.append(...cards); renderedSignature = nextSignature; return true; }; const updateProjectCounts = (count) => { projectCounts.forEach((element) => { element.textContent = String(count); }); }; refreshButton.addEventListener("click", async () => { refreshButton.disabled = true; refreshButton.classList.add("is-loading"); grid.setAttribute("aria-busy", "true"); status.textContent = "正在重新扫描项目..."; try { const response = await fetch("/api/projects", { cache: "no-store", headers: { Accept: "application/json" }, }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const payload = await response.json(); if (!Array.isArray(payload.projects)) { throw new Error("项目列表格式无效"); } const changed = renderProjects(payload.projects); updateProjectCounts(payload.projects.length); status.textContent = changed ? "项目列表已刷新" : "项目列表已是最新"; } catch (error) { console.error("刷新项目列表失败", error); status.textContent = "刷新失败,请稍后重试"; } finally { refreshButton.disabled = false; refreshButton.classList.remove("is-loading"); grid.removeAttribute("aria-busy"); } }); if (!("EventSource" in window)) { liveStatusLabel.textContent = "手动刷新"; return; } const eventSource = new EventSource("/api/projects/events"); eventSource.addEventListener("open", () => { liveStatusLabel.textContent = "实时同步"; liveStatus.classList.add("is-live"); liveStatus.classList.remove("is-reconnecting"); }); eventSource.addEventListener("projects", (event) => { try { const payload = JSON.parse(event.data); if (!Array.isArray(payload.projects)) { throw new Error("项目列表格式无效"); } const changed = renderProjects(payload.projects); updateProjectCounts(payload.projects.length); if (changed) { status.textContent = "检测到目录变化,项目列表已自动更新"; } } catch (error) { console.error("处理项目更新事件失败", error); status.textContent = "收到项目更新,但无法解析"; } }); eventSource.addEventListener("error", () => { liveStatusLabel.textContent = "同步重连中"; liveStatus.classList.remove("is-live"); liveStatus.classList.add("is-reconnecting"); }); })();