Files
bill/frontend/src/stores/offline.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

354 lines
9.1 KiB
TypeScript

import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface OfflineAction {
id: string
type: 'create' | 'update' | 'delete'
entity: 'transaction' | 'category' | 'account'
data: any
timestamp: string
retryCount: number
maxRetries: number
error?: string
}
export const useOfflineStore = defineStore('offline', () => {
// 网络状态
const isOnline = ref(navigator.onLine)
const connectionType = ref<string>('unknown')
const lastOnlineTime = ref<Date | null>(null)
const lastOfflineTime = ref<Date | null>(null)
// 离线操作队列
const pendingActions = ref<OfflineAction[]>([])
const failedActions = ref<OfflineAction[]>([])
// 同步状态
const isSyncing = ref(false)
const syncProgress = ref(0)
const syncError = ref<string | null>(null)
const lastSyncTime = ref<Date | null>(null)
// 离线提示
const showOfflineIndicator = ref(false)
const offlineMessage = ref('')
// 计算属性
const hasConnection = computed(() => isOnline.value)
const hasPendingActions = computed(() => pendingActions.value.length > 0)
const hasFailedActions = computed(() => failedActions.value.length > 0)
const totalPendingCount = computed(() => pendingActions.value.length + failedActions.value.length)
const connectionStatus = computed(() => {
if (isOnline.value) {
return {
status: 'online',
message: '已连接',
color: 'green'
}
} else {
return {
status: 'offline',
message: '离线模式',
color: 'orange'
}
}
})
// 网络状态监听
function setupNetworkListeners() {
// 监听网络状态变化
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
// 检测连接类型(如果支持)
if ('connection' in navigator) {
const connection = (navigator as any).connection
connectionType.value = connection.effectiveType || 'unknown'
connection.addEventListener('change', () => {
connectionType.value = connection.effectiveType || 'unknown'
})
}
// 初始状态
updateNetworkStatus()
}
// 处理上线事件
function handleOnline() {
isOnline.value = true
lastOnlineTime.value = new Date()
showOfflineIndicator.value = false
console.log('网络已连接,开始同步离线数据')
// 自动同步离线操作
if (hasPendingActions.value || hasFailedActions.value) {
syncOfflineActions()
}
}
// 处理离线事件
function handleOffline() {
isOnline.value = false
lastOfflineTime.value = new Date()
showOfflineIndicator.value = true
offlineMessage.value = '当前处于离线模式,数据将在网络恢复后自动同步'
console.log('网络已断开,进入离线模式')
}
// 更新网络状态
function updateNetworkStatus() {
const wasOnline = isOnline.value
isOnline.value = navigator.onLine
if (wasOnline !== isOnline.value) {
if (isOnline.value) {
handleOnline()
} else {
handleOffline()
}
}
}
// 添加离线操作到队列
function addOfflineAction(action: Omit<OfflineAction, 'id' | 'timestamp' | 'retryCount'>) {
const offlineAction: OfflineAction = {
...action,
id: generateActionId(),
timestamp: new Date().toISOString(),
retryCount: 0
}
pendingActions.value.push(offlineAction)
console.log('添加离线操作到队列:', offlineAction)
}
// 生成操作ID
function generateActionId(): string {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
// 同步离线操作
async function syncOfflineActions() {
if (isSyncing.value || !isOnline.value) return
isSyncing.value = true
syncProgress.value = 0
syncError.value = null
try {
const allActions = [...pendingActions.value, ...failedActions.value]
const totalActions = allActions.length
if (totalActions === 0) {
isSyncing.value = false
return
}
console.log(`开始同步 ${totalActions} 个离线操作`)
for (let i = 0; i < allActions.length; i++) {
const action = allActions[i]
try {
await executeOfflineAction(action)
// 从队列中移除成功的操作
removeActionFromQueues(action.id)
syncProgress.value = ((i + 1) / totalActions) * 100
} catch (error) {
console.error('同步操作失败:', action, error)
// 增加重试次数
action.retryCount++
action.error = error instanceof Error ? error.message : 'Unknown error'
// 如果超过最大重试次数,移到失败队列
if (action.retryCount >= action.maxRetries) {
moveActionToFailed(action)
}
}
}
lastSyncTime.value = new Date()
console.log('离线操作同步完成')
} catch (error) {
syncError.value = error instanceof Error ? error.message : 'Sync failed'
console.error('同步过程中发生错误:', error)
} finally {
isSyncing.value = false
syncProgress.value = 100
}
}
// 执行离线操作
async function executeOfflineAction(action: OfflineAction) {
// 这里将在实际的云端同步功能中实现
// 目前只是模拟执行
console.log('执行离线操作:', action)
// 模拟网络请求延迟
await new Promise(resolve => setTimeout(resolve, 500))
// 模拟可能的失败
if (Math.random() < 0.1) { // 10% 失败率
throw new Error('模拟网络错误')
}
}
// 从队列中移除操作
function removeActionFromQueues(actionId: string) {
pendingActions.value = pendingActions.value.filter(a => a.id !== actionId)
failedActions.value = failedActions.value.filter(a => a.id !== actionId)
}
// 将操作移到失败队列
function moveActionToFailed(action: OfflineAction) {
pendingActions.value = pendingActions.value.filter(a => a.id !== action.id)
if (!failedActions.value.find(a => a.id === action.id)) {
failedActions.value.push(action)
}
}
// 重试失败的操作
async function retryFailedActions() {
if (!isOnline.value) {
throw new Error('网络未连接,无法重试')
}
// 将失败的操作移回待处理队列
const actionsToRetry = failedActions.value.map(action => ({
...action,
retryCount: 0,
error: undefined
}))
failedActions.value = []
pendingActions.value.push(...actionsToRetry)
// 开始同步
await syncOfflineActions()
}
// 清除失败的操作
function clearFailedActions() {
failedActions.value = []
}
// 清除所有离线操作
function clearAllActions() {
pendingActions.value = []
failedActions.value = []
}
// 手动触发同步
async function manualSync() {
if (!isOnline.value) {
throw new Error('网络未连接,无法同步')
}
await syncOfflineActions()
}
// 获取离线统计
function getOfflineStats() {
return {
isOnline: isOnline.value,
connectionType: connectionType.value,
pendingCount: pendingActions.value.length,
failedCount: failedActions.value.length,
totalCount: totalPendingCount.value,
lastOnlineTime: lastOnlineTime.value,
lastOfflineTime: lastOfflineTime.value,
lastSyncTime: lastSyncTime.value,
isSyncing: isSyncing.value,
syncProgress: syncProgress.value
}
}
// 设置离线提示
function setOfflineMessage(message: string, show = true) {
offlineMessage.value = message
showOfflineIndicator.value = show
}
// 隐藏离线提示
function hideOfflineIndicator() {
showOfflineIndicator.value = false
}
// 检查网络连接质量
async function checkConnectionQuality() {
if (!isOnline.value) return 'offline'
try {
const start = Date.now()
const response = await fetch('/api/ping', {
method: 'HEAD',
cache: 'no-cache'
})
const duration = Date.now() - start
if (response.ok) {
if (duration < 100) return 'excellent'
if (duration < 300) return 'good'
if (duration < 1000) return 'fair'
return 'poor'
} else {
return 'poor'
}
} catch (error) {
return 'offline'
}
}
// 清理资源
function cleanup() {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
return {
// 状态
isOnline,
connectionType,
lastOnlineTime,
lastOfflineTime,
pendingActions,
failedActions,
isSyncing,
syncProgress,
syncError,
lastSyncTime,
showOfflineIndicator,
offlineMessage,
// 计算属性
hasConnection,
hasPendingActions,
hasFailedActions,
totalPendingCount,
connectionStatus,
// 方法
setupNetworkListeners,
updateNetworkStatus,
addOfflineAction,
syncOfflineActions,
retryFailedActions,
clearFailedActions,
clearAllActions,
manualSync,
getOfflineStats,
setOfflineMessage,
hideOfflineIndicator,
checkConnectionQuality,
cleanup
}
})