325 lines
10 KiB
JavaScript
325 lines
10 KiB
JavaScript
|
// 统一初始化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 });
|
|||
|
});
|