feat:新增离线存储

This commit is contained in:
2025-11-10 14:14:51 +08:00
parent 2e2caeaab5
commit ba757017ae
20 changed files with 969 additions and 20 deletions

View 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;
}