import 'dart:async'; 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 '../../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 { final _results = []; bool _scanning = false; String? _connectingId; StreamSubscription? _scanSub; @override void initState() { super.initState(); // 一次性订阅,持续整个页面生命周期 _scanSub = FlutterBluePlus.scanResults.listen((list) { if (!mounted) return; setState(() { _results.clear(); _results.addAll(list); }); }); _start(); } @override void dispose() { _scanSub?.cancel(); FlutterBluePlus.stopScan(); super.dispose(); } Future _start() async { setState(() => _scanning = true); await [Permission.bluetoothScan, Permission.bluetoothConnect].request(); try { await FlutterBluePlus.turnOn(); } catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); } try { await FlutterBluePlus.stopScan(); } catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); } 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(); 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); ble.readings.listen((reading) async { try { final api = ref.read(apiClientProvider); await api.post('/api/health-records', data: reading.toHealthRecord()); await ref.read(omronDeviceProvider.notifier).onReading(reading); } catch (e) { debugPrint('[BLE Scan] 操作失败: $e'); } }); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text('已连接 $name'), backgroundColor: AppColors.success), ); Navigator.pop(context); } } catch (_) { if (mounted) { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text('连接失败,请重试'), backgroundColor: AppColors.error), ); _start(); } } finally { if (mounted) setState(() => _connectingId = null); } } // ═══════════════════ UI ═══════════════════ @override Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.background, appBar: AppBar(backgroundColor: AppColors.cardBackground, title: const Text('添加血压计')), body: Column(children: [ Container( width: double.infinity, padding: const EdgeInsets.all(16), color: AppColors.iconBg, child: Row(children: [ if (_scanning) ...[ const SizedBox(width: 16, height: 16, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF5B8DEF))), const SizedBox(width: 12), ], Text(_scanning ? '正在扫描...' : '发现 ${_results.length} 台设备', style: const TextStyle(fontSize: 15, color: Color(0xFF666666))), ]), ), if (!_scanning && _results.isEmpty) _buildEmpty(), Expanded( child: ListView.builder( padding: const EdgeInsets.symmetric(vertical: 8), 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(0xFF5B8DEF)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), padding: const EdgeInsets.symmetric(vertical: 14), ), )), ), ]), ); } Widget _buildTile(ScanResult r) { final isBP = OmronBleService.isBpDevice(r); final name = r.advertisementData.localName.isNotEmpty ? r.advertisementData.localName : (r.device.platformName.isNotEmpty ? r.device.platformName : '未知设备'); final isConnecting = _connectingId == r.device.remoteId.toString(); return Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: AppColors.cardBackground, borderRadius: BorderRadius.circular(16), boxShadow: AppColors.cardShadow, border: isBP ? Border.all(color: AppColors.primary, width: 1.5) : null, ), child: Row(children: [ Container( width: 44, height: 44, decoration: BoxDecoration(color: isBP ? AppColors.iconBg : AppColors.cardInner, borderRadius: BorderRadius.circular(12)), child: Icon(Icons.bluetooth, size: 24, color: isBP ? AppColors.primary : AppColors.textHint), ), const SizedBox(width: 12), Expanded( child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ Row(children: [ Flexible(child: Text(name, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.w600, color: AppColors.textPrimary))), if (isBP) ...[ const SizedBox(width: 6), Container(padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), decoration: BoxDecoration(color: AppColors.primary, borderRadius: BorderRadius.circular(4)), child: const Text('血压计', style: TextStyle(fontSize: 11, color: Colors.white))), ], ]), const SizedBox(height: 2), Text('信号: ${_rssiLabel(r.rssi)} · ${r.device.remoteId}', style: const TextStyle(fontSize: 13, color: AppColors.textHint)), ]), ), if (isConnecting) const SizedBox(width: 24, height: 24, child: CircularProgressIndicator(strokeWidth: 2, color: Color(0xFF5B8DEF))) else GestureDetector( onTap: () => _connect(r), child: Container(padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), decoration: BoxDecoration(gradient: AppColors.primaryGradient, borderRadius: BorderRadius.circular(12)), child: const Text('连接', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.white))), ), ]), ); } Widget _buildEmpty() => Padding( padding: const EdgeInsets.all(32), child: Column(children: [ Icon(Icons.bluetooth_disabled, size: 64, color: AppColors.textHint), const SizedBox(height: 16), const Text('未发现设备', style: TextStyle(fontSize: 18, fontWeight: FontWeight.w500, color: AppColors.textPrimary)), const SizedBox(height: 24), _tip('1', '确保血压计已装电池'), _tip('2', '长按血压计蓝牙键 3 秒直到图标闪烁'), _tip('3', '手机靠近血压计(1米内)'), _tip('4', '打开手机蓝牙'), ]), ); Widget _tip(String num, String text) => Padding( padding: const EdgeInsets.only(bottom: 8), child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ Container(width: 20, height: 20, alignment: Alignment.center, decoration: BoxDecoration(color: AppColors.primary, borderRadius: BorderRadius.circular(10)), child: Text(num, style: const TextStyle(color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600))), const SizedBox(width: 10), Expanded(child: Text(text, style: TextStyle(fontSize: 14, color: AppColors.textSecondary))), ]), ); String _rssiLabel(int rssi) { if (rssi > -55) return '强'; if (rssi > -70) return '中'; return '弱'; } }