// IndexedDB 存储封装 const DB = (function () { const DB_NAME = 'calendar_reminder'; const DB_VERSION = 2; // 升级版本以创建 outbox 队列 const STORE_EVENTS = 'events'; const STORE_OUTBOX = 'outbox'; let _db; function openDB() { return new Promise((resolve, reject) => { const req = indexedDB.open(DB_NAME, DB_VERSION); req.onupgradeneeded = (ev) => { const db = ev.target.result; // 事件存储 if (!db.objectStoreNames.contains(STORE_EVENTS)) { const store = db.createObjectStore(STORE_EVENTS, { keyPath: 'id' }); store.createIndex('by_start', 'start'); store.createIndex('by_updated', 'updatedAt'); } // 离线 outbox 队列 if (!db.objectStoreNames.contains(STORE_OUTBOX)) { db.createObjectStore(STORE_OUTBOX, { keyPath: 'id' }); } }; req.onsuccess = () => { _db = req.result; resolve(_db); }; req.onerror = () => reject(req.error); }); } function tx(storeName, mode = 'readonly') { const t = _db.transaction(storeName, mode); return t.objectStore(storeName); } async function initDB() { if (_db) return _db; return await openDB(); } function addEvent(ev) { return new Promise((resolve, reject) => { const store = tx(STORE_EVENTS, 'readwrite'); const req = store.add(ev); req.onsuccess = () => resolve(ev); req.onerror = () => reject(req.error); }); } function updateEvent(ev) { return new Promise((resolve, reject) => { const store = tx(STORE_EVENTS, 'readwrite'); const req = store.put(ev); req.onsuccess = () => resolve(ev); req.onerror = () => reject(req.error); }); } function deleteEvent(id) { return new Promise((resolve, reject) => { const store = tx(STORE_EVENTS, 'readwrite'); const req = store.delete(id); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } function getEvent(id) { return new Promise((resolve, reject) => { const store = tx(STORE_EVENTS, 'readonly'); const req = store.get(id); req.onsuccess = () => resolve(req.result); req.onerror = () => reject(req.error); }); } function listEventsByMonth(year, month) { // month: 1..12 const start = new Date(Date.UTC(year, month - 1, 1, 0, 0, 0)); const end = new Date(Date.UTC(year, month, 0, 23, 59, 59)); return new Promise((resolve, reject) => { const store = tx(STORE_EVENTS, 'readonly'); const idx = store.index('by_start'); const range = IDBKeyRange.bound(start.toISOString(), end.toISOString()); const result = []; const req = idx.openCursor(range); req.onsuccess = (e) => { const cursor = e.target.result; if (cursor) { result.push(cursor.value); cursor.continue(); } else { resolve(result); } }; req.onerror = () => reject(req.error); }); } // --- Outbox 队列 --- function genId() { return Date.now() + '-' + Math.random().toString(36).slice(2); } function addOutbox(entry) { return new Promise((resolve, reject) => { const store = tx(STORE_OUTBOX, 'readwrite'); const req = store.add(entry); req.onsuccess = () => resolve(entry); req.onerror = () => reject(req.error); }); } function addOutboxUpsert(ev) { const entry = { id: genId(), type: 'upsert', payload: ev, timestamp: new Date().toISOString() }; return addOutbox(entry); } function addOutboxDelete(id) { const entry = { id: genId(), type: 'delete', payload: { id }, timestamp: new Date().toISOString() }; return addOutbox(entry); } function listOutbox() { return new Promise((resolve, reject) => { const store = tx(STORE_OUTBOX, 'readonly'); const result = []; const req = store.openCursor(); req.onsuccess = (e) => { const cursor = e.target.result; if (cursor) { result.push(cursor.value); cursor.continue(); } else { resolve(result); } }; req.onerror = () => reject(req.error); }); } function removeOutbox(id) { return new Promise((resolve, reject) => { const store = tx(STORE_OUTBOX, 'readwrite'); const req = store.delete(id); req.onsuccess = () => resolve(); req.onerror = () => reject(req.error); }); } return { initDB, addEvent, updateEvent, deleteEvent, getEvent, listEventsByMonth, // outbox addOutboxUpsert, addOutboxDelete, listOutbox, removeOutbox, }; })(); window.DB = DB;