feat: 完成任务2 - 实现本地数据模型和离线功能
任务2.1 创建本地数据模型 - 完善 SQLite 数据库表结构和数据模型 - 创建完整的 TypeScript 接口定义 - 实现数据验证和格式化工具函数 - 添加数据清理和类型守卫功能 任务2.2 实现本地数据 CRUD 操作 - 扩展数据库服务,添加完整的 CRUD 操作 - 实现 Repository 模式封装数据访问层 - 创建 TransactionRepository、CategoryRepository、AccountRepository - 添加批量操作、统计查询和数据迁移功能 - 实现 RepositoryManager 统一管理数据操作 任务2.3 建立离线优先的数据架构 - 重构 Pinia store 使用 Repository 模式 - 创建离线状态管理 store (useOfflineStore) - 实现网络状态监听和离线操作队列 - 添加离线指示器组件和用户界面 - 建立数据同步标记和冲突检测机制 - 实现离线优先的数据缓存策略 核心特性: - 完整的本地数据 CRUD 操作 - 离线优先架构,支持断网使用 - 数据验证和格式化 - Repository 模式数据访问层 - 网络状态监听和自动同步 - 用户友好的离线提示界面
This commit is contained in:
207
frontend/src/components/common/OfflineIndicator.vue
Normal file
207
frontend/src/components/common/OfflineIndicator.vue
Normal file
@@ -0,0 +1,207 @@
|
||||
<template>
|
||||
<Transition
|
||||
enter-active-class="transition-all duration-300 ease-out"
|
||||
enter-from-class="transform -translate-y-full opacity-0"
|
||||
enter-to-class="transform translate-y-0 opacity-100"
|
||||
leave-active-class="transition-all duration-300 ease-in"
|
||||
leave-from-class="transform translate-y-0 opacity-100"
|
||||
leave-to-class="transform -translate-y-full opacity-0"
|
||||
>
|
||||
<div
|
||||
v-if="showIndicator"
|
||||
class="fixed top-0 left-0 right-0 z-50 bg-orange-500 text-white px-4 py-2 text-sm"
|
||||
>
|
||||
<div class="max-w-md mx-auto flex items-center justify-between">
|
||||
<div class="flex items-center space-x-2">
|
||||
<svg
|
||||
v-if="!offlineStore.isOnline"
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M18.364 5.636l-12.728 12.728m0 0L5.636 18.364m12.728-12.728L5.636 5.636m12.728 12.728L18.364 18.364"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-else-if="offlineStore.isSyncing"
|
||||
class="w-4 h-4 animate-spin"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<svg
|
||||
v-else
|
||||
class="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"
|
||||
/>
|
||||
</svg>
|
||||
|
||||
<span>{{ statusMessage }}</span>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<!-- 待同步数量 -->
|
||||
<span
|
||||
v-if="offlineStore.totalPendingCount > 0"
|
||||
class="bg-white bg-opacity-20 px-2 py-1 rounded text-xs"
|
||||
>
|
||||
{{ offlineStore.totalPendingCount }} 待同步
|
||||
</span>
|
||||
|
||||
<!-- 同步进度 -->
|
||||
<div
|
||||
v-if="offlineStore.isSyncing"
|
||||
class="w-16 h-1 bg-white bg-opacity-20 rounded-full overflow-hidden"
|
||||
>
|
||||
<div
|
||||
class="h-full bg-white transition-all duration-300"
|
||||
:style="{ width: `${offlineStore.syncProgress}%` }"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<button
|
||||
v-if="offlineStore.isOnline && offlineStore.totalPendingCount > 0 && !offlineStore.isSyncing"
|
||||
@click="handleSync"
|
||||
class="text-xs underline hover:no-underline"
|
||||
>
|
||||
立即同步
|
||||
</button>
|
||||
|
||||
<button
|
||||
v-if="offlineStore.hasFailedActions"
|
||||
@click="handleRetry"
|
||||
class="text-xs underline hover:no-underline"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
|
||||
<!-- 关闭按钮 -->
|
||||
<button
|
||||
@click="handleClose"
|
||||
class="ml-2 hover:bg-white hover:bg-opacity-20 rounded p-1"
|
||||
>
|
||||
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useOfflineStore } from '@/stores/offline'
|
||||
|
||||
const offlineStore = useOfflineStore()
|
||||
|
||||
// 显示指示器的条件
|
||||
const showIndicator = computed(() => {
|
||||
return (
|
||||
offlineStore.showOfflineIndicator ||
|
||||
!offlineStore.isOnline ||
|
||||
offlineStore.isSyncing ||
|
||||
offlineStore.totalPendingCount > 0
|
||||
)
|
||||
})
|
||||
|
||||
// 状态消息
|
||||
const statusMessage = computed(() => {
|
||||
if (offlineStore.isSyncing) {
|
||||
return `正在同步... (${Math.round(offlineStore.syncProgress)}%)`
|
||||
}
|
||||
|
||||
if (!offlineStore.isOnline) {
|
||||
return offlineStore.offlineMessage || '离线模式 - 数据将在网络恢复后同步'
|
||||
}
|
||||
|
||||
if (offlineStore.totalPendingCount > 0) {
|
||||
return `有 ${offlineStore.totalPendingCount} 项更改待同步`
|
||||
}
|
||||
|
||||
if (offlineStore.hasFailedActions) {
|
||||
return '部分数据同步失败'
|
||||
}
|
||||
|
||||
return '已连接'
|
||||
})
|
||||
|
||||
// 处理同步
|
||||
async function handleSync() {
|
||||
try {
|
||||
await offlineStore.manualSync()
|
||||
} catch (error) {
|
||||
console.error('手动同步失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理重试
|
||||
async function handleRetry() {
|
||||
try {
|
||||
await offlineStore.retryFailedActions()
|
||||
} catch (error) {
|
||||
console.error('重试失败:', error)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理关闭
|
||||
function handleClose() {
|
||||
offlineStore.hideOfflineIndicator()
|
||||
}
|
||||
|
||||
// 自动隐藏成功消息
|
||||
let hideTimer: NodeJS.Timeout | null = null
|
||||
|
||||
function scheduleAutoHide() {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
|
||||
// 如果是成功状态,3秒后自动隐藏
|
||||
if (offlineStore.isOnline && offlineStore.totalPendingCount === 0 && !offlineStore.isSyncing) {
|
||||
hideTimer = setTimeout(() => {
|
||||
offlineStore.hideOfflineIndicator()
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// 监听状态变化,自动隐藏成功消息
|
||||
scheduleAutoHide()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
if (hideTimer) {
|
||||
clearTimeout(hideTimer)
|
||||
}
|
||||
})
|
||||
</script>
|
Reference in New Issue
Block a user