rtc_prd/airhub_app/lib/pages/wifi_config_page.dart
2026-02-10 18:21:21 +08:00

776 lines
22 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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<String, dynamic>? extra;
const WifiConfigPage({super.key, this.extra});
@override
ConsumerState<WifiConfigPage> createState() => _WifiConfigPageState();
}
class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
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<String, dynamic> _deviceInfo = {};
// BLE Provisioning
BleProvisioningService? _provService;
List<ScannedWifi> _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<void> _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<void> _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<void> _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<double>(
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,
),
],
),
);
}
}