更新包

-- [CST 2026-08-04 Tuesday 16:05:05]
This commit is contained in:
biss
2026-08-04 16:05:05 +08:00 Unverified
commit f839e19947
112 changed files with 63309 additions and 0 deletions
+45
View File
@@ -0,0 +1,45 @@
(() => {
const redirectPath = '/go/';
const urlParams = new URLSearchParams(window.location.search);
if (window.location.pathname === redirectPath || window.location.pathname === '/go') return;
if (urlParams.get('noExternalRedirect') === '1') return;
function getExternalUrl(anchor) {
const href = anchor.getAttribute('href');
if (!href || href.startsWith('#')) return null;
let targetUrl;
try {
targetUrl = new URL(href, window.location.href);
} catch {
return null;
}
if (!['http:', 'https:'].includes(targetUrl.protocol)) return null;
if (targetUrl.origin === window.location.origin) return null;
return targetUrl.href;
}
document.addEventListener('click', (event) => {
const anchor = event.target.closest?.('a[href]');
if (!anchor) return;
if (event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || event.altKey) return;
const externalUrl = getExternalUrl(anchor);
if (!externalUrl) return;
event.preventDefault();
const redirectUrl = `${redirectPath}?url=${encodeURIComponent(externalUrl)}&from=${encodeURIComponent(window.location.href)}`;
const target = anchor.getAttribute('target');
if (target && target !== '_self') {
window.open(redirectUrl, target, 'noopener,noreferrer');
return;
}
window.location.href = redirectUrl;
});
})();
+104
View File
@@ -0,0 +1,104 @@
(() => {
if (!('serviceWorker' in navigator)) return;
const toast = document.querySelector('[data-pwa-toast]');
const title = toast?.querySelector('[data-pwa-title]');
const message = toast?.querySelector('[data-pwa-message]');
const primary = toast?.querySelector('[data-pwa-primary]');
const close = toast?.querySelector('[data-pwa-close]');
let installPrompt = null;
let waitingWorker = null;
let toastMode = '';
let refreshing = false;
const showToast = (mode, heading, detail, actionLabel) => {
if (!toast || !title || !message || !primary) return;
toastMode = mode;
title.textContent = heading;
message.textContent = detail;
primary.textContent = actionLabel;
primary.hidden = !actionLabel;
toast.hidden = false;
};
const hideToast = () => {
if (toast) toast.hidden = true;
};
const installDismissedRecently = () => {
try {
const dismissedAt = Number(localStorage.getItem('pwa-install-dismissed-at'));
return dismissedAt > 0 && Date.now() - dismissedAt < 30 * 86400000;
} catch {
return false;
}
};
window.addEventListener('beforeinstallprompt', (event) => {
event.preventDefault();
installPrompt = event;
if (!installDismissedRecently()) {
window.setTimeout(() => {
if (installPrompt) showToast('install', '安装 Bi\'s Blog', '添加到桌面,像应用一样打开并阅读缓存过的文章。', '立即安装');
}, 2500);
}
});
window.addEventListener('appinstalled', () => {
installPrompt = null;
showToast('ready', '安装完成', '博客已经添加到你的设备。', '知道了');
});
primary?.addEventListener('click', async () => {
if (toastMode === 'install' && installPrompt) {
await installPrompt.prompt();
await installPrompt.userChoice;
installPrompt = null;
hideToast();
return;
}
if (toastMode === 'update' && waitingWorker) {
refreshing = true;
waitingWorker.postMessage({ type: 'SKIP_WAITING' });
return;
}
hideToast();
});
close?.addEventListener('click', () => {
if (toastMode === 'install') {
try { localStorage.setItem('pwa-install-dismissed-at', String(Date.now())); } catch {}
}
hideToast();
});
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (refreshing) window.location.reload();
});
navigator.serviceWorker.register('/sw.js', { scope: '/' }).then((registration) => {
if (registration.waiting && navigator.serviceWorker.controller) {
waitingWorker = registration.waiting;
showToast('update', '发现新版本', '刷新后即可使用博客的最新内容和功能。', '立即更新');
}
registration.addEventListener('updatefound', () => {
const worker = registration.installing;
worker?.addEventListener('statechange', () => {
if (worker.state === 'installed' && navigator.serviceWorker.controller) {
waitingWorker = worker;
showToast('update', '发现新版本', '刷新后即可使用博客的最新内容和功能。', '立即更新');
}
});
});
}).catch((error) => {
console.warn('PWA registration failed:', error);
});
const params = new URLSearchParams(window.location.search);
if (params.get('action') === 'search') {
window.setTimeout(() => document.querySelector('.search-typesense-trigger')?.click(), 350);
}
})();
+93
View File
@@ -0,0 +1,93 @@
function randomPost(trigger) {
const source = trigger instanceof Element
? trigger
: document.activeElement?.closest?.('.nav-random, [onclick*="randomPost"]');
const overlay = document.createElement('div');
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
overlay.className = 'random-post-transition';
overlay.setAttribute('role', 'status');
overlay.setAttribute('aria-live', 'polite');
overlay.innerHTML = `
<div class="random-post-flight" aria-hidden="true"></div>
<div class="random-post-transition-card">
<i class="fa-solid fa-shuffle" aria-hidden="true"></i>
<span>\u6b63\u5728\u62bd\u53d6\u4e00\u7bc7\u6587\u7ae0</span>
</div>
`;
source?.classList.add('is-randomizing');
source?.setAttribute('aria-busy', 'true');
document.body.appendChild(overlay);
requestAnimationFrame(() => overlay.classList.add('is-visible'));
const finish = href => {
const delay = reduceMotion ? 80 : 1250;
window.setTimeout(() => {
location.href = href;
}, delay);
};
const startCardFlight = posts => {
if (reduceMotion) return;
const flight = overlay.querySelector('.random-post-flight');
if (!flight) return;
const candidates = posts
.filter(post => post && typeof post.href === 'string' && post.href.startsWith('/posts/'))
.map(post => ({
title: typeof post.title === 'string' && post.title.trim() ? post.title.trim() : '\u672a\u547d\u540d\u6587\u7ae0',
meta: typeof post.category === 'string' && post.category.trim() ? post.category.trim() : '\u968f\u673a\u6587\u7ae0',
}));
const pool = candidates.length ? candidates : [{ title: '\u968f\u673a\u6587\u7ae0', meta: '\u6b63\u5728\u62bd\u53d6' }];
const count = Math.min(9, Math.max(5, pool.length));
for (let index = 0; index < count; index += 1) {
const post = pool[Math.floor(Math.random() * pool.length)];
const card = document.createElement('div');
card.className = 'random-post-flying-card';
card.style.setProperty('--flight-delay', `${index * 0.11}s`);
card.style.setProperty('--flight-y', `${-42 + Math.random() * 84}px`);
card.style.setProperty('--flight-rotate', `${-8 + Math.random() * 16}deg`);
const meta = document.createElement('small');
const title = document.createElement('strong');
meta.textContent = post.meta;
title.textContent = post.title;
card.append(meta, title);
flight.appendChild(card);
}
};
const reset = () => {
source?.classList.remove('is-randomizing');
source?.removeAttribute('aria-busy');
overlay.classList.remove('is-visible');
window.setTimeout(() => overlay.remove(), reduceMotion ? 0 : 180);
};
fetch('/api/posts.json')
.then(res => {
if (!res.ok) throw new Error(`Failed to load posts: ${res.status}`);
return res.json();
})
.then(posts => {
const postLinks = posts
.map(post => post && post.href)
.filter(href => typeof href === 'string' && href.startsWith('/posts/'));
if (postLinks.length === 0) {
console.warn('No posts available for random navigation.');
reset();
return;
}
startCardFlight(posts);
finish(postLinks[Math.floor(Math.random() * postLinks.length)]);
})
.catch(error => {
console.error('Random post navigation failed:', error);
reset();
});
}
+528
View File
@@ -0,0 +1,528 @@
(function () {
'use strict';
// ============================================================================
// 配置区域 - 请根据实际情况修改
// ============================================================================
const CONFIG = window.SITE_CONFIG?.search;
if (!CONFIG) {
console.warn('Typesense 搜索配置未加载');
return;
}
// ============================================================================
// 状态管理
// ============================================================================
let searchInstance = null;
let isInitialized = false;
let isSearchOpen = false;
let initRetryCount = 0;
const MAX_INIT_RETRIES = CONFIG.ui.maxInitRetries || 30; // 最多重试30次 (3秒)
// ============================================================================
// 错误提示函数
// ============================================================================
function showErrorMessage() {
const hitsContainer = document.getElementById('hits');
if (!hitsContainer) return;
hitsContainer.innerHTML =
'<div class="ts-empty">' +
'<div style="color: #f44336;"><i class="fas fa-exclamation-triangle" style="font-size: 3rem;"></i></div>' +
'<div style="font-size: 1.1rem; font-weight: bold; margin: 15px 0;">搜索服务加载失败</div>' +
'<div style="font-size: 0.9rem; color: #666; line-height: 1.8;">' +
'<p>依赖库未能正确加载,请检查以下配置:</p>' +
'<ol style="text-align: left; max-width: 500px; margin: 15px auto;">' +
'<li>确认已在 <code>_config.butterfly.yml</code> 中正确引入依赖</li>' +
'<li>检查 JS 文件加载顺序(先 instantsearch.js,再 adapter</li>' +
'<li>尝试更换 CDN 或使用本地文件</li>' +
'<li>打开浏览器控制台查看详细错误信息</li>' +
'</ol>' +
'</div>' +
'<div style="margin-top: 20px;">' +
'<button onclick="location.reload()" style="padding: 10px 20px; background: #49b1f5; color: white; border: none; border-radius: 5px; cursor: pointer;">重新加载页面</button>' +
'</div>' +
'</div>';
}
// ============================================================================
// 1. 动态插入 HTML 结构
// ============================================================================
const searchHTML = `
<div id="typesense-search-mask" class="ts-mask" style="display:none;">
<div id="typesense-search-container" class="ts-container">
<div class="ts-header">
<span class="ts-title">
<i class="fas fa-search"></i> 本站搜索
</span>
<span id="close-typesense" class="ts-close" aria-label="关闭搜索">&times;</span>
</div>
<div id="searchbox"></div>
<div id="stats" class="ts-stats"></div>
<div id="hits" class="ts-hits"></div>
<div id="pagination" class="ts-pagination"></div>
<div class="ts-footer">
<small>Search powered by <strong>Typesense</strong></small>
</div>
</div>
</div>
<style>
.ts-mask {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
z-index: 10000;
backdrop-filter: blur(5px);
-webkit-backdrop-filter: blur(5px);
opacity: 0;
transition: opacity 300ms ease;
}
.ts-mask.active { opacity: 1; }
.ts-container {
margin: 5% auto;
width: 90%;
max-width: 650px;
background: var(--search-bg, var(--card-bg, #fff));
padding: 25px;
border-radius: 12px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.3);
position: relative;
z-index: 10001;
transform: translateY(-50px);
opacity: 0;
transition: all 300ms ease;
}
.ts-mask.active .ts-container {
transform: translateY(0);
opacity: 1;
}
.ts-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
border-bottom: 2px solid var(--text-highlight-color, #49b1f5);
padding-bottom: 10px;
}
.ts-title {
font-size: 1.2rem;
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
}
.ts-close {
cursor: pointer;
font-size: 28px;
color: var(--font-color, #333);
line-height: 1;
transition: color 0.2s, transform 0.2s;
}
.ts-close:hover {
color: var(--text-highlight-color, #49b1f5);
transform: scale(1.1);
}
.ais-SearchBox-input {
position: relative;
z-index: 10002;
cursor: text;
padding: 12px 40px 12px 15px !important;
border-radius: 8px !important;
border: 2px solid #eee !important;
width: 100%;
outline: none;
transition: border-color 0.3s, box-shadow 0.3s;
background: var(--card-bg, #fff);
color: var(--font-color, #333);
font-size: 1rem;
}
.ais-SearchBox-input:focus {
border-color: var(--text-highlight-color, #49b1f5) !important;
box-shadow: 0 0 0 3px rgba(73, 177, 245, 0.1);
}
.ts-stats {
margin: 10px 0;
font-size: 0.85rem;
color: var(--font-color, #666);
opacity: 0.8;
}
.ts-hits {
max-height: 55vh;
overflow-y: auto;
margin-top: 15px;
padding-right: 5px;
}
.ts-hits::-webkit-scrollbar { width: 6px; }
.ts-hits::-webkit-scrollbar-track {
background: var(--card-bg, #f1f1f1);
border-radius: 10px;
}
.ts-hits::-webkit-scrollbar-thumb {
background: var(--text-highlight-color, #49b1f5);
border-radius: 10px;
}
.ts-empty {
text-align: center;
padding: 40px 20px;
color: var(--font-color, #999);
}
.ts-empty i {
font-size: 3rem;
margin-bottom: 15px;
opacity: 0.3;
}
.ts-empty code {
background: #f5f5f5;
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
color: #e91e63;
}
.ts-empty ol {
padding-left: 20px;
}
.ts-empty li {
margin: 8px 0;
}
.ts-result-item {
border-radius: 8px;
transition: all 0.2s ease;
margin-bottom: 10px;
padding: 15px;
border: 1px solid transparent;
text-decoration: none;
display: block;
background: var(--card-bg, #fff);
}
.ts-result-item:hover {
background: var(--text-bg-hover, rgba(73, 177, 245, 0.05));
border-color: var(--text-highlight-color, #49b1f5);
transform: translateX(5px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.ts-result-title {
font-weight: bold;
color: var(--text-highlight-color, #49b1f5);
font-size: 1.1rem;
margin-bottom: 8px;
display: block;
line-height: 1.4;
}
.ts-result-content {
font-size: 0.9rem;
color: var(--font-color, #666);
line-height: 1.6;
opacity: 0.85;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.ts-result-item mark {
background: #ffeb3b;
color: #000;
padding: 2px 4px;
border-radius: 3px;
font-weight: 500;
}
.ts-pagination {
margin-top: 20px;
display: flex;
justify-content: center;
gap: 5px;
}
.ais-Pagination-list {
display: flex;
list-style: none;
padding: 0;
margin: 0;
gap: 5px;
}
.ais-Pagination-link {
display: block;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 5px;
color: var(--font-color, #333);
text-decoration: none;
transition: all 0.2s;
background: var(--card-bg, #fff);
}
.ais-Pagination-link:hover {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ais-Pagination-item--selected .ais-Pagination-link {
background: var(--text-highlight-color, #49b1f5);
color: #fff;
}
.ts-footer {
text-align: right;
margin-top: 15px;
border-top: 1px solid var(--border-color, #eee);
padding-top: 10px;
}
@media (max-width: 768px) {
.ts-container {
margin: 10px;
width: calc(100% - 20px);
padding: 20px 15px;
}
.ts-hits { max-height: 50vh; }
}
[data-theme="dark"] .ts-mask,
.dark-mode .ts-mask {
background: rgba(0, 0, 0, 0.85);
}
</style>
`;
document.body.insertAdjacentHTML('beforeend', searchHTML);
const mask = document.getElementById('typesense-search-mask');
const closeBtn = document.getElementById('close-typesense');
const container = document.getElementById('typesense-search-container');
// ============================================================================
// 搜索控制
// ============================================================================
function openSearch() {
if (isSearchOpen) return;
isSearchOpen = true;
mask.style.display = 'block';
void mask.offsetWidth;
mask.classList.add('active');
document.body.style.overflow = 'hidden';
if (!isInitialized) {
initTypesense();
}
focusSearchInput();
}
function closeSearch() {
if (!isSearchOpen) return;
isSearchOpen = false;
mask.classList.remove('active');
setTimeout(function() {
mask.style.display = 'none';
document.body.style.overflow = '';
}, CONFIG.ui.animationDuration || 300);
}
function focusSearchInput(retryCount) {
retryCount = retryCount || 0;
const input = document.querySelector('.ais-SearchBox-input');
if (input) {
input.focus();
input.select();
} else if (retryCount < (CONFIG.ui.maxRetries || 10)) {
setTimeout(function() {
focusSearchInput(retryCount + 1);
}, CONFIG.ui.retryDelay || 100);
}
}
// ============================================================================
// 事件监听
// ============================================================================
document.addEventListener('click', function(e) {
if (e.target.closest('.search-typesense-trigger')) {
e.preventDefault();
openSearch();
}
});
closeBtn.addEventListener('click', closeSearch);
mask.addEventListener('click', function(e) {
if (e.target === mask) closeSearch();
});
container.addEventListener('click', function(e) {
e.stopPropagation();
});
window.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && isSearchOpen) {
closeSearch();
}
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
e.preventDefault();
openSearch();
}
});
// ============================================================================
// Typesense 初始化(带重试限制)
// ============================================================================
function initTypesense() {
if (isInitialized || searchInstance) {
console.warn('Typesense 搜索已初始化');
return;
}
var instantsearchLoaded = typeof instantsearch !== 'undefined';
var adapterLoaded = typeof TypesenseInstantSearchAdapter !== 'undefined' ||
typeof window.TypesenseInstantSearchAdapter !== 'undefined';
console.log('📦 依赖库检查 (' + (initRetryCount + 1) + '/' + MAX_INIT_RETRIES + '):');
console.log(' instantsearch.js:', instantsearchLoaded ? '✅ 已加载' : '❌ 未加载');
console.log(' TypesenseAdapter:', adapterLoaded ? '✅ 已加载' : '❌ 未加载');
if (!instantsearchLoaded || !adapterLoaded) {
initRetryCount++;
if (initRetryCount >= MAX_INIT_RETRIES) {
console.error('');
console.error('❌ Typesense 依赖库加载失败!');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.error('');
console.error('🔧 请检查 _config.butterfly.yml 配置:');
console.error('');
console.error('inject:');
console.error(' bottom: # ⚠️ 使用 bottom 而不是 head');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4.56.0"></script>');
console.error(' - <script src="https://cdn.jsdelivr.net/npm/typesense-instantsearch-adapter@2.7.0/dist/typesense-instantsearch-adapter.min.js"></script>');
console.error(' - <script src="/js/typesense-search-fixed.js"></script>');
console.error('');
console.error('💡 或在控制台手动检查:');
console.error(' typeof instantsearch');
console.error(' typeof TypesenseInstantSearchAdapter');
console.error('');
console.error('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
showErrorMessage();
return;
}
console.warn('⏳ 100ms 后重试...');
setTimeout(initTypesense, 100);
return;
}
initRetryCount = 0;
console.log('🚀 开始初始化 Typesense...');
try {
const typesenseAdapter = new TypesenseInstantSearchAdapter({
server: {
apiKey: CONFIG.apiKey,
nodes: [{
host: CONFIG.server.host,
port: CONFIG.server.port,
protocol: CONFIG.server.protocol
}],
cacheSearchResultsForSeconds: 120
},
additionalSearchParameters: CONFIG.searchParams
});
searchInstance = instantsearch({
searchClient: typesenseAdapter.searchClient,
indexName: CONFIG.indexName,
routing: false
});
searchInstance.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox',
placeholder: CONFIG.ui.placeholder || '输入关键词寻找故事...',
autofocus: true,
showReset: true,
showSubmit: false,
showLoadingIndicator: true
}),
instantsearch.widgets.stats({
container: '#stats',
templates: {
text: function(data) {
if (!data.query) return '';
return '找到 <strong>' + data.nbHits + '</strong> 条结果 (' + data.processingTimeMS + 'ms)';
}
}
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
empty: function(results) {
return '<div class="ts-empty">' +
'<div><i class="fas fa-search"></i></div>' +
'<div>找不到与 "<strong>' + results.query + '</strong>" 相关的内容</div>' +
'<div style="margin-top: 10px;">试试其他关键词吧 (´·ω·`)</div>' +
'</div>';
},
item: function(hit) {
// 使用 _highlightResult 获取高亮文本
var titleHighlight = hit._highlightResult && hit._highlightResult.title
? hit._highlightResult.title.value
: (hit.title || '');
var contentHighlight = hit._highlightResult && hit._highlightResult.content
? hit._highlightResult.content.value
: (hit.content || '');
// 截取内容长度
if (contentHighlight.length > 200) {
contentHighlight = contentHighlight.substring(0, 200) + '...';
}
return '<a href="' + hit.url + '" class="ts-result-item">' +
'<div class="ts-result-title">' + titleHighlight + '</div>' +
'<div class="ts-result-content">' + contentHighlight + '</div>' +
'</a>';
}
}
}),
instantsearch.widgets.pagination({
container: '#pagination',
padding: 2,
showFirst: false,
showLast: false
})
]);
searchInstance.start();
isInitialized = true;
console.log('✅ Typesense 初始化成功!');
searchInstance.on('render', function() {
if (isSearchOpen) {
const input = document.querySelector('.ais-SearchBox-input');
if (input && document.activeElement !== input) {
input.focus();
}
}
});
} catch (error) {
console.error('❌ 初始化失败:', error);
showErrorMessage();
}
}
// ============================================================================
// 初始化
// ============================================================================
function init() {
console.log('🔍 Typesense 搜索已准备就绪');
console.log('💡 快捷键: Ctrl/Cmd + K');
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
// ============================================================================
// 全局接口
// ============================================================================
window.TypesenseSearch = {
open: openSearch,
close: closeSearch,
isOpen: function() { return isSearchOpen; },
getInstance: function() { return searchInstance; }
};
})();
+363
View File
@@ -0,0 +1,363 @@
function renderTalks() {
const talkContainer = document.querySelector('#talk');
if (!talkContainer) return;
const config = window.SITE_CONFIG?.talks || {};
talkContainer.innerHTML = '';
const generateIconSVG = () => {
return `<svg viewBox="0 0 512 512"xmlns="http://www.w3.org/2000/svg"class="is-badge icon"><path d="m512 268c0 17.9-4.3 34.5-12.9 49.7s-20.1 27.1-34.6 35.4c.4 2.7.6 6.9.6 12.6 0 27.1-9.1 50.1-27.1 69.1-18.1 19.1-39.9 28.6-65.4 28.6-11.4 0-22.3-2.1-32.6-6.3-8 16.4-19.5 29.6-34.6 39.7-15 10.2-31.5 15.2-49.4 15.2-18.3 0-34.9-4.9-49.7-14.9-14.9-9.9-26.3-23.2-34.3-40-10.3 4.2-21.1 6.3-32.6 6.3-25.5 0-47.4-9.5-65.7-28.6-18.3-19-27.4-42.1-27.4-69.1 0-3 .4-7.2 1.1-12.6-14.5-8.4-26-20.2-34.6-35.4-8.5-15.2-12.8-31.8-12.8-49.7 0-19 4.8-36.5 14.3-52.3s22.3-27.5 38.3-35.1c-4.2-11.4-6.3-22.9-6.3-34.3 0-27 9.1-50.1 27.4-69.1s40.2-28.6 65.7-28.6c11.4 0 22.3 2.1 32.6 6.3 8-16.4 19.5-29.6 34.6-39.7 15-10.1 31.5-15.2 49.4-15.2s34.4 5.1 49.4 15.1c15 10.1 26.6 23.3 34.6 39.7 10.3-4.2 21.1-6.3 32.6-6.3 25.5 0 47.3 9.5 65.4 28.6s27.1 42.1 27.1 69.1c0 12.6-1.9 24-5.7 34.3 16 7.6 28.8 19.3 38.3 35.1 9.5 15.9 14.3 33.4 14.3 52.4zm-266.9 77.1 105.7-158.3c2.7-4.2 3.5-8.8 2.6-13.7-1-4.9-3.5-8.8-7.7-11.4-4.2-2.7-8.8-3.6-13.7-2.9-5 .8-9 3.2-12 7.4l-93.1 140-42.9-42.8c-3.8-3.8-8.2-5.6-13.1-5.4-5 .2-9.3 2-13.1 5.4-3.4 3.4-5.1 7.7-5.1 12.9 0 5.1 1.7 9.4 5.1 12.9l58.9 58.9 2.9 2.3c3.4 2.3 6.9 3.4 10.3 3.4 6.7-.1 11.8-2.9 15.2-8.7z"fill="#1da1f2"></path></svg>`;
}
const waterfall = (a) => {
function b(a, b) {
var c = window.getComputedStyle(b);
return parseFloat(c["margin" + a]) || 0
}
function c(a) {
return a + "px"
}
function d(a) {
return parseFloat(a.style.top)
}
function e(a) {
return parseFloat(a.style.left)
}
function f(a) {
return a.clientWidth
}
function g(a) {
return a.clientHeight
}
function h(a) {
return d(a) + g(a) + b("Bottom", a)
}
function i(a) {
return e(a) + f(a) + b("Right", a)
}
function j(a) {
a = a.sort(function (a, b) {
return h(a) === h(b) ? e(b) - e(a) : h(b) - h(a)
})
}
function k(b) {
f(a) != t && (b.target.removeEventListener(b.type, arguments.callee), waterfall(a))
}
"string" == typeof a && (a = document.querySelector(a));
var l = [].map.call(a.children, function (a) {
return a.style.position = "absolute", a
});
a.style.position = "relative";
var m = [];
l.length && (l[0].style.top = "0px", l[0].style.left = c(b("Left", l[0])), m.push(l[0]));
for (var n = 1; n < l.length; n++) {
var o = l[n - 1],
p = l[n],
q = i(o) + f(p) <= f(a);
if (!q) break;
p.style.top = o.style.top, p.style.left = c(i(o) + b("Left", p)), m.push(p)
}
for (; n < l.length; n++) {
j(m);
var p = l[n],
r = m.pop();
p.style.top = c(h(r) + b("Top", p)), p.style.left = c(e(r)), m.push(p)
}
j(m);
var s = m[0];
a.style.height = c(h(s) + b("Bottom", s));
var t = f(a);
window.addEventListener ? window.addEventListener("resize", k) : document.body.onresize = k
};
const fetchAndRenderTalks = () => {
const url = config.apiUrl;
if (!url) return;
const cacheKey = config.cacheKey || 'talksCache';
const cacheTimeKey = config.cacheTimeKey || 'talksCacheTime';
const cacheDuration = config.cacheDurationMs || 30 * 60 * 1000;
const cachedData = localStorage.getItem(cacheKey);
const cachedTime = localStorage.getItem(cacheTimeKey);
const now = Date.now();
if (cachedData && cachedTime && (now - cachedTime < cacheDuration)) {
renderTalksList(JSON.parse(cachedData));
} else {
fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: config.pageSize || 30 })
})
.then(res => res.json())
.then(data => {
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(cacheKey, JSON.stringify(data.data.items));
localStorage.setItem(cacheTimeKey, now.toString());
renderTalksList(data.data.items);
}
})
.catch(err => console.error('Error fetching:', err));
}
};
const renderTalksList = (list) => {
list.map(formatTalk).forEach(item => talkContainer.appendChild(generateTalkElement(item)));
waterfall('#talk');
};
const withImageParams = (url) => {
if (!url) return '';
try {
const parsedUrl = new URL(url, window.location.origin);
if (!parsedUrl.searchParams.has('fmt')) parsedUrl.searchParams.set('fmt', 'webp');
if (!parsedUrl.searchParams.has('q')) parsedUrl.searchParams.set('q', '75');
return parsedUrl.toString();
} catch {
return url;
}
};
const getImageUrl = (image) => {
if (!image) return '';
if (typeof image === 'string') return image;
return image.image_url || image.url || image.src || image.file?.url || '';
};
const getTalkImages = (item) => {
if (Array.isArray(item.images) && item.images.length > 0) {
return item.images.map(getImageUrl).filter(Boolean);
}
if (Array.isArray(item.echo_files) && item.echo_files.length > 0) {
return item.echo_files
.map((echoFile) => getImageUrl(echoFile.file || echoFile))
.filter(Boolean);
}
return [];
};
const formatTalk = (item) => {
const date = formatTime(item.created_at);
let content = item.content || '';
content = content.replace(/\[(.*?)\]\((.*?)\)/g, `<a href="$2" target="_blank" rel="nofollow noopener">@$1</a>`)
.replace(/- \[ \]/g, '⚪')
.replace(/- \[x\]/g, '⚫')
.replace(/\n/g, '<br>');
content = `<div class="talk_content_text">${content}</div>`;
// 图片
const images = getTalkImages(item);
if (images.length > 0) {
const imgDiv = document.createElement('div');
imgDiv.className = 'zone_imgbox';
images.forEach(url => {
const imageUrl = withImageParams(url);
const link = document.createElement('a');
link.href = imageUrl;
link.setAttribute('data-fancybox', 'gallery');
link.className = 'fancybox';
const imgTag = document.createElement('img');
imgTag.src = imageUrl;
imgTag.loading = 'lazy';
imgTag.alt = '';
link.appendChild(imgTag);
imgDiv.appendChild(link);
});
content += imgDiv.outerHTML;
}
// 外链 / GitHub 项目
// 外链 / GitHub 项目
if (['WEBSITE', 'GITHUBPROJ'].includes(item.extension_type)) {
let siteUrl = '', title = '';
let extensionBack = "https://pic.biss.click/image/1971bdc1-4349-4bb9-b683-20404f5da7d7.webp";
// 解析 extension 字段
try {
const extObj = typeof item.extension === 'string' ? JSON.parse(item.extension) : item.extension;
siteUrl = extObj.site || extObj.url || item.extension;
title = extObj.title || siteUrl;
} catch {
siteUrl = item.extension;
title = siteUrl;
}
// 特殊处理 GitHub 项目
if (item.extension_type === 'GITHUBPROJ') {
extensionBack = "https://pic.biss.click/image/ed410d4e-d3f8-4b26-8840-50dd58f7dc4e.webp";
// 提取 GitHub 项目名
const match = siteUrl.match(/^https?:\/\/github\.com\/[^/]+\/([^/?#]+)/i);
if (match) {
title = match[1]; // 获取仓库名
} else {
// fallback:从最后一个路径段提取
try {
const parts = new URL(siteUrl).pathname.split('/').filter(Boolean);
title = parts.pop() || siteUrl;
} catch {
// 如果 URL 无效则保留原始
}
}
}
// 输出 HTML 结构
content += `
<div class="shuoshuo-external-link">
<a class="external-link" href="${siteUrl}" target="_blank" rel="nofollow noopener">
<div class="external-link-left" style="background-image:url(${extensionBack})"></div>
<div class="external-link-right">
<div class="external-link-title">${title}</div>
<div>点击跳转<i class="fa-solid fa-angle-right"></i></div>
</div>
</a>
</div>`;
}
// 音乐
if (item.extension_type === 'MUSIC' && item.extension) {
const link = item.extension;
let server = '';
if (link.includes('music.163.com')) server = 'netease';
else if (link.includes('y.qq.com')) server = 'tencent';
const idMatch = link.match(/id=(\d+)/);
const id = idMatch ? idMatch[1] : '';
if (server && id) {
content += `<meting-js server="${server}" type="song" id="${id}" api="https://meting.qjqq.cn/?server=:server&type=:type&id=:id&auth=:auth&r=:r"></meting-js>`;
}
}
// 视频
if (item.extension_type === 'VIDEO' && item.extension) {
const video = item.extension;
if (video.startsWith('BV')) {
const bilibiliUrl = `https://www.bilibili.com/blackboard/html5mobileplayer.html?bvid=${video}&as_wide=1&high_quality=1&danmaku=0`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${bilibiliUrl}"
frameborder="no"
allowfullscreen="true"
loading="lazy"></iframe>
</div>`;
} else {
const youtubeUrl = `https://www.youtube.com/embed/${video}`;
content += `
<div style="position: relative; padding: 30% 45%; margin-top: 10px;">
<iframe style="position:absolute;width:100%;height:100%;left:0;top:0;border-radius:12px;"
src="${youtubeUrl}"
title="YouTube video player"
frameborder="0"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen></iframe>
</div>`;
}
}
return {
content,
user: item.username || '匿名',
avatar: config.fallbackAvatar || window.SITE_CONFIG?.author?.avatar || '',
date,
location: '',
tags: Array.isArray(item.tags) && item.tags.length ? item.tags.map(t => t.name) : ['无标签'],
text: content.replace(/\[(.*?)\]\((.*?)\)/g, '[链接]')
};
};
const generateTalkElement = (item) => {
const talkItem = document.createElement('div');
talkItem.className = 'talk_item';
const talkMeta = document.createElement('div');
talkMeta.className = 'talk_meta';
const avatar = document.createElement('img');
avatar.className = 'no-lightbox avatar';
avatar.src = item.avatar;
const info = document.createElement('div');
info.className = 'info';
const nick = document.createElement('span');
nick.className = 'talk_nick';
nick.innerHTML = `${item.user} ${generateIconSVG()}`;
const date = document.createElement('span');
date.className = 'talk_date';
date.textContent = item.date;
info.appendChild(nick);
info.appendChild(date);
talkMeta.appendChild(avatar);
talkMeta.appendChild(info);
const talkContent = document.createElement('div');
talkContent.className = 'talk_content';
talkContent.innerHTML = item.content;
const talkBottom = document.createElement('div');
talkBottom.className = 'talk_bottom';
const tags = document.createElement('div');
const tag = document.createElement('span');
tag.className = 'talk_tag';
tag.textContent = `🏷️${item.tags}`;
//const loc = document.createElement('span');
//loc.className = 'location_tag';
//loc.textContent = `🌍${item.location}`;
tags.appendChild(tag);
//tags.appendChild(loc);
const commentLink = document.createElement('a');
commentLink.href = 'javascript:;';
commentLink.onclick = () => goComment(item.text);
const icon = document.createElement('span');
icon.className = 'icon';
icon.innerHTML = '<i class="fa-solid fa-message fa-fw"></i>';
commentLink.appendChild(icon);
talkBottom.appendChild(tags);
talkBottom.appendChild(commentLink);
talkItem.appendChild(talkMeta);
talkItem.appendChild(talkContent);
talkItem.appendChild(talkBottom);
return talkItem;
};
const goComment = (e) => {
const match = e.match(/<div class="talk_content_text">([\s\S]*?)<\/div>/);
const textContent = match ? match[1] : "";
const textarea = document.querySelector("#twikoo .el-textarea__inner");
textarea.value = `> ${textContent}\n\n`;
textarea.focus();
btf.snackbarShow("已为您引用该说说,不删除空格效果更佳");
};
const formatTime = (time) => {
const timestamp = Number(time);
const d = Number.isFinite(timestamp)
? new Date(timestamp < 100000000000 ? timestamp * 1000 : timestamp)
: new Date(time);
const pad = (n) => n.toString().padStart(2, '0');
if (Number.isNaN(d.getTime())) return '';
return `${d.getFullYear()}-${pad(d.getMonth()+1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`;
};
fetchAndRenderTalks();
}
renderTalks();
// function whenDOMReady() {
// const talkContainer = document.querySelector('#talk');
// talkContainer.innerHTML = '';
// fetchAndRenderTalks();
// }
// whenDOMReady();
// document.addEventListener("pjax:complete", whenDOMReady);
+103
View File
@@ -0,0 +1,103 @@
let talkTimer = null;
const cacheKey = 'talksCache';
const cacheTimeKey = 'talksCacheTime';
const cacheDuration = 30 * 60 * 1000; // 缓存有效期 30分钟
function indexTalk() {
if (talkTimer) {
clearInterval(talkTimer);
talkTimer = null;
}
if (!document.getElementById('bber-talk')) return;
const config = window.SITE_CONFIG?.talks || {};
const talksCacheKey = config.cacheKey || cacheKey;
const talksCacheTimeKey = config.cacheTimeKey || cacheTimeKey;
const talksCacheDuration = config.cacheDurationMs || cacheDuration;
function toText(ls) {
return ls.map(item => {
let c = item.content || '';
const hasImg = /\!\[.*?\]\(.*?\)/.test(c);
const hasLink = /\[.*?\]\(.*?\)/.test(c);
c = c
.replace(/#(.*?)\s/g, '')
.replace(/\{.*?\}/g, '')
.replace(/\!\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-image"></i>')
.replace(/\[.*?\]\(.*?\)/g, '<i class="fa-solid fa-link"></i>');
const icons = [];
if (item.images?.length && !hasImg) icons.push('fa-solid fa-image');
if (item.extension_type === 'VIDEO') icons.push('fa-solid fa-video');
if (item.extension_type === 'MUSIC') icons.push('fa-solid fa-music');
if (item.extension_type === 'WEBSITE' && !hasLink) icons.push('fa-solid fa-link');
if (item.extension_type === 'GITHUBPROJ' && !hasLink) icons.push('fab fa-github');
if (icons.length) c += ' ' + icons.map(i => `<i class="${i}"></i>`).join(' ');
return c;
});
}
// 渲染与轮播
function talk(ls) {
let html = '';
ls.forEach((item, i) => {
html += `<li class="item item-${i + 1}">${item}</li>`;
});
let box = document.querySelector("#bber-talk .talk-list");
if (!box) return;
box.innerHTML = html;
talkTimer = setInterval(() => {
if (box.children.length > 0) {
box.appendChild(box.children[0]);
}
}, 3000);
}
const cachedData = localStorage.getItem(talksCacheKey);
const cachedTime = localStorage.getItem(talksCacheTimeKey);
const currentTime = new Date().getTime();
// 判断缓存是否有效
if (cachedData && cachedTime && (currentTime - cachedTime < talksCacheDuration)) {
const data = toText(JSON.parse(cachedData));
talk(data.slice(0, config.homeLimit || 6)); // 使用缓存渲染数据
} else {
if (!config.apiUrl) return;
fetch(config.apiUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ page: 1, pageSize: config.pageSize || 30 })
})
.then(res => res.json())
.then(data => {
// 适配新版结构:code=1 且 data.items 存在
if (data.code === 1 && data.data && Array.isArray(data.data.items)) {
localStorage.setItem(talksCacheKey, JSON.stringify(data.data.items));
localStorage.setItem(talksCacheTimeKey, currentTime.toString());
const formattedData = toText(data.data.items);
talk(formattedData.slice(0, config.homeLimit || 6));
} else {
console.warn('Unexpected API response format:', data);
}
})
.catch(error => console.error('Error fetching data:', error));
}
}
// pjax 支持
function whenDOMReady() {
indexTalk();
}
whenDOMReady();
document.addEventListener("pjax:complete", whenDOMReady);
+204
View File
@@ -0,0 +1,204 @@
// weather.js
(function () {
const WEATHER_ID = "aside-weather";
const LOCATION_WAIT_MS = 3000;
const LOCATION_POLL_INTERVAL_MS = 100;
const WEATHER_KEY = "70201debab4245379e376d116b05f8a2";
const WEATHER_HOST = "https://ny5vxmbbkd.re.qweatherapi.com";
const WEATHER_LANG = "zh";
function debugLog(label, payload) {
console.log(`[weather.js] ${label}`, payload);
}
function maskSecret(secret) {
if (!secret || secret.length < 10) return secret;
return `${secret.slice(0, 4)}***${secret.slice(-4)}`;
}
function readExistingLocation() {
return window.ipLocation?.result?.location || null;
}
function waitForExistingLocation(timeout = LOCATION_WAIT_MS) {
return new Promise((resolve, reject) => {
const existingLocation = readExistingLocation();
if (existingLocation) {
resolve(existingLocation);
return;
}
const startTime = Date.now();
const timer = window.setInterval(() => {
const location = readExistingLocation();
if (location) {
window.clearInterval(timer);
resolve(location);
return;
}
if (Date.now() - startTime >= timeout) {
window.clearInterval(timer);
reject(new Error("等待 ipLocation 超时"));
}
}, LOCATION_POLL_INTERVAL_MS);
});
}
function requestLocationFallback() {
return new Promise((resolve, reject) => {
if (!window.$?.ajax) {
reject(new Error("jQuery 未加载,无法执行定位兜底请求"));
return;
}
$.ajax({
type: 'get',
url: 'https://apis.map.qq.com/ws/location/v1/ip',
data: {
key: '你的key',
output: 'jsonp',
callback: '?',
},
dataType: 'jsonp',
success: function (res) {
debugLog("定位兜底返回", res);
if (res && res.status === 0 && res.result?.location) {
window.ipLocation = res;
resolve(res.result.location);
} else {
reject(new Error("定位失败"));
}
},
error: function () {
reject(new Error("定位请求失败"));
}
});
});
}
function getLocation() {
return waitForExistingLocation()
.then(location => {
debugLog("使用 txmap.js 定位", location);
return location;
})
.catch(error => {
debugLog("等待 txmap.js 定位失败,转兜底请求", error);
return requestLocationFallback();
});
}
function getWeather(lng, lat) {
const location = `${lng.toFixed(2)},${lat.toFixed(2)}`;
const params = new URLSearchParams({
location,
lang: WEATHER_LANG,
key: WEATHER_KEY
});
const requestUrl = `${WEATHER_HOST}/v7/weather/now?${params.toString()}`;
debugLog("天气请求参数", {
host: WEATHER_HOST,
location,
lang: WEATHER_LANG,
key: maskSecret(WEATHER_KEY)
});
return fetch(requestUrl, {
headers: {
Accept: "application/json"
}
}).then(async res => {
const responseText = await res.text();
let data;
try {
data = JSON.parse(responseText);
} catch (parseError) {
debugLog("天气接口返回非 JSON", {
status: res.status,
statusText: res.statusText,
body: responseText
});
throw new Error(`天气接口返回非 JSONHTTP ${res.status}`);
}
debugLog("天气接口响应", {
status: res.status,
statusText: res.statusText,
body: data
});
if (!res.ok) {
const detail = data?.error?.detail || data?.msg || `HTTP ${res.status}`;
throw new Error(`天气接口请求失败:${detail}`);
}
return data;
});
}
function renderWeather(data) {
const el = document.getElementById(WEATHER_ID);
if (!el) return;
if (data.code !== "200") {
debugLog("天气接口业务错误", data);
el.innerHTML = `天气获取失败:${data.code || "未知错误"}`;
return;
}
const now = data.now;
const emojiMap = {
"晴": "☀️",
"多云": "⛅",
"阴": "☁️",
"雨": "🌧️",
"雪": "❄️",
"雷": "⛈️"
};
const emoji = emojiMap[now.text] || "🌡️";
const weatherSentence = `当前天气${now.text},气温${now.temp}℃,体感${now.feelsLike}℃,${now.windDir}${now.windScale}级,湿度${now.humidity}%,能见度${now.vis}km。`;
el.innerHTML = `
<div class="aside-weather-card">
<div class="weather-top">
</div>
<div class="weather-info">
${weatherSentence}
</div>
</div>
`;
}
function initWeather() {
const el = document.getElementById(WEATHER_ID);
if (!el) return;
el.innerHTML = "🌥️ 获取天气中...";
getLocation()
.then(loc => getWeather(loc.lng, loc.lat))
.then(renderWeather)
.catch(err => {
console.error("[weather.js] 天气加载失败", err);
el.innerHTML = `🌧️ 天气加载失败:${err.message || "未知错误"}`;
});
}
// 🚀 初次加载
window.addEventListener("load", initWeather);
// 🔄 pjax
document.addEventListener("pjax:complete", initWeather);
})();