2026-01-31 19:24:22 +08:00
|
|
|
// 判断是否为移动端
|
|
|
|
|
window.isMobile = function() {
|
|
|
|
|
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端
|
|
|
|
|
}
|
|
|
|
|
};
|
2026-01-31 21:27:06 +08:00
|
|
|
|
|
|
|
|
// Toast Message Helper
|
2026-01-31 22:06:19 +08:00
|
|
|
window.showToast = function(text, type) {
|
|
|
|
|
type = type || 'info'; // default to info
|
2026-01-31 21:27:06 +08:00
|
|
|
if (typeof Qmsg !== 'undefined') {
|
2026-01-31 22:06:19 +08:00
|
|
|
// Map common types to Qmsg methods
|
|
|
|
|
if (typeof Qmsg[type] === 'function') {
|
|
|
|
|
Qmsg[type](text);
|
|
|
|
|
} else {
|
|
|
|
|
Qmsg.info(text);
|
|
|
|
|
}
|
2026-01-31 21:27:06 +08:00
|
|
|
} else {
|
|
|
|
|
// Fallback custom toast
|
|
|
|
|
let toast = document.querySelector('.custom-toast');
|
|
|
|
|
if (!toast) {
|
|
|
|
|
toast = document.createElement('div');
|
|
|
|
|
toast.className = 'custom-toast';
|
|
|
|
|
toast.style.cssText = `
|
|
|
|
|
position: fixed;
|
|
|
|
|
top: 20px;
|
|
|
|
|
left: 50%;
|
|
|
|
|
transform: translateX(-50%);
|
|
|
|
|
background: rgba(0,0,0,0.8);
|
|
|
|
|
color: white;
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
z-index: 9999;
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
transition: opacity 0.3s;
|
|
|
|
|
pointer-events: none;
|
2026-01-31 22:06:19 +08:00
|
|
|
display: flex;
|
|
|
|
|
align-items: center;
|
|
|
|
|
gap: 8px;
|
2026-01-31 21:27:06 +08:00
|
|
|
`;
|
|
|
|
|
document.body.appendChild(toast);
|
|
|
|
|
}
|
2026-01-31 22:06:19 +08:00
|
|
|
|
|
|
|
|
// Simple icon mapping for fallback
|
|
|
|
|
let icon = '';
|
|
|
|
|
if (type === 'success') icon = '✅ ';
|
|
|
|
|
else if (type === 'error') icon = '❌ ';
|
|
|
|
|
else if (type === 'warning') icon = '⚠️ ';
|
|
|
|
|
|
|
|
|
|
toast.innerText = icon + text;
|
2026-01-31 21:27:06 +08:00
|
|
|
toast.style.opacity = '1';
|
|
|
|
|
|
|
|
|
|
if (window.toastTimeout) clearTimeout(window.toastTimeout);
|
|
|
|
|
window.toastTimeout = setTimeout(() => {
|
|
|
|
|
toast.style.opacity = '0';
|
2026-01-31 22:06:19 +08:00
|
|
|
}, 2500);
|
2026-01-31 21:27:06 +08:00
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Theme Toggle Logic
|
|
|
|
|
window.initThemeToggle = function() {
|
|
|
|
|
const avatar = document.getElementById('theme-toggle-avatar');
|
|
|
|
|
if (!avatar) return;
|
|
|
|
|
|
|
|
|
|
// Remove existing listeners to avoid duplicates (if any)
|
|
|
|
|
const newAvatar = avatar.cloneNode(true);
|
|
|
|
|
avatar.parentNode.replaceChild(newAvatar, avatar);
|
|
|
|
|
|
|
|
|
|
newAvatar.addEventListener('click', function(e) {
|
|
|
|
|
// Prevent if clicking on something interactive inside (unlikely for avatar)
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
|
|
|
|
|
const html = document.documentElement;
|
|
|
|
|
const currentTheme = html.getAttribute('data-theme');
|
|
|
|
|
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
|
|
|
|
|
|
|
|
|
|
html.setAttribute('data-theme', newTheme);
|
|
|
|
|
localStorage.setItem('theme', newTheme);
|
|
|
|
|
|
2026-01-31 22:06:19 +08:00
|
|
|
window.showToast('已切换至 ' + (newTheme === 'dark' ? '深色' : '浅色') + '模式', 'success');
|
2026-01-31 21:27:06 +08:00
|
|
|
|
|
|
|
|
// Trigger generic resize event to force charts/etc to redraw if needed
|
|
|
|
|
window.dispatchEvent(new Event('resize'));
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Code Folding Logic
|
|
|
|
|
window.initCodeFold = function() {
|
|
|
|
|
const codeBlocks = document.querySelectorAll('.highlight');
|
|
|
|
|
codeBlocks.forEach(block => {
|
|
|
|
|
// Check height (approx 300px)
|
|
|
|
|
if (block.offsetHeight > 360) {
|
|
|
|
|
// Check if already processed
|
|
|
|
|
if (block.parentNode.classList.contains('code-fold-container')) return;
|
|
|
|
|
|
|
|
|
|
// Wrap
|
|
|
|
|
const wrapper = document.createElement('div');
|
|
|
|
|
wrapper.className = 'code-fold-container collapsed';
|
|
|
|
|
block.parentNode.insertBefore(wrapper, block);
|
|
|
|
|
wrapper.appendChild(block);
|
|
|
|
|
|
|
|
|
|
// Add Button
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.className = 'code-expand-btn';
|
2026-02-01 12:45:32 +08:00
|
|
|
btn.innerHTML = '<span><i class="ri-arrow-down-s-line" style="margin-right:4px;"></i> 展开代码 </span>';
|
2026-01-31 21:27:06 +08:00
|
|
|
btn.onclick = function() {
|
|
|
|
|
wrapper.classList.remove('collapsed');
|
|
|
|
|
// Hide button after expand, or change to collapse?
|
|
|
|
|
// Usually just expand is enough.
|
|
|
|
|
btn.style.display = 'none';
|
|
|
|
|
};
|
|
|
|
|
wrapper.appendChild(btn);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Code Copy Logic
|
|
|
|
|
window.initCodeCopy = function() {
|
|
|
|
|
// Target pre tags that contain code, or .highlight containers
|
|
|
|
|
const codeBlocks = document.querySelectorAll('pre');
|
|
|
|
|
|
|
|
|
|
codeBlocks.forEach(pre => {
|
|
|
|
|
// Avoid double init
|
|
|
|
|
if (pre.parentNode.querySelector('.copy-code-btn')) return;
|
|
|
|
|
if (pre.querySelector('.copy-code-btn')) return;
|
|
|
|
|
|
|
|
|
|
// Check if it's inside a highlight block (Hugo usually does .highlight > pre)
|
|
|
|
|
// We attach button to the pre or the highlight container?
|
|
|
|
|
// Attaching to pre is safer for positioning.
|
|
|
|
|
|
|
|
|
|
const btn = document.createElement('button');
|
|
|
|
|
btn.className = 'copy-code-btn';
|
|
|
|
|
btn.innerHTML = '<i class="ri-file-copy-line"></i>';
|
2026-02-01 12:45:32 +08:00
|
|
|
btn.title = "复制代码";
|
2026-01-31 21:27:06 +08:00
|
|
|
btn.style.cssText = `
|
|
|
|
|
position: absolute;
|
2026-02-01 12:45:32 +08:00
|
|
|
top: -6px;
|
2026-01-31 21:27:06 +08:00
|
|
|
right: 5px;
|
|
|
|
|
background: transparent;
|
|
|
|
|
border: none;
|
|
|
|
|
color: #888;
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
padding: 5px;
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
transition: all 0.2s;
|
|
|
|
|
opacity: 0;
|
|
|
|
|
z-index: 10;
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
// Show on hover
|
|
|
|
|
pre.addEventListener('mouseenter', () => btn.style.opacity = '1');
|
|
|
|
|
pre.addEventListener('mouseleave', () => btn.style.opacity = '0');
|
|
|
|
|
|
|
|
|
|
// Handle touch devices
|
|
|
|
|
if (window.isMobile()) {
|
|
|
|
|
btn.style.opacity = '1';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
btn.addEventListener('click', (e) => {
|
|
|
|
|
e.stopPropagation();
|
|
|
|
|
const code = pre.querySelector('code') ? pre.querySelector('code').innerText : pre.innerText;
|
|
|
|
|
|
|
|
|
|
if (navigator.clipboard) {
|
|
|
|
|
navigator.clipboard.writeText(code).then(() => {
|
2026-01-31 22:06:19 +08:00
|
|
|
window.showToast('代码已复制 / Copied!', 'success');
|
2026-01-31 21:27:06 +08:00
|
|
|
btn.innerHTML = '<i class="ri-check-line"></i>';
|
|
|
|
|
setTimeout(() => btn.innerHTML = '<i class="ri-file-copy-line"></i>', 2000);
|
|
|
|
|
}).catch(err => {
|
|
|
|
|
console.error('Copy failed', err);
|
2026-01-31 22:06:19 +08:00
|
|
|
window.showToast('复制失败 / Failed', 'error');
|
2026-01-31 21:27:06 +08:00
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
// Fallback
|
|
|
|
|
const textarea = document.createElement('textarea');
|
|
|
|
|
textarea.value = code;
|
|
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
textarea.select();
|
|
|
|
|
document.execCommand('copy');
|
|
|
|
|
document.body.removeChild(textarea);
|
2026-01-31 22:06:19 +08:00
|
|
|
window.showToast('代码已复制 / Copied!', 'success');
|
2026-01-31 21:27:06 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Ensure relative positioning
|
|
|
|
|
if (getComputedStyle(pre).position === 'static') {
|
|
|
|
|
pre.style.position = 'relative';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pre.appendChild(btn);
|
|
|
|
|
});
|
|
|
|
|
};
|