diff --git a/lib/components/capsule_toast.dart b/lib/components/capsule_toast.dart new file mode 100644 index 00000000..edc1c83b --- /dev/null +++ b/lib/components/capsule_toast.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; + +/// 胶囊样式全局 Toast:体积小、大圆角、清爽,数秒后自动消失。 +/// 通过根 Overlay 展示,不依赖调用方 BuildContext 和 Navigator, +/// 适用于页面销毁(dispose)后仍需弹出提示的场景。 +class CapsuleToast { + static OverlayEntry? _entry; + static bool _showing = false; + + /// 显示胶囊提示 + /// [message] 提示文案 + /// [duration] 显示时长,默认 2 秒 + /// [showCheck] 是否显示对勾图标 + static void show( + String message, { + Duration duration = const Duration(seconds: 2), + bool showCheck = true, + }) { + // 从根 Element 向下遍历子树,查找第一个 OverlayState + // (Overlay 是根的子节点,不能用 findAncestorStateOfType 向上找), + // 无需依赖调用方 context,页面销毁后也能正常弹出。 + final root = WidgetsBinding.instance.rootElement; + if (root == null) return; + final overlay = _findOverlayState(root); + if (overlay == null) { + debugPrint('💊 [CapsuleToast] 未找到 Overlay,无法弹出: $message'); + return; + } + debugPrint('💊 [CapsuleToast] 弹出: $message'); + + // 延到下一帧再插入:页面退出(dispose)瞬间 Overlay 可能正在重建, + // 直接插入可能随旧页面一起被销毁。 + WidgetsBinding.instance.addPostFrameCallback((_) { + _insertEntry(message, overlay, duration: duration, showCheck: showCheck); + }); + } + + /// 真正插入 OverlayEntry + static void _insertEntry( + String message, + OverlayState overlay, { + required Duration duration, + required bool showCheck, + }) { + if (!overlay.mounted) return; + + // 有新的提示时先移除旧的 + if (_showing) { + _entry?.remove(); + _showing = false; + } + + _entry = OverlayEntry( + builder: (context) => IgnorePointer( + child: SafeArea( + child: Align( + alignment: Alignment.topCenter, + child: Container( + margin: const EdgeInsets.only(top: 64), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xE62C2C2C), + borderRadius: BorderRadius.circular(100), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.15), + blurRadius: 8, + offset: const Offset(0, 2), + ), + ], + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (showCheck) ...[ + const Icon( + Icons.check_circle, + size: 14, + color: Color(0xFF4CD964), + ), + const SizedBox(width: 6), + ], + Text( + message, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + ), + ), + ), + ); + + overlay.insert(_entry!); + _showing = true; + + Future.delayed(duration, () { + if (_showing) { + _entry?.remove(); + _showing = false; + } + }); + } + + /// 从指定 Element 开始深度优先遍历子树,找到第一个 OverlayState + static OverlayState? _findOverlayState(Element root) { + OverlayState? result; + void visitor(Element element) { + if (result != null) return; + if (element is StatefulElement && element.state is OverlayState) { + result = element.state as OverlayState; + return; + } + element.visitChildElements(visitor); + } + + root.visitChildElements(visitor); + return result; + } +} diff --git a/lib/core/consts/http_api_consts.dart b/lib/core/consts/http_api_consts.dart index 41b6f8b2..cdd61f6d 100644 --- a/lib/core/consts/http_api_consts.dart +++ b/lib/core/consts/http_api_consts.dart @@ -1,7 +1,8 @@ class HttpApiConsts { // static const String baseUrl = "http://8.159.134.0:8012"; // 旧地址 // static const String baseUrl = "http://1.95.137.212:59015"; // 测试地址 - static const String baseUrl = "http://1.95.137.212:8081"; // 生产地址 + // static const String baseUrl = "http://1.95.137.212:8081"; // 生产地址(暂时切到测试服 59003,回退时恢复此行) + static const String baseUrl = "http://1.95.137.212:59003"; // 测试服地址 /// 账号相关 // 登录 @@ -65,7 +66,7 @@ class HttpApiConsts { static const String flightTaskCommand = "$baseUrl/iot/UAV/flightTaskCommand"; /// 告警相关 - static const String alarmBaseUrl = "http://1.95.137.212:8081"; + static const String alarmBaseUrl = baseUrl; // 获取告警工单配置列表 static const String alarmOrderConfigList = "$alarmBaseUrl/iot/alarmOrderConfig/list"; @@ -104,6 +105,10 @@ class HttpApiConsts { // 设备操作权限校验 static const String hasPermission = "$baseUrl/iot/device/hasPermission"; + // 释放远程控制权限(请求体:platform/type/deviceId/token) + static const String releaseControlUrl = + "$baseUrl/forward/device/releaseControl"; + /// 用户相关 // 获取用户列表 static const String systemUserList = "$baseUrl/system/user/list"; diff --git a/lib/core/env/env_config.dart b/lib/core/env/env_config.dart index c6bee51b..66a7dff8 100644 --- a/lib/core/env/env_config.dart +++ b/lib/core/env/env_config.dart @@ -18,10 +18,10 @@ class EnvConfig { static String get tcpIp => '1.95.137.212'; static int get tcpPort { - // 测试服: 59016, 生产服: 9001 + // 测试服: 59016, 生产服: 9001(目前暂时都走测试服 59004,回退时改回 9001) if (environment == 'prod') { - return 9001; + return 59004; } - return 9001; // TCP 端口 + return 59004; // TCP 端口 } } diff --git a/lib/core/utils/image_url_util.dart b/lib/core/utils/image_url_util.dart new file mode 100644 index 00000000..b27ef6f5 --- /dev/null +++ b/lib/core/utils/image_url_util.dart @@ -0,0 +1,24 @@ +import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart'; + +/// 图片地址工具:后端返回的图片路径可能是相对路径(如 /profile/avatar/xxx.jpg), +/// 直接交给 Image.network 会因缺少 host 抛 "No host specified in URI" 异常。 +/// 此工具负责将相对路径拼接成完整的服务器地址。 +class ImageUrlUtil { + ImageUrlUtil._(); + + /// 补全图片地址 + /// - 已是完整 http(s) 地址:原样返回 + /// - 以 / 开头的相对路径:拼接 baseUrl + /// - 其他相对路径:拼接 baseUrl + / + /// - null 或空:返回 null + static String? resolve(String? path) { + if (path == null || path.isEmpty) return null; + if (path.startsWith('http://') || path.startsWith('https://')) { + return path; + } + if (path.startsWith('/')) { + return '${HttpApiConsts.baseUrl}$path'; + } + return '${HttpApiConsts.baseUrl}/$path'; + } +} diff --git a/lib/features/auth/presentation/pages/login_page.dart b/lib/features/auth/presentation/pages/login_page.dart index 1cfb1313..d285ee67 100644 --- a/lib/features/auth/presentation/pages/login_page.dart +++ b/lib/features/auth/presentation/pages/login_page.dart @@ -93,7 +93,7 @@ class _LoginPageState extends State { const SizedBox(height: 16), Text( - "账号登录v1.1.5", + "账号登录", style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: context.appColors.textPrimary), ), const SizedBox(height: 8), 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 0d5148a3..0f4a39f5 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 @@ -34,7 +34,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { logger.logWithLevel( '用户点击bindDevice方法API 请求开始', level: 'INFO', - data: {'url': 'http://1.95.137.212:8081/iot/device/bindDevice'}, + data: {'url': 'http://1.95.137.212:59003/iot/device/bindDevice'}, ); var response = await dio.post( HttpApiConsts.bindDevice, @@ -44,7 +44,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { '请求详细参数', level: 'DEBUG', data: { - 'url': 'http://1.95.137.212:8081/iot/device/bindDevice', + 'url': 'http://1.95.137.212:59003/iot/device/bindDevice', 'deviceId': deviceId, 'deviceAlias': deviceAlias, }, @@ -122,7 +122,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { logger.logWithLevel( '用户点击switchDevice方法API 请求开始', level: 'INFO', - data: {'url': 'http://1.95.137.212:8081/iot/device/switchDevice'}, + data: {'url': 'http://1.95.137.212:59003/iot/device/switchDevice'}, ); var response = await dio.post( HttpApiConsts.switchDevice, @@ -138,7 +138,7 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { '请求详细参数', level: 'DEBUG', data: { - 'url': 'http://1.95.137.212:8081/iot/device/switchDevice', + 'url': 'http://1.95.137.212:59003/iot/device/switchDevice', 'platform': platform, 'deviceId': deviceId, }, @@ -219,12 +219,12 @@ class DeviceHttpDatasourceImpl implements DeviceHttpDatasource { '请求详细参数', level: 'DEBUG', data: { - 'url': 'http://1.95.137.212:8081/iot/device/userDevice', + 'url': 'http://1.95.137.212:59003/iot/device/userDevice', 'tenantName': deviceame, }, ); final response = await dio.get( - 'http://1.95.137.212:8081/iot/device/userDevice', + 'http://1.95.137.212:59003/iot/device/userDevice', queryParameters: {'tenantName': deviceame}, // options: Options( // headers: { diff --git a/lib/features/devices/data/repositories/device_hostriry_work_impl.dart b/lib/features/devices/data/repositories/device_hostriry_work_impl.dart index f2d9fee7..11a528f7 100644 --- a/lib/features/devices/data/repositories/device_hostriry_work_impl.dart +++ b/lib/features/devices/data/repositories/device_hostriry_work_impl.dart @@ -35,7 +35,7 @@ class DeviceHostrityWorkRepositoryImpl implements DeviceHostrityWorkRepositoryRe _logger.logWithLevel('用户信息获取成功,Token: $token', level: 'info'); final response = await client.get( - 'http://1.95.137.212:8081/iot/device/getDeviceRunStatistics?deviceId=$deviceId', + 'http://1.95.137.212:59003/iot/device/getDeviceRunStatistics?deviceId=$deviceId', options: Options(headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'}), ); diff --git a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart index ab4c723b..68f1b83a 100644 --- a/lib/features/devices/data/repositories/generate_path_repository_Impl.dart +++ b/lib/features/devices/data/repositories/generate_path_repository_Impl.dart @@ -47,7 +47,7 @@ class PathRepositoryImpl implements PathRepository { required String userId, required String jsonData, }) async { - final url = Uri.parse('http://1.95.137.212:8081/iot/workRecord/add'); + final url = Uri.parse('http://1.95.137.212:59003/iot/workRecord/add'); final headers = {'Content-Type': 'application/json'}; final body = jsonEncode({ 'workName': workName, @@ -74,7 +74,7 @@ class PathRepositoryImpl implements PathRepository { }) async { final timestamp = DateTime.now().millisecondsSinceEpoch; final url = Uri.parse( - 'http://1.95.137.212:8081/iot/workRecord/selectByUserId', + 'http://1.95.137.212:59003/iot/workRecord/selectByUserId', ).replace(queryParameters: {'userId': userId, '_t': timestamp.toString()}); try { @@ -103,7 +103,7 @@ class PathRepositoryImpl implements PathRepository { final timestamp = DateTime.now().millisecondsSinceEpoch; final url = Uri.parse( - 'http://1.95.137.212:8081/iot/workRecord/deleteByWorkName', + 'http://1.95.137.212:59003/iot/workRecord/deleteByWorkName', ).replace( queryParameters: {'workName': workName, '_t': timestamp.toString()}, ); @@ -132,7 +132,7 @@ class PathRepositoryImpl implements PathRepository { final timestamp = DateTime.now().millisecondsSinceEpoch; final url = Uri.parse( - 'http://1.95.137.212:8081/iot/workRecord/selectByWorkName', + 'http://1.95.137.212:59003/iot/workRecord/selectByWorkName', ).replace( queryParameters: {'workName': workName, '_t': timestamp.toString()}, ); @@ -268,7 +268,7 @@ class PathRepositoryImpl implements PathRepository { final timestamp = DateTime.now().millisecondsSinceEpoch; final url = Uri.parse( - 'http://1.95.137.212:8081/iot/workRecord/selectBySiteId', + 'http://1.95.137.212:59003/iot/workRecord/selectBySiteId', ).replace( queryParameters: { 'siteId': siteId.toString(), @@ -508,7 +508,7 @@ class PathRepositoryImpl implements PathRepository { } /// 鍒涘缓璁惧�浠诲姟锛堥€氳繃鎺ュ彛鎵ц�浣滀笟锛? - /// 鎺ュ彛鍦板潃: http://1.95.137.212:8081/iot/deviceTask/createDeviceTask + /// 鎺ュ彛鍦板潃: http://1.95.137.212:59003/iot/deviceTask/createDeviceTask /// 鍏ュ弬: {"deviceId":"...","routeId":76,"siteId":22,"orgId":5} /// 杩斿洖: 鍒涘缓鎴愬姛鐨勪换鍔�D @override @@ -531,7 +531,7 @@ class PathRepositoryImpl implements PathRepository { try { final response = await dio.post( - 'http://1.95.137.212:8081/iot/deviceTask/createDeviceTask', + 'http://1.95.137.212:59003/iot/deviceTask/createDeviceTask', data: body, ); diff --git a/lib/features/home/presentation/widgets/map/testmap_pages.dart b/lib/features/home/presentation/widgets/map/testmap_pages.dart index aaaff280..dee3581e 100644 --- a/lib/features/home/presentation/widgets/map/testmap_pages.dart +++ b/lib/features/home/presentation/widgets/map/testmap_pages.dart @@ -959,7 +959,7 @@ class _MapPageEnterpriseState extends State { final String workRecordJson = jsonEncode(workRecord); final http.MultipartRequest request = http.MultipartRequest( 'POST', - Uri.parse('http://1.95.137.212:8081/iot/workRecord/add'), + Uri.parse('http://1.95.137.212:59003/iot/workRecord/add'), ); // 🔥 添加 Authorization 认证头 @@ -1027,7 +1027,7 @@ class _MapPageEnterpriseState extends State { // debugPrint('📤 [保存地块] path长度: ${(savePath['path'] as List?)?.length ?? 0}'); // debugPrint('📤 [保存地块] outer长度: ${(savePath['outer'] as List?)?.length ?? 0}'); // debugPrint('📤 [保存地块] 是否有图片文件: ${imgBase64 != null && imgBase64.isNotEmpty}'); - // debugPrint('📤 [保存地块] 请求URL: http://1.95.137.212:8081/iot/workRecord/add'); + // debugPrint('📤 [保存地块] 请求URL: http://1.95.137.212:59003/iot/workRecord/add'); // debugPrint('📤 [保存地块] workRecord(完整JSON): $workRecordJson'); // debugPrint('📤 [保存地块] savePath(原始对象): $savePath'); // debugPrint('📤 [保存地块] ========== 请求参数 END =========='); @@ -2514,7 +2514,7 @@ class _MapPageEnterpriseState extends State { try { final dio = sl(); final response = await dio.get( - 'http://1.95.137.212:8081/iot/taskPath/getTaskPath', + 'http://1.95.137.212:59003/iot/taskPath/getTaskPath', queryParameters: { 'taskId': currentTaskId, }, diff --git a/lib/features/my/repository/my_repository_impl.dart b/lib/features/my/repository/my_repository_impl.dart index 5313721e..20e9ec32 100644 --- a/lib/features/my/repository/my_repository_impl.dart +++ b/lib/features/my/repository/my_repository_impl.dart @@ -26,7 +26,7 @@ class MyRepositoryImpl implements MyRepository { // 核心修复:严格匹配抽象类的方法签名 @override Future> updateName(String nickName) async { - final url = Uri.parse('http://1.95.137.212:8081/system/user/profile'); + final url = Uri.parse('http://1.95.137.212:59003/system/user/profile'); // 补充:从 UserStorage 获取 token(接口通常需要认证) final token = await _getToken(); diff --git a/lib/features/remote_control/data/datasources/remote_http_datasource.dart b/lib/features/remote_control/data/datasources/remote_http_datasource.dart index 15815057..4a21c10d 100644 --- a/lib/features/remote_control/data/datasources/remote_http_datasource.dart +++ b/lib/features/remote_control/data/datasources/remote_http_datasource.dart @@ -2,6 +2,7 @@ import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:get_it/get_it.dart'; +import '../../../../core/consts/http_api_consts.dart'; import '../../../../core/logging/i_logger_service.dart'; import '../../../../core/storage/user_storage.dart'; @@ -29,21 +30,29 @@ class RemoteHttpDatasource { /// 🔥 返回完整权限信息: {hasPermission, owner} Future> requestRemoteControlViaHttp(String deviceName, String platform) async { - print(deviceName+"@@@"); + final logPrefix = '🔑 [RemoteHttp][获取权限接口]'; final user = await _userStorage.getUser(); if (user == null) { debugPrint('❌ [RemoteHttp] 用户未登录,无法请求远程控制权限'); - _logger.log('❌ [RemoteHttp] 用户未登录,无法请求远程控制权限'); + _logger.logWithLevel('❌ [RemoteHttp] 用户未登录,无法请求远程控制权限', shouldLog: true); return {'hasPermission': false, 'owner': null}; } final token = user.token; - - debugPrint('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform'); - _logger.log('🔑 [RemoteHttp] 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform'); - + + debugPrint('$logPrefix 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform'); + _logger.logWithLevel( + '$logPrefix 开始请求远程控制权限 - deviceName: $deviceName, platform: $platform', + shouldLog: true, + ); + try { - debugPrint('📡 [RemoteHttp] 正在发送 HTTP POST 请求到 /forward/device/remoteControl'); - debugPrint('📡 [RemoteHttp] 请求参数: deviceId=$deviceName, platform=$platform'); + final requestData = {'deviceId': deviceName, 'platform': platform}; + debugPrint('$logPrefix 请求地址: POST /forward/device/remoteControl'); + debugPrint('$logPrefix 实际请求体: $requestData'); + _logger.logWithLevel( + '$logPrefix 请求地址: POST /forward/device/remoteControl, 请求体: $requestData', + shouldLog: true, + ); final response = await _dio.post( '/forward/device/remoteControl', options: Options( @@ -52,15 +61,14 @@ class RemoteHttpDatasource { 'Authorization': 'Bearer ${token}', }, ), - data: {'deviceId': deviceName, 'platform': platform}); - debugPrint('📥 [RemoteHttp] HTTP 请求已发送,等待响应...'); - - debugPrint("📊 [RemoteHttp] HTTP响应状态码: ${response.statusCode}"); - _logger.log("📊 [RemoteHttp] HTTP响应状态码: ${response.statusCode}"); - + data: requestData); + + debugPrint("$logPrefix HTTP响应状态码: ${response.statusCode}"); + _logger.logWithLevel("$logPrefix HTTP响应状态码: ${response.statusCode}", shouldLog: true); + final responseData = response.data as Map; - debugPrint("📊 [RemoteHttp] 完整响应数据:$responseData"); - _logger.log("📊 [RemoteHttp] 完整响应数据:$responseData"); + debugPrint("$logPrefix 完整响应数据:$responseData"); + _logger.logWithLevel("$logPrefix 完整响应数据:$responseData", shouldLog: true); // 🔥 关键:先获取响应的 data 字段,再获取 remoteControl 和 owner final dataField = responseData['data'] as Map?; @@ -68,54 +76,67 @@ class RemoteHttpDatasource { final bool hasRemoteControl = dataField['remoteControl'] as bool? ?? false; final String? owner = dataField['owner'] as String?; - debugPrint("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl, owner=$owner"); - _logger.log("✅ [RemoteHttp] 解析成功 - remoteControl=$hasRemoteControl, owner=$owner"); + debugPrint("$logPrefix 解析成功 - remoteControl=$hasRemoteControl, owner=$owner"); + _logger.logWithLevel( + "$logPrefix 解析成功 - remoteControl=$hasRemoteControl, owner=$owner", + shouldLog: true, + ); // 🔥 返回完整权限信息 return {'hasPermission': hasRemoteControl, 'owner': owner}; } else { - debugPrint("❌ [RemoteHttp] 缺少 data 字段,响应结构异常"); - _logger.log("❌ [RemoteHttp] 缺少 data 字段,响应结构异常"); + debugPrint("$logPrefix 缺少 data 字段,响应结构异常"); + _logger.logWithLevel("$logPrefix 缺少 data 字段,响应结构异常", shouldLog: true); return {'hasPermission': false, 'owner': null}; } } catch (e) { - debugPrint('❌ [RemoteHttp] 请求远程控制权限失败:$e'); - _logger.log('❌ [RemoteHttp] 请求远程控制权限失败:$e'); + debugPrint('$logPrefix 请求远程控制权限失败:$e'); + _logger.logWithLevel('$logPrefix 请求远程控制权限失败:$e', level: 'ERROR', shouldLog: true); return {'hasPermission': false, 'owner': null}; } } ///app退出远程控制后释放权限 - Future releaseRemoteControlViaHttp( String platform) async { - // _logger.logWithLevel("app退出远程控制后释放权限 开始"); + /// [type] 1=从机器人详情页退出释放, 2=退出远程遥控页面释放 + Future releaseRemoteControlViaHttp({ + required String deviceId, + required int type, + }) async { final user = await _userStorage.getUser(); if (user == null) { + debugPrint('❌ [RemoteHttp] releaseControl 未获取到用户信息,跳过释放'); return false; } final token = user.token; - final response = await _dio.post( - '/forward/device/releaseControl', - options: Options( - headers: { - 'Content-Type': 'application/json', - 'Authorization': 'Bearer ${token}', - }, - ), - data: {'platform': "app",'type':2}); + final requestData = { + 'platform': 'app', + 'type': type, + 'deviceId': deviceId, + 'token': token, + }; + debugPrint('📤 [RemoteHttp] releaseControl 实际请求体: $requestData'); try { + final response = await _dio.post( + HttpApiConsts.releaseControlUrl, + options: Options( + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ${token}', + }, + ), + data: requestData); final responseData = response.data as Map; - //debugPrint("📊 [HTTP 响应] 完整数据:$responseData"); - //_logger.log("releaseRemoteControlViaHttp 响应] 完整数据:$responseData"); - if(responseData['code']==200){ - // _logger.logWithLevel("app退出远程控制后释放权限 成功"); - return true; - } - return false; + debugPrint('📋 [RemoteHttp] releaseControl 响应: $responseData'); + if (responseData['code'] == 200) { + return true; + } + debugPrint( + '⚠️ [RemoteHttp] releaseControl 返回 code=${responseData['code']},释放未成功'); + return false; } catch (e) { - // debugPrint('❌ [RemoteHttp] 解析响应失败:$e'); - _logger.log('❌ [RemoteHttp] 解析响应失败:$e'); + debugPrint('❌ [RemoteHttp] releaseControl 请求失败:$e'); + _logger.log('❌ [RemoteHttp] releaseControl 请求失败:$e'); return false; } - } } diff --git a/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart b/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart index ce6f06e2..f745f2ee 100644 --- a/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart +++ b/lib/features/remote_control/data/datasources/remote_tcp_datasource.dart @@ -59,7 +59,10 @@ class RemoteTcpDatasource { final jsonBytes = utf8.encode(jsonStr); debugPrint('sendSwitchControlRequest[TCP] 发送权限请求指令:$jsonStr'); - debugPrint('sendSwitchControlRequest[TCP] 发送权限请求指令:$jsonBytes'); + _logger.logWithLevel( + '🔑 [TCP][获取权限] 发送权限请求指令:$jsonStr', + shouldLog: true, + ); // 使用 0x12 指令发送(cmdGetAuth) // 注意:根据你的协议文档,这里可能是 0x04 或 0x12 @@ -81,8 +84,17 @@ class RemoteTcpDatasource { debugPrint( '✅ sendSwitchControlRequest[TCP] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})', ); + _logger.logWithLevel( + '✅ [TCP][获取权限] 已发送 switch_control 权限请求 (0x${MachineProtocolConstants.cmdGetAuth.toRadixString(16)})', + shouldLog: true, + ); } catch (e) { debugPrint('❌ sendSwitchControlRequest[TCP] 发送权限请求失败:$e'); + _logger.logWithLevel( + '❌ [TCP][获取权限] 发送权限请求失败:$e', + level: 'ERROR', + shouldLog: true, + ); rethrow; } } diff --git a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart index e97762cc..47e77d32 100644 --- a/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart +++ b/lib/features/remote_control/data/repositories/remote_control_repository_impl.dart @@ -61,7 +61,7 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { @override void sendTcpPermissionRequest(String deviceName) { _remoteTcp.sendSwitchControlRequest(deviceName); - _logger.logWithLevel('🔑 [RemoteControl] TCP已发送权限请求'); + _logger.logWithLevel('🔑 [RemoteControl] TCP已发送权限请求', shouldLog: true); } @override @@ -71,7 +71,10 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { ) async { try { // 1. 先发送 HTTP 权限请求检查权限 - _logger.logWithLevel('🔑 [RemoteControl] 检查 HTTP 权限...'); + _logger.logWithLevel( + '🔑 [RemoteControl] 检查 HTTP 权限... deviceName=$deviceName, platform=$deviceId', + shouldLog: true, + ); final result = await _remoteHttp.requestRemoteControlViaHttp( deviceName, deviceId, @@ -79,16 +82,20 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { // 2. 如果没有权限或权限为 null,才发送 TCP 请求 final hasPermission = result['hasPermission'] as bool? ?? false; + _logger.logWithLevel( + '🔑 [RemoteControl] HTTP权限查询结果: hasPermission=$hasPermission, owner=${result['owner']}', + shouldLog: true, + ); if (!hasPermission) { - _logger.logWithLevel('🔑 [RemoteControl] 无权限,发送 TCP 权限请求'); + _logger.logWithLevel('🔑 [RemoteControl] 无权限,发送 TCP 权限请求', shouldLog: true); _remoteTcp.sendSwitchControlRequest(deviceName); } else { - _logger.logWithLevel('✅ [RemoteControl] 已有权限,无需 TCP 请求'); + _logger.logWithLevel('✅ [RemoteControl] 已有权限,无需 TCP 请求', shouldLog: true); } return result; } catch (e) { - _logger.logWithLevel('❌ [RemoteControl] 权限请求失败:$e'); + _logger.logWithLevel('❌ [RemoteControl] 权限请求失败:$e', level: 'ERROR', shouldLog: true); rethrow; } } @@ -149,7 +156,13 @@ class RemoteControlRepositoryImpl implements RemoteControlRepository { ///app退出远程控制后释放权限 @override - Future releasePermission(String platform) async { - return await _remoteHttp.releaseRemoteControlViaHttp(platform); + Future releasePermission({ + required String deviceId, + required int type, + }) async { + return await _remoteHttp.releaseRemoteControlViaHttp( + deviceId: deviceId, + type: type, + ); } } diff --git a/lib/features/remote_control/domain/repositories/remote_control_repository.dart b/lib/features/remote_control/domain/repositories/remote_control_repository.dart index 10071a64..d6410fdd 100644 --- a/lib/features/remote_control/domain/repositories/remote_control_repository.dart +++ b/lib/features/remote_control/domain/repositories/remote_control_repository.dart @@ -23,6 +23,7 @@ abstract class RemoteControlRepository { /// 新增:响应控制权限 同意和拒绝(发送 0x05 指令) void respondPermission(bool bool, String deviceId); - ///APP 推出远程控制页面后释放权限 - Future releasePermission(String platform); + ///APP 退出远程控制/详情页后释放权限 + /// [type] 1=从机器人详情页退出, 2=退出远程遥控页面 + Future releasePermission({required String deviceId, required int type}); } diff --git a/lib/features/remote_control/domain/usecase/request_control_permission_usecase.dart b/lib/features/remote_control/domain/usecase/request_control_permission_usecase.dart index 888eccf3..d1c53661 100644 --- a/lib/features/remote_control/domain/usecase/request_control_permission_usecase.dart +++ b/lib/features/remote_control/domain/usecase/request_control_permission_usecase.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:fpdart/fpdart.dart'; import 'package:maibu_satabot_v2/core/error/failure.dart'; import 'package:maibu_satabot_v2/core/domain/usecases/base_usecase.dart'; @@ -11,10 +12,15 @@ class RequestControlPermissionUseCase extends BaseUseCase, @override Future>> call(RequestControlPermissionParams params) async { + debugPrint( + '🔑 [权限链][UseCase] 开始执行请求 - deviceName: ${params.deviceName}, platform: ${params.deviceId}', + ); try { final result = await repository.requestControlPermission(params.deviceName, params.deviceId); + debugPrint('🔑 [权限链][UseCase] 请求成功,返回: $result'); return right(result); } catch (e) { + debugPrint('❌ [权限链][UseCase] 请求异常: $e'); return left(Failure(e.toString())); } } diff --git a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart index 88edf44c..d661be9b 100644 --- a/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart +++ b/lib/features/remote_control/presentation/bloc/remote_control_cubit.dart @@ -742,15 +742,20 @@ class RemoteControlCubit extends Cubit { debugPrint('${_getTimePrefix()} ====权限弹窗响应结束====='); } + /// 请求控制权限(HTTP 查询接口 /forward/device/remoteControl) + /// + /// [grabControl] true = 查询后若无权限立即发 TCP 0x12 switch_control 抢占(手动点击时使用); + /// false = 仅查询,把接口 remoteControl 结果如实同步到 UI(进入页面时使用) Future requestControlPermissionS( String deviceName, String deviceId, { String source = '自动', + bool grabControl = false, }) async { final timePrefix = _getTimePrefix(); final logPrefix = '$timePrefix 🔑 [RemoteControl] [请求权限接口-$source]'; debugPrint('$logPrefix ========================================='); - debugPrint('$logPrefix 开始请求控制权'); + debugPrint('$logPrefix 开始请求控制权 (grabControl=$grabControl)'); debugPrint('$logPrefix deviceName: $deviceName'); debugPrint('$logPrefix platform: $deviceId'); debugPrint( @@ -808,8 +813,21 @@ class RemoteControlCubit extends Cubit { shouldLog: true, ); - // 🔥 关键逻辑: 如果没有权限 或owner为null,则发送TCP 请求 - if (!hasPermission || owner == null) { + // 🔥 仅查询模式:不抢占,把接口 remoteControl 结果如实同步到 UI + if (!grabControl) { + debugPrint( + '$successLogPrefix 👀 仅查询模式,不发送TCP抢占,直接同步 remoteControl=$hasPermission', + ); + _logger.logWithLevel( + '$successLogPrefix 👀 仅查询模式,直接同步 remoteControl=$hasPermission', + shouldLog: true, + ); + if (state.hasPermission != hasPermission) { + emit(state.copyWith(hasPermission: hasPermission)); + } + } + // 🔥 抢占模式: 如果没有权限 或owner为null,则发送TCP 请求 + else if (!hasPermission || owner == null) { debugPrint('$successLogPrefix ⚠️ APP无权限或owner为null,发送TCP请求...'); _logger.logWithLevel( '$successLogPrefix ⚠️ APP无权限或owner为null,发送TCP请求', @@ -850,6 +868,23 @@ class RemoteControlCubit extends Cubit { ); } + /// 🔥 退出遥控页时重置权限状态,避免单例状态残留导致下次进入不再查询接口 + void resetPermissionState() { + if (!isClosed) { + emit( + state.copyWith( + hasPermission: false, + showPermissionRequestDialog: false, + ), + ); + debugPrint('🔑 [RemoteControl] 已重置权限状态 hasPermission=false'); + _logger.logWithLevel( + '🔑 [RemoteControl] 已重置权限状态 hasPermission=false', + shouldLog: true, + ); + } + } + /// 🔥 重置弹窗状态 - 在弹窗关闭后调用(已简化,不再需要标志位) void resetPermissionCoolDown() { // 标志位已移除,此方法保留以保持向后兼容性 @@ -972,9 +1007,12 @@ class RemoteControlCubit extends Cubit { } } - //app退出远程遥控界面释放权限 - Future releasePermission(String platform) async { - return await _repository.releasePermission(platform); + //app退出远程遥控界面释放权限(type=2),退出机器人详情页释放权限(type=1) + Future releasePermission({ + required String deviceId, + required int type, + }) async { + return await _repository.releasePermission(deviceId: deviceId, type: type); } /// 🔥 设置待控制的设备(从机器人列表点击进入时调用) diff --git a/lib/features/remote_control/presentation/pages/remote_control_page.dart b/lib/features/remote_control/presentation/pages/remote_control_page.dart index fde41f58..2e59b46b 100644 --- a/lib/features/remote_control/presentation/pages/remote_control_page.dart +++ b/lib/features/remote_control/presentation/pages/remote_control_page.dart @@ -5,6 +5,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/components/capsule_toast.dart'; import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart'; import 'package:maibu_satabot_v2/core/localization/app_localizations.dart'; @@ -129,18 +130,23 @@ class _RemoteControlPageState extends State { // 🔥 离开安全模式:启动 90 秒倒计时 GetIt.I().exitSafeMode(); - // 释放远程控制权限 - _cubit.releasePermission("app"); - //print("远程控制要推出啦"); - //final deviceState = _devicesCubit?.state; - //if (deviceState?.selectedDevice != null) { - // _cubit.releasePermission("app"); - // } + // 释放远程控制权限(type=2:退出远程遥控页面释放) + final deviceId = _cubit.state.targetDevice?.deviceName; + if (deviceId != null && deviceId.isNotEmpty) { + _cubit.releasePermission(deviceId: deviceId, type: 2).then((success) { + CapsuleToast.show( + success ? '已释放远程控制权限' : '控制权限释放失败', + showCheck: success, + ); + }); + } _permissionSubscription?.cancel(); // 🔥 取消订阅 SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge); _cubit.stopControlLoop(); + // 🔥 重置权限状态,避免单例 hasPermission 残留导致下次进入不再查询接口 + _cubit.resetPermissionState(); super.dispose(); } @@ -149,7 +155,6 @@ class _RemoteControlPageState extends State { // 🔥 只监听 RemoteControlCubit,避免其他状态变化导致频繁 rebuild final remoteCubit = context.read(); final targetDevice = remoteCubit.state.targetDevice; - final hasPermission = remoteCubit.state.hasPermission; // 🔥 调试日志:检查设备状态 debugPrint('🔍 [RemoteControlPage] build - targetDevice: ${targetDevice?.deviceName}'); @@ -159,26 +164,22 @@ class _RemoteControlPageState extends State { return _buildOfflineScaffold(); } - // 🔥 只在没有权限时自动请求,避免重复调用 - if (!hasPermission && _isLoadingPermission) { - // 🔥 只使用 targetDevice - final deviceName = remoteCubit.state.targetDevice?.deviceName; - - if (deviceName != null && deviceName.isNotEmpty) { - // debugPrint('🔑 [RemoteControl] 检测到无权限,自动发送权限请求 - deviceName: $deviceName'); - setState(() => _isLoadingPermission = false); // 🔥 标记为已请求 - remoteCubit.requestControlPermissionS(deviceName, "app").then((_) { - if (mounted) { - setState(() => _isLoadingPermission = false); // 🔥 请求完成后重置 - } - }); - } else { - // debugPrint('❌ [RemoteControl] 无法发送权限请求 - targetDevice 为空'); - // debugPrint(' targetDevice: ${remoteCubit.state.targetDevice}'); - setState(() => _isLoadingPermission = false); - } - } else { - // debugPrint('✅ [RemoteControl] 已有权限或已请求过,跳过'); + // 🔥 进入页面只查询一次真实权限状态(不抢占),按接口 remoteControl 如实展示 + if (_isLoadingPermission) { + // 直接置标志位,不在 build 期间调用 setState + _isLoadingPermission = false; + final deviceName = targetDevice.deviceName; + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted && deviceName.isNotEmpty) { + debugPrint('🔑 [RemoteControlPage] 进入页面,仅查询权限状态 - deviceName: $deviceName'); + remoteCubit.requestControlPermissionS( + deviceName, + 'app', + source: '进入页面查询', + grabControl: false, + ); + } + }); } return Scaffold( diff --git a/lib/features/remote_control/presentation/widgets/top_status_bar.dart b/lib/features/remote_control/presentation/widgets/top_status_bar.dart index 37365e02..5b6e27b9 100644 --- a/lib/features/remote_control/presentation/widgets/top_status_bar.dart +++ b/lib/features/remote_control/presentation/widgets/top_status_bar.dart @@ -97,8 +97,8 @@ class TopStatusBar extends StatelessWidget { breathing: !remoteState.hasPermission, onTap: () { if (!remoteState.hasPermission) { - // 🔥 点击后重新请求权限,和刚进入页面时的逻辑一致 - debugPrint('🔑 [TopStatusBar] 👆 用户手动点击“未在控制”,重新请求权限'); + // 🔥 点击后发起抢占:查权限 + 无权限则发 TCP switch_control + debugPrint('🔑 [TopStatusBar] 👆 用户手动点击“未在控制”,请求抢占控制权'); final targetDevice = _remoteControlCubit.state.targetDevice; if (targetDevice != null) { @@ -106,6 +106,7 @@ class TopStatusBar extends StatelessWidget { targetDevice.deviceName, 'app', source: '手动点击', + grabControl: true, ); } else { debugPrint('❌ [TopStatusBar] targetDevice 为空,无法请求权限'); diff --git a/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart index 57ce50c4..646c2b5d 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_video_control_page.dart @@ -1460,7 +1460,7 @@ class _DroneVideoControlPageState extends State { try { final dio = DioClient.create(); final response = await dio.post( - 'http://1.95.137.212:8081/iot/UAV/flightTaskCommand', + 'http://1.95.137.212:59003/iot/UAV/flightTaskCommand', data: {'command': command, 'deviceSn': widget.droneSn}, ); diff --git a/lib/features/v2/device_list/presentation/pages/robot_control_page.dart b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart index f55bf842..b888b17a 100644 --- a/lib/features/v2/device_list/presentation/pages/robot_control_page.dart +++ b/lib/features/v2/device_list/presentation/pages/robot_control_page.dart @@ -1,6 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:get_it/get_it.dart'; import 'package:go_router/go_router.dart'; +import 'package:maibu_satabot_v2/components/capsule_toast.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; +import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart'; import '../../../../../core/router/route_paths.dart'; import '../widgets/robot_header_card.dart'; import '../widgets/robot_status_bar.dart'; @@ -10,11 +13,35 @@ import '../widgets/robot_task_info.dart'; import '../widgets/robot_action_buttons.dart'; /// 机器人控制详情页 -class RobotControlPage extends StatelessWidget { +class RobotControlPage extends StatefulWidget { final Map robot; const RobotControlPage({super.key, required this.robot}); + @override + State createState() => _RobotControlPageState(); +} + +class _RobotControlPageState extends State { + Map get robot => widget.robot; + + @override + void dispose() { + // 🔥 退出详情页返回设备列表时释放远程控制权限(type=1) + final deviceId = robot['name']?.toString(); // 长序列号 deviceName + if (deviceId != null && deviceId.isNotEmpty) { + GetIt.I() + .releasePermission(deviceId: deviceId, type: 1) + .then((success) { + CapsuleToast.show( + success ? '已释放远程控制权限' : '控制权限释放失败', + showCheck: success, + ); + }); + } + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/features/v2/my/presentation/pages/profile_detail_page.dart b/lib/features/v2/my/presentation/pages/profile_detail_page.dart index 14e9d9ec..d690db2f 100644 --- a/lib/features/v2/my/presentation/pages/profile_detail_page.dart +++ b/lib/features/v2/my/presentation/pages/profile_detail_page.dart @@ -4,6 +4,7 @@ import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart'; import 'package:maibu_satabot_v2/core/app/app_user_state.dart'; import 'package:maibu_satabot_v2/core/domain/entities/user_entity.dart'; import 'package:maibu_satabot_v2/core/theme/AppTheme.dart'; +import 'package:maibu_satabot_v2/core/utils/image_url_util.dart'; /// 个人信息详情页 class ProfileDetailPage extends StatelessWidget { @@ -116,9 +117,11 @@ class ProfileDetailPage extends StatelessWidget { /// 构建头像 Widget _buildAvatar(String? avatarUrl) { - if (avatarUrl != null && avatarUrl.isNotEmpty) { + // 后端可能返回相对路径,先补全为完整服务器地址 + final url = ImageUrlUtil.resolve(avatarUrl); + if (url != null && url.isNotEmpty) { return Image.network( - avatarUrl, + url, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return _buildDefaultAvatar(); diff --git a/lib/features/v2/my/presentation/widgets/user_profile_card.dart b/lib/features/v2/my/presentation/widgets/user_profile_card.dart index fee3012d..d3712607 100644 --- a/lib/features/v2/my/presentation/widgets/user_profile_card.dart +++ b/lib/features/v2/my/presentation/widgets/user_profile_card.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:maibu_satabot_v2/core/utils/image_url_util.dart'; import 'package:maibu_satabot_v2/features/v2/my/presentation/constants/my_constants.dart'; /// 用户信息卡片组件(100% 还原设计稿) @@ -90,10 +91,12 @@ class UserProfileCard extends StatelessWidget { } Widget _buildAvatar() { + // 后端可能返回相对路径,先补全为完整服务器地址 + final url = ImageUrlUtil.resolve(avatar); // 如果有头像 URL,显示网络图片 - if (avatar != null && avatar!.isNotEmpty) { + if (url != null && url.isNotEmpty) { return Image.network( - avatar!, + url, fit: BoxFit.cover, errorBuilder: (context, error, stackTrace) { return _buildDefaultAvatar();