优化swup视图,高亮菜单,代码块名称
This commit is contained in:
parent
fc7653011b
commit
2c71dcdbd9
@ -8,7 +8,6 @@ import rehypeExternalLinks from "rehype-external-links";
|
||||
import sitemap from "@astrojs/sitemap";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import swup from "@swup/astro"
|
||||
import { SITE_URL } from "./src/consts";
|
||||
import compressor from "astro-compressor";
|
||||
import vercel from "@astrojs/vercel";
|
||||
@ -54,10 +53,6 @@ export default defineConfig({
|
||||
integrations: [
|
||||
// 使用Astro官方的MDX支持
|
||||
mdx(),
|
||||
swup({
|
||||
cache: true,
|
||||
preload: true,
|
||||
}),
|
||||
react(),
|
||||
// 使用文章索引生成器
|
||||
articleIndexerIntegration(),
|
||||
|
5847
package-lock.json
generated
5847
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -18,8 +18,10 @@
|
||||
"@expressive-code/plugin-collapsible-sections": "^0.41.2",
|
||||
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
||||
"@mermaid-js/mermaid-cli": "^11.4.2",
|
||||
"@swup/astro": "^1.6.0",
|
||||
"@swup/fragment-plugin": "^1.1.1",
|
||||
"@swup/head-plugin": "^2.3.1",
|
||||
"@swup/preload-plugin": "^3.2.11",
|
||||
"@swup/progress-plugin": "^3.2.0",
|
||||
"@tailwindcss/vite": "^4.1.4",
|
||||
"@types/react": "^19.1.2",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
|
@ -195,10 +195,11 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
// 单独保存清理事件的监听器引用
|
||||
const cleanupListeners = [];
|
||||
|
||||
|
||||
// 添加事件监听器并记录,方便后续统一清理
|
||||
function addListener(element, eventType, handler, options) {
|
||||
if (!element) {
|
||||
console.warn(`[面包屑尝试为不存在的元素添加事件`);
|
||||
console.warn(`[面包屑]尝试为不存在的元素添加事件`);
|
||||
return null;
|
||||
}
|
||||
|
||||
@ -214,7 +215,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
try {
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
} catch (err) {
|
||||
console.error(`[面包屑移除事件监听器出错:`, err);
|
||||
console.error(`[面包屑]移除事件监听器出错:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
@ -226,7 +227,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
try {
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
} catch (err) {
|
||||
console.error(`[面包屑移除清理监听器出错:`, err);
|
||||
console.error(`[面包屑]移除清理监听器出错:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
@ -234,6 +235,386 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
cleanupListeners.length = 0;
|
||||
}
|
||||
|
||||
// 获取当前URL路径(与导航栏保持一致)
|
||||
function getCurrentPath() {
|
||||
const path = window.location.pathname;
|
||||
return path === '/' ? '/' : path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
}
|
||||
|
||||
// 解析URL参数
|
||||
function getUrlSearchParams() {
|
||||
return new URLSearchParams(window.location.search);
|
||||
}
|
||||
|
||||
// 获取页面类型
|
||||
function getPageType(path) {
|
||||
if (path === '/filtered' || path.startsWith('/filtered')) {
|
||||
return 'filter';
|
||||
} else if (path.includes('/articles/') && !path.endsWith('/articles/')) {
|
||||
// 检查是否是文章详情页
|
||||
const segments = path.split('/').filter(s => s);
|
||||
// 如果路径中包含.html或.md,则认为是文章详情页
|
||||
if (segments.length > 1 && (segments[segments.length - 1].includes('.html') || segments[segments.length - 1].includes('.md'))) {
|
||||
return 'article';
|
||||
}
|
||||
return 'grid'; // 默认为网格视图
|
||||
} else {
|
||||
return 'grid'; // 默认为网格视图
|
||||
}
|
||||
}
|
||||
|
||||
// 提取路径段
|
||||
function getPathSegments(path) {
|
||||
// 如果路径为空或根路径,直接返回空数组
|
||||
if (!path || path === '/') {
|
||||
return [];
|
||||
}
|
||||
|
||||
// 标准化路径:移除结尾的斜杠并确保开头有斜杠
|
||||
let normalizedPath = path;
|
||||
if (normalizedPath.endsWith('/')) {
|
||||
normalizedPath = normalizedPath.slice(0, -1);
|
||||
}
|
||||
if (!normalizedPath.startsWith('/')) {
|
||||
normalizedPath = '/' + normalizedPath;
|
||||
}
|
||||
|
||||
// 拆分路径为段落
|
||||
const segments = normalizedPath.split('/').filter(s => s);
|
||||
|
||||
// 确定基础路径部分 - 在本应用中是 "articles"
|
||||
const basePathSegment = 'articles';
|
||||
|
||||
// 移除基础路径部分(如果存在于路径的第一段)
|
||||
const pathWithoutBase = segments[0] === basePathSegment
|
||||
? segments.slice(1)
|
||||
: segments;
|
||||
|
||||
// 移除尾部的文件名(如果存在)
|
||||
let result = [...pathWithoutBase];
|
||||
const lastSegment = result[result.length - 1];
|
||||
if (lastSegment && (lastSegment.includes('.html') || lastSegment.includes('.md'))) {
|
||||
result.pop();
|
||||
}
|
||||
|
||||
// 对每个段进行解码
|
||||
return result.map(segment => decodeURIComponent(segment));
|
||||
}
|
||||
|
||||
// 获取文章标题(对于文章页面)
|
||||
function getArticleTitle(path) {
|
||||
// 从路径中提取文件名
|
||||
const segments = path.split('/');
|
||||
const fileName = segments[segments.length - 1];
|
||||
|
||||
if (fileName && (fileName.includes('.html') || fileName.includes('.md'))) {
|
||||
// 移除扩展名,将连字符替换为空格,首字母大写
|
||||
let title = fileName.replace(/\.(html|md)$/, '')
|
||||
.replace(/-/g, ' ')
|
||||
.replace(/\b\w/g, c => c.toUpperCase());
|
||||
return title;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
// 动态更新面包屑
|
||||
function updateBreadcrumbs() {
|
||||
// 获取当前路径和相关信息
|
||||
const currentPath = getCurrentPath();
|
||||
const pageType = getPageType(currentPath);
|
||||
const searchParams = getUrlSearchParams();
|
||||
const pathSegments = getPathSegments(currentPath);
|
||||
const articleTitle = pageType === 'article' ? getArticleTitle(currentPath) : '';
|
||||
|
||||
// 获取面包屑容器
|
||||
const breadcrumbContainer = document.querySelector('.flex.items-center.text-sm.overflow-hidden');
|
||||
if (!breadcrumbContainer) {
|
||||
console.warn('[面包屑]找不到面包屑容器,无法更新');
|
||||
return;
|
||||
}
|
||||
|
||||
// 生成路径面包屑HTML
|
||||
let breadcrumbsHtml = `
|
||||
<a href="/articles/" class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center flex-shrink-0">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd" />
|
||||
</svg>
|
||||
文章
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 网格视图或文章详情中的目录路径
|
||||
if (pageType === 'grid' || (pageType === 'article' && pathSegments.length > 0)) {
|
||||
breadcrumbsHtml += `<div class="flex items-center overflow-hidden">
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>`;
|
||||
|
||||
// 移动端使用智能截断
|
||||
breadcrumbsHtml += `<div class="flex md:hidden items-center">`;
|
||||
|
||||
if (pathSegments.length > 2) {
|
||||
// 第一个路径段
|
||||
const firstSegment = pathSegments[0];
|
||||
const firstPath = encodeURIComponent(pathSegments.slice(0, 1).join('/'));
|
||||
|
||||
breadcrumbsHtml += `
|
||||
<a
|
||||
href="/articles/${firstPath}/"
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[80px] sm:max-w-[100px] flex-shrink-0"
|
||||
>
|
||||
${firstSegment}
|
||||
</a>
|
||||
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">...</span>
|
||||
`;
|
||||
|
||||
// 最后一个路径段
|
||||
if (pathSegments.length > 1) {
|
||||
const lastSegment = pathSegments[pathSegments.length - 1];
|
||||
const lastPath = pathSegments.map(encodeURIComponent).join('/');
|
||||
|
||||
breadcrumbsHtml += `
|
||||
<a
|
||||
href="/articles/${lastPath}/"
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[80px] sm:max-w-[120px] flex-shrink-0"
|
||||
>
|
||||
${lastSegment}
|
||||
</a>
|
||||
`;
|
||||
}
|
||||
} else {
|
||||
// 如果段落不多,则全部显示
|
||||
breadcrumbsHtml += pathSegments.map((segment, index) => {
|
||||
const segmentPath = pathSegments.slice(0, index + 1).map(encodeURIComponent).join('/');
|
||||
return `
|
||||
<span class="flex items-center flex-shrink-0">
|
||||
${index > 0 ? '<span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>' : ''}
|
||||
<a
|
||||
href="/articles/${segmentPath}/"
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[100px] sm:max-w-[150px]"
|
||||
>
|
||||
${segment}
|
||||
</a>
|
||||
</span>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
breadcrumbsHtml += `</div>`;
|
||||
|
||||
// 桌面端显示全部路径段
|
||||
breadcrumbsHtml += `<div class="hidden md:flex items-center flex-wrap">`;
|
||||
|
||||
breadcrumbsHtml += pathSegments.map((segment, index) => {
|
||||
const segmentPath = pathSegments.slice(0, index + 1).map(encodeURIComponent).join('/');
|
||||
return `
|
||||
<span class="flex items-center flex-shrink-0">
|
||||
${index > 0 ? '<span class="mx-2 text-secondary-300 dark:text-secondary-600">/</span>' : ''}
|
||||
<a
|
||||
href="/articles/${segmentPath}/"
|
||||
class="text-secondary-600 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 truncate max-w-[200px] lg:max-w-[250px] xl:max-w-[300px]"
|
||||
>
|
||||
${segment}
|
||||
</a>
|
||||
</span>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
breadcrumbsHtml += `</div></div>`;
|
||||
}
|
||||
|
||||
// 筛选视图中的搜索参数展示
|
||||
if (pageType === 'filter' && searchParams.toString()) {
|
||||
breadcrumbsHtml += `
|
||||
<div class="flex items-center overflow-hidden">
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>
|
||||
<span class="text-secondary-600 dark:text-secondary-400 truncate max-w-[120px] sm:max-w-[180px] md:max-w-[250px]">
|
||||
筛选
|
||||
${searchParams.toString() ? '<span class="ml-1">- 搜索结果</span>' : ''}
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 文章标题 - 仅在文章详情页显示
|
||||
if (pageType === 'article' && articleTitle) {
|
||||
breadcrumbsHtml += `
|
||||
<span class="mx-2 text-secondary-300 dark:text-secondary-600 flex-shrink-0">/</span>
|
||||
<span class="text-secondary-600 dark:text-secondary-400 truncate max-w-[120px] sm:max-w-[180px] md:max-w-[250px]">${articleTitle}</span>
|
||||
`;
|
||||
}
|
||||
|
||||
// 更新面包屑容器内容
|
||||
breadcrumbContainer.innerHTML = breadcrumbsHtml;
|
||||
|
||||
// 更新视图切换按钮
|
||||
updateViewSwitchButtons(pageType, currentPath, searchParams);
|
||||
|
||||
// 更新返回按钮(对于文章页面)
|
||||
if (pageType === 'article') {
|
||||
updateBackButton(currentPath, pathSegments.join('/'));
|
||||
}
|
||||
}
|
||||
|
||||
// 更新视图切换按钮
|
||||
function updateViewSwitchButtons(pageType, currentPath, searchParams) {
|
||||
// 获取视图切换按钮容器
|
||||
const viewSwitchContainer = document.querySelector('.flex.items-center.gap-px.flex-shrink-0.ml-auto');
|
||||
if (!viewSwitchContainer || !(pageType === 'filter' || pageType === 'grid')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const searchParamsStr = searchParams.toString() ? `?${searchParams.toString()}` : '';
|
||||
const pathStr = currentPath.includes('/articles/') ? currentPath.replace('/articles/', '') : '';
|
||||
|
||||
// 生成视图切换按钮HTML
|
||||
const switchButtonsHtml = `
|
||||
<a href="/filtered${searchParamsStr}"
|
||||
class="px-3 py-1.5 flex items-center gap-1 ${
|
||||
pageType === 'filter'
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400'
|
||||
}"
|
||||
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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
|
||||
</svg>
|
||||
<span class="hidden sm:inline text-xs">筛选</span>
|
||||
</a>
|
||||
<a href="${pathStr ? `/articles/${pathStr}/` : `/articles/`}"
|
||||
class="px-3 py-1.5 flex items-center gap-1 ${
|
||||
pageType === 'grid'
|
||||
? 'text-primary-600 dark:text-primary-400 font-medium'
|
||||
: 'text-gray-400 hover:text-gray-500 dark:text-gray-500 dark:hover:text-gray-400'
|
||||
}"
|
||||
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">
|
||||
<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>
|
||||
<span class="hidden sm:inline text-xs">网格</span>
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 更新视图切换按钮容器内容
|
||||
viewSwitchContainer.innerHTML = switchButtonsHtml;
|
||||
}
|
||||
|
||||
// 更新返回按钮
|
||||
function updateBackButton(currentPath, pathWithoutFile) {
|
||||
// 获取视图切换按钮容器的父元素
|
||||
const parentContainer = document.querySelector('.flex.items-center.justify-between.w-full.flex-wrap.sm\\:flex-nowrap');
|
||||
if (!parentContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查是否已有返回按钮容器
|
||||
let backButtonContainer = document.querySelector('.flex.items-center.shrink-0.ml-auto');
|
||||
|
||||
// 如果没有返回按钮容器,创建一个
|
||||
if (!backButtonContainer) {
|
||||
backButtonContainer = document.createElement('div');
|
||||
backButtonContainer.className = 'flex items-center shrink-0 ml-auto';
|
||||
parentContainer.appendChild(backButtonContainer);
|
||||
}
|
||||
|
||||
// 确保路径使用编码后的形式用于URL
|
||||
const encodedPath = pathWithoutFile.split('/').map(encodeURIComponent).join('/');
|
||||
|
||||
// 生成返回按钮HTML
|
||||
const backButtonHtml = `
|
||||
<a
|
||||
href="/articles/${encodedPath}/"
|
||||
class="text-secondary-500 dark:text-secondary-400 hover:text-primary-600 dark:hover:text-primary-400 flex items-center text-sm back-button"
|
||||
data-astro-prefetch="hover"
|
||||
data-path="/articles/${encodedPath}/"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4 mr-1"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M10 19l-7-7m0 0l7-7m-7 7h18"
|
||||
></path>
|
||||
</svg>
|
||||
返回文章列表
|
||||
</a>
|
||||
`;
|
||||
|
||||
// 更新返回按钮容器内容
|
||||
backButtonContainer.innerHTML = backButtonHtml;
|
||||
|
||||
// 设置返回按钮功能
|
||||
setupBackButton();
|
||||
}
|
||||
|
||||
// 添加路径变化检测和自动更新
|
||||
function setupPathChangeDetection() {
|
||||
let lastPathChecked = getCurrentPath();
|
||||
|
||||
// 统一的路径变化处理函数
|
||||
function handlePathChange() {
|
||||
const currentPath = getCurrentPath();
|
||||
if (currentPath !== lastPathChecked) {
|
||||
// 更新面包屑
|
||||
updateBreadcrumbs();
|
||||
|
||||
// 更新记录的路径
|
||||
lastPathChecked = currentPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||
addListener(window, 'hashchange', () => {
|
||||
handlePathChange();
|
||||
});
|
||||
|
||||
// 为所有导航链接添加点击拦截
|
||||
addListener(document, 'click', (e) => {
|
||||
// 检查点击的是否为站内导航链接
|
||||
const link = e.target.closest('a');
|
||||
if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
||||
// 延迟检查以确保导航已完成
|
||||
setTimeout(handlePathChange, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听history API的方法
|
||||
const originalPushState = window.history.pushState;
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
|
||||
// 重写pushState
|
||||
window.history.pushState = function() {
|
||||
originalPushState.apply(this, arguments);
|
||||
handlePathChange();
|
||||
};
|
||||
|
||||
// 重写replaceState
|
||||
window.history.replaceState = function() {
|
||||
originalReplaceState.apply(this, arguments);
|
||||
handlePathChange();
|
||||
};
|
||||
|
||||
// 添加到清理列表
|
||||
addListener(window, 'beforeunload', () => {
|
||||
// 恢复原始history方法
|
||||
window.history.pushState = originalPushState;
|
||||
window.history.replaceState = originalReplaceState;
|
||||
}, { once: true });
|
||||
|
||||
// 监听popstate事件
|
||||
addListener(window, 'popstate', () => {
|
||||
setTimeout(() => {
|
||||
handlePathChange();
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
// 注册清理事件,并保存引用
|
||||
function registerCleanupEvents() {
|
||||
// 创建一次性事件处理函数
|
||||
@ -275,25 +656,33 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
return;
|
||||
}
|
||||
|
||||
const clickHandler = (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
// 获取当前URL信息
|
||||
const url = new URL(window.location.href);
|
||||
const searchParams = url.search;
|
||||
|
||||
// 检查URL中是否有查询参数
|
||||
// 根据是否有查询参数确定返回目标
|
||||
let targetHref;
|
||||
if (searchParams) {
|
||||
// 有查询参数,返回筛选页面
|
||||
window.location.href = `/filtered${searchParams}`;
|
||||
targetHref = `/filtered${searchParams}`;
|
||||
} else {
|
||||
// 没有查询参数,返回默认路径
|
||||
const defaultPath = backButton.getAttribute('data-path') || '';
|
||||
window.location.href = defaultPath;
|
||||
targetHref = backButton.getAttribute('data-path') || '';
|
||||
}
|
||||
};
|
||||
|
||||
// 添加点击事件监听
|
||||
addListener(backButton, 'click', clickHandler);
|
||||
// 修改返回按钮属性,使其成为swup可识别的链接
|
||||
backButton.setAttribute('href', targetHref);
|
||||
|
||||
// 如果支持swup,确保swup能处理此链接
|
||||
if (typeof window.swup !== 'undefined') {
|
||||
// 移除可能阻止swup处理的属性
|
||||
backButton.removeAttribute('data-no-swup');
|
||||
|
||||
// 确保链接有正确的prefetch属性
|
||||
if (!backButton.hasAttribute('data-astro-prefetch')) {
|
||||
backButton.setAttribute('data-astro-prefetch', 'hover');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 主初始化函数
|
||||
@ -301,21 +690,22 @@ const breadcrumbs: Breadcrumb[] = pathSegments
|
||||
// 注册清理事件
|
||||
registerCleanupEvents();
|
||||
|
||||
// 设置路径变化检测
|
||||
setupPathChangeDetection();
|
||||
|
||||
// 执行初始更新
|
||||
updateBreadcrumbs();
|
||||
|
||||
// 设置返回按钮
|
||||
setupBackButton();
|
||||
|
||||
// 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时
|
||||
if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') {
|
||||
// 仅监听一个事件,保持最小侵入性
|
||||
// 监听页面加载事件
|
||||
const pageLoadHandler = () => {
|
||||
// 检查是否在文章页面
|
||||
const backButton = document.querySelector('.back-button');
|
||||
if (backButton) {
|
||||
// 先销毁已有的所有处理函数
|
||||
selfDestruct();
|
||||
// 重新初始化
|
||||
init();
|
||||
}
|
||||
// 重新执行一次更新
|
||||
updateBreadcrumbs();
|
||||
setupBackButton();
|
||||
};
|
||||
|
||||
if (typeof document.startViewTransition !== 'undefined') {
|
||||
|
@ -343,6 +343,11 @@ const navSelectorClassName = "mr-4";
|
||||
|
||||
// 单独保存清理事件的监听器引用
|
||||
const cleanupListeners = [];
|
||||
// 获取当前URL路径
|
||||
function getCurrentPath() {
|
||||
const path = window.location.pathname;
|
||||
return path === '/' ? '/' : path.endsWith('/') ? path.slice(0, -1) : path;
|
||||
}
|
||||
|
||||
// 添加事件监听器并记录,方便后续统一清理
|
||||
function addListener(element, eventType, handler, options) {
|
||||
@ -387,7 +392,6 @@ const navSelectorClassName = "mr-4";
|
||||
|
||||
// 注册清理事件,并保存引用
|
||||
function registerCleanupEvents() {
|
||||
|
||||
// 创建一次性事件处理函数
|
||||
const beforeSwapHandler = () => {
|
||||
selfDestruct();
|
||||
@ -629,6 +633,113 @@ const navSelectorClassName = "mr-4";
|
||||
// 注册清理事件
|
||||
registerCleanupEvents();
|
||||
|
||||
// 更新高亮背景 - 提升到全局作用域
|
||||
function updateHighlights(immediate = false) {
|
||||
requestAnimationFrame(() => {
|
||||
const highlightPositions = calculateHighlightPositions({
|
||||
navSelector,
|
||||
immediate
|
||||
});
|
||||
|
||||
if (highlightPositions) {
|
||||
highlightPositions.applyPositions(immediate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 初始化当前页面的激活状态 - 提升到全局作用域
|
||||
function initActiveState() {
|
||||
// 获取当前路径
|
||||
const currentPath = getCurrentPath();
|
||||
|
||||
// 先清除所有导航项的激活状态
|
||||
const allNavItems = document.querySelectorAll('.nav-item, .nav-subitem');
|
||||
allNavItems.forEach(item => {
|
||||
item.classList.remove('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
item.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
});
|
||||
|
||||
const allNavGroups = document.querySelectorAll('.nav-group');
|
||||
allNavGroups.forEach(group => {
|
||||
group.classList.remove('active');
|
||||
const toggle = group.querySelector('.nav-group-toggle');
|
||||
if (toggle) {
|
||||
toggle.classList.remove('menu-up', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
toggle.classList.add('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
}
|
||||
});
|
||||
|
||||
// 标记变量,用于跟踪是否找到匹配的导航项
|
||||
let foundMatch = false;
|
||||
|
||||
// 先检查子菜单项
|
||||
const subItems = document.querySelectorAll('.nav-subitem');
|
||||
for (const subItem of subItems) {
|
||||
const href = subItem.getAttribute('href');
|
||||
if (!href) continue;
|
||||
|
||||
// 检查是否匹配当前路径(完全匹配或前缀匹配,但排除根路径的前缀匹配)
|
||||
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
|
||||
// 找到匹配项,设置为active
|
||||
subItem.classList.add('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
subItem.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
|
||||
// 激活父组
|
||||
const parentId = subItem.dataset.parentId;
|
||||
if (parentId) {
|
||||
const parentGroup = document.querySelector(`.nav-group[data-group-id="${parentId}"]`);
|
||||
if (parentGroup) {
|
||||
parentGroup.classList.add('active');
|
||||
|
||||
// 设置组切换按钮样式
|
||||
const toggle = parentGroup.querySelector('.nav-group-toggle');
|
||||
if (toggle) {
|
||||
toggle.classList.add('menu-up', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
toggle.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
}
|
||||
|
||||
// 确保子菜单可见
|
||||
const items = parentGroup.querySelector('.nav-group-items');
|
||||
if (items) {
|
||||
items.classList.remove('hidden');
|
||||
items.classList.add('menu-visible');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没找到匹配的子菜单项,检查主菜单项
|
||||
if (!foundMatch) {
|
||||
const items = document.querySelectorAll('.nav-item');
|
||||
for (const item of items) {
|
||||
const href = item.getAttribute('href');
|
||||
if (!href) continue;
|
||||
|
||||
// 检查是否匹配当前路径(完全匹配或前缀匹配,但排除根路径的前缀匹配)
|
||||
if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
|
||||
// 找到匹配项,设置为active
|
||||
item.classList.add('active', 'font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
item.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
|
||||
foundMatch = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果仍然没找到匹配项,记录警告
|
||||
if (!foundMatch) {
|
||||
console.warn(`导航: 找不到与当前路径匹配的导航项: ${currentPath}`);
|
||||
}
|
||||
|
||||
// 计算正确高亮位置
|
||||
updateHighlights(true);
|
||||
}
|
||||
|
||||
// DOM加载完成后执行初始化
|
||||
function initNavigation() {
|
||||
// 检查DOM是否已加载
|
||||
@ -641,13 +752,64 @@ const navSelectorClassName = "mr-4";
|
||||
|
||||
// 主要设置函数
|
||||
function setupNavigation() {
|
||||
|
||||
// 设置桌面导航
|
||||
setupNavSelector();
|
||||
|
||||
// 设置移动端导航
|
||||
setupMobileNav();
|
||||
|
||||
// 记录最后一次路径值
|
||||
let lastPathLogged = getCurrentPath();
|
||||
|
||||
// 统一的路径变化处理函数
|
||||
function handlePathChange() {
|
||||
const currentPath = getCurrentPath();
|
||||
if (currentPath !== lastPathLogged) {
|
||||
// 主动调用初始化和更新高亮
|
||||
initActiveState();
|
||||
updateHighlights(true);
|
||||
|
||||
lastPathLogged = currentPath;
|
||||
}
|
||||
}
|
||||
|
||||
// 监听hashchange事件 - 当URL的hash部分改变时触发
|
||||
addListener(window, 'hashchange', () => {
|
||||
handlePathChange();
|
||||
});
|
||||
|
||||
// 为所有导航链接添加点击拦截
|
||||
addListener(document, 'click', (e) => {
|
||||
// 检查点击的是否为站内导航链接
|
||||
const link = e.target.closest('a');
|
||||
if (link && link.host === window.location.host && !e.ctrlKey && !e.metaKey) {
|
||||
// 延迟检查以确保导航已完成
|
||||
setTimeout(handlePathChange, 50);
|
||||
}
|
||||
});
|
||||
|
||||
// 监听history API的方法
|
||||
const originalPushState = window.history.pushState;
|
||||
const originalReplaceState = window.history.replaceState;
|
||||
|
||||
// 重写pushState
|
||||
window.history.pushState = function() {
|
||||
originalPushState.apply(this, arguments);
|
||||
handlePathChange();
|
||||
};
|
||||
|
||||
// 重写replaceState
|
||||
window.history.replaceState = function() {
|
||||
originalReplaceState.apply(this, arguments);
|
||||
handlePathChange();
|
||||
};
|
||||
|
||||
// 添加到清理列表
|
||||
addListener(window, 'beforeunload', () => {
|
||||
// 恢复原始history方法
|
||||
window.history.pushState = originalPushState;
|
||||
window.history.replaceState = originalReplaceState;
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// 初始化导航选择器
|
||||
@ -666,8 +828,6 @@ const navSelectorClassName = "mr-4";
|
||||
console.warn(`导航脚本未找到高亮元素,导航功能可能不完整`);
|
||||
}
|
||||
|
||||
// 获取过渡动画持续时间
|
||||
const transitionDuration = parseInt(navSelector.dataset.duration || 300);
|
||||
const activeClass = "font-medium";
|
||||
|
||||
// 当前活动项的状态 - 从DOM中提取当前激活状态
|
||||
@ -730,63 +890,6 @@ const navSelectorClassName = "mr-4";
|
||||
});
|
||||
}
|
||||
|
||||
// 兼容swup的导航方法
|
||||
function navigateTo(href) {
|
||||
// 如果使用swup
|
||||
const hasSwup = typeof window.swup !== 'undefined';
|
||||
|
||||
if (hasSwup) {
|
||||
try {
|
||||
// 正确使用swup的API
|
||||
window.swup.navigate(href);
|
||||
return;
|
||||
} catch (err) {
|
||||
// 尝试使用swup的替代方法
|
||||
try {
|
||||
window.swup.loadPage({
|
||||
url: href
|
||||
});
|
||||
return;
|
||||
} catch (err2) {
|
||||
console.warn(`导航脚本Swup导航失败:`, err2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有匹配的链接,使用其点击事件可能会触发注册在链接上的swup事件
|
||||
const existingLink = document.querySelector(`a[href="${href}"]`);
|
||||
if (existingLink) {
|
||||
existingLink.click();
|
||||
return;
|
||||
}
|
||||
|
||||
// 以下是在swup不可用或出错时的回退方案
|
||||
|
||||
// 如果使用Astro的View Transitions
|
||||
if (document.startViewTransition) {
|
||||
document.startViewTransition(() => {
|
||||
window.location.href = href;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// 最后的回退:普通导航
|
||||
window.location.href = href;
|
||||
}
|
||||
|
||||
// 更新高亮背景
|
||||
function updateHighlights(immediate = false) {
|
||||
requestAnimationFrame(() => {
|
||||
const highlightPositions = calculateHighlightPositions({
|
||||
navSelector,
|
||||
immediate
|
||||
});
|
||||
|
||||
if (highlightPositions) {
|
||||
highlightPositions.applyPositions(immediate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 设置激活的一级菜单项
|
||||
function setActiveItem(itemId) {
|
||||
@ -1036,12 +1139,11 @@ const navSelectorClassName = "mr-4";
|
||||
// 设置激活状态
|
||||
activeGroupId = groupId;
|
||||
|
||||
// 如果没有选中的二级菜单项,则选中第一个并导航
|
||||
// 如果没有选中的二级菜单项,则选中第一个并模拟点击
|
||||
if (!activeSubItemId || !activeSubItemId.startsWith(groupId)) {
|
||||
const firstSubItem = targetGroup.querySelector('.nav-subitem');
|
||||
if (firstSubItem) {
|
||||
const subItemId = firstSubItem.dataset.subitemId;
|
||||
const href = firstSubItem.getAttribute('href');
|
||||
|
||||
// 清除所有二级菜单项的高亮
|
||||
navSubItems.forEach(item => {
|
||||
@ -1058,10 +1160,10 @@ const navSelectorClassName = "mr-4";
|
||||
// 设置高亮
|
||||
activeSubItemId = subItemId;
|
||||
|
||||
// 导航到第一个子项
|
||||
if (href) {
|
||||
navigateTo(href);
|
||||
}
|
||||
// 直接模拟点击第一个子菜单项
|
||||
setTimeout(() => {
|
||||
firstSubItem.click();
|
||||
}, 10);
|
||||
}
|
||||
} else {
|
||||
// 已有选中的二级菜单项,找到并重新设置高亮
|
||||
@ -1086,38 +1188,6 @@ const navSelectorClassName = "mr-4";
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化当前页面的激活状态
|
||||
function initActiveState() {
|
||||
// 高亮背景已经在服务器端渲染时预设,现在需确保应用正确的文字颜色
|
||||
|
||||
// 查找当前活动项
|
||||
const activeItemElement = document.querySelector('.nav-item.active');
|
||||
const activeSubItemElement = document.querySelector('.nav-subitem.active');
|
||||
const activeGroupElement = document.querySelector('.nav-group.active');
|
||||
|
||||
// 应用激活文字样式
|
||||
if (activeItemElement) {
|
||||
activeItemElement.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
activeItemElement.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
}
|
||||
|
||||
if (activeSubItemElement) {
|
||||
activeSubItemElement.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
activeSubItemElement.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
}
|
||||
|
||||
if (activeGroupElement) {
|
||||
const toggle = activeGroupElement.querySelector('.nav-group-toggle');
|
||||
if (toggle) {
|
||||
toggle.classList.add('font-semibold', 'text-primary-700', 'dark:text-primary-300');
|
||||
toggle.classList.remove('text-gray-600', 'dark:text-gray-300', 'hover:text-primary-600', 'dark:hover:text-primary-400');
|
||||
}
|
||||
}
|
||||
|
||||
// 计算正确高亮位置
|
||||
updateHighlights(true);
|
||||
}
|
||||
|
||||
// 注册事件监听
|
||||
|
||||
// 普通菜单项点击
|
||||
@ -1126,15 +1196,11 @@ const navSelectorClassName = "mr-4";
|
||||
e.preventDefault();
|
||||
|
||||
const itemId = item.dataset.itemId;
|
||||
const href = item.getAttribute('href');
|
||||
|
||||
// 设置高亮
|
||||
setActiveItem(itemId);
|
||||
|
||||
// 导航到目标页面
|
||||
if (href) {
|
||||
navigateTo(href);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -1157,15 +1223,11 @@ const navSelectorClassName = "mr-4";
|
||||
e.preventDefault();
|
||||
|
||||
const subItemId = item.dataset.subitemId;
|
||||
const href = item.getAttribute('href');
|
||||
|
||||
// 设置高亮
|
||||
setActiveSubItem(subItemId);
|
||||
|
||||
// 导航到目标页面
|
||||
if (href) {
|
||||
navigateTo(href);
|
||||
}
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@ -1177,12 +1239,9 @@ const navSelectorClassName = "mr-4";
|
||||
if (hasSwup) {
|
||||
// 页面内容替换后重新初始化
|
||||
addListener(document, 'swup:contentReplaced', () => {
|
||||
setTimeout(initActiveState, 50);
|
||||
});
|
||||
|
||||
// 视图转换开始
|
||||
addListener(document, 'swup:animationInStart', () => {
|
||||
// 这里可以添加动画开始时的处理逻辑
|
||||
setTimeout(() => {
|
||||
initActiveState();
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 视图转换结束
|
||||
@ -1194,23 +1253,33 @@ const navSelectorClassName = "mr-4";
|
||||
});
|
||||
}
|
||||
|
||||
// 添加 astro:after-swap 事件监听
|
||||
addListener(document, 'astro:after-swap', () => {
|
||||
setTimeout(() => {
|
||||
initActiveState();
|
||||
updateHighlights(true);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 添加Astro View Transitions事件监听
|
||||
addListener(document, 'astro:page-load', () => {
|
||||
setTimeout(initActiveState, 50);
|
||||
updateHighlights(true);
|
||||
});
|
||||
|
||||
// 添加popstate事件监听
|
||||
addListener(window, 'popstate', () => {
|
||||
setTimeout(() => {
|
||||
initActiveState();
|
||||
updateHighlights(true);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
|
||||
// 窗口大小变化时重新计算高亮位置
|
||||
addListener(window, 'resize', () => {
|
||||
updateHighlights(true);
|
||||
});
|
||||
|
||||
// 在document上添加自定义方法,方便外部调用,但使用弱引用避免内存泄漏
|
||||
if (!document.resetNavigation) {
|
||||
document.resetNavigation = function() {
|
||||
initActiveState();
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化移动端菜单和搜索功能
|
||||
@ -1460,12 +1529,6 @@ const navSelectorClassName = "mr-4";
|
||||
setupMobileSubmenuToggles();
|
||||
updateMobileMenuHighlight();
|
||||
|
||||
// 在document上添加自定义方法,方便外部调用(可选)
|
||||
document.updateMobileMenuHighlight = updateMobileMenuHighlight;
|
||||
document.closeMobileMenu = closeMobileMenu;
|
||||
document.closeMobileSearch = closeMobileSearch;
|
||||
document.toggleMobileSubmenu = toggleSubmenu;
|
||||
|
||||
return updateMobileMenuHighlight;
|
||||
}
|
||||
|
||||
|
@ -92,51 +92,99 @@ const {
|
||||
]}
|
||||
/>
|
||||
|
||||
<!-- 主题切换脚本 -->
|
||||
<!-- 主题切换脚本 - 升级为自销毁模式 -->
|
||||
<script is:inline>
|
||||
// 立即执行主题初始化,采用"无闪烁"加载方式
|
||||
(function () {
|
||||
// 存储事件监听器,便于清理
|
||||
const listeners = [];
|
||||
// 集中管理所有事件监听器
|
||||
const allListeners = [];
|
||||
|
||||
// 单独保存清理事件的监听器引用
|
||||
const cleanupListeners = [];
|
||||
|
||||
// 定时器引用
|
||||
let themeUpdateTimeout = null;
|
||||
|
||||
// 添加事件监听器并记录,方便后续统一清理
|
||||
function addListener(element, eventType, handler, options) {
|
||||
if (!element) return null;
|
||||
if (!element) {
|
||||
console.error(`尝试为不存在的元素添加事件:`, eventType);
|
||||
return null;
|
||||
}
|
||||
|
||||
element.addEventListener(eventType, handler, options);
|
||||
listeners.push({ element, eventType, handler, options });
|
||||
allListeners.push({ element, eventType, handler, options });
|
||||
return handler;
|
||||
}
|
||||
|
||||
// 清理函数 - 移除所有事件监听器
|
||||
function cleanup() {
|
||||
listeners.forEach(({ element, eventType, handler, options }) => {
|
||||
// 统一的清理函数,执行完整清理并自销毁
|
||||
function selfDestruct() {
|
||||
// 1. 清理所有计时器
|
||||
if (themeUpdateTimeout) {
|
||||
clearTimeout(themeUpdateTimeout);
|
||||
themeUpdateTimeout = null;
|
||||
}
|
||||
|
||||
// 2. 移除普通事件监听器
|
||||
allListeners.forEach(({ element, eventType, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
} catch (err) {
|
||||
// 忽略错误
|
||||
console.error(`主题初始化移除事件监听器出错:`, err);
|
||||
}
|
||||
});
|
||||
|
||||
// 清空数组
|
||||
listeners.length = 0;
|
||||
// 清空监听器数组
|
||||
allListeners.length = 0;
|
||||
|
||||
// 3. 最后移除清理事件监听器自身
|
||||
cleanupListeners.forEach(({ element, eventType, handler, options }) => {
|
||||
try {
|
||||
element.removeEventListener(eventType, handler, options);
|
||||
} catch (err) {
|
||||
console.error(`主题初始化移除清理监听器出错:`, err);
|
||||
}
|
||||
|
||||
// 注册清理函数 - 确保在页面转换前清理事件
|
||||
function registerCleanup() {
|
||||
const cleanupEvents = [
|
||||
"astro:before-preparation",
|
||||
"astro:before-swap",
|
||||
"swup:willReplaceContent"
|
||||
];
|
||||
|
||||
// 为每个事件注册一次性清理函数
|
||||
cleanupEvents.forEach((eventName) => {
|
||||
document.addEventListener(eventName, cleanup, { once: true });
|
||||
});
|
||||
|
||||
// 页面卸载时清理
|
||||
window.addEventListener("beforeunload", cleanup, { once: true });
|
||||
// 清空清理监听器数组
|
||||
cleanupListeners.length = 0;
|
||||
}
|
||||
|
||||
// 注册清理事件,并保存引用
|
||||
function registerCleanupEvents() {
|
||||
// 创建一次性事件处理函数
|
||||
const beforeSwapHandler = () => {
|
||||
selfDestruct();
|
||||
};
|
||||
|
||||
const beforeUnloadHandler = () => {
|
||||
selfDestruct();
|
||||
};
|
||||
|
||||
// 添加清理事件监听器并保存引用
|
||||
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
|
||||
window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
|
||||
|
||||
// Astro特有的页面准备事件
|
||||
document.addEventListener("astro:before-preparation", beforeSwapHandler, { once: true });
|
||||
|
||||
// SPA框架可能使用的事件
|
||||
if (typeof document.addEventListener === 'function') {
|
||||
document.addEventListener("swup:willReplaceContent", beforeSwapHandler, { once: true });
|
||||
}
|
||||
|
||||
// 保存清理事件引用,用于完全销毁
|
||||
cleanupListeners.push(
|
||||
{ element: document, eventType: "astro:before-swap", handler: beforeSwapHandler, options: { once: true } },
|
||||
{ element: window, eventType: "beforeunload", handler: beforeUnloadHandler, options: { once: true } },
|
||||
{ element: document, eventType: "astro:before-preparation", handler: beforeSwapHandler, options: { once: true } }
|
||||
);
|
||||
|
||||
if (typeof document.addEventListener === 'function') {
|
||||
cleanupListeners.push(
|
||||
{ element: document, eventType: "swup:willReplaceContent", handler: beforeSwapHandler, options: { once: true } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
@ -181,29 +229,55 @@ const {
|
||||
addListener(mediaQuery, "change", handleMediaChange);
|
||||
|
||||
// 注册清理函数
|
||||
registerCleanup();
|
||||
registerCleanupEvents();
|
||||
|
||||
// 监听页面转换事件,确保在页面转换后重新初始化
|
||||
function onPageTransition() {
|
||||
// 防止重复执行,使用防抖
|
||||
if (themeUpdateTimeout) {
|
||||
clearTimeout(themeUpdateTimeout);
|
||||
}
|
||||
|
||||
// 使用微小延迟确保DOM完全就绪
|
||||
themeUpdateTimeout = setTimeout(() => {
|
||||
try {
|
||||
// 重新初始化主题
|
||||
const storedTheme = localStorage.getItem("theme");
|
||||
if (storedTheme) {
|
||||
document.documentElement.dataset.theme = storedTheme;
|
||||
} else {
|
||||
const systemTheme = getSystemTheme();
|
||||
document.documentElement.dataset.theme = systemTheme;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("页面转换后主题更新出错:", err);
|
||||
} finally {
|
||||
themeUpdateTimeout = null;
|
||||
}
|
||||
}, 10);
|
||||
}
|
||||
|
||||
// 设置页面转换事件监听
|
||||
document.addEventListener("astro:page-load", onPageTransition);
|
||||
document.addEventListener("astro:after-swap", onPageTransition);
|
||||
addListener(document, "astro:page-load", onPageTransition);
|
||||
addListener(document, "astro:after-swap", onPageTransition);
|
||||
|
||||
} catch (error) {
|
||||
console.error("主题初始化失败:", error);
|
||||
// 出错时应用默认浅色主题,确保页面正常显示
|
||||
document.documentElement.dataset.theme = "light";
|
||||
// 即使出错也尝试注册清理事件
|
||||
try {
|
||||
registerCleanupEvents();
|
||||
} catch (e) {
|
||||
// 忽略清理注册错误
|
||||
}
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import '../scripts/swup-init.js';
|
||||
</script>
|
||||
</head>
|
||||
<body
|
||||
class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg flex flex-col min-h-screen"
|
||||
@ -219,37 +293,5 @@ const {
|
||||
psbIcpUrl={PSB_ICP_URL}
|
||||
/>
|
||||
|
||||
<!-- 预获取脚本 -->
|
||||
<script is:inline>
|
||||
// 在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>
|
||||
</html>
|
||||
|
@ -291,9 +291,9 @@ const tableOfContents = generateTableOfContents(headings);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章头部 -->
|
||||
<!-- 文章头部 - 包含面包屑,保持不变 -->
|
||||
<header class="mb-8">
|
||||
<!-- 导航区域 -->
|
||||
<!-- 导航区域/面包屑 - 不参与视图切换 -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl p-4 mb-6 shadow-lg border border-gray-200 dark:border-gray-700 relative z-10"
|
||||
>
|
||||
@ -312,6 +312,11 @@ const tableOfContents = generateTableOfContents(headings);
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文章过期提醒 - 放入article-content容器内 -->
|
||||
</header>
|
||||
|
||||
<!-- 文章内容区域 - 只有这部分参与视图切换 -->
|
||||
<div id="article-content">
|
||||
<!-- 文章过期提醒 -->
|
||||
{
|
||||
(() => {
|
||||
@ -426,52 +431,13 @@ const tableOfContents = generateTableOfContents(headings);
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</header>
|
||||
|
||||
<!-- 文章内容 -->
|
||||
<article
|
||||
class="prose prose-lg dark:prose-invert prose-primary prose-table:rounded-lg prose-table:border-separate prose-table:border-2 prose-thead:bg-primary-50 dark:prose-thead:bg-gray-800 prose-ul:list-disc prose-ol:list-decimal prose-li:my-1 prose-blockquote:border-l-4 prose-blockquote:border-primary-500 prose-blockquote:bg-gray-100 prose-blockquote:dark:bg-gray-800 prose-a:text-primary-600 prose-a:dark:text-primary-400 prose-a:no-underline prose-a:border-b prose-a:border-primary-300 prose-a:hover:border-primary-600 max-w-none mb-12"
|
||||
>
|
||||
<Content />
|
||||
</article>
|
||||
|
||||
<!-- 目录 -->
|
||||
<section
|
||||
class="hidden 2xl:block"
|
||||
id="toc-panel"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="panel-header"
|
||||
>
|
||||
<h3>
|
||||
<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 6h16M4 12h16M4 18h7"
|
||||
></path>
|
||||
</svg>
|
||||
文章目录
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="toc-content"
|
||||
class="scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent"
|
||||
set:html={tableOfContents}
|
||||
>
|
||||
<!-- 目录内容在服务端生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- 相关文章 -->
|
||||
{
|
||||
relatedArticles.length > 0 && (
|
||||
@ -562,6 +528,44 @@ const tableOfContents = generateTableOfContents(headings);
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 目录 -->
|
||||
<section
|
||||
class="hidden 2xl:block"
|
||||
id="toc-panel"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="panel-header"
|
||||
>
|
||||
<h3>
|
||||
<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 6h16M4 12h16M4 18h7"
|
||||
></path>
|
||||
</svg>
|
||||
文章目录
|
||||
</h3>
|
||||
</div>
|
||||
<div
|
||||
id="toc-content"
|
||||
class="scrollbar-thin scrollbar-thumb-primary-200 dark:scrollbar-thumb-primary-800 scrollbar-track-transparent"
|
||||
set:html={tableOfContents}
|
||||
>
|
||||
<!-- 目录内容在服务端生成 -->
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<!-- 文章页面脚本 -->
|
||||
<script is:inline>
|
||||
// 文章页面交互脚本 - 自销毁模式
|
||||
|
@ -74,7 +74,7 @@ const pageTitle = currentPath ? currentPath : '文章列表';
|
||||
<slot name="head" slot="head" />
|
||||
|
||||
<div class="py-6 w-full">
|
||||
<!-- 导航栏 -->
|
||||
<!-- 导航栏/面包屑 - 保持不变 -->
|
||||
<div class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700">
|
||||
<div class="px-4 py-3">
|
||||
<Breadcrumb
|
||||
@ -85,11 +85,12 @@ const pageTitle = currentPath ? currentPath : '文章列表';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 内容卡片网格 -->
|
||||
<!-- 内容区域 - 只有这部分参与视图切换 -->
|
||||
<div id="article-content">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12">
|
||||
{/* 上一级目录卡片 - 仅在浏览目录时显示 */}
|
||||
{pathSegments.length > 0 && (
|
||||
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}/`}
|
||||
<a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}`}
|
||||
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">
|
||||
@ -230,4 +231,5 @@ const pageTitle = currentPath ? currentPath : '文章列表';
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
@ -9,7 +9,7 @@ const searchParams = Astro.url.searchParams;
|
||||
|
||||
<Layout title="文章筛选">
|
||||
<div class="w-full">
|
||||
<!-- 导航栏 -->
|
||||
<!-- 导航栏/面包屑 - 不参与视图切换 -->
|
||||
<div
|
||||
class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700"
|
||||
>
|
||||
@ -21,11 +21,13 @@ const searchParams = Astro.url.searchParams;
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 使用ArticleFilter组件 -->
|
||||
<!-- 内容区域 - 只有这部分参与视图切换 -->
|
||||
<div id="article-content">
|
||||
<ArticleFilter
|
||||
searchParams={searchParams}
|
||||
client:load
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Layout>
|
||||
|
||||
|
@ -22,10 +22,16 @@ export function rehypeCodeBlocks() {
|
||||
className => typeof className === 'string' && className.startsWith('language-')
|
||||
);
|
||||
|
||||
// 提取语言标识
|
||||
// 从父节点获取 Shiki 设置的语言标识(dataLanguage 属性)
|
||||
let shikiLanguage = '';
|
||||
if (node.properties && node.properties.dataLanguage) {
|
||||
shikiLanguage = node.properties.dataLanguage;
|
||||
}
|
||||
|
||||
// 提取语言标识 - 优先使用 language 类,其次使用 Shiki 语言标识
|
||||
const language = languageClass
|
||||
? languageClass.split('-')[1].toUpperCase()
|
||||
: 'TEXT';
|
||||
: (shikiLanguage ? shikiLanguage.toUpperCase() : 'TEXT');
|
||||
|
||||
// 跳过处理 mermaid 图表
|
||||
if (language === 'MERMAID') {
|
||||
|
325
src/scripts/swup-init.js
Normal file
325
src/scripts/swup-init.js
Normal file
@ -0,0 +1,325 @@
|
||||
// 统一初始化Swup和所有插件
|
||||
import Swup from 'swup';
|
||||
import SwupFragmentPlugin from '@swup/fragment-plugin';
|
||||
// 添加Head插件解决CSS丢失问题
|
||||
import SwupHeadPlugin from '@swup/head-plugin';
|
||||
// 添加预加载插件 - 优化导航体验
|
||||
import SwupPreloadPlugin from '@swup/preload-plugin';
|
||||
|
||||
// 检查是否是文章相关页面
|
||||
function isArticlePage() {
|
||||
const path = window.location.pathname;
|
||||
return path.includes('/articles') || path.includes('/filtered');
|
||||
}
|
||||
|
||||
// 为元素应用动画样式
|
||||
function applyAnimationStyles(element, className, duration = 300) {
|
||||
if (!element) return;
|
||||
|
||||
// 添加动画类
|
||||
element.classList.add(className);
|
||||
|
||||
// 设置过渡属性
|
||||
element.style.transition = 'opacity 0.3s ease';
|
||||
element.style.animationDuration = '0.3s';
|
||||
element.style.opacity = '1';
|
||||
|
||||
// 添加data-swup属性标记
|
||||
element.setAttribute('data-swup-transition', 'true');
|
||||
element.setAttribute('data-swup-animation-duration', duration.toString());
|
||||
}
|
||||
|
||||
// 设置元素淡入/淡出效果
|
||||
function setElementOpacity(element, opacity) {
|
||||
if (!element) return;
|
||||
element.style.opacity = opacity.toString();
|
||||
if (opacity === 0) {
|
||||
element.style.transition = 'opacity 0.3s ease';
|
||||
}
|
||||
}
|
||||
|
||||
// 直接应用样式到元素上
|
||||
function applyStylesDirectly() {
|
||||
// 应用到主容器 - 只在非文章页面
|
||||
const mainElement = document.querySelector('main');
|
||||
if (mainElement) {
|
||||
mainElement.classList.add('transition-fade');
|
||||
|
||||
// 只有在非文章页面时,才为main添加必要的动画样式
|
||||
if (!isArticlePage()) {
|
||||
applyAnimationStyles(mainElement, 'transition-fade');
|
||||
}
|
||||
}
|
||||
|
||||
// 应用到文章内容 - 只在文章页面
|
||||
const articleContent = document.querySelector('#article-content');
|
||||
if (articleContent) {
|
||||
applyAnimationStyles(articleContent, 'swup-transition-article');
|
||||
}
|
||||
}
|
||||
|
||||
// 获取当前页面的活跃元素(用于动画)
|
||||
function getActiveElement() {
|
||||
if (isArticlePage()) {
|
||||
return document.querySelector('#article-content');
|
||||
} else {
|
||||
return document.querySelector('main');
|
||||
}
|
||||
}
|
||||
|
||||
// 在DOM加载完成后初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// 直接应用样式
|
||||
applyStylesDirectly();
|
||||
|
||||
// 创建Swup实例
|
||||
const swup = new Swup({
|
||||
// Swup的基本配置
|
||||
animationSelector: '[class*="transition-"], #article-content, .swup-transition-article, main',
|
||||
cache: true,
|
||||
containers: ['main'],
|
||||
animationScope: 'html', // 确保动画状态类添加到html元素
|
||||
linkSelector: 'a[href^="/"]:not([data-no-swup]), a[href^="' + window.location.origin + '"]:not([data-no-swup])',
|
||||
skipPopStateHandling: (event) => {
|
||||
return event.state && event.state.source === 'swup';
|
||||
},
|
||||
plugins: [] // 手动添加插件以控制顺序
|
||||
});
|
||||
|
||||
// 添加预加载插件 - 代替原有的预加载功能
|
||||
const preloadPlugin = new SwupPreloadPlugin({
|
||||
// 最多同时预加载5个链接
|
||||
throttle: 5,
|
||||
// 开启鼠标悬停预加载
|
||||
preloadHoveredLinks: true,
|
||||
// 开启视口内链接预加载,自定义配置
|
||||
preloadVisibleLinks: {
|
||||
// 链接可见面积达到30%时预加载
|
||||
threshold: 0.3,
|
||||
// 链接可见500毫秒后开始预加载
|
||||
delay: 500,
|
||||
// 在哪些容器内寻找链接
|
||||
containers: ['body'],
|
||||
// 忽略带有data-no-preload属性的链接
|
||||
ignore: (el) => el.hasAttribute('data-no-preload')
|
||||
},
|
||||
// 预加载初始页面,以便"后退"导航更快
|
||||
preloadInitialPage: true
|
||||
});
|
||||
swup.use(preloadPlugin);
|
||||
|
||||
// 创建并注册Head插件,用于解决CSS丢失问题
|
||||
const headPlugin = new SwupHeadPlugin();
|
||||
swup.use(headPlugin);
|
||||
|
||||
// 创建Fragment插件 - 简化规则避免匹配问题
|
||||
const fragmentPlugin = new SwupFragmentPlugin({
|
||||
debug: false, // 关闭调试模式
|
||||
// 简化规则,确保基本匹配
|
||||
rules: [
|
||||
{
|
||||
// 文章页面之间的导航
|
||||
name: 'article-pages',
|
||||
from: '/articles', // 简化匹配规则
|
||||
to: '/articles',
|
||||
containers: ['#article-content']
|
||||
},
|
||||
{
|
||||
// 从文章到筛选页面
|
||||
name: 'article-to-filter',
|
||||
from: '/articles',
|
||||
to: '/filtered',
|
||||
containers: ['#article-content']
|
||||
},
|
||||
{
|
||||
// 从筛选到文章页面
|
||||
name: 'filter-to-article',
|
||||
from: '/filtered',
|
||||
to: '/articles',
|
||||
containers: ['#article-content']
|
||||
},
|
||||
{
|
||||
// 筛选页面内部导航
|
||||
name: 'filter-pages',
|
||||
from: '/filtered',
|
||||
to: '/filtered',
|
||||
containers: ['#article-content']
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
// 添加Fragment插件到Swup
|
||||
swup.use(fragmentPlugin);
|
||||
|
||||
// 初始化后手动扫描并预加载带有data-swup-preload属性的链接
|
||||
setTimeout(() => {
|
||||
swup.preloadLinks();
|
||||
}, 1000);
|
||||
|
||||
// 强制应用动画样式到特定元素
|
||||
function setupTransition() {
|
||||
// 直接应用样式 - 会根据页面类型自动选择正确的元素
|
||||
applyStylesDirectly();
|
||||
|
||||
// 确保初始状态正确
|
||||
setTimeout(() => {
|
||||
// 获取并设置当前活跃元素的不透明度
|
||||
const activeElement = getActiveElement();
|
||||
if (activeElement) {
|
||||
activeElement.style.opacity = '1';
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// 初始化时设置
|
||||
setupTransition();
|
||||
|
||||
// 在页面内容加载后重新应用样式
|
||||
swup.hooks.on('content:replace', () => {
|
||||
// 重新设置过渡样式
|
||||
setTimeout(() => {
|
||||
setupTransition();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// 监听动画开始和结束
|
||||
swup.hooks.on('animation:out:start', () => {
|
||||
// 获取并淡出当前活跃元素
|
||||
const activeElement = getActiveElement();
|
||||
setElementOpacity(activeElement, 0);
|
||||
});
|
||||
|
||||
swup.hooks.on('animation:in:start', () => {
|
||||
// 等待短暂延迟后恢复可见度
|
||||
setTimeout(() => {
|
||||
// 获取并淡入当前活跃元素
|
||||
const activeElement = getActiveElement();
|
||||
setElementOpacity(activeElement, 1);
|
||||
}, 50); // 短暂延迟确保可以看到效果
|
||||
});
|
||||
|
||||
// 添加手动强制动画事件
|
||||
document.addEventListener('swup:willReplaceContent', () => {
|
||||
// 获取并淡出当前活跃元素
|
||||
const activeElement = getActiveElement();
|
||||
setElementOpacity(activeElement, 0);
|
||||
});
|
||||
|
||||
// 在页面内容替换后强制应用动画
|
||||
document.addEventListener('swup:contentReplaced', () => {
|
||||
// 获取活跃元素
|
||||
const activeElement = getActiveElement();
|
||||
if (!activeElement) return;
|
||||
|
||||
// 先设置透明
|
||||
setElementOpacity(activeElement, 0);
|
||||
|
||||
// 重新应用适当的类和属性
|
||||
if (isArticlePage() && activeElement.id === 'article-content') {
|
||||
applyAnimationStyles(activeElement, 'swup-transition-article');
|
||||
} else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') {
|
||||
applyAnimationStyles(activeElement, 'transition-fade');
|
||||
}
|
||||
|
||||
// 延迟后淡入
|
||||
setTimeout(() => {
|
||||
setElementOpacity(activeElement, 1);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 监听URL变化以更新动画行为
|
||||
swup.hooks.on('visit:start', (visit) => {
|
||||
// 检查目标URL是否为文章相关页面
|
||||
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
|
||||
const isCurrentArticlePage = isArticlePage();
|
||||
|
||||
// 如果当前是文章页面,但目标不是,恢复main动画
|
||||
if (isCurrentArticlePage && !isTargetArticlePage) {
|
||||
const mainElement = document.querySelector('main');
|
||||
if (mainElement) {
|
||||
setElementOpacity(mainElement, 0);
|
||||
}
|
||||
}
|
||||
// 如果当前不是文章页面,但目标是,准备article-content动画
|
||||
else if (!isCurrentArticlePage && isTargetArticlePage) {
|
||||
const mainElement = document.querySelector('main');
|
||||
if (mainElement) {
|
||||
// 移除main的动画效果
|
||||
mainElement.style.transition = '';
|
||||
mainElement.style.opacity = '1';
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Fragment导航后手动更新面包屑
|
||||
function updateBreadcrumb(url) {
|
||||
// 1. 获取新页面的HTML以提取面包屑
|
||||
fetch(url)
|
||||
.then(response => response.text())
|
||||
.then(html => {
|
||||
// 创建一个临时的DOM解析新页面
|
||||
const parser = new DOMParser();
|
||||
const newDoc = parser.parseFromString(html, 'text/html');
|
||||
|
||||
// 获取新页面的面包屑容器 - 使用更精确的选择器
|
||||
const newBreadcrumbContainer = newDoc.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
||||
|
||||
// 获取当前页面的面包屑容器
|
||||
const currentBreadcrumbContainer = document.querySelector('.bg-white.dark\\:bg-gray-800.rounded-xl.mb-4, .bg-white.dark\\:bg-gray-800.rounded-xl.p-4');
|
||||
|
||||
if (newBreadcrumbContainer && currentBreadcrumbContainer) {
|
||||
// 更新面包屑内容
|
||||
currentBreadcrumbContainer.innerHTML = newBreadcrumbContainer.innerHTML;
|
||||
|
||||
// 重新初始化面包屑相关脚本
|
||||
const breadcrumbScript = currentBreadcrumbContainer.querySelector('script');
|
||||
if (breadcrumbScript) {
|
||||
const newScript = document.createElement('script');
|
||||
newScript.textContent = breadcrumbScript.textContent;
|
||||
breadcrumbScript.parentNode.replaceChild(newScript, breadcrumbScript);
|
||||
}
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
// 出错时静默处理
|
||||
});
|
||||
}
|
||||
|
||||
// 在每次页面转换结束后更新面包屑
|
||||
swup.hooks.on('visit:end', (visit) => {
|
||||
// 所有导航都更新面包屑
|
||||
updateBreadcrumb(visit.to.url);
|
||||
|
||||
// 确保在页面加载完成后元素有正确样式
|
||||
setTimeout(() => {
|
||||
setupTransition();
|
||||
|
||||
// 加载完成后重新扫描预加载链接
|
||||
setTimeout(() => {
|
||||
swup.preloadLinks();
|
||||
}, 500);
|
||||
}, 50);
|
||||
});
|
||||
|
||||
// 监听Fragment插件是否成功应用
|
||||
document.addEventListener('swup:fragmentReplaced', () => {
|
||||
// 确保新内容有正确的过渡样式
|
||||
setTimeout(() => {
|
||||
setupTransition();
|
||||
}, 10);
|
||||
});
|
||||
|
||||
// 在页面卸载和Astro视图转换时清理资源
|
||||
const cleanup = () => {
|
||||
if (swup) {
|
||||
swup.unuse(fragmentPlugin);
|
||||
swup.unuse(headPlugin);
|
||||
swup.unuse(preloadPlugin);
|
||||
swup.destroy();
|
||||
}
|
||||
};
|
||||
|
||||
// 注册清理事件
|
||||
window.addEventListener('beforeunload', cleanup, { once: true });
|
||||
document.addEventListener('astro:before-swap', cleanup, { once: true });
|
||||
});
|
Loading…
Reference in New Issue
Block a user