import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../core/services/ble_provisioning_service.dart'; import '../features/device/presentation/controllers/device_controller.dart'; import '../widgets/animated_gradient_background.dart'; import '../widgets/gradient_button.dart'; class WifiConfigPage extends ConsumerStatefulWidget { final Map? extra; const WifiConfigPage({super.key, this.extra}); @override ConsumerState createState() => _WifiConfigPageState(); } class _WifiConfigPageState extends ConsumerState with TickerProviderStateMixin { int _currentStep = 1; String _selectedWifiSsid = ''; final TextEditingController _passwordController = TextEditingController(); bool _obscurePassword = true; // Progress State double _progress = 0.0; String _progressText = '正在连接WiFi...'; bool _connectFailed = false; // Device Info Map _deviceInfo = {}; // BLE Provisioning BleProvisioningService? _provService; List _wifiList = []; bool _isScanning = false; // Subscriptions StreamSubscription? _wifiListSub; StreamSubscription? _wifiStatusSub; StreamSubscription? _disconnectSub; @override void initState() { super.initState(); _deviceInfo = widget.extra ?? {}; _provService = _deviceInfo['provService'] as BleProvisioningService?; if (_provService != null) { _setupBleListeners(); // 自动开始 WiFi 扫描 _requestWifiScan(); } } @override void dispose() { _wifiListSub?.cancel(); _wifiStatusSub?.cancel(); _disconnectSub?.cancel(); _provService?.dispose(); _passwordController.dispose(); super.dispose(); } void _setupBleListeners() { // 监听 WiFi 列表 _wifiListSub = _provService!.onWifiList.listen((list) { if (!mounted) return; debugPrint('[WiFi Config] 收到 WiFi 列表: ${list.length} 个'); setState(() { _wifiList = list; _isScanning = false; }); }); // 监听 WiFi 连接状态 _wifiStatusSub = _provService!.onWifiStatus.listen((result) { if (!mounted) return; debugPrint('[WiFi Config] WiFi 状态: success=${result.success}, reason=${result.reasonCode}'); if (result.success) { setState(() { _progress = 1.0; _progressText = '配网成功!'; _currentStep = 4; }); } else { setState(() { _connectFailed = true; _progressText = '连接失败 (错误码: ${result.reasonCode})'; }); } }); // 监听 BLE 断开 _disconnectSub = _provService!.onDisconnect.listen((_) { if (!mounted) return; debugPrint('[WiFi Config] BLE 连接已断开'); // 如果在配网中断开,可能是成功后设备重启 if (_currentStep == 3 && !_connectFailed) { setState(() { _progress = 1.0; _progressText = '设备正在重启...'; _currentStep = 4; }); } }); } Future _requestWifiScan() async { if (_provService == null) return; setState(() => _isScanning = true); await _provService!.requestWifiScan(); // WiFi 列表会通过 onWifiList stream 回调 // 设置超时:10 秒后如果还没收到列表,停止加载 Future.delayed(const Duration(seconds: 10), () { if (mounted && _isScanning) { setState(() => _isScanning = false); } }); } Future _handleNext() async { if (_currentStep == 1 && _selectedWifiSsid.isEmpty) return; if (_currentStep == 2 && _passwordController.text.isEmpty) return; if (_currentStep == 4) { final sn = _deviceInfo['sn'] as String? ?? ''; if (sn.isNotEmpty) { debugPrint('[WiFi Config] Binding device sn=$sn'); await ref.read(deviceControllerProvider.notifier).bindDevice(sn); } if (!mounted) return; context.go('/device-control'); return; } setState(() { _currentStep++; }); if (_currentStep == 3) { _startConnecting(); } } void _handleBack() { if (_currentStep > 1) { setState(() { _currentStep--; if (_currentStep == 1) { _connectFailed = false; _progress = 0.0; } }); } else { _provService?.disconnect(); context.go('/bluetooth'); } } Future _startConnecting() async { setState(() { _progress = 0.1; _progressText = '正在发送WiFi信息...'; _connectFailed = false; }); if (_provService != null && _provService!.isConnected) { // 通过 BLE 发送 WiFi 凭证 setState(() { _progress = 0.3; _progressText = '正在发送WiFi凭证...'; }); await _provService!.sendWifiCredentials( _selectedWifiSsid, _passwordController.text, ); setState(() { _progress = 0.5; _progressText = '等待设备连接WiFi...'; }); // WiFi 状态会通过 onWifiStatus stream 回调 // 设置超时:60 秒后如果还没收到结果 Future.delayed(const Duration(seconds: 60), () { if (mounted && _currentStep == 3 && !_connectFailed) { setState(() { _connectFailed = true; _progressText = '连接超时,请重试'; }); } }); } else { // 无 BLE 连接(模拟模式),使用 mock 流程 _startMockConnecting(); } } void _startMockConnecting() { const steps = [ {'progress': 0.3, 'text': '正在连接WiFi...'}, {'progress': 0.6, 'text': '正在验证密码...'}, {'progress': 0.9, 'text': '正在同步设备...'}, {'progress': 1.0, 'text': '完成!'}, ]; int stepIndex = 0; Timer.periodic(const Duration(milliseconds: 800), (timer) { if (stepIndex < steps.length) { if (mounted) { setState(() { _progress = steps[stepIndex]['progress'] as double; _progressText = steps[stepIndex]['text'] as String; }); } stepIndex++; } else { timer.cancel(); if (mounted) { setState(() => _currentStep = 4); } } }); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, resizeToAvoidBottomInset: true, body: Stack( children: [ const AnimatedGradientBackground(), Positioned.fill( child: SafeArea( child: Column( children: [ _buildHeader(), Expanded( child: SingleChildScrollView( padding: const EdgeInsets.symmetric(horizontal: 32), child: Column( children: [ _buildStepIndicator(), const SizedBox(height: 32), _buildCurrentStepContent(), ], ), ), ), _buildFooter(), ], ), ), ), ], ), ); } Widget _buildHeader() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( children: [ GestureDetector( onTap: _handleBack, child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), color: Colors.white.withOpacity(0.6), ), child: const Icon( Icons.arrow_back_ios_new, size: 18, color: Color(0xFF4B5563), ), ), ), Expanded( child: Text( 'WiFi配网', textAlign: TextAlign.center, style: GoogleFonts.outfit( fontSize: 17, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ), ), ), const SizedBox(width: 48), ], ), ); } Widget _buildStepIndicator() { return Row( mainAxisAlignment: MainAxisAlignment.center, children: List.generate(4, (index) { int step = index + 1; bool isActive = step == _currentStep; bool isCompleted = step < _currentStep; return AnimatedContainer( duration: const Duration(milliseconds: 300), width: isActive ? 24 : 8, height: 8, margin: const EdgeInsets.symmetric(horizontal: 4), decoration: BoxDecoration( color: isCompleted ? const Color(0xFF22C55E) : isActive ? const Color(0xFF8B5CF6) : const Color(0xFF8B5CF6).withOpacity(0.3), borderRadius: BorderRadius.circular(4), ), ); }), ); } Widget _buildCurrentStepContent() { switch (_currentStep) { case 1: return _buildStep1(); case 2: return _buildStep2(); case 3: return _buildStep3(); case 4: return _buildStep4(); default: return const SizedBox.shrink(); } } // Step 1: 选择 WiFi 网络 Widget _buildStep1() { return Column( children: [ Container( margin: const EdgeInsets.only(bottom: 24), child: const Icon(Icons.wifi, size: 80, color: Color(0xFF8B5CF6)), ), Text( '选择WiFi网络', style: GoogleFonts.outfit( fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 8), const Text( '设备需要连接WiFi以使用AI功能', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), const SizedBox(height: 24), if (_isScanning) const Padding( padding: EdgeInsets.symmetric(vertical: 40), child: Column( children: [ CircularProgressIndicator(color: Color(0xFF8B5CF6)), SizedBox(height: 16), Text( '正在通过设备扫描WiFi...', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), ], ), ) else if (_wifiList.isEmpty) Column( children: [ const Padding( padding: EdgeInsets.symmetric(vertical: 40), child: Text( '未扫描到WiFi网络', style: TextStyle(fontSize: 14, color: Color(0xFF9CA3AF)), ), ), GestureDetector( onTap: _requestWifiScan, child: const Text( '重新扫描', style: TextStyle( fontSize: 14, color: Color(0xFF8B5CF6), decoration: TextDecoration.underline, ), ), ), ], ) else Column( children: [ ..._wifiList.map((wifi) => _buildWifiItem(wifi)), const SizedBox(height: 8), GestureDetector( onTap: _requestWifiScan, child: const Text( '重新扫描', style: TextStyle( fontSize: 14, color: Color(0xFF8B5CF6), decoration: TextDecoration.underline, ), ), ), ], ), ], ); } Widget _buildWifiItem(ScannedWifi wifi) { bool isSelected = _selectedWifiSsid == wifi.ssid; return GestureDetector( onTap: () { setState(() => _selectedWifiSsid = wifi.ssid); }, child: Container( padding: const EdgeInsets.all(16), margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(16), border: Border.all( color: isSelected ? const Color(0xFF8B5CF6) : Colors.white.withOpacity(0.5), width: isSelected ? 2 : 1, ), boxShadow: isSelected ? [ BoxShadow( color: const Color(0xFF8B5CF6).withOpacity(0.2), blurRadius: 0, spreadRadius: 2, ), ] : null, ), child: Row( children: [ Expanded( child: Text( wifi.ssid, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF1F2937), ), ), ), // WiFi 信号图标 Icon( wifi.level >= 3 ? Icons.wifi : wifi.level == 2 ? Icons.wifi_2_bar : Icons.wifi_1_bar, size: 24, color: const Color(0xFF6B7280), ), ], ), ), ); } // Step 2: 输入密码 Widget _buildStep2() { return Column( children: [ Container( margin: const EdgeInsets.only(bottom: 24), child: const Icon( Icons.lock_outline, size: 80, color: Color(0xFF8B5CF6), ), ), Text( _selectedWifiSsid, style: GoogleFonts.outfit( fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 8), const Text( '请输入WiFi密码', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), const SizedBox(height: 24), TextField( controller: _passwordController, obscureText: _obscurePassword, onChanged: (_) => setState(() {}), decoration: InputDecoration( hintText: '输入密码', filled: true, fillColor: Colors.white.withOpacity(0.8), border: OutlineInputBorder( borderRadius: BorderRadius.circular(16), borderSide: BorderSide.none, ), contentPadding: const EdgeInsets.all(20), suffixIcon: Padding( padding: const EdgeInsets.only(right: 8), child: IconButton( icon: Icon( _obscurePassword ? Icons.visibility_off : Icons.visibility, color: const Color(0xFF9CA3AF), size: 22, ), onPressed: () { setState(() => _obscurePassword = !_obscurePassword); }, ), ), ), style: const TextStyle(fontSize: 16), ), ], ); } // Step 3: 正在连接 Widget _buildStep3() { return Column( children: [ SizedBox( height: 120, child: Center( child: TweenAnimationBuilder( tween: Tween(begin: 0, end: 1), duration: const Duration(seconds: 1), builder: (context, value, child) { return Icon( Icons.wifi_tethering, size: 80 + (value * 10), color: const Color(0xFF8B5CF6).withOpacity(1 - value * 0.5), ); }, onEnd: () {}, ), ), ), Text( '正在配网...', style: GoogleFonts.outfit( fontSize: 20, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 32), ClipRRect( borderRadius: BorderRadius.circular(3), child: SizedBox( height: 6, child: LinearProgressIndicator( value: _progress, backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.2), valueColor: AlwaysStoppedAnimation( _connectFailed ? const Color(0xFFEF4444) : const Color(0xFF8B5CF6), ), ), ), ), const SizedBox(height: 16), Text( _progressText, style: TextStyle( fontSize: 14, color: _connectFailed ? const Color(0xFFEF4444) : const Color(0xFF6B7280), ), ), if (_connectFailed) ...[ const SizedBox(height: 24), GestureDetector( onTap: () { setState(() { _currentStep = 1; _connectFailed = false; _progress = 0.0; }); }, child: const Text( '返回重新选择', style: TextStyle( fontSize: 14, color: Color(0xFF8B5CF6), decoration: TextDecoration.underline, ), ), ), ], ], ); } String _getDeviceIconPath() { final type = _deviceInfo['type'] as String? ?? 'plush'; switch (type) { case 'plush_core': case 'plush': return 'assets/www/icons/pixel-capybara.svg'; case 'badgeAi': return 'assets/www/icons/pixel-badge-ai.svg'; case 'badge': return 'assets/www/icons/pixel-badge-basic.svg'; default: return 'assets/www/icons/pixel-capybara.svg'; } } // Step 4: 配网成功 Widget _buildStep4() { return Column( children: [ const SizedBox(height: 80), Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ SizedBox( width: 120, height: 120, child: SvgPicture.asset( _getDeviceIconPath(), width: 120, height: 120, placeholderBuilder: (_) => const Icon( Icons.smart_toy, size: 80, color: Color(0xFF8B5CF6), ), ), ), Positioned( bottom: -5, right: -5, child: Container( width: 32, height: 32, decoration: BoxDecoration( color: const Color(0xFF22C55E), shape: BoxShape.circle, border: Border.all(color: Colors.white, width: 3), boxShadow: [ BoxShadow( color: const Color(0xFF22C55E).withOpacity(0.3), blurRadius: 10, offset: const Offset(0, 4), ), ], ), child: const Icon(Icons.check, color: Colors.white, size: 18), ), ), ], ), const SizedBox(height: 24), Text( '配网成功!', style: GoogleFonts.outfit( fontSize: 24, fontWeight: FontWeight.bold, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 8), const Text( '设备已成功连接到网络', style: TextStyle(fontSize: 14, color: Color(0xFF6B7280)), ), ], ); } Widget _buildFooter() { bool showNext = false; String nextText = '下一步'; if (_currentStep == 1 && _selectedWifiSsid.isNotEmpty) showNext = true; if (_currentStep == 2 && _passwordController.text.isNotEmpty) { showNext = true; nextText = '连接'; } if (_currentStep == 4) { showNext = true; nextText = '进入设备'; } if (!showNext && _currentStep != 3) { return Padding( padding: const EdgeInsets.all(32), child: TextButton( onPressed: () { _provService?.disconnect(); context.go('/bluetooth'); }, child: const Text( '取消', style: TextStyle(color: Color(0xFF6B7280)), ), ), ); } if (_currentStep == 3) { return const SizedBox(height: 100); } return Container( padding: EdgeInsets.fromLTRB( 32, 20, 32, MediaQuery.of(context).padding.bottom + 40, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ if (_currentStep < 4) GestureDetector( onTap: _handleBack, child: Container( padding: const EdgeInsets.symmetric( horizontal: 32, vertical: 14, ), decoration: BoxDecoration( color: Colors.white.withOpacity(0.8), borderRadius: BorderRadius.circular(25), border: Border.all(color: const Color(0xFFE5E7EB)), ), child: const Text( '取消', style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), ), ), if (_currentStep < 4) const SizedBox(width: 16), GradientButton( text: nextText, onPressed: _handleNext, height: 56, width: _currentStep == 4 ? 200 : 160, ), ], ), ); } }