更新包

-- [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
File diff suppressed because one or more lines are too long
+384
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
[{"title":"网站迁移日志","href":"/posts/8f3b9d21/"},{"title":"用 Umami 分享链接给网站信息卡提供统计数据","href":"/posts/4c91a8f2/"},{"title":"把普通的一天过成自己的节奏","href":"/posts/efe1e805/"},{"title":"GPT Image2 初试","href":"/posts/3f8de22/"},{"title":"电子价签玩法","href":"/posts/8712baaa/"},{"title":"OpenGL- Cohen-Sutherland 编码裁剪算法","href":"/posts/9b2ef00a/"},{"title":"OpenGL-直线的扫描转换","href":"/posts/7207243b/"},{"title":"OpenGL-基础程序","href":"/posts/437a5198/"},{"title":"配置OpenGL环境","href":"/posts/c4477b0c/"},{"title":"opencv应用-算术运算","href":"/posts/b559997d/"},{"title":"opencv应用-基础操作","href":"/posts/e8f95ead/"},{"title":"配置opencv","href":"/posts/6e3332b/"},{"title":"自建bitwarden服务","href":"/posts/7baa41fc/"},{"title":"自建renovate-bot","href":"/posts/56f57c0b/"},{"title":"新年快乐!","href":"/posts/5785bd01/"},{"title":"添加网站左上角菜单","href":"/posts/7e921903/"},{"title":"将博客仓库转移到gitea","href":"/posts/d2c8521/"},{"title":"安装gitea","href":"/posts/34725d47/"},{"title":"添加typesense搜索","href":"/posts/f287c563/"},{"title":"自定义字体","href":"/posts/b5601a7e/"},{"title":"在侧边栏添加日历和倒计时","href":"/posts/5ed2f1e6/"},{"title":"和朋友们的故事","href":"/posts/a31d95d9/"},{"title":"Adguard和Openclash共存","href":"/posts/66e66374/"},{"title":"Adguard规则分享","href":"/posts/69b16001/"},{"title":"在Openwrt上安装AdguardHome","href":"/posts/b57500e9/"},{"title":"在Openwrt中安装OpenClash","href":"/posts/2b2fb1a7/"},{"title":"Cudy TR3000刷入Openwrt系统","href":"/posts/8802e397/"},{"title":"搭建Owncloud并集成Onlyoffice","href":"/posts/59c8572a/"},{"title":"为博客添加CMS系统","href":"/posts/3a26a97a/"},{"title":"为1Panel添加自己想要的应用","href":"/posts/bb18d851/"},{"title":"使用GitHub推送Hexo到服务器","href":"/posts/ce1ec3fe/"},{"title":"C# 基本语法","href":"/posts/e9a8e898/"},{"title":"C# 入门","href":"/posts/bc56f789/"},{"title":"自定义页脚(新)","href":"/posts/c6143ad3/"},{"title":"自定义分类条","href":"/posts/33249733/"},{"title":"自定义导航栏","href":"/posts/3e61a389/"},{"title":"利用 SiteMap 随机访问站内页面","href":"/posts/b5866b9e/"},{"title":"博客添加AI总结","href":"/posts/41b9aff7/"},{"title":"利用插件自定义页脚菜单","href":"/posts/a6ab0925/"},{"title":"英语文章分享-去奋力生活吧,就好像死亡近在咫尺","href":"/posts/f0a1a1f4/"},{"title":"自定义右键菜单","href":"/posts/8bdb35fb/"},{"title":"在侧边栏中添加欢迎信息","href":"/posts/da17d00/"},{"title":"配置说说页面","href":"/posts/ad244066/"},{"title":"部署历程","href":"/posts/6e5f5039/"}]
+380
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+384
View File
File diff suppressed because one or more lines are too long
+383
View File
File diff suppressed because one or more lines are too long
+501
View File
File diff suppressed because one or more lines are too long
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 457 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

+626
View File
File diff suppressed because one or more lines are too long
+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);
})();
+378
View File
File diff suppressed because one or more lines are too long
+46
View File
@@ -0,0 +1,46 @@
{
"id": "/",
"name": "Bi's Blog",
"short_name": "Bi's Blog",
"description": "好奇心转化为代码的空间。",
"lang": "zh-CN",
"start_url": "/",
"scope": "/",
"display": "standalone",
"display_override": ["window-controls-overlay", "standalone", "minimal-ui"],
"theme_color": "#0f766e",
"background_color": "#f7f5ef",
"categories": ["blog", "education", "technology"],
"icons": [
{
"src": "/images/pwa-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/images/pwa-512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "/images/pwa-maskable-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"shortcuts": [
{
"name": "搜索文章",
"short_name": "搜索",
"url": "/?action=search",
"icons": [{ "src": "/images/pwa-192x192.png", "sizes": "192x192" }]
},
{
"name": "随机文章",
"short_name": "随机文章",
"url": "/go/",
"icons": [{ "src": "/images/pwa-192x192.png", "sizes": "192x192" }]
}
]
}
+29
View File
@@ -0,0 +1,29 @@
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#0f766e" />
<title>暂时离线 | Bi's Blog</title>
<style>
* { box-sizing: border-box; }
body { min-height: 100vh; margin: 0; display: grid; place-items: center; padding: 24px; color: #27313a; font-family: system-ui, "Microsoft YaHei", sans-serif; background: linear-gradient(145deg, #f7f5ef, #e5f5ef); }
main { width: min(520px, 100%); border: 1px solid rgba(255,255,255,.8); border-radius: 18px; padding: 40px; text-align: center; background: rgba(255,253,248,.82); box-shadow: 0 24px 70px rgba(44,55,63,.16); }
.icon { display: grid; width: 72px; height: 72px; margin: 0 auto 22px; place-items: center; border-radius: 20px; color: white; font-size: 30px; font-weight: 700; background: #0f766e; }
h1 { margin: 0; font-size: clamp(1.8rem, 6vw, 2.6rem); }
p { margin: 14px 0 26px; color: #686f78; line-height: 1.8; }
a, button { display: inline-flex; min-height: 42px; align-items: center; justify-content: center; border: 0; border-radius: 999px; padding: 9px 18px; color: white; font: inherit; font-weight: 700; text-decoration: none; background: #0f766e; cursor: pointer; }
a { margin-left: 8px; color: #0f766e; background: #dff5ed; }
@media (prefers-color-scheme: dark) { body { color: #eef4f6; background: #0b1014; } main { border-color: rgba(181,198,207,.2); background: #141b21; } p { color: #b6c3c9; } }
</style>
</head>
<body>
<main>
<div class="icon" aria-hidden="true">Bi</div>
<h1>网络暂时走丢了</h1>
<p>已经读过的文章仍可从浏览器历史或返回按钮打开。网络恢复后,再来继续探索吧。</p>
<button type="button" onclick="location.reload()">重新连接</button>
<a href="/">返回首页</a>
</main>
</body>
</html>
+626
View File
File diff suppressed because one or more lines are too long
+626
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+382
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+261
View File
@@ -0,0 +1,261 @@
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
xmlns:atom="http://www.w3.org/2005/Atom">
<xsl:output method="html" encoding="UTF-8" indent="yes" doctype-system="about:legacy-compat"/>
<xsl:template match="/">
<html lang="zh-CN">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title><xsl:value-of select="/rss/channel/title"/> RSS</title>
<style>
:root {
color-scheme: light dark;
--bg: #f6f4ee;
--surface: #fffdf8;
--text: #20242a;
--muted: #65707a;
--line: #dfd8ca;
--accent: #0f766e;
--accent-soft: #d9f3ee;
--shadow: 0 18px 48px rgba(45, 39, 28, 0.12);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f171a;
--surface: #162225;
--text: #e9f0ee;
--muted: #a7b4b1;
--line: rgba(174, 205, 197, 0.18);
--accent: #72ded2;
--accent-soft: rgba(114, 222, 210, 0.14);
--shadow: 0 18px 48px rgba(0, 0, 0, 0.28);
}
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
font-family: ui-serif, "Songti SC", "Noto Serif CJK SC", Georgia, serif;
line-height: 1.75;
background:
linear-gradient(rgba(246, 244, 238, 0.88), rgba(246, 244, 238, 0.98)),
url("/images/background.png") center top / cover fixed,
var(--bg);
}
@media (prefers-color-scheme: dark) {
body {
background:
linear-gradient(rgba(8, 13, 16, 0.88), rgba(10, 17, 20, 0.96)),
url("/images/background.png") center top / cover fixed,
var(--bg);
}
}
a {
color: inherit;
text-decoration: none;
}
.wrap {
width: min(1040px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0 56px;
}
.hero {
display: grid;
gap: 18px;
padding: clamp(28px, 5vw, 52px);
border: 1px solid rgba(255, 255, 255, 0.62);
border-radius: 8px;
background: color-mix(in srgb, var(--surface) 86%, transparent);
box-shadow: var(--shadow);
backdrop-filter: blur(18px) saturate(150%);
}
.badge {
display: inline-flex;
width: fit-content;
align-items: center;
gap: 8px;
padding: 6px 12px;
border: 1px solid var(--line);
border-radius: 999px;
color: var(--accent);
font: 700 0.86rem/1.2 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
background: var(--accent-soft);
}
.badge svg {
width: 16px;
height: 16px;
fill: currentColor;
}
h1 {
max-width: 12ch;
margin: 0;
font-size: clamp(2.3rem, 6vw, 4.8rem);
line-height: 1.05;
letter-spacing: 0;
}
.description {
max-width: 680px;
margin: 0;
color: var(--muted);
font-size: clamp(1rem, 2vw, 1.18rem);
}
.actions {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
color: var(--muted);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.94rem;
}
.button {
display: inline-flex;
min-height: 42px;
align-items: center;
justify-content: center;
padding: 8px 14px;
border: 1px solid var(--accent);
border-radius: 999px;
color: #fff;
background: var(--accent);
font-weight: 700;
}
.button.secondary {
color: var(--accent);
background: transparent;
}
.feed {
display: grid;
gap: 14px;
margin-top: 24px;
}
.item {
padding: clamp(20px, 4vw, 30px);
border: 1px solid var(--line);
border-radius: 8px;
background: color-mix(in srgb, var(--surface) 92%, transparent);
box-shadow: 0 10px 30px rgba(45, 39, 28, 0.08);
}
.item h2 {
margin: 0;
font-size: clamp(1.25rem, 3vw, 1.75rem);
line-height: 1.35;
letter-spacing: 0;
}
.item h2 a:hover {
color: var(--accent);
}
.meta {
margin: 8px 0 0;
color: var(--muted);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 0.9rem;
}
.summary {
margin: 14px 0 0;
color: var(--muted);
}
.summary * {
max-width: 100%;
}
.summary a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.read {
display: inline-flex;
margin-top: 14px;
color: var(--accent);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-weight: 700;
}
@media (max-width: 640px) {
.wrap {
width: min(100% - 20px, 1040px);
padding-top: 20px;
}
.hero {
padding: 24px 18px;
}
.actions {
align-items: stretch;
flex-direction: column;
}
.button {
width: 100%;
}
}
</style>
</head>
<body>
<main class="wrap">
<section class="hero">
<div class="badge">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6.18 17.82a2.18 2.18 0 1 1 0 4.36 2.18 2.18 0 0 1 0-4.36ZM4 9.5c5.8 0 10.5 4.7 10.5 10.5h-3A7.5 7.5 0 0 0 4 12.5v-3Zm0-6C13.11 3.5 20.5 10.89 20.5 20h-3C17.5 12.55 11.45 6.5 4 6.5v-3Z"/>
</svg>
RSS Feed
</div>
<h1><xsl:value-of select="/rss/channel/title"/></h1>
<p class="description"><xsl:value-of select="/rss/channel/description"/></p>
<div class="actions">
<a class="button" href="{/rss/channel/link}">返回博客</a>
<a class="button secondary" href="/rss.xml">订阅源地址</a>
<span>把这个地址添加到 RSS 阅读器,就能持续收到新文章。</span>
</div>
</section>
<section class="feed" aria-label="文章列表">
<xsl:for-each select="/rss/channel/item">
<article class="item">
<h2>
<a href="{link}">
<xsl:value-of select="title"/>
</a>
</h2>
<p class="meta"><xsl:value-of select="pubDate"/></p>
<div class="summary">
<xsl:value-of select="description" disable-output-escaping="yes"/>
</div>
<a class="read" href="{link}">阅读全文</a>
</article>
</xsl:for-each>
</section>
</main>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
+424
View File
File diff suppressed because one or more lines are too long
+1
View File
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"><sitemap><loc>https://blog.biss.click/sitemap-0.xml</loc></sitemap></sitemapindex>
+110
View File
@@ -0,0 +1,110 @@
const CACHE_VERSION = '2026-07-17-v1';
const CACHE_PREFIX = 'bis-blog-';
const CORE_CACHE = `${CACHE_PREFIX}core-${CACHE_VERSION}`;
const PAGE_CACHE = `${CACHE_PREFIX}pages-${CACHE_VERSION}`;
const ASSET_CACHE = `${CACHE_PREFIX}assets-${CACHE_VERSION}`;
const IMAGE_CACHE = `${CACHE_PREFIX}images-${CACHE_VERSION}`;
const CORE_ASSETS = [
'/',
'/offline.html',
'/manifest.webmanifest',
'/images/Bi.ico',
'/images/pwa-192x192.png',
'/images/pwa-512x512.png',
'/images/pwa-maskable-512x512.png',
'/images/apple-touch-icon.png',
];
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CORE_CACHE)
.then((cache) => cache.addAll(CORE_ASSETS))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil((async () => {
const cacheNames = await caches.keys();
await Promise.all(
cacheNames
.filter((name) => name.startsWith(CACHE_PREFIX) && ![CORE_CACHE, PAGE_CACHE, ASSET_CACHE, IMAGE_CACHE].includes(name))
.map((name) => caches.delete(name))
);
await self.clients.claim();
const clients = await self.clients.matchAll({ type: 'window' });
clients.forEach((client) => client.postMessage({ type: 'OFFLINE_READY' }));
})());
});
self.addEventListener('message', (event) => {
if (event.data?.type === 'SKIP_WAITING') self.skipWaiting();
});
async function trimCache(cacheName, maxEntries) {
const cache = await caches.open(cacheName);
const keys = await cache.keys();
if (keys.length > maxEntries) {
await Promise.all(keys.slice(0, keys.length - maxEntries).map((key) => cache.delete(key)));
}
}
async function networkFirst(request) {
try {
const response = await fetch(request);
if (response.ok) {
const cache = await caches.open(PAGE_CACHE);
await cache.put(request, response.clone());
void trimCache(PAGE_CACHE, 40);
}
return response;
} catch {
return (await caches.match(request)) || (await caches.match('/offline.html'));
}
}
async function cacheFirst(request, cacheName, maxEntries) {
const cached = await caches.match(request);
if (cached) return cached;
const response = await fetch(request);
if (response.ok || response.type === 'opaque') {
const cache = await caches.open(cacheName);
await cache.put(request, response.clone());
void trimCache(cacheName, maxEntries);
}
return response;
}
async function staleWhileRevalidate(request) {
const cache = await caches.open(ASSET_CACHE);
const cached = await cache.match(request);
const network = fetch(request).then((response) => {
if (response.ok) cache.put(request, response.clone());
return response;
}).catch(() => cached);
return cached || network;
}
self.addEventListener('fetch', (event) => {
const { request } = event;
if (request.method !== 'GET') return;
const url = new URL(request.url);
if (request.mode === 'navigate') {
event.respondWith(networkFirst(request));
return;
}
if (request.destination === 'image') {
event.respondWith(cacheFirst(request, IMAGE_CACHE, 100));
return;
}
if (
url.origin === self.location.origin &&
['style', 'script', 'font', 'worker'].includes(request.destination)
) {
event.respondWith(staleWhileRevalidate(request));
}
});
File diff suppressed because one or more lines are too long
+376
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More