优化swup视图,高亮菜单,代码块名称

This commit is contained in:
lsy 2025-05-12 22:13:30 +08:00
parent fc7653011b
commit 2c71dcdbd9
11 changed files with 1309 additions and 6289 deletions

View File

@ -8,7 +8,6 @@ import rehypeExternalLinks from "rehype-external-links";
import sitemap from "@astrojs/sitemap"; import sitemap from "@astrojs/sitemap";
import fs from "node:fs"; import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import swup from "@swup/astro"
import { SITE_URL } from "./src/consts"; import { SITE_URL } from "./src/consts";
import compressor from "astro-compressor"; import compressor from "astro-compressor";
import vercel from "@astrojs/vercel"; import vercel from "@astrojs/vercel";
@ -54,10 +53,6 @@ export default defineConfig({
integrations: [ integrations: [
// 使用Astro官方的MDX支持 // 使用Astro官方的MDX支持
mdx(), mdx(),
swup({
cache: true,
preload: true,
}),
react(), react(),
// 使用文章索引生成器 // 使用文章索引生成器
articleIndexerIntegration(), articleIndexerIntegration(),

5847
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -18,8 +18,10 @@
"@expressive-code/plugin-collapsible-sections": "^0.41.2", "@expressive-code/plugin-collapsible-sections": "^0.41.2",
"@expressive-code/plugin-line-numbers": "^0.41.2", "@expressive-code/plugin-line-numbers": "^0.41.2",
"@mermaid-js/mermaid-cli": "^11.4.2", "@mermaid-js/mermaid-cli": "^11.4.2",
"@swup/astro": "^1.6.0",
"@swup/fragment-plugin": "^1.1.1", "@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", "@tailwindcss/vite": "^4.1.4",
"@types/react": "^19.1.2", "@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2", "@types/react-dom": "^19.1.2",

View File

@ -195,10 +195,11 @@ const breadcrumbs: Breadcrumb[] = pathSegments
// 单独保存清理事件的监听器引用 // 单独保存清理事件的监听器引用
const cleanupListeners = []; const cleanupListeners = [];
// 添加事件监听器并记录,方便后续统一清理 // 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) { function addListener(element, eventType, handler, options) {
if (!element) { if (!element) {
console.warn(`[面包屑尝试为不存在的元素添加事件`); console.warn(`[面包屑]尝试为不存在的元素添加事件`);
return null; return null;
} }
@ -214,7 +215,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
try { try {
element.removeEventListener(eventType, handler, options); element.removeEventListener(eventType, handler, options);
} catch (err) { } catch (err) {
console.error(`[面包屑移除事件监听器出错:`, err); console.error(`[面包屑]移除事件监听器出错:`, err);
} }
}); });
@ -226,7 +227,7 @@ const breadcrumbs: Breadcrumb[] = pathSegments
try { try {
element.removeEventListener(eventType, handler, options); element.removeEventListener(eventType, handler, options);
} catch (err) { } catch (err) {
console.error(`[面包屑移除清理监听器出错:`, err); console.error(`[面包屑]移除清理监听器出错:`, err);
} }
}); });
@ -234,6 +235,386 @@ const breadcrumbs: Breadcrumb[] = pathSegments
cleanupListeners.length = 0; 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() { function registerCleanupEvents() {
// 创建一次性事件处理函数 // 创建一次性事件处理函数
@ -275,25 +656,33 @@ const breadcrumbs: Breadcrumb[] = pathSegments
return; return;
} }
const clickHandler = (e) => { // 获取当前URL信息
e.preventDefault(); const url = new URL(window.location.href);
const searchParams = url.search;
const url = new URL(window.location.href);
const searchParams = url.search;
// 检查URL中是否有查询参数
if (searchParams) {
// 有查询参数,返回筛选页面
window.location.href = `/filtered${searchParams}`;
} else {
// 没有查询参数,返回默认路径
const defaultPath = backButton.getAttribute('data-path') || '';
window.location.href = defaultPath;
}
};
// 添加点击事件监听 // 根据是否有查询参数确定返回目标
addListener(backButton, 'click', clickHandler); let targetHref;
if (searchParams) {
// 有查询参数,返回筛选页面
targetHref = `/filtered${searchParams}`;
} else {
// 没有查询参数,返回默认路径
targetHref = backButton.getAttribute('data-path') || '';
}
// 修改返回按钮属性使其成为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(); registerCleanupEvents();
// 设置路径变化检测
setupPathChangeDetection();
// 执行初始更新
updateBreadcrumbs();
// 设置返回按钮 // 设置返回按钮
setupBackButton(); setupBackButton();
// 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时 // 注册页面加载后的处理函数 - 仅当使用View Transitions或Swup时
if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') { if (typeof document.startViewTransition !== 'undefined' || typeof window.swup !== 'undefined') {
// 仅监听一个事件,保持最小侵入性 // 监听页面加载事件
const pageLoadHandler = () => { const pageLoadHandler = () => {
// 检查是否在文章页面 // 重新执行一次更新
const backButton = document.querySelector('.back-button'); updateBreadcrumbs();
if (backButton) { setupBackButton();
// 先销毁已有的所有处理函数
selfDestruct();
// 重新初始化
init();
}
}; };
if (typeof document.startViewTransition !== 'undefined') { if (typeof document.startViewTransition !== 'undefined') {

View File

@ -343,6 +343,11 @@ const navSelectorClassName = "mr-4";
// 单独保存清理事件的监听器引用 // 单独保存清理事件的监听器引用
const cleanupListeners = []; 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) { function addListener(element, eventType, handler, options) {
@ -387,7 +392,6 @@ const navSelectorClassName = "mr-4";
// 注册清理事件,并保存引用 // 注册清理事件,并保存引用
function registerCleanupEvents() { function registerCleanupEvents() {
// 创建一次性事件处理函数 // 创建一次性事件处理函数
const beforeSwapHandler = () => { const beforeSwapHandler = () => {
selfDestruct(); selfDestruct();
@ -629,6 +633,113 @@ const navSelectorClassName = "mr-4";
// 注册清理事件 // 注册清理事件
registerCleanupEvents(); 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加载完成后执行初始化 // DOM加载完成后执行初始化
function initNavigation() { function initNavigation() {
// 检查DOM是否已加载 // 检查DOM是否已加载
@ -641,13 +752,64 @@ const navSelectorClassName = "mr-4";
// 主要设置函数 // 主要设置函数
function setupNavigation() { function setupNavigation() {
// 设置桌面导航 // 设置桌面导航
setupNavSelector(); setupNavSelector();
// 设置移动端导航 // 设置移动端导航
setupMobileNav(); 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(`导航脚本未找到高亮元素,导航功能可能不完整`); console.warn(`导航脚本未找到高亮元素,导航功能可能不完整`);
} }
// 获取过渡动画持续时间
const transitionDuration = parseInt(navSelector.dataset.duration || 300);
const activeClass = "font-medium"; const activeClass = "font-medium";
// 当前活动项的状态 - 从DOM中提取当前激活状态 // 当前活动项的状态 - 从DOM中提取当前激活状态
@ -730,64 +890,7 @@ 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) { function setActiveItem(itemId) {
// 清除所有菜单状态 // 清除所有菜单状态
@ -1036,12 +1139,11 @@ const navSelectorClassName = "mr-4";
// 设置激活状态 // 设置激活状态
activeGroupId = groupId; activeGroupId = groupId;
// 如果没有选中的二级菜单项,则选中第一个并导航 // 如果没有选中的二级菜单项,则选中第一个并模拟点击
if (!activeSubItemId || !activeSubItemId.startsWith(groupId)) { if (!activeSubItemId || !activeSubItemId.startsWith(groupId)) {
const firstSubItem = targetGroup.querySelector('.nav-subitem'); const firstSubItem = targetGroup.querySelector('.nav-subitem');
if (firstSubItem) { if (firstSubItem) {
const subItemId = firstSubItem.dataset.subitemId; const subItemId = firstSubItem.dataset.subitemId;
const href = firstSubItem.getAttribute('href');
// 清除所有二级菜单项的高亮 // 清除所有二级菜单项的高亮
navSubItems.forEach(item => { navSubItems.forEach(item => {
@ -1058,10 +1160,10 @@ const navSelectorClassName = "mr-4";
// 设置高亮 // 设置高亮
activeSubItemId = subItemId; activeSubItemId = subItemId;
// 导航到第一个子 // 直接模拟点击第一个子菜单
if (href) { setTimeout(() => {
navigateTo(href); firstSubItem.click();
} }, 10);
} }
} else { } 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(); e.preventDefault();
const itemId = item.dataset.itemId; const itemId = item.dataset.itemId;
const href = item.getAttribute('href');
// 设置高亮 // 设置高亮
setActiveItem(itemId); setActiveItem(itemId);
// 导航到目标页面
if (href) {
navigateTo(href);
}
}); });
}); });
@ -1157,15 +1223,11 @@ const navSelectorClassName = "mr-4";
e.preventDefault(); e.preventDefault();
const subItemId = item.dataset.subitemId; const subItemId = item.dataset.subitemId;
const href = item.getAttribute('href');
// 设置高亮 // 设置高亮
setActiveSubItem(subItemId); setActiveSubItem(subItemId);
// 导航到目标页面
if (href) {
navigateTo(href);
}
}); });
}); });
@ -1177,12 +1239,9 @@ const navSelectorClassName = "mr-4";
if (hasSwup) { if (hasSwup) {
// 页面内容替换后重新初始化 // 页面内容替换后重新初始化
addListener(document, 'swup:contentReplaced', () => { addListener(document, 'swup:contentReplaced', () => {
setTimeout(initActiveState, 50); setTimeout(() => {
}); initActiveState();
}, 50);
// 视图转换开始
addListener(document, 'swup:animationInStart', () => {
// 这里可以添加动画开始时的处理逻辑
}); });
// 视图转换结束 // 视图转换结束
@ -1194,23 +1253,33 @@ const navSelectorClassName = "mr-4";
}); });
} }
// 添加 astro:after-swap 事件监听
addListener(document, 'astro:after-swap', () => {
setTimeout(() => {
initActiveState();
updateHighlights(true);
}, 50);
});
// 添加Astro View Transitions事件监听 // 添加Astro View Transitions事件监听
addListener(document, 'astro:page-load', () => { addListener(document, 'astro:page-load', () => {
setTimeout(initActiveState, 50); setTimeout(initActiveState, 50);
updateHighlights(true); updateHighlights(true);
}); });
// 添加popstate事件监听
addListener(window, 'popstate', () => {
setTimeout(() => {
initActiveState();
updateHighlights(true);
}, 50);
});
// 窗口大小变化时重新计算高亮位置 // 窗口大小变化时重新计算高亮位置
addListener(window, 'resize', () => { addListener(window, 'resize', () => {
updateHighlights(true); updateHighlights(true);
}); });
// 在document上添加自定义方法方便外部调用但使用弱引用避免内存泄漏
if (!document.resetNavigation) {
document.resetNavigation = function() {
initActiveState();
};
}
} }
// 初始化移动端菜单和搜索功能 // 初始化移动端菜单和搜索功能
@ -1460,12 +1529,6 @@ const navSelectorClassName = "mr-4";
setupMobileSubmenuToggles(); setupMobileSubmenuToggles();
updateMobileMenuHighlight(); updateMobileMenuHighlight();
// 在document上添加自定义方法方便外部调用可选
document.updateMobileMenuHighlight = updateMobileMenuHighlight;
document.closeMobileMenu = closeMobileMenu;
document.closeMobileSearch = closeMobileSearch;
document.toggleMobileSubmenu = toggleSubmenu;
return updateMobileMenuHighlight; return updateMobileMenuHighlight;
} }

View File

@ -92,51 +92,99 @@ const {
]} ]}
/> />
<!-- 主题切换脚本 --> <!-- 主题切换脚本 - 升级为自销毁模式 -->
<script is:inline> <script is:inline>
// 立即执行主题初始化,采用"无闪烁"加载方式 // 立即执行主题初始化,采用"无闪烁"加载方式
(function () { (function () {
// 存储事件监听器,便于清理 // 集中管理所有事件监听器
const listeners = []; const allListeners = [];
// 单独保存清理事件的监听器引用
const cleanupListeners = [];
// 定时器引用
let themeUpdateTimeout = null;
// 添加事件监听器并记录,方便后续统一清理 // 添加事件监听器并记录,方便后续统一清理
function addListener(element, eventType, handler, options) { function addListener(element, eventType, handler, options) {
if (!element) return null; if (!element) {
console.error(`尝试为不存在的元素添加事件:`, eventType);
return null;
}
element.addEventListener(eventType, handler, options); element.addEventListener(eventType, handler, options);
listeners.push({ element, eventType, handler, options }); allListeners.push({ element, eventType, handler, options });
return handler; return handler;
} }
// 清理函数 - 移除所有事件监听器 // 统一的清理函数,执行完整清理并自销毁
function cleanup() { function selfDestruct() {
listeners.forEach(({ element, eventType, handler, options }) => { // 1. 清理所有计时器
if (themeUpdateTimeout) {
clearTimeout(themeUpdateTimeout);
themeUpdateTimeout = null;
}
// 2. 移除普通事件监听器
allListeners.forEach(({ element, eventType, handler, options }) => {
try { try {
element.removeEventListener(eventType, handler, options); element.removeEventListener(eventType, handler, options);
} catch (err) { } 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);
}
});
// 清空清理监听器数组
cleanupListeners.length = 0;
} }
// 注册清理函数 - 确保在页面转换前清理事件 // 注册清理事件,并保存引用
function registerCleanup() { function registerCleanupEvents() {
const cleanupEvents = [ // 创建一次性事件处理函数
"astro:before-preparation", const beforeSwapHandler = () => {
"astro:before-swap", selfDestruct();
"swup:willReplaceContent" };
];
const beforeUnloadHandler = () => {
// 为每个事件注册一次性清理函数 selfDestruct();
cleanupEvents.forEach((eventName) => { };
document.addEventListener(eventName, cleanup, { once: true });
}); // 添加清理事件监听器并保存引用
document.addEventListener("astro:before-swap", beforeSwapHandler, { once: true });
// 页面卸载时清理 window.addEventListener("beforeunload", beforeUnloadHandler, { once: true });
window.addEventListener("beforeunload", cleanup, { 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 { try {
@ -181,29 +229,55 @@ const {
addListener(mediaQuery, "change", handleMediaChange); addListener(mediaQuery, "change", handleMediaChange);
// 注册清理函数 // 注册清理函数
registerCleanup(); registerCleanupEvents();
// 监听页面转换事件,确保在页面转换后重新初始化 // 监听页面转换事件,确保在页面转换后重新初始化
function onPageTransition() { function onPageTransition() {
// 重新初始化主题 // 防止重复执行,使用防抖
if (storedTheme) { if (themeUpdateTimeout) {
document.documentElement.dataset.theme = storedTheme; clearTimeout(themeUpdateTimeout);
} else {
const systemTheme = getSystemTheme();
document.documentElement.dataset.theme = systemTheme;
} }
// 使用微小延迟确保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); addListener(document, "astro:page-load", onPageTransition);
document.addEventListener("astro:after-swap", onPageTransition); addListener(document, "astro:after-swap", onPageTransition);
} catch (error) { } catch (error) {
console.error("主题初始化失败:", error);
// 出错时应用默认浅色主题,确保页面正常显示 // 出错时应用默认浅色主题,确保页面正常显示
document.documentElement.dataset.theme = "light"; document.documentElement.dataset.theme = "light";
// 即使出错也尝试注册清理事件
try {
registerCleanupEvents();
} catch (e) {
// 忽略清理注册错误
}
} }
})(); })();
</script> </script>
<script>
import '../scripts/swup-init.js';
</script>
</head> </head>
<body <body
class="m-0 w-full h-full bg-gray-50 dark:bg-dark-bg flex flex-col min-h-screen" 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} 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> </body>
</html> </html>

View File

@ -291,9 +291,9 @@ const tableOfContents = generateTableOfContents(headings);
</div> </div>
</div> </div>
<!-- 文章头部 --> <!-- 文章头部 - 包含面包屑,保持不变 -->
<header class="mb-8"> <header class="mb-8">
<!-- 导航区域 --> <!-- 导航区域/面包屑 - 不参与视图切换 -->
<div <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" 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>
</div> </div>
<!-- 文章过期提醒 - 放入article-content容器内 -->
</header>
<!-- 文章内容区域 - 只有这部分参与视图切换 -->
<div id="article-content">
<!-- 文章过期提醒 --> <!-- 文章过期提醒 -->
{ {
(() => { (() => {
@ -426,14 +431,103 @@ const tableOfContents = generateTableOfContents(headings);
</div> </div>
) )
} }
</header>
<!-- 文章内容 --> <article
<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"
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 />
<Content /> </article>
</article>
<!-- 相关文章 -->
{
relatedArticles.length > 0 && (
<aside class="mt-12 pt-8 border-t border-secondary-200 dark:border-gray-700">
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
{relatedArticlesMatchType === "tag"
? "相关文章"
: relatedArticlesMatchType === "directory"
? "同类文章"
: "推荐阅读"}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedArticles.map((relatedArticle: ArticleEntry) => (
<a
href={getArticleUrl(relatedArticle.id)}
class="article-card"
data-astro-prefetch="viewport"
>
<div class="article-card-content">
<div class="article-card-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
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"
/>
</svg>
</div>
<div class="article-card-body">
<h3 class="article-card-title">
{relatedArticle.data.title}
</h3>
<p class="article-card-date">
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
</p>
{relatedArticle.data.summary && (
<p class="article-card-summary">
{relatedArticle.data.summary}
</p>
)}
<div class="article-card-footer">
<time
datetime={relatedArticle.data.date.toISOString()}
class="article-card-date"
>
{relatedArticle.data.date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
<span class="article-card-read-more">阅读全文</span>
</div>
</div>
</div>
</a>
))}
</div>
</aside>
)
}
<!-- 返回顶部按钮 -->
<button
id="back-to-top"
class="fixed bottom-8 right-8 w-12 h-12 rounded-full bg-primary-500 dark:bg-primary-600 text-white shadow-md flex items-center justify-center opacity-0 invisible translate-y-5 hover:bg-primary-600 dark:hover:bg-primary-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
></path>
</svg>
</button>
</div>
<!-- 目录 --> <!-- 目录 -->
<section <section
@ -471,96 +565,6 @@ const tableOfContents = generateTableOfContents(headings);
</div> </div>
</div> </div>
</section> </section>
<!-- 相关文章 -->
{
relatedArticles.length > 0 && (
<aside class="mt-12 pt-8 border-t border-secondary-200 dark:border-gray-700">
<h2 class="text-2xl font-bold mb-6 text-primary-900 dark:text-primary-100">
{relatedArticlesMatchType === "tag"
? "相关文章"
: relatedArticlesMatchType === "directory"
? "同类文章"
: "推荐阅读"}
</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
{relatedArticles.map((relatedArticle: ArticleEntry) => (
<a
href={getArticleUrl(relatedArticle.id)}
class="article-card"
data-astro-prefetch="viewport"
>
<div class="article-card-content">
<div class="article-card-icon">
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-5 w-5"
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"
/>
</svg>
</div>
<div class="article-card-body">
<h3 class="article-card-title">
{relatedArticle.data.title}
</h3>
<p class="article-card-date">
{relatedArticle.data.date.toLocaleDateString("zh-CN")}
</p>
{relatedArticle.data.summary && (
<p class="article-card-summary">
{relatedArticle.data.summary}
</p>
)}
<div class="article-card-footer">
<time
datetime={relatedArticle.data.date.toISOString()}
class="article-card-date"
>
{relatedArticle.data.date.toLocaleDateString("zh-CN", {
year: "numeric",
month: "long",
day: "numeric",
})}
</time>
<span class="article-card-read-more">阅读全文</span>
</div>
</div>
</div>
</a>
))}
</div>
</aside>
)
}
<!-- 返回顶部按钮 -->
<button
id="back-to-top"
class="fixed bottom-8 right-8 w-12 h-12 rounded-full bg-primary-500 dark:bg-primary-600 text-white shadow-md flex items-center justify-center opacity-0 invisible translate-y-5 hover:bg-primary-600 dark:hover:bg-primary-700"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 10l7-7m0 0l7 7m-7-7v18"
></path>
</svg>
</button>
</div> </div>
<!-- 文章页面脚本 --> <!-- 文章页面脚本 -->
<script is:inline> <script is:inline>

View File

@ -74,7 +74,7 @@ const pageTitle = currentPath ? currentPath : '文章列表';
<slot name="head" slot="head" /> <slot name="head" slot="head" />
<div class="py-6 w-full"> <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="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"> <div class="px-4 py-3">
<Breadcrumb <Breadcrumb
@ -85,149 +85,151 @@ const pageTitle = currentPath ? currentPath : '文章列表';
</div> </div>
</div> </div>
<!-- 内容卡片网格 --> <!-- 内容区域 - 只有这部分参与视图切换 -->
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-12"> <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('/') : ''}/`} {pathSegments.length > 0 && (
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" <a href={`/articles/${pathSegments.length > 1 ? pathSegments.slice(0, -1).join('/') : ''}`}
data-astro-prefetch="hover">
<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">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
</svg>
</div>
<div class="ml-3 flex-1">
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300">返回上级目录</div>
<div class="text-xs text-gray-500">返回上一级</div>
</div>
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div>
</a>
)}
{/* 目录卡片 */}
{currentSections.map(section => {
// 确保目录链接正确生成
const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
return (
<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"> 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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 17l-5-5m0 0l5-5m-5 5h12" />
</svg> </svg>
</div> </div>
<div class="ml-3 flex-1"> <div class="ml-3 flex-1">
<div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1">{section.name}</div> <div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300">返回上级目录</div>
<div class="text-xs text-gray-500 flex items-center mt-1"> <div class="text-xs text-gray-500">返回上一级</div>
{section.sections.length > 0 && (
<span class="flex items-center mr-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</svg>
{section.sections.length} 个子目录
</span>
)}
{section.articles.length > 0 && (
<span class="flex items-center">
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{section.articles.length} 篇文章
</span>
)}
</div>
</div> </div>
<div class="text-primary-500 opacity-0 group-hover:opacity-100"> <div class="text-primary-500 opacity-0 group-hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"> <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg> </svg>
</div> </div>
</div> </div>
</a> </a>
); )}
})}
{/* 文章卡片 */}
{currentArticles.map(articlePath => {
// 获取文章ID - 不需要移除src/content前缀因为contentStructure中已经是相对路径
const articleId = articlePath;
// 尝试匹配文章 {/* 目录卡片 */}
const article = articles.find(a => a.id === articleId); {currentSections.map(section => {
// 确保目录链接正确生成
if (!article) { const dirLink = currentPath ? `${currentPath}/${section.name}` : section.name;
return ( return (
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg"> <a href={`/articles/${dirLink}/`}
<div class="flex items-start"> 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"
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600"> data-astro-prefetch="viewport">
<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">
<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">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" /> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 19a2 2 0 01-2-2V7a2 2 0 012-2h4l2 2h4a2 2 0 012 2v1M5 19h14a2 2 0 002-2v-5a2 2 0 00-2-2H9a2 2 0 00-2 2v5a2 2 0 01-2 2z" />
</svg> </svg>
</div> </div>
<div class="ml-3 flex-1"> <div class="ml-3 flex-1">
<h3 class="font-bold text-base text-red-800">文章不存在</h3> <div class="font-bold text-base text-gray-800 dark:text-gray-100 group-hover:text-primary-700 dark:group-hover:text-primary-300 line-clamp-1">{section.name}</div>
<p class="text-xs text-red-600 mt-1"> <div class="text-xs text-gray-500 flex items-center mt-1">
<div>原始路径: {articlePath}</div> {section.sections.length > 0 && (
<div>文章ID: {articleId}</div> <span class="flex items-center mr-3">
<div>当前目录: {currentPath}</div> <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
</p> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 7v10a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-6l-2-2H5a2 2 0 00-2 2z" />
</div> </svg>
</div> {section.sections.length} 个子目录
</div> </span>
); )}
} {section.articles.length > 0 && (
<span class="flex items-center">
return ( <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<div class="article-card"> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
<a href={`/articles/${article.id}`} </svg>
class="article-card-link" {section.articles.length} 篇文章
data-astro-prefetch="viewport"> </span>
<div class="article-card-content"> )}
<div class="article-card-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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" />
</svg>
</div>
<div class="article-card-body">
<h3 class="article-card-title">{article.data.title}</h3>
{article.body && (
<p class="article-card-summary">
{article.data.summary}
</p>
)}
<div class="article-card-footer">
<time datetime={article.data.date.toISOString()} class="article-card-date">
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
</time>
<span class="article-card-read-more">阅读全文</span>
</div> </div>
</div> </div>
<div class="text-primary-500 opacity-0 group-hover:opacity-100">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</div>
</div> </div>
</a> </a>
</div> );
); })}
})}
</div> {/* 文章卡片 */}
{currentArticles.map(articlePath => {
{/* 空内容提示 */} // 获取文章ID - 不需要移除src/content前缀因为contentStructure中已经是相对路径
{(currentSections.length === 0 && currentArticles.length === 0) && ( const articleId = articlePath;
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"> // 尝试匹配文章
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" /> const article = articles.find(a => a.id === articleId);
</svg>
<h3 class="text-2xl font-bold text-gray-700 mb-2">此目录为空</h3> if (!article) {
<p class="text-gray-500 max-w-md mx-auto">此目录下暂无内容,请浏览其他目录或返回上一级</p> return (
<div class="flex flex-col h-full p-5 border border-red-200 rounded-xl bg-red-50 shadow-lg">
<div class="flex items-start">
<div class="w-10 h-10 flex-shrink-0 flex items-center justify-center rounded-lg bg-red-100 text-red-600">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
</div>
<div class="ml-3 flex-1">
<h3 class="font-bold text-base text-red-800">文章不存在</h3>
<p class="text-xs text-red-600 mt-1">
<div>原始路径: {articlePath}</div>
<div>文章ID: {articleId}</div>
<div>当前目录: {currentPath}</div>
</p>
</div>
</div>
</div>
);
}
return (
<div class="article-card">
<a href={`/articles/${article.id}`}
class="article-card-link"
data-astro-prefetch="viewport">
<div class="article-card-content">
<div class="article-card-icon">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" 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" />
</svg>
</div>
<div class="article-card-body">
<h3 class="article-card-title">{article.data.title}</h3>
{article.body && (
<p class="article-card-summary">
{article.data.summary}
</p>
)}
<div class="article-card-footer">
<time datetime={article.data.date.toISOString()} class="article-card-date">
{article.data.date.toLocaleDateString('zh-CN', {year: 'numeric', month: 'long', day: 'numeric'})}
</time>
<span class="article-card-read-more">阅读全文</span>
</div>
</div>
</div>
</a>
</div>
);
})}
</div> </div>
)}
{/* 空内容提示 */}
{(currentSections.length === 0 && currentArticles.length === 0) && (
<div class="text-center py-16 bg-white rounded-xl shadow-lg border border-gray-200 dark:border-gray-700 mb-12">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 mx-auto text-primary-200 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
</svg>
<h3 class="text-2xl font-bold text-gray-700 mb-2">此目录为空</h3>
<p class="text-gray-500 max-w-md mx-auto">此目录下暂无内容,请浏览其他目录或返回上一级</p>
</div>
)}
</div>
</div> </div>
</Layout> </Layout>

View File

@ -9,7 +9,7 @@ const searchParams = Astro.url.searchParams;
<Layout title="文章筛选"> <Layout title="文章筛选">
<div class="w-full"> <div class="w-full">
<!-- 导航栏 --> <!-- 导航栏/面包屑 - 不参与视图切换 -->
<div <div
class="bg-white dark:bg-gray-800 rounded-xl mb-4 shadow-lg border border-gray-200 dark:border-gray-700" 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>
</div> </div>
<!-- 使用ArticleFilter组件 --> <!-- 内容区域 - 只有这部分参与视图切换 -->
<ArticleFilter <div id="article-content">
searchParams={searchParams} <ArticleFilter
client:load searchParams={searchParams}
/> client:load
/>
</div>
</div> </div>
</Layout> </Layout>

View File

@ -22,10 +22,16 @@ export function rehypeCodeBlocks() {
className => typeof className === 'string' && className.startsWith('language-') 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 const language = languageClass
? languageClass.split('-')[1].toUpperCase() ? languageClass.split('-')[1].toUpperCase()
: 'TEXT'; : (shikiLanguage ? shikiLanguage.toUpperCase() : 'TEXT');
// 跳过处理 mermaid 图表 // 跳过处理 mermaid 图表
if (language === 'MERMAID') { if (language === 'MERMAID') {

325
src/scripts/swup-init.js Normal file
View 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 });
});