rtc_prd/airhub_app/lib/pages/wifi_config_page.dart
seaislee1209 f9666d4aa3 feat: UI规范化 + 故事吸入动画 + 音乐页面优化
- 全局字体统一(Outfit/DM Sans), 头部/按钮/Toast规范化
- 故事详情页: Genie Suck吸入动画(标题+卡片一起缩小模糊消失)
- 书架页: bookPop弹出+粒子效果(三段式动画完整链路)
- 音乐页面: 心情卡片emoji换Material图标+彩色圆块横排布局
- 音乐页面: 进度条胶囊宽度对齐, 播放按钮位置修复, 间距均匀化
- 音乐播放: 接入just_audio, 支持播放暂停进度拖拽自动切歌
- 新增: iOS风格毛玻璃Toast, 渐变背景组件, 通知页面
- 阶段总结文档更新

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 19:34:53 +08:00

628 lines
17 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:flutter_svg/flutter_svg.dart';
import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart';
class WifiConfigPage extends StatefulWidget {
const WifiConfigPage({super.key});
@override
State<WifiConfigPage> createState() => _WifiConfigPageState();
}
class _WifiConfigPageState extends State<WifiConfigPage>
with TickerProviderStateMixin {
int _currentStep = 1;
String _selectedWifiSsid = '';
final TextEditingController _passwordController = TextEditingController();
bool _obscurePassword = true;
// Progress State
double _progress = 0.0;
String _progressText = '正在连接WiFi...';
// Device Info (Mock or from Route Args)
// We'll try to get it from arguments, default to a fallback
Map<String, dynamic> _deviceInfo = {};
// Mock WiFi List
final List<Map<String, dynamic>> _wifiList = [
{'ssid': 'Home_5G', 'level': 4},
{'ssid': 'Office_WiFi', 'level': 3},
{'ssid': 'Guest_Network', 'level': 2},
];
@override
void didChangeDependencies() {
super.didChangeDependencies();
// Retrieve device info from arguments
final args = ModalRoute.of(context)?.settings.arguments;
if (args is Map<String, dynamic>) {
_deviceInfo = args;
}
}
void _handleNext() {
if (_currentStep == 1 && _selectedWifiSsid.isEmpty) return;
if (_currentStep == 2 && _passwordController.text.isEmpty) return;
if (_currentStep == 4) {
// Navigate to Device Control
// Use pushNamedAndRemoveUntil to remove Bluetooth and WiFi pages from stack
// but keep Home page so back button goes to Home
context.go('/device-control');
return;
}
setState(() {
_currentStep++;
});
if (_currentStep == 3) {
_startConnecting();
}
}
void _handleBack() {
if (_currentStep > 1) {
setState(() {
_currentStep--;
});
} else {
context.go('/home');
}
}
void _startConnecting() {
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: [
// Background
_buildGradientBackground(),
Positioned.fill(
child: SafeArea(
child: Column(
children: [
// Header
_buildHeader(),
// Content
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 32),
child: Column(
children: [
// Steps Indicator
_buildStepIndicator(),
const SizedBox(height: 32),
// Dynamic Step Content
_buildCurrentStepContent(),
],
),
),
),
// Footer
_buildFooter(),
],
),
),
),
],
),
);
}
// Common Gradient Background
Widget _buildGradientBackground() {
return const AnimatedGradientBackground();
}
Widget _buildHeader() {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
children: [
// Back button - HTML: bg rgba(255,255,255,0.6), border-radius: 12px, color #4B5563
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), // Gray per HTML, not purple
),
),
),
Expanded(
child: Text(
'WiFi配网',
textAlign: TextAlign.center,
style: GoogleFonts.outfit(
fontSize: 17,
fontWeight: FontWeight.w600,
color: const Color(0xFF1F2937),
),
),
),
const SizedBox(width: 48), // Balance back button
],
),
);
}
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) // Green for completed
: isActive
? const Color(0xFF8B5CF6) // Purple for active
: const Color(0xFF8B5CF6).withOpacity(0.3), // Faded purple
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: Select Network
Widget _buildStep1() {
return Column(
children: [
// Icon
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),
// List
Column(
children: _wifiList.map((wifi) => _buildWifiItem(wifi)).toList(),
),
],
);
}
Widget _buildWifiItem(Map<String, dynamic> 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),
),
),
),
// HTML uses per-level SVG icons: wifi-1.svg to wifi-4.svg
Opacity(
opacity: 0.8,
child: SvgPicture.asset(
'assets/www/icons/wifi-${wifi['level']}.svg',
width: 24,
height: 24,
colorFilter: const ColorFilter.mode(
Color(0xFF6B7280),
BlendMode.srcIn,
),
),
),
],
),
),
);
}
// Step 2: Enter Password
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),
Text(
'请输入WiFi密码',
style: TextStyle(
fontSize: 14,
color: const 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: Connecting
Widget _buildStep3() {
return Column(
children: [
// Animation placeholder (using Icon for now, can be upgraded to Wave animation)
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:
() {}, // Repeat logic usually handled by AnimationController
),
),
),
Text(
'正在配网...',
style: GoogleFonts.outfit(
fontSize: 20,
fontWeight: FontWeight.bold,
color: const Color(0xFF1F2937),
),
),
const SizedBox(height: 32),
// Progress Bar
ClipRRect(
borderRadius: BorderRadius.circular(3),
child: SizedBox(
height: 6,
child: LinearProgressIndicator(
value: _progress,
backgroundColor: const Color(0xFF8B5CF6).withOpacity(0.2),
valueColor: const AlwaysStoppedAnimation(Color(0xFF8B5CF6)),
),
),
),
const SizedBox(height: 16),
Text(
_progressText,
style: TextStyle(
fontSize: 14,
color: const Color(0xFF6B7280),
),
),
],
);
}
// Get device icon path based on device type
String _getDeviceIconPath() {
final type = _deviceInfo['type'] as String? ?? 'plush';
switch (type) {
case 'plush_core':
case 'plush':
return 'assets/www/icons/pixel-capybara.svg';
case 'badge_ai':
return 'assets/www/icons/pixel-badge-ai.svg';
case 'badge_basic':
case 'badge':
return 'assets/www/icons/pixel-badge-basic.svg';
default:
return 'assets/www/icons/pixel-capybara.svg';
}
}
// Step 4: Result (Success) - centered vertically
Widget _buildStep4() {
return Column(
children: [
const SizedBox(height: 80),
// Success Icon Stack - HTML: no white background
Stack(
clipBehavior: Clip.none,
alignment: Alignment.center,
children: [
// Device icon container - 120x120 per HTML
SizedBox(
width: 120,
height: 120,
child: SvgPicture.asset(
_getDeviceIconPath(),
width: 120,
height: 120,
placeholderBuilder: (_) => const Icon(
Icons.smart_toy,
size: 80,
color: Color(0xFF8B5CF6),
),
),
),
// Check badge
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),
Text(
'设备已成功连接到网络',
style: TextStyle(
fontSize: 14,
color: const 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) {
// Show cancel only?
return Padding(
padding: const EdgeInsets.all(32),
child: TextButton(
onPressed: () => context.go('/bluetooth'),
child: Text(
'取消',
style: TextStyle(
color: const Color(0xFF6B7280),
),
),
),
);
}
if (_currentStep == 3)
return const SizedBox(height: 100); // Hide buttons during connection
return Container(
padding: EdgeInsets.fromLTRB(
32,
20,
32,
MediaQuery.of(context).padding.bottom + 40,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Cancel button - frosted glass style
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),
// Constrained button (not full-width)
GradientButton(
text: nextText,
onPressed: _handleNext,
height: 56,
width: _currentStep == 4 ? 200 : 160,
),
],
),
);
}
}