feat: 全新设计的现代化页面加载系统

设计亮点:
- SVG 圆环进度指示器,实时显示加载进度
- 智能进度算法:自动递增 + 缓动效果
- 中心旋转图标 + 脉冲动画
- 延迟显示骨架屏(避免快速加载时闪烁)
- 最小显示时间控制(400ms)防止闪烁

加载逻辑优化:
- 智能进度管理:0-90% 自动递增,完成时跳转 100%
- 缓动函数:越接近完成速度越慢,更自然
- 定时器管理:防止内存泄漏和状态冲突
- 骨架屏延迟 150ms 显示,快速加载不显示

视觉设计:
- 渐变背景 + 毛玻璃效果
- 弹性入场动画(scale + translateY)
- 流畅的光影扫过效果
- 完整的响应式适配
- 支持无障碍访问(prefers-reduced-motion)
This commit is contained in:
2026-01-27 16:52:11 +08:00
parent 73103ea853
commit 1eb5d85eaf
2 changed files with 432 additions and 301 deletions

View File

@@ -3200,64 +3200,69 @@ var pjaxContainers = pjaxContainerSelectors.filter(function(selector) {
});
// ==========================================================================
// 页面加载动画管理器
// 现代化页面加载系统
// ==========================================================================
/**
* 加载动画管理器
* 负责创建、显示和隐藏页面加载动画
* 页面加载管理器 - 提供智能加载动画和进度追踪
*/
const LoadingOverlay = (function() {
const OVERLAY_ID = 'article-loading-overlay';
const ANIMATION_DURATION = 300;
const CLASS_VISIBLE = 'is-visible';
const CLASS_HIDING = 'is-hiding';
const PageLoader = (function() {
// 配置常量
const CONFIG = {
OVERLAY_ID: 'page-loader',
MIN_DISPLAY_TIME: 400, // 最小显示时间(避免闪烁)
FADE_DURATION: 350, // 淡出动画时长
PROGRESS_STEP: 0.1, // 进度条步进
PROGRESS_INTERVAL: 200, // 进度更新间隔
SKELETON_DELAY: 150 // 骨架屏延迟显示
};
let overlayElement = null;
let hideTimer = null;
// 状态管理
let state = {
element: null,
isVisible: false,
startTime: 0,
progress: 0,
progressTimer: null,
hideTimer: null,
skeletonTimer: null
};
/**
* 创建骨架屏 HTML 结构
* @returns {string} HTML 字符串
* 创建加载动画 HTML
*/
function createSkeletonHTML() {
function createHTML() {
return `
<div class="loading-overlay-content">
<div class="loading-card">
<div class="loading-thumb">
<div class="loading-shimmer"></div>
</div>
<div class="loading-body">
<div class="loading-meta">
<div class="loading-avatar"></div>
<div class="loading-meta-text">
<div class="loading-meta-line" style="width: 120px"></div>
<div class="loading-meta-line" style="width: 80px"></div>
</div>
</div>
<div class="loading-title"></div>
<div class="loading-text">
<div class="loading-line" style="width: 95%"></div>
<div class="loading-line" style="width: 88%"></div>
<div class="loading-line" style="width: 92%"></div>
<div class="loading-line" style="width: 78%"></div>
</div>
<div class="loading-tags">
<div class="loading-tag"></div>
<div class="loading-tag"></div>
<div class="loading-tag"></div>
</div>
<div class="page-loader-backdrop"></div>
<div class="page-loader-content">
<!-- 进度环 -->
<div class="loader-ring-container">
<svg class="loader-ring" viewBox="0 0 100 100">
<circle class="loader-ring-bg" cx="50" cy="50" r="45"></circle>
<circle class="loader-ring-progress" cx="50" cy="50" r="45"></circle>
</svg>
<div class="loader-icon">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v4m0 12v4M4.93 4.93l2.83 2.83m8.48 8.48l2.83 2.83M2 12h4m12 0h4M4.93 19.07l2.83-2.83m8.48-8.48l2.83-2.83"/>
</svg>
</div>
</div>
<div class="loading-spinner-wrapper">
<div class="loading-spinner">
<div class="loading-spinner-ring"></div>
</div>
<div class="loading-text-hint">正在加载精彩内容</div>
<div class="loading-dots">
<span class="loading-dot"></span>
<span class="loading-dot"></span>
<span class="loading-dot"></span>
<!-- 加载文字 -->
<div class="loader-text">
<div class="loader-title">加载中</div>
<div class="loader-subtitle">正在为您准备内容</div>
</div>
<!-- 骨架屏(延迟显示) -->
<div class="loader-skeleton">
<div class="skeleton-card">
<div class="skeleton-image"></div>
<div class="skeleton-content">
<div class="skeleton-title"></div>
<div class="skeleton-line"></div>
<div class="skeleton-line short"></div>
</div>
</div>
</div>
</div>
@@ -3265,101 +3270,198 @@ const LoadingOverlay = (function() {
}
/**
* 创建加载动画元素
* @returns {HTMLElement} 创建的元素
* 创建加载元素
*/
function createElement() {
const el = document.createElement('div');
el.id = OVERLAY_ID;
el.className = 'loading-overlay';
el.innerHTML = createSkeletonHTML();
el.id = CONFIG.OVERLAY_ID;
el.className = 'page-loader';
el.innerHTML = createHTML();
return el;
}
/**
* 获取或创建加载动画元素
* @returns {HTMLElement} 加载动画元素
* 获取或创建元素
*/
function getElement() {
if (!overlayElement) {
overlayElement = document.getElementById(OVERLAY_ID);
if (!state.element) {
state.element = document.getElementById(CONFIG.OVERLAY_ID);
if (!state.element) {
state.element = createElement();
document.body.appendChild(state.element);
}
}
if (!overlayElement) {
overlayElement = createElement();
document.body.appendChild(overlayElement);
}
return overlayElement;
return state.element;
}
/**
* 显示加载动画
* 更新进度环
*/
function updateProgress(progress) {
const el = state.element;
if (!el) return;
const circle = el.querySelector('.loader-ring-progress');
if (circle) {
const circumference = 2 * Math.PI * 45;
const offset = circumference - (progress / 100) * circumference;
circle.style.strokeDashoffset = offset;
}
state.progress = progress;
}
/**
* 自动递增进度
*/
function startProgressAnimation() {
stopProgressAnimation();
state.progress = 0;
updateProgress(0);
state.progressTimer = setInterval(function() {
if (state.progress < 90) {
// 使用缓动函数,越接近 90% 越慢
const increment = CONFIG.PROGRESS_STEP * (1 - state.progress / 100);
state.progress = Math.min(90, state.progress + increment * 10);
updateProgress(state.progress);
}
}, CONFIG.PROGRESS_INTERVAL);
}
/**
* 停止进度动画
*/
function stopProgressAnimation() {
if (state.progressTimer) {
clearInterval(state.progressTimer);
state.progressTimer = null;
}
}
/**
* 完成进度到 100%
*/
function completeProgress() {
stopProgressAnimation();
updateProgress(100);
}
/**
* 显示加载器
*/
function show() {
// 清除可能存在的隐藏定时器
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
// 清理之前的定时器
if (state.hideTimer) {
clearTimeout(state.hideTimer);
state.hideTimer = null;
}
if (state.skeletonTimer) {
clearTimeout(state.skeletonTimer);
state.skeletonTimer = null;
}
const el = getElement();
el.classList.remove(CLASS_HIDING);
state.startTime = Date.now();
state.isVisible = true;
// 强制重排以确保动画生效
void el.offsetWidth;
// 移除隐藏类,添加显示类
el.classList.remove('is-hiding');
void el.offsetWidth; // 强制重排
el.classList.add('is-visible');
el.classList.add(CLASS_VISIBLE);
// 启动进度动画
startProgressAnimation();
// 延迟显示骨架屏(避免快速加载时闪烁)
state.skeletonTimer = setTimeout(function() {
if (state.isVisible && el) {
el.classList.add('show-skeleton');
}
}, CONFIG.SKELETON_DELAY);
}
/**
* 隐藏加载动画
* 隐藏加载
*/
function hide() {
const el = document.getElementById(OVERLAY_ID);
if (!state.isVisible) return;
const el = state.element;
if (!el) return;
el.classList.add(CLASS_HIDING);
// 完成进度
completeProgress();
// 动画结束后清理状态
hideTimer = setTimeout(function() {
el.classList.remove(CLASS_VISIBLE, CLASS_HIDING);
hideTimer = null;
}, ANIMATION_DURATION);
// 计算已显示时间
const elapsedTime = Date.now() - state.startTime;
const remainingTime = Math.max(0, CONFIG.MIN_DISPLAY_TIME - elapsedTime);
// 确保最小显示时间后再隐藏
state.hideTimer = setTimeout(function() {
el.classList.add('is-hiding');
el.classList.remove('show-skeleton');
// 动画结束后清理
setTimeout(function() {
el.classList.remove('is-visible', 'is-hiding');
state.isVisible = false;
stopProgressAnimation();
}, CONFIG.FADE_DURATION);
}, remainingTime);
}
/**
* 销毁加载动画元素
* 设置进度(手动控制)
*/
function setProgress(progress) {
progress = Math.max(0, Math.min(100, progress));
stopProgressAnimation();
updateProgress(progress);
}
/**
* 销毁加载器
*/
function destroy() {
if (hideTimer) {
clearTimeout(hideTimer);
hideTimer = null;
stopProgressAnimation();
if (state.hideTimer) {
clearTimeout(state.hideTimer);
state.hideTimer = null;
}
if (overlayElement && overlayElement.parentNode) {
overlayElement.parentNode.removeChild(overlayElement);
overlayElement = null;
if (state.skeletonTimer) {
clearTimeout(state.skeletonTimer);
state.skeletonTimer = null;
}
if (state.element && state.element.parentNode) {
state.element.parentNode.removeChild(state.element);
}
state.element = null;
state.isVisible = false;
}
// 公开 API
return {
show: show,
hide: hide,
setProgress: setProgress,
destroy: destroy
};
})();
/**
* 显示页面加载动画(向后兼容
* 向后兼容的函数
*/
function showLoadingOverlay() {
LoadingOverlay.show();
PageLoader.show();
}
/**
* 隐藏页面加载动画(向后兼容)
*/
function hideLoadingOverlay() {
LoadingOverlay.hide();
PageLoader.hide();
}
function startPageTransition() {
document.documentElement.classList.add('page-transition-enter');