feat(round-11): 弱网离线队列/跨国会议一键预约/团队多语言名片批量向导/NFC硬件出厂封印 (83套件984测试全绿)
这个提交包含在:
194
mpuniapp/common/offline-queue.uts
普通文件
194
mpuniapp/common/offline-queue.uts
普通文件
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* OmniCard AI (CRMmp) - 移动端弱网离线视觉指示器与断网重放队列
|
||||
* 严格遵循 AGENTS.md 规范与 uts 强类型规范,一套代码兼顾 App 与 Web/H5
|
||||
*/
|
||||
|
||||
export interface OfflineAction {
|
||||
id?: string;
|
||||
type: string;
|
||||
url: string;
|
||||
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
data?: any;
|
||||
header?: Record<string, any>;
|
||||
timestamp?: number;
|
||||
retryCount?: number;
|
||||
}
|
||||
|
||||
export interface OfflineQueueStatus {
|
||||
isOnline: boolean;
|
||||
networkType: string;
|
||||
pendingCount: number;
|
||||
lastSyncTime?: number;
|
||||
}
|
||||
|
||||
export interface ReplayResult {
|
||||
total: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
}
|
||||
|
||||
const STORAGE_KEY = 'omni_offline_queue';
|
||||
const DEFAULT_MAX_RETRIES = 3;
|
||||
|
||||
let onlineState = true;
|
||||
let currentNetworkType = 'wifi';
|
||||
let lastSyncTimestamp: number | undefined = undefined;
|
||||
|
||||
/**
|
||||
* 获取当前网络在线状态
|
||||
*/
|
||||
export function isOnline(): boolean {
|
||||
return onlineState;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动或通过原生事件更新网络状态
|
||||
*/
|
||||
export function setNetworkStatus(online: boolean, networkType = 'unknown'): void {
|
||||
onlineState = online;
|
||||
currentNetworkType = networkType;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离线队列所有条目
|
||||
*/
|
||||
export function getOfflineQueue(): OfflineAction[] {
|
||||
try {
|
||||
const raw = uni.getStorageSync(STORAGE_KEY);
|
||||
if (!raw) return [];
|
||||
const list = JSON.parse(String(raw));
|
||||
return Array.isArray(list) ? list : [];
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空离线队列
|
||||
*/
|
||||
export function clearOfflineQueue(): void {
|
||||
try {
|
||||
uni.removeStorageSync(STORAGE_KEY);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 暂存一个离线操作到本地队列 (FIFO)
|
||||
*/
|
||||
export function enqueueOfflineAction(action: OfflineAction): number {
|
||||
const queue = getOfflineQueue();
|
||||
const item: OfflineAction = {
|
||||
...action,
|
||||
id: action.id || `offline_${Date.now()}_${Math.random().toString(36).slice(2, 7)}`,
|
||||
timestamp: action.timestamp || Date.now(),
|
||||
retryCount: action.retryCount || 0,
|
||||
};
|
||||
|
||||
queue.push(item);
|
||||
try {
|
||||
uni.setStorageSync(STORAGE_KEY, JSON.stringify(queue));
|
||||
} catch (e) {}
|
||||
|
||||
return queue.length;
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络恢复时自动重放离线队列
|
||||
*/
|
||||
export async function replayOfflineQueue(
|
||||
executor: (action: OfflineAction) => Promise<boolean>,
|
||||
maxRetries = DEFAULT_MAX_RETRIES
|
||||
): Promise<ReplayResult> {
|
||||
const queue = getOfflineQueue();
|
||||
if (queue.length === 0) {
|
||||
return { total: 0, success: 0, failed: 0 };
|
||||
}
|
||||
|
||||
const remaining: OfflineAction[] = [];
|
||||
let successCount = 0;
|
||||
let failedCount = 0;
|
||||
|
||||
for (const item of queue) {
|
||||
let ok = false;
|
||||
try {
|
||||
ok = await executor(item);
|
||||
} catch (e) {
|
||||
ok = false;
|
||||
}
|
||||
|
||||
if (ok) {
|
||||
successCount++;
|
||||
} else {
|
||||
failedCount++;
|
||||
const nextRetries = (item.retryCount || 0) + 1;
|
||||
if (nextRetries < maxRetries) {
|
||||
remaining.push({
|
||||
...item,
|
||||
retryCount: nextRetries,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (remaining.length > 0) {
|
||||
uni.setStorageSync(STORAGE_KEY, JSON.stringify(remaining));
|
||||
} else {
|
||||
uni.removeStorageSync(STORAGE_KEY);
|
||||
}
|
||||
} catch (e) {}
|
||||
|
||||
lastSyncTimestamp = Date.now();
|
||||
return {
|
||||
total: queue.length,
|
||||
success: successCount,
|
||||
failed: failedCount,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取离线队列综合状态
|
||||
*/
|
||||
export function getOfflineQueueStatus(): OfflineQueueStatus {
|
||||
const queue = getOfflineQueue();
|
||||
return {
|
||||
isOnline: onlineState,
|
||||
networkType: currentNetworkType,
|
||||
pendingCount: queue.length,
|
||||
lastSyncTime: lastSyncTimestamp,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化多语言横幅提示文案
|
||||
*/
|
||||
export function formatOfflineBannerText(lang: string = 'en', pendingCount = 0): string {
|
||||
const normalized = (lang || 'en').toLowerCase().split('-')[0];
|
||||
switch (normalized) {
|
||||
case 'zh':
|
||||
return pendingCount > 0
|
||||
? `网络已断开 (${pendingCount} 条暂存待同步)`
|
||||
: '网络已断开,正在尝试重连...';
|
||||
case 'ar':
|
||||
return pendingCount > 0
|
||||
? `غير متصل بالشبكة (${pendingCount} في الانتظار)`
|
||||
: 'غير متصل بالشبكة، جاري إعادة الاتصال...';
|
||||
case 'es':
|
||||
return pendingCount > 0
|
||||
? `Sin conexión (${pendingCount} pendientes)`
|
||||
: 'Sin conexión, reintentando...';
|
||||
case 'fr':
|
||||
return pendingCount > 0
|
||||
? `Hors ligne (${pendingCount} en attente)`
|
||||
: 'Hors ligne, reconnexion en cours...';
|
||||
case 'ja':
|
||||
return pendingCount > 0
|
||||
? `オフライン (${pendingCount} 件の未送信保留)`
|
||||
: 'オフラインです。再接続中...';
|
||||
case 'en':
|
||||
default:
|
||||
return pendingCount > 0
|
||||
? `Offline mode (${pendingCount} pending)`
|
||||
: 'Offline, reconnecting...';
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户