// 开发环境仅展示广告位,不加载真实联盟广告。 document.querySelectorAll("[data-ad-slot]").forEach((slot) => { slot.dataset.ready = "true"; }); document.querySelectorAll("[data-header-search]").forEach((search) => { const toggle = search.querySelector("[data-search-toggle]"); const input = search.querySelector("input"); if (!toggle || !input) return; const setOpen = (open) => { search.classList.toggle("is-open", open); toggle.setAttribute("aria-expanded", open ? "true" : "false"); toggle.setAttribute("aria-label", open ? "收起搜索" : "展开搜索"); if (open) window.setTimeout(() => input.focus(), 0); }; toggle.addEventListener("click", () => setOpen(!search.classList.contains("is-open"))); document.addEventListener("click", (event) => { if (!search.contains(event.target)) setOpen(false); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") { setOpen(false); toggle.focus(); } }); }); document.querySelectorAll("[data-news-carousel]").forEach((carousel) => { const slides = Array.from(carousel.querySelectorAll("[data-carousel-slide]")); const dots = Array.from(carousel.querySelectorAll("[data-carousel-dot]")); if (slides.length < 2) return; let current = 0; let timer; let pointerStart; const loadSlideImage = (slide) => { const image = slide.querySelector("[data-carousel-src]"); if (!image) return; image.src = image.dataset.carouselSrc; if (image.dataset.carouselSrcset) image.srcset = image.dataset.carouselSrcset; delete image.dataset.carouselSrc; delete image.dataset.carouselSrcset; }; const show = (index) => { current = (index + slides.length) % slides.length; loadSlideImage(slides[current]); slides.forEach((slide, slideIndex) => { const active = slideIndex === current; slide.classList.toggle("is-active", active); slide.setAttribute("aria-hidden", active ? "false" : "true"); }); dots.forEach((dot, dotIndex) => { const active = dotIndex === current; dot.classList.toggle("is-active", active); dot.setAttribute("aria-current", active ? "true" : "false"); }); }; const stop = () => window.clearInterval(timer); const start = () => { stop(); if (!document.hidden) timer = window.setInterval(() => show(current + 1), 5000); }; carousel.querySelector("[data-carousel-prev]")?.addEventListener("click", () => { show(current - 1); start(); }); carousel.querySelector("[data-carousel-next]")?.addEventListener("click", () => { show(current + 1); start(); }); dots.forEach((dot) => dot.addEventListener("click", () => { show(Number(dot.dataset.carouselDot)); start(); })); carousel.addEventListener("mouseenter", stop); carousel.addEventListener("mouseleave", start); carousel.addEventListener("focusin", stop); carousel.addEventListener("focusout", start); carousel.addEventListener("pointerdown", (event) => { pointerStart = event.clientX; }); carousel.addEventListener("pointerup", (event) => { if (pointerStart === undefined) return; const distance = event.clientX - pointerStart; pointerStart = undefined; if (Math.abs(distance) < 45) return; show(current + (distance < 0 ? 1 : -1)); start(); }); document.addEventListener("visibilitychange", start); start(); }); document.querySelectorAll("[data-load-more]").forEach((button) => { const list = button.closest(".media-feed")?.querySelector("[data-latest-list]"); const status = button.parentElement?.querySelector("[data-load-more-status]"); if (!list) return; const appendArticle = (article) => { const item = document.createElement("article"); item.className = `media-excerpt${article.imageUrl ? "" : " is-textonly"}`; let imageLink = null; if (article.imageUrl) { imageLink = document.createElement("a"); imageLink.className = "media-excerpt-image"; imageLink.href = article.url; imageLink.setAttribute("aria-label", `阅读 ${article.title}`); const image = document.createElement("img"); image.src = article.imageUrl; if (article.imageSrcSet) image.srcset = article.imageSrcSet; image.sizes = "(max-width: 640px) 116px, 220px"; image.alt = ""; image.width = 220; image.height = 138; image.loading = "lazy"; imageLink.append(image); } const body = document.createElement("div"); body.className = "media-excerpt-body"; const heading = document.createElement("h2"); const articleLink = document.createElement("a"); articleLink.href = article.url; articleLink.textContent = article.title; heading.append(articleLink); const summary = document.createElement("p"); summary.textContent = article.summary; const meta = document.createElement("div"); meta.className = "media-excerpt-meta"; const time = document.createElement("time"); time.dateTime = article.publishedAt; time.textContent = article.publishedDate; const categoryLink = document.createElement("a"); categoryLink.href = `/category/${encodeURIComponent(article.categorySlug)}`; categoryLink.textContent = article.categoryName; meta.append(time, categoryLink); body.append(heading, summary, meta); if (imageLink) item.append(imageLink); item.append(body); list.append(item); }; button.addEventListener("click", async () => { if (button.disabled) return; button.disabled = true; button.textContent = "加载中…"; if (status) status.textContent = "正在加载更多文章"; try { const offset = Number(button.dataset.offset || "14"); const endpoint = new URL(button.dataset.loadMoreUrl || "/api/home/latest/More", window.location.origin); endpoint.searchParams.set("offset", String(offset)); const response = await fetch(endpoint, { headers: { Accept: "application/json" } }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const result = await response.json(); result.items.forEach(appendArticle); button.dataset.offset = String(result.nextOffset); if (status) status.textContent = `已加载 ${result.items.length} 篇文章`; if (!result.hasMore || result.items.length === 0) { button.parentElement?.remove(); return; } button.textContent = "加载更多"; button.disabled = false; } catch (error) { console.error("加载更多文章失败", error); button.textContent = "重试加载"; button.disabled = false; if (status) status.textContent = "加载失败,请重试"; } }); }); document.querySelectorAll("[data-article-share]").forEach((share) => { const url = window.location.href; const title = share.dataset.shareTitle || document.title; const status = share.querySelector("[data-share-status]"); const encodedUrl = encodeURIComponent(url); const encodedTitle = encodeURIComponent(title); const targets = { weibo: `https://service.weibo.com/share/share.php?url=${encodedUrl}&title=${encodedTitle}`, qq: `https://connect.qq.com/widget/shareqq/index.html?url=${encodedUrl}&title=${encodedTitle}`, qzone: `https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${encodedUrl}&title=${encodedTitle}`, line: `https://social-plugins.line.me/lineit/share?url=${encodedUrl}`, x: `https://twitter.com/intent/tweet?url=${encodedUrl}&text=${encodedTitle}`, facebook: `https://www.facebook.com/sharer/sharer.php?u=${encodedUrl}`, telegram: `https://t.me/share/url?url=${encodedUrl}&text=${encodedTitle}` }; share.querySelectorAll("[data-share-network]").forEach((link) => { const target = targets[link.dataset.shareNetwork]; if (target) link.href = target; }); const setStatus = (message) => { if (!status) return; status.textContent = message; window.setTimeout(() => { if (status.textContent === message) status.textContent = ""; }, 3000); }; const copyLink = async (successMessage) => { try { if (navigator.clipboard?.writeText) { await navigator.clipboard.writeText(url); } else { const input = document.createElement("textarea"); input.value = url; input.style.position = "fixed"; input.style.opacity = "0"; document.body.append(input); input.select(); document.execCommand("copy"); input.remove(); } setStatus(successMessage); } catch (error) { console.error("复制文章链接失败", error); setStatus("复制失败,请从地址栏复制链接"); } }; share.querySelector("[data-share-copy]")?.addEventListener("click", () => copyLink("文章链接已复制")); const wechatWrap = share.querySelector("[data-share-wechat-wrap]"); const wechatButton = share.querySelector("[data-share-wechat]"); const wechatQr = share.querySelector("[data-share-wechat-qr]"); const wechatCanvas = share.querySelector("[data-share-wechat-canvas]"); let qrRendered = false; const ensureQrCode = () => { if (qrRendered || !wechatCanvas || !window.QrCode) return; try { const qr = window.QrCode.encodeText(window.location.href, window.QrCode.Ecc.MEDIUM); wechatCanvas.innerHTML = window.QrCode.toSvgString(qr, 2, "#ffffff", "#000000"); qrRendered = true; } catch (error) { console.error("生成微信二维码失败", error); } }; const setWechatOpen = (open) => { if (!wechatWrap || !wechatButton || !wechatQr) return; if (open) ensureQrCode(); wechatWrap.classList.toggle("is-open", open); wechatButton.setAttribute("aria-expanded", open ? "true" : "false"); wechatQr.setAttribute("aria-hidden", open ? "false" : "true"); }; wechatButton?.addEventListener("click", (event) => { event.stopPropagation(); setWechatOpen(!wechatWrap?.classList.contains("is-open")); }); wechatWrap?.addEventListener("mouseenter", ensureQrCode); wechatWrap?.addEventListener("focusin", ensureQrCode); document.addEventListener("click", (event) => { if (wechatWrap && !wechatWrap.contains(event.target)) setWechatOpen(false); }); document.addEventListener("keydown", (event) => { if (event.key === "Escape") setWechatOpen(false); }); }); // 静态搜索页:当被作为静态壳提供(服务端未渲染结果、仅显示提示)而 URL 带有 q 时, // 通过只读接口 /api/search 在客户端拉取并渲染结果,使前台无需读取 SQLite。 document.querySelectorAll("[data-search-page]").forEach((page) => { const prompt = page.querySelector("[data-search-prompt]"); const term = (new URLSearchParams(window.location.search).get("q") || "").trim(); if (!prompt || !term || term.length > 100) return; const renderEmpty = () => { page.innerHTML = ""; const heading = document.createElement("div"); heading.className = "media-section-heading"; const h1 = document.createElement("h1"); h1.textContent = "“" + term + "” 的搜索结果"; heading.append(h1); const empty = document.createElement("p"); empty.className = "channel-empty"; empty.textContent = "没有找到匹配的文章,试试更换关键词。"; page.append(heading, empty); }; const renderResults = (items) => { page.innerHTML = ""; const heading = document.createElement("div"); heading.className = "media-section-heading"; const h1 = document.createElement("h1"); h1.textContent = "“" + term + "” 的搜索结果"; const count = document.createElement("span"); count.className = "search-result-count"; count.textContent = "共 " + items.length + " 条"; heading.append(h1, count); const list = document.createElement("div"); list.className = "media-feed-list"; for (const item of items) { const article = document.createElement("article"); article.className = "media-excerpt is-textonly"; const body = document.createElement("div"); body.className = "media-excerpt-body"; const title = document.createElement("h2"); const link = document.createElement("a"); link.href = item.url; link.textContent = item.title; title.append(link); const summary = document.createElement("p"); summary.textContent = item.summary || ""; body.append(title, summary); article.append(body); list.append(article); } page.append(heading, list); }; fetch("/api/search?q=" + encodeURIComponent(term), { headers: { Accept: "application/json" } }) .then((response) => (response.ok ? response.json() : [])) .then((items) => { if (!Array.isArray(items) || items.length === 0) { renderEmpty(); return; } renderResults(items.map((item) => ({ title: item.title, summary: item.summary, url: item.url }))); }) .catch(() => renderEmpty()); }); document.querySelectorAll("[data-back-to-top]").forEach((button) => { let scheduled = false; const updateVisibility = () => { button.classList.toggle("is-visible", window.scrollY > 520); scheduled = false; }; window.addEventListener("scroll", () => { if (scheduled) return; scheduled = true; window.requestAnimationFrame(updateVisibility); }, { passive: true }); button.addEventListener("click", () => { window.scrollTo({ top: 0, behavior: window.matchMedia("(prefers-reduced-motion: reduce)").matches ? "auto" : "smooth" }); }); updateVisibility(); }); document.querySelectorAll(".js-article-channel").forEach((channelSelect) => { const categorySelect = channelSelect.closest("form")?.querySelector(".js-article-category"); if (!categorySelect) return; const synchronizeCategories = (resetSelection) => { const channelId = channelSelect.value; let selectedIsAvailable = false; Array.from(categorySelect.options).forEach((option) => { if (!option.value) return; const available = channelId !== "" && option.dataset.channelId === channelId; option.hidden = !available; option.disabled = !available; if (available && option.selected) selectedIsAvailable = true; }); if (resetSelection || !selectedIsAvailable) categorySelect.value = ""; categorySelect.options[0].text = channelId ? "请选择二级分类" : "请先选择一级分类"; }; synchronizeCategories(false); channelSelect.addEventListener("change", () => synchronizeCategories(true)); });