From 2fabae87386d00de035fc361328472d81c1b02f6 Mon Sep 17 00:00:00 2001 From: repair-agent Date: Sat, 28 Feb 2026 14:35:50 +0800 Subject: [PATCH] fix: auto repair bugs #53 --- airhub_app/ios/Flutter/AppFrameworkInfo.plist | 2 - airhub_app/ios/Runner/AppDelegate.swift | 7 +- airhub_app/ios/Runner/Info.plist | 55 +++++-- airhub_app/lib/core/router/app_router.dart | 5 + airhub_app/lib/core/router/app_router.g.dart | 2 +- .../device/domain/entities/device.g.dart | 12 +- .../controllers/device_controller.dart | 5 + .../controllers/device_controller.g.dart | 2 +- .../lib/pages/product_selection_page.dart | 154 ++++++++++-------- airhub_app/lib/pages/settings_page.dart | 6 +- airhub_app/pubspec.lock | 32 ++-- 11 files changed, 159 insertions(+), 123 deletions(-) diff --git a/airhub_app/ios/Flutter/AppFrameworkInfo.plist b/airhub_app/ios/Flutter/AppFrameworkInfo.plist index 1dc6cf7..391a902 100644 --- a/airhub_app/ios/Flutter/AppFrameworkInfo.plist +++ b/airhub_app/ios/Flutter/AppFrameworkInfo.plist @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 13.0 diff --git a/airhub_app/ios/Runner/AppDelegate.swift b/airhub_app/ios/Runner/AppDelegate.swift index 6266644..c30b367 100644 --- a/airhub_app/ios/Runner/AppDelegate.swift +++ b/airhub_app/ios/Runner/AppDelegate.swift @@ -2,12 +2,15 @@ import Flutter import UIKit @main -@objc class AppDelegate: FlutterAppDelegate { +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/airhub_app/ios/Runner/Info.plist b/airhub_app/ios/Runner/Info.plist index 3083e79..943fe1e 100644 --- a/airhub_app/ios/Runner/Info.plist +++ b/airhub_app/ios/Runner/Info.plist @@ -2,6 +2,8 @@ + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName @@ -24,6 +26,42 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + NSBluetoothAlwaysUsageDescription + 需要蓝牙权限来搜索和连接您的设备 + NSBluetoothPeripheralUsageDescription + 需要蓝牙权限来搜索和连接您的设备 + NSLocationWhenInUseUsageDescription + 需要位置权限以扫描附近的蓝牙设备 + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + FlutterSceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + + UILaunchScreen + + UIColorName + LaunchBackgroundColor + UIImageName + LaunchImage + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -41,22 +79,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - NSBluetoothAlwaysUsageDescription - 需要蓝牙权限来搜索和连接您的设备 - NSBluetoothPeripheralUsageDescription - 需要蓝牙权限来搜索和连接您的设备 - NSLocationWhenInUseUsageDescription - 需要位置权限以扫描附近的蓝牙设备 - UILaunchScreen - - UIColorName - LaunchBackgroundColor - UIImageName - LaunchImage - diff --git a/airhub_app/lib/core/router/app_router.dart b/airhub_app/lib/core/router/app_router.dart index fe4a7df..95fdfd2 100644 --- a/airhub_app/lib/core/router/app_router.dart +++ b/airhub_app/lib/core/router/app_router.dart @@ -5,6 +5,7 @@ import '../../features/auth/presentation/pages/login_page.dart'; import '../../pages/bluetooth_page.dart'; import '../../pages/device_control_page.dart'; import '../../pages/home_page.dart'; +import '../../pages/product_selection_page.dart'; import '../../pages/profile/profile_page.dart'; import '../../pages/webview_page.dart'; import '../../pages/wifi_config_page.dart'; @@ -48,6 +49,10 @@ GoRouter goRouter(Ref ref) { extra: state.extra as Map?, ), ), + GoRoute( + path: '/product-selection', + builder: (context, state) => const ProductSelectionPage(), + ), GoRoute( path: '/device-control', builder: (context, state) => const DeviceControlPage(), diff --git a/airhub_app/lib/core/router/app_router.g.dart b/airhub_app/lib/core/router/app_router.g.dart index c2f3d9a..105a644 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'8e620e452bb81f2c6ed87b136283a9e508dca2e9'; +String _$goRouterHash() => r'9f77a00bcbc90890c4b6594a9709288e5206c7d8'; diff --git a/airhub_app/lib/features/device/domain/entities/device.g.dart b/airhub_app/lib/features/device/domain/entities/device.g.dart index 04c0e22..733476b 100644 --- a/airhub_app/lib/features/device/domain/entities/device.g.dart +++ b/airhub_app/lib/features/device/domain/entities/device.g.dart @@ -30,12 +30,12 @@ Map _$DeviceTypeToJson(_DeviceType instance) => _DeviceInfo _$DeviceInfoFromJson(Map json) => _DeviceInfo( id: (json['id'] as num).toInt(), sn: json['sn'] as String, - deviceType: (json['device_type'] is Map) - ? DeviceType.fromJson(json['device_type'] as Map) - : null, - deviceTypeInfo: (json['device_type_info'] is Map) - ? DeviceType.fromJson(json['device_type_info'] as Map) - : null, + deviceType: json['device_type'] == null + ? null + : DeviceType.fromJson(json['device_type'] as Map), + deviceTypeInfo: json['device_type_info'] == null + ? null + : DeviceType.fromJson(json['device_type_info'] as Map), macAddress: json['mac_address'] as String?, name: json['name'] as String? ?? '', status: json['status'] as String? ?? 'in_stock', diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart index 0f03c29..4009689 100644 --- a/airhub_app/lib/features/device/presentation/controllers/device_controller.dart +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.dart @@ -21,6 +21,7 @@ class DeviceController extends _$DeviceController { Future bindDevice(String sn, {int? spiritId}) async { final repository = ref.read(deviceRepositoryProvider); final result = await repository.bindDevice(sn, spiritId: spiritId); + if (!ref.mounted) return false; return result.fold( (failure) => false, (bindingId) { @@ -33,6 +34,7 @@ class DeviceController extends _$DeviceController { Future unbindDevice(int userDeviceId) async { final repository = ref.read(deviceRepositoryProvider); final result = await repository.unbindDevice(userDeviceId); + if (!ref.mounted) return false; return result.fold( (failure) => false, (_) { @@ -48,6 +50,7 @@ class DeviceController extends _$DeviceController { Future updateSpirit(int userDeviceId, int spiritId) async { final repository = ref.read(deviceRepositoryProvider); final result = await repository.updateSpirit(userDeviceId, spiritId); + if (!ref.mounted) return false; return result.fold( (failure) => false, (updated) { @@ -78,6 +81,7 @@ class DeviceDetailController extends _$DeviceDetailController { Future updateSettings(Map settings) async { final repository = ref.read(deviceRepositoryProvider); final result = await repository.updateSettings(userDeviceId, settings); + if (!ref.mounted) return false; return result.fold( (failure) => false, (_) { @@ -90,6 +94,7 @@ class DeviceDetailController extends _$DeviceDetailController { Future configWifi(String ssid) async { final repository = ref.read(deviceRepositoryProvider); final result = await repository.configWifi(userDeviceId, ssid); + if (!ref.mounted) return false; return result.fold( (failure) => false, (_) { diff --git a/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart index 05de583..84e11e3 100644 --- a/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart +++ b/airhub_app/lib/features/device/presentation/controllers/device_controller.g.dart @@ -36,7 +36,7 @@ final class DeviceControllerProvider DeviceController create() => DeviceController(); } -String _$deviceControllerHash() => r'9b39117bd54964ba0035aad0eca10250454efaa7'; +String _$deviceControllerHash() => r'3f73a13c7f93fecb9fe781efc4ee305b6186639e'; /// 管理用户设备列表 diff --git a/airhub_app/lib/pages/product_selection_page.dart b/airhub_app/lib/pages/product_selection_page.dart index 28c0aa9..4688a0b 100644 --- a/airhub_app/lib/pages/product_selection_page.dart +++ b/airhub_app/lib/pages/product_selection_page.dart @@ -1,17 +1,20 @@ import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:flutter_svg/flutter_svg.dart'; import '../widgets/animated_gradient_background.dart'; -import '../widgets/ios_toast.dart'; +import '../features/device/presentation/controllers/device_controller.dart'; +import '../features/device/domain/entities/device.dart'; -class ProductSelectionPage extends StatefulWidget { +class ProductSelectionPage extends ConsumerStatefulWidget { const ProductSelectionPage({super.key}); @override - State createState() => _ProductSelectionPageState(); + ConsumerState createState() => _ProductSelectionPageState(); } -class _ProductSelectionPageState extends State { +class _ProductSelectionPageState extends ConsumerState { final ScrollController _scrollController = ScrollController(); @override @@ -28,12 +31,30 @@ class _ProductSelectionPageState extends State { super.dispose(); } + /// 产品 ID 到后端 product_code 的映射 + static const Map> _productCodeMap = { + 'capybara': ['KPBL-ON'], + 'badge-ai': ['DZBJ-ON'], + 'badge-basic': ['DZBJ-OFF'], + }; + + /// 查找用户是否已绑定该产品类型的设备 + UserDevice? _findBoundDevice(String productId, List devices) { + final codes = _productCodeMap[productId]; + if (codes == null || codes.isEmpty) return null; + for (final device in devices) { + final dt = device.device.deviceType ?? device.device.deviceTypeInfo; + if (dt != null && codes.contains(dt.productCode)) { + return device; + } + } + return null; + } + static final List> _products = [ { 'id': 'capybara', 'name': '毛绒机芯', - 'status': '已连接', - 'statusColor': const Color(0xFF10B981), 'icon': 'assets/www/Capybara.png', 'isPng': true, 'hasTag': true, @@ -45,13 +66,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFFC9A07A), - 'selected': true, }, { 'id': 'badge-ai', 'name': '电子吧唧 AI', - 'status': '离线', - 'statusColor': const Color(0xFFE5E7EB), 'icon': 'assets/www/icons/icon-product-badge.svg', 'isPng': false, 'hasTag': true, @@ -63,13 +81,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFF6366F1), - 'selected': false, }, { 'id': 'badge-basic', 'name': '普通吧唧', - 'status': '未配对', - 'statusColor': const Color(0xFFE5E7EB), 'icon': 'assets/www/icons/icon-product-badge.svg', 'isPng': false, 'hasTag': false, @@ -80,13 +95,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFFA78BFA), - 'selected': false, }, { 'id': 'bracelet', 'name': 'AI 手链', - 'status': '点击扫描', - 'statusColor': const Color(0xFFE5E7EB), 'icon': 'assets/www/icons/icon-product-badge.svg', 'isPng': false, 'hasTag': true, @@ -98,13 +110,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFFE07B54), - 'selected': false, }, { 'id': 'vsinger', 'name': '洛天依', - 'status': '去下载专属 APP →', - 'statusColor': Colors.transparent, 'icon': 'assets/www/icons/icon-product-luo.svg', 'isPng': false, 'hasTag': false, @@ -115,13 +124,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFF2DD4BF), - 'selected': false, }, { 'id': 'nightlight', 'name': 'AI 星空夜灯', - 'status': '未配对', - 'statusColor': const Color(0xFFE5E7EB), 'icon': 'assets/www/icons/icon-product-badge.svg', 'isPng': false, 'hasTag': true, @@ -133,13 +139,10 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFF7C3AED), - 'selected': false, }, { 'id': 'feeder', 'name': '智能喂食器', - 'status': '点击扫描', - 'statusColor': const Color(0xFFE5E7EB), 'icon': 'assets/www/icons/icon-product-badge.svg', 'isPng': false, 'hasTag': false, @@ -150,24 +153,24 @@ class _ProductSelectionPageState extends State { end: Alignment.centerRight, ), 'shadowColor': const Color(0xFFE11D48), - 'selected': false, }, ]; @override Widget build(BuildContext context) { final safeTop = MediaQuery.of(context).padding.top; - // 头部高度 = safeArea + padding + 按钮高度 + padding final headerHeight = safeTop + 12 + 40 + 12; + // 获取用户已绑定的设备列表 + final devicesAsync = ref.watch(deviceControllerProvider); + final devices = devicesAsync.value ?? []; + return Scaffold( backgroundColor: Colors.transparent, body: Stack( children: [ - // 1. 全屏流动背景 const AnimatedGradientBackground(), - // 2. 列表 ScrollConfiguration( behavior: ScrollConfiguration.of(context).copyWith( scrollbars: false, @@ -178,17 +181,28 @@ class _ProductSelectionPageState extends State { itemCount: _products.length, separatorBuilder: (_, __) => const SizedBox(height: 16), itemBuilder: (context, index) { + final product = _products[index]; + final boundDevice = _findBoundDevice(product['id'] as String, devices); return _FadeOnScrollCard( - key: ValueKey(_products[index]['id']), - product: _products[index], + key: ValueKey(product['id']), + product: product, + isBound: boundDevice != null, fadeStartY: headerHeight + 16, fadeEndY: safeTop, + onTap: () { + if (boundDevice != null) { + // 已绑定 → 直接进入设备控制页 + context.go('/device-control'); + } else { + // 未绑定 → 跳转蓝牙搜索页 + context.go('/bluetooth'); + } + }, ); }, ), ), - // 3. 头部 — 统一规范:返回按钮 + 居中标题 Positioned( top: 0, left: 0, @@ -198,7 +212,6 @@ class _ProductSelectionPageState extends State { child: Stack( alignment: Alignment.center, children: [ - // 居中标题 Text( '选择产品', style: GoogleFonts.outfit( @@ -207,7 +220,6 @@ class _ProductSelectionPageState extends State { color: const Color(0xFF1F2937), ), ), - // 左侧返回按钮 Align( alignment: Alignment.centerLeft, child: GestureDetector( @@ -238,17 +250,21 @@ class _ProductSelectionPageState extends State { } } -/// 带滚动渐隐的卡片包装器 — 纯 Opacity,无 ShaderMask,无 widget 切换 +/// 带滚动渐隐的卡片包装器 class _FadeOnScrollCard extends StatefulWidget { final Map product; - final double fadeStartY; // 卡片顶部到此位置开始淡出 - final double fadeEndY; // 卡片顶部到此位置完全消失 + final bool isBound; + final double fadeStartY; + final double fadeEndY; + final VoidCallback onTap; const _FadeOnScrollCard({ super.key, required this.product, + required this.isBound, required this.fadeStartY, required this.fadeEndY, + required this.onTap, }); @override @@ -279,7 +295,11 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> { return Opacity( key: _posKey, opacity: opacity, - child: _ProductCard(product: widget.product), + child: _ProductCard( + product: widget.product, + isBound: widget.isBound, + onTap: widget.onTap, + ), ); } } @@ -287,22 +307,20 @@ class _FadeOnScrollCardState extends State<_FadeOnScrollCard> { /// 产品卡片 class _ProductCard extends StatelessWidget { final Map product; + final bool isBound; + final VoidCallback onTap; - const _ProductCard({super.key, required this.product}); + const _ProductCard({ + super.key, + required this.product, + required this.isBound, + required this.onTap, + }); @override Widget build(BuildContext context) { - bool isSelected = product['selected'] == true; - return GestureDetector( - onTap: () { - if (isSelected) { - Navigator.of(context).pop(); - } else { - AppToast.show( - context, '${product['name']} 离线或未配对', isError: true); - } - }, + onTap: onTap, child: Container( height: 120, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 20), @@ -343,29 +361,27 @@ class _ProductCard extends StatelessWidget { const SizedBox(height: 6), Row( children: [ - if ((product['statusColor'] as Color) != - Colors.transparent) - Container( - width: 7, - height: 7, - margin: const EdgeInsets.only(right: 6), - decoration: BoxDecoration( - color: product['id'] == 'capybara' - ? const Color(0xFF34D399) - : Colors.white.withOpacity(0.5), - shape: BoxShape.circle, - boxShadow: product['id'] == 'capybara' - ? [ - BoxShadow( - color: const Color(0xFF34D399) - .withOpacity(0.4), - spreadRadius: 2) - ] - : [], - ), + Container( + width: 7, + height: 7, + margin: const EdgeInsets.only(right: 6), + decoration: BoxDecoration( + color: isBound + ? const Color(0xFF34D399) + : Colors.white.withOpacity(0.5), + shape: BoxShape.circle, + boxShadow: isBound + ? [ + BoxShadow( + color: const Color(0xFF34D399) + .withOpacity(0.4), + spreadRadius: 2) + ] + : [], ), + ), Text( - product['status'], + isBound ? '已连接' : '点击配对', style: TextStyle( fontSize: 13, color: Colors.white.withOpacity(0.85)), diff --git a/airhub_app/lib/pages/settings_page.dart b/airhub_app/lib/pages/settings_page.dart index a1ca3c1..94111ee 100644 --- a/airhub_app/lib/pages/settings_page.dart +++ b/airhub_app/lib/pages/settings_page.dart @@ -2,7 +2,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; -import 'product_selection_page.dart'; import '../widgets/glass_dialog.dart'; import '../widgets/animated_gradient_background.dart'; import '../widgets/ios_toast.dart'; @@ -494,10 +493,7 @@ class _SettingsPageState extends ConsumerState { .unbindDevice(_userDeviceId!); if (mounted) { if (success) { - Navigator.pop(context); // close settings - Navigator.of(context).pushReplacement( - MaterialPageRoute(builder: (context) => const ProductSelectionPage()), - ); + context.go('/product-selection'); } else { AppToast.show(context, '解绑失败', isError: true); } diff --git a/airhub_app/pubspec.lock b/airhub_app/pubspec.lock index f8b4f0a..0f96872 100644 --- a/airhub_app/pubspec.lock +++ b/airhub_app/pubspec.lock @@ -132,10 +132,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" checked_yaml: dependency: transitive description: @@ -644,14 +644,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" - js: - dependency: transitive - description: - name: js - sha256: "53385261521cc4a0c4658fd0ad07a7d14591cf8fc33abbceae306ddb974888dc" - url: "https://pub.dev" - source: hosted - version: "0.7.2" json_annotation: dependency: "direct main" description: @@ -736,18 +728,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.18" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -1205,26 +1197,26 @@ packages: dependency: transitive description: name: test - sha256: "75906bf273541b676716d1ca7627a17e4c4070a3a16272b7a3dc7da3b9f3f6b7" + sha256: "54c516bbb7cee2754d327ad4fca637f78abfc3cbcc5ace83b3eda117e42cd71a" url: "https://pub.dev" source: hosted - version: "1.26.3" + version: "1.29.0" test_api: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.9" test_core: dependency: transitive description: name: test_core - sha256: "0cc24b5ff94b38d2ae73e1eb43cc302b77964fbf67abad1e296025b78deb53d0" + sha256: "394f07d21f0f2255ec9e3989f21e54d3c7dc0e6e9dbce160e5a9c1a6be0e2943" url: "https://pub.dev" source: hosted - version: "0.6.12" + version: "0.6.15" typed_data: dependency: transitive description: -- 2.47.2