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
|
||
|
}
|
||
|
})
|