diff --git a/src/pages/404.astro b/src/pages/404.astro
index 04cbc65..257fbb5 100644
--- a/src/pages/404.astro
+++ b/src/pages/404.astro
@@ -1,23 +1,33 @@
---
-import Layout from '@/components/Layout.astro';
+import Layout from "@/components/Layout.astro";
---
-
-
-v class="min-h-[calc(100vh-4rem)] flex items-center justify-center">
-
-
- 404
-
-
- 页面未找到
-
-
- 您访问的页面不存在或已被移动到其他位置
-
-
- 返回首页
-
-
+
+
+
+ 404
+
+
+ 页面未找到
+
+
+ 您访问的页面不存在或已被移动到其他位置
+
+
+ 返回首页
+
-
\ No newline at end of file
+
diff --git a/src/pages/articles/[...id].astro b/src/pages/articles/[...id].astro
index b1a36bc..d8e3c1e 100644
--- a/src/pages/articles/[...id].astro
+++ b/src/pages/articles/[...id].astro
@@ -421,7 +421,7 @@ const tableOfContents = generateTableOfContents(headings);
{article.data.tags.map((tag: string) => (
diff --git a/src/scripts/swup-init.js b/src/scripts/swup-init.js
index 7cf125d..73a0709 100644
--- a/src/scripts/swup-init.js
+++ b/src/scripts/swup-init.js
@@ -8,6 +8,130 @@ import SwupPreloadPlugin from '@swup/preload-plugin';
// 添加Scripts插件 - 确保页面转场后脚本能重新执行
import SwupScriptsPlugin from '@swup/scripts-plugin';
+// 创建加载动画元素
+function createLoadingSpinner() {
+ // 检查是否已存在加载动画元素
+ const existingSpinner = document.getElementById('swup-loading-spinner');
+ if (existingSpinner) {
+ return existingSpinner;
+ }
+
+ // 创建加载动画容器
+ const spinner = document.createElement('div');
+ spinner.id = 'swup-loading-spinner';
+ spinner.className = 'loading-spinner-container';
+
+ // 创建内部旋转元素
+ const spinnerInner = document.createElement('div');
+ spinnerInner.className = 'loading-spinner';
+
+ // 添加到页面
+ spinner.appendChild(spinnerInner);
+
+ // 默认隐藏
+ spinner.style.display = 'none';
+
+ return spinner;
+}
+
+// 将加载动画添加到body并固定在内容区域的中心
+function addSpinnerToBody(spinner) {
+ if (!spinner) return;
+
+ try {
+ // 先从DOM中移除(如果已存在)
+ if (spinner.parentNode) {
+ spinner.parentNode.removeChild(spinner);
+ }
+
+ // 获取当前活跃元素
+ const activeElement = getActiveElement();
+
+ // 添加到body而不是活跃容器,避免内容替换时被移除
+ document.body.appendChild(spinner);
+
+ // 如果有活跃元素,根据其位置调整加载动画的位置
+ if (activeElement) {
+ // 获取活跃元素的位置信息
+ const rect = activeElement.getBoundingClientRect();
+
+ // 计算中心点相对于视口的位置
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ // 设置加载动画位置
+ spinner.style.position = 'fixed';
+ spinner.style.top = centerY + 'px';
+ spinner.style.left = centerX + 'px';
+ spinner.style.transform = 'translate(-50%, -50%)';
+ spinner.style.zIndex = '9999'; // 确保在最顶层
+ } else {
+ // 如果没有活跃元素,则居中显示
+ spinner.style.position = 'fixed';
+ spinner.style.top = '50%';
+ spinner.style.left = '50%';
+ spinner.style.transform = 'translate(-50%, -50%)';
+ spinner.style.zIndex = '9999'; // 确保在最顶层
+ }
+ } catch (error) {
+ console.error('添加加载动画时出错:', error);
+ }
+}
+
+// 显示加载动画
+function showLoadingSpinner(spinner, forceNew = false) {
+ if (!spinner) return;
+
+ // 确保加载动画已添加到body
+ addSpinnerToBody(spinner);
+
+ // 检查加载动画是否已在显示
+ if (spinner.classList.contains('is-active') && !forceNew) {
+ return;
+ }
+
+ spinner.style.display = 'flex';
+ spinner.classList.add('is-active');
+}
+
+// 隐藏加载动画
+function hideLoadingSpinner(spinner) {
+ if (!spinner) return;
+
+ if (!spinner.classList.contains('is-active')) {
+ return;
+ }
+
+ // 检查元素是否在DOM中
+ if (!document.body.contains(spinner)) {
+ return;
+ }
+
+ completeHideLoadingSpinner(spinner);
+}
+
+// 实际执行隐藏加载动画的函数
+function completeHideLoadingSpinner(spinner) {
+ if (!spinner) return;
+
+ if (!document.body.contains(spinner)) {
+ return;
+ }
+
+ if (!spinner.classList.contains('is-active')) {
+ return;
+ }
+
+ spinner.classList.remove('is-active');
+
+ // 添加淡出效果后移除
+ setTimeout(() => {
+ if (spinner && document.body.contains(spinner)) {
+ spinner.style.display = 'none';
+ }
+ }, 300);
+}
+
// 检查是否是文章相关页面
function isArticlePage() {
const path = window.location.pathname;
@@ -63,6 +187,14 @@ document.addEventListener('DOMContentLoaded', () => {
// 应用过渡效果
applyTransitions();
+ // 创建加载动画元素
+ const spinner = createLoadingSpinner();
+
+ // 页面状态跟踪
+ let isLoading = false;
+ let contentReplaced = false;
+ let spinnerCheckInterval = null;
+
// 创建Swup实例
const swup = new Swup({
// Swup的基本配置
@@ -88,6 +220,27 @@ document.addEventListener('DOMContentLoaded', () => {
document.dispatchEvent(event);
}
+ // 检查加载动画状态并根据需要重新显示
+ function checkAndRestoreSpinner() {
+ if (isLoading && !contentReplaced && spinner) {
+ // 如果正在加载但加载动画不可见,则重新显示
+ if (!spinner.classList.contains('is-active') || spinner.style.display === 'none') {
+ showLoadingSpinner(spinner, true);
+ }
+ } else if (!isLoading || contentReplaced) {
+ // 如果加载已完成但动画仍在显示,隐藏动画
+ if (spinner && spinner.classList.contains('is-active')) {
+ hideLoadingSpinner(spinner);
+ }
+
+ // 如果有轮询,停止轮询
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ spinnerCheckInterval = null;
+ }
+ }
+ }
+
// 添加预加载插件 - 代替原有的预加载功能
const preloadPlugin = new SwupPreloadPlugin({
// 最多同时预加载5个链接
@@ -169,12 +322,81 @@ document.addEventListener('DOMContentLoaded', () => {
// 初始化时设置
setupTransition();
- // 在页面内容加载后重新应用样式
+ // 监听willReplaceContent事件,确保加载动画在内容替换前可见
+ document.addEventListener('swup:willReplaceContent', () => {
+ if (isLoading && !contentReplaced && spinner) {
+ // 确保加载动画在内容替换过程中可见
+ if (!spinner.classList.contains('is-active')) {
+ showLoadingSpinner(spinner, true);
+ }
+ }
+ });
+
+ // 注册从服务器获取内容事件 - 使用正确的钩子名称
+ swup.hooks.on('visit:start', () => {
+ isLoading = true;
+ contentReplaced = false;
+
+ // 开始定期检查加载动画状态
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ }
+
+ spinnerCheckInterval = setInterval(() => {
+ checkAndRestoreSpinner();
+ }, 500); // 每500ms检查一次
+ });
+
+ // 注册内容加载完成事件
swup.hooks.on('content:replace', () => {
+ contentReplaced = true;
+
// 重新设置过渡样式
setTimeout(() => {
setupTransition();
}, 10);
+
+ // 只有在完成内容替换后才隐藏加载动画
+ hideLoadingSpinner(spinner);
+
+ // 停止轮询
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ spinnerCheckInterval = null;
+ }
+ });
+
+ // 页面加载完成事件
+ swup.hooks.on('page:view', () => {
+ isLoading = false;
+
+ // 如果内容替换失败,确保隐藏加载动画
+ if (!contentReplaced) {
+ hideLoadingSpinner(spinner);
+ }
+
+ // 停止轮询
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ spinnerCheckInterval = null;
+ }
+ });
+
+ // 加载失败处理
+ swup.hooks.on('fetch:error', (error) => {
+ isLoading = false;
+
+ // 错误处理时确保隐藏加载动画
+ hideLoadingSpinner(spinner);
+
+ // 停止轮询
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ spinnerCheckInterval = null;
+ }
+
+ // 可以在这里添加错误提示UI
+ alert('页面加载失败,请重试或检查网络连接。');
});
// 监听动画开始和结束
@@ -228,6 +450,11 @@ document.addEventListener('DOMContentLoaded', () => {
setTimeout(() => {
setElementOpacity(activeElement, 1);
}, 50);
+
+ // 确保加载动画在内容加载完成后立即隐藏
+ if (spinner && spinner.classList.contains('is-active')) {
+ hideLoadingSpinner(spinner);
+ }
});
// 监听URL变化以更新动画行为
@@ -235,6 +462,18 @@ document.addEventListener('DOMContentLoaded', () => {
// 发送页面切换事件
sendPageTransitionEvent();
+ // 确保先前的加载动画已隐藏
+ if (spinner.classList.contains('is-active')) {
+ completeHideLoadingSpinner(spinner);
+ // 短暂延迟后再显示新的加载动画
+ setTimeout(() => {
+ showLoadingSpinner(spinner, true);
+ }, 50);
+ } else {
+ // 直接显示加载动画
+ showLoadingSpinner(spinner);
+ }
+
// 检查目标URL是否为文章相关页面
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
const isCurrentArticlePage = isArticlePage();
@@ -257,7 +496,6 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
-
// 监听Fragment插件是否成功应用
document.addEventListener('swup:fragmentReplaced', () => {
// 确保新内容有正确的过渡样式
@@ -271,6 +509,12 @@ document.addEventListener('DOMContentLoaded', () => {
// 发送页面切换事件
sendPageTransitionEvent();
+ // 清除轮询定时器
+ if (spinnerCheckInterval) {
+ clearInterval(spinnerCheckInterval);
+ spinnerCheckInterval = null;
+ }
+
if (swup) {
swup.unuse(fragmentPlugin);
swup.unuse(headPlugin);
diff --git a/src/styles/global.css b/src/styles/global.css
index 6bc1dc1..c895d05 100644
--- a/src/styles/global.css
+++ b/src/styles/global.css
@@ -4,6 +4,94 @@
/* 定义深色模式选择器 */
@custom-variant dark (&:where([data-theme=dark], [data-theme=dark] *));
+/* 加载旋转动画 - 无遮罩版本 */
+.loading-spinner-container {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ z-index: 9999;
+ opacity: 0;
+ visibility: hidden;
+ transition: opacity 0.3s ease, visibility 0.3s ease;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ pointer-events: none; /* 允许点击穿透 */
+}
+
+/* 激活状态 */
+.loading-spinner-container.is-active {
+ opacity: 1;
+ visibility: visible;
+}
+
+/* 旋转动画元素 */
+.loading-spinner {
+ width: 50px;
+ height: 50px;
+ border: 3px solid transparent;
+ border-top-color: var(--color-primary-500);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ position: relative;
+ /* 添加背景色以增强可见性 */
+ background-color: rgba(255, 255, 255, 0.8);
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.1);
+ padding: 15px;
+}
+
+/* 深色模式下旋转动画颜色 */
+[data-theme='dark'] .loading-spinner {
+ border-top-color: var(--color-primary-400);
+ background-color: rgba(15, 23, 42, 0.8);
+ box-shadow: 0 0 15px rgba(0, 0, 0, 0.3);
+}
+
+.loading-spinner:before,
+.loading-spinner:after {
+ content: '';
+ position: absolute;
+ border: 3px solid transparent;
+ border-radius: 50%;
+}
+
+.loading-spinner:before {
+ top: -3px;
+ left: -3px;
+ right: -3px;
+ bottom: -3px;
+ border-top-color: var(--color-primary-300);
+ animation: spin 1.5s linear infinite;
+}
+
+.loading-spinner:after {
+ top: 6px;
+ left: 6px;
+ right: 6px;
+ bottom: 6px;
+ border-top-color: var(--color-primary-600);
+ animation: spin 0.75s linear infinite;
+}
+
+/* 深色模式下的内外圈颜色 */
+[data-theme='dark'] .loading-spinner:before {
+ border-top-color: var(--color-primary-200);
+}
+
+[data-theme='dark'] .loading-spinner:after {
+ border-top-color: var(--color-primary-500);
+}
+
+/* 旋转动画 */
+@keyframes spin {
+ 0% {
+ transform: rotate(0deg);
+ }
+ 100% {
+ transform: rotate(360deg);
+ }
+}
@theme {
/* 主色调 - 使用更现代的蓝紫色 */