feat:新增离线存储
This commit is contained in:
177
apps/frontend/src/lib/storage/offline-db.ts
Normal file
177
apps/frontend/src/lib/storage/offline-db.ts
Normal file
@@ -0,0 +1,177 @@
|
||||
import { Capacitor } from '@capacitor/core';
|
||||
import {
|
||||
CapacitorSQLite,
|
||||
SQLiteConnection,
|
||||
type SQLiteDBConnection
|
||||
} from '@capacitor-community/sqlite';
|
||||
import type { Transaction } from '../../types/transaction';
|
||||
import type { TransactionPayload } from '../../types/transaction';
|
||||
|
||||
const isNative = Capacitor.isNativePlatform();
|
||||
|
||||
let sqlite: SQLiteConnection | null = null;
|
||||
let db: SQLiteDBConnection | null = null;
|
||||
|
||||
const WEB_STORAGE_KEY = 'ai-bill-offline-transactions';
|
||||
const PENDING_KEY = 'ai-bill-pending-transactions';
|
||||
const canUseLocalStorage = typeof window !== 'undefined' && typeof window.localStorage !== 'undefined';
|
||||
|
||||
async function getDb(): Promise<SQLiteDBConnection | null> {
|
||||
if (!isNative) return null;
|
||||
if (!sqlite) {
|
||||
sqlite = new SQLiteConnection(CapacitorSQLite);
|
||||
}
|
||||
if (!db) {
|
||||
db = await sqlite.createConnection('ai_bill_offline', false, 'no-encryption', 1, false);
|
||||
await db.open();
|
||||
await db.execute(
|
||||
`CREATE TABLE IF NOT EXISTS transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
title TEXT,
|
||||
amount REAL,
|
||||
currency TEXT,
|
||||
category TEXT,
|
||||
type TEXT,
|
||||
source TEXT,
|
||||
status TEXT,
|
||||
occurredAt TEXT,
|
||||
notes TEXT,
|
||||
metadata TEXT
|
||||
);`
|
||||
);
|
||||
await db.execute(
|
||||
`CREATE TABLE IF NOT EXISTS pending_transactions (
|
||||
id TEXT PRIMARY KEY,
|
||||
payload TEXT
|
||||
);`
|
||||
);
|
||||
}
|
||||
return db;
|
||||
}
|
||||
|
||||
export async function persistTransactionsOffline(transactions: Transaction[]): Promise<void> {
|
||||
const database = await getDb();
|
||||
if (!database) {
|
||||
if (canUseLocalStorage) {
|
||||
localStorage.setItem(WEB_STORAGE_KEY, JSON.stringify(transactions));
|
||||
}
|
||||
return;
|
||||
}
|
||||
await database.execute('DELETE FROM transactions;');
|
||||
const statements = transactions.map((txn) => ({
|
||||
statement:
|
||||
'INSERT OR REPLACE INTO transactions (id, title, amount, currency, category, type, source, status, occurredAt, notes, metadata) VALUES (?,?,?,?,?,?,?,?,?,?,?);',
|
||||
values: [
|
||||
txn.id,
|
||||
txn.title,
|
||||
txn.amount,
|
||||
txn.currency,
|
||||
txn.category,
|
||||
txn.type,
|
||||
txn.source,
|
||||
txn.status,
|
||||
txn.occurredAt,
|
||||
txn.notes ?? null,
|
||||
JSON.stringify(txn.metadata ?? {})
|
||||
]
|
||||
}));
|
||||
await database.executeSet(statements);
|
||||
}
|
||||
|
||||
export async function readTransactionsOffline(): Promise<Transaction[]> {
|
||||
const database = await getDb();
|
||||
if (!database) {
|
||||
if (!canUseLocalStorage) return [];
|
||||
const stored = localStorage.getItem(WEB_STORAGE_KEY);
|
||||
return stored ? (JSON.parse(stored) as Transaction[]) : [];
|
||||
}
|
||||
const result = await database.query('SELECT * FROM transactions ORDER BY occurredAt DESC;');
|
||||
return (
|
||||
result.values?.map((row) => ({
|
||||
id: row.id,
|
||||
title: row.title,
|
||||
amount: row.amount,
|
||||
currency: row.currency,
|
||||
category: row.category,
|
||||
type: row.type,
|
||||
source: row.source,
|
||||
status: row.status,
|
||||
occurredAt: row.occurredAt,
|
||||
notes: row.notes ?? undefined,
|
||||
metadata: row.metadata ? JSON.parse(row.metadata) : undefined
|
||||
})) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
const randomId = () => {
|
||||
const uuid =
|
||||
typeof globalThis.crypto !== 'undefined' && typeof globalThis.crypto.randomUUID === 'function'
|
||||
? globalThis.crypto.randomUUID()
|
||||
: `${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
return `pending_${uuid}`;
|
||||
};
|
||||
|
||||
export async function queuePendingTransaction(payload: TransactionPayload): Promise<void> {
|
||||
const database = await getDb();
|
||||
const item = { id: randomId(), payload };
|
||||
if (!database) {
|
||||
if (!canUseLocalStorage) return;
|
||||
const stored = localStorage.getItem(PENDING_KEY);
|
||||
const list: typeof item[] = stored ? JSON.parse(stored) : [];
|
||||
list.push(item);
|
||||
localStorage.setItem(PENDING_KEY, JSON.stringify(list));
|
||||
return;
|
||||
}
|
||||
await database.run('INSERT OR REPLACE INTO pending_transactions (id, payload) VALUES (?, ?);', [
|
||||
item.id,
|
||||
JSON.stringify(payload)
|
||||
]);
|
||||
}
|
||||
|
||||
async function readPendingTransactions(): Promise<Array<{ id: string; payload: TransactionPayload }>> {
|
||||
const database = await getDb();
|
||||
if (!database) {
|
||||
if (!canUseLocalStorage) return [];
|
||||
const stored = localStorage.getItem(PENDING_KEY);
|
||||
return stored ? JSON.parse(stored) : [];
|
||||
}
|
||||
const result = await database.query('SELECT * FROM pending_transactions;');
|
||||
return (
|
||||
result.values?.map((row) => ({
|
||||
id: row.id,
|
||||
payload: JSON.parse(row.payload)
|
||||
})) ?? []
|
||||
);
|
||||
}
|
||||
|
||||
async function removePendingTransaction(id: string): Promise<void> {
|
||||
const database = await getDb();
|
||||
if (!database) {
|
||||
if (!canUseLocalStorage) return;
|
||||
const stored = localStorage.getItem(PENDING_KEY);
|
||||
if (!stored) return;
|
||||
const next = (JSON.parse(stored) as Array<{ id: string; payload: TransactionPayload }>).filter(
|
||||
(item) => item.id !== id
|
||||
);
|
||||
localStorage.setItem(PENDING_KEY, JSON.stringify(next));
|
||||
return;
|
||||
}
|
||||
await database.run('DELETE FROM pending_transactions WHERE id = ?;', [id]);
|
||||
}
|
||||
|
||||
export async function syncPendingTransactions(
|
||||
uploader: (payload: TransactionPayload) => Promise<void>
|
||||
): Promise<number> {
|
||||
const pending = await readPendingTransactions();
|
||||
let synced = 0;
|
||||
for (const item of pending) {
|
||||
try {
|
||||
await uploader(item.payload);
|
||||
await removePendingTransaction(item.id);
|
||||
synced += 1;
|
||||
} catch (_error) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return synced;
|
||||
}
|
||||
Reference in New Issue
Block a user