import 'dart:async'; import 'dart:io' show Platform; import 'package:flutter/material.dart'; import 'package:flutter/foundation.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/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.localName.isNotEmpty ? r.advertisementData.localName : (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 { await ref.read(apiClientProvider).post('/api/health-records', data: reading.toHealthRecord()); 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 Scaffold( backgroundColor: Colors.white, appBar: AppBar( backgroundColor: Colors.white, 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: Color(0xFFE5E7EB)), 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.primary.withOpacity(0.08), ), ), ), Container( width: 64, height: 64, decoration: BoxDecoration( color: const Color(0xFFF0F0FF), borderRadius: BorderRadius.circular(20), ), child: const Icon(Icons.bluetooth_searching, size: 32, color: AppColors.primary), ), ]), 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( color: _readingReceived ? const Color(0xFFD1FAE5) : const Color(0xFFF0F0FF), borderRadius: BorderRadius.circular(32), ), child: Icon( _readingReceived ? Icons.check_rounded : Icons.bluetooth_connected, size: 48, color: _readingReceived ? const Color(0xFF10B981) : AppColors.primary, ), ), 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.withOpacity(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: const Color(0xFFEFF6FF), 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.withOpacity(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.localName.isNotEmpty ? r.advertisementData.localName : (r.device.platformName.isNotEmpty ? r.device.platformName : '未知设备'); return Container( margin: const EdgeInsets.only(bottom: 8), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: const Color(0xFFEEEEEE)), ), child: Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration(color: const Color(0xFFF0F0FF), borderRadius: BorderRadius.circular(12)), child: const Icon(Icons.bluetooth, size: 22, color: AppColors.primary), ), 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(color: AppColors.primary, borderRadius: BorderRadius.circular(12)), 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 '弱'; } }