/** * YCat Notification Bell Component * 在页面 header 中显示通知铃铛,点击展开通知下拉面板 * 使用方式:在 HTML 中引入 * 自动在 #notificationBell 容器中渲染,或在 body 中自动注入 */ (function() { 'use strict'; // 样式注入 const style = document.createElement('style'); style.textContent = ` .notif-bell-wrapper { position: relative; display: inline-flex; align-items: center; cursor: pointer; padding: 6px 10px; border-radius: 8px; transition: background 0.2s; } .notif-bell-wrapper:hover { background: rgba(0,0,0,0.05); } .notif-bell-icon { font-size: 20px; line-height: 1; } .notif-badge { position: absolute; top: 2px; right: 4px; background: #ef4444; color: #fff; font-size: 11px; font-weight: 700; min-width: 18px; height: 18px; border-radius: 9px; display: flex; align-items: center; justify-content: center; padding: 0 4px; animation: notif-pulse 2s infinite; } .notif-badge.hidden { display: none; } @keyframes notif-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.1); } } .notif-dropdown { position: absolute; top: 100%; right: 0; width: 380px; max-height: 500px; background: #fff; border-radius: 12px; box-shadow: 0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.08); z-index: 10000; overflow: hidden; display: none; flex-direction: column; } .notif-dropdown.open { display: flex; } .notif-header { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border-bottom: 1px solid #f0f0f0; background: #fafafa; } .notif-header h3 { font-size: 15px; font-weight: 600; margin: 0; } .notif-header-actions { display: flex; gap: 8px; } .notif-header-actions button { font-size: 12px; color: #6366f1; background: none; border: none; cursor: pointer; padding: 2px 6px; border-radius: 4px; } .notif-header-actions button:hover { background: #f0f0ff; } .notif-list { flex: 1; overflow-y: auto; max-height: 400px; } .notif-item { display: flex; gap: 10px; padding: 12px 16px; border-bottom: 1px solid #f5f5f5; cursor: pointer; transition: background 0.15s; } .notif-item:hover { background: #f8f9ff; } .notif-item.unread { background: #f0f4ff; } .notif-item.unread:hover { background: #e8edff; } .notif-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; font-size: 16px; flex-shrink: 0; } .notif-icon.publish_success { background: #dcfce7; } .notif-icon.publish_failed { background: #fee2e2; } .notif-icon.team_invite { background: #dbeafe; } .notif-icon.approval_request { background: #fef3c7; } .notif-icon.approval_result { background: #e0e7ff; } .notif-icon.subscription { background: #fce7f3; } .notif-icon.system { background: #f3f4f6; } .notif-icon.comment { background: #ccfbf1; } .notif-icon.version { background: #fde68a; } .notif-body { flex: 1; min-width: 0; } .notif-title { font-size: 13px; font-weight: 600; color: #1a1a2e; margin-bottom: 2px; } .notif-text { font-size: 12px; color: #666; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .notif-time { font-size: 11px; color: #999; margin-top: 3px; } .notif-empty { padding: 40px 16px; text-align: center; color: #999; font-size: 14px; } .notif-empty-icon { font-size: 32px; margin-bottom: 8px; } @media (max-width: 480px) { .notif-dropdown { position: fixed; top: auto; bottom: 0; left: 0; right: 0; width: 100%; max-height: 70vh; border-radius: 16px 16px 0 0; } } `; document.head.appendChild(style); // 类型图标映射 const TYPE_ICONS = { publish_success: '✅', publish_failed: '❌', team_invite: '👥', approval_request: '📝', approval_result: '📋', subscription: '💎', system: '🔔', comment: '💬', version: '📦', }; let isOpen = false; let bellContainer = null; function timeAgo(dateStr) { const now = Date.now(); const then = new Date(dateStr).getTime(); const diff = Math.floor((now - then) / 1000); if (diff < 60) return '刚刚'; if (diff < 3600) return Math.floor(diff / 60) + '分钟前'; if (diff < 86400) return Math.floor(diff / 3600) + '小时前'; if (diff < 604800) return Math.floor(diff / 86400) + '天前'; return new Date(dateStr).toLocaleDateString('zh-CN'); } function renderBadge(count) { const badge = bellContainer?.querySelector('.notif-badge'); if (!badge) return; if (count > 0) { badge.textContent = count > 99 ? '99+' : count; badge.classList.remove('hidden'); } else { badge.classList.add('hidden'); } } function renderItem(n) { const icon = TYPE_ICONS[n.type] || '🔔'; const isUnread = !n.is_read; return `
${icon}
${n.title}
${n.content || ''}
${timeAgo(n.created_at)}
`; } async function loadNotifications() { const dropdown = bellContainer?.querySelector('.notif-list'); if (!dropdown) return; try { const res = await API.getNotifications({ limit: 30 }); if (!res.ok) return; const notifs = res.notifications || []; renderBadge(res.unreadCount || 0); if (notifs.length === 0) { dropdown.innerHTML = '
📭
暂无通知
'; return; } dropdown.innerHTML = notifs.map(renderItem).join(''); // 点击通知项 dropdown.querySelectorAll('.notif-item').forEach(el => { el.addEventListener('click', async () => { const id = el.dataset.id; const link = el.dataset.link; // 标记已读 if (el.classList.contains('unread')) { await API.markNotifRead(id); el.classList.remove('unread'); // 更新 badge const badge = bellContainer?.querySelector('.notif-badge'); if (badge) { const cnt = parseInt(badge.textContent) - 1; renderBadge(cnt); } } // 跳转 if (link) window.location.href = link; }); }); } catch (err) { dropdown.innerHTML = '
加载失败
'; } } function toggleDropdown() { const dropdown = bellContainer?.querySelector('.notif-dropdown'); if (!dropdown) return; isOpen = !isOpen; if (isOpen) { dropdown.classList.add('open'); loadNotifications(); } else { dropdown.classList.remove('open'); } } // 点击外部关闭 document.addEventListener('click', (e) => { if (isOpen && bellContainer && !bellContainer.contains(e.target)) { isOpen = false; bellContainer.querySelector('.notif-dropdown')?.classList.remove('open'); } }); // 初始化 function init() { // 查找插入点 bellContainer = document.getElementById('notificationBell'); if (!bellContainer) { // 自动创建并插入到 header/nav const nav = document.querySelector('nav, header, .navbar, .header-bar'); if (nav) { bellContainer = document.createElement('div'); bellContainer.id = 'notificationBell'; bellContainer.style.cssText = 'display:inline-flex;align-items:center;margin-left:auto;'; nav.appendChild(bellContainer); } else { return; // 没有合适的容器 } } bellContainer.innerHTML = `
🔔

通知

📭
暂无通知
`; // 事件绑定 bellContainer.querySelector('.notif-bell-wrapper').addEventListener('click', (e) => { e.stopPropagation(); toggleDropdown(); }); bellContainer.querySelector('#notifMarkAll').addEventListener('click', async () => { await API.markAllNotifRead(); renderBadge(0); loadNotifications(); }); bellContainer.querySelector('#notifClearRead').addEventListener('click', async () => { await API.clearReadNotifs(); loadNotifications(); }); // 初始加载未读数 if (typeof API !== 'undefined' && API.getUnreadCount) { API.getUnreadCount().then(res => { if (res.ok) renderBadge(res.unreadCount); }).catch(() => {}); } // 每30秒刷新未读数 setInterval(async () => { try { const res = await API.getUnreadCount(); if (res.ok) renderBadge(res.unreadCount); } catch {} }, 30000); } // DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => setTimeout(init, 500)); } else { setTimeout(init, 500); } })();