diff --git a/airhub_app/ios/Runner/Info.plist b/airhub_app/ios/Runner/Info.plist
index 943fe1e..a5ebd0d 100644
--- a/airhub_app/ios/Runner/Info.plist
+++ b/airhub_app/ios/Runner/Info.plist
@@ -26,6 +26,10 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NSCameraUsageDescription
+ 需要相机权限来拍照传图到徽章设备
+ NSPhotoLibraryUsageDescription
+ 需要相册权限来选择图片传图到徽章设备
NSBluetoothAlwaysUsageDescription
需要蓝牙权限来搜索和连接您的设备
NSBluetoothPeripheralUsageDescription
diff --git a/airhub_app/lib/core/network/api_client.dart b/airhub_app/lib/core/network/api_client.dart
index 3b3b8bb..cae9f7e 100644
--- a/airhub_app/lib/core/network/api_client.dart
+++ b/airhub_app/lib/core/network/api_client.dart
@@ -143,7 +143,6 @@ class _AuthInterceptor extends Interceptor {
'/auth/phone-login/',
'/auth/refresh/',
'/version/check/',
- '/devices/query-by-mac/',
];
final needsAuth = !noAuthPaths.any((p) => options.path.contains(p));
diff --git a/airhub_app/lib/core/network/api_config.dart b/airhub_app/lib/core/network/api_config.dart
index e6e3598..b4011e3 100644
--- a/airhub_app/lib/core/network/api_config.dart
+++ b/airhub_app/lib/core/network/api_config.dart
@@ -1,6 +1,14 @@
+import 'package:flutter/foundation.dart' show kIsWeb;
+
class ApiConfig {
- /// 后端服务器地址
- static const String baseUrl = 'https://qiyuan-rtc-api.airlabs.art';
+ /// 本地开发地址(Web 调试用)
+ 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,用于阿里云号码认证)
static const String authBaseUrl = 'https://qiyuan-rtc-api.airlabs.art';
diff --git a/airhub_app/lib/core/router/app_router.dart b/airhub_app/lib/core/router/app_router.dart
index 7216857..1dc2bfb 100644
--- a/airhub_app/lib/core/router/app_router.dart
+++ b/airhub_app/lib/core/router/app_router.dart
@@ -1,4 +1,5 @@
import 'dart:typed_data';
+import 'package:flutter/foundation.dart';
import 'package:go_router/go_router.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/webview_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_home_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';
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
GoRouter goRouter(Ref ref) {
final tokenManager = ref.watch(tokenManagerProvider);
@@ -32,6 +43,37 @@ GoRouter goRouter(Ref ref) {
return '/login';
}
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 null;
@@ -69,6 +111,10 @@ GoRouter goRouter(Ref ref) {
path: '/badge-control',
builder: (context, state) => const BadgeControlPage(),
),
+ GoRoute(
+ path: '/badge-basic-control',
+ builder: (context, state) => const BadgeBasicControlPage(),
+ ),
GoRoute(
path: '/badge',
builder: (context, state) => const BadgeHomePage(),
diff --git a/airhub_app/lib/core/router/app_router.g.dart b/airhub_app/lib/core/router/app_router.g.dart
index 26cd54c..2b81a6b 100644
--- a/airhub_app/lib/core/router/app_router.g.dart
+++ b/airhub_app/lib/core/router/app_router.g.dart
@@ -48,4 +48,4 @@ final class GoRouterProvider
}
}
-String _$goRouterHash() => r'25447f0e21cf92fb17956c2d137db4fe19e43338';
+String _$goRouterHash() => r'276ed56c903c6bd5bb43569f7b8f58103d73198c';
diff --git a/airhub_app/lib/features/auth/presentation/pages/login_page.dart b/airhub_app/lib/features/auth/presentation/pages/login_page.dart
index bf9e330..9d0465d 100644
--- a/airhub_app/lib/features/auth/presentation/pages/login_page.dart
+++ b/airhub_app/lib/features/auth/presentation/pages/login_page.dart
@@ -219,7 +219,7 @@ class _LoginPageState extends ConsumerState {
final success = await ref.read(authControllerProvider.notifier).tokenLogin(token);
debugPrint('[Login] tokenLogin 结果: $success');
if (success && mounted) {
- await _navigateAfterLogin();
+ context.go('/login');
}
}
@@ -274,25 +274,8 @@ class _LoginPageState extends ConsumerState {
.read(authControllerProvider.notifier)
.codeLogin(_phoneController.text, _codeController.text);
if (success && mounted) {
- await _navigateAfterLogin();
- }
- }
-
- Future _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');
+ // 登录成功后跳到 /login,触发 router redirect 统一处理跳转
+ context.go('/login');
}
}
diff --git a/airhub_app/lib/features/badge/presentation/pages/badge_basic_control_page.dart b/airhub_app/lib/features/badge/presentation/pages/badge_basic_control_page.dart
new file mode 100644
index 0000000..7bd9819
--- /dev/null
+++ b/airhub_app/lib/features/badge/presentation/pages/badge_basic_control_page.dart
@@ -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 createState() =>
+ _BadgeBasicControlPageState();
+}
+
+class _BadgeBasicControlPageState extends ConsumerState
+ with SingleTickerProviderStateMixin {
+ int _currentTab = 0;
+ late AnimationController _floatController;
+ late Animation _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(begin: -8, end: 8).animate(
+ CurvedAnimation(parent: _floatController, curve: Curves.easeInOut),
+ );
+ _loadLastImage();
+ }
+
+ @override
+ void dispose() {
+ _floatController.dispose();
+ super.dispose();
+ }
+
+ Future _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;
+ final data = body['data'] as Map? ?? {};
+ final images = (data['images'] as List? ?? [])
+ .cast