6d4368fbd1
Features: - Modern UI with dark mode support - Code highlighting with copy button - Auto table of contents - Reading time & views counter - Responsive design - Admin settings page - AJAX search
335 lines
12 KiB
JavaScript
Executable File
335 lines
12 KiB
JavaScript
Executable File
/**
|
|
* TechBlog Pro - Main JavaScript
|
|
*
|
|
* @package TechBlog Pro
|
|
* @version 1.0.0
|
|
*/
|
|
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ==================== Theme Toggle ====================
|
|
const ThemeManager = {
|
|
init() {
|
|
this.toggle = document.getElementById('theme-toggle');
|
|
this.sunIcon = this.toggle?.querySelector('.sun-icon');
|
|
this.moonIcon = this.toggle?.querySelector('.moon-icon');
|
|
|
|
if (!this.toggle) return;
|
|
|
|
// Load saved theme
|
|
const savedTheme = localStorage.getItem('theme') || 'light';
|
|
this.setTheme(savedTheme);
|
|
|
|
// Toggle event
|
|
this.toggle.addEventListener('click', () => {
|
|
const current = document.documentElement.getAttribute('data-theme');
|
|
this.setTheme(current === 'dark' ? 'light' : 'dark');
|
|
});
|
|
},
|
|
|
|
setTheme(theme) {
|
|
document.documentElement.setAttribute('data-theme', theme);
|
|
localStorage.setItem('theme', theme);
|
|
|
|
if (this.sunIcon && this.moonIcon) {
|
|
this.sunIcon.style.display = theme === 'dark' ? 'none' : 'block';
|
|
this.moonIcon.style.display = theme === 'dark' ? 'block' : 'none';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== Search Overlay ====================
|
|
const SearchManager = {
|
|
init() {
|
|
this.overlay = document.getElementById('search-overlay');
|
|
this.toggle = document.getElementById('search-toggle');
|
|
this.input = this.overlay?.querySelector('.search-input');
|
|
|
|
if (!this.overlay || !this.toggle) return;
|
|
|
|
// Open search
|
|
this.toggle.addEventListener('click', () => this.open());
|
|
|
|
// Close on overlay click
|
|
this.overlay.addEventListener('click', (e) => {
|
|
if (e.target === this.overlay) this.close();
|
|
});
|
|
|
|
// Keyboard shortcuts
|
|
document.addEventListener('keydown', (e) => {
|
|
// CMD/CTRL + K to open
|
|
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
|
|
e.preventDefault();
|
|
this.open();
|
|
}
|
|
|
|
// ESC to close
|
|
if (e.key === 'Escape') {
|
|
this.close();
|
|
}
|
|
});
|
|
},
|
|
|
|
open() {
|
|
this.overlay.classList.add('active');
|
|
setTimeout(() => this.input?.focus(), 100);
|
|
document.body.style.overflow = 'hidden';
|
|
},
|
|
|
|
close() {
|
|
this.overlay.classList.remove('active');
|
|
document.body.style.overflow = '';
|
|
}
|
|
};
|
|
|
|
// ==================== Mobile Menu ====================
|
|
const MobileMenu = {
|
|
init() {
|
|
this.toggle = document.getElementById('mobile-menu-toggle');
|
|
this.nav = document.getElementById('main-nav');
|
|
|
|
if (!this.toggle || !this.nav) return;
|
|
|
|
this.toggle.addEventListener('click', () => {
|
|
this.nav.classList.toggle('active');
|
|
this.toggle.classList.toggle('active');
|
|
});
|
|
|
|
// Close on outside click
|
|
document.addEventListener('click', (e) => {
|
|
if (!this.nav.contains(e.target) && !this.toggle.contains(e.target)) {
|
|
this.nav.classList.remove('active');
|
|
this.toggle.classList.remove('active');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// ==================== Table of Contents ====================
|
|
const TOCManager = {
|
|
init() {
|
|
this.tocLinks = document.querySelectorAll('.toc-list a');
|
|
this.headings = [];
|
|
|
|
if (this.tocLinks.length === 0) return;
|
|
|
|
// Collect headings
|
|
this.tocLinks.forEach(link => {
|
|
const id = link.getAttribute('href')?.slice(1);
|
|
const heading = document.getElementById(id);
|
|
if (heading) {
|
|
this.headings.push({ id, element: heading, link });
|
|
}
|
|
});
|
|
|
|
// Scroll spy
|
|
this.observer = new IntersectionObserver(
|
|
(entries) => this.handleIntersection(entries),
|
|
{ rootMargin: '-80px 0px -80% 0px' }
|
|
);
|
|
|
|
this.headings.forEach(({ element }) => {
|
|
this.observer.observe(element);
|
|
});
|
|
|
|
// Smooth scroll
|
|
this.tocLinks.forEach(link => {
|
|
link.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
const id = link.getAttribute('href')?.slice(1);
|
|
const target = document.getElementById(id);
|
|
if (target) {
|
|
target.scrollIntoView({ behavior: 'smooth' });
|
|
}
|
|
});
|
|
});
|
|
},
|
|
|
|
handleIntersection(entries) {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
// Remove all active states
|
|
this.tocLinks.forEach(link => link.classList.remove('active'));
|
|
|
|
// Add active to current
|
|
const activeLink = this.headings.find(h => h.id === entry.target.id)?.link;
|
|
activeLink?.classList.add('active');
|
|
}
|
|
});
|
|
}
|
|
};
|
|
|
|
// ==================== Code Copy ====================
|
|
window.copyCode = function(button) {
|
|
const pre = button.closest('pre');
|
|
const code = pre?.querySelector('code');
|
|
|
|
if (!code) return;
|
|
|
|
const text = code.textContent;
|
|
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
const originalText = button.innerHTML;
|
|
button.innerHTML = `
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<polyline points="20 6 9 17 4 12"/>
|
|
</svg>
|
|
已复制
|
|
`;
|
|
button.style.color = '#22c55e';
|
|
|
|
setTimeout(() => {
|
|
button.innerHTML = originalText;
|
|
button.style.color = '';
|
|
}, 2000);
|
|
}).catch(err => {
|
|
console.error('Failed to copy:', err);
|
|
});
|
|
};
|
|
|
|
// ==================== Reading Progress ====================
|
|
const ReadingProgress = {
|
|
init() {
|
|
this.progressBar = document.createElement('div');
|
|
this.progressBar.style.cssText = `
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 0%;
|
|
height: 3px;
|
|
background: linear-gradient(90deg, var(--primary-color), var(--accent-color));
|
|
z-index: 9999;
|
|
transition: width 0.1s ease;
|
|
`;
|
|
document.body.appendChild(this.progressBar);
|
|
|
|
window.addEventListener('scroll', () => this.update());
|
|
},
|
|
|
|
update() {
|
|
const scrollTop = window.scrollY;
|
|
const docHeight = document.documentElement.scrollHeight - window.innerHeight;
|
|
const progress = (scrollTop / docHeight) * 100;
|
|
|
|
this.progressBar.style.width = `${Math.min(progress, 100)}%`;
|
|
}
|
|
};
|
|
|
|
// ==================== Lazy Load Images ====================
|
|
const LazyLoad = {
|
|
init() {
|
|
if ('loading' in HTMLImageElement.prototype) {
|
|
// Native lazy loading supported
|
|
document.querySelectorAll('img[loading="lazy"]').forEach(img => {
|
|
img.src = img.dataset.src;
|
|
});
|
|
} else {
|
|
// Fallback with IntersectionObserver
|
|
this.observer = new IntersectionObserver(
|
|
(entries) => {
|
|
entries.forEach(entry => {
|
|
if (entry.isIntersecting) {
|
|
const img = entry.target;
|
|
img.src = img.dataset.src;
|
|
this.observer.unobserve(img);
|
|
}
|
|
});
|
|
},
|
|
{ rootMargin: '50px' }
|
|
);
|
|
|
|
document.querySelectorAll('img[data-src]').forEach(img => {
|
|
this.observer.observe(img);
|
|
});
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== Copy Link ====================
|
|
window.copyLink = function() {
|
|
const url = window.location.href;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
const btn = document.querySelector('.share-copy');
|
|
btn.classList.add('copied');
|
|
btn.title = '已复制';
|
|
|
|
setTimeout(() => {
|
|
btn.classList.remove('copied');
|
|
btn.title = '复制链接';
|
|
}, 2000);
|
|
});
|
|
};
|
|
|
|
// ==================== Back to Top ====================
|
|
const BackToTop = {
|
|
init() {
|
|
this.button = document.createElement('button');
|
|
this.button.innerHTML = `
|
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20">
|
|
<line x1="12" y1="19" x2="12" y2="5"/>
|
|
<polyline points="5 12 12 5 19 12"/>
|
|
</svg>
|
|
`;
|
|
this.button.style.cssText = `
|
|
position: fixed;
|
|
bottom: 24px;
|
|
right: 24px;
|
|
width: 44px;
|
|
height: 44px;
|
|
background: var(--bg-primary);
|
|
border: 1px solid var(--border-color);
|
|
border-radius: 50%;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
visibility: hidden;
|
|
transition: all 0.3s ease;
|
|
z-index: 100;
|
|
color: var(--text-secondary);
|
|
box-shadow: var(--shadow-md);
|
|
`;
|
|
|
|
this.button.addEventListener('click', () => {
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
});
|
|
|
|
document.body.appendChild(this.button);
|
|
|
|
window.addEventListener('scroll', () => this.toggle());
|
|
},
|
|
|
|
toggle() {
|
|
if (window.scrollY > 300) {
|
|
this.button.style.opacity = '1';
|
|
this.button.style.visibility = 'visible';
|
|
} else {
|
|
this.button.style.opacity = '0';
|
|
this.button.style.visibility = 'hidden';
|
|
}
|
|
}
|
|
};
|
|
|
|
// ==================== Initialize ====================
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
ThemeManager.init();
|
|
SearchManager.init();
|
|
MobileMenu.init();
|
|
TOCManager.init();
|
|
ReadingProgress.init();
|
|
LazyLoad.init();
|
|
BackToTop.init();
|
|
|
|
// Add current page class to nav
|
|
const currentPath = window.location.pathname;
|
|
document.querySelectorAll('.main-nav a').forEach(link => {
|
|
if (link.pathname === currentPath) {
|
|
link.classList.add('current');
|
|
}
|
|
});
|
|
});
|
|
|
|
})();
|