任务2.1 创建本地数据模型 - 完善 SQLite 数据库表结构和数据模型 - 创建完整的 TypeScript 接口定义 - 实现数据验证和格式化工具函数 - 添加数据清理和类型守卫功能 任务2.2 实现本地数据 CRUD 操作 - 扩展数据库服务,添加完整的 CRUD 操作 - 实现 Repository 模式封装数据访问层 - 创建 TransactionRepository、CategoryRepository、AccountRepository - 添加批量操作、统计查询和数据迁移功能 - 实现 RepositoryManager 统一管理数据操作 任务2.3 建立离线优先的数据架构 - 重构 Pinia store 使用 Repository 模式 - 创建离线状态管理 store (useOfflineStore) - 实现网络状态监听和离线操作队列 - 添加离线指示器组件和用户界面 - 建立数据同步标记和冲突检测机制 - 实现离线优先的数据缓存策略 核心特性: - 完整的本地数据 CRUD 操作 - 离线优先架构,支持断网使用 - 数据验证和格式化 - Repository 模式数据访问层 - 网络状态监听和自动同步 - 用户友好的离线提示界面
450 lines
12 KiB
TypeScript
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
|
|
}
|
|
}) |