Files
bill/frontend/src/stores/database.ts
Jafeng 0920667719 feat: 完成任务2 - 实现本地数据模型和离线功能
任务2.1  创建本地数据模型
- 完善 SQLite 数据库表结构和数据模型
- 创建完整的 TypeScript 接口定义
- 实现数据验证和格式化工具函数
- 添加数据清理和类型守卫功能

任务2.2  实现本地数据 CRUD 操作
- 扩展数据库服务,添加完整的 CRUD 操作
- 实现 Repository 模式封装数据访问层
- 创建 TransactionRepository、CategoryRepository、AccountRepository
- 添加批量操作、统计查询和数据迁移功能
- 实现 RepositoryManager 统一管理数据操作

任务2.3  建立离线优先的数据架构
- 重构 Pinia store 使用 Repository 模式
- 创建离线状态管理 store (useOfflineStore)
- 实现网络状态监听和离线操作队列
- 添加离线指示器组件和用户界面
- 建立数据同步标记和冲突检测机制
- 实现离线优先的数据缓存策略

核心特性:
- 完整的本地数据 CRUD 操作
- 离线优先架构,支持断网使用
- 数据验证和格式化
- Repository 模式数据访问层
- 网络状态监听和自动同步
- 用户友好的离线提示界面
2025-08-14 15:46:13 +08:00

450 lines
12 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { databaseService } from '@/services/database'
import { repositoryManager } from '@/repositories'
import type {
Transaction,
Category,
Account,
TransactionWithDetails,
TransactionFilters,
SyncStatus
} from '@/types'
export const useDatabaseStore = defineStore('database', () => {
// 基础状态
const isInitialized = ref(false)
const isLoading = ref(false)
const error = ref<string | null>(null)
// 数据状态
const transactions = ref<TransactionWithDetails[]>([])
const categories = ref<Category[]>([])
const accounts = ref<Account[]>([])
// 离线状态
const isOnline = ref(navigator.onLine)
const syncStatus = ref<SyncStatus>({
isOnline: navigator.onLine,
pendingChanges: 0,
syncInProgress: false
})
// 缓存状态
const lastRefreshTime = ref<Date | null>(null)
const cacheExpiry = 5 * 60 * 1000 // 5分钟缓存
// 计算属性
const pendingTransactions = computed(() =>
transactions.value.filter(t => t.sync_status === 'pending')
)
const conflictTransactions = computed(() =>
transactions.value.filter(t => t.sync_status === 'conflict')
)
const totalBalance = computed(() => {
const income = transactions.value
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0)
const expense = transactions.value
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0)
return income - expense
})
const monthlyStats = computed(() => {
const now = new Date()
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
const endOfMonth = new Date(now.getFullYear(), now.getMonth() + 1, 0)
const monthlyTransactions = transactions.value.filter(t => {
const transactionDate = new Date(t.date)
return transactionDate >= startOfMonth && transactionDate <= endOfMonth
})
const income = monthlyTransactions
.filter(t => t.type === 'income')
.reduce((sum, t) => sum + t.amount, 0)
const expense = monthlyTransactions
.filter(t => t.type === 'expense')
.reduce((sum, t) => sum + t.amount, 0)
return {
income,
expense,
balance: income - expense,
transactionCount: monthlyTransactions.length
}
})
// 网络状态监听
function setupNetworkListeners() {
window.addEventListener('online', () => {
isOnline.value = true
syncStatus.value.isOnline = true
// 自动同步待处理的更改
syncPendingChanges()
})
window.addEventListener('offline', () => {
isOnline.value = false
syncStatus.value.isOnline = false
})
}
// 初始化数据库
async function initialize() {
if (isInitialized.value) return
isLoading.value = true
error.value = null
try {
await databaseService.initialize()
await loadInitialData()
setupNetworkListeners()
updateSyncStatus()
isInitialized.value = true
console.log('Database store initialized successfully')
} catch (err) {
error.value = err instanceof Error ? err.message : 'Database initialization failed'
console.error('Database store initialization failed:', err)
} finally {
isLoading.value = false
}
}
// 加载初始数据
async function loadInitialData() {
try {
const [transactionsData, categoriesData, accountsData] = await Promise.all([
repositoryManager.transaction.findAll({ page: 1, limit: 50, offset: 0 }),
repositoryManager.category.findAll(),
repositoryManager.account.findAll()
])
transactions.value = transactionsData
categories.value = categoriesData
accounts.value = accountsData
lastRefreshTime.value = new Date()
} catch (err) {
console.error('Failed to load initial data:', err)
throw err
}
}
// 刷新数据(如果缓存过期)
async function refreshIfNeeded() {
if (!lastRefreshTime.value ||
Date.now() - lastRefreshTime.value.getTime() > cacheExpiry) {
await loadInitialData()
}
}
// 更新同步状态
function updateSyncStatus() {
syncStatus.value = {
isOnline: isOnline.value,
lastSyncTime: syncStatus.value.lastSyncTime,
pendingChanges: pendingTransactions.value.length,
syncInProgress: syncStatus.value.syncInProgress,
syncError: syncStatus.value.syncError
}
}
// 交易操作
async function addTransaction(transaction: Omit<Transaction, 'id'>) {
try {
const id = await repositoryManager.transaction.create(transaction)
const newTransaction = {
...transaction,
id,
sync_status: 'pending' as const
} as TransactionWithDetails
transactions.value.unshift(newTransaction)
updateSyncStatus()
// 如果在线,尝试立即同步
if (isOnline.value) {
syncPendingChanges()
}
return id
} catch (err) {
console.error('Failed to add transaction:', err)
throw err
}
}
async function updateTransaction(id: number, updates: Partial<Transaction>) {
try {
await repositoryManager.transaction.update(id, {
...updates,
sync_status: 'pending'
})
const index = transactions.value.findIndex(t => t.id === id)
if (index !== -1) {
transactions.value[index] = {
...transactions.value[index],
...updates,
sync_status: 'pending'
}
}
updateSyncStatus()
if (isOnline.value) {
syncPendingChanges()
}
} catch (err) {
console.error('Failed to update transaction:', err)
throw err
}
}
async function deleteTransaction(id: number) {
try {
await repositoryManager.transaction.delete(id)
const index = transactions.value.findIndex(t => t.id === id)
if (index !== -1) {
transactions.value.splice(index, 1)
}
updateSyncStatus()
} catch (err) {
console.error('Failed to delete transaction:', err)
throw err
}
}
// 分类操作
async function addCategory(category: Omit<Category, 'id'>) {
try {
const id = await repositoryManager.category.create(category)
const newCategory = { ...category, id }
categories.value.push(newCategory)
return id
} catch (err) {
console.error('Failed to add category:', err)
throw err
}
}
async function updateCategory(id: number, updates: Partial<Category>) {
try {
await repositoryManager.category.update(id, updates)
const index = categories.value.findIndex(c => c.id === id)
if (index !== -1) {
categories.value[index] = { ...categories.value[index], ...updates }
}
} catch (err) {
console.error('Failed to update category:', err)
throw err
}
}
async function deleteCategory(id: number) {
try {
await repositoryManager.category.delete(id)
const index = categories.value.findIndex(c => c.id === id)
if (index !== -1) {
categories.value.splice(index, 1)
}
} catch (err) {
console.error('Failed to delete category:', err)
throw err
}
}
// 账户操作
async function addAccount(account: Omit<Account, 'id'>) {
try {
const id = await repositoryManager.account.create(account)
const newAccount = { ...account, id }
accounts.value.push(newAccount)
return id
} catch (err) {
console.error('Failed to add account:', err)
throw err
}
}
async function updateAccount(id: number, updates: Partial<Account>) {
try {
await repositoryManager.account.update(id, updates)
const index = accounts.value.findIndex(a => a.id === id)
if (index !== -1) {
accounts.value[index] = { ...accounts.value[index], ...updates }
}
} catch (err) {
console.error('Failed to update account:', err)
throw err
}
}
async function deleteAccount(id: number) {
try {
await repositoryManager.account.delete(id)
const index = accounts.value.findIndex(a => a.id === id)
if (index !== -1) {
accounts.value.splice(index, 1)
}
} catch (err) {
console.error('Failed to delete account:', err)
throw err
}
}
// 搜索和筛选
async function searchTransactions(query: string) {
try {
const results = await repositoryManager.transaction.search(query)
return results
} catch (err) {
console.error('Failed to search transactions:', err)
throw err
}
}
async function filterTransactions(filters: TransactionFilters) {
try {
const results = await repositoryManager.transaction.findWithFilters(filters)
return results
} catch (err) {
console.error('Failed to filter transactions:', err)
throw err
}
}
// 获取默认分类和账户
function getDefaultCategory(type: 'income' | 'expense'): Category | undefined {
return categories.value.find(c => c.is_default && (c.type === type || c.type === 'both'))
}
function getDefaultAccount(): Account | undefined {
return accounts.value.find(a => a.is_default)
}
// 同步功能(占位符,将在云端功能中实现)
async function syncPendingChanges() {
if (!isOnline.value || syncStatus.value.syncInProgress) return
syncStatus.value.syncInProgress = true
try {
// 这里将在后续任务中实现实际的同步逻辑
console.log('Sync pending changes (placeholder)')
// 模拟同步延迟
await new Promise(resolve => setTimeout(resolve, 1000))
syncStatus.value.lastSyncTime = new Date().toISOString()
syncStatus.value.syncError = undefined
} catch (err) {
syncStatus.value.syncError = err instanceof Error ? err.message : 'Sync failed'
console.error('Sync failed:', err)
} finally {
syncStatus.value.syncInProgress = false
updateSyncStatus()
}
}
// 强制同步
async function forceSync() {
await syncPendingChanges()
}
// 离线模式提示
function showOfflineMessage() {
return !isOnline.value
}
// 数据统计
async function getStats(startDate?: string, endDate?: string) {
try {
return await repositoryManager.transaction.getStats(startDate, endDate)
} catch (err) {
console.error('Failed to get stats:', err)
throw err
}
}
// 数据完整性检查
async function checkDataIntegrity() {
try {
return await repositoryManager.checkDataIntegrity()
} catch (err) {
console.error('Failed to check data integrity:', err)
throw err
}
}
// 清理错误状态
function clearError() {
error.value = null
syncStatus.value.syncError = undefined
}
return {
// 基础状态
isInitialized,
isLoading,
error,
// 数据状态
transactions,
categories,
accounts,
// 离线状态
isOnline,
syncStatus,
// 计算属性
pendingTransactions,
conflictTransactions,
totalBalance,
monthlyStats,
// 基础方法
initialize,
loadInitialData,
refreshIfNeeded,
clearError,
// 交易操作
addTransaction,
updateTransaction,
deleteTransaction,
// 分类操作
addCategory,
updateCategory,
deleteCategory,
// 账户操作
addAccount,
updateAccount,
deleteAccount,
// 搜索和筛选
searchTransactions,
filterTransactions,
// 工具方法
getDefaultCategory,
getDefaultAccount,
getStats,
checkDataIntegrity,
// 同步功能
syncPendingChanges,
forceSync,
showOfflineMessage
}
})