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