541 lines
16 KiB
Markdown
541 lines
16 KiB
Markdown
# 离线功能触发机制与页面集成规划
|
||
|
||
**版本**: 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. **测试覆盖**: 需要覆盖离线/在线切换场景的测试
|