feat: 全新设计的现代化页面加载系统
设计亮点: - SVG 圆环进度指示器,实时显示加载进度 - 智能进度算法:自动递增 + 缓动效果 - 中心旋转图标 + 脉冲动画 - 延迟显示骨架屏(避免快速加载时闪烁) - 最小显示时间控制(400ms)防止闪烁 加载逻辑优化: - 智能进度管理:0-90% 自动递增,完成时跳转 100% - 缓动函数:越接近完成速度越慢,更自然 - 定时器管理:防止内存泄漏和状态冲突 - 骨架屏延迟 150ms 显示,快速加载不显示 视觉设计: - 渐变背景 + 毛玻璃效果 - 弹性入场动画(scale + translateY) - 流畅的光影扫过效果 - 完整的响应式适配 - 支持无障碍访问(prefers-reduced-motion)
This commit is contained in:
278
argontheme.js
278
argontheme.js
@@ -3200,166 +3200,268 @@ var pjaxContainers = pjaxContainerSelectors.filter(function(selector) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// 页面加载动画管理器
|
// 现代化页面加载系统
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 加载动画管理器
|
* 页面加载管理器 - 提供智能加载动画和进度追踪
|
||||||
* 负责创建、显示和隐藏页面加载动画
|
|
||||||
*/
|
*/
|
||||||
const LoadingOverlay = (function() {
|
const PageLoader = (function() {
|
||||||
const OVERLAY_ID = 'article-loading-overlay';
|
// 配置常量
|
||||||
const ANIMATION_DURATION = 300;
|
const CONFIG = {
|
||||||
const CLASS_VISIBLE = 'is-visible';
|
OVERLAY_ID: 'page-loader',
|
||||||
const CLASS_HIDING = 'is-hiding';
|
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 结构
|
* 创建加载动画 HTML
|
||||||
* @returns {string} HTML 字符串
|
|
||||||
*/
|
*/
|
||||||
function createSkeletonHTML() {
|
function createHTML() {
|
||||||
return `
|
return `
|
||||||
<div class="loading-overlay-content">
|
<div class="page-loader-backdrop"></div>
|
||||||
<div class="loading-card">
|
<div class="page-loader-content">
|
||||||
<div class="loading-thumb">
|
<!-- 进度环 -->
|
||||||
<div class="loading-shimmer"></div>
|
<div class="loader-ring-container">
|
||||||
</div>
|
<svg class="loader-ring" viewBox="0 0 100 100">
|
||||||
<div class="loading-body">
|
<circle class="loader-ring-bg" cx="50" cy="50" r="45"></circle>
|
||||||
<div class="loading-meta">
|
<circle class="loader-ring-progress" cx="50" cy="50" r="45"></circle>
|
||||||
<div class="loading-avatar"></div>
|
</svg>
|
||||||
<div class="loading-meta-text">
|
<div class="loader-icon">
|
||||||
<div class="loading-meta-line" style="width: 120px"></div>
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
<div class="loading-meta-line" style="width: 80px"></div>
|
<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>
|
</div>
|
||||||
<div class="loading-title"></div>
|
|
||||||
<div class="loading-text">
|
<!-- 加载文字 -->
|
||||||
<div class="loading-line" style="width: 95%"></div>
|
<div class="loader-text">
|
||||||
<div class="loading-line" style="width: 88%"></div>
|
<div class="loader-title">加载中</div>
|
||||||
<div class="loading-line" style="width: 92%"></div>
|
<div class="loader-subtitle">正在为您准备内容</div>
|
||||||
<div class="loading-line" style="width: 78%"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="loading-tags">
|
|
||||||
<div class="loading-tag"></div>
|
<!-- 骨架屏(延迟显示) -->
|
||||||
<div class="loading-tag"></div>
|
<div class="loader-skeleton">
|
||||||
<div class="loading-tag"></div>
|
<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>
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 创建加载动画元素
|
* 创建加载器元素
|
||||||
* @returns {HTMLElement} 创建的元素
|
|
||||||
*/
|
*/
|
||||||
function createElement() {
|
function createElement() {
|
||||||
const el = document.createElement('div');
|
const el = document.createElement('div');
|
||||||
el.id = OVERLAY_ID;
|
el.id = CONFIG.OVERLAY_ID;
|
||||||
el.className = 'loading-overlay';
|
el.className = 'page-loader';
|
||||||
el.innerHTML = createSkeletonHTML();
|
el.innerHTML = createHTML();
|
||||||
return el;
|
return el;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取或创建加载动画元素
|
* 获取或创建元素
|
||||||
* @returns {HTMLElement} 加载动画元素
|
|
||||||
*/
|
*/
|
||||||
function getElement() {
|
function getElement() {
|
||||||
if (!overlayElement) {
|
if (!state.element) {
|
||||||
overlayElement = document.getElementById(OVERLAY_ID);
|
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() {
|
function show() {
|
||||||
// 清除可能存在的隐藏定时器
|
// 清理之前的定时器
|
||||||
if (hideTimer) {
|
if (state.hideTimer) {
|
||||||
clearTimeout(hideTimer);
|
clearTimeout(state.hideTimer);
|
||||||
hideTimer = null;
|
state.hideTimer = null;
|
||||||
|
}
|
||||||
|
if (state.skeletonTimer) {
|
||||||
|
clearTimeout(state.skeletonTimer);
|
||||||
|
state.skeletonTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const el = getElement();
|
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() {
|
function hide() {
|
||||||
const el = document.getElementById(OVERLAY_ID);
|
if (!state.isVisible) return;
|
||||||
|
|
||||||
|
const el = state.element;
|
||||||
if (!el) return;
|
if (!el) return;
|
||||||
|
|
||||||
el.classList.add(CLASS_HIDING);
|
// 完成进度
|
||||||
|
completeProgress();
|
||||||
|
|
||||||
// 动画结束后清理状态
|
// 计算已显示时间
|
||||||
hideTimer = setTimeout(function() {
|
const elapsedTime = Date.now() - state.startTime;
|
||||||
el.classList.remove(CLASS_VISIBLE, CLASS_HIDING);
|
const remainingTime = Math.max(0, CONFIG.MIN_DISPLAY_TIME - elapsedTime);
|
||||||
hideTimer = null;
|
|
||||||
}, ANIMATION_DURATION);
|
// 确保最小显示时间后再隐藏
|
||||||
|
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() {
|
function destroy() {
|
||||||
if (hideTimer) {
|
stopProgressAnimation();
|
||||||
clearTimeout(hideTimer);
|
|
||||||
hideTimer = null;
|
if (state.hideTimer) {
|
||||||
|
clearTimeout(state.hideTimer);
|
||||||
|
state.hideTimer = null;
|
||||||
}
|
}
|
||||||
if (overlayElement && overlayElement.parentNode) {
|
if (state.skeletonTimer) {
|
||||||
overlayElement.parentNode.removeChild(overlayElement);
|
clearTimeout(state.skeletonTimer);
|
||||||
overlayElement = null;
|
state.skeletonTimer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.element && state.element.parentNode) {
|
||||||
|
state.element.parentNode.removeChild(state.element);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.element = null;
|
||||||
|
state.isVisible = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 公开 API
|
// 公开 API
|
||||||
return {
|
return {
|
||||||
show: show,
|
show: show,
|
||||||
hide: hide,
|
hide: hide,
|
||||||
|
setProgress: setProgress,
|
||||||
destroy: destroy
|
destroy: destroy
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示页面加载动画(向后兼容)
|
* 向后兼容的函数
|
||||||
*/
|
*/
|
||||||
function showLoadingOverlay() {
|
function showLoadingOverlay() {
|
||||||
LoadingOverlay.show();
|
PageLoader.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 隐藏页面加载动画(向后兼容)
|
|
||||||
*/
|
|
||||||
function hideLoadingOverlay() {
|
function hideLoadingOverlay() {
|
||||||
LoadingOverlay.hide();
|
PageLoader.hide();
|
||||||
}
|
}
|
||||||
function startPageTransition() {
|
function startPageTransition() {
|
||||||
document.documentElement.classList.add('page-transition-enter');
|
document.documentElement.classList.add('page-transition-enter');
|
||||||
|
|||||||
449
style.css
449
style.css
@@ -17048,10 +17048,10 @@ article img.loaded, .post-thumbnail img.loaded, article img:not([loading="lazy"]
|
|||||||
@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 {
|
@keyframes modernSpinnerRotate {
|
||||||
to {
|
to {
|
||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
@@ -17078,90 +17078,220 @@ article img.loaded, .post-thumbnail img.loaded, article img:not([loading="lazy"]
|
|||||||
box-shadow: 0 0 10px rgba(var(--themecolor-rgbstr), 0.5);
|
box-shadow: 0 0 10px rgba(var(--themecolor-rgbstr), 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 加载遮罩层 ---------- */
|
/* ---------- 页面加载器容器 ---------- */
|
||||||
.loading-overlay {
|
.page-loader {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
inset: 0;
|
inset: 0;
|
||||||
z-index: 9998;
|
z-index: 9998;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: rgba(255, 255, 255, 0.3);
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
transition: opacity var(--animation-normal) var(--ease-standard), visibility 0s linear var(--animation-normal);
|
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s linear 0.35s;
|
||||||
-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%));
|
|
||||||
}
|
}
|
||||||
html.darkmode .loading-overlay {
|
.page-loader.is-visible {
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
.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 0.35s cubic-bezier(0.4, 0, 0.2, 1), visibility 0s;
|
||||||
}
|
}
|
||||||
.loading-overlay.is-hiding {
|
.page-loader.is-hiding {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
visibility: visible;
|
transition: opacity 0.35s cubic-bezier(0.4, 0, 0.6, 1);
|
||||||
pointer-events: none;
|
}
|
||||||
transition: opacity var(--animation-normal) var(--ease-standard);
|
|
||||||
|
/* ---------- 背景遮罩 ---------- */
|
||||||
|
.page-loader-backdrop {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: linear-gradient(135deg, rgba(var(--themecolor-rgbstr), 0.03) 0%, rgba(var(--themecolor-rgbstr), 0.08) 100%);
|
||||||
|
-webkit-backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
backdrop-filter: blur(24px) saturate(180%);
|
||||||
|
}
|
||||||
|
html.darkmode .page-loader-backdrop {
|
||||||
|
background: linear-gradient(135deg, rgba(0, 0, 0, 0.4) 0%, rgba(0, 0, 0, 0.6) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 内容容器 ---------- */
|
/* ---------- 内容容器 ---------- */
|
||||||
.loading-overlay-content {
|
.page-loader-content {
|
||||||
width: min(720px, 90vw);
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 32px;
|
||||||
|
transform: translateY(20px) scale(0.95);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translate3d(0, 12px, 0) scale(0.98);
|
transition: transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.4s ease;
|
||||||
transition: opacity var(--animation-normal) var(--ease-standard), transform var(--animation-normal) var(--ease-emphasized-decelerate);
|
|
||||||
}
|
}
|
||||||
.loading-overlay.is-visible .loading-overlay-content {
|
.page-loader.is-visible .page-loader-content {
|
||||||
|
transform: translateY(0) scale(1);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: translate3d(0, 0, 0) scale(1);
|
transition-delay: 0.1s;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 卡片容器 ---------- */
|
/* ---------- 进度环容器 ---------- */
|
||||||
.loading-card {
|
.loader-ring-container {
|
||||||
|
position: relative;
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- SVG 进度环 ---------- */
|
||||||
|
.loader-ring {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
filter: drop-shadow(0 4px 12px rgba(var(--themecolor-rgbstr), 0.2));
|
||||||
|
}
|
||||||
|
.loader-ring-bg {
|
||||||
|
fill: none;
|
||||||
|
stroke: var(--color-border);
|
||||||
|
stroke-width: 3;
|
||||||
|
opacity: 0.2;
|
||||||
|
}
|
||||||
|
.loader-ring-progress {
|
||||||
|
fill: none;
|
||||||
|
stroke: url(#loaderGradient);
|
||||||
|
stroke-width: 3;
|
||||||
|
stroke-linecap: round;
|
||||||
|
stroke-dasharray: 283;
|
||||||
|
stroke-dashoffset: 283;
|
||||||
|
transition: stroke-dashoffset 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 渐变定义(通过 JS 动态添加或使用内联 SVG) */
|
||||||
|
.loader-ring::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: conic-gradient(from 0deg, var(--themecolor), transparent);
|
||||||
|
opacity: 0.1;
|
||||||
|
animation: ringGlow 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
@keyframes ringGlow {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 中心图标 ---------- */
|
||||||
|
.loader-icon {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
color: var(--themecolor);
|
||||||
|
animation: iconPulse 2s ease-in-out infinite;
|
||||||
|
}
|
||||||
|
.loader-icon svg {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
animation: iconRotate 3s linear infinite;
|
||||||
|
}
|
||||||
|
@keyframes iconPulse {
|
||||||
|
0%, 100% {
|
||||||
|
opacity: 0.6;
|
||||||
|
transform: translate(-50%, -50%) scale(1);
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%) scale(1.1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@keyframes iconRotate {
|
||||||
|
to {
|
||||||
|
transform: rotate(360deg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 加载文字 ---------- */
|
||||||
|
.loader-text {
|
||||||
|
text-align: center;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
animation: textFadeIn 0.5s ease forwards;
|
||||||
|
animation-delay: 0.3s;
|
||||||
|
}
|
||||||
|
@keyframes textFadeIn {
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.loader-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--color-font);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
.loader-subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: var(--color-font-sub);
|
||||||
|
font-weight: 400;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 骨架屏(延迟显示) ---------- */
|
||||||
|
.loader-skeleton {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: opacity 0.4s ease, transform 0.4s cubic-bezier(0.34, 1.56, 0.64, 1);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.page-loader.show-skeleton .loader-skeleton {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 骨架卡片 ---------- */
|
||||||
|
.skeleton-card {
|
||||||
|
width: min(480px, 85vw);
|
||||||
background: var(--color-foreground);
|
background: var(--color-foreground);
|
||||||
border-radius: var(--card-radius);
|
border-radius: 16px;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
html.darkmode .loading-card {
|
html.darkmode .skeleton-card {
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 缩略图骨架 ---------- */
|
/* ---------- 骨架图片 ---------- */
|
||||||
.loading-thumb {
|
.skeleton-image {
|
||||||
position: relative;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 240px;
|
height: 200px;
|
||||||
overflow: hidden;
|
background: linear-gradient(90deg, var(--color-border-on-foreground) 0%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 100%);
|
||||||
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: skeletonPulse 1.8s ease-in-out infinite;
|
animation: skeletonShimmer 1.5s ease-in-out infinite;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
.loading-shimmer {
|
.skeleton-image::after {
|
||||||
|
content: '';
|
||||||
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.2), transparent);
|
||||||
animation: shimmerMove 2s infinite;
|
animation: shimmerSlide 2s infinite;
|
||||||
}
|
}
|
||||||
html.darkmode .loading-shimmer {
|
html.darkmode .skeleton-image::after {
|
||||||
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.1), transparent);
|
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.05), transparent);
|
||||||
}
|
}
|
||||||
@keyframes shimmerMove {
|
@keyframes shimmerSlide {
|
||||||
to {
|
to {
|
||||||
left: 100%;
|
left: 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@keyframes skeletonPulse {
|
@keyframes skeletonShimmer {
|
||||||
0%, 100% {
|
0%, 100% {
|
||||||
background-position: 0% 0%;
|
background-position: 0% 0%;
|
||||||
}
|
}
|
||||||
@@ -17170,193 +17300,92 @@ html.darkmode .loading-shimmer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 卡片主体 ---------- */
|
/* ---------- 骨架内容 ---------- */
|
||||||
.loading-body {
|
.skeleton-content {
|
||||||
padding: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 元信息区域 ---------- */
|
|
||||||
.loading-meta {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.loading-avatar {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border-radius: 50%;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
.loading-meta-text {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.loading-meta-line {
|
|
||||||
height: 12px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 标题骨架 ---------- */
|
|
||||||
.loading-title {
|
|
||||||
height: 28px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
border-radius: 14px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 文本行骨架 ---------- */
|
|
||||||
.loading-text {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.loading-line {
|
|
||||||
height: 14px;
|
|
||||||
margin: 8px 0;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 标签骨架 ---------- */
|
|
||||||
.loading-tags {
|
|
||||||
display: flex;
|
|
||||||
gap: 8px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
.loading-tag {
|
|
||||||
width: 60px;
|
|
||||||
height: 24px;
|
|
||||||
border-radius: 12px;
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 加载旋转器 ---------- */
|
|
||||||
.loading-spinner-wrapper {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
padding: 20px;
|
padding: 20px;
|
||||||
}
|
}
|
||||||
.loading-spinner {
|
.skeleton-title {
|
||||||
position: relative;
|
height: 24px;
|
||||||
width: 56px;
|
width: 70%;
|
||||||
height: 56px;
|
border-radius: 12px;
|
||||||
|
background: linear-gradient(90deg, var(--color-border-on-foreground) 0%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 100%);
|
||||||
|
background-size: 200% 100%;
|
||||||
|
animation: skeletonShimmer 1.5s ease-in-out infinite;
|
||||||
|
margin-bottom: 16px;
|
||||||
}
|
}
|
||||||
.loading-spinner::before {
|
.skeleton-line {
|
||||||
content: '';
|
height: 14px;
|
||||||
position: absolute;
|
border-radius: 7px;
|
||||||
inset: 0;
|
background: linear-gradient(90deg, var(--color-border-on-foreground) 0%, var(--color-border-on-foreground-deeper) 50%, var(--color-border-on-foreground) 100%);
|
||||||
border-radius: 50%;
|
background-size: 200% 100%;
|
||||||
border: 3px solid var(--color-border);
|
animation: skeletonShimmer 1.5s ease-in-out infinite;
|
||||||
opacity: 0.2;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
.loading-spinner-ring {
|
.skeleton-line.short {
|
||||||
position: absolute;
|
width: 60%;
|
||||||
inset: 0;
|
|
||||||
border-radius: 50%;
|
|
||||||
border: 3px solid transparent;
|
|
||||||
border-top-color: var(--themecolor);
|
|
||||||
border-right-color: var(--themecolor);
|
|
||||||
animation: spinnerRotate 1s cubic-bezier(0.68, -0.55, 0.265, 1.55) infinite;
|
|
||||||
}
|
|
||||||
@keyframes spinnerRotate {
|
|
||||||
0% {
|
|
||||||
transform: rotate(0deg);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: rotate(360deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 加载文字 ---------- */
|
|
||||||
.loading-text-hint {
|
|
||||||
color: var(--color-font);
|
|
||||||
font-size: 15px;
|
|
||||||
font-weight: 500;
|
|
||||||
letter-spacing: 0.3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* ---------- 加载点动画 ---------- */
|
|
||||||
.loading-dots {
|
|
||||||
display: flex;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.loading-dot {
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: var(--themecolor);
|
|
||||||
animation: dotBounce 1.4s infinite ease-in-out both;
|
|
||||||
}
|
|
||||||
.loading-dot:nth-child(1) {
|
|
||||||
animation-delay: -0.32s;
|
|
||||||
}
|
|
||||||
.loading-dot:nth-child(2) {
|
|
||||||
animation-delay: -0.16s;
|
|
||||||
}
|
|
||||||
@keyframes dotBounce {
|
|
||||||
0%, 80%, 100% {
|
|
||||||
transform: scale(0.6);
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
40% {
|
|
||||||
transform: scale(1);
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ---------- 响应式适配 ---------- */
|
/* ---------- 响应式适配 ---------- */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.loading-thumb {
|
.loader-ring-container {
|
||||||
height: 180px;
|
width: 100px;
|
||||||
|
height: 100px;
|
||||||
}
|
}
|
||||||
.loading-body {
|
.loader-icon {
|
||||||
padding: 20px;
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
}
|
}
|
||||||
.loading-title {
|
.loader-title {
|
||||||
height: 24px;
|
font-size: 18px;
|
||||||
width: 90%;
|
|
||||||
}
|
}
|
||||||
.loading-spinner {
|
.loader-subtitle {
|
||||||
width: 48px;
|
font-size: 13px;
|
||||||
height: 48px;
|
|
||||||
}
|
}
|
||||||
.loading-text-hint {
|
.skeleton-image {
|
||||||
font-size: 14px;
|
height: 160px;
|
||||||
|
}
|
||||||
|
.skeleton-content {
|
||||||
|
padding: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@media (max-width: 480px) {
|
@media (max-width: 480px) {
|
||||||
.loading-overlay-content {
|
.page-loader-content {
|
||||||
width: 95vw;
|
gap: 24px;
|
||||||
}
|
}
|
||||||
.loading-thumb {
|
.loader-ring-container {
|
||||||
height: 160px;
|
width: 90px;
|
||||||
|
height: 90px;
|
||||||
}
|
}
|
||||||
.loading-body {
|
.loader-icon {
|
||||||
padding: 16px;
|
|
||||||
}
|
|
||||||
.loading-meta {
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
.loading-avatar {
|
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
}
|
}
|
||||||
|
.skeleton-card {
|
||||||
|
width: 92vw;
|
||||||
|
}
|
||||||
|
.skeleton-image {
|
||||||
|
height: 140px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---------- 减少动画(无障碍) ---------- */
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
.page-loader,
|
||||||
|
.page-loader-content,
|
||||||
|
.loader-ring-progress,
|
||||||
|
.loader-icon,
|
||||||
|
.loader-text,
|
||||||
|
.loader-skeleton,
|
||||||
|
.skeleton-image,
|
||||||
|
.skeleton-title,
|
||||||
|
.skeleton-line {
|
||||||
|
animation: none !important;
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
.page-loader.is-visible .page-loader-content {
|
||||||
|
transform: none;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 页面过渡内容容器 */
|
/* 页面过渡内容容器 */
|
||||||
|
|||||||
Reference in New Issue
Block a user