优化搜索后自动关闭搜索框,添加相关页面
This commit is contained in:
parent
474c777bbd
commit
864d134acd
@ -97,6 +97,7 @@ const normalizedPath =
|
|||||||
? "text-primary-600 dark:text-primary-400 border-b-2 border-primary-600 dark:border-primary-400"
|
? "text-primary-600 dark:text-primary-400 border-b-2 border-primary-600 dark:border-primary-400"
|
||||||
: "text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 hover:border-b-2 hover:border-primary-300 dark:hover:border-primary-700"
|
: "text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 hover:border-b-2 hover:border-primary-300 dark:hover:border-primary-700"
|
||||||
}`}
|
}`}
|
||||||
|
data-astro-prefetch="hover"
|
||||||
>
|
>
|
||||||
{link.text}
|
{link.text}
|
||||||
</a>
|
</a>
|
||||||
@ -252,7 +253,10 @@ const normalizedPath =
|
|||||||
class="hidden md:hidden fixed inset-x-0 top-16 z-40"
|
class="hidden md:hidden fixed inset-x-0 top-16 z-40"
|
||||||
id="mobile-menu"
|
id="mobile-menu"
|
||||||
>
|
>
|
||||||
<div id="mobile-menu-bg" class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm shadow-lg border-t border-gray-200 dark:border-gray-700/50 rounded-b-lg">
|
<div
|
||||||
|
id="mobile-menu-bg"
|
||||||
|
class="bg-white/95 dark:bg-gray-800/95 backdrop-blur-sm shadow-lg border-t border-gray-200 dark:border-gray-700/50 rounded-b-lg"
|
||||||
|
>
|
||||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-2">
|
||||||
<div class="grid gap-1">
|
<div class="grid gap-1">
|
||||||
{
|
{
|
||||||
@ -264,6 +268,7 @@ const normalizedPath =
|
|||||||
? "text-white bg-primary-600 dark:bg-primary-500 shadow-sm"
|
? "text-white bg-primary-600 dark:bg-primary-500 shadow-sm"
|
||||||
: "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800/70"
|
: "text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-800/70"
|
||||||
}`}
|
}`}
|
||||||
|
data-astro-prefetch="hover"
|
||||||
>
|
>
|
||||||
{link.text}
|
{link.text}
|
||||||
</a>
|
</a>
|
||||||
@ -286,9 +291,8 @@ const normalizedPath =
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// Header组件逻辑 - 使用"进入→绑定→退出完全清理"模式
|
// Header组件逻辑 - 使用"进入→绑定→退出完全清理"模式
|
||||||
(function() {
|
(function () {
|
||||||
|
|
||||||
// 存储所有事件监听器,便于统一清理
|
// 存储所有事件监听器,便于统一清理
|
||||||
const listeners: Array<{
|
const listeners: Array<{
|
||||||
element: EventTarget;
|
element: EventTarget;
|
||||||
@ -315,7 +319,7 @@ const normalizedPath =
|
|||||||
element: EventTarget | null,
|
element: EventTarget | null,
|
||||||
eventType: string,
|
eventType: string,
|
||||||
handler: EventListenerOrEventListenerObject,
|
handler: EventListenerOrEventListenerObject,
|
||||||
options?: boolean | AddEventListenerOptions
|
options?: boolean | AddEventListenerOptions,
|
||||||
): EventListenerOrEventListenerObject | null {
|
): EventListenerOrEventListenerObject | null {
|
||||||
if (!element) return null;
|
if (!element) return null;
|
||||||
|
|
||||||
@ -345,7 +349,27 @@ const normalizedPath =
|
|||||||
const scrollThreshold = 50;
|
const scrollThreshold = 50;
|
||||||
|
|
||||||
// 获取桌面端导航链接(排除移动端菜单中的链接)
|
// 获取桌面端导航链接(排除移动端菜单中的链接)
|
||||||
const navLinks = document.querySelectorAll('.hidden.md\\:flex a[href]');
|
const navLinks = document.querySelectorAll(".hidden.md\\:flex a[href]");
|
||||||
|
|
||||||
|
// 检查路径是否匹配导航项
|
||||||
|
function isPathMatchingNavItem(
|
||||||
|
currentPath: string,
|
||||||
|
navPath: string,
|
||||||
|
): boolean {
|
||||||
|
// 精确匹配
|
||||||
|
if (currentPath === navPath) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文章页面特殊处理 - 如果当前路径以 "/articles/" 开头,则匹配 "/articles" 导航
|
||||||
|
if (currentPath.startsWith("/articles/") && navPath === "/articles") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可以添加其他特殊情况的处理逻辑
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// 更新导航高亮状态
|
// 更新导航高亮状态
|
||||||
function updateNavHighlight(): void {
|
function updateNavHighlight(): void {
|
||||||
@ -360,7 +384,9 @@ const normalizedPath =
|
|||||||
// 更新桌面端导航链接
|
// 更新桌面端导航链接
|
||||||
navLinks.forEach((link) => {
|
navLinks.forEach((link) => {
|
||||||
const href = link.getAttribute("href");
|
const href = link.getAttribute("href");
|
||||||
const isActive = href === normalizedPath;
|
const isActive = href
|
||||||
|
? isPathMatchingNavItem(normalizedPath, href)
|
||||||
|
: false;
|
||||||
|
|
||||||
// 使用 classList.toggle 来切换类
|
// 使用 classList.toggle 来切换类
|
||||||
link.classList.toggle("text-primary-600", isActive);
|
link.classList.toggle("text-primary-600", isActive);
|
||||||
@ -379,10 +405,14 @@ const normalizedPath =
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 更新移动端导航链接
|
// 更新移动端导航链接
|
||||||
const mobileNavLinks = document.querySelectorAll('#mobile-menu a[href]');
|
const mobileNavLinks = document.querySelectorAll(
|
||||||
|
"#mobile-menu a[href]",
|
||||||
|
);
|
||||||
mobileNavLinks.forEach((link) => {
|
mobileNavLinks.forEach((link) => {
|
||||||
const href = link.getAttribute("href");
|
const href = link.getAttribute("href");
|
||||||
const isActive = href === normalizedPath;
|
const isActive = href
|
||||||
|
? isPathMatchingNavItem(normalizedPath, href)
|
||||||
|
: false;
|
||||||
|
|
||||||
// 使用 classList.toggle 来切换类
|
// 使用 classList.toggle 来切换类
|
||||||
link.classList.toggle("text-white", isActive);
|
link.classList.toggle("text-white", isActive);
|
||||||
@ -414,8 +444,8 @@ const normalizedPath =
|
|||||||
addListener(window, "scroll", updateHeaderBackground);
|
addListener(window, "scroll", updateHeaderBackground);
|
||||||
|
|
||||||
// 监听路由变化
|
// 监听路由变化
|
||||||
addListener(document, 'astro:page-load', updateNavHighlight);
|
addListener(document, "astro:page-load", updateNavHighlight);
|
||||||
addListener(document, 'astro:after-swap', updateNavHighlight);
|
addListener(document, "astro:after-swap", updateNavHighlight);
|
||||||
|
|
||||||
// 移动端菜单逻辑
|
// 移动端菜单逻辑
|
||||||
const mobileMenuButton = document.getElementById("mobile-menu-button");
|
const mobileMenuButton = document.getElementById("mobile-menu-button");
|
||||||
@ -424,7 +454,9 @@ const normalizedPath =
|
|||||||
const menuCloseIcon = document.getElementById("menu-close-icon");
|
const menuCloseIcon = document.getElementById("menu-close-icon");
|
||||||
|
|
||||||
// 移动端搜索面板元素
|
// 移动端搜索面板元素
|
||||||
const mobileSearchButton = document.getElementById("mobile-search-button");
|
const mobileSearchButton = document.getElementById(
|
||||||
|
"mobile-search-button",
|
||||||
|
);
|
||||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||||
const mobileSearch = document.getElementById("mobile-search");
|
const mobileSearch = document.getElementById("mobile-search");
|
||||||
const mobileSearchClose = document.getElementById("mobile-search-close");
|
const mobileSearchClose = document.getElementById("mobile-search-close");
|
||||||
@ -448,16 +480,23 @@ const normalizedPath =
|
|||||||
|
|
||||||
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
|
if (mobileMenuButton && mobileMenu && menuOpenIcon && menuCloseIcon) {
|
||||||
// 移动端菜单按钮点击事件 - 使用捕获模式确保事件优先处理
|
// 移动端菜单按钮点击事件 - 使用捕获模式确保事件优先处理
|
||||||
(mobileMenuButton as HTMLElement).style.pointerEvents = 'auto';
|
(mobileMenuButton as HTMLElement).style.pointerEvents = "auto";
|
||||||
|
|
||||||
addListener(mobileMenuButton, "click", (e) => {
|
addListener(
|
||||||
|
mobileMenuButton,
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
const expanded = mobileMenuButton.getAttribute("aria-expanded") === "true";
|
const expanded =
|
||||||
|
mobileMenuButton.getAttribute("aria-expanded") === "true";
|
||||||
|
|
||||||
// 切换菜单状态
|
// 切换菜单状态
|
||||||
mobileMenuButton.setAttribute("aria-expanded", (!expanded).toString());
|
mobileMenuButton.setAttribute(
|
||||||
|
"aria-expanded",
|
||||||
|
(!expanded).toString(),
|
||||||
|
);
|
||||||
|
|
||||||
if (expanded) {
|
if (expanded) {
|
||||||
// 直接隐藏菜单,不使用过渡效果
|
// 直接隐藏菜单,不使用过渡效果
|
||||||
@ -473,31 +512,44 @@ const normalizedPath =
|
|||||||
// 切换图标
|
// 切换图标
|
||||||
menuOpenIcon.classList.toggle("hidden");
|
menuOpenIcon.classList.toggle("hidden");
|
||||||
menuCloseIcon.classList.toggle("hidden");
|
menuCloseIcon.classList.toggle("hidden");
|
||||||
}, { capture: true });
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
|
|
||||||
// 为移动端导航链接添加点击事件
|
// 为移动端导航链接添加点击事件
|
||||||
const mobileNavLinks = document.querySelectorAll('#mobile-menu a[href]');
|
const mobileNavLinks = document.querySelectorAll(
|
||||||
mobileNavLinks.forEach(link => {
|
"#mobile-menu a[href]",
|
||||||
(link as HTMLElement).style.pointerEvents = 'auto';
|
);
|
||||||
addListener(link, 'click', (e) => {
|
mobileNavLinks.forEach((link) => {
|
||||||
|
(link as HTMLElement).style.pointerEvents = "auto";
|
||||||
|
addListener(
|
||||||
|
link,
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
// 不要阻止默认行为,因为需要跳转
|
// 不要阻止默认行为,因为需要跳转
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMobileMenu();
|
closeMobileMenu();
|
||||||
}, { capture: true });
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动端搜索按钮
|
// 移动端搜索按钮
|
||||||
if (mobileSearchButton && mobileSearchPanel) {
|
if (mobileSearchButton && mobileSearchPanel) {
|
||||||
// 搜索按钮点击事件 - 使用捕获模式确保事件优先处理
|
// 搜索按钮点击事件 - 使用捕获模式确保事件优先处理
|
||||||
(mobileSearchButton as HTMLElement).style.pointerEvents = 'auto';
|
(mobileSearchButton as HTMLElement).style.pointerEvents = "auto";
|
||||||
|
|
||||||
addListener(mobileSearchButton, "click", (e) => {
|
addListener(
|
||||||
|
mobileSearchButton,
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
// 检查搜索面板是否已经打开
|
// 检查搜索面板是否已经打开
|
||||||
const isSearchVisible = !mobileSearchPanel.classList.contains("hidden");
|
const isSearchVisible =
|
||||||
|
!mobileSearchPanel.classList.contains("hidden");
|
||||||
|
|
||||||
if (isSearchVisible) {
|
if (isSearchVisible) {
|
||||||
// 如果搜索面板已打开,则关闭它
|
// 如果搜索面板已打开,则关闭它
|
||||||
@ -510,35 +562,54 @@ const normalizedPath =
|
|||||||
mobileSearchPanel.classList.remove("hidden");
|
mobileSearchPanel.classList.remove("hidden");
|
||||||
if (mobileSearch) mobileSearch.focus();
|
if (mobileSearch) mobileSearch.focus();
|
||||||
}
|
}
|
||||||
}, { capture: true });
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
|
|
||||||
// 搜索面板关闭按钮
|
// 搜索面板关闭按钮
|
||||||
if (mobileSearchClose) {
|
if (mobileSearchClose) {
|
||||||
(mobileSearchClose as HTMLElement).style.pointerEvents = 'auto';
|
(mobileSearchClose as HTMLElement).style.pointerEvents = "auto";
|
||||||
addListener(mobileSearchClose, "click", (e) => {
|
addListener(
|
||||||
|
mobileSearchClose,
|
||||||
|
"click",
|
||||||
|
(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
closeMobileSearch();
|
closeMobileSearch();
|
||||||
}, { capture: true });
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理移动端主题切换容器
|
// 处理移动端主题切换容器
|
||||||
const themeToggleContainer = document.getElementById('theme-toggle-container');
|
const themeToggleContainer = document.getElementById(
|
||||||
|
"theme-toggle-container",
|
||||||
|
);
|
||||||
if (themeToggleContainer) {
|
if (themeToggleContainer) {
|
||||||
(themeToggleContainer as HTMLElement).style.pointerEvents = 'auto';
|
(themeToggleContainer as HTMLElement).style.pointerEvents = "auto";
|
||||||
addListener(themeToggleContainer, 'click', (e: Event) => {
|
addListener(
|
||||||
|
themeToggleContainer,
|
||||||
|
"click",
|
||||||
|
(e: Event) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
// 如果点击的不是主题切换按钮本身,则手动触发主题切换
|
// 如果点击的不是主题切换按钮本身,则手动触发主题切换
|
||||||
if (target.id !== 'theme-toggle-button' && !target.closest('#theme-toggle-button')) {
|
if (
|
||||||
|
target.id !== "theme-toggle-button" &&
|
||||||
|
!target.closest("#theme-toggle-button")
|
||||||
|
) {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
// 获取容器内的主题切换按钮并模拟点击
|
// 获取容器内的主题切换按钮并模拟点击
|
||||||
const toggleButton = themeToggleContainer.querySelector('#theme-toggle-button');
|
const toggleButton = themeToggleContainer.querySelector(
|
||||||
|
"#theme-toggle-button",
|
||||||
|
);
|
||||||
if (toggleButton) {
|
if (toggleButton) {
|
||||||
(toggleButton as HTMLElement).click();
|
(toggleButton as HTMLElement).click();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, { capture: true });
|
},
|
||||||
|
{ capture: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -566,6 +637,7 @@ const normalizedPath =
|
|||||||
const mobileResults = document.getElementById("mobile-search-results");
|
const mobileResults = document.getElementById("mobile-search-results");
|
||||||
const mobileList = document.getElementById("mobile-search-list");
|
const mobileList = document.getElementById("mobile-search-list");
|
||||||
const mobileMessage = document.getElementById("mobile-search-message");
|
const mobileMessage = document.getElementById("mobile-search-message");
|
||||||
|
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||||
|
|
||||||
// 获取文章数据
|
// 获取文章数据
|
||||||
async function fetchArticles(): Promise<void> {
|
async function fetchArticles(): Promise<void> {
|
||||||
@ -710,7 +782,7 @@ const normalizedPath =
|
|||||||
|
|
||||||
return `
|
return `
|
||||||
<li>
|
<li>
|
||||||
<a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/70">
|
<a href="/articles/${article.id}" class="block px-4 py-3 hover:bg-gray-100 dark:hover:bg-gray-700/70 search-result-link">
|
||||||
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${highlightedTitle}</h3>
|
<h3 class="text-sm font-medium text-gray-800 dark:text-gray-200 truncate">${highlightedTitle}</h3>
|
||||||
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${highlightedSummary}</p>` : ""}
|
${article.summary ? `<p class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate">${highlightedSummary}</p>` : ""}
|
||||||
${contentMatch}
|
${contentMatch}
|
||||||
@ -736,6 +808,9 @@ const normalizedPath =
|
|||||||
`;
|
`;
|
||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
|
|
||||||
|
// 为搜索结果链接添加点击事件,点击后关闭搜索面板
|
||||||
|
addSearchResultsClickListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 节流搜索
|
// 节流搜索
|
||||||
@ -744,7 +819,7 @@ const normalizedPath =
|
|||||||
searchArticles(
|
searchArticles(
|
||||||
value,
|
value,
|
||||||
desktopList as HTMLElement,
|
desktopList as HTMLElement,
|
||||||
desktopMessage as HTMLElement
|
desktopMessage as HTMLElement,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
@ -754,7 +829,7 @@ const normalizedPath =
|
|||||||
searchArticles(
|
searchArticles(
|
||||||
value,
|
value,
|
||||||
mobileList as HTMLElement,
|
mobileList as HTMLElement,
|
||||||
mobileMessage as HTMLElement
|
mobileMessage as HTMLElement,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, 300);
|
}, 300);
|
||||||
@ -812,7 +887,9 @@ const normalizedPath =
|
|||||||
// ESC键关闭搜索面板
|
// ESC键关闭搜索面板
|
||||||
addListener(mobileSearch, "keydown", ((e: KeyboardEvent) => {
|
addListener(mobileSearch, "keydown", ((e: KeyboardEvent) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
const mobileSearchPanel = document.getElementById(
|
||||||
|
"mobile-search-panel",
|
||||||
|
);
|
||||||
if (mobileSearchPanel) {
|
if (mobileSearchPanel) {
|
||||||
mobileSearchPanel.classList.add("hidden");
|
mobileSearchPanel.classList.add("hidden");
|
||||||
}
|
}
|
||||||
@ -821,17 +898,50 @@ const normalizedPath =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 关闭所有搜索面板的函数
|
||||||
|
function closeSearchPanels(): void {
|
||||||
|
// 关闭桌面端搜索结果
|
||||||
|
const desktopSearchResults = document.getElementById("desktop-search-results");
|
||||||
|
if (desktopSearchResults) {
|
||||||
|
desktopSearchResults.classList.add("hidden");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 关闭移动端搜索面板
|
||||||
|
const mobileSearchPanel = document.getElementById("mobile-search-panel");
|
||||||
|
if (mobileSearchPanel) {
|
||||||
|
mobileSearchPanel.classList.add("hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 为搜索结果链接添加点击事件
|
||||||
|
function addSearchResultsClickListeners(): void {
|
||||||
|
// 获取所有搜索结果链接
|
||||||
|
const searchResultLinks = document.querySelectorAll('.search-result-link');
|
||||||
|
|
||||||
|
// 为每个链接添加点击事件
|
||||||
|
searchResultLinks.forEach(link => {
|
||||||
|
addListener(link, 'click', () => {
|
||||||
|
// 点击搜索结果后关闭所有搜索面板
|
||||||
|
closeSearchPanels();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 注册清理函数
|
// 注册清理函数
|
||||||
function registerCleanup(): void {
|
function registerCleanup(): void {
|
||||||
// Astro 事件
|
// Astro 事件
|
||||||
document.addEventListener('astro:before-preparation', cleanup, { once: true });
|
document.addEventListener("astro:before-preparation", cleanup, {
|
||||||
document.addEventListener('astro:before-swap', cleanup, { once: true });
|
once: true,
|
||||||
|
});
|
||||||
|
document.addEventListener("astro:before-swap", cleanup, { once: true });
|
||||||
|
|
||||||
// Swup 事件
|
// Swup 事件
|
||||||
document.addEventListener('swup:willReplaceContent', cleanup, { once: true });
|
document.addEventListener("swup:willReplaceContent", cleanup, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
|
||||||
// 页面卸载
|
// 页面卸载
|
||||||
window.addEventListener('beforeunload', cleanup, { once: true });
|
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化全部功能
|
// 初始化全部功能
|
||||||
@ -843,22 +953,28 @@ const normalizedPath =
|
|||||||
initHeader();
|
initHeader();
|
||||||
initSearch();
|
initSearch();
|
||||||
registerCleanup();
|
registerCleanup();
|
||||||
|
|
||||||
|
// 在页面路由变化时关闭搜索面板
|
||||||
|
addListener(document, 'astro:page-load', closeSearchPanels);
|
||||||
|
addListener(document, 'astro:after-swap', closeSearchPanels);
|
||||||
|
addListener(document, 'swup:contentReplaced', closeSearchPanels);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在页面加载时初始化
|
// 在页面加载时初始化
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", setupHeader, { once: true });
|
document.addEventListener("DOMContentLoaded", setupHeader, {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// 使用setTimeout确保处于事件队列末尾,避免可能的事件冲突
|
// 使用setTimeout确保处于事件队列末尾,避免可能的事件冲突
|
||||||
setTimeout(setupHeader, 0);
|
setTimeout(setupHeader, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 在页面转换后重新初始化
|
// 在页面转换后重新初始化
|
||||||
document.addEventListener('astro:after-swap', setupHeader);
|
document.addEventListener("astro:after-swap", setupHeader);
|
||||||
document.addEventListener('astro:page-load', setupHeader);
|
document.addEventListener("astro:page-load", setupHeader);
|
||||||
|
|
||||||
// Swup页面内容替换后重新初始化
|
// Swup页面内容替换后重新初始化
|
||||||
document.addEventListener('swup:contentReplaced', setupHeader);
|
document.addEventListener("swup:contentReplaced", setupHeader);
|
||||||
})();
|
})();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -105,5 +105,36 @@ const { title = SITE_NAME, description = SITE_DESCRIPTION, date, author, tags, i
|
|||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
<Footer icp={ICP} psbIcp={PSB_ICP} psbIcpUrl={PSB_ICP_URL} />
|
<Footer icp={ICP} psbIcp={PSB_ICP} psbIcpUrl={PSB_ICP_URL} />
|
||||||
|
|
||||||
|
<!-- 预获取脚本 -->
|
||||||
|
<script>
|
||||||
|
// 在DOM加载完成后执行
|
||||||
|
document.addEventListener('astro:page-load', () => {
|
||||||
|
// 获取所有视口预获取链接
|
||||||
|
const viewportLinks = document.querySelectorAll('[data-astro-prefetch="viewport"]');
|
||||||
|
|
||||||
|
if (viewportLinks.length > 0) {
|
||||||
|
// 创建一个交叉观察器
|
||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
const link = entry.target;
|
||||||
|
// 进入视口时,添加data-astro-prefetch="true"属性触发预获取
|
||||||
|
if (link.getAttribute('data-astro-prefetch') === 'viewport') {
|
||||||
|
link.setAttribute('data-astro-prefetch', 'true');
|
||||||
|
}
|
||||||
|
// 一旦预获取,就不再观察这个链接
|
||||||
|
observer.unobserve(link);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 观察所有视口预获取链接
|
||||||
|
viewportLinks.forEach(link => {
|
||||||
|
observer.observe(link);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
@ -81,17 +81,69 @@ const breadcrumbs = section ? section.split("/") : [];
|
|||||||
|
|
||||||
// 获取相关文章
|
// 获取相关文章
|
||||||
const allArticles = await getCollection("articles");
|
const allArticles = await getCollection("articles");
|
||||||
const relatedArticles = allArticles
|
|
||||||
|
// 1. 尝试通过标签匹配相关文章
|
||||||
|
let relatedArticles = allArticles
|
||||||
.filter(
|
.filter(
|
||||||
(a) =>
|
(a) => {
|
||||||
a.id !== article.id &&
|
const hasCommonTags = a.id !== article.id &&
|
||||||
a.data.tags &&
|
a.data.tags &&
|
||||||
article.data.tags &&
|
article.data.tags &&
|
||||||
a.data.tags.some((tag) => article.data.tags?.includes(tag)),
|
a.data.tags.length > 0 &&
|
||||||
|
article.data.tags.length > 0 &&
|
||||||
|
a.data.tags.some((tag) => article.data.tags?.includes(tag));
|
||||||
|
|
||||||
|
return hasCommonTags;
|
||||||
|
}
|
||||||
)
|
)
|
||||||
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||||
.slice(0, 3);
|
.slice(0, 3);
|
||||||
|
|
||||||
|
// 跟踪相关文章的匹配方式: "tag", "directory", "latest"
|
||||||
|
let relatedArticlesMatchType = relatedArticles.length > 0 ? "tag" : "";
|
||||||
|
|
||||||
|
// 2. 如果标签匹配没有找到足够的相关文章,尝试根据目录结构匹配
|
||||||
|
if (relatedArticles.length < 3) {
|
||||||
|
// 获取当前文章的目录路径
|
||||||
|
const currentPath = article.id.includes('/')
|
||||||
|
? article.id.substring(0, article.id.lastIndexOf('/'))
|
||||||
|
: '';
|
||||||
|
|
||||||
|
// 如果有目录路径,查找同目录的其他文章
|
||||||
|
if (currentPath) {
|
||||||
|
// 收集同目录下的文章,但排除已经通过标签匹配的和当前文章
|
||||||
|
const dirRelatedArticles = allArticles
|
||||||
|
.filter(a =>
|
||||||
|
a.id !== article.id &&
|
||||||
|
a.id.startsWith(currentPath + '/') &&
|
||||||
|
!relatedArticles.some(r => r.id === a.id)
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||||
|
.slice(0, 3 - relatedArticles.length);
|
||||||
|
|
||||||
|
if (dirRelatedArticles.length > 0) {
|
||||||
|
relatedArticles = [...relatedArticles, ...dirRelatedArticles];
|
||||||
|
relatedArticlesMatchType = relatedArticles.length > 0 && !relatedArticlesMatchType ? "directory" : relatedArticlesMatchType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 如果仍然没有找到足够的相关文章,则选择最新的文章(排除当前文章和已选择的文章)
|
||||||
|
if (relatedArticles.length < 3) {
|
||||||
|
const latestArticles = allArticles
|
||||||
|
.filter(a =>
|
||||||
|
a.id !== article.id &&
|
||||||
|
!relatedArticles.some(r => r.id === a.id)
|
||||||
|
)
|
||||||
|
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime())
|
||||||
|
.slice(0, 3 - relatedArticles.length);
|
||||||
|
|
||||||
|
if (latestArticles.length > 0) {
|
||||||
|
relatedArticles = [...relatedArticles, ...latestArticles];
|
||||||
|
relatedArticlesMatchType = relatedArticles.length > 0 && !relatedArticlesMatchType ? "latest" : relatedArticlesMatchType;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 准备文章描述
|
// 准备文章描述
|
||||||
const description =
|
const description =
|
||||||
article.data.summary ||
|
article.data.summary ||
|
||||||
@ -147,6 +199,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
<a
|
<a
|
||||||
href={`/articles${view ? `/${view}` : ""}`}
|
href={`/articles${view ? `/${view}` : ""}`}
|
||||||
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm"
|
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm"
|
||||||
|
data-astro-prefetch="hover"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
@ -196,7 +249,6 @@ function getArticleUrl(articleId: string) {
|
|||||||
{article.data.date.toLocaleDateString("zh-CN")}
|
{article.data.date.toLocaleDateString("zh-CN")}
|
||||||
</time>
|
</time>
|
||||||
|
|
||||||
{/* 显示文章所在目录 */}
|
|
||||||
{
|
{
|
||||||
section && (
|
section && (
|
||||||
<span class="flex items-center">
|
<span class="flex items-center">
|
||||||
@ -232,6 +284,7 @@ function getArticleUrl(articleId: string) {
|
|||||||
<a
|
<a
|
||||||
href={`/articles?tag=${tag}`}
|
href={`/articles?tag=${tag}`}
|
||||||
class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded hover:bg-primary-100 dark:hover:bg-primary-800/30"
|
class="text-xs bg-primary-50 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400 py-1 px-2 rounded hover:bg-primary-100 dark:hover:bg-primary-800/30"
|
||||||
|
data-astro-prefetch="hover"
|
||||||
>
|
>
|
||||||
#{tag}
|
#{tag}
|
||||||
</a>
|
</a>
|
||||||
@ -322,13 +375,15 @@ function getArticleUrl(articleId: string) {
|
|||||||
relatedArticles.length > 0 && (
|
relatedArticles.length > 0 && (
|
||||||
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-dark-border">
|
<div class="mt-12 pt-8 border-t border-secondary-200 dark:border-dark-border">
|
||||||
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
|
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
|
||||||
相关文章
|
{relatedArticlesMatchType === "tag" ? "相关文章" :
|
||||||
|
relatedArticlesMatchType === "directory" ? "同类文章" : "推荐阅读"}
|
||||||
</h2>
|
</h2>
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||||
{relatedArticles.map((relatedArticle) => (
|
{relatedArticles.map((relatedArticle) => (
|
||||||
<a
|
<a
|
||||||
href={getArticleUrl(relatedArticle.id)}
|
href={getArticleUrl(relatedArticle.id)}
|
||||||
class="block p-5 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-dark-card hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
class="block p-5 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-dark-card hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||||
|
data-astro-prefetch="viewport"
|
||||||
>
|
>
|
||||||
<h3 class="font-bold text-lg mb-2 line-clamp-2 text-gray-800 dark:text-gray-200 hover:text-primary-700 dark:hover:text-primary-400">
|
<h3 class="font-bold text-lg mb-2 line-clamp-2 text-gray-800 dark:text-gray-200 hover:text-primary-700 dark:hover:text-primary-400">
|
||||||
{relatedArticle.data.title}
|
{relatedArticle.data.title}
|
||||||
|
@ -222,7 +222,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
viewMode === 'grid'
|
viewMode === 'grid'
|
||||||
? 'text-primary-600'
|
? 'text-primary-600'
|
||||||
: 'text-gray-400 hover:text-gray-500'
|
: 'text-gray-400 hover:text-gray-500'
|
||||||
}`}>
|
}`}
|
||||||
|
data-astro-prefetch="hover">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -232,7 +233,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
viewMode === 'timeline'
|
viewMode === 'timeline'
|
||||||
? 'text-primary-600'
|
? 'text-primary-600'
|
||||||
: 'text-gray-400 hover:text-gray-500'
|
: 'text-gray-400 hover:text-gray-500'
|
||||||
}`}>
|
}`}
|
||||||
|
data-astro-prefetch="hover">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 20H5a2 2 0 01-2-2V6a2 2 0 012-2h10a2 2 0 012 2v1m2 13a2 2 0 01-2-2V7m2 13a2 2 0 002-2V9a2 2 0 00-2-2h-2m-4-3H9M7 16h6M7 8h6v4H7V8z" />
|
||||||
</svg>
|
</svg>
|
||||||
@ -249,7 +251,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
||||||
{!tagFilter && pathSegments.length > 0 && (
|
{!tagFilter && pathSegments.length > 0 && (
|
||||||
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/${view}`}
|
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/${view}`}
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg">
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||||
|
data-astro-prefetch="hover">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -276,7 +279,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`/articles/${dirLink}`}
|
<a href={`/articles/${dirLink}`}
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg">
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg"
|
||||||
|
data-astro-prefetch="viewport">
|
||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
<div class="w-10 h-10 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -319,7 +323,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
// 显示标签过滤后的文章
|
// 显示标签过滤后的文章
|
||||||
filteredArticles.map(article => (
|
filteredArticles.map(article => (
|
||||||
<a href={getArticleUrl(article.id)}
|
<a href={getArticleUrl(article.id)}
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg">
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article"
|
||||||
|
data-astro-prefetch="viewport">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -389,7 +394,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<a href={`/articles/${article.id}`}
|
<a href={`/articles/${article.id}`}
|
||||||
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg">
|
class="group flex flex-col h-full p-5 border border-gray-200 dark:border-gray-700 rounded-xl bg-white dark:bg-gray-800 hover:shadow-xl hover:-translate-y-1 shadow-lg recent-article"
|
||||||
|
data-astro-prefetch="viewport">
|
||||||
<div class="flex items-start">
|
<div class="flex items-start">
|
||||||
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-primary-100 text-primary-600 group-hover:bg-primary-200">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
@ -459,7 +465,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
isActive
|
isActive
|
||||||
? 'bg-primary-600 text-white dark:bg-primary-500 dark:text-gray-100 hover:bg-primary-700 dark:hover:bg-primary-600 shadow-md hover:shadow-lg'
|
? 'bg-primary-600 text-white dark:bg-primary-500 dark:text-gray-100 hover:bg-primary-700 dark:hover:bg-primary-600 shadow-md hover:shadow-lg'
|
||||||
: 'bg-primary-50 dark:bg-gray-700/50 text-primary-600 dark:text-gray-300 hover:bg-primary-100 dark:hover:bg-gray-700 hover:text-primary-700 dark:hover:text-primary-400'
|
: 'bg-primary-50 dark:bg-gray-700/50 text-primary-600 dark:text-gray-300 hover:bg-primary-100 dark:hover:bg-gray-700 hover:text-primary-700 dark:hover:text-primary-400'
|
||||||
}`}>
|
}`}
|
||||||
|
data-astro-prefetch="hover">
|
||||||
{tag}
|
{tag}
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
@ -483,7 +490,8 @@ function getArticleUrl(articleId: string) {
|
|||||||
|
|
||||||
{/* 文章卡片 */}
|
{/* 文章卡片 */}
|
||||||
<a href={`/articles/${article.id}${viewMode ? `/${viewMode}` : ''}`}
|
<a href={`/articles/${article.id}${viewMode ? `/${viewMode}` : ''}`}
|
||||||
class={`group/card ml-10 md:ml-0 ${isEven ? 'md:mr-[50%] md:pr-8' : 'md:ml-[50%] md:pl-8'} block`}>
|
class={`group/card ml-10 md:ml-0 ${isEven ? 'md:mr-[50%] md:pr-8' : 'md:ml-[50%] md:pl-8'} block`}
|
||||||
|
data-astro-prefetch="viewport">
|
||||||
<article class="relative flex flex-col gap-4 rounded-xl bg-white dark:bg-gray-800 p-6 shadow-lg hover:shadow-xl hover:-translate-y-1 border border-gray-200 dark:border-gray-700">
|
<article class="relative flex flex-col gap-4 rounded-xl bg-white dark:bg-gray-800 p-6 shadow-lg hover:shadow-xl hover:-translate-y-1 border border-gray-200 dark:border-gray-700">
|
||||||
{/* 日期标签 */}
|
{/* 日期标签 */}
|
||||||
<time datetime={article.data.date.toISOString()}
|
<time datetime={article.data.date.toISOString()}
|
||||||
|
Loading…
Reference in New Issue
Block a user