ether-docs/_archive/domains-old/08-OFFLINE_INTEGRATION.md

541 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 离线功能触发机制与页面集成规划
**版本**: v1.0
**创建日期**: 2026-02-26
---
## 一、离线/同步触发机制
### 1.1 离线触发场景
```
┌─────────────────────────────────────────────────────────────┐
│ 离线触发场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 网络断开 │
│ - 用户进入电梯、地下室、隧道等信号弱区域 │
│ - 用户关闭移动数据/WiFi │
│ - 飞行模式开启 │
│ │
│ 2. 服务器不可达 │
│ - 服务器维护 │
│ - 网关超时 │
│ - DNS解析失败 │
│ │
│ 3. 请求失败 │
│ - 请求超时 │
│ - 连接错误 │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 同步触发场景
```
┌─────────────────────────────────────────────────────────────┐
│ 同步触发场景 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. 自动触发 │
│ ├── 网络恢复时 (onNetworkStatusChange) │
│ ├── 操作完成后立即尝试 (如果在线) │
│ └── 定时同步 (可配置默认30秒) │
│ │
│ 2. 手动触发 │
│ ├── 用户下拉刷新 │
│ ├── 用户点击"同步"按钮 │
│ └── 用户重试失败项 │
│ │
│ 3. 应用生命周期 │
│ ├── 应用启动时 │
│ ├── 应用从后台恢复时 │
│ └── 应用退出前 (可选) │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 1.3 当前代码中的触发点
| 触发点 | 文件 | 触发条件 | 处理逻辑 |
|--------|------|---------|---------|
| 网络状态变化 | `sync-queue.ts:36-44` | `uni.onNetworkStatusChange` | 离线→在线时自动处理队列 |
| 操作添加时 | `sync-queue.ts:74-76` | `add()` 方法调用 | 如果在线立即尝试同步 |
| 定时重试 | `sync-queue.ts:184-188` | 重试延迟后 | 检查网络后处理 |
| 手动触发 | `sync-queue.ts:205-215` | `retryFailed()` 调用 | 重置失败项并处理 |
### 1.4 业务操作触发流程
```
用户操作 (如: 接单)
┌───────────────────────────────────────┐
│ Repository 方法 (如: accept) │
│ if (在线) { │
│ 直接调用API │
│ 成功 → 更新本地状态 │
│ 失败 → 加入同步队列 │
│ } else { │
│ 更新本地状态 │
│ 加入同步队列 │
│ } │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 同步队列 (syncQueue.add) │
│ - 生成唯一ID │
│ - 记录时间戳 │
│ - 设置优先级 │
│ - 持久化到本地存储 │
└───────────────────────────────────────┘
┌───────────────────────────────────────┐
│ 如果在线 → 立即尝试同步 │
│ 如果离线 → 等待网络恢复 │
└───────────────────────────────────────┘
```
---
## 二、页面集成规划
### 2.1 需要集成的页面
| 页面 | 文件路径 | 集成内容 |
|------|---------|---------|
| 工单列表 | `pages/work-order/list.vue` | 离线工单显示、状态筛选 |
| 工单详情 | `pages/work-order/detail.vue` | 离线操作按钮、同步状态 |
| 巡检任务 | `pages/inspection/task/index.vue` | 离线任务显示 |
| 巡检执行 | `pages/inspection/execute/index.vue` | 离线签到、结果提交 |
| 访客登记 | `pages/visitor/register.vue` | 离线登记 |
| 访客记录 | `pages/visitor/records.vue` | 离线记录显示 |
| 首页 | `pages/index/index.vue` | 离线状态提示、待同步数量 |
### 2.2 公共组件
需要创建以下公共组件:
```
src/components/offline/
├── OfflineStatus.vue # 离线状态指示器
├── SyncIndicator.vue # 同步进度指示器
├── PendingBadge.vue # 待同步数量徽章
└── OfflineNotice.vue # 离线通知横幅
```
---
## 三、UI组件设计
### 3.1 离线状态指示器 (OfflineStatus.vue)
```vue
<template>
<view class="offline-status" :class="{ offline: !isOnline }">
<view class="status-icon">
<text class="icon">{{ isOnline ? '📶' : '📵' }}</text>
</view>
<text class="status-text">{{ statusText }}</text>
<view v-if="pendingCount > 0" class="pending-badge">
<text>{{ pendingCount }}</text>
</view>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { syncQueue } from '@/offline'
const isOnline = computed(() => syncQueue.isOnline())
const pendingCount = computed(() => syncQueue.count.value)
const statusText = computed(() => {
if (!isOnline.value) return '离线'
if (pendingCount.value > 0) return `同步中(${pendingCount.value})`
return '在线'
})
</script>
```
### 3.2 同步进度指示器 (SyncIndicator.vue)
```vue
<template>
<view v-if="show" class="sync-indicator">
<view class="sync-animation">
<text class="icon">🔄</text>
</view>
<text class="sync-text">正在同步...</text>
</view>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'
import { syncQueue } from '@/offline'
const show = ref(false)
let syncStartTime = 0
const handleSyncStart = () => {
show.value = true
syncStartTime = Date.now()
}
const handleSyncComplete = () => {
const elapsed = Date.now() - syncStartTime
setTimeout(() => {
show.value = false
}, Math.max(0, 500 - elapsed))
}
</script>
```
### 3.3 离线通知横幅 (OfflineNotice.vue)
```vue
<template>
<view v-if="!isOnline" class="offline-notice">
<text class="notice-icon">⚠️</text>
<text class="notice-text">当前处于离线状态,操作将在联网后自动同步</text>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { syncQueue } from '@/offline'
const isOnline = computed(() => syncQueue.isOnline())
</script>
<style scoped>
.offline-notice {
position: fixed;
top: 0;
left: 0;
right: 0;
background: #ff9800;
color: white;
padding: 8px 16px;
display: flex;
align-items: center;
z-index: 9999;
}
</style>
```
### 3.4 待同步徽章 (PendingBadge.vue)
```vue
<template>
<view v-if="count > 0" class="pending-badge">
<text class="badge-count">{{ count > 99 ? '99+' : count }}</text>
</view>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { syncQueue } from '@/offline'
const count = computed(() => syncQueue.count.value)
</script>
<style scoped>
.pending-badge {
background: #f56c6c;
color: white;
border-radius: 10px;
padding: 2px 6px;
font-size: 12px;
min-width: 18px;
text-align: center;
}
</style>
```
---
## 四、页面集成示例
### 4.1 工单详情页集成
```vue
<template>
<view class="work-order-detail">
<!-- 离线通知 -->
<OfflineNotice />
<!-- 同步状态 -->
<view class="sync-status" v-if="entity._offline">
<text v-if="entity._offline.syncStatus === 'PENDING'" class="pending">
⏳ 待同步
</text>
<text v-else-if="entity._offline.syncStatus === 'CONFLICT'" class="conflict">
⚠️ 同步冲突
</text>
<text v-else class="synced">
✅ 已同步
</text>
</view>
<!-- 工单内容 -->
<view class="content">
<!-- ... -->
</view>
<!-- 操作按钮 -->
<view class="actions">
<button
v-if="canAccept"
@click="handleAccept"
:loading="accepting"
>
接单
</button>
<button
v-if="canStart"
@click="handleStart"
:loading="starting"
>
开始处理
</button>
<button
v-if="canComplete"
@click="handleComplete"
:loading="completing"
>
完成工单
</button>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { workOrderRepository, syncQueue } from '@/offline'
import OfflineNotice from '@/components/offline/OfflineNotice.vue'
const props = defineProps<{ id: string }>()
const entity = ref<any>({})
const accepting = ref(false)
const starting = ref(false)
const completing = ref(false)
const isOnline = computed(() => syncQueue.isOnline())
const canAccept = computed(() =>
entity.value.status === 'ASSIGNED'
)
const canStart = computed(() =>
entity.value.status === 'ACCEPTED'
)
const canComplete = computed(() =>
entity.value.status === 'IN_PROGRESS'
)
onMounted(async () => {
entity.value = await workOrderRepository.get(props.id)
})
async function handleAccept() {
accepting.value = true
try {
await workOrderRepository.accept(props.id)
entity.value = await workOrderRepository.get(props.id)
if (!isOnline.value) {
uni.showToast({
title: '已离线接单,联网后自动同步',
icon: 'none'
})
}
} catch (error) {
uni.showToast({ title: '操作失败', icon: 'none' })
} finally {
accepting.value = false
}
}
async function handleStart() {
starting.value = true
try {
await workOrderRepository.start(props.id)
entity.value = await workOrderRepository.get(props.id)
} catch (error) {
uni.showToast({ title: '操作失败', icon: 'none' })
} finally {
starting.value = false
}
}
async function handleComplete() {
completing.value = true
try {
await workOrderRepository.complete(props.id, '处理完成')
entity.value = await workOrderRepository.get(props.id)
} catch (error) {
uni.showToast({ title: '操作失败', icon: 'none' })
} finally {
completing.value = false
}
}
</script>
```
### 4.2 首页集成
```vue
<template>
<view class="home">
<!-- 离线状态栏 -->
<view class="status-bar">
<OfflineStatus />
<view v-if="failedCount > 0" class="failed-sync" @click="handleRetryFailed">
<text>{{ failedCount }}项同步失败,点击重试</text>
</view>
</view>
<!-- 统计卡片 -->
<view class="stats">
<view class="stat-card">
<text class="stat-value">{{ pendingWorkOrders }}</text>
<text class="stat-label">待处理工单</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ todayInspections }}</text>
<text class="stat-label">今日巡检</text>
</view>
<view class="stat-card">
<text class="stat-value">{{ todayVisitors }}</text>
<text class="stat-label">今日访客</text>
</view>
</view>
<!-- 快捷入口 -->
<!-- ... -->
</view>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { workOrderRepository, inspectionRepository, visitorRepository, syncQueue } from '@/offline'
import OfflineStatus from '@/components/offline/OfflineStatus.vue'
const pendingWorkOrders = ref(0)
const todayInspections = ref(0)
const todayVisitors = ref(0)
const failedCount = computed(() => syncQueue.getFailedItems().length)
onMounted(async () => {
// 从离线存储加载数据
const workOrders = await workOrderRepository.getByStatus('ASSIGNED')
pendingWorkOrders.value = workOrders.length
const inspections = await inspectionRepository.getTodayTasks()
todayInspections.value = inspections.length
const visitors = await visitorRepository.getTodayVisitors()
todayVisitors.value = visitors.length
})
function handleRetryFailed() {
syncQueue.retryFailed()
uni.showToast({ title: '正在重试同步...', icon: 'none' })
}
</script>
```
---
## 五、初始化集成
### 5.1 main.ts 集成
```typescript
// src/main.ts
import { createSSRApp } from 'vue'
import App from './App.vue'
import { initOfflineStorage } from './offline'
// 初始化离线存储
initOfflineStorage()
export function createApp() {
const app = createSSRApp(App)
return { app }
}
```
### 5.2 App.vue 全局状态
```vue
<script setup lang="ts">
import { onLaunch, onShow, onHide } from '@dcloudio/uni-app'
import { syncQueue } from '@/offline'
onLaunch(() => {
console.log('App Launch')
})
onShow(() => {
// 应用从后台恢复时,尝试同步
if (syncQueue.isOnline()) {
syncQueue.process()
}
})
onHide(() => {
console.log('App Hide')
})
</script>
```
---
## 六、实施计划
### 阶段一:公共组件开发 (1天)
| 任务 | 预计时间 |
|------|---------|
| 创建 OfflineStatus.vue | 1小时 |
| 创建 SyncIndicator.vue | 1小时 |
| 创建 OfflineNotice.vue | 0.5小时 |
| 创建 PendingBadge.vue | 0.5小时 |
| 组件测试 | 2小时 |
### 阶段二:页面集成 (2天)
| 任务 | 预计时间 |
|------|---------|
| 首页集成 | 2小时 |
| 工单列表/详情集成 | 3小时 |
| 巡检任务/执行集成 | 3小时 |
| 访客登记/记录集成 | 2小时 |
| 集成测试 | 4小时 |
### 阶段三:优化完善 (1天)
| 任务 | 预计时间 |
|------|---------|
| 照片离线处理 | 3小时 |
| 消息模块离线 | 2小时 |
| 性能优化 | 2小时 |
| 文档完善 | 1小时 |
---
## 七、注意事项
1. **数据一致性**: 离线操作可能与服务器数据冲突,需要处理冲突场景
2. **存储容量**: 注意本地存储容量限制,定期清理过期数据
3. **用户体验**: 离线操作要有明确提示,避免用户困惑
4. **错误处理**: 同步失败要有重试机制和错误提示
5. **测试覆盖**: 需要覆盖离线/在线切换场景的测试