import { useState, useCallback, useMemo, useEffect, useRef } from 'react'; import { View, Text } from '@tarojs/components'; import Taro, { useRouter } from '@tarojs/taro'; import { useThrottledDidShow } from '@/hooks/useThrottledDidShow'; import { BLEManager } from '@/services/ble/BLEManager'; import { XiaomiBandAdapter } from '@/services/ble/adapters/XiaomiBandAdapter'; import { BloodPressureAdapter } from '@/services/ble/adapters/BloodPressureAdapter'; import { GlucoseMeterAdapter } from '@/services/ble/adapters/GlucoseMeterAdapter'; import { CustomBandAdapter } from '@/services/ble/adapters/GenericBleAdapter'; import { DataSyncScheduler } from '@/services/ble/DataSyncScheduler'; import { uploadReadings } from '@/services/device-sync'; import { useAuthStore } from '@/stores/auth'; import type { BLEDevice, NormalizedReading } from '@/services/ble/types'; import { useElderClass } from '../../hooks/useElderClass'; import './index.scss'; /** liveReadings 最大保留条数,防止内存无限增长 */ const MAX_LIVE_READINGS = 200; type PageState = 'idle' | 'scanning' | 'connecting' | 'connected' | 'syncing' | 'done' | 'error'; export default function DeviceSync() { const modeClass = useElderClass(); const currentPatient = useAuthStore((s) => s.currentPatient); const router = useRouter(); const returnTo = router.params.returnTo || ''; const [pageState, setPageState] = useState('idle'); const [devices, setDevices] = useState([]); const [selectedDevice, setSelectedDevice] = useState(null); const [liveReadings, setLiveReadings] = useState([]); const [syncCount, setSyncCount] = useState(0); const [errorMsg, setErrorMsg] = useState(''); const [lastSyncAt, setLastSyncAt] = useState(null); const [pendingCount, setPendingCount] = useState(0); const scheduler = useMemo(() => new DataSyncScheduler({ intervalMs: 60 * 60 * 1000, }), []); const bleManagerRef = useRef(null); const getBleManager = useCallback(() => { if (!bleManagerRef.current) { const mgr = new BLEManager({ scanTimeout: 10000, retryCount: 3 }); mgr.registerAdapter(XiaomiBandAdapter); mgr.registerAdapter(BloodPressureAdapter); mgr.registerAdapter(GlucoseMeterAdapter); mgr.registerAdapter(CustomBandAdapter); bleManagerRef.current = mgr; } return bleManagerRef.current; }, []); useThrottledDidShow(() => { const bleManager = getBleManager(); bleManager.setOnConnectionChange(() => {}); bleManager.setOnReadings((readings) => { setLiveReadings((prev) => { const merged = [...prev, ...readings]; return merged.length > MAX_LIVE_READINGS ? merged.slice(-MAX_LIVE_READINGS) : merged; }); }); // 显示上次同步时间 setLastSyncAt(scheduler.getLastSyncAt()); // 检查是否有未上传的缓冲数据 const buffer = (bleManager as any).dataBuffer; if (buffer) { setPendingCount(buffer.size()); } // 自动同步:超过间隔时尝试上传缓冲数据 if (currentPatient && scheduler.needsSync()) { scheduler.tryAutoSync(async () => { const count = await bleManager.flushPendingReadings(async (readings) => { return uploadReadings(currentPatient.id, 'buffered', undefined, readings); }); setLastSyncAt(Date.now()); setPendingCount(0); return { success: count > 0, uploadedCount: count }; }); } }, 10000); useEffect(() => { return () => { scheduler.destroy(); if (bleManagerRef.current) { bleManagerRef.current.destroy(); bleManagerRef.current = null; } }; }, [scheduler]); const handleScan = useCallback(async () => { setPageState('scanning'); setDevices([]); setErrorMsg(''); try { const found = await getBleManager().scanDevices(); setDevices(found); if (found.length === 0) { setErrorMsg('未发现支持的设备,请确认设备已开启蓝牙并靠近手机'); } setPageState('idle'); } catch (e: any) { setErrorMsg(e.message || '扫描失败'); setPageState('error'); } }, []); const handleConnect = useCallback(async (device: BLEDevice) => { setSelectedDevice(device); setPageState('connecting'); setErrorMsg(''); try { await getBleManager().connect(device); setPageState('connected'); } catch (e: any) { setErrorMsg(e.message || '连接失败'); setPageState('error'); } }, []); const handleSync = useCallback(async () => { if (!currentPatient || !selectedDevice) return; setPageState('syncing'); setErrorMsg(''); try { const result = await getBleManager().syncToServer(async (readings) => { return uploadReadings( currentPatient.id, selectedDevice.deviceId, selectedDevice.name, readings, ); }); if (result.success) { setSyncCount(result.uploadedCount); setLastSyncAt(Date.now()); setPageState('done'); // 如果从体征录入页跳转而来,将最新读数写入 storage 供回填 if (returnTo === 'input' && liveReadings.length > 0) { const mapped: Record = {}; for (const r of liveReadings) { if (r.device_type === 'blood_pressure') { if (r.metric === 'systolic' && typeof r.values.value === 'number') mapped.systolic = r.values.value; if (r.metric === 'diastolic' && typeof r.values.value === 'number') mapped.diastolic = r.values.value; // 兼容 values 中直接包含 systolic/diastolic 的格式 if (typeof r.values.systolic === 'number') mapped.systolic = r.values.systolic as number; if (typeof r.values.diastolic === 'number') mapped.diastolic = r.values.diastolic as number; } else if (r.device_type === 'blood_glucose' && typeof r.values.blood_glucose === 'number') { mapped.blood_sugar = r.values.blood_glucose as number; } else if (r.device_type === 'heart_rate' && typeof r.values.heart_rate === 'number') { mapped.heart_rate = r.values.heart_rate as number; } } if (Object.keys(mapped).length > 0) { Taro.setStorageSync('device_sync_result', JSON.stringify(mapped)); } } } else { setErrorMsg(result.error || '同步失败'); setPageState('error'); } } catch (e: any) { setErrorMsg(e.message || '同步失败'); setPageState('error'); } }, [currentPatient, selectedDevice, liveReadings, returnTo]); const handleDisconnect = useCallback(async () => { await getBleManager().disconnect(); setPageState('idle'); setSelectedDevice(null); setLiveReadings([]); setSyncCount(0); setErrorMsg(''); }, []); const renderIdle = () => ( D 设备同步 连接智能手环、血压计、血糖仪,自动采集健康数据 {(lastSyncAt || pendingCount > 0) && ( {lastSyncAt && ( 上次同步: {new Date(lastSyncAt).toLocaleTimeString()} )} {pendingCount > 0 && ( {pendingCount} 条数据待上传 )} )} 扫描设备 {devices.length > 0 && ( 发现的设备 {devices.map((d) => ( handleConnect(d)} > {d.name} {d.adapter?.name} 信号 {d.RSSI > -60 ? '强' : d.RSSI > -80 ? '中' : '弱'} ))} )} ); const renderConnected = () => ( 已连接: {selectedDevice?.name} {liveReadings.length > 0 && ( 实时数据 {liveReadings.slice(-5).reverse().map((r, i) => ( {r.device_type === 'heart_rate' ? '心率' : r.device_type === 'blood_pressure' ? `血压(${r.metric === 'systolic' ? '收缩压' : r.metric === 'diastolic' ? '舒张压' : 'MAP'})` : r.device_type === 'blood_glucose' ? '血糖' : r.device_type} {r.device_type === 'heart_rate' ? `${r.values.heart_rate} bpm` : r.metric ? `${r.values.value} ${r.values.unit}` : JSON.stringify(r.values)} ))} 已采集 {liveReadings.length} 条数据 )} 上传数据 断开连接 ); const renderDone = () => ( V 同步完成 成功上传 {syncCount} 条数据 { handleDisconnect(); if (returnTo === 'input') { Taro.navigateBack(); } }}> {returnTo === 'input' ? '返回录入' : '完成'} ); return ( 设备同步 {errorMsg && ( {errorMsg} )} {(pageState === 'scanning' || pageState === 'connecting' || pageState === 'syncing') && ( {pageState === 'scanning' && '正在扫描设备...'} {pageState === 'connecting' && '正在连接设备...'} {pageState === 'syncing' && '正在上传数据...'} )} {(pageState === 'idle' || pageState === 'error') && renderIdle()} {pageState === 'connected' && renderConnected()} {pageState === 'done' && renderDone()} ); }