refactor: 重构加载动画代码,提升可维护性

JavaScript 重构:
- 创建 LoadingOverlay 模块化管理器(IIFE 模式)
- 封装元素创建、显示、隐藏和销毁逻辑
- 添加定时器管理,防止内存泄漏
- 提供向后兼容的函数接口
- 使用常量管理配置项(ID、类名、动画时长)
- 改进代码注释和 JSDoc 文档

CSS 重构:
- 统一类名前缀为 loading-*,语义更清晰
- 移除 ID 选择器依赖,改用类选择器
- 分离关注点:基础旋转器、进度条、遮罩层、骨架屏
- 添加结构化注释,便于定位和修改
- 优化选择器层级,提升性能
- 独立动画关键帧定义

代码改进:
- 减少重复代码,提高复用性
- 更好的错误处理和边界情况处理
- 支持多次调用不会重复创建元素
- 清晰的模块边界和职责划分
This commit is contained in:
2026-01-27 16:46:13 +08:00
parent 67d1465014
commit 73103ea853
2 changed files with 260 additions and 115 deletions

View File

@@ -3199,73 +3199,167 @@ var pjaxContainers = pjaxContainerSelectors.filter(function(selector) {
return document.querySelector(selector); return document.querySelector(selector);
}); });
// ==========================================================================
// 页面加载动画管理器
// ==========================================================================
/** /**
* 显示页面加载动画遮罩 * 加载动画管理器
* 负责创建、显示和隐藏页面加载动画
*/ */
function showLoadingOverlay() { const LoadingOverlay = (function() {
let el = document.getElementById('article-loading-overlay'); const OVERLAY_ID = 'article-loading-overlay';
if (!el) { const ANIMATION_DURATION = 300;
// 动态创建加载动画元素 const CLASS_VISIBLE = 'is-visible';
el = document.createElement('div'); const CLASS_HIDING = 'is-hiding';
el.id = 'article-loading-overlay';
el.innerHTML = ` let overlayElement = null;
<div class="overlay-content"> let hideTimer = null;
<div class="overlay-card">
<div class="overlay-thumb skeleton"> /**
<div class="overlay-thumb-shimmer"></div> * 创建骨架屏 HTML 结构
* @returns {string} HTML 字符串
*/
function createSkeletonHTML() {
return `
<div class="loading-overlay-content">
<div class="loading-card">
<div class="loading-thumb">
<div class="loading-shimmer"></div>
</div> </div>
<div class="overlay-body"> <div class="loading-body">
<div class="overlay-meta"> <div class="loading-meta">
<div class="overlay-avatar skeleton"></div> <div class="loading-avatar"></div>
<div class="overlay-meta-text"> <div class="loading-meta-text">
<div class="overlay-meta-line skeleton" style="width: 120px"></div> <div class="loading-meta-line" style="width: 120px"></div>
<div class="overlay-meta-line skeleton" style="width: 80px"></div> <div class="loading-meta-line" style="width: 80px"></div>
</div> </div>
</div> </div>
<div class="overlay-title skeleton"></div> <div class="loading-title"></div>
<div class="overlay-text"> <div class="loading-text">
<div class="overlay-row skeleton" style="width: 95%"></div> <div class="loading-line" style="width: 95%"></div>
<div class="overlay-row skeleton" style="width: 88%"></div> <div class="loading-line" style="width: 88%"></div>
<div class="overlay-row skeleton" style="width: 92%"></div> <div class="loading-line" style="width: 92%"></div>
<div class="overlay-row skeleton" style="width: 78%"></div> <div class="loading-line" style="width: 78%"></div>
</div> </div>
<div class="overlay-tags"> <div class="loading-tags">
<div class="overlay-tag skeleton"></div> <div class="loading-tag"></div>
<div class="overlay-tag skeleton"></div> <div class="loading-tag"></div>
<div class="overlay-tag skeleton"></div> <div class="loading-tag"></div>
</div> </div>
</div> </div>
</div> </div>
<div class="overlay-spinner"> <div class="loading-spinner-wrapper">
<div class="spinner-ring"> <div class="loading-spinner">
<div class="spinner-ring-inner"></div> <div class="loading-spinner-ring"></div>
</div> </div>
<div class="spinner-text">正在加载精彩内容</div> <div class="loading-text-hint">正在加载精彩内容</div>
<div class="spinner-dots"> <div class="loading-dots">
<span class="dot"></span> <span class="loading-dot"></span>
<span class="dot"></span> <span class="loading-dot"></span>
<span class="dot"></span> <span class="loading-dot"></span>
</div> </div>
</div> </div>
</div> </div>
`; `;
document.body.appendChild(el);
} }
el.classList.remove('is-hiding');
el.classList.add('is-visible'); /**
* 创建加载动画元素
* @returns {HTMLElement} 创建的元素
*/
function createElement() {
const el = document.createElement('div');
el.id = OVERLAY_ID;
el.className = 'loading-overlay';
el.innerHTML = createSkeletonHTML();
return el;
}
/**
* 获取或创建加载动画元素
* @returns {HTMLElement} 加载动画元素
*/
function getElement() {
if (!overlayElement) {
overlayElement = document.getElementById(OVERLAY_ID);
}
if (!overlayElement) {
overlayElement = createElement();
document.body.appendChild(overlayElement);
}
return overlayElement;
}
/**
* 显示加载动画
*/
function show() {
// 清除可能存在的隐藏定时器
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
const el = getElement();
el.classList.remove(CLASS_HIDING);
// 强制重排以确保动画生效
void el.offsetWidth;
el.classList.add(CLASS_VISIBLE);
}
/**
* 隐藏加载动画
*/
function hide() {
const el = document.getElementById(OVERLAY_ID);
if (!el) return;
el.classList.add(CLASS_HIDING);
// 动画结束后清理状态
hideTimer = setTimeout(function() {
el.classList.remove(CLASS_VISIBLE, CLASS_HIDING);
hideTimer = null;
}, ANIMATION_DURATION);
}
/**
* 销毁加载动画元素
*/
function destroy() {
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
}
if (overlayElement && overlayElement.parentNode) {
overlayElement.parentNode.removeChild(overlayElement);
overlayElement = null;
}
}
// 公开 API
return {
show: show,
hide: hide,
destroy: destroy
};
})();
/**
* 显示页面加载动画(向后兼容)
*/
function showLoadingOverlay() {
LoadingOverlay.show();
} }
/** /**
* 隐藏页面加载动画遮罩 * 隐藏页面加载动画(向后兼容)
*/ */
function hideLoadingOverlay() { function hideLoadingOverlay() {
let el = document.getElementById('article-loading-overlay'); LoadingOverlay.hide();
if (!el) return;
el.classList.add('is-hiding');
setTimeout(function() {
el.classList.remove('is-visible');
el.classList.remove('is-hiding');
}, 300);
} }
function startPageTransition() { function startPageTransition() {
document.documentElement.classList.add('page-transition-enter'); document.documentElement.classList.add('page-transition-enter');
@@ -3479,7 +3573,7 @@ $(document).on("click", ".reference-link , .reference-list-backlink" , function(
}, 1); }, 1);
}); });
/*Tags Dialog pjax 加载后自动关闭/ /*Tags Dialog pjax 加载后自动关闭 */
$(document).on("click" , "#blog_tags .tag" , function(){ $(document).on("click" , "#blog_tags .tag" , function(){
$("#blog_tags button.close").trigger("click"); $("#blog_tags button.close").trigger("click");
}); });

185
style.css
View File

@@ -17047,13 +17047,39 @@ article img.loaded, .post-thumbnail img.loaded, article img:not([loading="lazy"]
/* 11. 骨架屏和加载动画 */ /* 11. 骨架屏和加载动画 */
@keyframes modernSkeletonPulse { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } } @keyframes modernSkeletonPulse { 0% { background-position: -200% 0; } 100% { background-position: 200% 0; } }
.skeleton { background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%); background-size: 200% 100%; animation: modernSkeletonPulse 1.5s ease-in-out infinite; border-radius: var(--card-radius); } .skeleton { background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%); background-size: 200% 100%; animation: modernSkeletonPulse 1.5s ease-in-out infinite; border-radius: var(--card-radius); }
/* ---------- 加载动画 ---------- */ /* ==========================================================================
@keyframes modernSpinnerRotate { to { transform: rotate(360deg); } } 页面加载动画
.loading-spinner { width: 24px; height: 24px; border: 2px solid var(--color-border); border-top-color: var(--themecolor); border-radius: 50%; animation: modernSpinnerRotate 0.8s linear infinite; } ========================================================================== */
#page-loading-bar { position: fixed; top: 0; left: 0; height: 3px; background: var(--themecolor-gradient); z-index: 9999; transition: width var(--animation-fast) var(--ease-out-expo); box-shadow: 0 0 10px rgba(var(--themecolor-rgbstr), 0.5); }
/* 加载遮罩层 */ /* ---------- 基础旋转器 ---------- */
#article-loading-overlay { @keyframes modernSpinnerRotate {
to {
transform: rotate(360deg);
}
}
.loading-spinner {
width: 24px;
height: 24px;
border: 2px solid var(--color-border);
border-top-color: var(--themecolor);
border-radius: 50%;
animation: modernSpinnerRotate 0.8s linear infinite;
}
/* ---------- 顶部进度条 ---------- */
#page-loading-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: var(--themecolor-gradient);
z-index: 9999;
transition: width var(--animation-fast) var(--ease-out-expo);
box-shadow: 0 0 10px rgba(var(--themecolor-rgbstr), 0.5);
}
/* ---------- 加载遮罩层 ---------- */
.loading-overlay {
position: fixed; position: fixed;
inset: 0; inset: 0;
z-index: 9998; z-index: 9998;
@@ -17068,149 +17094,170 @@ article img.loaded, .post-thumbnail img.loaded, article img:not([loading="lazy"]
-webkit-backdrop-filter: blur(var(--card-blur, 20px)) saturate(var(--card-saturate, 180%)); -webkit-backdrop-filter: blur(var(--card-blur, 20px)) saturate(var(--card-saturate, 180%));
backdrop-filter: blur(var(--card-blur, 20px)) saturate(var(--card-saturate, 180%)); backdrop-filter: blur(var(--card-blur, 20px)) saturate(var(--card-saturate, 180%));
} }
html.darkmode #article-loading-overlay { html.darkmode .loading-overlay {
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.5);
} }
#article-loading-overlay.is-visible { .loading-overlay.is-visible {
opacity: 1; opacity: 1;
visibility: visible; visibility: visible;
pointer-events: auto; pointer-events: auto;
transition: opacity var(--animation-normal) var(--ease-standard), visibility 0s; transition: opacity var(--animation-normal) var(--ease-standard), visibility 0s;
} }
#article-loading-overlay.is-hiding { .loading-overlay.is-hiding {
opacity: 0; opacity: 0;
visibility: visible; visibility: visible;
pointer-events: none; pointer-events: none;
transition: opacity var(--animation-normal) var(--ease-standard); transition: opacity var(--animation-normal) var(--ease-standard);
} }
/* 加载内容容器 */ /* ---------- 内容容器 ---------- */
#article-loading-overlay .overlay-content { .loading-overlay-content {
width: min(720px, 90vw); width: min(720px, 90vw);
opacity: 0; opacity: 0;
transform: translate3d(0, 12px, 0) scale(0.98); transform: translate3d(0, 12px, 0) scale(0.98);
transition: opacity var(--animation-normal) var(--ease-standard), transform var(--animation-normal) var(--ease-emphasized-decelerate); transition: opacity var(--animation-normal) var(--ease-standard), transform var(--animation-normal) var(--ease-emphasized-decelerate);
} }
#article-loading-overlay.is-visible .overlay-content { .loading-overlay.is-visible .loading-overlay-content {
opacity: 1; opacity: 1;
transform: translate3d(0, 0, 0) scale(1); transform: translate3d(0, 0, 0) scale(1);
} }
/* 卡片容器 */ /* ---------- 卡片容器 ---------- */
#article-loading-overlay .overlay-card { .loading-card {
background: var(--color-foreground); background: var(--color-foreground);
border-radius: var(--card-radius); border-radius: var(--card-radius);
overflow: hidden; overflow: hidden;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.08);
margin-bottom: 24px; margin-bottom: 24px;
} }
html.darkmode #article-loading-overlay .overlay-card { html.darkmode .loading-card {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
} }
/* 缩略图 */ /* ---------- 缩略图骨架 ---------- */
#article-loading-overlay .overlay-thumb { .loading-thumb {
position: relative; position: relative;
width: 100%; width: 100%;
height: 240px; height: 240px;
overflow: hidden; overflow: hidden;
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%;
animation: skeletonPulse 1.8s ease-in-out infinite;
} }
#article-loading-overlay .overlay-thumb-shimmer { .loading-shimmer {
position: absolute; position: absolute;
top: 0; top: 0;
left: -100%; left: -100%;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
animation: shimmer 2s infinite; animation: shimmerMove 2s infinite;
} }
html.darkmode #article-loading-overlay .overlay-thumb-shimmer { html.darkmode .loading-shimmer {
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent); background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
} }
@keyframes shimmer { @keyframes shimmerMove {
to { left: 100%; } to {
left: 100%;
}
}
@keyframes skeletonPulse {
0%, 100% {
background-position: 0% 0%;
}
50% {
background-position: 100% 0%;
}
} }
/* 卡片主体 */ /* ---------- 卡片主体 ---------- */
#article-loading-overlay .overlay-body { .loading-body {
padding: 24px; padding: 24px;
} }
/* 元信息区域 */ /* ---------- 元信息区域 ---------- */
#article-loading-overlay .overlay-meta { .loading-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 12px;
margin-bottom: 16px; margin-bottom: 16px;
} }
#article-loading-overlay .overlay-avatar { .loading-avatar {
width: 40px; width: 40px;
height: 40px; height: 40px;
border-radius: 50%; border-radius: 50%;
flex-shrink: 0; flex-shrink: 0;
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%;
animation: skeletonPulse 1.8s ease-in-out infinite;
} }
#article-loading-overlay .overlay-meta-text { .loading-meta-text {
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 6px; gap: 6px;
} }
#article-loading-overlay .overlay-meta-line { .loading-meta-line {
height: 12px; height: 12px;
border-radius: 6px; border-radius: 6px;
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%;
animation: skeletonPulse 1.8s ease-in-out infinite;
} }
/* 标题 */ /* ---------- 标题骨架 ---------- */
#article-loading-overlay .overlay-title { .loading-title {
height: 28px; height: 28px;
margin-bottom: 16px; margin-bottom: 16px;
border-radius: 14px; border-radius: 14px;
width: 85%; width: 85%;
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%;
animation: skeletonPulse 1.8s ease-in-out infinite;
} }
/* 文本行 */ /* ---------- 文本行骨架 ---------- */
#article-loading-overlay .overlay-text { .loading-text {
margin-bottom: 16px; margin-bottom: 16px;
} }
#article-loading-overlay .overlay-row { .loading-line {
height: 14px; height: 14px;
margin: 8px 0; margin: 8px 0;
border-radius: 7px; border-radius: 7px;
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%;
animation: skeletonPulse 1.8s ease-in-out infinite;
} }
/* 标签 */ /* ---------- 标签骨架 ---------- */
#article-loading-overlay .overlay-tags { .loading-tags {
display: flex; display: flex;
gap: 8px; gap: 8px;
flex-wrap: wrap; flex-wrap: wrap;
} }
#article-loading-overlay .overlay-tag { .loading-tag {
width: 60px; width: 60px;
height: 24px; height: 24px;
border-radius: 12px; border-radius: 12px;
}
/* 骨架屏动画 */
#article-loading-overlay .skeleton {
background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%); background: linear-gradient(90deg, var(--color-border-on-foreground) 25%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 75%);
background-size: 200% 100%; background-size: 200% 100%;
animation: modernSkeletonPulse 1.8s ease-in-out infinite; animation: skeletonPulse 1.8s ease-in-out infinite;
} }
/* 加载旋转器 */ /* ---------- 加载旋转器 ---------- */
#article-loading-overlay .overlay-spinner { .loading-spinner-wrapper {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
gap: 16px; gap: 16px;
padding: 20px; padding: 20px;
} }
#article-loading-overlay .spinner-ring { .loading-spinner {
position: relative; position: relative;
width: 56px; width: 56px;
height: 56px; height: 56px;
} }
#article-loading-overlay .spinner-ring::before { .loading-spinner::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
@@ -17218,7 +17265,7 @@ html.darkmode #article-loading-overlay .overlay-thumb-shimmer {
border: 3px solid var(--color-border); border: 3px solid var(--color-border);
opacity: 0.2; opacity: 0.2;
} }
#article-loading-overlay .spinner-ring-inner { .loading-spinner-ring {
position: absolute; position: absolute;
inset: 0; inset: 0;
border-radius: 50%; border-radius: 50%;
@@ -17228,34 +17275,38 @@ html.darkmode #article-loading-overlay .overlay-thumb-shimmer {
animation: spinnerRotate 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite; animation: spinnerRotate 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
} }
@keyframes spinnerRotate { @keyframes spinnerRotate {
0% { transform: rotate(0deg); } 0% {
100% { transform: rotate(360deg); } transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
} }
/* 加载文字 */ /* ---------- 加载文字 ---------- */
#article-loading-overlay .spinner-text { .loading-text-hint {
color: var(--color-font); color: var(--color-font);
font-size: 15px; font-size: 15px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.3px; letter-spacing: 0.3px;
} }
/* 加载点动画 */ /* ---------- 加载点动画 ---------- */
#article-loading-overlay .spinner-dots { .loading-dots {
display: flex; display: flex;
gap: 6px; gap: 6px;
} }
#article-loading-overlay .spinner-dots .dot { .loading-dot {
width: 6px; width: 6px;
height: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: var(--themecolor); background: var(--themecolor);
animation: dotBounce 1.4s infinite ease-in-out both; animation: dotBounce 1.4s infinite ease-in-out both;
} }
#article-loading-overlay .spinner-dots .dot:nth-child(1) { .loading-dot:nth-child(1) {
animation-delay: -0.32s; animation-delay: -0.32s;
} }
#article-loading-overlay .spinner-dots .dot:nth-child(2) { .loading-dot:nth-child(2) {
animation-delay: -0.16s; animation-delay: -0.16s;
} }
@keyframes dotBounce { @keyframes dotBounce {
@@ -17269,40 +17320,40 @@ html.darkmode #article-loading-overlay .overlay-thumb-shimmer {
} }
} }
/* 响应式适配 */ /* ---------- 响应式适配 ---------- */
@media (max-width: 768px) { @media (max-width: 768px) {
#article-loading-overlay .overlay-thumb { .loading-thumb {
height: 180px; height: 180px;
} }
#article-loading-overlay .overlay-body { .loading-body {
padding: 20px; padding: 20px;
} }
#article-loading-overlay .overlay-title { .loading-title {
height: 24px; height: 24px;
width: 90%; width: 90%;
} }
#article-loading-overlay .spinner-ring { .loading-spinner {
width: 48px; width: 48px;
height: 48px; height: 48px;
} }
#article-loading-overlay .spinner-text { .loading-text-hint {
font-size: 14px; font-size: 14px;
} }
} }
@media (max-width: 480px) { @media (max-width: 480px) {
#article-loading-overlay .overlay-content { .loading-overlay-content {
width: 95vw; width: 95vw;
} }
#article-loading-overlay .overlay-thumb { .loading-thumb {
height: 160px; height: 160px;
} }
#article-loading-overlay .overlay-body { .loading-body {
padding: 16px; padding: 16px;
} }
#article-loading-overlay .overlay-meta { .loading-meta {
gap: 10px; gap: 10px;
} }
#article-loading-overlay .overlay-avatar { .loading-avatar {
width: 36px; width: 36px;
height: 36px; height: 36px;
} }