1:修复远程遥控控制状态显示不正确的问题
2:更换端口映射为测试服的8081-59003,9001-59004
This commit is contained in:
125
lib/components/capsule_toast.dart
Normal file
125
lib/components/capsule_toast.dart
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
|
||||
6
lib/core/env/env_config.dart
vendored
6
lib/core/env/env_config.dart
vendored
@@ -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 端口
|
||||
}
|
||||
}
|
||||
|
||||
24
lib/core/utils/image_url_util.dart
Normal file
24
lib/core/utils/image_url_util.dart
Normal 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';
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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'}),
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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});
|
||||
}
|
||||
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// 🔥 设置待控制的设备(从机器人列表点击进入时调用)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 为空,无法请求权限');
|
||||
|
||||
@@ -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},
|
||||
);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user