From 786d11c9360436dc33b2810fac918e64db2bb6a0 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Wed, 4 Mar 2026 16:33:56 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E5=AE=8C=E6=88=90=E5=B0=81=E8=A3=85?= =?UTF-8?q?=E6=89=93=E5=8D=B0=E5=92=8C=E6=8E=A8=E9=80=81=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=96=B9=E6=B3=95=E5=92=8C=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=20=E5=AE=8C=E6=88=90=E6=B7=BB=E5=8A=A0=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E8=AE=BE=E5=A4=87=E6=8E=A5=E5=8F=A3=E7=9A=84=E6=97=A5=E5=BF=97?= =?UTF-8?q?=E4=BE=BF=E4=BA=8E=E8=BF=BD=E6=BA=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/core/env/env_config.dart | 4 +- .../logging/sentry_logger_impl.dart | 71 +++++++++-- lib/core/logging/i_logger_service.dart | 5 + lib/core/network/dio_client.dart | 116 +++++++++++++++--- .../auth/presentation/bloc/auth_cubit.dart | 7 ++ .../impl/device_http_datasource_impl.dart | 61 ++++++++- 6 files changed, 239 insertions(+), 25 deletions(-) diff --git a/lib/core/env/env_config.dart b/lib/core/env/env_config.dart index 5859fc23..6a682e47 100644 --- a/lib/core/env/env_config.dart +++ b/lib/core/env/env_config.dart @@ -7,9 +7,9 @@ class EnvConfig { static String get sentryDsn { // 根据环境切换 DSN(去 Sentry 后台创建两个项目:Test 和 Prod) if (environment == 'prod') { - return 'https://your_prod_dsn@sentry.io/123'; + return 'https://d5fdda78335793935593efa3f4ed11bb@o4510984107261952.ingest.de.sentry.io/4510984527937616'; } - return 'https://your_test_dsn@sentry.io/456'; + return 'https://1a72051f8ac6c21cde96ded18a318217@o4510984107261952.ingest.de.sentry.io/4510984468824144'; } static bool get isProduction => environment == 'prod'; diff --git a/lib/core/infrastructure/logging/sentry_logger_impl.dart b/lib/core/infrastructure/logging/sentry_logger_impl.dart index 91ae2199..14bcee3a 100644 --- a/lib/core/infrastructure/logging/sentry_logger_impl.dart +++ b/lib/core/infrastructure/logging/sentry_logger_impl.dart @@ -10,17 +10,26 @@ import 'package:intl/intl.dart'; class SentryLoggerImpl implements ILoggerService { File? _localLogFile; + //调试模式开关 + bool _isDebugModeEnabled = false; @override Future init() async { - // await SentryFlutter.init((options) { - // options.dsn = EnvConfig.sentryDsn; - // options.environment = EnvConfig.environment; - // // 生产环境只采样 20% 的性能数据,测试环境全开 - // options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0; - // }); - await _collectDeviceInfo(); + _isDebugModeEnabled = false; + await SentryFlutter.init((options) { + options.dsn = EnvConfig.sentryDsn; + options.environment = EnvConfig.environment; + // 生产环境只采样 20% 的性能数据,测试环境全开 + options.tracesSampleRate = EnvConfig.isProduction ? 0.2 : 1.0; + // 动态设置 Sentry 接收的日志级别 + // 如果开启调试模式,接收 DEBUG;否则生产环境只接收 INFO 以上 + + options.debug = _isDebugModeEnabled; // 开启 SDK 内部调试 + + }); + + await _collectDeviceInfo(); await _prepareLocalFile(); } @@ -93,6 +102,54 @@ class SentryLoggerImpl implements ILoggerService { // 本地写入失败不应崩溃 } } + @override + void logWithLevel( + String message, { + String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR' + Map? data, + }) { + final timestamp = DateTime.now().toIso8601String(); + final logEntry = "[$timestamp] [$level] $message ${data ?? ''}\n"; + + // --- 策略判断 --- + // 1. 开发环境:全部允许 + // 2. 生产环境: + // - 如果是 DEBUG 级别:只有 _isDebugModeEnabled 为 true 时才允许 + // - 其他级别:始终允许 + bool shouldProcess = !EnvConfig.isProduction || + level != 'DEBUG' || + _isDebugModeEnabled; + + if (shouldProcess) { + // 写入本地文件 + _writeToLocalFile(logEntry); + + // 上报 Sentry + SentryLevel sentryLevel; + switch (level) { + case 'DEBUG': sentryLevel = SentryLevel.debug; break; + case 'WARNING': sentryLevel = SentryLevel.warning; break; + case 'ERROR': sentryLevel = SentryLevel.error; break; + default: sentryLevel = SentryLevel.info; + } + + // 使用 addBreadcrumb 记录轨迹 + Sentry.addBreadcrumb(Breadcrumb( + message: message, + level: sentryLevel, + data: data, + )); + + // 如果是 ERROR,额外捕获为一个事件 + if (level == 'ERROR') { + Sentry.captureMessage(message, level: sentryLevel); + } + } + + if (kDebugMode) { + debugPrint(logEntry); + } + } // 增加一个获取本地日志文件路径的方法,方便你以后做“导出日志”功能 Future getLocalLogPath() async { diff --git a/lib/core/logging/i_logger_service.dart b/lib/core/logging/i_logger_service.dart index 6314cb86..3f23e0da 100644 --- a/lib/core/logging/i_logger_service.dart +++ b/lib/core/logging/i_logger_service.dart @@ -6,6 +6,11 @@ abstract class ILoggerService { bool isWarning = false, Map? data, }); + void logWithLevel( + String message, { + String level = 'INFO', // 'DEBUG', 'INFO', 'WARNING', 'ERROR' + Map? data, + }); void addBreadcrumb(String message, {String? category}); void setUser(String userId); } diff --git a/lib/core/network/dio_client.dart b/lib/core/network/dio_client.dart index 22c2ae85..b91d6339 100644 --- a/lib/core/network/dio_client.dart +++ b/lib/core/network/dio_client.dart @@ -1,8 +1,12 @@ import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; +import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; import '../app/app_user_cubit.dart'; import '../di/injection.dart'; +import '../infrastructure/logging/sentry_logger_impl.dart'; +import '../logging/i_logger_service.dart'; class DioClient { static Dio create() { @@ -14,26 +18,110 @@ class DioClient { headers: {'Content-Type': 'application/json'}, ), ); - dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); - dio.interceptors.add( - InterceptorsWrapper( - onRequest: (options, handler) { - // 1. 从 GetIt 中获取全局的 AppUserCubit + if (kDebugMode) { + dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); + } + // dio.interceptors.add(LogInterceptor(requestBody: true, responseBody: true)); + // dio.interceptors.add( + // InterceptorsWrapper( + // onRequest: (options, handler) { + // // 1. 从 GetIt 中获取全局的 AppUserCubit + // final userCubit = sl(); + // + // // 2. 从 Cubit 的状态中提取 Token + // final token = userCubit.state.user?.token; + // + // // 3. 如果 Token 存在,则添加到请求头 + // if (token != null && token.isNotEmpty) { + // options.headers['Authorization'] = 'Bearer $token'; + // } + // + // // 继续发送请求 + // return handler.next(options); + // }, + // ), + // ); + // return dio; + // } + dio.interceptors.add(InterceptorsWrapper( + onError: (DioException error, ErrorInterceptorHandler handler) async { + // 捕获所有网络异常 (超时、无网络、DNS 错误等) + await _reportToSentry(error); + return handler.next(error); + }, + onResponse: (Response response, ResponseInterceptorHandler handler) async { + // 捕获业务错误 (如 HTTP 4xx, 5xx) + if (response.statusCode != null && response.statusCode! >= 400) { + await _reportHttpError(response); + } + return handler.next(response); + }, + onRequest: (RequestOptions options, RequestInterceptorHandler handler) { + // 原有 Token 逻辑保持不变 + try { final userCubit = sl(); - - // 2. 从 Cubit 的状态中提取 Token final token = userCubit.state.user?.token; - - // 3. 如果 Token 存在,则添加到请求头 if (token != null && token.isNotEmpty) { options.headers['Authorization'] = 'Bearer $token'; } + } catch (e) { + // 防止获取 Token 时出错阻断请求 + debugPrint('获取 Token 失败: $e'); + } + return handler.next(options); + }, + )); - // 继续发送请求 - return handler.next(options); - }, - ), - ); return dio; } +// 上报 HTTP 状态码错误 (404, 500 等) + static Future _reportHttpError(Response response) async { + try { + final logger = GetIt.I() as SentryLoggerImpl; + + // 记录详细的错误上下文 + logger.logWithLevel( + 'HTTP 请求失败: ${response.statusCode}', + level: 'ERROR', + data: { + 'url': response.requestOptions.uri.toString(), + 'method': response.requestOptions.method, + 'statusCode': response.statusCode, + 'responseData': response.data, // 注意:如果响应包含敏感信息,请脱敏 + }, + ); + + // 可选:如果希望每个 4xx/5xx 都生成一个独立 Issue,取消下面注释 + // await Sentry.captureMessage( + // 'HTTP Error ${response.statusCode}: ${response.requestOptions.path}', + // level: SentryLevel.error, + // extra: {'data': response.data}, + // ); + } catch (e) { + debugPrint('Sentry 上报 HTTP 错误失败: $e'); + } + } + + // 上报网络异常 (超时、断开等) + static Future _reportToSentry(DioException error) async { + try { + final logger = GetIt.I() as SentryLoggerImpl; + + logger.logWithLevel( + '网络异常: ${error.type}', + level: 'ERROR', + data: { + 'url': error.requestOptions.uri.toString(), + 'method': error.requestOptions.method, + 'message': error.message, + 'type': error.type.name, + }, + ); + + // 捕获完整堆栈 + logger.captureException(error, stackTrace: error.stackTrace); + } catch (e) { + debugPrint('Sentry 上报网络异常失败: $e'); + } + } } diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index 7762739a..6a488b72 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -1,11 +1,14 @@ import 'dart:async'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart'; import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart'; import '../../../../core/app/app_user_cubit.dart'; +import '../../../../core/infrastructure/logging/sentry_logger_impl.dart'; +import '../../../../core/logging/i_logger_service.dart'; import '../../../../core/network/tcp/tcp_client.dart'; import '../../../../core/storage/user_storage.dart'; import '../../../devices/domain/usecases/get_user_device_usecase.dart'; @@ -51,7 +54,9 @@ class AuthCubit extends Cubit { /// 当 HTTP 登录/注册成功后调用 Future loginSuccess(UserEntity user) async { + final logger = GetIt.I() as SentryLoggerImpl; await storage.saveUser(user); + logger.setUser(user.userId); await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); await _authTcpDatasource.sendAuthPacket();//包括发送认证包和获取列表和切换函数 tcp.startHeartbeat(interval: const Duration(seconds: 4)); //启动心跳 @@ -62,6 +67,8 @@ class AuthCubit extends Cubit { /// 退出登录 (主动或被动) Future logout() async { print("退出登录"); + final logger = GetIt.I() as SentryLoggerImpl; + logger.setUser('');// 清除用户信息 await storage.deleteUser(); tcp.disconnect(); appCubit.clearAuth(); diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index f8831468..bc6bccb6 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -1,9 +1,12 @@ import 'package:dio/dio.dart'; import 'package:flutter/widgets.dart'; +import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; import 'package:maibu_satabot_v2/features/devices/data/datasources/device_http_datasource.dart'; import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart'; +import '../../../../../core/infrastructure/logging/sentry_logger_impl.dart'; +import '../../../../../core/logging/i_logger_service.dart'; import '../../../../../core/storage/user_storage.dart'; import '../../../domain/entities/device_location_entity.dart'; @@ -77,7 +80,15 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future switchDevice(String platform, String deviceId) async { + final logger = GetIt.I() as SentryLoggerImpl; final token = await _getToken(); + print('用户点击switchDevice方法开始触发: $token'); + logger.logWithLevel('用户点击switchDevice方法开始触发', level: 'DEBUG'); + logger.logWithLevel( + '用户点击switchDevice方法API 请求开始', + level: 'INFO', + data: {'url': 'https://serviceri.satabot.com/iot/device/switchDevice'} + ); var response = await dio.post( HttpApiConsts.switchDevice, data: {'platform': platform, 'deviceId': deviceId}, @@ -88,16 +99,38 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { }, ), ); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/switchDevice', + 'platform': platform, + 'deviceId': deviceId + } + ); if (response.statusCode != 200) { + logger.logWithLevel( + 'API 请求失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; - + logger.logWithLevel( + 'API 响应成功', + level: 'INFO', + data: {'statusCode': response.statusCode, 'data': response.data} + ); if (responseData['code'] != 200 || responseData['data'] != true) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception(responseData['msg'] ?? '业务异常'); } - return 1; } @@ -142,9 +175,19 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future getDeviceLocation(String deviceame) async { + final logger = GetIt.I() as SentryLoggerImpl; // TODO: implement getDeviceLocation try { // 发起 GET 请求 + logger.logWithLevel('用户点击getDeviceLocation方法开始触发', level: 'INFO'); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/userDevice', + 'tenantName': deviceame + } + ); final response = await dio.get( 'https://serviceri.satabot.com/iot/device/userDevice', queryParameters: {'tenantName': deviceame}, @@ -159,21 +202,35 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { // 检查响应状态码 if (response.statusCode == 200) { final data = response.data; + logger.logWithLevel( + 'API 响应接收', + level: 'DEBUG', + data: {'statusCode': response.statusCode, 'data': response.data} + ); if (data['code'] == 200) { //return data['data']; // 返回设备数据 final locationData = data['data']; // 假设数据结构,测试时候如有不妥就修改结果的实例化 + logger.logWithLevel( + '成功获取设备位置', + level: 'INFO', + data: {'lat': locationData['latitude'], 'lng': locationData['longitude']} + ); return DeviceLocationEntity( deviceName: locationData['deviceName'], latitude: locationData['latitude'], longitude: locationData['longitude'], ); } else { + logger.logWithLevel(data['msg'] ?? '业务异常', level: 'ERROR'); throw Exception(data['msg'] ?? '业务异常'); } } else { + logger.logWithLevel('网络请求失败: ${response.statusCode}', level: 'ERROR'); throw Exception('网络请求失败: ${response.statusCode}'); } } catch (e) { + logger.logWithLevel('获取设备位置失败: $e', level: 'ERROR'); + logger.captureException(e, stackTrace: e); throw Exception('获取设备位置失败: $e'); } From 1cff8f9ee2236cc8cf5681dc93e0d30634b04fc7 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Wed, 4 Mar 2026 17:36:28 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=89=AB=E7=A0=81?= =?UTF-8?q?=E7=BB=91=E5=AE=9A=E8=AE=BE=E5=A4=87-=E5=88=9D=E6=AD=A5?= =?UTF-8?q?=E6=B5=8B=E8=AF=95=E6=AD=A3=E7=A1=AE=E5=AE=8C=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../impl/device_http_datasource_impl.dart | 26 +++ .../presentation/bloc/devices_cubit.dart | 24 ++- .../web_view/user_info_pages.dart | 159 ++++++++++++------ pubspec.lock | 4 +- pubspec.yaml | 2 +- 5 files changed, 157 insertions(+), 58 deletions(-) diff --git a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart index bc6bccb6..11f4204a 100644 --- a/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart +++ b/lib/features/devices/data/datasources/impl/device_http_datasource_impl.dart @@ -26,17 +26,43 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { @override Future bindDevice(String deviceId, String deviceAlias) async { + final logger = GetIt.I() as SentryLoggerImpl; + logger.logWithLevel('用户点击bindDevice方法开始触发', level: 'INFO'); + logger.logWithLevel( + '用户点击bindDevice方法API 请求开始', + level: 'INFO', + data: {'url': 'https://serviceri.satabot.com/iot/device/bindDevice'} + ); var response = await dio.post( HttpApiConsts.bindDevice, data: {'deviceId': deviceId, 'deviceAlias': deviceAlias}, ); + logger.logWithLevel( + '请求详细参数', + level: 'DEBUG', + data: { + 'url': 'https://serviceri.satabot.com/iot/device/bindDevice', + 'deviceId': deviceId, + 'deviceAlias': deviceAlias + } + ); if (response.statusCode != 200) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception('网络请求失败:${response.statusCode}'); } final responseData = response.data; if (responseData['code'] != 200 || responseData['data'] != true) { + logger.logWithLevel( + 'API 响应失败', + level: 'ERROR', + data: {'statusCode': response.statusCode, 'data': response.data} + ); throw Exception(responseData['msg'] ?? '业务异常'); } diff --git a/lib/features/devices/presentation/bloc/devices_cubit.dart b/lib/features/devices/presentation/bloc/devices_cubit.dart index 96482e2b..c855f13d 100644 --- a/lib/features/devices/presentation/bloc/devices_cubit.dart +++ b/lib/features/devices/presentation/bloc/devices_cubit.dart @@ -180,16 +180,21 @@ class DevicesCubit extends Cubit { } /// 绑定设备 Future bindDevice(String deviceId, String deviceAlias) async { + emit(state.copyWith(isLoading: true, errorMessage: '')); try { final params = BindDeviceParams(deviceId, deviceAlias); final result = await _bindDeviceUseCase.call(params); result.fold( // 失败处理 - (failure) => emit(state.copyWith( - isLoading: false, - errorMessage: failure.message ?? '绑定设备失败', - )), + (failure) { + emit(state.copyWith( + isLoading: false, + errorMessage: failure.message ?? '绑定设备失败', + )); + // 抛出异常,携带后端返回的错误消息(如“设备不存在”) + throw Exception(failure.message ?? '绑定设备失败'); + }, // 成功处理(返回 int 状态码) (successCode) { if (successCode == 1) { @@ -201,14 +206,17 @@ class DevicesCubit extends Cubit { isLoading: false, errorMessage: '绑定失败:状态码 $successCode', )); + throw Exception('绑定失败:状态码 $successCode'); } }, ); } catch (e) { - emit(state.copyWith( - isLoading: false, - errorMessage: '绑定异常:${e.toString()}', - )); + // emit(state.copyWith( + // isLoading: false, + // errorMessage: '绑定异常:${e.toString()}', + // )); + // 🔥 重新抛出,传递给 UI + rethrow; } } diff --git a/lib/features/my/presentation/web_view/user_info_pages.dart b/lib/features/my/presentation/web_view/user_info_pages.dart index 184b07d3..a8484c05 100644 --- a/lib/features/my/presentation/web_view/user_info_pages.dart +++ b/lib/features/my/presentation/web_view/user_info_pages.dart @@ -1,10 +1,9 @@ -// user_info_card.dart import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart'; -import 'package:mobile_scanner/mobile_scanner.dart'; // 1. 导入扫码库 -import 'package:get_it/get_it.dart'; // 用于获取 Cubit 实例 -// 2. 导入你的 Cubit 文件,请根据实际路径调整 -import '../../../auth/presentation/bloc/auth_cubit.dart'; +import 'package:mobile_scanner/mobile_scanner.dart'; +import 'package:get_it/get_it.dart'; +// 其他导入保持不变... class UserInfoCard extends StatelessWidget { final String userName; @@ -27,91 +26,157 @@ class UserInfoCard extends StatelessWidget { ), ), ), - SizedBox(width: 12), + const SizedBox(width: 12), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(userName, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), - Text('用户 id:$userId', style: TextStyle(fontSize: 12, color: Colors.grey)), + Text(userName, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Text('用户 id:$userId', style: const TextStyle(fontSize: 12, color: Colors.grey)), ], ), ), IconButton( - icon: Icon(Icons.edit), + icon: const Icon(Icons.edit), onPressed: () {}, ), - Spacer(), + const Spacer(), IconButton( - icon: Icon(Icons.add), - // 3. 绑定点击事件 + icon: const Icon(Icons.add), onPressed: () => _handleScanAndBind(context), ), ], ); } - // 4. 定义扫码与绑定逻辑 Future _handleScanAndBind(BuildContext context) async { - // 跳转到扫码页面 - final String? qrCode = await Navigator.push( + bool isScanned = false; + + // 1. 扫码逻辑 (保持不变) + final String? deviceCode = await Navigator.push( context, MaterialPageRoute( - builder: (context) => MobileScanner( - onDetect: (capture) { - final List barcodes = capture.barcodes; - if (barcodes.isNotEmpty) { - debugPrint('识别到的数据: $barcodes'); - final String? code = barcodes.first.rawValue; - if (code != null && code.isNotEmpty) { - // 识别成功,立即返回结果并关闭页面 - debugPrint('扫码成功,识别到的数据: $code'); - // Navigator.pop(context, code); + builder: (context) => Scaffold( + backgroundColor: Colors.transparent, + appBar: AppBar( + title: const Text('扫码绑定设备'), + backgroundColor: Colors.black54, + leading: IconButton( + icon: const Icon(Icons.close), + onPressed: () => Navigator.pop(context), + ), + ), + body: MobileScanner( + onDetect: (BarcodeCapture capture) { + if (isScanned) return; + final List barcodes = capture.barcodes; + if (barcodes.isNotEmpty) { + final String? code = barcodes.first.rawValue; + if (code != null && code.isNotEmpty) { + isScanned = true; + // 使用 addPostFrameCallback 确保在帧渲染后执行,避免锁死 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted && Navigator.canPop(context)) { + Navigator.pop(context, code); + } + }); + } } - } - }, + }, + ), ), ), ); - // 如果用户取消或未扫到码,直接返回 - if (qrCode == null || qrCode.isEmpty) { + if (deviceCode == null || deviceCode.isEmpty) { return; } - // 显示加载对话框 + // 2. 确认对话框 (保持不变) + final bool? confirmBind = await showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: const Text('确认绑定设备'), + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('设备信息:'), + const SizedBox(height: 8), + Text('设备编码:$deviceCode'), + const Text('设备状态:未绑定'), + const Text('设备类型:智能设备'), + ], + ), + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + child: const Text('取消'), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context, true), + child: const Text('确定绑定'), + style: ElevatedButton.styleFrom(backgroundColor: Colors.blue), + ), + ], + ), + ); + + if (confirmBind != true) { + return; + } + + // 3. 显示加载框 + // 保存 BuildContext 引用,防止后续 context 变化导致找不到正确的 Navigator showDialog( context: context, barrierDismissible: false, - builder: (context) => Center(child: CircularProgressIndicator()), + builder: (loadingContext) => const Center(child: CircularProgressIndicator()), ); try { - // 5. 获取 Cubit 实例并调用绑定接口 - final Cubit = GetIt.I(); + final devicesCubit = GetIt.I(); + await devicesCubit.bindDevice(deviceCode, ''); - // 执行绑定逻辑 - //await Cubit.bindDevice(qrCode); - - // 关闭加载框 - if (context.mounted) Navigator.pop(context); - - // 成功提示 - if (context.mounted) { + // ✅ 成功:安全关闭加载框 + if (context.mounted && Navigator.canPop(context)) { + Navigator.pop(context); ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('设备绑定成功!'), backgroundColor: Colors.green), + const SnackBar(content: Text('设备绑定成功!'), backgroundColor: Colors.green), ); } } catch (e) { - // 关闭加载框 - if (context.mounted) Navigator.pop(context); + // ✅ 失败:安全关闭加载框 (关键修复点) + // 即使发生了 Framework 错误,也要尝试关闭弹窗 + if (context.mounted && Navigator.canPop(context)) { + Navigator.pop(context); - // 失败提示 - if (context.mounted) { + // 显示具体错误信息 ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('绑定失败:$e'), backgroundColor: Colors.red), + SnackBar( + content: Text('绑定失败:${e.toString()}'), + backgroundColor: Colors.red, + duration: const Duration(seconds: 4), + ), ); + } else { + // 极端情况:如果 Navigator 已经锁死无法 pop,尝试用其他方式提示用户 + // 或者等待下一帧再试 + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + // 尝试再次关闭,或者只显示错误提示 + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('绑定失败:${e.toString()} (请手动返回)'), + backgroundColor: Colors.orange, + ), + ); + } + }); } + debugPrint('设备绑定失败详情:$e'); } } + } diff --git a/pubspec.lock b/pubspec.lock index 5c1e32e1..e453798d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -811,10 +811,10 @@ packages: dependency: "direct dev" description: name: mobile_scanner - sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3" + sha256: "827765afbd4792ff3fd105ad593821ac0f6d8a7d352689013b07ee85be336312" url: "https://pub.flutter-io.cn" source: hosted - version: "3.5.7" + version: "4.0.1" native_toolchain_c: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 21c2ee9d..3bbf4a35 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -143,7 +143,7 @@ dev_dependencies: flutter_launcher_icons: ^0.13.1 - mobile_scanner: ^3.4.1 # 请根据实际需求选择最新版本 + mobile_scanner: ^4.0.1 # 升级到最新稳定版 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec