/** * OfflineIndicator Component * * Displays offline mode status, pending message count, and reconnection info. * Shows a prominent banner when the app is offline with visual feedback. */ import { useState, useEffect } from 'react'; import { motion, AnimatePresence } from 'framer-motion'; import { WifiOff, CloudOff, RefreshCw, Clock, AlertCircle, CheckCircle, Send, X, ChevronDown, ChevronUp, } from 'lucide-react'; import { useOfflineStore, type QueuedMessage } from '../store/offlineStore'; import { useConnectionStore } from '../store/connectionStore'; interface OfflineIndicatorProps { /** Show compact version (minimal) */ compact?: boolean; /** Show pending messages list */ showQueue?: boolean; /** Additional CSS classes */ className?: string; /** Callback when reconnect button is clicked */ onReconnect?: () => void; } /** * Format relative time */ function formatRelativeTime(timestamp: number): string { const seconds = Math.floor((Date.now() - timestamp) / 1000); if (seconds < 60) return '刚刚'; if (seconds < 3600) return `${Math.floor(seconds / 60)}分钟前`; if (seconds < 86400) return `${Math.floor(seconds / 3600)}小时前`; return `${Math.floor(seconds / 86400)}天前`; } /** * Format reconnect delay for display */ function formatReconnectDelay(delay: number): string { if (delay < 1000) return '立即'; if (delay < 60000) return `${Math.ceil(delay / 1000)}秒`; return `${Math.ceil(delay / 60000)}分钟`; } /** * Truncate message content for display */ function truncateContent(content: string, maxLength: number = 50): string { if (content.length <= maxLength) return content; return content.slice(0, maxLength) + '...'; } /** * Full offline indicator with banner, queue, and reconnect info */ export function OfflineIndicator({ compact = false, showQueue = true, className = '', onReconnect, }: OfflineIndicatorProps) { const { isOffline, isReconnecting, reconnectAttempt, nextReconnectDelay, queuedMessages, cancelReconnect, } = useOfflineStore(); const connect = useConnectionStore((s) => s.connect); const [showMessageQueue, setShowMessageQueue] = useState(false); const [countdown, setCountdown] = useState(null); // Countdown timer for reconnection useEffect(() => { if (!isReconnecting || !nextReconnectDelay) { setCountdown(null); return; } const endTime = Date.now() + nextReconnectDelay; setCountdown(nextReconnectDelay); const interval = setInterval(() => { const remaining = Math.max(0, endTime - Date.now()); setCountdown(remaining); if (remaining === 0) { clearInterval(interval); } }, 1000); return () => clearInterval(interval); }, [isReconnecting, nextReconnectDelay]); // Handle manual reconnect const handleReconnect = async () => { onReconnect?.(); try { await connect(); } catch (err) { console.error('[OfflineIndicator] Manual reconnect failed:', err); } }; const pendingCount = queuedMessages.filter( (m) => m.status === 'pending' || m.status === 'failed' ).length; // Don't show if online and no pending messages if (!isOffline && pendingCount === 0) { return null; } // Compact version for headers/toolbars if (compact) { return (
{isOffline ? ( <> 离线模式 {pendingCount > 0 && ( {pendingCount} 条待发 )} ) : ( <> 已恢复连接 {pendingCount > 0 && ( 发送中 {pendingCount} 条 )} )}
); } // Full banner version return ( {/* Main Banner */}
{/* Status Icon */} {isOffline ? ( ) : ( )} {/* Status Text */}
{isOffline ? '后端服务不可用' : '连接已恢复'}
{isReconnecting ? ( <> 正在尝试重连 ({reconnectAttempt}次) {countdown !== null && ( {formatReconnectDelay(countdown)}后重试 )} ) : isOffline ? ( '消息将保存在本地,连接后自动发送' ) : pendingCount > 0 ? ( `正在发送 ${pendingCount} 条排队消息...` ) : ( '所有消息已同步' )}
{/* Actions */}
{isOffline && !isReconnecting && ( )} {isReconnecting && ( )} {showQueue && pendingCount > 0 && ( )}
{/* Message Queue */} {showMessageQueue && pendingCount > 0 && (
排队消息
{queuedMessages .filter((m) => m.status === 'pending' || m.status === 'failed') .map((msg) => ( ))}
)}
); } /** * Individual queued message item */ function QueuedMessageItem({ message }: { message: QueuedMessage }) { const { removeMessage } = useOfflineStore(); const statusConfig = { pending: { icon: Clock, color: 'text-gray-400', label: '等待中' }, sending: { icon: Send, color: 'text-blue-500', label: '发送中' }, failed: { icon: AlertCircle, color: 'text-red-500', label: '发送失败' }, sent: { icon: CheckCircle, color: 'text-green-500', label: '已发送' }, }; const config = statusConfig[message.status]; const StatusIcon = config.icon; return (

{truncateContent(message.content)}

{formatRelativeTime(message.timestamp)} {message.status === 'failed' && message.lastError && ( {message.lastError} )}
{message.status === 'failed' && ( )}
); } /** * Minimal connection status indicator for headers */ export function ConnectionStatusBadge({ className = '' }: { className?: string }) { const connectionState = useConnectionStore((s) => s.connectionState); const queuedMessages = useOfflineStore((s) => s.queuedMessages); const pendingCount = queuedMessages.filter( (m) => m.status === 'pending' || m.status === 'failed' ).length; const isConnected = connectionState === 'connected'; return (
{isConnected ? '在线' : connectionState === 'reconnecting' ? '重连中' : '离线'} {pendingCount > 0 && ( {pendingCount} )}
); } export default OfflineIndicator;