🔔 Notification Center

通知中心

设置专属提醒,让 XYanLab 在对的时间找到你。
纯本地推送,无服务器,完全隐私。

🔔
检查通知权限...
正在读取浏览器设置
⚡️
习惯打卡提醒
每天一次,督促你完成打卡
🌙
情绪记录提醒
每天晚上,记录今天的心情
🎯
专注开始提醒
每天早晨,进入高效状态
📊
每周回顾
每周日,回顾数据与计划
🛡️ 工作原理
通知设置存储在本地 IndexedDB,不上传任何服务器。
每次打开 XYanLab 任意页面,Service Worker 会自动检查是否到了提醒时间。
Chrome / Android 用户安装到主屏幕后,即使关闭浏览器也可收到提醒(PeriodicBackgroundSync)。
每种提醒每天最多发送一次,不会重复打扰。
已保存
// ── State ────────────────────────────────────────────────── let settings = {}; // ── IndexedDB (page side) ────────────────────────────────── function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open('xyanlab_notif_db', 1); req.onupgradeneeded = (e) => { const db = e.target.result; if (!db.objectStoreNames.contains('settings')) { db.createObjectStore('settings', { keyPath: 'key' }); } }; req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } async function dbGet(key) { const db = await openDB(); return new Promise((resolve) => { const tx = db.transaction('settings', 'readonly'); const req = tx.objectStore('settings').get(key); req.onsuccess = () => resolve(req.result ? req.result.value : null); req.onerror = () => resolve(null); }); } async function dbSet(key, value) { const db = await openDB(); return new Promise((resolve) => { const tx = db.transaction('settings', 'readwrite'); tx.objectStore('settings').put({ key, value }); tx.oncomplete = resolve; }); } // ── Permission Handling ──────────────────────────────────── async function checkPermission() { const card = document.getElementById('permission-card'); const icon = document.getElementById('perm-icon'); const title = document.getElementById('perm-title'); const desc = document.getElementById('perm-desc'); const btn = document.getElementById('perm-btn'); if (!('Notification' in window)) { icon.innerText = '❌'; title.innerText = '浏览器不支持通知'; desc.innerText = '请使用 Chrome / Safari / Edge 等现代浏览器。'; card.className = 'mb-8 p-6 rounded-2xl border-2 border-red-200 dark:border-red-800 bg-red-50 dark:bg-red-900/10 slide-in'; return false; } const status = Notification.permission; if (status === 'granted') { icon.innerText = '✅'; title.innerText = '通知已授权'; desc.innerText = '一切就绪!所有提醒开关均已可用。'; card.className = 'mb-8 p-6 rounded-2xl border-2 border-emerald-200 dark:border-emerald-800 bg-emerald-50 dark:bg-emerald-900/10 slide-in'; checkPeriodicSync(); return true; } else if (status === 'denied') { icon.innerText = '🔕'; title.innerText = '通知权限已被拒绝'; desc.innerText = '请在浏览器地址栏左侧 → 点击🔒图标 → 找到「通知」→ 改为「允许」。'; card.className = 'mb-8 p-6 rounded-2xl border-2 border-orange-200 dark:border-orange-800 bg-orange-50 dark:bg-orange-900/10 slide-in'; return false; } else { icon.innerText = '🔔'; title.innerText = '点击开启通知权限'; desc.innerText = '首次使用需要授权,之后即可接收本地推送提醒。'; card.className = 'mb-8 p-6 rounded-2xl border-2 border-indigo-200 dark:border-indigo-700 bg-indigo-50 dark:bg-indigo-900/10 slide-in'; btn.classList.remove('hidden'); btn.onclick = requestPermission; return false; } } async function requestPermission() { const result = await Notification.requestPermission(); if (result === 'granted') { checkPermission(); showToast('✅', '通知权限已开启!'); // Register periodic sync if available if ('periodicSync' in navigator.serviceWorker) { const sw = await navigator.serviceWorker.ready; await sw.periodicSync.register('xyanlab-daily-check', { minInterval: 60 * 60 * 1000 }).catch(() => {}); } } else { showToast('❌', '权限获取失败,请在设置中手动开启'); } } async function checkPeriodicSync() { const info = document.getElementById('periodic-sync-info'); const dot = document.getElementById('periodic-sync-dot'); const text = document.getElementById('periodic-sync-text'); info.classList.remove('hidden'); if ('periodicSync' in (await navigator.serviceWorker.ready)) { dot.className = 'w-2 h-2 rounded-full bg-emerald-500'; text.innerText = '✨ 你的浏览器支持后台推送 (PeriodicBackgroundSync),关闭浏览器也能收到提醒。'; } else { dot.className = 'w-2 h-2 rounded-full bg-amber-400'; text.innerText = '当前浏览器不支持后台唤醒,提醒仅在打开网站时触发。推荐用 Chrome 并将网站添加到主屏幕。'; } } // ── Toggle / Load / Save ─────────────────────────────────── function toggleNotif(track) { const id = track.dataset.toggle; const isOn = !track.classList.contains('on'); track.classList.toggle('on', isOn); const card = track.closest('.notif-card'); card.classList.toggle('disabled', !isOn); if (!settings[id]) settings[id] = { enabled: false }; settings[id].enabled = isOn; saveSettings(); } function saveSettings() { // Collect time values document.querySelectorAll('[data-time]').forEach(input => { const id = input.dataset.time; const [h, m] = input.value.split(':').map(Number); if (!settings[id]) settings[id] = { enabled: false }; settings[id].hour = h; settings[id].minute = m; }); // Save to IndexedDB dbSet('notif_settings', settings).then(() => { // Also ping the SW if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage({ type: 'SAVE_NOTIF_SETTINGS', settings, }); } showToast('💾', '设置已保存'); }); } async function loadSettings() { const saved = await dbGet('notif_settings'); if (saved) settings = saved; document.querySelectorAll('.toggle-track[data-toggle]').forEach(track => { const id = track.dataset.toggle; const cfg = settings[id]; const isOn = cfg?.enabled || false; track.classList.toggle('on', isOn); track.closest('.notif-card').classList.toggle('disabled', !isOn); }); document.querySelectorAll('[data-time]').forEach(input => { const id = input.dataset.time; const cfg = settings[id]; if (cfg?.hour !== undefined) { input.value = `${String(cfg.hour).padStart(2, '0')}:${String(cfg.minute || 0).padStart(2, '0')}`; } }); } // ── Test Notification ───────────────────────────────────── const NOTIF_META = { habit: { title: '⚡️ 习惯打卡提醒', body: '这是测试通知 - 今天的习惯还没打卡!', url: '/tools/habit-tracker.html' }, mood: { title: '🌙 情绪记录', body: '这是测试通知 - 今天心情怎么样?', url: '/tools/mood-calendar.html' }, focus: { title: '🎯 专注时间到了', body: '这是测试通知 - 开始深度工作吧!', url: '/tools/focus-flow.html' }, weekly: { title: '📊 每周回顾', body: '这是测试通知 - 来看看本周数据。', url: '/tools/dashboard.html' }, }; async function testNotif(btn) { const id = btn.dataset.test; if (Notification.permission !== 'granted') { showToast('⚠️', '请先开启通知权限'); return; } const meta = NOTIF_META[id]; const sw = await navigator.serviceWorker.ready; await sw.showNotification(meta.title, { body: meta.body, icon: '../assets/favicon.png', tag: `xyanlab-test-${id}`, data: { url: meta.url }, actions: [{ action: 'open', title: '打开工具' }], }); showToast('🔔', '测试通知已发送!'); } // ── Toast ────────────────────────────────────────────────── let toastTimer; function showToast(icon, msg) { const toast = document.getElementById('toast'); document.getElementById('toast-icon').innerText = icon; document.getElementById('toast-text').innerText = msg; toast.classList.add('show'); clearTimeout(toastTimer); toastTimer = setTimeout(() => toast.classList.remove('show'), 2500); } // ── Page-level check trigger ─────────────────────────────── async function triggerCheck() { if (navigator.serviceWorker.controller) { navigator.serviceWorker.controller.postMessage({ type: 'CHECK_NOTIFICATIONS' }); } } // ── Init ────────────────────────────────────────────────── (async () => { const granted = await checkPermission(); await loadSettings(); if (granted) triggerCheck(); })();