ddshi 62aa5cd54c feat: 优化提醒时间选择器和 SW 宽限期
- 缩短 SW 宽限期从10分钟改为3分钟
- 新增 PopoverTimePicker 弹出式时间选择器
- 支持数字输入和30分钟间隔选择
- 替换原有的 WheelTimePicker

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-11 14:54:17 +08:00

269 lines
7.4 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 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();