Files
blog/themes/Ying/assets/js/main.js
T
2026-01-29 20:33:30 +08:00

756 lines
25 KiB
JavaScript

// 判断是否为移动端
function isMobile() {
if (window.navigator.userAgent.match(/(phone|pad|pod|iPhone|iPod|ios|iPad|Android|Mobile|BlackBerry|IEMobile|MQQBrowser|JUC|Fennec|wOSBrowser|BrowserNG|WebOS|Symbian|Windows Phone)/i)) {
return true; // 移动端
} else {
return false; // PC端
}
}
// 文章图片灯箱
window.ViewImage && ViewImage.init('.post-content img');
// Pure JavaScript Back to Top (High Performance)
document.addEventListener('DOMContentLoaded', function() {
var backToTopButton = document.getElementById('back-to-top');
if (backToTopButton) {
var isVisible = false;
var ticking = false;
function update() {
if (window.scrollY > 300) {
if (!isVisible) {
backToTopButton.classList.add('show');
isVisible = true;
}
} else {
if (isVisible) {
backToTopButton.classList.remove('show');
isVisible = false;
}
}
ticking = false;
}
window.addEventListener('scroll', function() {
if (!ticking) {
window.requestAnimationFrame(update);
ticking = true;
}
}, { passive: true });
backToTopButton.addEventListener('click', function(e) {
e.preventDefault();
window.scrollTo({
top: 0,
behavior: 'smooth'
});
});
}
});
// loader定时器
$(document).ready(function () {
setTimeout(function () {
$("#loader").addClass("hide");
}, 30000);
});
// 点击进入按钮
$("#closeloader").click(function () {
$("#loader").addClass("hide");
});
// nav栏样式调整
$('#libabys').first().attr("style", "margin-left: -5px");
$('#libabys').eq(1).attr("style", "margin-left: -5px");
// 友链列表版样式调整
$('.itp-index:odd').attr("style", "float: right;width:48%;");
//文章图片懒加载
if (typeof lazyLoadInit === 'function') {
lazyLoadInit({
// coverColor: "white",
// coverDiv: "<div style='display: flex; justify-content: center; align-items: center; height: 200px; color: #000000;'><span>优世界·一个记录生活点滴的小站</span></div>",
offsetBottom: 800,
offsetTopm: 800,
showTime: 1100
});
} else {
console.warn('lazyLoadInit is not defined');
}
// 异步初始化 Artalk 并获取评论数据 (Moved to barrage.js)
async function fetchHitokoto() {
try {
const response = await fetch('https://v1.hitokoto.cn/?c=d&c=i'); // 获取句子
const data = await response.json();
return data.hitokoto;
} catch (error) {
console.error('获取一言失败:', error);
return null;
}
}
// 代码块复制功能
function initCodeCopy() {
const codeBlocks = document.querySelectorAll('.post-content pre code');
codeBlocks.forEach(codeBlock => {
// 检查是否已经添加了复制按钮,避免重复添加
if (codeBlock.parentNode.querySelector('.copy-code-btn')) return;
const pre = codeBlock.parentNode;
pre.style.position = 'relative';
const copyBtn = document.createElement('button');
copyBtn.className = 'copy-code-btn';
copyBtn.innerHTML = '<i class="fa fa-copy"></i>'; // 使用 FontAwesome 图标
copyBtn.title = '复制代码';
// 样式 (建议放到 CSS 文件中)
copyBtn.style.position = 'absolute';
copyBtn.style.top = '5px';
copyBtn.style.right = '5px';
copyBtn.style.border = 'none';
copyBtn.style.background = 'transparent';
copyBtn.style.color = '#999';
copyBtn.style.cursor = 'pointer';
copyBtn.style.padding = '5px';
copyBtn.style.borderRadius = '4px';
copyBtn.style.transition = 'all 0.3s';
// Hover 效果
copyBtn.addEventListener('mouseenter', () => {
copyBtn.style.color = '#333';
copyBtn.style.background = 'rgba(0,0,0,0.1)';
});
copyBtn.addEventListener('mouseleave', () => {
copyBtn.style.color = '#999';
copyBtn.style.background = 'transparent';
});
copyBtn.addEventListener('click', () => {
const code = codeBlock.innerText;
navigator.clipboard.writeText(code).then(() => {
copyBtn.innerHTML = '<i class="fa fa-check"></i>';
setTimeout(() => {
copyBtn.innerHTML = '<i class="fa fa-copy"></i>';
}, 2000);
Qmsg.success("代码复制成功");
}).catch(err => {
console.error('复制失败:', err);
copyBtn.innerHTML = '<i class="fa fa-times"></i>';
Qmsg.error("复制失败");
});
});
pre.appendChild(copyBtn);
});
}
// 页面加载完成后初始化 (Merged into main DOMContentLoaded listener)
// document.addEventListener('DOMContentLoaded', function() {
// initCodeCopy();
// initDescriptionHitokoto();
// });
// 初始化个人描述一言切换
function initDescriptionHitokoto() {
const descEl = document.querySelector('.yiyandes');
if (!descEl) return;
descEl.title = '点击切换一言';
descEl.addEventListener('click', async () => {
if (descEl.dataset.loading === 'true') return;
descEl.dataset.loading = 'true';
const originalText = descEl.innerText;
descEl.innerText = '正在获取灵感...';
const hitokoto = await fetchHitokoto();
if (hitokoto) {
typeWriter(descEl, hitokoto);
} else {
descEl.innerText = originalText;
descEl.dataset.loading = 'false';
}
});
}
// 打字机效果
function typeWriter(element, text, speed = 100) {
element.innerText = '';
element.classList.add('typing');
let i = 0;
function type() {
if (i < text.length) {
element.innerText += text.charAt(i);
i++;
setTimeout(type, speed);
} else {
element.dataset.loading = 'false';
element.classList.remove('typing');
}
}
type();
}
// 友链信息一键复制
function copySzhContent() {
var szhElement = document.getElementById('szh');
if (szhElement) {
var textToCopy = szhElement.innerText;
navigator.clipboard.writeText(textToCopy).then(function() {
Qmsg.success("复制成功");
}).catch(function(err) {
console.error('Could not copy text: ', err);
Qmsg.error("复制失败");
});
}
}
// Update Theme Icon
function updateThemeIcon(isDark) {
const toggleBtns = document.querySelectorAll('#theme-toggle');
toggleBtns.forEach(btn => {
const icon = btn.querySelector('i');
if (icon) {
// Add rotation class for animation
btn.classList.add('rotate');
setTimeout(() => btn.classList.remove('rotate'), 500);
// Switch icon
if (isDark) {
icon.className = 'ri-moon-line';
} else {
icon.className = 'ri-sun-line';
}
}
});
}
// Theme Toggle Logic
function toggleTheme() {
const currentTheme = document.documentElement.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
const isDark = newTheme === 'dark';
document.documentElement.setAttribute('data-theme', newTheme);
localStorage.setItem('theme', newTheme);
// Update Icon
updateThemeIcon(isDark);
// Optimize: Defer heavy Artalk update to avoid blocking UI animation
setTimeout(() => {
// Notify Artalk if present (Instance or Static)
if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
window.artalk.setDarkMode(isDark);
} else if (window.Artalk && typeof Artalk.setDarkMode === 'function') {
Artalk.setDarkMode(isDark);
}
}, 50);
}
// Listen for system theme changes
try {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const handleSystemThemeChange = (e) => {
// Only follow system if user hasn't manually set a preference
if (!localStorage.getItem('theme')) {
const newTheme = e.matches ? 'dark' : 'light';
document.documentElement.setAttribute('data-theme', newTheme);
updateThemeIcon(newTheme === 'dark');
// Notify Artalk
if (window.artalk && typeof window.artalk.setDarkMode === 'function') {
window.artalk.setDarkMode(newTheme === 'dark');
} else if (window.Artalk && typeof Artalk.setDarkMode === 'function') {
Artalk.setDarkMode(newTheme === 'dark');
}
}
};
// Modern browsers
if (mediaQuery.addEventListener) {
mediaQuery.addEventListener('change', handleSystemThemeChange);
} else if (mediaQuery.addListener) {
// Old Safari/IE
mediaQuery.addListener(handleSystemThemeChange);
}
} catch (e) {
console.warn('System theme detection not supported', e);
}
// Update Floating Tools Visibility based on current page
function updateFloatingToolsVisibility() {
const tools = document.querySelector('.floating-tools');
if (!tools) return;
// Hide on Homepage
const isHome = window.location.pathname === '/' || window.location.pathname === '/index.html';
if (isHome) {
tools.style.display = 'none';
} else {
tools.style.display = 'flex';
}
// Hide Comment button if no comments section
const commentBtn = document.getElementById('to-comments');
if (commentBtn) {
const commentsSection = document.getElementById('Comments');
commentBtn.style.display = commentsSection ? 'flex' : 'none';
}
}
// Floating Tools Initialization
function initFloatingTools() {
// Initialize Theme Icon based on current theme
const currentTheme = document.documentElement.getAttribute('data-theme');
const isDark = currentTheme === 'dark';
const toggleBtns = document.querySelectorAll('#theme-toggle');
toggleBtns.forEach(btn => {
const icon = btn.querySelector('i');
if (icon) {
if (isDark) {
icon.className = 'ri-moon-line';
} else {
icon.className = 'ri-sun-line';
}
}
});
// Initial visibility check
updateFloatingToolsVisibility();
// Prevent multiple event bindings
if (window.floatingToolsInitialized) return;
window.floatingToolsInitialized = true;
// Use Event Delegation for robust handling
document.addEventListener('click', function(e) {
// Theme Toggle
const toggleBtn = e.target.closest('#theme-toggle');
if (toggleBtn) {
e.preventDefault();
toggleTheme();
}
// Jump to Comments
const commentBtn = e.target.closest('#to-comments');
if (commentBtn) {
e.preventDefault();
const commentsSection = document.getElementById('Comments');
if (commentsSection) {
commentsSection.scrollIntoView({ behavior: 'smooth' });
}
}
});
// Theme Toggle (Avatar)
const avatar = document.querySelector('.head_author_img');
if (avatar) {
avatar.addEventListener('click', function(e) {
e.preventDefault();
toggleTheme();
});
}
}
// Listen for PJAX complete to update visibility
document.addEventListener('pjax:complete', function() {
updateFloatingToolsVisibility();
hideSelectionPopup();
// PJAX 跳转后重新加载弹幕 (Moved to barrage.js)
});
document.addEventListener('DOMContentLoaded', function() {
initFloatingTools();
initSelectionPopup();
initCodeCopy();
initDescriptionHitokoto();
// 页面首次加载时启动弹幕 (Moved to barrage.js)
});
// Selection Popup Logic
let selectionPopup = null;
let selectionTimeout;
function initSelectionPopup() {
if (document.getElementById('selection-popup')) return;
selectionPopup = document.createElement('div');
selectionPopup.id = 'selection-popup';
selectionPopup.innerHTML = '<i class="ri-chat-quote-line"></i> <span>引用评论</span>';
document.body.appendChild(selectionPopup);
// Prevent mousedown on popup from clearing selection
selectionPopup.addEventListener('mousedown', function(e) {
e.preventDefault();
e.stopPropagation();
});
// Handle click on popup
selectionPopup.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
let selection = window.getSelection().toString();
// Optimize: Remove extra whitespace and limit length
if (selection) {
// Collapse multiple empty lines into max 2 newlines, and trim each line
selection = selection.split('\n')
.map(line => line.trim())
.filter(line => line !== '')
.join('\n');
if (selection.length > 500) {
selection = selection.substring(0, 500) + '...';
}
scrollToCommentsAndQuote(selection);
hideSelectionPopup();
window.getSelection().removeAllRanges();
}
});
// Global listeners for selection
document.addEventListener('mouseup', handleSelectionChange);
document.addEventListener('keyup', handleSelectionChange);
}
function hideSelectionPopup() {
if (selectionPopup) {
selectionPopup.style.display = 'none';
}
}
function handleSelectionChange(e) {
// Debounce to avoid rapid firing
clearTimeout(selectionTimeout);
selectionTimeout = setTimeout(() => {
const selection = window.getSelection();
const text = selection.toString().trim();
const content = document.querySelector('.post-content'); // Main article container
if (!selectionPopup || !content) return;
// Check if selection is empty
if (!text || selection.rangeCount === 0) {
hideSelectionPopup();
return;
}
const range = selection.getRangeAt(0);
// Check if the common ancestor is inside .post-content
// Optimization: Check container existence before heavy contains check
if (!content.contains(range.commonAncestorContainer)) {
hideSelectionPopup();
return;
}
// Optimization: Use requestAnimationFrame for UI updates
requestAnimationFrame(() => {
// Show first to ensure measurements if needed (though we set top/left directly)
selectionPopup.style.display = 'flex';
// Position logic: Follow mouse if available (mouseup), else fallback to selection rect (keyup)
if (e && e.type === 'mouseup') {
// Optimization: Avoid getBoundingClientRect on mouseup
// Position above the mouse cursor
selectionPopup.style.top = `${e.clientY - 40}px`;
selectionPopup.style.left = `${e.clientX}px`;
} else {
// Fallback for keyboard selection (needs rect)
const rect = range.getBoundingClientRect();
// Check if visible and valid
if (rect.width === 0 || rect.height === 0) {
hideSelectionPopup();
return;
}
selectionPopup.style.top = `${rect.top}px`;
selectionPopup.style.left = `${rect.left + rect.width / 2}px`;
}
});
}, 150);
}
function scrollToCommentsAndQuote(text) {
const comments = document.getElementById('Comments');
if (comments) {
comments.scrollIntoView({ behavior: 'smooth' });
// Wait for scroll to complete (approximate)
setTimeout(() => {
// Find Artalk textarea
const textarea = document.querySelector('.atk-textarea');
if (textarea) {
const quote = `> ${text}\n\n`;
// Overwrite existing content as requested
textarea.value = quote;
textarea.focus();
// Trigger input event for Vue/React binding (Artalk uses Vue)
textarea.dispatchEvent(new Event('input', { bubbles: true }));
textarea.dispatchEvent(new Event('change', { bubbles: true }));
if (window.Qmsg) {
Qmsg.success('引用成功');
}
}
}, 500);
}
}
/* Search Functionality */
function initSearchFunctionality() {
const searchBtn = document.getElementById('search-btn');
const searchModal = document.getElementById('search-modal');
const closeSearch = document.querySelector('.close-search');
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
const searchSubmit = document.getElementById('search-submit');
// Global variables (window scope) to persist across re-inits if needed,
// but better to keep them scoped if we re-fetch.
// For simplicity, we keep index loading check.
if (!window.searchIndex) window.searchIndex = [];
if (!window.isIndexLoaded) window.isIndexLoaded = false;
if (!searchBtn || !searchModal) return;
// Open Modal
// Use named function to avoid duplicate listeners if we didn't clean up (though init is usually called once per page load/pjax)
function openModal() {
searchModal.classList.add('show');
if (!window.isIndexLoaded) {
loadSearchIndex();
}
setTimeout(() => searchInput.focus(), 100);
}
searchBtn.removeEventListener('click', openModal); // Prevent duplicates
searchBtn.addEventListener('click', openModal);
// Close Modal
function closeModal() {
searchModal.classList.remove('show');
}
if (closeSearch) {
closeSearch.onclick = closeModal;
}
// Close when clicking outside
window.onclick = function(e) {
if (e.target === searchModal) {
closeModal();
}
};
// Close on Escape key
document.onkeydown = function(e) {
if (e.key === 'Escape' && searchModal.classList.contains('show')) {
closeModal();
}
};
// Load Index
function loadSearchIndex() {
fetch('/index.json')
.then(response => response.json())
.then(data => {
window.searchIndex = data;
window.isIndexLoaded = true;
})
.catch(error => {
console.error('Error loading search index:', error);
if(searchResults) searchResults.innerHTML = '<p class="search-tip">搜索索引加载失败</p>';
});
}
// Perform Search
function performSearch() {
if(!searchInput) return;
const query = searchInput.value.trim().toLowerCase();
if (query === '') {
searchResults.innerHTML = '<p class="search-tip">输入关键词并回车进行搜索</p>';
return;
}
if (!window.isIndexLoaded) {
searchResults.innerHTML = '<p class="search-tip">正在加载索引...</p>';
// Retry after a short delay if index is loading
setTimeout(performSearch, 500);
return;
}
const results = window.searchIndex.filter(item => {
const title = item.title ? item.title.toLowerCase() : '';
const summary = item.summary ? item.summary.toLowerCase() : '';
return title.includes(query) || summary.includes(query);
});
displayResults(results);
}
// Display Results
function displayResults(results) {
if (results.length === 0) {
searchResults.innerHTML = '<p class="search-tip">未找到相关文章</p>';
return;
}
let html = '';
results.forEach(item => {
html += `
<a href="${item.permalink}" class="search-result-item" style="text-decoration:none; display:block; color:inherit;">
<span class="search-result-title" style="display:block;font-weight:600;margin-bottom:5px;">${item.title}</span>
<span class="search-result-date" style="font-size:12px;color:#999;display:block;margin-bottom:5px;">${item.date}</span>
<div class="search-result-summary">${item.summary}</div>
</a>
`;
});
searchResults.innerHTML = html;
if (window.pjax) window.pjax.refresh(searchResults);
// Add click listener to close modal on result click
const resultLinks = searchResults.querySelectorAll('.search-result-item');
resultLinks.forEach(link => {
link.addEventListener('click', function() {
closeModal();
});
});
}
// Event Listeners for Search
if (searchSubmit) {
searchSubmit.onclick = performSearch;
}
if (searchInput) {
searchInput.onkeypress = function(e) {
if (e.key === 'Enter') {
performSearch();
}
};
// Optional: Live search with debounce
let debounceTimer;
searchInput.oninput = function() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(performSearch, 300);
};
}
/* Inline Search Logic */
const essayHeader = document.getElementById('essay-header');
const searchContainer = document.getElementById('search-container');
const inlineSearchInput = document.getElementById('inline-search-input');
const inlineSearchResults = document.getElementById('inline-search-results');
if (essayHeader && searchContainer && inlineSearchInput) {
window.toggleSearchMode = function(show) {
if (show) {
essayHeader.style.display = 'none';
searchContainer.style.display = 'block';
inlineSearchInput.focus();
if (!window.isIndexLoaded) {
loadSearchIndex();
}
} else {
essayHeader.style.display = 'block';
searchContainer.style.display = 'none';
inlineSearchInput.value = '';
inlineSearchResults.style.display = 'none';
}
};
// Close when clicking outside - delegated to document body to handle pjax updates
document.body.onclick = function(e) {
const isClickInside = essayHeader.contains(e.target) ||
searchContainer.contains(e.target) ||
inlineSearchResults.contains(e.target);
if (!isClickInside && searchContainer.style.display === 'block') {
window.toggleSearchMode(false);
}
// Also handle modal outside click here if needed, but window.onclick handles that
if (e.target === searchModal) {
closeModal();
}
};
// Inline Search Function
function performInlineSearch() {
const query = inlineSearchInput.value.trim().toLowerCase();
if (query === '') {
inlineSearchResults.style.display = 'none';
return;
}
if (!window.isIndexLoaded) {
// Wait if loading
setTimeout(performInlineSearch, 500);
return;
}
const results = window.searchIndex.filter(item => {
const title = item.title ? item.title.toLowerCase() : '';
const summary = item.summary ? item.summary.toLowerCase() : '';
return title.includes(query) || summary.includes(query);
});
displayInlineResults(results);
}
function displayInlineResults(results) {
if (results.length === 0) {
inlineSearchResults.innerHTML = '<div class="search-result-item" style="text-align:center;color:#999;">未找到相关文章</div>';
inlineSearchResults.style.display = 'block';
return;
}
let html = '';
results.slice(0, 5).forEach(item => { // Limit to 5 inline results
html += `
<a href="${item.permalink}" class="search-result-item">
<span class="search-result-title">${item.title}</span>
<div class="search-result-summary">${item.summary}</div>
</a>
`;
});
inlineSearchResults.innerHTML = html;
inlineSearchResults.style.display = 'block';
if (window.pjax) window.pjax.refresh(inlineSearchResults);
}
let inlineDebounce;
inlineSearchInput.oninput = function() {
clearTimeout(inlineDebounce);
inlineDebounce = setTimeout(performInlineSearch, 300);
};
// Handle Escape key
inlineSearchInput.onkeydown = function(e) {
if (e.key === 'Escape') {
window.toggleSearchMode(false);
}
};
}
}
document.addEventListener('DOMContentLoaded', initSearchFunctionality);
// Re-init on Pjax complete
document.addEventListener('pjax:complete', initSearchFunctionality);