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:
2025-08-14 15:46:13 +08:00
parent 49fcc374f6
commit 0920667719
15 changed files with 2878 additions and 56 deletions

View 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>