1010 lines
34 KiB
JavaScript
1010 lines
34 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)
|
|
|
|
// Global Artalk Initialization Function (Supports PJAX)
|
|
window.initArtalk = function() {
|
|
if (!document.querySelector("#Comments")) return;
|
|
|
|
// Check if configuration exists
|
|
if (!window.artalkConfig) {
|
|
console.warn('window.artalkConfig is not defined. Skipping Artalk init.');
|
|
return;
|
|
}
|
|
|
|
// Check if Artalk library is loaded
|
|
if (typeof Artalk === 'undefined') {
|
|
console.warn('Artalk library is not loaded yet. Waiting for bundle...');
|
|
// Optional: Retry logic could be added here if needed, but usually bundle is loaded by now
|
|
return;
|
|
}
|
|
|
|
var isDark = document.documentElement.getAttribute('data-theme') === 'dark';
|
|
|
|
// Initialize Artalk
|
|
window.artalk = Artalk.init({
|
|
el: window.artalkConfig.el || '#Comments',
|
|
server: window.artalkConfig.server,
|
|
site: window.artalkConfig.site,
|
|
pageKey: window.artalkConfig.pageKey, // Dynamic pageKey from config
|
|
flatMode: true,
|
|
darkMode: isDark,
|
|
emoticons: window.artalkConfig.emoticons
|
|
});
|
|
|
|
// Inject AI Comment
|
|
window.artalk.on('list-loaded', function() {
|
|
if (window.artalkConfig.aiComment) {
|
|
// Force hide "No comments" message via CSS class
|
|
var list = document.querySelector('.atk-list');
|
|
if (list) {
|
|
list.classList.add('has-ai-comment');
|
|
}
|
|
|
|
if (!document.querySelector('.atk-ai-comment')) {
|
|
var listBody = document.querySelector('.atk-list-body') || document.querySelector('.atk-list');
|
|
if (listBody) {
|
|
var aiData = window.artalkConfig.aiComment;
|
|
var aiDiv = document.createElement('div');
|
|
aiDiv.className = 'atk-comment atk-ai-comment';
|
|
// Initially empty content for typewriter effect
|
|
aiDiv.innerHTML = `
|
|
<div class="atk-avatar">
|
|
<img src="${aiData.avatar}">
|
|
</div>
|
|
<div class="atk-main">
|
|
<div class="atk-header">
|
|
<span class="atk-nick">${aiData.name}</span>
|
|
<span class="atk-badge">BOT</span>
|
|
<span class="atk-date" title="${aiData.date}">${aiData.date}</span>
|
|
</div>
|
|
<div class="atk-body">
|
|
<div class="atk-content">
|
|
<p class="ai-content-text"></p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
`;
|
|
listBody.prepend(aiDiv);
|
|
|
|
// Typewriter Effect
|
|
const contentEl = aiDiv.querySelector('.ai-content-text');
|
|
const text = aiData.content;
|
|
let i = 0;
|
|
|
|
function type() {
|
|
if (i < text.length) {
|
|
contentEl.textContent += text.charAt(i);
|
|
i++;
|
|
// Random delay for more natural typing (20-50ms)
|
|
setTimeout(type, Math.random() * 30 + 20);
|
|
}
|
|
}
|
|
|
|
// Start typing after a short delay
|
|
setTimeout(type, 500);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Artalk 初始化完成后,再加载弹幕 (If available)
|
|
if (typeof isMobile === 'function' && !isMobile() && window.loadBarrager) {
|
|
window.loadBarrager();
|
|
}
|
|
};
|
|
|
|
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="ri-file-copy-2-line"></i>'; // 使用 RemixIcon
|
|
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="ri-check-line"></i>';
|
|
setTimeout(() => {
|
|
copyBtn.innerHTML = '<i class="ri-file-copy-2-line"></i>';
|
|
}, 2000);
|
|
|
|
Qmsg.success("代码复制成功");
|
|
}).catch(err => {
|
|
console.error('复制失败:', err);
|
|
copyBtn.innerHTML = '<i class="ri-close-line"></i>';
|
|
Qmsg.error("复制失败");
|
|
});
|
|
});
|
|
|
|
pre.appendChild(copyBtn);
|
|
});
|
|
}
|
|
|
|
// 代码折叠功能
|
|
function initCodeFold() {
|
|
const threshold = 360;
|
|
|
|
// Helper to wrap element
|
|
const wrap = (el, wrapper) => {
|
|
el.parentNode.insertBefore(wrapper, el);
|
|
wrapper.appendChild(el);
|
|
};
|
|
|
|
// 1. Process .highlight blocks (Hugo generated)
|
|
document.querySelectorAll('.highlight').forEach(hl => {
|
|
if (hl.closest('.code-fold-container')) return; // Avoid double processing
|
|
|
|
// Check height
|
|
if (hl.scrollHeight > threshold) {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'code-fold-container collapsed';
|
|
|
|
// Wrap the highlight block
|
|
wrap(hl, wrapper);
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'code-expand-btn';
|
|
// Use FontAwesome for reliability
|
|
btn.innerHTML = '<span><i class="fa fa-chevron-down" style="margin-right: 4px;"></i> 展开代码</span>';
|
|
|
|
btn.addEventListener('click', () => {
|
|
wrapper.classList.remove('collapsed');
|
|
btn.remove();
|
|
});
|
|
|
|
wrapper.appendChild(btn);
|
|
}
|
|
});
|
|
|
|
// 2. Process bare pre blocks (Fallback)
|
|
document.querySelectorAll('.post-content > pre').forEach(pre => {
|
|
if (pre.parentNode.classList.contains('highlight')) return;
|
|
if (pre.closest('.code-fold-container')) return;
|
|
if (pre.closest('.code-wrapper')) return; // Old wrapper check
|
|
|
|
if (pre.scrollHeight > threshold) {
|
|
const wrapper = document.createElement('div');
|
|
wrapper.className = 'code-fold-container collapsed';
|
|
|
|
wrap(pre, wrapper);
|
|
|
|
const btn = document.createElement('button');
|
|
btn.className = 'code-expand-btn';
|
|
btn.innerHTML = '<span><i class="fa fa-chevron-down" style="margin-right: 4px;"></i> 展开代码</span>';
|
|
|
|
btn.addEventListener('click', () => {
|
|
wrapper.classList.remove('collapsed');
|
|
btn.remove();
|
|
});
|
|
wrapper.appendChild(btn);
|
|
}
|
|
});
|
|
}
|
|
|
|
// 页面加载完成后初始化
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
initCodeCopy();
|
|
initCodeFold();
|
|
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);
|
|
|
|
// Call on ready
|
|
$(document).ready(function() {
|
|
window.initLinkStatus();
|
|
});
|
|
|
|
// Link Status Check
|
|
window.initLinkStatus = function() {
|
|
console.log('initLinkStatus called');
|
|
|
|
// Selectors for both Card View (.links-item) and List View (.links li)
|
|
var $cardLinks = $('.links-item a');
|
|
var $listLinks = $('.links ul > li > a');
|
|
|
|
console.log('Found card links:', $cardLinks.length);
|
|
console.log('Found list links:', $listLinks.length);
|
|
|
|
// Only run if there are any links to check
|
|
if ($cardLinks.length === 0 && $listLinks.length === 0) return;
|
|
|
|
console.log('Fetching /link_status.json...');
|
|
fetch('/link_status.json?t=' + new Date().getTime())
|
|
.then(response => {
|
|
console.log('Fetch response status:', response.status);
|
|
if (!response.ok) throw new Error('Status file not found');
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('Link status data loaded, keys:', Object.keys(data).length);
|
|
let activeCount = 0;
|
|
let deadCount = 0;
|
|
|
|
// Helper function to check URL status
|
|
const getUrlStatus = (url) => {
|
|
if (!url) return 'unknown';
|
|
var cleanUrl = url.replace(/\/$/, '');
|
|
var httpsUrl = cleanUrl.replace(/^http:/, 'https:');
|
|
var httpUrl = cleanUrl.replace(/^https:/, 'http:');
|
|
|
|
const keys = [
|
|
url,
|
|
cleanUrl,
|
|
cleanUrl + '/',
|
|
httpsUrl,
|
|
httpUrl,
|
|
httpsUrl + '/',
|
|
httpUrl + '/'
|
|
];
|
|
|
|
let isDead = false;
|
|
for (const key of keys) {
|
|
if (data[key] === true) return 'alive';
|
|
if (data[key] === false) isDead = true;
|
|
}
|
|
|
|
return isDead ? 'dead' : 'unknown';
|
|
};
|
|
|
|
// Process Card View Links
|
|
$cardLinks.each(function() {
|
|
var $link = $(this);
|
|
var url = $link.attr('href');
|
|
var status = getUrlStatus(url);
|
|
|
|
if (status === 'alive') {
|
|
var $item = $link.closest('.links-item');
|
|
if ($item.find('.link-active-badge').length === 0) {
|
|
$item.append('<span class="link-active-badge" title="可访问"></span>');
|
|
activeCount++;
|
|
}
|
|
} else if (status === 'dead') {
|
|
var $item = $link.closest('.links-item');
|
|
if ($item.find('.link-dead-badge').length === 0) {
|
|
$item.append('<span class="link-dead-badge" title="无法访问"></span>');
|
|
deadCount++;
|
|
}
|
|
}
|
|
});
|
|
|
|
// Process List View Links
|
|
$listLinks.each(function() {
|
|
var $link = $(this);
|
|
var url = $link.attr('href');
|
|
var status = getUrlStatus(url);
|
|
|
|
if (status === 'alive') {
|
|
var $li = $link.closest('li');
|
|
$li.addClass('link-active-li');
|
|
$li.attr('title', '可访问');
|
|
activeCount++;
|
|
} else if (status === 'dead') {
|
|
var $li = $link.closest('li');
|
|
$li.addClass('link-dead-li');
|
|
$li.attr('title', '无法访问');
|
|
deadCount++;
|
|
}
|
|
});
|
|
|
|
console.log('Link status updated. Active:', activeCount, 'Dead:', deadCount);
|
|
})
|
|
.catch(err => console.error('Link status check failed:', err));
|
|
};
|