- 缩短 SW 宽限期从10分钟改为3分钟 - 新增 PopoverTimePicker 弹出式时间选择器 - 支持数字输入和30分钟间隔选择 - 替换原有的 WheelTimePicker Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
269 lines
7.4 KiB
JavaScript
269 lines
7.4 KiB
JavaScript
// Service Worker - 纯 JavaScript 版本
|
||
|
||
// 定时检查间隔(毫秒)
|
||
const CHECK_INTERVAL = 30 * 1000; // 30秒检查一次(更频繁)
|
||
|
||
// 已发送的通知标签集合(避免重复发送)
|
||
const sentNotifications = new Set();
|
||
|
||
// 从 IndexedDB 加载提醒事件
|
||
async function loadEvents() {
|
||
try {
|
||
const db = await openDB();
|
||
return new Promise((resolve, reject) => {
|
||
const tx = db.transaction('events', 'readonly');
|
||
const store = tx.objectStore('events');
|
||
const request = store.getAll();
|
||
request.onerror = () => reject(request.error);
|
||
request.onsuccess = () => resolve(request.result);
|
||
});
|
||
} catch {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// 打开 IndexedDB
|
||
function openDB() {
|
||
return new Promise((resolve, reject) => {
|
||
const request = indexedDB.open('qia-notifications', 1);
|
||
request.onerror = () => reject(request.error);
|
||
request.onsuccess = () => resolve(request.result);
|
||
request.onupgradeneeded = (event) => {
|
||
const db = event.target.result;
|
||
if (!db.objectStoreNames.contains('events')) {
|
||
db.createObjectStore('events', { keyPath: 'id' });
|
||
}
|
||
if (!db.objectStoreNames.contains('sent-notifications')) {
|
||
db.createObjectStore('sent-notifications', { keyPath: 'tag' });
|
||
}
|
||
};
|
||
});
|
||
}
|
||
|
||
// 检查是否有提醒时间到达
|
||
function checkReminders(events) {
|
||
const now = new Date();
|
||
const notifications = [];
|
||
|
||
for (const event of events) {
|
||
if (event.type !== 'reminder') continue;
|
||
if (event.is_completed) continue;
|
||
if (!event.reminder_times || event.reminder_times.length === 0) continue;
|
||
|
||
for (const reminderTime of event.reminder_times) {
|
||
const rt = new Date(reminderTime);
|
||
// 检查是否在最近3分钟内(宽限期,处理后台运行不稳定的情况)
|
||
const diffMs = now.getTime() - rt.getTime();
|
||
const diffMinutes = diffMs / (1000 * 60);
|
||
|
||
// 跳过还未到的提醒
|
||
if (diffMinutes < 0) continue;
|
||
|
||
// 如果在3分钟宽限期内,且还没发送过通知
|
||
if (diffMinutes < 3) {
|
||
const tag = `reminder-${event.id}-${reminderTime}`;
|
||
|
||
// 避免重复发送同一通知
|
||
if (!sentNotifications.has(tag)) {
|
||
sentNotifications.add(tag);
|
||
notifications.push({
|
||
id: `${event.id}-${Date.now()}`,
|
||
title: event.title,
|
||
body: event.content || getDefaultBodyText(event.date),
|
||
tag,
|
||
data: { eventId: event.id, reminderTime },
|
||
timestamp: now.getTime(),
|
||
});
|
||
console.log('SW: 找到到期提醒:', event.title, '- 差值(分钟):', diffMinutes.toFixed(2));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return notifications;
|
||
}
|
||
|
||
// 获取默认通知正文
|
||
function getDefaultBodyText(dateStr) {
|
||
const date = new Date(dateStr);
|
||
return date.toLocaleString('zh-CN', {
|
||
month: 'numeric',
|
||
day: 'numeric',
|
||
hour: '2-digit',
|
||
minute: '2-digit',
|
||
hour12: false,
|
||
});
|
||
}
|
||
|
||
// 显示浏览器通知
|
||
async function showNotification(notification) {
|
||
if (Notification.permission !== 'granted') return;
|
||
|
||
try {
|
||
await self.registration.showNotification(notification.title, {
|
||
body: notification.body,
|
||
icon: '/favicon.png',
|
||
badge: '/favicon.png',
|
||
tag: notification.tag,
|
||
data: notification.data,
|
||
requireInteraction: true,
|
||
actions: [
|
||
{ action: 'view', title: '查看' },
|
||
{ action: 'dismiss', title: '关闭' },
|
||
],
|
||
});
|
||
console.log('SW: 通知已发送:', notification.title);
|
||
} catch (error) {
|
||
console.error('SW: Failed to show notification:', error);
|
||
}
|
||
}
|
||
|
||
// 主检查函数
|
||
async function performCheck() {
|
||
try {
|
||
const events = await loadEvents();
|
||
const notifications = checkReminders(events);
|
||
|
||
if (notifications.length > 0) {
|
||
console.log('SW: 检查提醒,已加载事件数:', events.length, '将发送通知数:', notifications.length);
|
||
for (const notification of notifications) {
|
||
await showNotification(notification);
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('SW: Reminder check failed:', error);
|
||
}
|
||
}
|
||
|
||
// 定时检查
|
||
let checkTimer = null;
|
||
|
||
function startPeriodicCheck() {
|
||
if (checkTimer) clearInterval(checkTimer);
|
||
// 立即执行一次
|
||
performCheck();
|
||
// 然后定期执行
|
||
checkTimer = self.setInterval(performCheck, CHECK_INTERVAL);
|
||
console.log('SW: 定时检查已启动,间隔', CHECK_INTERVAL / 1000, '秒');
|
||
}
|
||
|
||
function stopPeriodicCheck() {
|
||
if (checkTimer) {
|
||
clearInterval(checkTimer);
|
||
checkTimer = null;
|
||
}
|
||
}
|
||
|
||
// 消息处理
|
||
self.addEventListener('message', (event) => {
|
||
console.log('SW: 收到消息', event.data);
|
||
|
||
if (event.data.type === 'SKIP_WAITING') {
|
||
self.skipWaiting();
|
||
}
|
||
|
||
if (event.data.type === 'START_CHECK') {
|
||
startPeriodicCheck();
|
||
}
|
||
|
||
if (event.data.type === 'STOP_CHECK') {
|
||
stopPeriodicCheck();
|
||
}
|
||
|
||
if (event.data.type === 'UPDATE_EVENTS') {
|
||
// 事件更新,保存到 IndexedDB 并立即检查
|
||
saveEventsToDB(event.data.events).then(() => {
|
||
console.log('SW: 数据已更新,立即检查...');
|
||
performCheck();
|
||
});
|
||
}
|
||
|
||
if (event.data.type === 'TRIGGER_CHECK') {
|
||
console.log('SW: 收到 TRIGGER_CHECK,手动执行检查');
|
||
performCheck();
|
||
}
|
||
|
||
if (event.data.type === 'GET_SENT_TAGS') {
|
||
if (event.ports && event.ports[0]) {
|
||
event.ports[0].postMessage({
|
||
type: 'SENT_TAGS',
|
||
tags: Array.from(sentNotifications)
|
||
});
|
||
}
|
||
}
|
||
|
||
if (event.data.type === 'CLEAR_SENT_NOTIFICATIONS') {
|
||
sentNotifications.clear();
|
||
console.log('SW: 已清除已发送通知记录');
|
||
}
|
||
});
|
||
|
||
// 保存事件到 IndexedDB
|
||
async function saveEventsToDB(events) {
|
||
try {
|
||
const db = await openDB();
|
||
const tx = db.transaction('events', 'readwrite');
|
||
const store = tx.objectStore('events');
|
||
await store.clear();
|
||
for (const event of events) {
|
||
await store.put(event);
|
||
}
|
||
console.log('SW: 已保存', events.length, '个事件到 IndexedDB');
|
||
} catch (error) {
|
||
console.error('SW: Failed to save events:', error);
|
||
}
|
||
}
|
||
|
||
// 安装
|
||
self.addEventListener('install', (event) => {
|
||
console.log('SW: Installing...');
|
||
event.waitUntil(self.skipWaiting());
|
||
});
|
||
|
||
// 激活
|
||
self.addEventListener('activate', (event) => {
|
||
console.log('SW: Activating...');
|
||
event.waitUntil(
|
||
self.clients.claim().then(() => {
|
||
console.log('SW: 已获取控制权,启动检查...');
|
||
startPeriodicCheck();
|
||
})
|
||
);
|
||
});
|
||
|
||
// 通知点击处理
|
||
self.addEventListener('notificationclick', (event) => {
|
||
event.notification.close();
|
||
|
||
if (event.action === 'view') {
|
||
event.waitUntil(
|
||
self.clients.matchAll({ type: 'window' }).then((clientList) => {
|
||
for (const client of clientList) {
|
||
if (client.url.includes(self.registration.scope) && 'focus' in client) {
|
||
return client.focus();
|
||
}
|
||
}
|
||
if (self.clients.openWindow) {
|
||
return self.clients.openWindow('/');
|
||
}
|
||
})
|
||
);
|
||
}
|
||
});
|
||
|
||
// 定期清理过期的 sentNotifications(保留24小时)
|
||
setInterval(() => {
|
||
const now = Date.now();
|
||
// 简单清理:只保留最近的通知记录
|
||
if (sentNotifications.size > 1000) {
|
||
const iterator = sentNotifications.keys();
|
||
while (sentNotifications.size > 500) {
|
||
sentNotifications.delete(iterator.next().value);
|
||
}
|
||
console.log('SW: 已清理过期的通知记录');
|
||
}
|
||
}, 60 * 60 * 1000); // 每小时清理一次
|
||
|
||
// 启动定时检查
|
||
startPeriodicCheck();
|