import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../theme/app_colors.dart'; /// 设备类型 enum DeviceType { plush, badgeAi, badge } /// 模拟设备数据模型 class MockDevice { final String sn; final String name; final DeviceType type; final bool hasAI; const MockDevice({ required this.sn, required this.name, required this.type, required this.hasAI, }); String get iconPath { switch (type) { case DeviceType.plush: return 'assets/www/icons/pixel-capybara.svg'; case DeviceType.badgeAi: return 'assets/www/icons/pixel-badge-ai.svg'; case DeviceType.badge: return 'assets/www/icons/pixel-badge-basic.svg'; } } String get typeLabel { switch (type) { case DeviceType.plush: return '毛绒机芯'; case DeviceType.badgeAi: return 'AI电子吧唧'; case DeviceType.badge: return '普通电子吧唧'; } } } /// 蓝牙搜索页面 class BluetoothPage extends StatefulWidget { const BluetoothPage({super.key}); @override State createState() => _BluetoothPageState(); } class _BluetoothPageState extends State with TickerProviderStateMixin { // 状态 bool _isSearching = true; List _devices = []; int _currentIndex = 0; bool _isAnimating = false; // 动画控制器 late AnimationController _searchAnimController; late AnimationController _cardAnimController; late Animation _cardAnimation; // 模拟设备数据 static const List _mockDevices = [ MockDevice( sn: 'PLUSH_01', name: '卡皮巴拉-001', type: DeviceType.plush, hasAI: true, ), MockDevice( sn: 'BADGE_01', name: 'AI电子吧唧-001', type: DeviceType.badgeAi, hasAI: true, ), MockDevice( sn: 'BADGE_02', name: '电子吧唧-001', type: DeviceType.badge, hasAI: false, ), MockDevice( sn: 'PLUSH_02', name: '卡皮巴拉-002', type: DeviceType.plush, hasAI: true, ), ]; @override void initState() { super.initState(); // 搜索动画 (神秘盒子浮动) _searchAnimController = AnimationController( duration: const Duration(seconds: 2), vsync: this, )..repeat(reverse: true); // 卡片切换动画 _cardAnimController = AnimationController( duration: const Duration(milliseconds: 500), vsync: this, ); _cardAnimation = Tween(begin: 0, end: 1).animate( CurvedAnimation(parent: _cardAnimController, curve: Curves.easeOutCubic), ); // 模拟搜索延迟 _startSearch(); } @override void dispose() { _searchAnimController.dispose(); _cardAnimController.dispose(); super.dispose(); } /// 开始搜索 (模拟) Future _startSearch() async { // 请求蓝牙权限 await _requestPermissions(); // 模拟 2 秒搜索延迟 await Future.delayed(const Duration(seconds: 2)); if (mounted) { // 随机选择 1-4 个设备 final count = Random().nextInt(4) + 1; setState(() { _devices = _mockDevices.take(count).toList(); _isSearching = false; }); } } /// 请求蓝牙权限 Future _requestPermissions() async { // 检查蓝牙状态 await Permission.bluetooth.request(); await Permission.bluetoothScan.request(); await Permission.bluetoothConnect.request(); await Permission.location.request(); } /// 切换到下一个设备 void _swipeUp() { if (_isAnimating || _devices.length <= 1) return; _animateToIndex((_currentIndex + 1) % _devices.length, isUp: true); } /// 切换到上一个设备 void _swipeDown() { if (_isAnimating || _devices.length <= 1) return; _animateToIndex( (_currentIndex - 1 + _devices.length) % _devices.length, isUp: false, ); } /// 动画切换到指定索引 void _animateToIndex(int newIndex, {required bool isUp}) { _isAnimating = true; _cardAnimController.forward(from: 0).then((_) { if (mounted) { setState(() { _currentIndex = newIndex; _isAnimating = false; }); } }); } /// 连接设备 void _handleConnect() { if (_devices.isEmpty) return; final device = _devices[_currentIndex]; // TODO: 保存设备信息到本地存储 if (device.type == DeviceType.badge) { // 普通吧唧 -> 设备控制页 Navigator.of(context).pushReplacementNamed('/device-control'); } else { // 其他 -> WiFi 配网页 Navigator.of(context).pushReplacementNamed('/wifi-config'); } } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, body: Stack( children: [ // 渐变背景 _buildGradientBackground(), // 内容 SafeArea( child: Column( children: [ // Header _buildHeader(), // 设备数量提示 _buildCountLabel(), // 主内容区域 Expanded( child: _isSearching ? _buildSearchingState() : _buildDeviceCards(), ), // Footer _buildFooter(), ], ), ), ], ), ); } /// 渐变背景 Widget _buildGradientBackground() { final size = MediaQuery.of(context).size; return Positioned.fill( child: Stack( children: [ // Layer 1 - Pink Positioned( bottom: -size.width * 0.5, left: -size.width * 0.5, width: size.width * 2, height: size.width * 2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFFFC8DC).withOpacity(0.6), Colors.transparent, ], radius: 0.5, ), ), ), ), // Layer 2 - Cyan Positioned( top: -size.width * 0.5, right: -size.width * 0.5, width: size.width * 2, height: size.width * 2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFB4F0F0).withOpacity(0.5), Colors.transparent, ], radius: 0.5, ), ), ), ), // Layer 3 - Lavender Positioned( top: size.height * 0.2, left: size.width * 0.1, width: size.width * 1.2, height: size.width * 1.2, child: Container( decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFE6D2FA).withOpacity(0.45), Colors.transparent, ], radius: 0.5, ), ), ), ), ], ), ); } /// Header - HTML: padding 16px 20px (vertical horizontal) Widget _buildHeader() { return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16), child: Row( children: [ // 返回按钮 - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border GestureDetector( onTap: () => Navigator.of(context).pop(), child: Container( width: 40, height: 40, decoration: BoxDecoration( borderRadius: BorderRadius.circular(12), // Rounded square, not circle color: Colors.white.withOpacity(0.6), // No border per HTML ), child: const Icon( Icons.arrow_back_ios_new, size: 18, color: Color(0xFF4B5563), // Gray per HTML, not purple ), ), ), // 标题 Expanded( child: Text( '搜索设备', textAlign: TextAlign.center, style: TextStyle(fontFamily: 'Inter', fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ), ), ), // 占位 const SizedBox(width: 40), ], ), ); } /// 设备数量标签 Widget _buildCountLabel() { return AnimatedOpacity( duration: const Duration(milliseconds: 300), opacity: _isSearching ? 0 : 1, child: Container( padding: const EdgeInsets.symmetric(vertical: 20), child: _devices.isEmpty ? const SizedBox.shrink() : Text.rich( TextSpan( text: '找到 ', style: TextStyle(fontFamily: 'Inter', fontSize: 14, color: const Color(0xFF9CA3AF), ), children: [ TextSpan( text: '${_devices.length}', style: TextStyle(fontFamily: 'Inter', fontSize: 14, fontWeight: FontWeight.w600, color: const Color(0xFF8B5CF6), ), ), TextSpan( text: _devices.length > 1 ? ' 个设备 · 滑动切换' : ' 个设备', ), ], ), textAlign: TextAlign.center, ), ), ); } /// 搜索中状态 Widget _buildSearchingState() { return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ // 神秘盒子动画 AnimatedBuilder( animation: _searchAnimController, builder: (context, child) { return Transform.translate( offset: Offset(0, -15 * _searchAnimController.value), child: child, ); }, // HTML: mystery-box is transparent, icon is 120x120 with amber drop-shadow child: SvgPicture.asset( 'assets/www/icons/pixel-mystery-box.svg', width: 120, height: 120, placeholderBuilder: (_) => Text( '?', style: TextStyle(fontFamily: 'Inter', fontSize: 48, fontWeight: FontWeight.w700, color: const Color(0xFFF59E0B), // Amber color per HTML ), ), ), ), const SizedBox(height: 24), // 搜索状态文字 Text( '正在搜索附近设备', style: TextStyle(fontFamily: 'Inter', fontSize: 16, color: const Color(0xFF4B5563), ), ), ], ), ); } /// 设备卡片区域 Widget _buildDeviceCards() { if (_devices.isEmpty) { return Center( child: Text( '未找到设备', style: TextStyle(fontFamily: 'Inter', fontSize: 16, color: const Color(0xFF9CA3AF), ), ), ); } return Stack( children: [ // 卡片容器 (支持滑动) GestureDetector( onVerticalDragEnd: (details) { if (details.primaryVelocity == null) return; if (details.primaryVelocity! < -50) { _swipeUp(); } else if (details.primaryVelocity! > 50) { _swipeDown(); } }, child: Container( color: Colors.transparent, child: Center(child: _buildDeviceCard(_devices[_currentIndex])), ), ), // 右侧指示器 if (_devices.length > 1) Positioned( right: 20, top: 0, bottom: 0, child: Center( child: Column( mainAxisSize: MainAxisSize.min, children: List.generate( _devices.length, (index) => _buildDot(index == _currentIndex), ), ), ), ), ], ); } /// 单个设备卡片 Widget _buildDeviceCard(MockDevice device) { return Column( mainAxisSize: MainAxisSize.min, children: [ // 图标 + AI 徽章 Stack( clipBehavior: Clip.none, children: [ // 设备图标 - HTML: no background wrapper, icon is 120x120 SizedBox( width: 120, height: 120, child: _buildDeviceIcon(device), ), // AI 徽章 if (device.hasAI) Positioned( top: -4, right: -4, child: Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 4, ), decoration: BoxDecoration( gradient: const LinearGradient( colors: [Color(0xFF8B5CF6), Color(0xFF6366F1)], ), borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( color: const Color(0xFF8B5CF6).withOpacity(0.4), offset: const Offset(0, 4), blurRadius: 10, ), ], ), child: Text( 'AI', style: TextStyle(fontFamily: 'Inter', fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ), ], ), const SizedBox(height: 24), // 设备名称 Text( device.name, style: TextStyle(fontFamily: 'Inter', fontSize: 24, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ), ), const SizedBox(height: 4), // 设备类型 Text( device.typeLabel, style: TextStyle(fontFamily: 'Inter', fontSize: 15, color: const Color(0xFF6B7280), ), ), ], ); } /// 设备图标 - HTML: 120x120 per CSS .card-icon-img Widget _buildDeviceIcon(MockDevice device) { return SvgPicture.asset( device.iconPath, width: 120, height: 120, fit: BoxFit.contain, placeholderBuilder: (_) { IconData icon; switch (device.type) { case DeviceType.plush: icon = Icons.pets; case DeviceType.badgeAi: icon = Icons.smart_toy; case DeviceType.badge: icon = Icons.badge; } return Icon(icon, size: 80, color: const Color(0xFF8B5CF6)); }, ); } /// 指示器圆点 Widget _buildDot(bool isActive) { return AnimatedContainer( duration: const Duration(milliseconds: 300), curve: Curves.easeOutCubic, margin: const EdgeInsets.symmetric(vertical: 4), width: 6, height: isActive ? 18 : 6, decoration: BoxDecoration( color: isActive ? const Color(0xFF8B5CF6) : const Color(0xFF8B5CF6).withOpacity(0.2), borderRadius: BorderRadius.circular(isActive ? 3 : 3), ), ); } /// Footer - HTML: padding 20px 20px 60px, gap 16px, centered buttons Widget _buildFooter() { return Container( padding: EdgeInsets.fromLTRB( 20, // HTML: 20px sides 20, // HTML: 20px top 20, MediaQuery.of(context).padding.bottom + 60, // HTML: safe-area + 60px ), child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ // 取消按钮 - HTML: frosted glass with border GestureDetector( onTap: () => Navigator.of(context).pop(), child: ClipRRect( borderRadius: BorderRadius.circular(25), 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: Text( _isSearching ? '取消搜索' : '取消', style: const TextStyle( fontFamily: 'Inter', fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), ), ), ), // 连接按钮 (搜索完成后显示) if (!_isSearching && _devices.isNotEmpty) ...[ const SizedBox(width: 16), // HTML: gap 16px GestureDetector( onTap: _handleConnect, child: Container( padding: const EdgeInsets.symmetric(horizontal: 48, vertical: 16), decoration: BoxDecoration( gradient: AppColors.btnPrimaryGradient, borderRadius: BorderRadius.circular(29), // HTML: 29px // HTML: 5-layer glow effect boxShadow: [ BoxShadow( color: const Color(0xFF22D3EE).withOpacity(0.35), offset: Offset.zero, blurRadius: 15, ), BoxShadow( color: const Color(0xFF6366F1).withOpacity(0.25), offset: Offset.zero, blurRadius: 30, ), BoxShadow( color: const Color(0xFF6366F1).withOpacity(0.4), offset: const Offset(0, 6), blurRadius: 20, ), ], ), child: Stack( children: [ // Shine overlay Positioned.fill( child: ClipRRect( borderRadius: BorderRadius.circular(29), child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.white.withOpacity(0.15), Colors.transparent, ], stops: const [0.0, 0.5], ), ), ), ), ), Text( '连接设备', style: const TextStyle( fontFamily: 'Inter', fontSize: 17, // HTML: 17px fontWeight: FontWeight.w600, color: Colors.white, ), ), ], ), ), ), ], ], ), ); } }