fix device bind status
This commit is contained in:
parent
90609d97a3
commit
f43fd80e3a
@ -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>
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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';
|
||||||
|
|||||||
@ -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(),
|
||||||
|
|||||||
@ -48,4 +48,4 @@ final class GoRouterProvider
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
String _$goRouterHash() => r'25447f0e21cf92fb17956c2d137db4fe19e43338';
|
String _$goRouterHash() => r'276ed56c903c6bd5bb43569f7b8f58103d73198c';
|
||||||
|
|||||||
@ -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');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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(
|
||||||
|
|||||||
@ -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;
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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(
|
||||||
|
|||||||
@ -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),
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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');
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user