1:修复远程遥控控制状态显示不正确的问题

2:更换端口映射为测试服的8081-59003,9001-59004
This commit is contained in:
2026-08-26 14:23:20 +08:00
parent ca616faf11
commit 7c01229b9e
22 changed files with 399 additions and 119 deletions

View File

@@ -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;
}
}

View File

@@ -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";

View File

@@ -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 端口
}
}

View File

@@ -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';
}
}

View File

@@ -93,7 +93,7 @@ class _LoginPageState extends State<LoginPage> {
const SizedBox(height: 16),
Text(
"账号登录v1.1.5",
"账号登录",
style: TextStyle(fontSize: 28, fontWeight: FontWeight.bold, color: context.appColors.textPrimary),
),
const SizedBox(height: 8),

View File

@@ -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: {

View File

@@ -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'}),
);

View File

@@ -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 {
}
/// 鍒涘缓璁惧<E79281>浠诲姟锛堥€氳繃鎺ュ彛鎵ц<E98EB5>浣滀笟锛?
/// 鎺ュ彛鍦板潃: 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}
/// 杩斿洖: 鍒涘缓鎴愬姛鐨勪换鍔<E68DA2>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,
);

View File

@@ -959,7 +959,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
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<MapPageEnterprise> {
// 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<MapPageEnterprise> {
try {
final dio = sl<Dio>();
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,
},

View File

@@ -26,7 +26,7 @@ class MyRepositoryImpl implements MyRepository {
// 核心修复:严格匹配抽象类的方法签名
@override
Future<Either<DeviceFailure, int>> 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();

View File

@@ -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<Map<String, dynamic>> 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<String, dynamic>;
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<String, dynamic>?;
@@ -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<bool> releaseRemoteControlViaHttp( String platform) async {
// _logger.logWithLevel("app退出远程控制后释放权限 开始");
/// [type] 1=从机器人详情页退出释放, 2=退出远程遥控页面释放
Future<bool> 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<String, dynamic>;
//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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<bool> releasePermission(String platform) async {
return await _remoteHttp.releaseRemoteControlViaHttp(platform);
Future<bool> releasePermission({
required String deviceId,
required int type,
}) async {
return await _remoteHttp.releaseRemoteControlViaHttp(
deviceId: deviceId,
type: type,
);
}
}

View File

@@ -23,6 +23,7 @@ abstract class RemoteControlRepository {
/// 新增:响应控制权限 同意和拒绝(发送 0x05 指令)
void respondPermission(bool bool, String deviceId);
///APP 推出远程控制页面后释放权限
Future<bool> releasePermission(String platform);
///APP 退出远程控制/详情页后释放权限
/// [type] 1=从机器人详情页退出, 2=退出远程遥控页面
Future<bool> releasePermission({required String deviceId, required int type});
}

View File

@@ -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<Map<String, dynamic>,
@override
Future<Either<Failure, Map<String, dynamic>>> 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()));
}
}

View File

@@ -742,15 +742,20 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
debugPrint('${_getTimePrefix()} ====权限弹窗响应结束=====');
}
/// 请求控制权限(HTTP 查询接口 /forward/device/remoteControl)
///
/// [grabControl] true = 查询后若无权限立即发 TCP 0x12 switch_control 抢占(手动点击时使用);
/// false = 仅查询,把接口 remoteControl 结果如实同步到 UI(进入页面时使用)
Future<void> 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<RemoteControlState> {
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<RemoteControlState> {
);
}
/// 🔥 退出遥控页时重置权限状态,避免单例状态残留导致下次进入不再查询接口
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<RemoteControlState> {
}
}
//app退出远程遥控界面释放权限
Future<bool> releasePermission(String platform) async {
return await _repository.releasePermission(platform);
//app退出远程遥控界面释放权限(type=2),退出机器人详情页释放权限(type=1)
Future<bool> releasePermission({
required String deviceId,
required int type,
}) async {
return await _repository.releasePermission(deviceId: deviceId, type: type);
}
/// 🔥 设置待控制的设备(从机器人列表点击进入时调用)

View File

@@ -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<RemoteControlPage> {
// 🔥 离开安全模式:启动 90 秒倒计时
GetIt.I<AuthCubit>().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<RemoteControlPage> {
// 🔥 只监听 RemoteControlCubit,避免其他状态变化导致频繁 rebuild
final remoteCubit = context.read<RemoteControlCubit>();
final targetDevice = remoteCubit.state.targetDevice;
final hasPermission = remoteCubit.state.hasPermission;
// 🔥 调试日志:检查设备状态
debugPrint('🔍 [RemoteControlPage] build - targetDevice: ${targetDevice?.deviceName}');
@@ -159,26 +164,22 @@ class _RemoteControlPageState extends State<RemoteControlPage> {
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(

View File

@@ -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 为空,无法请求权限');

View File

@@ -1460,7 +1460,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
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},
);

View File

@@ -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<String, dynamic> robot;
const RobotControlPage({super.key, required this.robot});
@override
State<RobotControlPage> createState() => _RobotControlPageState();
}
class _RobotControlPageState extends State<RobotControlPage> {
Map<String, dynamic> get robot => widget.robot;
@override
void dispose() {
// 🔥 退出详情页返回设备列表时释放远程控制权限(type=1)
final deviceId = robot['name']?.toString(); // 长序列号 deviceName
if (deviceId != null && deviceId.isNotEmpty) {
GetIt.I<RemoteControlCubit>()
.releasePermission(deviceId: deviceId, type: 1)
.then((success) {
CapsuleToast.show(
success ? '已释放远程控制权限' : '控制权限释放失败',
showCheck: success,
);
});
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(

View File

@@ -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();

View File

@@ -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();