import 'dart:async'; import 'dart:math'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../widgets/animated_gradient_background.dart'; import '../theme/app_colors.dart'; import '../widgets/gradient_button.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; // 动画控制器 late AnimationController _searchAnimController; // 滚轮控制器 late FixedExtentScrollController _wheelController; // 模拟设备数据 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(); // 搜索动画 (神秘盒子浮动) - 800ms 周期,2s 搜索内可完成多次弹跳 _searchAnimController = AnimationController( duration: const Duration(milliseconds: 800), vsync: this, )..repeat(reverse: true); // 滚轮控制器 _wheelController = FixedExtentScrollController(initialItem: _currentIndex); // 模拟搜索延迟 _startSearch(); } @override void dispose() { _searchAnimController.dispose(); _wheelController.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; }); } } /// 请求蓝牙权限(模拟器上可能失败,不影响 mock 搜索) Future _requestPermissions() async { try { await Permission.bluetooth.request(); await Permission.bluetoothScan.request(); await Permission.bluetoothConnect.request(); await Permission.location.request(); } catch (_) { // 模拟器上蓝牙不可用,忽略权限错误,继续用 mock 数据 } } /// 连接设备 void _handleConnect() { if (_devices.isEmpty) return; final device = _devices[_currentIndex]; // TODO: 保存设备信息到本地存储 if (device.type == DeviceType.badge) { // 普通吧唧 -> 设备控制页 context.go('/device-control'); } else { // 其他 -> WiFi 配网页 context.go('/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() { return const AnimatedGradientBackground(); } /// 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: () => context.go('/home'), 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: GoogleFonts.outfit( fontSize: 17, 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( fontSize: 14, color: const Color(0xFF9CA3AF), ), children: [ TextSpan( text: '${_devices.length}', style: TextStyle( 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: GoogleFonts.outfit( fontSize: 48, fontWeight: FontWeight.w700, color: const Color(0xFFF59E0B), // Amber color per HTML ), ), ), ), const SizedBox(height: 24), // 搜索状态文字 Text( '正在搜索附近设备', style: TextStyle( fontSize: 16, color: const Color(0xFF4B5563), ), ), ], ), ); } /// 设备卡片区域 - 滚轮选择器风格 Widget _buildDeviceCards() { if (_devices.isEmpty) { return Center( child: Text( '未找到设备', style: TextStyle( fontSize: 16, color: const Color(0xFF9CA3AF), ), ), ); } final itemExtent = 180.0; return SizedBox( height: MediaQuery.of(context).size.height * 0.46, child: Stack( children: [ // 滚轮视图 + ShaderMask 实现上下边缘渐隐 ShaderMask( shaderCallback: (Rect rect) { return const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.transparent, Colors.black, Colors.black, Colors.transparent, ], stops: [0.05, 0.38, 0.62, 0.95], ).createShader(rect); }, blendMode: BlendMode.dstIn, child: ListWheelScrollView.useDelegate( controller: _wheelController, itemExtent: itemExtent, diameterRatio: 1.8, perspective: 0.006, physics: const FixedExtentScrollPhysics(), onSelectedItemChanged: (index) { setState(() => _currentIndex = index); }, childDelegate: ListWheelChildBuilderDelegate( childCount: _devices.length, builder: (context, index) { return AnimatedBuilder( animation: _wheelController, builder: (context, child) { final offset = _wheelController.hasClients ? _wheelController.offset : _currentIndex * itemExtent; // 当前 item 中心与视口中心的距离 final distFromCenter = (index * itemExtent - offset).abs(); // 归一化到 0~1 (0=居中, 1=远离) final t = (distFromCenter / itemExtent).clamp(0.0, 1.5); // 居中=1.0, 远离=0.65 final scale = 1.0 - (t * 0.25); return Transform.scale( scale: scale.clamp(0.65, 1.0), child: child, ); }, child: Center(child: _buildDeviceCard(_devices[index])), ); }, ), ), ), // 右侧圆点指示器 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( fontSize: 11, fontWeight: FontWeight.w700, color: Colors.white, ), ), ), ), ], ), const SizedBox(height: 10), // 设备名称 Text( device.name, style: GoogleFonts.outfit( fontSize: 18, fontWeight: FontWeight.w600, color: const Color(0xFF1F2937), ), ), ], ); } /// 设备图标 - 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: () => context.go('/home'), 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( fontSize: 16, fontWeight: FontWeight.w500, color: Color(0xFF6B7280), ), ), ), ), ), // 连接按钮 (搜索完成后显示) if (!_isSearching && _devices.isNotEmpty) ...[ const SizedBox(width: 16), // HTML: gap 16px GradientButton( text: '连接设备', width: 180, height: 52, onPressed: _handleConnect, ), ], ], ), ); } }