319 lines
11 KiB
JavaScript
319 lines
11 KiB
JavaScript
|
|
/**
|
||
|
|
* Argon Theme - Modern UI Enhancements JavaScript
|
||
|
|
* 现代化交互动画增强
|
||
|
|
*/
|
||
|
|
|
||
|
|
(function() {
|
||
|
|
'use strict';
|
||
|
|
|
||
|
|
// ========== 1. 触摸涟漪效果 ==========
|
||
|
|
function createRipple(event, element) {
|
||
|
|
// 只在移动端或触摸设备上启用
|
||
|
|
if (window.matchMedia('(hover: hover)').matches) return;
|
||
|
|
|
||
|
|
const rect = element.getBoundingClientRect();
|
||
|
|
const ripple = document.createElement('span');
|
||
|
|
const size = Math.max(rect.width, rect.height);
|
||
|
|
|
||
|
|
ripple.style.width = ripple.style.height = size + 'px';
|
||
|
|
ripple.style.left = (event.clientX || event.touches[0].clientX) - rect.left - size / 2 + 'px';
|
||
|
|
ripple.style.top = (event.clientY || event.touches[0].clientY) - rect.top - size / 2 + 'px';
|
||
|
|
ripple.className = 'touch-ripple';
|
||
|
|
|
||
|
|
// 移除旧的涟漪
|
||
|
|
const oldRipple = element.querySelector('.touch-ripple');
|
||
|
|
if (oldRipple) oldRipple.remove();
|
||
|
|
|
||
|
|
element.style.position = 'relative';
|
||
|
|
element.style.overflow = 'hidden';
|
||
|
|
element.appendChild(ripple);
|
||
|
|
|
||
|
|
// 动画结束后移除
|
||
|
|
setTimeout(() => ripple.remove(), 600);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 为可点击元素添加涟漪效果
|
||
|
|
function initRippleEffect() {
|
||
|
|
const rippleElements = document.querySelectorAll(
|
||
|
|
'.btn, .card, .nav-link, .dropdown-item, .page-link, ' +
|
||
|
|
'.leftbar-mobile-menu-item > a, .leftbar-mobile-action, ' +
|
||
|
|
'.fabtn, .comment-reply, .tag, .badge'
|
||
|
|
);
|
||
|
|
|
||
|
|
rippleElements.forEach(el => {
|
||
|
|
if (el.dataset.rippleInit) return;
|
||
|
|
el.dataset.rippleInit = 'true';
|
||
|
|
|
||
|
|
el.addEventListener('touchstart', function(e) {
|
||
|
|
createRipple(e, this);
|
||
|
|
}, { passive: true });
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 2. 图片加载动画 ==========
|
||
|
|
function initImageLoadAnimation() {
|
||
|
|
const images = document.querySelectorAll('article img[loading="lazy"], .post-thumbnail img');
|
||
|
|
|
||
|
|
images.forEach(img => {
|
||
|
|
if (img.dataset.loadAnimInit) return;
|
||
|
|
img.dataset.loadAnimInit = 'true';
|
||
|
|
|
||
|
|
if (img.complete) {
|
||
|
|
img.classList.add('loaded');
|
||
|
|
} else {
|
||
|
|
img.addEventListener('load', function() {
|
||
|
|
this.classList.add('loaded');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 3. 滚动入场动画 (Intersection Observer) ==========
|
||
|
|
function initScrollAnimations() {
|
||
|
|
if (!('IntersectionObserver' in window)) return;
|
||
|
|
|
||
|
|
const animatedElements = document.querySelectorAll(
|
||
|
|
'.article-list article.post, .comment-item, .timeline-item, ' +
|
||
|
|
'.friend-link-item, #leftbar .card, #rightbar .card'
|
||
|
|
);
|
||
|
|
|
||
|
|
const observer = new IntersectionObserver((entries) => {
|
||
|
|
entries.forEach(entry => {
|
||
|
|
if (entry.isIntersecting) {
|
||
|
|
entry.target.classList.add('animate-in');
|
||
|
|
observer.unobserve(entry.target);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}, {
|
||
|
|
threshold: 0.1,
|
||
|
|
rootMargin: '0px 0px -50px 0px'
|
||
|
|
});
|
||
|
|
|
||
|
|
animatedElements.forEach(el => {
|
||
|
|
if (!el.classList.contains('animate-in')) {
|
||
|
|
observer.observe(el);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 4. 平滑滚动增强 ==========
|
||
|
|
function initSmoothScroll() {
|
||
|
|
document.querySelectorAll('a[href^="#"]').forEach(anchor => {
|
||
|
|
if (anchor.dataset.smoothScrollInit) return;
|
||
|
|
anchor.dataset.smoothScrollInit = 'true';
|
||
|
|
|
||
|
|
anchor.addEventListener('click', function(e) {
|
||
|
|
const targetId = this.getAttribute('href');
|
||
|
|
if (targetId === '#') return;
|
||
|
|
|
||
|
|
const target = document.querySelector(targetId);
|
||
|
|
if (target) {
|
||
|
|
e.preventDefault();
|
||
|
|
target.scrollIntoView({
|
||
|
|
behavior: 'smooth',
|
||
|
|
block: 'start'
|
||
|
|
});
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 5. 页面加载进度条 ==========
|
||
|
|
function initLoadingBar() {
|
||
|
|
// 检查是否已存在
|
||
|
|
if (document.getElementById('page-loading-bar')) return;
|
||
|
|
|
||
|
|
const bar = document.createElement('div');
|
||
|
|
bar.id = 'page-loading-bar';
|
||
|
|
bar.style.width = '0%';
|
||
|
|
document.body.appendChild(bar);
|
||
|
|
|
||
|
|
// 模拟加载进度
|
||
|
|
let progress = 0;
|
||
|
|
const interval = setInterval(() => {
|
||
|
|
progress += Math.random() * 10;
|
||
|
|
if (progress >= 90) {
|
||
|
|
clearInterval(interval);
|
||
|
|
progress = 90;
|
||
|
|
}
|
||
|
|
bar.style.width = progress + '%';
|
||
|
|
}, 100);
|
||
|
|
|
||
|
|
// 页面加载完成
|
||
|
|
window.addEventListener('load', () => {
|
||
|
|
clearInterval(interval);
|
||
|
|
bar.style.width = '100%';
|
||
|
|
setTimeout(() => {
|
||
|
|
bar.style.opacity = '0';
|
||
|
|
setTimeout(() => bar.remove(), 300);
|
||
|
|
}, 200);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 6. PJAX 加载动画 ==========
|
||
|
|
function initPjaxAnimations() {
|
||
|
|
if (typeof jQuery === 'undefined') return;
|
||
|
|
|
||
|
|
jQuery(document).on('pjax:start', function() {
|
||
|
|
jQuery('#primary').addClass('pjax-loading');
|
||
|
|
|
||
|
|
// 显示加载进度条
|
||
|
|
let bar = document.getElementById('page-loading-bar');
|
||
|
|
if (!bar) {
|
||
|
|
bar = document.createElement('div');
|
||
|
|
bar.id = 'page-loading-bar';
|
||
|
|
document.body.appendChild(bar);
|
||
|
|
}
|
||
|
|
bar.style.opacity = '1';
|
||
|
|
bar.style.width = '30%';
|
||
|
|
|
||
|
|
setTimeout(() => bar.style.width = '60%', 200);
|
||
|
|
});
|
||
|
|
|
||
|
|
jQuery(document).on('pjax:end', function() {
|
||
|
|
jQuery('#primary').removeClass('pjax-loading');
|
||
|
|
|
||
|
|
const bar = document.getElementById('page-loading-bar');
|
||
|
|
if (bar) {
|
||
|
|
bar.style.width = '100%';
|
||
|
|
setTimeout(() => {
|
||
|
|
bar.style.opacity = '0';
|
||
|
|
setTimeout(() => bar.remove(), 300);
|
||
|
|
}, 200);
|
||
|
|
}
|
||
|
|
|
||
|
|
// 重新初始化动画
|
||
|
|
setTimeout(() => {
|
||
|
|
initRippleEffect();
|
||
|
|
initImageLoadAnimation();
|
||
|
|
initScrollAnimations();
|
||
|
|
initSmoothScroll();
|
||
|
|
}, 100);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 7. 卡片倾斜效果 (桌面端) ==========
|
||
|
|
function initCardTiltEffect() {
|
||
|
|
if (!window.matchMedia('(hover: hover)').matches) return;
|
||
|
|
|
||
|
|
const cards = document.querySelectorAll('article.post.card');
|
||
|
|
|
||
|
|
cards.forEach(card => {
|
||
|
|
if (card.dataset.tiltInit) return;
|
||
|
|
card.dataset.tiltInit = 'true';
|
||
|
|
|
||
|
|
card.addEventListener('mousemove', function(e) {
|
||
|
|
const rect = this.getBoundingClientRect();
|
||
|
|
const x = e.clientX - rect.left;
|
||
|
|
const y = e.clientY - rect.top;
|
||
|
|
const centerX = rect.width / 2;
|
||
|
|
const centerY = rect.height / 2;
|
||
|
|
|
||
|
|
const rotateX = (y - centerY) / 20;
|
||
|
|
const rotateY = (centerX - x) / 20;
|
||
|
|
|
||
|
|
this.style.transform = `perspective(1000px) rotateX(${rotateX}deg) rotateY(${rotateY}deg) translateY(-6px)`;
|
||
|
|
});
|
||
|
|
|
||
|
|
card.addEventListener('mouseleave', function() {
|
||
|
|
this.style.transform = '';
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 8. 浮动按钮显示/隐藏动画 ==========
|
||
|
|
function initFabAnimation() {
|
||
|
|
const fab = document.getElementById('float_action_buttons');
|
||
|
|
if (!fab) return;
|
||
|
|
|
||
|
|
let lastScrollY = window.scrollY;
|
||
|
|
let ticking = false;
|
||
|
|
|
||
|
|
window.addEventListener('scroll', function() {
|
||
|
|
if (!ticking) {
|
||
|
|
window.requestAnimationFrame(function() {
|
||
|
|
const currentScrollY = window.scrollY;
|
||
|
|
|
||
|
|
// 向下滚动时隐藏,向上滚动时显示
|
||
|
|
if (currentScrollY > lastScrollY && currentScrollY > 300) {
|
||
|
|
fab.style.transform = 'translateY(100px)';
|
||
|
|
fab.style.opacity = '0';
|
||
|
|
} else {
|
||
|
|
fab.style.transform = '';
|
||
|
|
fab.style.opacity = '';
|
||
|
|
}
|
||
|
|
|
||
|
|
lastScrollY = currentScrollY;
|
||
|
|
ticking = false;
|
||
|
|
});
|
||
|
|
ticking = true;
|
||
|
|
}
|
||
|
|
}, { passive: true });
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 9. 主题切换动画增强 ==========
|
||
|
|
function initThemeTransition() {
|
||
|
|
// 监听暗色模式切换
|
||
|
|
const observer = new MutationObserver(function(mutations) {
|
||
|
|
mutations.forEach(function(mutation) {
|
||
|
|
if (mutation.attributeName === 'class') {
|
||
|
|
const html = document.documentElement;
|
||
|
|
if (html.classList.contains('darkmode') || !html.classList.contains('darkmode')) {
|
||
|
|
// 添加过渡类
|
||
|
|
html.classList.add('theme-transitioning');
|
||
|
|
setTimeout(() => {
|
||
|
|
html.classList.remove('theme-transitioning');
|
||
|
|
}, 300);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
});
|
||
|
|
});
|
||
|
|
|
||
|
|
observer.observe(document.documentElement, {
|
||
|
|
attributes: true,
|
||
|
|
attributeFilter: ['class']
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 10. 减少动画偏好检测 ==========
|
||
|
|
function checkReducedMotion() {
|
||
|
|
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
|
||
|
|
document.documentElement.classList.add('reduced-motion');
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ========== 初始化 ==========
|
||
|
|
function init() {
|
||
|
|
checkReducedMotion();
|
||
|
|
initRippleEffect();
|
||
|
|
initImageLoadAnimation();
|
||
|
|
initScrollAnimations();
|
||
|
|
initSmoothScroll();
|
||
|
|
initPjaxAnimations();
|
||
|
|
initThemeTransition();
|
||
|
|
|
||
|
|
// 桌面端特效
|
||
|
|
if (window.matchMedia('(hover: hover)').matches) {
|
||
|
|
// initCardTiltEffect(); // 可选:卡片倾斜效果
|
||
|
|
}
|
||
|
|
|
||
|
|
// 移动端特效
|
||
|
|
if (!window.matchMedia('(hover: hover)').matches) {
|
||
|
|
// initFabAnimation(); // 可选:浮动按钮滚动隐藏
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// DOM 加载完成后初始化
|
||
|
|
if (document.readyState === 'loading') {
|
||
|
|
document.addEventListener('DOMContentLoaded', init);
|
||
|
|
} else {
|
||
|
|
init();
|
||
|
|
}
|
||
|
|
|
||
|
|
// 首次加载时显示进度条
|
||
|
|
if (document.readyState !== 'complete') {
|
||
|
|
initLoadingBar();
|
||
|
|
}
|
||
|
|
|
||
|
|
})();
|