mirror of
https://github.com/afoim/fuwari.git
synced 2026-01-31 09:03:18 +08:00
feat(widget): 新增文章更新检测与通知组件
- 添加新文章通知组件,通过比对 RSS 源检测新发布的文章并在页面右下角显示通知 - 添加内容差异高亮组件,通过比对本地缓存检测文章内容更新并高亮显示变化部分 - 新增 diff 依赖包用于文本差异比较,同时添加对应的类型定义 - 在布局中引入新组件,支持开发者调试模式
This commit is contained in:
114
src/components/widget/NewPostNotification.astro
Normal file
114
src/components/widget/NewPostNotification.astro
Normal file
@@ -0,0 +1,114 @@
|
||||
<div id="new-post-notification" class="fixed bottom-4 right-4 z-50 transform translate-y-20 opacity-0 transition-all duration-300 pointer-events-none">
|
||||
<div class="bg-[var(--card-bg)] border border-[var(--primary)] rounded-xl shadow-lg p-4 max-w-sm relative pointer-events-auto">
|
||||
<button id="close-notification" class="absolute top-2 right-2 text-black/50 dark:text-white/50 hover:text-[var(--primary)] transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="18" y1="6" x2="6" y2="18"></line><line x1="6" y1="6" x2="18" y2="18"></line></svg>
|
||||
</button>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="bg-[var(--primary)]/10 p-2 rounded-full shrink-0 text-[var(--primary)]">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 11a9 9 0 0 1 9 9"></path><path d="M4 4a16 16 0 0 1 16 16"></path><circle cx="5" cy="19" r="1"></circle></svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="font-bold text-black dark:text-white mb-1 transition-colors">发现新文章</h3>
|
||||
<div id="new-post-list" class="text-sm text-black/80 dark:text-white/80 transition-colors space-y-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script is:inline>
|
||||
(async function() {
|
||||
const STORAGE_KEY = 'blog-posts-cache';
|
||||
const NOTIFICATION_ID = 'new-post-notification';
|
||||
const LIST_ID = 'new-post-list';
|
||||
|
||||
// Helper to get current RSS items
|
||||
async function fetchRSS() {
|
||||
try {
|
||||
const response = await fetch('/rss.xml');
|
||||
const text = await response.text();
|
||||
const parser = new DOMParser();
|
||||
const xml = parser.parseFromString(text, 'text/xml');
|
||||
const items = Array.from(xml.querySelectorAll('item'));
|
||||
|
||||
return items.map(item => ({
|
||||
title: item.querySelector('title')?.textContent || '',
|
||||
link: item.querySelector('link')?.textContent || '',
|
||||
guid: item.querySelector('guid')?.textContent || item.querySelector('link')?.textContent || '',
|
||||
pubDate: new Date(item.querySelector('pubDate')?.textContent || '').getTime()
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch RSS:', e);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// Helper to show notification
|
||||
function showNotification(newPosts) {
|
||||
const notification = document.getElementById(NOTIFICATION_ID);
|
||||
const list = document.getElementById(LIST_ID);
|
||||
if (!notification || !list) return;
|
||||
|
||||
// Show all posts, no limit
|
||||
let html = '';
|
||||
newPosts.forEach(post => {
|
||||
// Added break-words to handle long titles wrapping
|
||||
html += `<a href="${post.link}" class="block hover:text-[var(--primary)] hover:underline break-words transition-colors text-black dark:text-white mb-2 last:mb-0" target="_blank">• ${post.title}</a>`;
|
||||
});
|
||||
|
||||
list.innerHTML = html;
|
||||
|
||||
// Show animation
|
||||
setTimeout(() => {
|
||||
notification.classList.remove('translate-y-20', 'opacity-0', 'pointer-events-none');
|
||||
}, 1000); // Slight delay after page load
|
||||
|
||||
// Close handler
|
||||
document.getElementById('close-notification')?.addEventListener('click', () => {
|
||||
notification.classList.add('translate-y-20', 'opacity-0', 'pointer-events-none');
|
||||
});
|
||||
}
|
||||
|
||||
// Main logic
|
||||
try {
|
||||
// Developer Mode Check
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const isDevMode = urlParams.has('debug-notification');
|
||||
|
||||
if (isDevMode) {
|
||||
console.log('[Notification] Developer mode active');
|
||||
const mockPosts = [
|
||||
{ title: '测试新文章 1', link: '#', guid: 'test-1' },
|
||||
{ title: '测试新文章 2 - 标题很长很长很长很长很长很长很长很长很长很长很长', link: '#', guid: 'test-2' },
|
||||
{ title: '测试新文章 3', link: '#', guid: 'test-3' },
|
||||
{ title: '测试新文章 4 (应该被隐藏)', link: '#', guid: 'test-4' }
|
||||
];
|
||||
showNotification(mockPosts);
|
||||
return; // Skip normal logic
|
||||
}
|
||||
|
||||
const currentPosts = await fetchRSS();
|
||||
if (currentPosts.length === 0) return;
|
||||
|
||||
const storedData = localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
if (!storedData) {
|
||||
// First visit: just cache current state
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentPosts.map(p => p.guid)));
|
||||
} else {
|
||||
// Subsequent visit: compare
|
||||
const cachedGuids = JSON.parse(storedData); // Array of strings
|
||||
const newPosts = currentPosts.filter(post => !cachedGuids.includes(post.guid));
|
||||
|
||||
// Only notify if we found new posts that are genuinely newer than what we have
|
||||
if (newPosts.length > 0) {
|
||||
showNotification(newPosts);
|
||||
}
|
||||
|
||||
// Update cache with current state
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(currentPosts.map(p => p.guid)));
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('New post check failed:', e);
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
Reference in New Issue
Block a user