# 离线功能触发机制与页面集成规划

**版本**: 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. **测试覆盖**: 需要覆盖离线/在线切换场景的测试
