feat: 实现小程序离线巡检与可靠同步
This commit is contained in:
@@ -120,6 +120,33 @@
|
||||
margin: $spacing-lg 0 $spacing-md;
|
||||
}
|
||||
|
||||
.queueMeta {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-text-tertiary;
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
.offlineCard {
|
||||
background: #fff8e6;
|
||||
border-radius: $radius-md;
|
||||
padding: $spacing-md;
|
||||
margin-bottom: $spacing-sm;
|
||||
box-shadow: $shadow-card;
|
||||
}
|
||||
|
||||
.offlineStatus {
|
||||
font-size: $font-size-xs;
|
||||
color: $color-warning;
|
||||
font-weight: $font-weight-semibold;
|
||||
}
|
||||
|
||||
.offlineError {
|
||||
display: block;
|
||||
font-size: $font-size-xs;
|
||||
color: $color-error;
|
||||
margin-top: $spacing-xs;
|
||||
}
|
||||
|
||||
.historyList {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -2,7 +2,10 @@ import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { Button, Image, ScrollView, Text, View } from '@tarojs/components';
|
||||
import Taro, { usePullDownRefresh } from '@tarojs/taro';
|
||||
import styles from './index.module.scss';
|
||||
import { listInspections, uploadInspection } from '@/api/inspections';
|
||||
import { listInspections } from '@/api/inspections';
|
||||
import { offlineQueue } from '@/services/offlineQueueApp';
|
||||
import type { OfflineInspectionItem } from '@/services/offlineQueue';
|
||||
import { useStore } from '@/store/useStore';
|
||||
import { formatRelativeTime } from '@/utils/format';
|
||||
import type { InspectionRecord } from '@/types';
|
||||
|
||||
@@ -14,11 +17,19 @@ const CLASS_LABELS: Record<string, string> = {
|
||||
const classLabel = (cls: string) => CLASS_LABELS[cls] || cls;
|
||||
|
||||
const InspectionPage: React.FC = () => {
|
||||
const syncOfflineQueue = useStore((s) => s.syncOfflineQueue);
|
||||
const [imagePath, setImagePath] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const [result, setResult] = useState<InspectionRecord | null>(null);
|
||||
const [history, setHistory] = useState<InspectionRecord[]>([]);
|
||||
const [loadingHistory, setLoadingHistory] = useState(false);
|
||||
const [offlineItems, setOfflineItems] = useState<OfflineInspectionItem[]>([]);
|
||||
const [queueStats, setQueueStats] = useState({ total: 0, pending: 0, synced: 0, failed: 0, conflict: 0, remaining: 30 });
|
||||
|
||||
const refreshQueue = useCallback(() => {
|
||||
setOfflineItems(offlineQueue.list());
|
||||
setQueueStats(offlineQueue.stats());
|
||||
}, []);
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
setLoadingHistory(true);
|
||||
@@ -31,13 +42,38 @@ const InspectionPage: React.FC = () => {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refreshQueue();
|
||||
loadHistory();
|
||||
}, [loadHistory]);
|
||||
syncOfflineQueue().then(() => {
|
||||
refreshQueue();
|
||||
loadHistory();
|
||||
});
|
||||
}, [loadHistory, refreshQueue, syncOfflineQueue]);
|
||||
|
||||
usePullDownRefresh(() => {
|
||||
loadHistory().then(() => Taro.stopPullDownRefresh());
|
||||
syncOfflineQueue()
|
||||
.then(() => {
|
||||
refreshQueue();
|
||||
loadHistory();
|
||||
})
|
||||
.finally(() => Taro.stopPullDownRefresh());
|
||||
});
|
||||
|
||||
const persistImage = (tempPath: string) =>
|
||||
new Promise<string>((resolve) => {
|
||||
const taroAny = Taro as any;
|
||||
const fs = taroAny.getFileSystemManager?.();
|
||||
if (!fs || !fs.saveFile) {
|
||||
resolve(tempPath);
|
||||
return;
|
||||
}
|
||||
fs.saveFile({
|
||||
tempFilePath: tempPath,
|
||||
success: (res: { savedFilePath: string }) => resolve(res.savedFilePath),
|
||||
fail: () => resolve(tempPath),
|
||||
});
|
||||
});
|
||||
|
||||
const handleChooseImage = () => {
|
||||
Taro.chooseImage({
|
||||
count: 1,
|
||||
@@ -60,9 +96,11 @@ const InspectionPage: React.FC = () => {
|
||||
}
|
||||
setUploading(true);
|
||||
try {
|
||||
const rec = await uploadInspection(imagePath);
|
||||
setResult(rec);
|
||||
Taro.showToast({ title: rec.aiStatus === 'done' ? '检测完成' : '检测失败', icon: 'none' });
|
||||
const savedPath = await persistImage(imagePath);
|
||||
offlineQueue.enqueue(savedPath);
|
||||
Taro.showToast({ title: '已保存到离线队列', icon: 'none' });
|
||||
await syncOfflineQueue();
|
||||
refreshQueue();
|
||||
loadHistory();
|
||||
} catch (err) {
|
||||
Taro.showToast({
|
||||
@@ -136,6 +174,28 @@ const InspectionPage: React.FC = () => {
|
||||
) : null}
|
||||
|
||||
<Text className={styles.sectionTitle}>巡检记录</Text>
|
||||
<Text className={styles.sectionTitle}>离线队列(剩余 {queueStats.remaining})</Text>
|
||||
<View className={styles.queueMeta}>
|
||||
待同步 {queueStats.pending} · 失败 {queueStats.failed} · 冲突 {queueStats.conflict}
|
||||
</View>
|
||||
{offlineItems.map((item) => (
|
||||
<View key={item.id} className={styles.offlineCard}>
|
||||
<View className={styles.historyHeader}>
|
||||
<Text className={styles.offlineStatus}>{item.state}</Text>
|
||||
<Text className={styles.historyTime}>{item.idempotencyKey.slice(0, 18)}</Text>
|
||||
</View>
|
||||
{item.error ? <Text className={styles.offlineError}>{item.error}</Text> : null}
|
||||
</View>
|
||||
))}
|
||||
<Button
|
||||
className={styles.btnPrimary}
|
||||
onClick={() => {
|
||||
offlineQueue.clearSynced();
|
||||
refreshQueue();
|
||||
}}
|
||||
>
|
||||
清理已同步
|
||||
</Button>
|
||||
<View className={styles.historyList}>
|
||||
{history.map((rec) => (
|
||||
<View key={rec.id} className={styles.historyCard}>
|
||||
|
||||
Reference in New Issue
Block a user