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:
+273
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user