feat: 插件适配页面与系统完善(v0.1.0)

- 修复 /categories、/tags 因变量为 List 误用 .items 导致的 500
- 新增 layout.html 布局契约,消除插件页面 fallback 警告
- 适配 plugin-moments / plugin-photos / plugin-links(含友链申请表单与 RSS 订阅页)
- 适配 halo-weread-plugin(书架页)、留言板单页模板
- 适配 plugin-extra-api 的代码高亮与暗色样式
- 修复暗黑模式评论区不可见(补充 color-scheme 与兜底样式)
- 更新元数据:作者暖心向阳335、版本 0.1.0、requires >=2.26.0
This commit is contained in:
2026-09-28 16:50:09 +08:00
parent 2ed0c768a4
commit 8e6dccbd09
35 changed files with 3986 additions and 301 deletions
+273
View File
@@ -1 +1,274 @@
import "../css/main.css";
/**
* whitespace 留白主题 - 全站脚本
* 移植自 Typecho 主题 whitespace 的 main.js
*/
// ========================================
// 主题切换
// ========================================
function currentTheme(): string {
return document.documentElement.getAttribute("data-theme") || "light";
}
function toggleTheme(): void {
const newTheme = currentTheme() === "dark" ? "light" : "dark";
document.documentElement.setAttribute("data-theme", newTheme);
try {
localStorage.setItem("theme", newTheme);
} catch {
/* ignore */
}
}
function watchSystemTheme(): void {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
mediaQuery.addEventListener("change", (e) => {
let stored: string | null = null;
try {
stored = localStorage.getItem("theme");
} catch {
/* ignore */
}
if (!stored) {
document.documentElement.setAttribute(
"data-theme",
e.matches ? "dark" : "light",
);
}
});
}
// ========================================
// 搜索(plugin-search-widget)
// ========================================
function initSearch(): void {
const searchToggle = document.getElementById("search-toggle");
if (!searchToggle) return;
searchToggle.addEventListener("click", () => {
const widget = (window as unknown as Record<string, unknown>).SearchWidget as
| { open?: () => void }
| undefined;
if (widget && typeof widget.open === "function") {
widget.open();
}
});
}
// ========================================
// 移动端导航
// ========================================
function initMobileNav(): void {
const mobileMenuToggle = document.getElementById("mobile-menu-toggle");
const mobileNav = document.getElementById("mobile-nav");
if (!mobileMenuToggle || !mobileNav) return;
const menuIcon = `
<line x1="3" y1="12" x2="21" y2="12"></line>
<line x1="3" y1="6" x2="21" y2="6"></line>
<line x1="3" y1="18" x2="21" y2="18"></line>
`;
const closeIcon = `
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
`;
const icon = mobileMenuToggle.querySelector(".icon");
const closeMobileNav = () => {
if (mobileNav.classList.contains("active")) {
mobileNav.classList.remove("active");
if (icon) icon.innerHTML = menuIcon;
}
};
mobileMenuToggle.addEventListener("click", () => {
mobileNav.classList.toggle("active");
if (icon) icon.innerHTML = mobileNav.classList.contains("active") ? closeIcon : menuIcon;
});
window.addEventListener("resize", () => {
if (window.innerWidth > 768) closeMobileNav();
});
document.addEventListener("click", (e) => {
const target = e.target as Node;
if (!mobileNav.contains(target) && !mobileMenuToggle.contains(target)) {
closeMobileNav();
}
});
}
// ========================================
// 回到顶部
// ========================================
function initBackToTop(): void {
const backToTop = document.getElementById("back-to-top");
if (!backToTop) return;
backToTop.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
const handleScroll = () => {
if (window.scrollY > 300) {
backToTop.classList.add("visible");
} else {
backToTop.classList.remove("visible");
}
};
window.addEventListener("scroll", handleScroll, { passive: true });
handleScroll();
}
// ========================================
// 相对时间
// ========================================
function initRelativeTime(): void {
const formatter = new Intl.RelativeTimeFormat(navigator.language, {
numeric: "always",
});
const units: Array<[Intl.RelativeTimeFormatUnit, number]> = [
["year", 31536000],
["month", 2592000],
["day", 86400],
["hour", 3600],
["minute", 60],
];
document.querySelectorAll<HTMLElement>("[data-relative-time]").forEach((el) => {
const datetime = el.getAttribute("datetime");
if (!datetime) return;
const time = Date.parse(datetime);
if (Number.isNaN(time)) return;
const diffSeconds = Math.floor((Date.now() - time) / 1000);
// 仅对 24 小时内的内容显示相对时间,其余保留日期显示
if (diffSeconds < 0 || diffSeconds >= 86400) return;
if (diffSeconds < 60) {
el.textContent = formatter.format(-diffSeconds, "second");
return;
}
for (const [unit, seconds] of units) {
if (diffSeconds >= seconds) {
el.textContent = formatter.format(-Math.floor(diffSeconds / seconds), unit);
break;
}
}
});
}
// ========================================
// 图片懒加载
// ========================================
function initLazyImages(): void {
const images = document.querySelectorAll('img[loading="lazy"]');
if (!("IntersectionObserver" in window)) return;
const imageObserver = new IntersectionObserver(
(entries, observer) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target;
img.classList.add("loaded");
observer.unobserve(img);
}
});
},
{ rootMargin: "50px 0px" },
);
images.forEach((img) => imageObserver.observe(img));
}
// ========================================
// 平滑滚动
// ========================================
function initSmoothScroll(): void {
document.querySelectorAll<HTMLAnchorElement>('a[href^="#"]').forEach((anchor) => {
anchor.addEventListener("click", (e) => {
const href = anchor.getAttribute("href");
if (!href || href === "#") return;
const target = document.querySelector(href);
if (target) {
e.preventDefault();
const header = document.querySelector(".header");
const headerHeight = header ? header.getBoundingClientRect().height : 0;
const targetPosition =
target.getBoundingClientRect().top + window.scrollY - headerHeight - 20;
window.scrollTo({ top: targetPosition, behavior: "smooth" });
}
});
});
}
// ========================================
// 外部链接处理
// ========================================
function initExternalLinks(): void {
document.querySelectorAll<HTMLAnchorElement>(".article-content a").forEach((link) => {
const href = link.getAttribute("href");
if (
href &&
href.startsWith("http") &&
!href.includes(window.location.hostname)
) {
link.setAttribute("target", "_blank");
link.setAttribute("rel", "noopener noreferrer");
}
});
}
// ========================================
// 键盘快捷键
// ========================================
function initKeyboardShortcuts(): void {
document.addEventListener("keydown", (e) => {
const searchToggle = document.getElementById("search-toggle");
if ((e.ctrlKey || e.metaKey) && e.key === "k" && searchToggle) {
e.preventDefault();
searchToggle.click();
}
if ((e.ctrlKey || e.metaKey) && e.key === "d") {
e.preventDefault();
toggleTheme();
}
});
}
// ========================================
// 初始化
// ========================================
function init(): void {
const themeToggle = document.getElementById("theme-toggle");
if (themeToggle) {
themeToggle.addEventListener("click", toggleTheme);
}
watchSystemTheme();
initSearch();
initMobileNav();
initBackToTop();
initRelativeTime();
initLazyImages();
initSmoothScroll();
initExternalLinks();
initKeyboardShortcuts();
}
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", init);
} else {
init();
}