fix device bind status

This commit is contained in:
repair-agent 2026-03-26 10:07:08 +08:00
parent 90609d97a3
commit f43fd80e3a
13 changed files with 752 additions and 134 deletions

View File

@ -26,6 +26,10 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>NSCameraUsageDescription</key>
<string>需要相机权限来拍照传图到徽章设备</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>需要相册权限来选择图片传图到徽章设备</string>
<key>NSBluetoothAlwaysUsageDescription</key> <key>NSBluetoothAlwaysUsageDescription</key>
<string>需要蓝牙权限来搜索和连接您的设备</string> <string>需要蓝牙权限来搜索和连接您的设备</string>
<key>NSBluetoothPeripheralUsageDescription</key> <key>NSBluetoothPeripheralUsageDescription</key>

View File

@ -143,7 +143,6 @@ class _AuthInterceptor extends Interceptor {
'/auth/phone-login/', '/auth/phone-login/',
'/auth/refresh/', '/auth/refresh/',
'/version/check/', '/version/check/',
'/devices/query-by-mac/',
]; ];
final needsAuth = !noAuthPaths.any((p) => options.path.contains(p)); final needsAuth = !noAuthPaths.any((p) => options.path.contains(p));

View File

@ -1,6 +1,14 @@
import 'package:flutter/foundation.dart' show kIsWeb;
class ApiConfig { class ApiConfig {
/// /// Web
static const String baseUrl = 'https://qiyuan-rtc-api.airlabs.art'; static const String _localUrl = 'http://192.168.124.8:8000';
/// 线APP
static const String _prodUrl = 'https://qiyuan-rtc-api.airlabs.art';
/// Web APP 线
static String get baseUrl => kIsWeb ? _localUrl : _prodUrl;
/// HTTPS /// HTTPS
static const String authBaseUrl = 'https://qiyuan-rtc-api.airlabs.art'; static const String authBaseUrl = 'https://qiyuan-rtc-api.airlabs.art';

View File

@ -1,4 +1,5 @@
import 'dart:typed_data'; import 'dart:typed_data';
import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
@ -10,13 +11,23 @@ import '../../pages/product_selection_page.dart';
import '../../pages/profile/profile_page.dart'; import '../../pages/profile/profile_page.dart';
import '../../pages/webview_page.dart'; import '../../pages/webview_page.dart';
import '../../pages/wifi_config_page.dart'; import '../../pages/wifi_config_page.dart';
import '../../features/badge/presentation/pages/badge_basic_control_page.dart';
import '../../features/badge/presentation/pages/badge_control_page.dart'; import '../../features/badge/presentation/pages/badge_control_page.dart';
import '../../features/badge/presentation/pages/badge_home_page.dart'; import '../../features/badge/presentation/pages/badge_home_page.dart';
import '../../features/badge/presentation/pages/badge_transfer_page.dart'; import '../../features/badge/presentation/pages/badge_transfer_page.dart';
import '../../features/device/data/datasources/device_remote_data_source.dart';
import '../../theme/product_theme.dart';
import '../network/token_manager.dart'; import '../network/token_manager.dart';
part 'app_router.g.dart'; part 'app_router.g.dart';
/// + ProductType
const _productCodeRoutes = {
'KPBL-ON': (route: '/device-control', type: ProductType.capybara),
'DZBJ-ON': (route: '/badge-control', type: ProductType.badgeAi),
'DZBJ-OFF': (route: '/badge-basic-control', type: ProductType.badgeBasic),
};
@riverpod @riverpod
GoRouter goRouter(Ref ref) { GoRouter goRouter(Ref ref) {
final tokenManager = ref.watch(tokenManagerProvider); final tokenManager = ref.watch(tokenManagerProvider);
@ -32,6 +43,37 @@ GoRouter goRouter(Ref ref) {
return '/login'; return '/login';
} }
if (hasToken && isLoginRoute) { if (hasToken && isLoginRoute) {
// 使
try {
final dataSource = ref.read(deviceRemoteDataSourceProvider);
final devices = await dataSource.getMyDevices();
debugPrint('[Router] 已绑定设备数: ${devices.length}');
if (devices.isNotEmpty) {
// last_online_at 使
devices.sort((a, b) {
final ta = a.device.lastOnlineAt ?? '';
final tb = b.device.lastOnlineAt ?? '';
return tb.compareTo(ta);
});
final recent = devices.first;
final dt = recent.device.deviceType;
final dti = recent.device.deviceTypeInfo;
debugPrint('[Router] 最近设备 sn=${recent.device.sn}');
debugPrint('[Router] deviceType=$dt');
debugPrint('[Router] deviceTypeInfo=$dti');
final resolvedDt = dt ?? dti;
final code = resolvedDt?.productCode ?? '';
debugPrint('[Router] productCode=$code');
final mapping = _productCodeRoutes[code];
debugPrint('[Router] mapping=$mapping → route=${mapping?.route}');
if (mapping != null) {
ref.read(currentProductTypeProvider.notifier).set(mapping.type);
return mapping.route;
}
}
} catch (e) {
debugPrint('[Router] 获取设备失败: $e');
}
return '/home'; return '/home';
} }
return null; return null;
@ -69,6 +111,10 @@ GoRouter goRouter(Ref ref) {
path: '/badge-control', path: '/badge-control',
builder: (context, state) => const BadgeControlPage(), builder: (context, state) => const BadgeControlPage(),
), ),
GoRoute(
path: '/badge-basic-control',
builder: (context, state) => const BadgeBasicControlPage(),
),
GoRoute( GoRoute(
path: '/badge', path: '/badge',
builder: (context, state) => const BadgeHomePage(), builder: (context, state) => const BadgeHomePage(),

View File

@ -48,4 +48,4 @@ final class GoRouterProvider
} }
} }
String _$goRouterHash() => r'25447f0e21cf92fb17956c2d137db4fe19e43338'; String _$goRouterHash() => r'276ed56c903c6bd5bb43569f7b8f58103d73198c';

View File

@ -219,7 +219,7 @@ class _LoginPageState extends ConsumerState<LoginPage> {
final success = await ref.read(authControllerProvider.notifier).tokenLogin(token); final success = await ref.read(authControllerProvider.notifier).tokenLogin(token);
debugPrint('[Login] tokenLogin 结果: $success'); debugPrint('[Login] tokenLogin 结果: $success');
if (success && mounted) { if (success && mounted) {
await _navigateAfterLogin(); context.go('/login');
} }
} }
@ -274,25 +274,8 @@ class _LoginPageState extends ConsumerState<LoginPage> {
.read(authControllerProvider.notifier) .read(authControllerProvider.notifier)
.codeLogin(_phoneController.text, _codeController.text); .codeLogin(_phoneController.text, _codeController.text);
if (success && mounted) { if (success && mounted) {
await _navigateAfterLogin(); // /login router redirect
} context.go('/login');
}
Future<void> _navigateAfterLogin() async {
if (!mounted) return;
try {
final devices = await ref.read(deviceControllerProvider.future);
if (!mounted) return;
if (devices.isNotEmpty) {
debugPrint('[Login] User has ${devices.length} device(s), navigating to device control');
context.go('/device-control');
} else {
debugPrint('[Login] No devices, navigating to home');
context.go('/home');
}
} catch (e) {
debugPrint('[Login] Device check failed: $e');
if (mounted) context.go('/home');
} }
} }

View File

@ -0,0 +1,424 @@
import 'dart:convert';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import '../../../../core/network/api_config.dart';
import '../../../../pages/profile/profile_page.dart';
import '../../../../theme/product_theme.dart';
import '../../../../widgets/animated_gradient_background.dart';
/// (DZBJ-OFF)
class BadgeBasicControlPage extends ConsumerStatefulWidget {
const BadgeBasicControlPage({super.key});
@override
ConsumerState<BadgeBasicControlPage> createState() =>
_BadgeBasicControlPageState();
}
class _BadgeBasicControlPageState extends ConsumerState<BadgeBasicControlPage>
with SingleTickerProviderStateMixin {
int _currentTab = 0;
late AnimationController _floatController;
late Animation<double> _floatAnimation;
String? _lastImageUrl;
bool _loading = true;
@override
void initState() {
super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic));
_floatController = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true);
_floatAnimation = Tween<double>(begin: -8, end: 8).animate(
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
);
_loadLastImage();
}
@override
void dispose() {
_floatController.dispose();
super.dispose();
}
Future<void> _loadLastImage() async {
try {
final prefs = await SharedPreferences.getInstance();
final token = prefs.getString('access_token');
final resp = await http.get(
Uri.parse('${ApiConfig.fullBaseUrl}/badge/history/'),
headers: {if (token != null) 'Authorization': 'Bearer $token'},
).timeout(const Duration(seconds: 10));
if (resp.statusCode == 200) {
final body = jsonDecode(resp.body) as Map<String, dynamic>;
final data = body['data'] as Map<String, dynamic>? ?? {};
final images = (data['images'] as List<dynamic>? ?? [])
.cast<Map<String, dynamic>>()
.where((img) =>
img['generation_status'] == 'completed' &&
(img['image_url'] as String?)?.isNotEmpty == true)
.toList();
if (images.isNotEmpty && mounted) {
setState(() => _lastImageUrl = images.first['image_url'] as String);
}
}
} catch (_) {}
if (mounted) setState(() => _loading = false);
}
@override
Widget build(BuildContext context) {
final productTheme = ref.watch(currentProductThemeProvider);
return Scaffold(
backgroundColor: Colors.white,
body: Stack(
children: [
const AnimatedGradientBackground(),
IndexedStack(
index: _currentTab,
children: [
_buildHomePage(productTheme),
const ProfilePage(),
],
),
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 12,
child: _buildBottomNavBar(productTheme),
),
],
),
);
}
Widget _buildHomePage(ProductThemeData productTheme) {
return SafeArea(
bottom: false,
child: Stack(
children: [
Center(
child: AnimatedBuilder(
animation: _floatAnimation,
builder: (context, child) => Transform.translate(
offset: Offset(0, _floatAnimation.value),
child: child,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_BadgePreviewCircle(
imageUrl: _lastImageUrl,
loading: _loading,
accentColor: productTheme.accentColor,
size: 240,
),
const SizedBox(height: 16),
if (!_loading && _lastImageUrl == null)
Text(
'点击右上角「传图」上传你的第一张图',
style: TextStyle(
fontSize: 13,
color: productTheme.accentColor.withOpacity(0.5),
),
),
],
),
),
),
Positioned(
top: 8,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
_GlassIconButton(
onTap: () => context.push('/product-selection'),
child: SvgPicture.asset(
'assets/www/icons/icon-switch.svg',
width: 20,
height: 20,
colorFilter: const ColorFilter.mode(
Color(0xFF4B5563),
BlendMode.srcIn,
),
),
),
_GlassPillButton(
onTap: () => context.push('/badge'),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'传图',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: productTheme.accentColor,
),
),
const SizedBox(width: 4),
Icon(Icons.send_rounded,
size: 16, color: productTheme.accentColor),
],
),
),
],
),
),
],
),
);
}
Widget _buildBottomNavBar(ProductThemeData productTheme) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
width: 180,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(32),
border: Border.all(color: Colors.white.withOpacity(0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF4B5563).withOpacity(0.08),
offset: const Offset(0, 10),
blurRadius: 30,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(0, 'assets/www/icons/icon-home-pixel.svg',
Icons.home, productTheme),
_buildNavItem(1, 'assets/www/icons/icon-user-pixel.svg',
Icons.person, productTheme),
],
),
),
),
),
);
}
Widget _buildNavItem(
int index, String iconPath, IconData fallback, ProductThemeData theme) {
final isActive = _currentTab == index;
return GestureDetector(
onTap: () => setState(() => _currentTab = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: isActive ? theme.buttonGradient : null,
borderRadius: BorderRadius.circular(28),
boxShadow: isActive
? [
BoxShadow(
color: theme.accentColor.withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 15,
),
]
: null,
),
alignment: Alignment.center,
child: SvgPicture.asset(
iconPath,
width: isActive ? 28 : 26,
height: isActive ? 28 : 26,
colorFilter: ColorFilter.mode(
isActive ? Colors.white : const Color(0xFF6B7280).withOpacity(0.6),
BlendMode.srcIn,
),
placeholderBuilder: (_) => Icon(
fallback,
color: isActive ? Colors.white : const Color(0xFF6B7280),
size: 24,
),
),
),
);
}
}
class _BadgePreviewCircle extends StatelessWidget {
final String? imageUrl;
final bool loading;
final Color accentColor;
final double size;
const _BadgePreviewCircle({
required this.imageUrl,
required this.loading,
required this.accentColor,
required this.size,
});
@override
Widget build(BuildContext context) {
return Container(
width: size,
height: size,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: const Color(0xFF2D3748),
border: Border.all(color: const Color(0xFF4A5568), width: 4),
boxShadow: [
BoxShadow(
color: accentColor.withOpacity(0.2),
blurRadius: 40,
spreadRadius: 8,
),
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 8),
),
],
),
padding: const EdgeInsets.all(8),
child: ClipOval(
child: loading
? Center(
child: CircularProgressIndicator(
strokeWidth: 2,
color: accentColor.withOpacity(0.5),
),
)
: imageUrl != null
? Image.network(
imageUrl!,
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => _buildPlaceholder(),
)
: _buildPlaceholder(),
),
);
}
Widget _buildPlaceholder() {
return Container(
color: const Color(0xFF1A202C),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.add_photo_alternate_outlined,
size: 48, color: accentColor.withOpacity(0.4)),
const SizedBox(height: 8),
Text(
'暂无图片',
style: TextStyle(
fontSize: 13,
color: accentColor.withOpacity(0.3),
),
),
],
),
),
);
}
}
class _GlassIconButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassIconButton({required this.onTap, required this.child});
@override
State<_GlassIconButton> createState() => _GlassIconButtonState();
}
class _GlassIconButtonState extends State<_GlassIconButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.92 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
alignment: Alignment.center,
child: widget.child,
),
),
);
}
}
class _GlassPillButton extends StatefulWidget {
final VoidCallback onTap;
final Widget child;
const _GlassPillButton({required this.onTap, required this.child});
@override
State<_GlassPillButton> createState() => _GlassPillButtonState();
}
class _GlassPillButtonState extends State<_GlassPillButton> {
bool _pressed = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (_) => setState(() => _pressed = true),
onTapUp: (_) => setState(() => _pressed = false),
onTapCancel: () => setState(() => _pressed = false),
onTap: () {
HapticFeedback.lightImpact();
widget.onTap();
},
child: AnimatedScale(
scale: _pressed ? 0.94 : 1.0,
duration: const Duration(milliseconds: 120),
curve: Curves.easeOut,
child: Container(
height: 44,
padding: const EdgeInsets.symmetric(horizontal: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(_pressed ? 0.45 : 0.25),
borderRadius: BorderRadius.circular(22),
border: Border.all(color: Colors.white.withOpacity(0.4)),
),
child: widget.child,
),
),
);
}
}

View File

@ -1,12 +1,15 @@
import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart'; import 'package:flutter_svg/flutter_svg.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../../../pages/profile/profile_page.dart';
import '../../../../theme/product_theme.dart'; import '../../../../theme/product_theme.dart';
import '../../../../widgets/animated_gradient_background.dart'; import '../../../../widgets/animated_gradient_background.dart';
/// AI (DZBJ-ON)
class BadgeControlPage extends ConsumerStatefulWidget { class BadgeControlPage extends ConsumerStatefulWidget {
const BadgeControlPage({super.key}); const BadgeControlPage({super.key});
@ -16,17 +19,18 @@ class BadgeControlPage extends ConsumerStatefulWidget {
class _BadgeControlPageState extends ConsumerState<BadgeControlPage> class _BadgeControlPageState extends ConsumerState<BadgeControlPage>
with SingleTickerProviderStateMixin { with SingleTickerProviderStateMixin {
int _currentTab = 0; // 0: , 1:
late AnimationController _floatController; late AnimationController _floatController;
late Animation<double> _floatAnimation; late Animation<double> _floatAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeAi));
_floatController = AnimationController( _floatController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(milliseconds: 3000), duration: const Duration(milliseconds: 3000),
)..repeat(reverse: true); )..repeat(reverse: true);
// 线
_floatAnimation = Tween<double>(begin: -8, end: 8).animate( _floatAnimation = Tween<double>(begin: -8, end: 8).animate(
CurvedAnimation(parent: _floatController, curve: Curves.easeInOut), CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
); );
@ -47,98 +51,106 @@ class _BadgeControlPageState extends ConsumerState<BadgeControlPage>
body: Stack( body: Stack(
children: [ children: [
const AnimatedGradientBackground(), const AnimatedGradientBackground(),
SafeArea(
child: Stack( //
IndexedStack(
index: _currentTab,
children: [
_buildHomePage(productTheme),
const ProfilePage(),
],
),
//
Positioned(
left: 0,
right: 0,
bottom: MediaQuery.of(context).padding.bottom + 12,
child: _buildBottomNavBar(productTheme),
),
],
),
);
}
Widget _buildHomePage(ProductThemeData productTheme) {
return SafeArea(
bottom: false,
child: Stack(
children: [
//
Center(
child: AnimatedBuilder(
animation: _floatAnimation,
builder: (context, child) => Transform.translate(
offset: Offset(0, _floatAnimation.value),
child: child,
),
child: Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: productTheme.accentColor.withOpacity(0.2),
blurRadius: 60,
spreadRadius: 15,
),
BoxShadow(
color: productTheme.accentColorLight.withOpacity(0.1),
blurRadius: 100,
spreadRadius: 30,
),
],
),
child: Image.asset(
'assets/www/Capybara.png',
width: 260,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Icon(
Icons.smart_toy,
size: 150,
color: productTheme.accentColor),
),
),
),
),
//
Positioned(
top: 8,
left: 16,
right: 16,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
// _GlassIconButton(
Center( onTap: () => context.push('/product-selection'),
child: AnimatedBuilder( child: SvgPicture.asset(
animation: _floatAnimation, 'assets/www/icons/icon-switch.svg',
builder: (context, child) { width: 20,
return Transform.translate( height: 20,
offset: Offset(0, _floatAnimation.value), colorFilter: const ColorFilter.mode(
child: child, Color(0xFF4B5563),
); BlendMode.srcIn,
},
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
//
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: productTheme.accentColor
.withOpacity(0.2),
blurRadius: 60,
spreadRadius: 15,
),
BoxShadow(
color: productTheme.accentColorLight
.withOpacity(0.1),
blurRadius: 100,
spreadRadius: 30,
),
],
),
child: Image.asset(
'assets/www/Capybara.png',
width: 260,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Icon(
Icons.smart_toy,
size: 150,
color: productTheme.accentColor),
),
),
],
), ),
), ),
), ),
_GlassPillButton(
// onTap: () => context.push('/badge'),
Positioned(
top: 8,
left: 16,
right: 16,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.min,
children: [ children: [
// Text(
_GlassIconButton( '传图',
onTap: () => context.push('/product-selection'), style: TextStyle(
child: SvgPicture.asset( fontSize: 14,
'assets/www/icons/icon-switch.svg', fontWeight: FontWeight.w600,
width: 20, color: productTheme.accentColor,
height: 20,
colorFilter: const ColorFilter.mode(
Color(0xFF4B5563),
BlendMode.srcIn,
),
),
),
//
_GlassPillButton(
onTap: () => context.push('/badge'),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'传图',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: productTheme.accentColor,
),
),
const SizedBox(width: 4),
Icon(Icons.send_rounded,
size: 16,
color: productTheme.accentColor),
],
), ),
), ),
const SizedBox(width: 4),
Icon(Icons.send_rounded,
size: 16, color: productTheme.accentColor),
], ],
), ),
), ),
@ -149,22 +161,95 @@ class _BadgeControlPageState extends ConsumerState<BadgeControlPage>
), ),
); );
} }
Widget _buildBottomNavBar(ProductThemeData productTheme) {
return Center(
child: ClipRRect(
borderRadius: BorderRadius.circular(32),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
child: Container(
width: 180,
padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(32),
border: Border.all(color: Colors.white.withOpacity(0.8)),
boxShadow: [
BoxShadow(
color: const Color(0xFF4B5563).withOpacity(0.08),
offset: const Offset(0, 10),
blurRadius: 30,
),
],
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildNavItem(0, 'assets/www/icons/icon-home-pixel.svg',
Icons.home, productTheme),
_buildNavItem(1, 'assets/www/icons/icon-user-pixel.svg',
Icons.person, productTheme),
],
),
),
),
),
);
}
Widget _buildNavItem(
int index, String iconPath, IconData fallback, ProductThemeData theme) {
final isActive = _currentTab == index;
return GestureDetector(
onTap: () => setState(() => _currentTab = index),
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
width: 56,
height: 56,
decoration: BoxDecoration(
gradient: isActive ? theme.buttonGradient : null,
borderRadius: BorderRadius.circular(28),
boxShadow: isActive
? [
BoxShadow(
color: theme.accentColor.withOpacity(0.4),
offset: const Offset(0, 4),
blurRadius: 15,
),
]
: null,
),
alignment: Alignment.center,
child: SvgPicture.asset(
iconPath,
width: isActive ? 28 : 26,
height: isActive ? 28 : 26,
colorFilter: ColorFilter.mode(
isActive ? Colors.white : const Color(0xFF6B7280).withOpacity(0.6),
BlendMode.srcIn,
),
placeholderBuilder: (_) => Icon(
fallback,
color: isActive ? Colors.white : const Color(0xFF6B7280),
size: 24,
),
),
),
);
}
} }
///
class _GlassIconButton extends StatefulWidget { class _GlassIconButton extends StatefulWidget {
final VoidCallback onTap; final VoidCallback onTap;
final Widget child; final Widget child;
const _GlassIconButton({required this.onTap, required this.child}); const _GlassIconButton({required this.onTap, required this.child});
@override @override
State<_GlassIconButton> createState() => _GlassIconButtonState(); State<_GlassIconButton> createState() => _GlassIconButtonState();
} }
class _GlassIconButtonState extends State<_GlassIconButton> { class _GlassIconButtonState extends State<_GlassIconButton> {
bool _pressed = false; bool _pressed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
@ -195,20 +280,16 @@ class _GlassIconButtonState extends State<_GlassIconButton> {
} }
} }
///
class _GlassPillButton extends StatefulWidget { class _GlassPillButton extends StatefulWidget {
final VoidCallback onTap; final VoidCallback onTap;
final Widget child; final Widget child;
const _GlassPillButton({required this.onTap, required this.child}); const _GlassPillButton({required this.onTap, required this.child});
@override @override
State<_GlassPillButton> createState() => _GlassPillButtonState(); State<_GlassPillButton> createState() => _GlassPillButtonState();
} }
class _GlassPillButtonState extends State<_GlassPillButton> { class _GlassPillButtonState extends State<_GlassPillButton> {
bool _pressed = false; bool _pressed = false;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(

View File

@ -19,16 +19,17 @@ class DeviceController extends _$DeviceController {
); );
} }
Future<bool> bindDevice(String sn, {int? spiritId}) async { /// null
Future<String?> bindDevice(String sn, {int? spiritId}) async {
final repository = ref.read(deviceRepositoryProvider); final repository = ref.read(deviceRepositoryProvider);
final result = await repository.bindDevice(sn, spiritId: spiritId); final result = await repository.bindDevice(sn, spiritId: spiritId);
if (!ref.mounted) return false; if (!ref.mounted) return '组件已卸载';
return result.fold( return result.fold(
(failure) => false, (failure) => failure.message,
(bindingId) { (bindingId) {
if (!ref.mounted) return false; if (!ref.mounted) return '组件已卸载';
ref.invalidateSelf(); ref.invalidateSelf();
return true; return null;
}, },
); );
} }

View File

@ -27,6 +27,7 @@ class MockDevice {
final DeviceType type; final DeviceType type;
final bool hasAI; final bool hasAI;
final bool isNetworkRequired; final bool isNetworkRequired;
final String bindStatus; // unbound / bound_by_me / bound_by_other
final BluetoothDevice? bleDevice; final BluetoothDevice? bleDevice;
const MockDevice({ const MockDevice({
@ -36,9 +37,12 @@ class MockDevice {
required this.type, required this.type,
required this.hasAI, required this.hasAI,
this.isNetworkRequired = true, this.isNetworkRequired = true,
this.bindStatus = 'unbound',
this.bleDevice, this.bleDevice,
}); });
bool get isBoundByOther => bindStatus == 'bound_by_other';
String get iconPath { String get iconPath {
switch (type) { switch (type) {
case DeviceType.plush: case DeviceType.plush:
@ -256,6 +260,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
final productCode = data['device_type']?['product_code'] as String? ?? ''; final productCode = data['device_type']?['product_code'] as String? ?? '';
final sn = data['sn'] as String? ?? ''; final sn = data['sn'] as String? ?? '';
final isNetworkRequired = data['device_type']?['is_network_required'] as bool? ?? true; final isNetworkRequired = data['device_type']?['is_network_required'] as bool? ?? true;
final bindStatus = data['bind_status'] as String? ?? 'unbound';
final bleDevice = _pendingBleDevices[mac]; final bleDevice = _pendingBleDevices[mac];
// API // API
@ -271,6 +276,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
type: _inferDeviceTypeByCode(productCode, displayName), type: _inferDeviceTypeByCode(productCode, displayName),
hasAI: _inferHasAI(displayName), hasAI: _inferHasAI(displayName),
isNetworkRequired: isNetworkRequired, isNetworkRequired: isNetworkRequired,
bindStatus: bindStatus,
bleDevice: bleDevice, bleDevice: bleDevice,
)); ));
} }
@ -387,7 +393,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
context.go('/badge-control'); context.go('/badge-control');
case DeviceType.badge: case DeviceType.badge:
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic); ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic);
context.go('/badge-control'); context.go('/badge-basic-control');
case DeviceType.plush: case DeviceType.plush:
ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara); ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara);
context.go('/device-control'); context.go('/device-control');
@ -405,14 +411,38 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
} }
final device = _devices[_currentIndex]; final device = _devices[_currentIndex];
debugPrint('[Bluetooth] 连接设备: sn=${device.sn}, mac=${device.macAddress}, needsNetwork=${device.isNetworkRequired}'); debugPrint('[Bluetooth] 连接设备: sn=${device.sn}, mac=${device.macAddress}, bindStatus=${device.bindStatus}');
//
if (device.isBoundByOther) {
showGlassDialog(
context: context,
title: '无法连接',
description: '该设备已被其他用户绑定,无法使用。如需解绑请联系设备所有者。',
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
if (!device.isNetworkRequired) { if (!device.isNetworkRequired) {
// -> // ->
if (device.sn.isNotEmpty) { if (device.sn.isNotEmpty) {
setState(() => _isConnecting = true); setState(() => _isConnecting = true);
try { try {
await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn); final error = await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn);
if (!mounted) return;
if (error != null) {
setState(() => _isConnecting = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} catch (e) { } catch (e) {
debugPrint('[Bluetooth] bindDevice 异常: $e'); debugPrint('[Bluetooth] bindDevice 异常: $e');
} }
@ -428,7 +458,19 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
debugPrint('[Bluetooth] Web 环境,直接绑定 sn=${device.sn}'); debugPrint('[Bluetooth] Web 环境,直接绑定 sn=${device.sn}');
setState(() => _isConnecting = true); setState(() => _isConnecting = true);
if (device.sn.isNotEmpty) { if (device.sn.isNotEmpty) {
await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn); final error = await ref.read(deviceControllerProvider.notifier).bindDevice(device.sn);
if (!mounted) return;
if (error != null) {
setState(() => _isConnecting = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} }
if (!mounted) return; if (!mounted) return;
setState(() => _isConnecting = false); setState(() => _isConnecting = false);
@ -523,7 +565,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
children: [ children: [
// - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border // - CSS: border-radius: 12px, bg: rgba(255,255,255,0.6), no border
GestureDetector( GestureDetector(
onTap: () => context.go('/home'), onTap: () => context.pop(),
child: Container( child: Container(
width: 40, width: 40,
height: 40, height: 40,
@ -788,9 +830,22 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
style: GoogleFonts.outfit( style: GoogleFonts.outfit(
fontSize: 18, fontSize: 18,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: const Color(0xFF1F2937), color: device.isBoundByOther
? const Color(0xFF9CA3AF)
: const Color(0xFF1F2937),
), ),
), ),
//
if (device.isBoundByOther) ...[
const SizedBox(height: 4),
Text(
'已被其他用户绑定',
style: TextStyle(
fontSize: 12,
color: const Color(0xFFEF4444),
),
),
],
], ],
); );
} }
@ -848,7 +903,7 @@ class _BluetoothPageState extends ConsumerState<BluetoothPage>
children: [ children: [
// - HTML: frosted glass with border // - HTML: frosted glass with border
GestureDetector( GestureDetector(
onTap: () => context.go('/home'), onTap: () => context.pop(),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.circular(25), borderRadius: BorderRadius.circular(25),
child: Container( child: Container(

View File

@ -19,6 +19,7 @@ import '../widgets/ios_toast.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../features/device/presentation/controllers/device_controller.dart'; import '../features/device/presentation/controllers/device_controller.dart';
import '../theme/product_theme.dart';
class DeviceControlPage extends ConsumerStatefulWidget { class DeviceControlPage extends ConsumerStatefulWidget {
const DeviceControlPage({super.key}); const DeviceControlPage({super.key});
@ -48,6 +49,7 @@ class _DeviceControlPageState extends ConsumerState<DeviceControlPage>
@override @override
void initState() { void initState() {
super.initState(); super.initState();
Future.microtask(() => ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara));
_mascotAnimController = AnimationController( _mascotAnimController = AnimationController(
vsync: this, vsync: this,
duration: const Duration(seconds: 4), duration: const Duration(seconds: 4),

View File

@ -205,14 +205,16 @@ class _ProductSelectionPageState extends ConsumerState<ProductSelectionPage> {
if (boundDevice != null) { if (boundDevice != null) {
// //
final pid = product['id'] as String; final pid = product['id'] as String;
if (pid == 'badge-ai' || pid == 'badge-basic') { if (pid == 'badge-ai') {
context.go('/badge-control'); context.go('/badge-control');
} else if (pid == 'badge-basic') {
context.go('/badge-basic-control');
} else { } else {
context.go('/device-control'); context.go('/device-control');
} }
} else { } else {
// // push
context.go('/bluetooth'); context.push('/bluetooth');
} }
}, },
); );

View File

@ -8,6 +8,7 @@ import '../core/services/ble_provisioning_service.dart';
import '../features/device/presentation/controllers/device_controller.dart'; import '../features/device/presentation/controllers/device_controller.dart';
import '../widgets/animated_gradient_background.dart'; import '../widgets/animated_gradient_background.dart';
import '../widgets/gradient_button.dart'; import '../widgets/gradient_button.dart';
import '../widgets/glass_dialog.dart';
import '../theme/product_theme.dart'; import '../theme/product_theme.dart';
class WifiConfigPage extends ConsumerStatefulWidget { class WifiConfigPage extends ConsumerStatefulWidget {
@ -137,7 +138,19 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
if (sn.isNotEmpty) { if (sn.isNotEmpty) {
try { try {
debugPrint('[WiFi Config] Binding device sn=$sn'); debugPrint('[WiFi Config] Binding device sn=$sn');
await ref.read(deviceControllerProvider.notifier).bindDevice(sn); final error = await ref.read(deviceControllerProvider.notifier).bindDevice(sn);
if (!mounted) return;
if (error != null) {
setState(() => _isBinding = false);
showGlassDialog(
context: context,
title: '绑定失败',
description: error,
confirmText: '确定',
onConfirm: () => Navigator.of(context).pop(),
);
return;
}
} catch (e) { } catch (e) {
debugPrint('[WiFi Config] bindDevice 异常: $e'); debugPrint('[WiFi Config] bindDevice 异常: $e');
} }
@ -150,7 +163,7 @@ class _WifiConfigPageState extends ConsumerState<WifiConfigPage>
context.go('/badge-control'); context.go('/badge-control');
} else if (deviceType == 'badge') { } else if (deviceType == 'badge') {
ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic); ref.read(currentProductTypeProvider.notifier).set(ProductType.badgeBasic);
context.go('/badge-control'); context.go('/badge-basic-control');
} else { } else {
ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara); ref.read(currentProductTypeProvider.notifier).set(ProductType.capybara);
context.go('/device-control'); context.go('/device-control');