import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:permission_handler/permission_handler.dart'; import '../../core/app_colors.dart'; import '../../core/app_theme.dart'; import '../../core/navigation_provider.dart'; import '../../providers/auth_provider.dart'; import '../../providers/omron_device_provider.dart'; import '../../services/omron_ble_service.dart'; class DeviceScanPage extends ConsumerStatefulWidget { const DeviceScanPage({super.key}); @override ConsumerState createState() => _DeviceScanPageState(); } class _DeviceScanPageState extends ConsumerState with SingleTickerProviderStateMixin { final _results = []; bool _scanning = false; String? _connectingId; StreamSubscription? _scanSub; bool _connected = false; String _deviceName = ''; bool _readingReceived = false; int? _systolic, _diastolic, _pulse; StreamSubscription? _readingSub; StreamSubscription? _bleConnSub; late AnimationController _pulseCtrl; late Animation _pulseAnim; @override void initState() { super.initState(); _pulseCtrl = AnimationController( vsync: this, duration: const Duration(milliseconds: 1200), ); _pulseAnim = Tween( begin: 0.8, end: 1.4, ).animate(CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut)); _pulseCtrl.repeat(reverse: true); _scanSub = FlutterBluePlus.scanResults.listen((list) { if (!mounted) return; final bpList = list.where((r) => OmronBleService.isBpDevice(r)).toList(); setState(() { _results.clear(); _results.addAll(bpList); }); }); _start(); } @override void dispose() { _pulseCtrl.dispose(); _scanSub?.cancel(); _readingSub?.cancel(); _bleConnSub?.cancel(); FlutterBluePlus.stopScan(); super.dispose(); } Future _start() async { setState(() { _scanning = true; _connected = false; }); _results.clear(); await [Permission.bluetoothScan, Permission.bluetoothConnect].request(); if (Platform.isAndroid) { final locStatus = await Permission.locationWhenInUse.serviceStatus; if (locStatus != ServiceStatus.enabled && mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar( content: Text('请先打开GPS定位,否则无法扫描蓝牙'), backgroundColor: AppColors.warning, ), ); } } final adapterState = await FlutterBluePlus.adapterState.first; if (adapterState != BluetoothAdapterState.on) { try { await FlutterBluePlus.turnOn(); } catch (_) {} } try { await FlutterBluePlus.stopScan(); } catch (_) {} await FlutterBluePlus.startScan( timeout: const Duration(seconds: 30), androidScanMode: AndroidScanMode.lowLatency, ); Future.delayed(const Duration(seconds: 30), () { FlutterBluePlus.stopScan(); if (mounted) setState(() => _scanning = false); }); } Future _connect(ScanResult r) async { setState(() => _connectingId = r.device.remoteId.toString()); FlutterBluePlus.stopScan(); _scanSub?.cancel(); try { final ble = ref.read(omronBleServiceProvider); await ble.connect(r.device); final name = r.advertisementData.advName.isNotEmpty ? r.advertisementData.advName : (r.device.platformName.isNotEmpty ? r.device.platformName : '血压计'); await ref .read(omronDeviceProvider.notifier) .bind(r.device.remoteId.toString(), name); _bleConnSub = r.device.connectionState.listen((s) { if (s == BluetoothConnectionState.disconnected && mounted) { _readingSub?.cancel(); ref.read(omronBleServiceProvider).disconnect(); if (_readingReceived) { popRoute(ref); } else { setState(() { _connected = false; }); _start(); } } }); _readingSub = ble.readings.listen((reading) async { debugPrint('[PAGE] 收到: ${reading.systolic}/${reading.diastolic}'); if (mounted) { setState(() { _readingReceived = true; _systolic = reading.systolic; _diastolic = reading.diastolic; _pulse = reading.pulse; }); try { final api = ref.read(apiClientProvider); await api.post('/api/health-records', data: reading.toHealthRecord()); final heartRateRecord = reading.toHeartRateRecord(); if (heartRateRecord != null) { await api.post('/api/health-records', data: heartRateRecord); } await ref.read(omronDeviceProvider.notifier).onReading(reading); } catch (e) { debugPrint('[PAGE] 上报失败: $e'); } Future.delayed(const Duration(milliseconds: 1500), () { if (mounted) popRoute(ref); }); } }); if (mounted) { setState(() { _connected = true; _deviceName = name; _connectingId = null; _readingReceived = false; }); } } catch (e) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('连接失败: $e'), backgroundColor: AppColors.error), ); _start(); } } } @override Widget build(BuildContext context) { return GradientScaffold( appBar: AppBar( backgroundColor: Colors.white.withValues(alpha: 0.9), elevation: 0, title: Text( _connected ? _deviceName : '添加设备', style: const TextStyle(color: AppColors.textPrimary), ), leading: IconButton( icon: const Icon(Icons.arrow_back, color: AppColors.textPrimary), onPressed: () { if (_connected) ref.read(omronBleServiceProvider).disconnect(); popRoute(ref); }, ), ), body: _connected ? _buildConnected() : _buildScan(), ); } // ── 扫描视图 ── Widget _buildScan() => Column( children: [ if (_scanning && _results.isEmpty) Expanded(child: _buildScanningCenter()), if (!_scanning && _results.isEmpty) Expanded(child: _buildScanningCenter()), if (_results.isNotEmpty) Expanded( child: ListView.builder( padding: const EdgeInsets.all(16), itemCount: _results.length, itemBuilder: (_, i) => _buildTile(_results[i]), ), ), if (!_scanning) Padding( padding: const EdgeInsets.all(16), child: SizedBox( width: double.infinity, child: OutlinedButton.icon( onPressed: _start, icon: const Icon(Icons.refresh), label: const Text('重新扫描'), style: OutlinedButton.styleFrom( foregroundColor: AppColors.primary, side: const BorderSide(color: AppColors.border), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(14), ), padding: const EdgeInsets.symmetric(vertical: 14), ), ), ), ), ], ); Widget _buildScanningCenter() => Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Stack( alignment: Alignment.center, children: [ AnimatedBuilder( animation: _pulseAnim, builder: (_, child) => Container( width: 80 * _pulseAnim.value, height: 80 * _pulseAnim.value, decoration: BoxDecoration( shape: BoxShape.circle, color: AppColors.auraIndigo.withValues(alpha: 0.10), ), ), ), Container( width: 64, height: 64, decoration: BoxDecoration( gradient: AppColors.calmHealthGradient, borderRadius: BorderRadius.circular(20), boxShadow: AppColors.buttonShadow, ), child: const Icon( Icons.bluetooth_searching, size: 32, color: Colors.white, ), ), ], ), const SizedBox(height: 20), Text( _scanning ? '正在搜索蓝牙设备...' : '未发现设备', style: const TextStyle(fontSize: 16, color: AppColors.textHint), ), const SizedBox(height: 8), Text( _scanning ? '请确保血压计处于通信模式' : '点击下方按钮重新搜索', style: const TextStyle(fontSize: 13, color: Color(0xFFCCCCCC)), ), ], ), ); // ── 已连接视图 ── Widget _buildConnected() => Center( child: Padding( padding: const EdgeInsets.all(32), child: Column( mainAxisSize: MainAxisSize.min, children: [ AnimatedContainer( duration: const Duration(milliseconds: 500), width: 100, height: 100, decoration: BoxDecoration( gradient: _readingReceived ? AppColors.calmHealthGradient : AppColors.primaryGradient, borderRadius: BorderRadius.circular(32), boxShadow: AppColors.buttonShadow, ), child: Icon( _readingReceived ? Icons.check_rounded : Icons.bluetooth_connected, size: 48, color: _readingReceived ? Colors.white : Colors.white, ), ), const SizedBox(height: 24), Text( _readingReceived ? '测量完成' : '设备已连接', style: const TextStyle( fontSize: 22, fontWeight: FontWeight.w700, color: AppColors.textPrimary, ), ), const SizedBox(height: 8), if (_readingReceived) ...[ Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '$_systolic', style: const TextStyle( fontSize: 56, fontWeight: FontWeight.w800, color: AppColors.textPrimary, height: 1, ), ), Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( '/', style: TextStyle( fontSize: 24, color: AppColors.textHint.withValues(alpha: 0.5), ), ), ), Text( '$_diastolic', style: const TextStyle( fontSize: 56, fontWeight: FontWeight.w800, color: AppColors.textPrimary, height: 1, ), ), const SizedBox(width: 8), Padding( padding: const EdgeInsets.only(bottom: 12), child: Text( 'mmHg', style: TextStyle(fontSize: 15, color: AppColors.textHint), ), ), ], ), if (_pulse != null) Padding( padding: const EdgeInsets.only(top: 8), child: Container( padding: const EdgeInsets.symmetric( horizontal: 14, vertical: 6, ), decoration: BoxDecoration( color: AppColors.infoLight, borderRadius: BorderRadius.circular(20), ), child: Text( '脉搏 $_pulse bpm', style: const TextStyle( fontSize: 15, fontWeight: FontWeight.w500, color: AppColors.primary, ), ), ), ), const SizedBox(height: 16), const Text( '数据已自动同步', style: TextStyle(fontSize: 14, color: AppColors.textHint), ), const SizedBox(height: 8), const SizedBox( width: 24, height: 24, child: CircularProgressIndicator( strokeWidth: 2, color: AppColors.primary, ), ), ] else ...[ const Text( '请按下血压计的开始键进行测量', style: TextStyle(fontSize: 15, color: AppColors.textHint), ), const SizedBox(height: 32), AnimatedBuilder( animation: _pulseAnim, builder: (_, _) => Container( width: 48, height: 48, decoration: BoxDecoration( shape: BoxShape.circle, border: Border.all( color: AppColors.primary.withValues(alpha: 0.3), width: 2, ), ), child: Center( child: Transform.scale( scale: _pulseAnim.value, child: Container( width: 12, height: 12, decoration: const BoxDecoration( shape: BoxShape.circle, color: AppColors.primary, ), ), ), ), ), ), ], ], ), ), ); // ── 设备列表项 ── Widget _buildTile(ScanResult r) { final isConnecting = _connectingId == r.device.remoteId.toString(); final name = r.advertisementData.advName.isNotEmpty ? r.advertisementData.advName : (r.device.platformName.isNotEmpty ? r.device.platformName : '未知设备'); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(14), decoration: BoxDecoration( gradient: AppColors.surfaceGradient, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.borderLight), boxShadow: AppColors.cardShadowLight, ), child: Row( children: [ Container( width: 44, height: 44, decoration: BoxDecoration( gradient: AppColors.calmHealthGradient, borderRadius: BorderRadius.circular(12), ), child: const Icon(Icons.bluetooth, size: 22, color: Colors.white), ), const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( name, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w600, color: AppColors.textPrimary, ), ), const SizedBox(height: 2), Text( '${r.device.remoteId} · 信号: ${_rssiLabel(r.rssi)}', style: const TextStyle( fontSize: 12, color: AppColors.textHint, ), ), ], ), ), if (isConnecting) const SizedBox( width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2), ) else GestureDetector( onTap: () => _connect(r), child: Container( padding: const EdgeInsets.symmetric( horizontal: 20, vertical: 10, ), decoration: BoxDecoration( gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(12), boxShadow: AppColors.buttonShadow, ), child: const Text( '连接', style: TextStyle( fontSize: 14, fontWeight: FontWeight.w600, color: Colors.white, ), ), ), ), ], ), ); } String _rssiLabel(int r) { if (r > -55) return '强'; if (r > -70) return '中'; return '弱'; } }