公用JS函数文件记录
作者: iTxGo |
发布于: 2026-01-04 10:32 |
更新于: 2026-01-04 13:55 |
分类: 笔记 |
浏览: 95 |
兼容版 √ - Edge Chrome + Safari
// assets/script.js
// =============================
// 全局滚动管理器
// =============================
const ScrollManager = (() => {
let scrollLock = false;
let lockTimeout = null;
const getHeaderOffset = () => {
const header = document.querySelector('.header-container');
return header ? header.offsetHeight : 0;
};
const canScroll = () => !scrollLock;
const smoothScrollTo = (targetY, options = {}) => {
if (scrollLock && !options.force) return false;
const duration = options.duration || 500;
const offset = options.offset || 0;
const onComplete = options.onComplete;
scrollLock = true;
clearTimeout(lockTimeout);
const startY = window.scrollY;
const distance = targetY - startY + offset;
let startTime = null;
const easeOutCubic = t => Math.min(1, 1.001 - Math.pow(2, -10 * t));
const animate = (currentTime) => {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(progress);
window.scrollTo(0, startY + distance * eased);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
lockTimeout = setTimeout(() => {
scrollLock = false;
if (onComplete) onComplete();
}, 50);
}
};
requestAnimationFrame(animate);
return true;
};
const unlock = () => {
scrollLock = false;
clearTimeout(lockTimeout);
};
const isLocked = () => scrollLock;
return {
getHeaderOffset,
canScroll,
smoothScrollTo,
unlock,
isLocked
};
})();
// =============================
// DOM加载完成初始化
// =============================
document.addEventListener('DOMContentLoaded', function() {
console.log("✅ 博客脚本已加载");
document.body.classList.add('page-loaded');
// 核心功能优先执行
initAlertAutoHide();
initModals();
initUserDropdown();
cleanUrlParams();
initReplyForm();
// 延迟执行非阻塞任务
// Safari对 requestIdleCallback 支持较好,但给予充足的 timeout
if (window.requestIdleCallback) {
requestIdleCallback(() => {
initArticleTOC();
initMainCommentsCollapse();
initScrollButtons();
initLikeButton();
requestAnimationFrame(initCodeBlocks);
}, { timeout: 2000 });
} else {
// 降级处理
setTimeout(() => {
initArticleTOC();
initMainCommentsCollapse();
initScrollButtons();
initLikeButton();
initCodeBlocks();
}, 100);
}
initSubtitleManager();
initSearchCooldown();
initHeaderShrink();
initGlobalDelegation();
});
// =============================
// 全局事件委托
// =============================
function initGlobalDelegation() {
// 1. 评论折叠/展开
document.addEventListener('click', (e) => {
const btn = e.target.closest('.toggle-replies-btn, .toggle-main-comments-btn');
if (!btn) return;
const isSubReplies = btn.classList.contains('toggle-replies-btn');
if (isSubReplies) {
const moreDiv = btn.previousElementSibling;
if (!moreDiv) return;
const isExpanded = btn.dataset.state === 'expanded';
const hiddenCount = btn.getAttribute('data-count') || '';
const collapsedLabel = `更多回复 <span class="circled-number">${hiddenCount}</span>`;
if (isExpanded) {
moreDiv.style.display = 'none';
btn.innerHTML = collapsedLabel;
btn.dataset.state = 'collapsed';
} else {
moreDiv.style.display = 'block';
btn.innerHTML = '收起回复';
btn.dataset.state = 'expanded';
}
} else {
const wrapper = btn.closest('.toggle-main-comments-wrapper');
const moreDiv = wrapper ? wrapper.previousElementSibling : null;
const hiddenCount = btn.getAttribute('data-count') || '';
if (moreDiv) {
const isExpanded = btn.dataset.state === 'expanded';
if (isExpanded) {
moreDiv.style.display = 'none';
btn.innerHTML = `➕ 更多评论 <span class="circled-number">${hiddenCount}</span>`;
btn.dataset.state = 'collapsed';
} else {
moreDiv.style.display = 'block';
btn.innerHTML = '➖ 收起评论';
btn.dataset.state = 'expanded';
}
}
}
});
// 2. 脚注跳转
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const targetId = a.hash.slice(1);
if (!targetId) return;
const el = document.getElementById(targetId);
if (!el) return;
e.preventDefault();
if (!ScrollManager.canScroll()) return;
const targetY = el.getBoundingClientRect().top + window.scrollY - 350;
ScrollManager.smoothScrollTo(targetY, { duration: 500 });
});
}
// =============================
// 1. 自动隐藏提示消息
// =============================
function initAlertAutoHide() {
const alerts = document.querySelectorAll('.alert');
if (!alerts.length) return;
const removeAlert = (alert) => {
alert.style.transition = "opacity 0.5s ease";
alert.style.opacity = "0";
setTimeout(() => alert.remove(), 500);
};
alerts.forEach(alert => setTimeout(() => removeAlert(alert), 3000));
}
// =============================
// 2. 清理 URL 参数
// =============================
function cleanUrlParams() {
if (!window.location.search.includes('success=') && !window.location.search.includes('error=')) return;
setTimeout(() => {
const url = new URL(window.location.href);
url.searchParams.delete('success');
url.searchParams.delete('error');
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
}, 3500);
}
// =============================
// 3. 评论回复表单逻辑
// =============================
function initReplyForm() {
const mainFormContainer = document.getElementById('main-comment-form');
const replyFormContainer = document.getElementById('reply-form-container');
const cancelReplyBtn = document.getElementById('cancel-reply');
const replyToSpan = document.getElementById('reply-to');
const parentIdInput = document.getElementById('parent_id');
let lastReplyCommentId = null;
document.addEventListener('click', (e) => {
const btn = e.target.closest('.reply-btn');
if (!btn) return;
const commentId = btn.getAttribute('data-comment-id');
const authorName = btn.getAttribute('data-author');
lastReplyCommentId = commentId;
if (mainFormContainer) mainFormContainer.style.display = 'none';
if (replyFormContainer) replyFormContainer.style.display = 'block';
if (replyToSpan) replyToSpan.textContent = authorName;
if (parentIdInput) parentIdInput.value = commentId;
const replyForm = document.getElementById('reply-form');
if (replyForm) {
const textarea = replyForm.querySelector('textarea[name="content"]');
if (textarea) textarea.value = '';
requestAnimationFrame(() => {
const maxScroll = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
) - window.innerHeight;
ScrollManager.smoothScrollTo(maxScroll, {
duration: 600,
force: true
});
});
}
});
if (cancelReplyBtn) {
cancelReplyBtn.addEventListener('click', function() {
if (replyFormContainer) replyFormContainer.style.display = 'none';
if (parentIdInput) parentIdInput.value = '';
if (mainFormContainer) mainFormContainer.style.display = 'block';
const replyForm = document.getElementById('reply-form');
if (replyForm) {
const textarea = replyForm.querySelector('textarea[name="content"]');
if (textarea) textarea.value = '';
}
if (lastReplyCommentId) {
const targetComment = document.getElementById('comment-' + lastReplyCommentId);
if (targetComment) {
const rect = targetComment.getBoundingClientRect();
const targetY = rect.top + window.scrollY - ScrollManager.getHeaderOffset() - 28;
ScrollManager.smoothScrollTo(targetY, { duration: 600, force: true });
}
lastReplyCommentId = null;
}
});
}
}
// =============================
// 4. 主评论折叠功能
// =============================
function initMainCommentsCollapse() {
const mainCommentsContainer = document.querySelector('.latest-comments-container .sidebar-section');
if (!mainCommentsContainer) return;
const allMainComments = mainCommentsContainer.querySelectorAll('.comment-container.level-0');
const totalMainComments = allMainComments.length;
if (totalMainComments <= 2) return;
const moreMainComments = document.createElement('div');
moreMainComments.className = 'more-main-comments';
moreMainComments.style.display = 'none';
const fragment = document.createDocumentFragment();
for (let i = 2; i < totalMainComments; i++) {
fragment.appendChild(allMainComments[i]);
}
moreMainComments.appendChild(fragment);
allMainComments[1].parentNode.insertBefore(moreMainComments, allMainComments[1].nextSibling);
const toggleMainBtn = document.createElement('div');
toggleMainBtn.className = 'toggle-main-comments-wrapper';
toggleMainBtn.style.padding = '10px 0';
const hiddenMainCount = totalMainComments - 2;
toggleMainBtn.innerHTML = `<button class="toggle-main-comments-btn" data-state="collapsed" data-count="${hiddenMainCount}">➕ 更多评论 <span class="circled-number">${hiddenMainCount}</span></button>`;
moreMainComments.parentNode.insertBefore(toggleMainBtn, moreMainComments.nextSibling);
}
// =============================
// 5. 右侧滚动按钮
// =============================
function initScrollButtons() {
const SCROLL_SPEED = 3;
let scrollRafId = null;
let stopScroll = () => { if (scrollRafId) cancelAnimationFrame(scrollRafId); scrollRafId = null; };
const setupBtn = (id, isDown) => {
const btn = document.getElementById(id);
if (!btn) return;
const continuousScroll = () => {
if (ScrollManager.isLocked()) return stopScroll();
window.scrollBy(0, isDown ? SCROLL_SPEED : -SCROLL_SPEED);
scrollRafId = requestAnimationFrame(continuousScroll);
};
btn.addEventListener('mouseenter', () => {
if (ScrollManager.isLocked()) return;
stopScroll();
scrollRafId = requestAnimationFrame(continuousScroll);
});
btn.addEventListener('mouseleave', stopScroll);
btn.addEventListener('click', () => {
stopScroll();
const targetY = isDown ? Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight : 0;
ScrollManager.smoothScrollTo(targetY, { duration: 800, force: true });
});
};
setupBtn('scrollUp', false);
setupBtn('scrollDown', true);
document.addEventListener('visibilitychange', () => document.hidden && stopScroll());
console.log('✅ 滚动按钮已初始化');
}
// =============================
// 6. 点赞功能
// =============================
function initLikeButton() {
const likeBtn = document.getElementById('like-btn');
if (!likeBtn) return;
likeBtn.addEventListener('click', function() {
if (this.disabled) return;
const articleId = this.getAttribute('data-article-id');
const isLiked = this.classList.contains('liked');
const action = isLiked ? 'unlike' : 'like';
this.disabled = true;
const formData = new FormData();
formData.append('action', action);
formData.append('article_id', articleId);
fetch('ajax_like.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
if (data.success) {
this.classList.toggle('liked', action === 'like');
const countSpan = document.getElementById('like-count');
if (countSpan) countSpan.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('网络错误,请重试');
})
.finally(() => {
this.disabled = false;
});
});
}
// =============================
// 7. 代码块功能
// =============================
const CODE_CONFIG = {
COLLAPSE_LINE_THRESHOLD: 5,
COLLAPSE_LINES: 5,
COPY_FEEDBACK_DURATION: 1200,
RESIZE_DEBOUNCE_DELAY: 150
};
async function initCodeBlocks() {
requestAnimationFrame(async () => {
if (document.fonts && document.fonts.ready) {
try {
await document.fonts.ready;
} catch (e) {
console.warn('Fonts ready failed', e);
}
}
const blocks = document.querySelectorAll(".article-content pre");
if (blocks.length === 0) return;
for (let i = 0; i < blocks.length; i++) {
await processBlock(blocks[i], i);
}
});
// Resize 处理
let resizeTimer = null;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
document.querySelectorAll(".article-content pre.collapsed").forEach(pre => {
const codeEl = pre.querySelector("code") || pre;
const cs = getComputedStyle(codeEl);
const fs = parseFloat(cs.fontSize) || 15;
const lh = parseFloat(cs.lineHeight) || fs * 1.6;
const ps = getComputedStyle(pre);
const pt = parseFloat(ps.paddingTop) || 48;
const pb = parseFloat(ps.paddingBottom) || 16;
const newHeight = Math.ceil(pt + lh * CODE_CONFIG.COLLAPSE_LINES + pb);
pre._collapsedHeight = newHeight;
pre.style.maxHeight = newHeight + "px";
});
}, CODE_CONFIG.RESIZE_DEBOUNCE_DELAY);
}, { passive: true });
}
// 代码块处理逻辑分离
async function processBlock(pre, index) {
// 分片处理,避免在主线程阻塞太久(Safari 尤其敏感)
if (index > 0 && index % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
const codeEl = pre.querySelector("code") || pre;
// 使用 textContent 避免 innerText 触发重排
let raw = (codeEl.textContent || "").replace(/^\n+|\n+$/g, "").replace(/\r\n/g, "\n");
const lines = raw ? raw.split("\n") : [];
// ==================== 生成行号 ====================
if (!pre.hasAttribute('data-line-numbers')) {
const lineNumbersText = lines.map((_, i) => i + 1).join('\n');
pre.setAttribute('data-line-numbers', lineNumbersText);
}
// ==================== 工具栏 ====================
if (!pre.querySelector(".code-tools")) {
const shouldCollapse = lines.length > CODE_CONFIG.COLLAPSE_LINE_THRESHOLD;
const toggleBtnHTML = shouldCollapse ? '<button class="code-btn code-toggle" type="button">展开</button>' : '';
const tools = document.createElement("div");
tools.className = "code-tools";
tools.innerHTML = `
<span class="code-title">CODE - iTxGo™️</span>
<div style="display:flex; gap:8px; align-items:center;">
${toggleBtnHTML}
<button class="code-btn code-copy" type="button" style="width:43px; min-width:43px; text-align:center;">复制</button>
</div>
`;
pre.insertBefore(tools, pre.firstChild);
}
// ==================== 复制功能 ====================
const copyBtn = pre.querySelector(".code-copy");
if (copyBtn && !copyBtn.hasListener) {
copyBtn.hasListener = true;
// Safari 下使用 touchstart 或确保 click 有效
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(raw);
showCopyFeedback(copyBtn);
} catch (e) {
// 降级处理
if (copyWithFallback(raw)) showCopyFeedback(copyBtn);
else showCopyError(copyBtn, "复制失败");
}
});
}
// ==================== 折叠功能 ====================
const shouldCollapse = lines.length > CODE_CONFIG.COLLAPSE_LINE_THRESHOLD;
if (!shouldCollapse) {
pre.classList.add("expanded");
pre.classList.remove("collapsed");
return;
}
// 计算折叠高度
const cs = getComputedStyle(codeEl);
const fontSize = parseFloat(cs.fontSize) || 15;
const lineHeight = parseFloat(cs.lineHeight) || fontSize * 1.6;
const ps = getComputedStyle(pre);
const paddingTop = parseFloat(ps.paddingTop) || 48;
const paddingBottom = parseFloat(ps.paddingBottom) || 16;
const collapsedHeight = Math.ceil(paddingTop + lineHeight * CODE_CONFIG.COLLAPSE_LINES + paddingBottom);
pre._collapsedHeight = collapsedHeight;
pre.style.maxHeight = collapsedHeight + "px";
pre.classList.add("collapsed");
pre.classList.remove("expanded");
// ==================== 折叠按钮事件 ====================
const toggleBtn = pre.querySelector(".code-toggle");
if (toggleBtn && !toggleBtn.hasListener) {
toggleBtn.hasListener = true;
toggleBtn.addEventListener("click", () => {
const isCollapsed = pre.classList.contains("collapsed");
if (isCollapsed) {
pre.classList.replace("collapsed", "expanded");
pre.style.maxHeight = "none";
toggleBtn.textContent = "折叠";
} else {
pre.classList.replace("expanded", "collapsed");
pre.style.maxHeight = pre._collapsedHeight + "px";
toggleBtn.textContent = "展开";
}
});
}
}
function copyWithFallback(text) {
try {
const textarea = document.createElement("textarea");
textarea.value = text;
// 防止 iOS 缩放和滚动
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
document.body.appendChild(textarea);
textarea.select();
textarea.setSelectionRange(0, 99999); // 移动端兼容
const success = document.execCommand("copy");
document.body.removeChild(textarea);
return success;
} catch (err) {
return false;
}
}
function showCopyFeedback(btn) {
const originalText = btn.textContent;
btn.textContent = "✓";
btn.disabled = true;
btn.style.color = "#4ade80";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.style.color = "";
}, CODE_CONFIG.COPY_FEEDBACK_DURATION);
}
function showCopyError(btn, message) {
const originalText = btn.textContent;
btn.textContent = "失败";
btn.disabled = true;
btn.style.color = "#ef4444";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.style.color = "";
}, CODE_CONFIG.COPY_FEEDBACK_DURATION);
}
// =============================
// 8. 用户下拉菜单
// =============================
function initUserDropdown() {
const userMenuContainer = document.querySelector('.user-menu-container');
const userMenuTrigger = document.querySelector('.user-menu-trigger');
const userDropdown = document.querySelector('.user-dropdown');
if (!userMenuContainer || !userMenuTrigger) return;
// 防止点击 a 标签默认跳转(如果有的话)
userMenuTrigger.addEventListener('click', e => e.preventDefault());
if (!userDropdown) return;
// 点击外部关闭
document.addEventListener('click', e => {
if (!userMenuContainer.contains(e.target)) {
userMenuContainer.classList.remove('dropdown-open');
}
});
// ESC 关闭
document.addEventListener('keydown', e => {
if (e.key === 'Escape') userMenuContainer.classList.remove('dropdown-open');
});
// 鼠标移出延时关闭(防止误触)
let leaveTimer = null;
userMenuContainer.addEventListener('mouseleave', () => {
leaveTimer = setTimeout(() => userMenuContainer.classList.remove('dropdown-open'), 300);
});
userMenuContainer.addEventListener('mouseenter', () => {
clearTimeout(leaveTimer);
leaveTimer = null;
});
// 高亮当前菜单项
userDropdown.querySelectorAll('.user-dropdown-item').forEach(item => {
if (window.location.pathname.includes(item.getAttribute('href'))) {
item.style.fontWeight = '600';
item.style.color = 'var(--color-text-primary)';
}
});
}
// =============================
// 9. 模态框功能
// =============================
function initModals() {
const formStates = {
loginForm: { originalText: '登录', isSubmitting: false },
registerForm: { originalText: '注册', isSubmitting: false }
};
function getModalId(formId) { return formId.replace('Form', 'Modal'); }
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.style.display = 'flex';
const messageDiv = modal.querySelector('.modal-message');
if (messageDiv) {
messageDiv.innerHTML = '';
messageDiv.className = 'modal-message';
}
resetForm(modalId.replace('Modal', 'Form'));
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.style.display = 'none';
}
function showMessage(modalId, message, isError = true) {
const messageDiv = document.getElementById(modalId)?.querySelector('.modal-message');
if (messageDiv) {
messageDiv.textContent = message;
messageDiv.className = `modal-message ${isError ? 'alert-error' : 'alert-success'}`;
}
}
function resetForm(formId) {
const form = document.getElementById(formId);
const state = formStates[formId];
if (form) form.reset();
if (state && state.isSubmitting) {
const submitBtn = form ? form.querySelector('button[type="submit"]') : null;
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
}
state.isSubmitting = false;
}
}
function handleFormSubmit(formId, endpoint, onSuccess) {
const form = document.getElementById(formId);
if (!form) return;
const state = formStates[formId];
const modalId = getModalId(formId);
form.addEventListener('submit', function(e) {
e.preventDefault();
if (state.isSubmitting) return;
const submitBtn = this.querySelector('button[type="submit"]');
state.isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = `${state.originalText}中...`;
fetch(endpoint, { method: 'POST', body: new FormData(this) })
.then(r => r.json())
.then(data => {
showMessage(modalId, data.message, !data.success);
if (data.success) {
if (onSuccess) onSuccess(data);
} else {
state.isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
}
})
.catch(error => {
console.error(error);
showMessage(modalId, '网络错误,请重试。', true);
state.isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
});
});
}
// 模态框交互事件绑定
document.querySelectorAll('[data-modal]').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
openModal(link.getAttribute('data-modal'));
});
});
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', () => {
closeModal(btn.getAttribute('data-modal'));
resetForm(btn.getAttribute('data-modal').replace('Modal', 'Form'));
});
});
document.querySelectorAll('.switch-modal').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
closeModal(link.getAttribute('data-from'));
resetForm(link.getAttribute('data-from').replace('Modal', 'Form'));
openModal(link.getAttribute('data-to'));
});
});
handleFormSubmit('loginForm', 'ajax_login.php', data => {
setTimeout(() => window.location.href = data.redirect || 'index.php', 1000);
});
handleFormSubmit('registerForm', 'ajax_register.php', () => {
setTimeout(() => {
closeModal('registerModal');
openModal('loginModal');
}, 1500);
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal').forEach(modal => {
if (modal.style.display === 'flex') {
closeModal(modal.id);
resetForm(modal.id.replace('Modal', 'Form'));
}
});
}
});
}
// =============================
// 10. 副标题动态内容
// =============================
function initSubtitleManager() {
class SubtitleManager {
constructor() {
this.subtitleElement = document.getElementById('subtitle');
this.subtitleTexts = [
"👋 欢迎来到我的博客 :)",
"⚠️ 网站完善中,敬请期待!",
"🔥 热爱可抵岁月漫长…",
"✨ 记录成长轨迹,分享生活微光。",
"🌱 Every day, in every way, I am getting better and better.",
];
this.init();
}
init() {
if (!this.subtitleElement) return;
// 批量设置样式
Object.assign(this.subtitleElement.style, {
minHeight: '16px',
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
});
this.setRandomSubtitle();
}
setRandomSubtitle() {
const randomText = this.subtitleTexts[Math.floor(Math.random() * this.subtitleTexts.length)];
this.subtitleElement.style.opacity = '0';
setTimeout(() => {
this.subtitleElement.textContent = randomText;
this.subtitleElement.style.opacity = '1';
}, 150);
}
}
window.subtitleManager = new SubtitleManager();
}
// =============================
// 11. 文章目录生成
// =============================
function initArticleTOC() {
const articleContent = document.querySelector('.article-content');
const placeholderDiv = document.getElementById('toc-placeholder');
if (!articleContent || !placeholderDiv) return;
const headings = articleContent.querySelectorAll('h2, h3');
if (headings.length === 0) {
placeholderDiv.innerHTML = '<p style="text-align:center;color:#999;padding:20px">本文暂无目录...</p>';
return;
}
const tocWrapper = document.createElement('div');
tocWrapper.className = 'toc-wrapper';
const fragment = document.createDocumentFragment();
let firstH2 = true;
headings.forEach((h, i) => {
if (!h.id) h.id = `heading-${i}`;
const level = h.tagName.toLowerCase();
const isTitle = level === 'h2' && firstH2;
if (isTitle) firstH2 = false;
const tocItem = document.createElement('div');
tocItem.className = `toc-item toc-${level}${isTitle ? ' toc-title' : ''}`;
const tocLink = document.createElement('a');
tocLink.href = `#${h.id}`;
tocLink.className = 'toc-link';
tocLink.dataset.id = h.id;
tocLink.title = h.textContent.trim();
const tocText = document.createElement('span');
tocText.className = 'toc-text';
tocText.textContent = h.textContent.trim();
tocLink.appendChild(tocText);
tocItem.appendChild(tocLink);
fragment.appendChild(tocItem);
});
tocWrapper.appendChild(fragment);
placeholderDiv.innerHTML = '';
placeholderDiv.appendChild(tocWrapper);
initTOCInteraction(headings);
}
// =============================
// 12. 目录交互功能
// =============================
function initTOCInteraction(headings) {
const links = document.querySelectorAll('.toc-link');
if (!links.length) return;
let isTOCScrolling = false;
let scrollEndTimer = null;
const setActive = (link) => {
const current = document.querySelector('.toc-link.active');
if (current === link) return;
if (current) current.classList.remove('active');
if (link) link.classList.add('active');
};
// 使用 IntersectionObserver 替代 scroll 事件计算
const observerOptions = {
rootMargin: '-100px 0px -66% 0px',
threshold: 0
};
const observer = new IntersectionObserver(entries => {
if (isTOCScrolling) return;
let lastEntry = null;
for (const entry of entries) {
if (entry.isIntersecting) {
lastEntry = entry;
}
}
if (lastEntry) {
const activeLink = document.querySelector(`.toc-link[data-id="${lastEntry.target.id}"]`);
if (activeLink) setActive(activeLink);
}
}, observerOptions);
headings.forEach(h => observer.observe(h));
// 点击事件
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = document.getElementById(link.dataset.id);
if (!target) return;
isTOCScrolling = true;
setActive(link);
const highlight = document.querySelector('.heading-highlight');
if (highlight) highlight.classList.remove('heading-highlight');
const header = document.querySelector('.header-container');
const currentHeaderHeight = header ? header.getBoundingClientRect().height : 80;
const targetY = target.getBoundingClientRect().top
+ window.scrollY
- currentHeaderHeight
- 168;
ScrollManager.smoothScrollTo(targetY, {
duration: 600,
onComplete: () => {
target.classList.add('heading-highlight');
setTimeout(() => {
target.classList.remove('heading-highlight');
isTOCScrolling = false;
}, 2500);
}
});
});
});
// 降级:Observer 不支持时
if (!('IntersectionObserver' in window)) {
window.addEventListener('scroll', () => {
if (isTOCScrolling) return;
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
const scrollY = window.scrollY;
let activeHeading = null;
let minDistance = Infinity;
headings.forEach(heading => {
const rect = heading.getBoundingClientRect();
const distance = Math.abs(rect.top - 200);
if (distance < minDistance && rect.top > 0) {
minDistance = distance;
activeHeading = heading;
}
});
if (!activeHeading) {
for (let i = headings.length - 1; i >= 0; i--) {
if (headings[i].getBoundingClientRect().top < window.innerHeight) {
activeHeading = headings[i];
break;
}
}
}
if (activeHeading) {
const activeLink = document.querySelector(`.toc-link[data-id="${activeHeading.id}"]`);
if (activeLink) setActive(activeLink);
}
}, 100);
}, { passive: true });
}
}
// =============================
// 13. 搜索冷却功能
// =============================
function initSearchCooldown() {
const form = document.getElementById('searchForm');
const input = document.getElementById('searchInput');
const button = document.getElementById('searchButton');
if (!form || !input || !button) return;
const isAdmin = document.body.hasAttribute('data-is-admin');
const cooldownEnd = parseInt(localStorage.getItem('search_cooldown_end') || '0', 10);
const now = Date.now();
if (!isAdmin && now < cooldownEnd) {
startCountdown(Math.ceil((cooldownEnd - now) / 1000));
} else {
enableSearch();
}
form.addEventListener('submit', function (e) {
if (!input.value.trim()) {
e.preventDefault();
return;
}
if (!isAdmin) {
const newCooldownEnd = Date.now() + 30 * 1000;
localStorage.setItem('search_cooldown_end', newCooldownEnd.toString());
}
});
function startCountdown(seconds) {
let remaining = seconds;
updateUI();
const interval = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(interval);
enableSearch();
localStorage.removeItem('search_cooldown_end');
} else {
updateUI();
}
}, 1000);
function updateUI() {
input.placeholder = `⏳ ${remaining} 秒...`;
input.disabled = true;
input.value = '';
button.disabled = true;
}
}
function enableSearch() {
input.placeholder = '搜索文章...';
input.disabled = false;
button.disabled = false;
}
}
// =============================
// 14. 导航栏滚动收缩功能 - 兼容版
// =============================
function initHeaderShrink() {
const runInit = () => {
const header = document.querySelector('.header-container');
if (!header) return;
const SHRINK_AT = 80;
const EXPAND_AT = 20;
let isShrunk = false;
let lastScrollY = window.scrollY;
let ticking = false;
const updateHeaderState = () => {
const scrollY = window.scrollY;
const isScrollingDown = scrollY > lastScrollY;
lastScrollY = scrollY;
if (isScrollingDown && !isShrunk && scrollY > SHRINK_AT) {
header.classList.add('shrunk');
isShrunk = true;
} else if (!isScrollingDown && isShrunk && scrollY < EXPAND_AT) {
header.classList.remove('shrunk');
isShrunk = false;
}
ticking = false;
};
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(updateHeaderState);
ticking = true;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
console.log('✅ Header 收缩功能已就绪');
};
if ('requestIdleCallback' in window) {
requestIdleCallback(runInit, { timeout: 100 });
} else {
setTimeout(runInit, 100);
}
}
console.log('✅ 所有脚本模块已优化并加载完成');
原版 - Edge Chrome
// assets/script.js
// =============================
// 全局滚动管理器
// =============================
const ScrollManager = (() => {
let scrollLock = false;
let lockTimeout = null;
const getHeaderOffset = () => {
// 使用缓存高度,减少 getBoundingClientRect 调用
const header = document.querySelector('.header-container');
return header ? header.offsetHeight : 0;
};
const canScroll = () => !scrollLock;
const smoothScrollTo = (targetY, options = {}) => {
if (scrollLock && !options.force) return false;
const duration = options.duration || 500;
const offset = options.offset || 0;
const onComplete = options.onComplete;
scrollLock = true;
clearTimeout(lockTimeout);
const startY = window.scrollY;
const distance = targetY - startY + offset;
let startTime = null;
const easeOutCubic = t => Math.min(1, 1.001 - Math.pow(2, -10 * t));
const animate = (currentTime) => {
if (!startTime) startTime = currentTime;
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const eased = easeOutCubic(progress);
window.scrollTo(0, startY + distance * eased);
if (progress < 1) {
requestAnimationFrame(animate);
} else {
lockTimeout = setTimeout(() => {
scrollLock = false;
onComplete?.();
}, 50);
}
};
requestAnimationFrame(animate);
return true;
};
const unlock = () => {
scrollLock = false;
clearTimeout(lockTimeout);
};
const isLocked = () => scrollLock;
return {
getHeaderOffset,
canScroll,
smoothScrollTo,
unlock,
isLocked
};
})();
// =============================
// DOM加载完成初始化
// =============================
document.addEventListener('DOMContentLoaded', function() {
console.log("✅ 博客脚本已加载");
document.body.classList.add('page-loaded');
// 核心功能优先执行
initAlertAutoHide();
initModals();
initUserDropdown();
cleanUrlParams();
initReplyForm();
// 延迟执行非阻塞任务,避免长任务
requestIdleCallback(() => {
initArticleTOC();
initMainCommentsCollapse();
initScrollButtons();
initLikeButton();
// 代码块初始化最耗时,放入下一帧处理
requestAnimationFrame(initCodeBlocks);
}, { timeout: 2000 }); // timeout确保即使在繁忙时也会在2秒内执行
// 副标题和搜索冷却
initSubtitleManager();
initSearchCooldown();
initHeaderShrink();
// 初始化通用事件委托
initGlobalDelegation();
});
// =============================
// 全局事件委托
// =============================
function initGlobalDelegation() {
// 1. 评论折叠/展开
document.addEventListener('click', (e) => {
// 查找最近的按钮
const btn = e.target.closest('.toggle-replies-btn, .toggle-main-comments-btn');
if (!btn) return;
const isSubReplies = btn.classList.contains('toggle-replies-btn');
if (isSubReplies) {
const moreDiv = btn.previousElementSibling;
if (!moreDiv) return;
const isExpanded = btn.dataset.state === 'expanded';
const hiddenCount = btn.getAttribute('data-count') || '';
const collapsedLabel = `更多回复 <span class="circled-number">${hiddenCount}</span>`;
if (isExpanded) {
moreDiv.style.display = 'none';
btn.innerHTML = collapsedLabel;
btn.dataset.state = 'collapsed';
} else {
moreDiv.style.display = 'block';
btn.innerHTML = '收起回复';
btn.dataset.state = 'expanded';
}
} else {
// 主评论折叠
const wrapper = btn.closest('.toggle-main-comments-wrapper');
const moreDiv = wrapper?.previousElementSibling;
const hiddenCount = btn.getAttribute('data-count') || '';
if (moreDiv) {
const isExpanded = btn.dataset.state === 'expanded';
if (isExpanded) {
moreDiv.style.display = 'none';
btn.innerHTML = `➕ 更多评论 <span class="circled-number">${hiddenCount}</span>`;
btn.dataset.state = 'collapsed';
} else {
moreDiv.style.display = 'block';
btn.innerHTML = '➖ 收起评论';
btn.dataset.state = 'expanded';
}
}
}
});
// 2. 脚注跳转
document.addEventListener('click', (e) => {
const a = e.target.closest('a[href^="#"]');
if (!a) return;
const targetId = a.hash.slice(1);
if (!targetId) return;
const el = document.getElementById(targetId);
if (!el) return;
e.preventDefault();
if (!ScrollManager.canScroll()) return;
const targetY = el.getBoundingClientRect().top + window.scrollY - 350;
ScrollManager.smoothScrollTo(targetY, { duration: 500 });
});
}
// =============================
// 1. 自动隐藏提示消息
// =============================
function initAlertAutoHide() {
// 一次性获取所有
const alerts = document.querySelectorAll('.alert');
if (!alerts.length) return;
const removeAlert = (alert) => {
alert.style.transition = "opacity 0.5s ease";
alert.style.opacity = "0";
setTimeout(() => alert.remove(), 500);
};
alerts.forEach(alert => setTimeout(() => removeAlert(alert), 3000));
}
// =============================
// 2. 清理 URL 参数
// =============================
function cleanUrlParams() {
if (!window.location.search.includes('success=') && !window.location.search.includes('error=')) return;
// 使用 requestIdleCallback 推迟非关键操作
setTimeout(() => {
const url = new URL(window.location.href);
url.searchParams.delete('success');
url.searchParams.delete('error');
window.history.replaceState({}, '', url.pathname + url.search + url.hash);
}, 3500);
}
// =============================
// 3. 评论回复表单逻辑
// =============================
function initReplyForm() {
const mainFormContainer = document.getElementById('main-comment-form');
const replyFormContainer = document.getElementById('reply-form-container');
const cancelReplyBtn = document.getElementById('cancel-reply');
const replyToSpan = document.getElementById('reply-to');
const parentIdInput = document.getElementById('parent_id');
let lastReplyCommentId = null;
// 使用事件委托
document.addEventListener('click', (e) => {
const btn = e.target.closest('.reply-btn');
if (!btn) return;
const commentId = btn.getAttribute('data-comment-id');
const authorName = btn.getAttribute('data-author');
lastReplyCommentId = commentId;
if (mainFormContainer) mainFormContainer.style.display = 'none';
if (replyFormContainer) replyFormContainer.style.display = 'block';
if (replyToSpan) replyToSpan.textContent = authorName;
if (parentIdInput) parentIdInput.value = commentId;
const replyForm = document.getElementById('reply-form');
if (replyForm) {
const textarea = replyForm.querySelector('textarea[name="content"]');
if (textarea) textarea.value = '';
// 延迟滚动
requestAnimationFrame(() => {
const maxScroll = Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight
) - window.innerHeight;
ScrollManager.smoothScrollTo(maxScroll, {
duration: 600,
force: true
});
}, 50);
}
});
if (cancelReplyBtn) {
cancelReplyBtn.addEventListener('click', function() {
if (replyFormContainer) replyFormContainer.style.display = 'none';
if (parentIdInput) parentIdInput.value = '';
if (mainFormContainer) mainFormContainer.style.display = 'block';
const replyForm = document.getElementById('reply-form');
if (replyForm) {
const textarea = replyForm.querySelector('textarea[name="content"]');
if (textarea) textarea.value = '';
}
if (lastReplyCommentId) {
const targetComment = document.getElementById('comment-' + lastReplyCommentId);
if (targetComment) {
// 一次性读取,避免强制重排
const rect = targetComment.getBoundingClientRect();
const targetY = rect.top + window.scrollY - ScrollManager.getHeaderOffset() - 28;
ScrollManager.smoothScrollTo(targetY, { duration: 600, force: true });
}
lastReplyCommentId = null;
}
});
}
}
// =============================
// 4. 主评论折叠功能
// =============================
function initMainCommentsCollapse() {
const mainCommentsContainer = document.querySelector('.latest-comments-container .sidebar-section');
if (!mainCommentsContainer) return;
const allMainComments = mainCommentsContainer.querySelectorAll('.comment-container.level-0');
const totalMainComments = allMainComments.length;
if (totalMainComments <= 2) return;
const moreMainComments = document.createElement('div');
moreMainComments.className = 'more-main-comments';
moreMainComments.style.display = 'none';
// DocumentFragment 批量插入,减少重排
const fragment = document.createDocumentFragment();
for (let i = 2; i < totalMainComments; i++) {
fragment.appendChild(allMainComments[i]);
}
moreMainComments.appendChild(fragment);
allMainComments[1].parentNode.insertBefore(moreMainComments, allMainComments[1].nextSibling);
const toggleMainBtn = document.createElement('div');
toggleMainBtn.className = 'toggle-main-comments-wrapper';
toggleMainBtn.style.padding = '10px 0';
const hiddenMainCount = totalMainComments - 2;
toggleMainBtn.innerHTML = `<button class="toggle-main-comments-btn" data-state="collapsed" data-count="${hiddenMainCount}">➕ 更多评论 <span class="circled-number">${hiddenMainCount}</span></button>`;
moreMainComments.parentNode.insertBefore(toggleMainBtn, moreMainComments.nextSibling);
}
// =============================
// 5. 右侧滚动按钮
// =============================
function initScrollButtons() {
const SCROLL_SPEED = 3;
let scrollRafId = null;
let stopScroll = () => { if (scrollRafId) cancelAnimationFrame(scrollRafId); scrollRafId = null; };
const setupBtn = (id, isDown) => {
const btn = document.getElementById(id);
if (!btn) return;
const continuousScroll = () => {
if (ScrollManager.isLocked()) return stopScroll();
window.scrollBy(0, isDown ? SCROLL_SPEED : -SCROLL_SPEED);
scrollRafId = requestAnimationFrame(continuousScroll);
};
btn.addEventListener('mouseenter', () => {
if (ScrollManager.isLocked()) return;
stopScroll();
scrollRafId = requestAnimationFrame(continuousScroll);
});
btn.addEventListener('mouseleave', stopScroll);
btn.addEventListener('click', () => {
stopScroll();
const targetY = isDown ? Math.max(document.body.scrollHeight, document.documentElement.scrollHeight) - window.innerHeight : 0;
ScrollManager.smoothScrollTo(targetY, { duration: 800, force: true });
});
};
setupBtn('scrollUp', false);
setupBtn('scrollDown', true);
document.addEventListener('visibilitychange', () => document.hidden && stopScroll());
console.log('✅ 滚动按钮已初始化');
}
// =============================
// 6. 点赞功能
// =============================
function initLikeButton() {
const likeBtn = document.getElementById('like-btn');
if (!likeBtn) return;
likeBtn.addEventListener('click', function() {
if (this.disabled) return; // 防止重复点击
const articleId = this.getAttribute('data-article-id');
const isLiked = this.classList.contains('liked');
const action = isLiked ? 'unlike' : 'like';
this.disabled = true;
const formData = new FormData();
formData.append('action', action);
formData.append('article_id', articleId);
fetch('ajax_like.php', { method: 'POST', body: formData })
.then(response => response.json())
.then(data => {
if (data.success) {
this.classList.toggle('liked', action === 'like');
const countSpan = document.getElementById('like-count');
if (countSpan) countSpan.textContent = data.count;
} else {
alert(data.message || '操作失败');
}
})
.catch(error => {
console.error('Error:', error);
alert('网络错误,请重试');
})
.finally(() => {
this.disabled = false;
});
});
}
// =============================
// 7. 代码块功能(修复版 - 兼容现有 CSS)
// =============================
const CODE_CONFIG = {
COLLAPSE_LINE_THRESHOLD: 5,
COLLAPSE_LINES: 5,
COPY_FEEDBACK_DURATION: 1200,
RESIZE_DEBOUNCE_DELAY: 150
};
async function initCodeBlocks() {
requestAnimationFrame(async () => {
if (document.fonts?.ready) await document.fonts.ready;
const blocks = document.querySelectorAll(".article-content pre");
if (blocks.length === 0) return;
const processBlock = (pre, index) => {
if (index > 0 && index % 5 === 0) {
return new Promise(resolve => setTimeout(resolve, 0));
}
const codeEl = pre.querySelector("code") || pre;
let raw = (codeEl.textContent || "").replace(/^\n+|\n+$/g, "").replace(/\r\n/g, "\n");
const lines = raw ? raw.split("\n") : [];
// ==================== 生成行号(用 data 属性) ====================
if (!pre.hasAttribute('data-line-numbers')) {
// 生成行号字符串:1\n2\n3\n...
const lineNumbersText = lines.map((_, i) => i + 1).join('\n');
pre.setAttribute('data-line-numbers', lineNumbersText);
}
// ==================== 工具栏 ====================
if (!pre.querySelector(".code-tools")) {
const shouldCollapse = lines.length > CODE_CONFIG.COLLAPSE_LINE_THRESHOLD;
const toggleBtnHTML = shouldCollapse ? '<button class="code-btn code-toggle" type="button">展开</button>' : '';
const tools = document.createElement("div");
tools.className = "code-tools";
tools.innerHTML = `
<span class="code-title">CODE - iTxGo™️</span>
<div style="display:flex; gap:8px; align-items:center;">
${toggleBtnHTML}
<button class="code-btn code-copy" type="button" style="width:43px; min-width:43px; text-align:center;">复制</button>
</div>
`;
pre.insertBefore(tools, pre.firstChild);
}
// ==================== 复制功能 ====================
const copyBtn = pre.querySelector(".code-copy");
if (copyBtn && !copyBtn.hasListener) {
copyBtn.hasListener = true;
copyBtn.addEventListener("click", async () => {
try {
await navigator.clipboard.writeText(raw);
showCopyFeedback(copyBtn);
} catch (e) {
if (copyWithFallback(raw)) showCopyFeedback(copyBtn);
else showCopyError(copyBtn, "复制失败");
}
});
}
// ==================== 折叠功能 ====================
const shouldCollapse = lines.length > CODE_CONFIG.COLLAPSE_LINE_THRESHOLD;
if (!shouldCollapse) {
pre.classList.add("expanded");
pre.classList.remove("collapsed");
return;
}
// 计算折叠高度
const cs = getComputedStyle(codeEl);
const fontSize = parseFloat(cs.fontSize) || 15;
const lineHeight = parseFloat(cs.lineHeight) || fontSize * 1.6;
// 根据你的 CSS:
// padding-top: 48px (工具栏) + CODE_CONFIG.COLLAPSE_LINES * lineHeight + padding-bottom
const ps = getComputedStyle(pre);
const paddingTop = parseFloat(ps.paddingTop) || 48;
const paddingBottom = parseFloat(ps.paddingBottom) || 16;
const collapsedHeight = Math.ceil(paddingTop + lineHeight * CODE_CONFIG.COLLAPSE_LINES + paddingBottom);
pre._collapsedHeight = collapsedHeight;
pre.style.maxHeight = collapsedHeight + "px";
pre.classList.add("collapsed");
pre.classList.remove("expanded");
// ==================== 折叠按钮事件 ====================
const toggleBtn = pre.querySelector(".code-toggle");
if (toggleBtn && !toggleBtn.hasListener) {
toggleBtn.hasListener = true;
toggleBtn.addEventListener("click", () => {
const isCollapsed = pre.classList.contains("collapsed");
if (isCollapsed) {
pre.classList.replace("collapsed", "expanded");
pre.style.maxHeight = "none";
toggleBtn.textContent = "折叠";
} else {
pre.classList.replace("expanded", "collapsed");
pre.style.maxHeight = pre._collapsedHeight + "px";
toggleBtn.textContent = "展开";
}
});
}
};
for (let i = 0; i < blocks.length; i++) {
await processBlock(blocks[i], i);
}
});
// ==================== Resize 处理 ====================
let resizeTimer = null;
window.addEventListener("resize", () => {
clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
document.querySelectorAll(".article-content pre.collapsed").forEach(pre => {
const codeEl = pre.querySelector("code") || pre;
const cs = getComputedStyle(codeEl);
const fs = parseFloat(cs.fontSize) || 15;
const lh = parseFloat(cs.lineHeight) || fs * 1.6;
const ps = getComputedStyle(pre);
const pt = parseFloat(ps.paddingTop) || 48;
const pb = parseFloat(ps.paddingBottom) || 16;
const newHeight = Math.ceil(pt + lh * CODE_CONFIG.COLLAPSE_LINES + pb);
pre._collapsedHeight = newHeight;
pre.style.maxHeight = newHeight + "px";
});
}, CODE_CONFIG.RESIZE_DEBOUNCE_DELAY);
}, { passive: true });
}
function copyWithFallback(text) {
try {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
document.body.appendChild(textarea);
textarea.select();
const success = document.execCommand("copy");
document.body.removeChild(textarea);
return success;
} catch (err) {
return false;
}
}
function showCopyFeedback(btn) {
const originalText = btn.textContent;
btn.textContent = "✓";
btn.disabled = true;
btn.style.color = "#4ade80";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.style.color = "";
}, CODE_CONFIG.COPY_FEEDBACK_DURATION);
}
function showCopyError(btn, message) {
const originalText = btn.textContent;
btn.textContent = "失败";
btn.disabled = true;
btn.style.color = "#ef4444";
setTimeout(() => {
btn.textContent = originalText;
btn.disabled = false;
btn.style.color = "";
}, CODE_CONFIG.COPY_FEEDBACK_DURATION);
}
// =============================
// 8. 用户下拉菜单
// =============================
function initUserDropdown() {
const userMenuContainer = document.querySelector('.user-menu-container');
const userMenuTrigger = document.querySelector('.user-menu-trigger');
const userDropdown = document.querySelector('.user-dropdown');
if (!userMenuContainer || !userMenuTrigger) return;
userMenuTrigger.addEventListener('click', e => e.preventDefault());
if (!userDropdown) return;
document.addEventListener('click', e => {
if (!userMenuContainer.contains(e.target)) {
userMenuContainer.classList.remove('dropdown-open');
}
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') userMenuContainer.classList.remove('dropdown-open');
});
let leaveTimer = null;
userMenuContainer.addEventListener('mouseleave', () => {
leaveTimer = setTimeout(() => userMenuContainer.classList.remove('dropdown-open'), 300);
});
userMenuContainer.addEventListener('mouseenter', () => {
clearTimeout(leaveTimer);
leaveTimer = null;
});
// 高亮当前菜单项
userDropdown.querySelectorAll('.user-dropdown-item').forEach(item => {
if (window.location.pathname.includes(item.getAttribute('href'))) {
item.style.fontWeight = '600';
item.style.color = 'var(--color-text-primary)';
}
});
}
// =============================
// 9. 模态框功能
// =============================
function initModals() {
const formStates = {
loginForm: { originalText: '登录', isSubmitting: false },
registerForm: { originalText: '注册', isSubmitting: false }
};
function getModalId(formId) { return formId.replace('Form', 'Modal'); }
function openModal(modalId) {
const modal = document.getElementById(modalId);
if (!modal) return;
modal.style.display = 'flex';
const messageDiv = modal.querySelector('.modal-message');
if (messageDiv) {
messageDiv.innerHTML = '';
messageDiv.className = 'modal-message';
}
resetForm(modalId.replace('Modal', 'Form'));
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.style.display = 'none';
}
function showMessage(modalId, message, isError = true) {
const messageDiv = document.getElementById(modalId)?.querySelector('.modal-message');
if (messageDiv) {
messageDiv.textContent = message;
messageDiv.className = `modal-message ${isError ? 'alert-error' : 'alert-success'}`;
}
}
function resetForm(formId) {
const form = document.getElementById(formId);
const state = formStates[formId];
if (form) form.reset();
if (state?.isSubmitting) {
const submitBtn = form?.querySelector('button[type="submit"]');
if (submitBtn) {
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
}
state.isSubmitting = false;
}
}
function handleFormSubmit(formId, endpoint, onSuccess) {
const form = document.getElementById(formId);
if (!form) return;
const state = formStates[formId];
const modalId = getModalId(formId);
form.addEventListener('submit', function(e) {
e.preventDefault();
if (state.isSubmitting) return;
const submitBtn = this.querySelector('button[type="submit"]');
state.isSubmitting = true;
submitBtn.disabled = true;
submitBtn.textContent = `${state.originalText}中...`;
fetch(endpoint, { method: 'POST', body: new FormData(this) })
.then(r => r.json())
.then(data => {
showMessage(modalId, data.message, !data.success);
if (data.success) {
onSuccess?.(data);
} else {
state.isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
}
})
.catch(error => {
showMessage(modalId, '网络错误,请重试。', true);
state.isSubmitting = false;
submitBtn.disabled = false;
submitBtn.textContent = state.originalText;
});
});
}
// 模态框交互事件
document.querySelectorAll('[data-modal]').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
openModal(link.getAttribute('data-modal'));
});
});
document.querySelectorAll('.modal-close').forEach(btn => {
btn.addEventListener('click', () => {
closeModal(btn.getAttribute('data-modal'));
resetForm(btn.getAttribute('data-modal').replace('Modal', 'Form'));
});
});
document.querySelectorAll('.switch-modal').forEach(link => {
link.addEventListener('click', e => {
e.preventDefault();
closeModal(link.getAttribute('data-from'));
resetForm(link.getAttribute('data-from').replace('Modal', 'Form'));
openModal(link.getAttribute('data-to'));
});
});
handleFormSubmit('loginForm', 'ajax_login.php', data => {
setTimeout(() => window.location.href = data.redirect || 'index.php', 1000);
});
handleFormSubmit('registerForm', 'ajax_register.php', () => {
setTimeout(() => {
closeModal('registerModal');
openModal('loginModal');
}, 1500);
});
document.addEventListener('keydown', e => {
if (e.key === 'Escape') {
document.querySelectorAll('.modal').forEach(modal => {
if (modal.style.display === 'flex') {
closeModal(modal.id);
resetForm(modal.id.replace('Modal', 'Form'));
}
});
}
});
}
// =============================
// 10. 副标题动态内容
// =============================
function initSubtitleManager() {
class SubtitleManager {
constructor() {
this.subtitleElement = document.getElementById('subtitle');
this.subtitleTexts = [
"👋 欢迎来到我的博客 :)",
"⚠️ 网站正在建设中,敬请期待!",
"🌱 Every day, in every way, I am getting better and better.",
"❤️🔥 热爱可抵岁月漫长…",
"✨ 记录成长轨迹,分享生活微光。",
];
this.init();
}
init() {
if (!this.subtitleElement) return;
// 批量设置样式
Object.assign(this.subtitleElement.style, {
minHeight: '16px',
display: 'flex',
alignItems: 'center',
whiteSpace: 'nowrap',
overflow: 'hidden',
textOverflow: 'ellipsis'
});
this.setRandomSubtitle();
}
setRandomSubtitle() {
const randomText = this.subtitleTexts[Math.floor(Math.random() * this.subtitleTexts.length)];
this.subtitleElement.style.opacity = '0';
setTimeout(() => {
this.subtitleElement.textContent = randomText;
this.subtitleElement.style.opacity = '1';
}, 150);
}
}
window.subtitleManager = new SubtitleManager();
}
// =============================
// 11. 文章目录生成
// =============================
function initArticleTOC() {
const articleContent = document.querySelector('.article-content');
const placeholderDiv = document.getElementById('toc-placeholder');
if (!articleContent || !placeholderDiv) return;
const headings = articleContent.querySelectorAll('h2, h3');
if (headings.length === 0) {
placeholderDiv.innerHTML = '<p style="text-align:center;color:#999;padding:20px">本文暂无目录...</p>';
return;
}
const tocWrapper = document.createElement('div');
tocWrapper.className = 'toc-wrapper';
// 使用 Fragment 减少重排
const fragment = document.createDocumentFragment();
let firstH2 = true;
headings.forEach((h, i) => {
if (!h.id) h.id = `heading-${i}`;
const level = h.tagName.toLowerCase();
const isTitle = level === 'h2' && firstH2;
if (isTitle) firstH2 = false;
const tocItem = document.createElement('div');
tocItem.className = `toc-item toc-${level}${isTitle ? ' toc-title' : ''}`;
const tocLink = document.createElement('a');
tocLink.href = `#${h.id}`;
tocLink.className = 'toc-link';
tocLink.dataset.id = h.id;
tocLink.title = h.textContent.trim();
const tocText = document.createElement('span');
tocText.className = 'toc-text';
tocText.textContent = h.textContent.trim();
tocLink.appendChild(tocText);
tocItem.appendChild(tocLink);
fragment.appendChild(tocItem);
});
tocWrapper.appendChild(fragment);
placeholderDiv.innerHTML = '';
placeholderDiv.appendChild(tocWrapper);
initTOCInteraction(headings);
}
// =============================
// 12. 目录交互功能
// =============================
function initTOCInteraction(headings) {
const links = document.querySelectorAll('.toc-link');
if (!links.length) return;
let isTOCScrolling = false;
let scrollEndTimer = null;
const setActive = (link) => {
const current = document.querySelector('.toc-link.active');
if (current === link) return;
current?.classList.remove('active');
link?.classList.add('active');
};
// 使用 IntersectionObserver 替代 scroll 事件计算,大幅提升性能
const observerOptions = {
rootMargin: '-100px 0px -66% 0px', // 视口中间偏上区域触发
threshold: 0
};
const observer = new IntersectionObserver(entries => {
if (isTOCScrolling) return; // 如果是点击触发的滚动,不更新高亮
// 获取当前视口内的最后一个标题
let lastEntry = null;
for (const entry of entries) {
if (entry.isIntersecting) {
lastEntry = entry;
}
}
if (lastEntry) {
const activeLink = document.querySelector(`.toc-link[data-id="${lastEntry.target.id}"]`);
if (activeLink) setActive(activeLink);
}
}, observerOptions);
headings.forEach(h => observer.observe(h));
// 点击事件
links.forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const target = document.getElementById(link.dataset.id);
if (!target) return;
isTOCScrolling = true;
setActive(link);
const highlight = document.querySelector('.heading-highlight');
highlight?.classList.remove('heading-highlight');
// 实时获取导航栏高度
const header = document.querySelector('.header-container');
const currentHeaderHeight = header ? header.getBoundingClientRect().height : 80;
const targetY = target.getBoundingClientRect().top
+ window.scrollY
- currentHeaderHeight
- 168;
ScrollManager.smoothScrollTo(targetY, {
duration: 600,
onComplete: () => {
target.classList.add('heading-highlight');
// 延时恢复 observer 控制
setTimeout(() => {
target.classList.remove('heading-highlight');
isTOCScrolling = false;
}, 2500);
}
});
});
});
// 备用:如果 Observer 不支持
if (!('IntersectionObserver' in window)) {
window.addEventListener('scroll', () => {
if (isTOCScrolling) return;
clearTimeout(scrollEndTimer);
scrollEndTimer = setTimeout(() => {
const scrollY = window.scrollY;
let activeHeading = null;
let minDistance = Infinity;
headings.forEach(heading => {
const rect = heading.getBoundingClientRect();
const distance = Math.abs(rect.top - 200);
if (distance < minDistance && rect.top > 0) {
minDistance = distance;
activeHeading = heading;
}
});
if (!activeHeading) {
for (let i = headings.length - 1; i >= 0; i--) {
if (headings[i].getBoundingClientRect().top < window.innerHeight) {
activeHeading = headings[i];
break;
}
}
}
if (activeHeading) {
const activeLink = document.querySelector(`.toc-link[data-id="${activeHeading.id}"]`);
if (activeLink) setActive(activeLink);
}
}, 100);
}, { passive: true });
}
}
// =============================
// 13. 搜索冷却功能
// =============================
function initSearchCooldown() {
const form = document.getElementById('searchForm');
const input = document.getElementById('searchInput');
const button = document.getElementById('searchButton');
if (!form || !input || !button) return;
const isAdmin = document.body.hasAttribute('data-is-admin');
const cooldownEnd = parseInt(localStorage.getItem('search_cooldown_end') || '0', 10);
const now = Date.now();
if (!isAdmin && now < cooldownEnd) {
startCountdown(Math.ceil((cooldownEnd - now) / 1000));
} else {
enableSearch();
}
form.addEventListener('submit', function (e) {
if (!input.value.trim()) {
e.preventDefault();
return;
}
if (!isAdmin) {
const newCooldownEnd = Date.now() + 30 * 1000;
localStorage.setItem('search_cooldown_end', newCooldownEnd.toString());
}
});
function startCountdown(seconds) {
let remaining = seconds;
updateUI();
const interval = setInterval(() => {
remaining--;
if (remaining <= 0) {
clearInterval(interval);
enableSearch();
localStorage.removeItem('search_cooldown_end');
} else {
updateUI();
}
}, 1000);
function updateUI() {
input.placeholder = `⏳ ${remaining} 秒...`;
input.disabled = true;
input.value = '';
button.disabled = true;
}
}
function enableSearch() {
input.placeholder = '搜索文章...';
input.disabled = false;
button.disabled = false;
}
}
// =============================
// 14. 导航栏滚动收缩功能 - 兼容版
// =============================
function initHeaderShrink() {
const runInit = () => {
const header = document.querySelector('.header-container');
if (!header) return;
const SHRINK_AT = 80;
const EXPAND_AT = 20;
let isShrunk = false;
let lastScrollY = window.scrollY;
let ticking = false;
const updateHeaderState = () => {
const scrollY = window.scrollY;
const isScrollingDown = scrollY > lastScrollY;
lastScrollY = scrollY;
if (isScrollingDown && !isShrunk && scrollY > SHRINK_AT) {
header.classList.add('shrunk');
isShrunk = true;
} else if (!isScrollingDown && isShrunk && scrollY < EXPAND_AT) {
header.classList.remove('shrunk');
isShrunk = false;
}
ticking = false;
};
const onScroll = () => {
if (!ticking) {
requestAnimationFrame(updateHeaderState);
ticking = true;
}
};
window.addEventListener('scroll', onScroll, { passive: true });
console.log('✅ Header 收缩功能已就绪');
};
// 100ms后执行
if ('requestIdleCallback' in window) {
requestIdleCallback(runInit, { timeout: 100 });
} else {
// 兼容老旧浏览器
setTimeout(runInit, 100);
}
}
// =============================
// 结束日志
// =============================
console.log('✅ 所有脚本模块已优化并加载完成');