rtc_prd/airhub_app/lib/pages/bluetooth_page.dart
2026-02-06 16:03:32 +08:00

690 lines
20 KiB
Dart

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<BluetoothPage> createState() => _BluetoothPageState();
}
class _BluetoothPageState extends State<BluetoothPage>
with TickerProviderStateMixin {
// 状态
bool _isSearching = true;
List<MockDevice> _devices = [];
int _currentIndex = 0;
bool _isAnimating = false;
// 动画控制器
late AnimationController _searchAnimController;
late AnimationController _cardAnimController;
late Animation<double> _cardAnimation;
// 模拟设备数据
static const List<MockDevice> _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<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _cardAnimController, curve: Curves.easeOutCubic),
);
// 模拟搜索延迟
_startSearch();
}
@override
void dispose() {
_searchAnimController.dispose();
_cardAnimController.dispose();
super.dispose();
}
/// 开始搜索 (模拟)
Future<void> _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<void> _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,
),
),
],
),
),
),
],
],
),
);
}
}