集成接口 开始执行任务的领域层和数据层和UI层的开发啊(待测试)
集成功能 暂停 取消功能 恢复功能的接口的领域层和数据层的开发 下一步待集成到页面上 优化功能,优化了接口异常和未知异常对页面渲染的影响对用户的体验的不好情况。具体通过弹窗友好提示! 优化更新了tcp指示灯点击出现机器状态中添加选中设备的编号 方便用户使用的明白明了!。 优化更新关闭了tcp重连操作内链条中的获取用户设备和切换设备操作项。 调整了获取无人机状态信息的更新频率 为14秒一次
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
import 'dart:convert';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../models/device_task_model.dart';
|
||||
|
||||
abstract class DeviceTaskDatasource {
|
||||
Future<List<DeviceTaskModel>> getDeviceTaskPool({
|
||||
required String userId,
|
||||
required int siteId,
|
||||
required int orgId,
|
||||
int pageNum = 1,
|
||||
int pageSize = 99999999,
|
||||
});
|
||||
|
||||
Future<bool> cancelTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
|
||||
Future<bool> pauseTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
|
||||
Future<Map<String, dynamic>> recoveryTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
}
|
||||
|
||||
class DeviceTaskDatasourceImpl implements DeviceTaskDatasource {
|
||||
final Dio dio;
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
|
||||
DeviceTaskDatasourceImpl(this.dio);
|
||||
|
||||
@override
|
||||
Future<List<DeviceTaskModel>> getDeviceTaskPool({
|
||||
required String userId,
|
||||
required int siteId,
|
||||
required int orgId,
|
||||
int pageNum = 1,
|
||||
int pageSize = 99999999,
|
||||
}) async {
|
||||
try {
|
||||
final url = 'http://1.95.137.212:59015/iot/deviceTask/deviceTaskPool';
|
||||
final response = await dio.get(
|
||||
url,
|
||||
queryParameters: {
|
||||
'userId': userId,
|
||||
'siteId': siteId,
|
||||
'orgId': orgId,
|
||||
'pageNum': pageNum,
|
||||
'pageSize': pageSize,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
if (data['code'] == 200) {
|
||||
final rows = data['rows'] as List? ?? [];
|
||||
return rows
|
||||
.map((item) => DeviceTaskModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
} else {
|
||||
throw Exception(data['msg'] ?? '获取任务池失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 获取任务池失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> cancelTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final url = 'http://1.95.137.212:59015/iot/deviceTask/cancelTask';
|
||||
final response = await dio.post(
|
||||
url,
|
||||
data: {
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'orgId': orgId,
|
||||
'siteId': siteId,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
if (data['code'] == 200) {
|
||||
return data['data'] as bool? ?? false;
|
||||
} else {
|
||||
throw Exception(data['msg'] ?? '取消任务失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 取消任务失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<bool> pauseTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final url = 'http://1.95.137.212:59015/iot/deviceTask/pauseTask';
|
||||
final response = await dio.post(
|
||||
url,
|
||||
data: {
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'orgId': orgId,
|
||||
'siteId': siteId,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
if (data['code'] == 200) {
|
||||
return data['data'] as bool? ?? false;
|
||||
} else {
|
||||
throw Exception(data['msg'] ?? '暂停任务失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 暂停任务失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, dynamic>> recoveryTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final url = 'http://1.95.137.212:59015/iot/deviceTask/recoveryTask';
|
||||
final response = await dio.post(
|
||||
url,
|
||||
data: {
|
||||
'deviceId': deviceId,
|
||||
'taskId': taskId,
|
||||
'orgId': orgId,
|
||||
'siteId': siteId,
|
||||
},
|
||||
);
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final data = response.data as Map<String, dynamic>;
|
||||
if (data['code'] == 200) {
|
||||
return data['data'] as Map<String, dynamic>? ?? {};
|
||||
} else {
|
||||
throw Exception(data['msg'] ?? '恢复任务失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 恢复任务失败: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
40
lib/features/devices/data/models/device_task_model.dart
Normal file
40
lib/features/devices/data/models/device_task_model.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import '../../domain/entities/device_task_entity.dart';
|
||||
|
||||
class DeviceTaskModel extends DeviceTaskEntity {
|
||||
const DeviceTaskModel({
|
||||
required super.id,
|
||||
required super.deviceId,
|
||||
required super.taskStatus,
|
||||
required super.taskStatusTranslate,
|
||||
super.routeId,
|
||||
super.siteId,
|
||||
super.orgId,
|
||||
super.createTime,
|
||||
});
|
||||
|
||||
factory DeviceTaskModel.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceTaskModel(
|
||||
id: json['id'] as int,
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskStatus: json['taskStaus'] as String? ?? '',
|
||||
taskStatusTranslate: json['taskStausTranslate'] as String? ?? '',
|
||||
routeId: json['routeId'] as int?,
|
||||
siteId: json['siteId'] as int?,
|
||||
orgId: json['orgId'] as int?,
|
||||
createTime: json['createTime'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
DeviceTaskEntity toEntity() {
|
||||
return DeviceTaskEntity(
|
||||
id: id,
|
||||
deviceId: deviceId,
|
||||
taskStatus: taskStatus,
|
||||
taskStatusTranslate: taskStatusTranslate,
|
||||
routeId: routeId,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
createTime: createTime,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../../domain/entities/device_task_entity.dart';
|
||||
import '../../domain/repositories/device_task_repository.dart';
|
||||
import '../datasources/device_task_datasource.dart';
|
||||
|
||||
class DeviceTaskRepositoryImpl implements DeviceTaskRepository {
|
||||
final DeviceTaskDatasource datasource;
|
||||
|
||||
DeviceTaskRepositoryImpl({required this.datasource});
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<DeviceTaskEntity>>> getDeviceTaskPool({
|
||||
required String userId,
|
||||
required int siteId,
|
||||
required int orgId,
|
||||
int pageNum = 1,
|
||||
int pageSize = 99999999,
|
||||
}) async {
|
||||
try {
|
||||
final models = await datasource.getDeviceTaskPool(
|
||||
userId: userId,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
pageNum: pageNum,
|
||||
pageSize: pageSize,
|
||||
);
|
||||
return Right(models.map((model) => model.toEntity()).toList());
|
||||
} catch (e) {
|
||||
return Left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> cancelTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await datasource.cancelTask(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
);
|
||||
return Right(result);
|
||||
} catch (e) {
|
||||
return Left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> pauseTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await datasource.pauseTask(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
);
|
||||
return Right(result);
|
||||
} catch (e) {
|
||||
return Left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> recoveryTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
final result = await datasource.recoveryTask(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: orgId,
|
||||
siteId: siteId,
|
||||
);
|
||||
return Right(result);
|
||||
} catch (e) {
|
||||
return Left(Failure(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,24 +198,24 @@ class PathRepositoryImpl implements PathRepository {
|
||||
url,
|
||||
headers: {'Accept': 'application/xml, text/xml, */*'},
|
||||
);
|
||||
print('[XML接口] 响应状态码: ${response.statusCode}');
|
||||
print('[XML接口] 响应内容长度: ${response.body.length}');
|
||||
print('[XML接口] Content-Type: ${response.headers['content-type']}');
|
||||
/// print('[XML接口] 响应状态码: ${response.statusCode}');
|
||||
////print('[XML接口] 响应内容长度: ${response.body.length}');
|
||||
///print('[XML接口] Content-Type: ${response.headers['content-type']}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
// 打印前200字符确认格式
|
||||
final preview = response.body.length > 200
|
||||
? response.body.substring(0, 200)
|
||||
: response.body;
|
||||
print('[XML接口] 响应开头: $preview');
|
||||
/// print('[XML接口] 响应开头: $preview');
|
||||
|
||||
// 判断是JSON还是XML格式
|
||||
final trimmed = response.body.trim();
|
||||
if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
|
||||
print('[XML接口] 检测到JSON格式,使用JSON解析');
|
||||
///print('[XML接口] 检测到JSON格式,使用JSON解析');
|
||||
return _parseJsonResponse(response.body);
|
||||
} else {
|
||||
print('[XML接口] 检测到XML格式,使用XML解析');
|
||||
///print('[XML接口] 检测到XML格式,使用XML解析');
|
||||
return _parseXmlResponse(response.body);
|
||||
}
|
||||
} else {
|
||||
@@ -224,7 +224,7 @@ class PathRepositoryImpl implements PathRepository {
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
print('[XML接口] 错误: $e');
|
||||
/// print('[XML接口] 错误: $e');
|
||||
throw Exception('Network error in getWorkRecordsBySiteId: $e');
|
||||
}
|
||||
}
|
||||
@@ -252,7 +252,7 @@ class PathRepositoryImpl implements PathRepository {
|
||||
records.add(WorkRecordEntity.fromJson(recordsData));
|
||||
}
|
||||
|
||||
print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
/// print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ class PathRepositoryImpl implements PathRepository {
|
||||
// 使用非贪婪匹配,确保每个data节点独立提取
|
||||
final dataRegex = RegExp(r'<data>([\s\S]*?)</data>');
|
||||
final dataMatches = dataRegex.allMatches(body);
|
||||
print('[XML接口] 找到data节点数量: ${dataMatches.length}');
|
||||
///print('[XML接口] 找到data节点数量: ${dataMatches.length}');
|
||||
|
||||
// 解析所有工作记录
|
||||
final List<WorkRecordEntity> records = [];
|
||||
@@ -292,9 +292,9 @@ class PathRepositoryImpl implements PathRepository {
|
||||
final recordElement = document.rootElement;
|
||||
|
||||
final recordData = _parseXmlRecord(recordElement);
|
||||
print(
|
||||
'[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}',
|
||||
);
|
||||
/// print(
|
||||
/// '[XML接口] data[$i] 解析结果: workName=${recordData['workName']}, id=${recordData['id']}, imgUrl=${recordData['imgUrl']}',
|
||||
/// );
|
||||
|
||||
records.add(WorkRecordEntity.fromXml(recordData));
|
||||
} catch (e) {
|
||||
@@ -302,7 +302,7 @@ class PathRepositoryImpl implements PathRepository {
|
||||
}
|
||||
}
|
||||
|
||||
print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
///print('[XML接口] 最终解析记录数: ${records.length}');
|
||||
return records;
|
||||
}
|
||||
|
||||
@@ -310,15 +310,15 @@ class PathRepositoryImpl implements PathRepository {
|
||||
Map<String, dynamic> _parseXmlRecord(XmlElement recordElement) {
|
||||
final Map<String, dynamic> result = {};
|
||||
|
||||
print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}');
|
||||
//// print('[XML解析] 开始解析节点,子元素数量: ${recordElement.childElements.length}');
|
||||
|
||||
for (final child in recordElement.childElements) {
|
||||
final tagName = child.name.local;
|
||||
final innerText = child.innerText.trim();
|
||||
|
||||
print(
|
||||
'[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}',
|
||||
);
|
||||
// print(
|
||||
// '[XML解析] 标签: $tagName, 值: ${innerText.length > 50 ? innerText.substring(0, 50) + '...' : innerText}',
|
||||
// );
|
||||
|
||||
// 特殊处理jsonData节点(包含嵌套结构)
|
||||
if (tagName == 'jsonData') {
|
||||
@@ -329,7 +329,7 @@ class PathRepositoryImpl implements PathRepository {
|
||||
}
|
||||
}
|
||||
|
||||
print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}');
|
||||
//print('[XML解析] 解析完成,结果keys: ${result.keys.toList()}');
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -392,4 +392,42 @@ class PathRepositoryImpl implements PathRepository {
|
||||
|
||||
return coordinates;
|
||||
}
|
||||
|
||||
/// 创建设备任务(通过接口执行作业)
|
||||
/// 接口地址: http://1.95.137.212:59015/iot/deviceTask/createDeviceTask
|
||||
/// 入参: {"deviceId":"...","routeId":76,"siteId":22}
|
||||
@override
|
||||
Future<void> createDeviceTask({
|
||||
required String deviceId,
|
||||
required int routeId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
final url = Uri.parse('http://1.95.137.212:59015/iot/deviceTask/createDeviceTask');
|
||||
|
||||
final body = jsonEncode({
|
||||
'deviceId': deviceId,
|
||||
'routeId': routeId,
|
||||
'siteId': siteId,
|
||||
});
|
||||
|
||||
print('📤 [创建设备任务] 请求参数: $body');
|
||||
|
||||
try {
|
||||
final response = await http.post(
|
||||
url,
|
||||
headers: {'Content-Type': 'application/json'},
|
||||
body: body,
|
||||
);
|
||||
|
||||
print('📥 [创建设备任务] 响应状态码: ${response.statusCode}');
|
||||
print('📥 [创建设备任务] 响应内容: ${response.body}');
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
throw Exception('创建设备任务失败: HTTP ${response.statusCode}');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ [创建设备任务] 错误: $e');
|
||||
throw Exception('创建设备任务失败: $e');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
48
lib/features/devices/domain/entities/device_task_entity.dart
Normal file
48
lib/features/devices/domain/entities/device_task_entity.dart
Normal file
@@ -0,0 +1,48 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class DeviceTaskEntity extends Equatable {
|
||||
final int id;
|
||||
final String deviceId;
|
||||
final String taskStatus;
|
||||
final String taskStatusTranslate;
|
||||
final int? routeId;
|
||||
final int? siteId;
|
||||
final int? orgId;
|
||||
final String? createTime;
|
||||
|
||||
const DeviceTaskEntity({
|
||||
required this.id,
|
||||
required this.deviceId,
|
||||
required this.taskStatus,
|
||||
required this.taskStatusTranslate,
|
||||
this.routeId,
|
||||
this.siteId,
|
||||
this.orgId,
|
||||
this.createTime,
|
||||
});
|
||||
|
||||
factory DeviceTaskEntity.fromJson(Map<String, dynamic> json) {
|
||||
return DeviceTaskEntity(
|
||||
id: json['id'] as int,
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
taskStatus: json['taskStaus'] as String? ?? '',
|
||||
taskStatusTranslate: json['taskStausTranslate'] as String? ?? '',
|
||||
routeId: json['routeId'] as int?,
|
||||
siteId: json['siteId'] as int?,
|
||||
orgId: json['orgId'] as int?,
|
||||
createTime: json['createTime'] as String?,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
deviceId,
|
||||
taskStatus,
|
||||
taskStatusTranslate,
|
||||
routeId,
|
||||
siteId,
|
||||
orgId,
|
||||
createTime,
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../entities/device_task_entity.dart';
|
||||
|
||||
abstract class DeviceTaskRepository {
|
||||
/// 获取设备任务池列表
|
||||
Future<Either<Failure, List<DeviceTaskEntity>>> getDeviceTaskPool({
|
||||
required String userId,
|
||||
required int siteId,
|
||||
required int orgId,
|
||||
int pageNum = 1,
|
||||
int pageSize = 99999999,
|
||||
});
|
||||
|
||||
/// 取消任务
|
||||
Future<Either<Failure, bool>> cancelTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
|
||||
/// 暂停任务
|
||||
Future<Either<Failure, bool>> pauseTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
|
||||
/// 恢复任务
|
||||
Future<Either<Failure, Map<String, dynamic>>> recoveryTask({
|
||||
required String deviceId,
|
||||
required int taskId,
|
||||
required int orgId,
|
||||
required int siteId,
|
||||
});
|
||||
}
|
||||
@@ -26,4 +26,14 @@ abstract class PathRepository {
|
||||
|
||||
/// 根据场站ID查询工作记录列表(XML格式)
|
||||
Future<List<WorkRecordEntity>> getWorkRecordsBySiteId({required int siteId});
|
||||
|
||||
/// 创建设备任务(通过接口执行作业)
|
||||
/// deviceId: 设备ID(targetDevice)
|
||||
/// routeId: 路线ID(选中的路线任务ID)
|
||||
/// siteId: 场站ID
|
||||
Future<void> createDeviceTask({
|
||||
required String deviceId,
|
||||
required int routeId,
|
||||
required int siteId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../repositories/device_task_repository.dart';
|
||||
|
||||
class CancelTaskUseCase implements BaseUseCase<bool, CancelTaskParams> {
|
||||
final DeviceTaskRepository repository;
|
||||
|
||||
CancelTaskUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> call(CancelTaskParams params) async {
|
||||
return await repository.cancelTask(
|
||||
deviceId: params.deviceId,
|
||||
taskId: params.taskId,
|
||||
orgId: params.orgId,
|
||||
siteId: params.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CancelTaskParams {
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final int orgId;
|
||||
final int siteId;
|
||||
|
||||
CancelTaskParams({
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
required this.orgId,
|
||||
required this.siteId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../domain/errors/device_failure.dart';
|
||||
import '../../domain/repositories/path_repository.dart';
|
||||
|
||||
class CreateDeviceTaskUseCase {
|
||||
final PathRepository repository;
|
||||
|
||||
CreateDeviceTaskUseCase(this.repository);
|
||||
|
||||
Future<Either<DeviceFailure, void>> execute({
|
||||
required String deviceId,
|
||||
required int routeId,
|
||||
required int siteId,
|
||||
}) async {
|
||||
try {
|
||||
await repository.createDeviceTask(
|
||||
deviceId: deviceId,
|
||||
routeId: routeId,
|
||||
siteId: siteId,
|
||||
);
|
||||
return const Right(null);
|
||||
} catch (e) {
|
||||
return Left(DeviceFailure.networkError(message: e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../entities/device_task_entity.dart';
|
||||
import '../repositories/device_task_repository.dart';
|
||||
|
||||
class GetDeviceTaskPoolUseCase
|
||||
implements BaseUseCase<List<DeviceTaskEntity>, GetDeviceTaskPoolParams> {
|
||||
final DeviceTaskRepository repository;
|
||||
|
||||
GetDeviceTaskPoolUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, List<DeviceTaskEntity>>> call(
|
||||
GetDeviceTaskPoolParams params,
|
||||
) async {
|
||||
return await repository.getDeviceTaskPool(
|
||||
userId: params.userId,
|
||||
siteId: params.siteId,
|
||||
orgId: params.orgId,
|
||||
pageNum: params.pageNum,
|
||||
pageSize: params.pageSize,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class GetDeviceTaskPoolParams {
|
||||
final String userId;
|
||||
final int siteId;
|
||||
final int orgId;
|
||||
final int pageNum;
|
||||
final int pageSize;
|
||||
|
||||
GetDeviceTaskPoolParams({
|
||||
required this.userId,
|
||||
required this.siteId,
|
||||
required this.orgId,
|
||||
this.pageNum = 1,
|
||||
this.pageSize = 99999999,
|
||||
});
|
||||
}
|
||||
34
lib/features/devices/domain/usecases/pause_task_usecase.dart
Normal file
34
lib/features/devices/domain/usecases/pause_task_usecase.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../repositories/device_task_repository.dart';
|
||||
|
||||
class PauseTaskUseCase implements BaseUseCase<bool, PauseTaskParams> {
|
||||
final DeviceTaskRepository repository;
|
||||
|
||||
PauseTaskUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, bool>> call(PauseTaskParams params) async {
|
||||
return await repository.pauseTask(
|
||||
deviceId: params.deviceId,
|
||||
taskId: params.taskId,
|
||||
orgId: params.orgId,
|
||||
siteId: params.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class PauseTaskParams {
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final int orgId;
|
||||
final int siteId;
|
||||
|
||||
PauseTaskParams({
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
required this.orgId,
|
||||
required this.siteId,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import 'package:fpdart/fpdart.dart';
|
||||
import '../../../../core/domain/usecases/base_usecase.dart';
|
||||
import '../../../../core/error/failure.dart';
|
||||
import '../repositories/device_task_repository.dart';
|
||||
|
||||
class RecoveryTaskUseCase
|
||||
implements BaseUseCase<Map<String, dynamic>, RecoveryTaskParams> {
|
||||
final DeviceTaskRepository repository;
|
||||
|
||||
RecoveryTaskUseCase(this.repository);
|
||||
|
||||
@override
|
||||
Future<Either<Failure, Map<String, dynamic>>> call(
|
||||
RecoveryTaskParams params,
|
||||
) async {
|
||||
return await repository.recoveryTask(
|
||||
deviceId: params.deviceId,
|
||||
taskId: params.taskId,
|
||||
orgId: params.orgId,
|
||||
siteId: params.siteId,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class RecoveryTaskParams {
|
||||
final String deviceId;
|
||||
final int taskId;
|
||||
final int orgId;
|
||||
final int siteId;
|
||||
|
||||
RecoveryTaskParams({
|
||||
required this.deviceId,
|
||||
required this.taskId,
|
||||
required this.orgId,
|
||||
required this.siteId,
|
||||
});
|
||||
}
|
||||
315
lib/features/devices/presentation/bloc/device_task_cubit.dart
Normal file
315
lib/features/devices/presentation/bloc/device_task_cubit.dart
Normal file
@@ -0,0 +1,315 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import '../../../../core/app/app_user_cubit.dart';
|
||||
import '../../../../core/logging/i_logger_service.dart';
|
||||
import '../../../../core/network/error_handler.dart';
|
||||
import '../../../../features/v2/site/presentation/cubit/site_cubit.dart';
|
||||
import '../../domain/usecases/cancel_task_usecase.dart';
|
||||
import '../../domain/usecases/get_device_task_pool_usecase.dart';
|
||||
import '../../domain/usecases/pause_task_usecase.dart';
|
||||
import '../../domain/usecases/recovery_task_usecase.dart';
|
||||
import 'device_task_state.dart';
|
||||
|
||||
class DeviceTaskCubit extends Cubit<DeviceTaskState> {
|
||||
final GetDeviceTaskPoolUseCase _getDeviceTaskPoolUseCase;
|
||||
final CancelTaskUseCase _cancelTaskUseCase;
|
||||
final PauseTaskUseCase _pauseTaskUseCase;
|
||||
final RecoveryTaskUseCase _recoveryTaskUseCase;
|
||||
final ILoggerService _logger = GetIt.I<ILoggerService>();
|
||||
|
||||
DeviceTaskCubit(
|
||||
this._getDeviceTaskPoolUseCase,
|
||||
this._cancelTaskUseCase,
|
||||
this._pauseTaskUseCase,
|
||||
this._recoveryTaskUseCase,
|
||||
) : super(const DeviceTaskState());
|
||||
|
||||
/// 获取任务池并过滤出当前设备的任务
|
||||
Future<void> fetchAndFilterTask(String deviceId) async {
|
||||
emit(state.copyWith(isLoading: true, errorMessage: null));
|
||||
|
||||
try {
|
||||
// 获取用户信息
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
if (user == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '用户未登录',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取场站ID
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
if (siteId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '未选择场站',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// 调用接口获取任务池
|
||||
final result = await _getDeviceTaskPoolUseCase.call(
|
||||
GetDeviceTaskPoolParams(
|
||||
userId: user.userId ?? '',
|
||||
siteId: siteId,
|
||||
orgId: user.orgId ?? 0,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 获取任务池失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
},
|
||||
(taskList) {
|
||||
// 过滤出当前设备的任务
|
||||
final deviceTasks = taskList
|
||||
.where((task) => task.deviceId == deviceId)
|
||||
.toList();
|
||||
|
||||
// 取第一个任务(或根据业务逻辑选择)
|
||||
final currentTask = deviceTasks.isNotEmpty ? deviceTasks.first : null;
|
||||
|
||||
_logger.logWithLevel(
|
||||
'✅ 找到 ${deviceTasks.length} 个任务,当前任务ID: ${currentTask?.id}',
|
||||
);
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
taskPool: taskList,
|
||||
currentTask: currentTask,
|
||||
currentTaskId: currentTask?.id,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 获取任务池异常: $e');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// 取消任务
|
||||
Future<void> cancelTask(String deviceId) async {
|
||||
final taskId = state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
emit(state.copyWith(errorMessage: '无可用任务'));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.cancel,
|
||||
));
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _cancelTaskUseCase.call(
|
||||
CancelTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
siteId: siteId,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 取消任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
},
|
||||
(success) {
|
||||
_logger.logWithLevel('✅ 取消任务成功');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 取消任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// 暂停任务
|
||||
Future<void> pauseTask(String deviceId) async {
|
||||
final taskId = state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
emit(state.copyWith(errorMessage: '无可用任务'));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.pause,
|
||||
));
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _pauseTaskUseCase.call(
|
||||
PauseTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
siteId: siteId,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 暂停任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
},
|
||||
(success) {
|
||||
_logger.logWithLevel('✅ 暂停任务成功');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 暂停任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// 恢复任务
|
||||
Future<void> recoveryTask(String deviceId) async {
|
||||
final taskId = state.currentTaskId;
|
||||
if (taskId == null) {
|
||||
emit(state.copyWith(errorMessage: '无可用任务'));
|
||||
return;
|
||||
}
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: true,
|
||||
operationType: DeviceTaskOperationType.recovery,
|
||||
));
|
||||
|
||||
try {
|
||||
final userCubit = GetIt.I<AppUserCubit>();
|
||||
final user = userCubit.state.user;
|
||||
final siteCubit = GetIt.I<SiteCubit>();
|
||||
final siteId = siteCubit.state.selectedSite?.id;
|
||||
|
||||
if (user == null || siteId == null) {
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: '参数不完整',
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final result = await _recoveryTaskUseCase.call(
|
||||
RecoveryTaskParams(
|
||||
deviceId: deviceId,
|
||||
taskId: taskId,
|
||||
orgId: user.orgId ?? 0,
|
||||
siteId: siteId,
|
||||
),
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
_logger.logWithLevel('❌ 恢复任务失败: ${failure.message}');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(failure.message),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
},
|
||||
(data) {
|
||||
_logger.logWithLevel('✅ 恢复任务成功: $data');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
));
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
_logger.logWithLevel('❌ 恢复任务异常: $e');
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
errorMessage: ErrorHandler.getErrorMessage(e),
|
||||
operationType: DeviceTaskOperationType.none,
|
||||
shouldShowError: true, // 🔥 标记需要显示错误弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新当前任务ID(当选择新航线时调用)
|
||||
void updateCurrentTaskId(int taskId) {
|
||||
emit(state.copyWith(currentTaskId: taskId));
|
||||
_logger.logWithLevel('🔄 更新当前任务ID: $taskId');
|
||||
}
|
||||
|
||||
/// 清除当前任务
|
||||
void clearCurrentTask() {
|
||||
emit(state.copyWith(
|
||||
currentTask: null,
|
||||
currentTaskId: null,
|
||||
));
|
||||
_logger.logWithLevel('🧹 清除当前任务');
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import '../../domain/entities/device_task_entity.dart';
|
||||
|
||||
enum DeviceTaskOperationType {
|
||||
none,
|
||||
cancel,
|
||||
pause,
|
||||
recovery,
|
||||
}
|
||||
|
||||
class DeviceTaskState extends Equatable {
|
||||
final List<DeviceTaskEntity> taskPool;
|
||||
final DeviceTaskEntity? currentTask;
|
||||
final int? currentTaskId;
|
||||
final bool isLoading;
|
||||
final String? errorMessage;
|
||||
final DeviceTaskOperationType operationType;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const DeviceTaskState({
|
||||
this.taskPool = const [],
|
||||
this.currentTask,
|
||||
this.currentTaskId,
|
||||
this.isLoading = false,
|
||||
this.errorMessage,
|
||||
this.operationType = DeviceTaskOperationType.none,
|
||||
this.shouldShowError = false,
|
||||
});
|
||||
|
||||
DeviceTaskState copyWith({
|
||||
List<DeviceTaskEntity>? taskPool,
|
||||
DeviceTaskEntity? currentTask,
|
||||
int? currentTaskId,
|
||||
bool? isLoading,
|
||||
String? errorMessage,
|
||||
DeviceTaskOperationType? operationType,
|
||||
bool? shouldShowError,
|
||||
}) {
|
||||
return DeviceTaskState(
|
||||
taskPool: taskPool ?? this.taskPool,
|
||||
currentTask: currentTask ?? this.currentTask,
|
||||
currentTaskId: currentTaskId ?? this.currentTaskId,
|
||||
isLoading: isLoading ?? this.isLoading,
|
||||
errorMessage: errorMessage,
|
||||
operationType: operationType ?? this.operationType,
|
||||
shouldShowError: shouldShowError ?? false, // 🔥 默认重置为 false
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
taskPool,
|
||||
currentTask,
|
||||
currentTaskId,
|
||||
isLoading,
|
||||
errorMessage,
|
||||
operationType,
|
||||
shouldShowError,
|
||||
];
|
||||
}
|
||||
@@ -623,4 +623,53 @@ class DevicesCubit extends Cubit<DevicesState> {
|
||||
//print('🔄 [DevicesCubit] 已重置到达位置');
|
||||
_logger.logWithLevel('🔄 [DevicesCubit] 已重置到达位置');
|
||||
}
|
||||
|
||||
/// 🔥 获取当前选中的设备
|
||||
DeviceEntity? getSelectedDevice() {
|
||||
return state.selectedDevice;
|
||||
}
|
||||
|
||||
/// 🔥 异步获取当前选中的设备(兼容 RemoteControlCubit 接口)
|
||||
Future<DeviceEntity?> getDevice() async {
|
||||
return state.selectedDevice;
|
||||
}
|
||||
|
||||
/// 🔥 判断是否有选中设备
|
||||
bool hasSelectedDevice() {
|
||||
return state.selectedDevice != null;
|
||||
}
|
||||
|
||||
/// 🔥 获取选中设备名称(安全获取,返回空字符串而非null)
|
||||
String getSelectedDeviceName() {
|
||||
return state.selectedDevice?.deviceName ?? '';
|
||||
}
|
||||
|
||||
/// 🔥 获取选中设备ID(安全获取,返回空字符串而非null)
|
||||
String getSelectedDeviceId() {
|
||||
return state.selectedDevice?.deviceName ?? '';
|
||||
}
|
||||
|
||||
/// 🔥 清除选中设备
|
||||
void clearSelectedDevice() {
|
||||
debugPrint('🧹 [DevicesCubit] 清除选中设备');
|
||||
_logger.logWithLevel('🧹 [DevicesCubit] 清除选中设备');
|
||||
emit(state.copyWith(selectedDevice: null));
|
||||
}
|
||||
|
||||
/// 🔥 检查设备是否在列表中
|
||||
bool isDeviceInList(String deviceName) {
|
||||
return state.devices.any((device) => device.deviceName == deviceName);
|
||||
}
|
||||
|
||||
/// 🔥 根据设备名称查找设备
|
||||
DeviceEntity? findDeviceByName(String deviceName) {
|
||||
try {
|
||||
return state.devices.firstWhere(
|
||||
(device) => device.deviceName == deviceName,
|
||||
orElse: () => throw Exception('Device not found'),
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,24 +61,26 @@ class _HomePageState extends State<HomePage> {
|
||||
// 🔥 自动选中第一个设备(保证是最新的)
|
||||
devicesCubit.selectDevice(firstDevice);
|
||||
|
||||
// 🔥 关键修复:首页不再自动连接TCP,改为选择设备时才连接
|
||||
// 连接 TCP
|
||||
final tcpClient = GetIt.instance<TcpClient>();
|
||||
if (!tcpClient.isConnected) {
|
||||
tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: firstDevice.deviceName).then((_) {
|
||||
debugPrint('✅ [HomePage] TCP 连接成功');
|
||||
tcpClient.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
|
||||
// 🔥 关键:TCP 连接成功后,主动请求权限以激活服务器的推送机制
|
||||
final remoteRepo = GetIt.instance<RemoteControlRepository>();
|
||||
remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
|
||||
debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送');
|
||||
});
|
||||
} else {
|
||||
// 🔥 TCP 已连接,也要发送权限请求
|
||||
final remoteRepo = GetIt.instance<RemoteControlRepository>();
|
||||
remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
|
||||
debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求');
|
||||
}
|
||||
// final tcpClient = GetIt.instance<TcpClient>();
|
||||
// if (!tcpClient.isConnected) {
|
||||
// tcpClient.connectBySwitch(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT, deviceName: firstDevice.deviceName).then((_) {
|
||||
// debugPrint('✅ [HomePage] TCP 连接成功');
|
||||
// tcpClient.startHeartbeat(interval: const Duration(seconds: 4));
|
||||
//
|
||||
// // 🔥 关键:TCP 连接成功后,主动请求权限以激活服务器的推送机制
|
||||
// final remoteRepo = GetIt.instance<RemoteControlRepository>();
|
||||
// remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
|
||||
// debugPrint('✅ [HomePage] 已发送权限请求,激活服务器推送');
|
||||
// });
|
||||
// } else {
|
||||
// // 🔥 TCP 已连接,也要发送权限请求
|
||||
// final remoteRepo = GetIt.instance<RemoteControlRepository>();
|
||||
// remoteRepo.requestControlPermission(firstDevice.deviceName, "app");
|
||||
// debugPrint('✅ [HomePage] TCP 已连接,已发送权限请求');
|
||||
// }
|
||||
debugPrint('ℹ️ [HomePage] TCP 连接已移至选择设备时执行');
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() => _isDevicesLoading = false);
|
||||
|
||||
@@ -16,6 +16,7 @@ import 'package:latlong2/latlong.dart';
|
||||
import 'package:maibu_satabot_v2/components/confrim_dialog.dart';
|
||||
import 'package:maibu_satabot_v2/components/toast.dart';
|
||||
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/core/consts/tcp_consts.dart';
|
||||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||||
import 'package:maibu_satabot_v2/core/localization/app_localizations.dart';
|
||||
@@ -28,6 +29,7 @@ import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_p
|
||||
as work_area_model;
|
||||
import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_work_record_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/create_device_task_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_event.dart';
|
||||
@@ -782,15 +784,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
Future<void> _savePlotData(String plotName, String? imgBase64) async {
|
||||
final loc = AppLocalizations.of(context);
|
||||
// 1. 获取用户ID
|
||||
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
|
||||
if (userId.isEmpty) {
|
||||
_showPageToast(
|
||||
message: loc.translate('route_planning.user_id_empty'),
|
||||
type: ToastType.error,
|
||||
);
|
||||
|
||||
//ToastUtils.showError(context, '用户ID为空,无法保存!');
|
||||
// 🔥 V2 适配:使用 SiteCubit 获取站点ID(从 v2 首页场站选择获取,有默认值)
|
||||
final siteId = sl<SiteCubit>().state.selectedSite?.id;
|
||||
if (siteId == null) {
|
||||
_showPageToast(message: '请先在场站列表中选择一个场站!', type: ToastType.error);
|
||||
return;
|
||||
}
|
||||
for (var i = 0; i < typedPathList.length; i++) {
|
||||
@@ -834,7 +831,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
// 3. 构造 workRecord (包裹一层)
|
||||
final Map<String, dynamic> workRecord = {
|
||||
'workName': plotName,
|
||||
'userId': userId,
|
||||
'siteId': siteId, // 修改:使用 siteId 代替 userId
|
||||
'jsonData': jsonEncode(savePath), // 将 savePath 转为 JSON 字符串
|
||||
};
|
||||
final String workRecordJson = jsonEncode(workRecord);
|
||||
@@ -1372,6 +1369,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
return GestureDetector(
|
||||
// 核心:点击列表项触发选中逻辑
|
||||
onTap: () async {
|
||||
// 🔥 添加日志:打印选中的路线任务详情
|
||||
debugPrint('📋 [路径规划] 选中路线任务:');
|
||||
debugPrint(' ├─ 地块名称: ${plot.plotName}');
|
||||
debugPrint(' └─ 数据ID: ${plot.id}');
|
||||
|
||||
// 【优化1】第一步就UI响应,不卡手
|
||||
setState(() {
|
||||
_isListBoxOpen = false;
|
||||
@@ -1969,13 +1971,96 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
/// 开始作业
|
||||
void _startWork() async {
|
||||
// 🔥 V2 适配:使用接口方式执行作业,不再使用 TCP
|
||||
|
||||
// 1. 校验选中的路线
|
||||
if (_selectedPlot == null) {
|
||||
_showPageToast(message: "请先选择一个路线任务", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
// 2. 获取设备ID(从 RemoteControlCubit 的 targetDevice)
|
||||
final targetDevice = context.read<RemoteControlCubit>().state.targetDevice;
|
||||
final deviceId = targetDevice?.deviceName;
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
_showPageToast(message: "请先选择一个设备", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. 获取路线ID(从选中的路线任务)
|
||||
final routeId = int.tryParse(_selectedPlot!.id);
|
||||
if (routeId == null) {
|
||||
_showPageToast(message: "路线ID无效", type: ToastType.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 获取场站ID(从 SiteCubit 的 selectedSite)
|
||||
final siteId = sl<SiteCubit>().state.selectedSite?.id;
|
||||
if (siteId == null) {
|
||||
_showPageToast(message: "请先选择一个场站", type: ToastType.info);
|
||||
return;
|
||||
}
|
||||
|
||||
// 5. 打印请求参数日志
|
||||
debugPrint('🚀 [开始作业] 请求参数:');
|
||||
debugPrint(' ├─ deviceId: $deviceId');
|
||||
debugPrint(' ├─ routeId: $routeId');
|
||||
debugPrint(' └─ siteId: $siteId');
|
||||
|
||||
// 6. 更新UI状态
|
||||
setState(() {
|
||||
isStopWork = false;
|
||||
isStartWork = true;
|
||||
_workStatus = WorkStatus.working;
|
||||
_traceManager.reset();
|
||||
tracePoint?.clear();
|
||||
gctracePoint?.clear();
|
||||
});
|
||||
|
||||
// 7. 更新应用状态
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
|
||||
try {
|
||||
// 8. 调用接口创建设备任务
|
||||
final result = await sl<CreateDeviceTaskUseCase>().execute(
|
||||
deviceId: deviceId,
|
||||
routeId: routeId,
|
||||
siteId: siteId,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) {
|
||||
// 失败
|
||||
debugPrint('❌ [开始作业] 创建设备任务失败: $failure');
|
||||
_showPageToast(message: "作业启动失败", type: ToastType.error);
|
||||
setState(() {
|
||||
isStartWork = false;
|
||||
_workStatus = WorkStatus.idle;
|
||||
});
|
||||
},
|
||||
(_) {
|
||||
// 成功
|
||||
debugPrint('✅ [开始作业] 创建设备任务成功');
|
||||
_showPageToast(message: "作业已开始", type: ToastType.success);
|
||||
_saveDataToLocal();
|
||||
},
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('❌ [开始作业] 异常: $e');
|
||||
_showPageToast(message: "作业启动异常: $e", type: ToastType.error);
|
||||
setState(() {
|
||||
isStartWork = false;
|
||||
_workStatus = WorkStatus.idle;
|
||||
});
|
||||
}
|
||||
|
||||
// ============ 以下是原有的 TCP 方式代码,已注释 ============
|
||||
/*
|
||||
if (startWorkList.isEmpty) {
|
||||
_showPageToast(message: "作业列表为空,请重新选择路径", type: ToastType.info);
|
||||
//ToastUtils.showInfo(context, '作业列表为空,请重新选择路径');
|
||||
return;
|
||||
}
|
||||
_traceManager.setMode(TPMode.LOCATION);
|
||||
//_traceManager.reset();
|
||||
tracePoint = _traceManager.getTracePoint();
|
||||
gctracePoint = batchWgs84ToGcj02(tracePoint!);
|
||||
_logger.log("[当前轨迹模式][转换后gctracePoint]开始作业: $tracePoint");
|
||||
@@ -1984,18 +2069,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
setState(() {
|
||||
isStopWork = false;
|
||||
|
||||
isStartWork = true; // 🔥 关键:停止作业标志
|
||||
isStartWork = true;
|
||||
_workStatus = WorkStatus.working;
|
||||
_traceManager.reset();
|
||||
tracePoint?.clear();
|
||||
gctracePoint?.clear();
|
||||
//_traceManager.setMode(TPMode.NAVIGATION);
|
||||
});
|
||||
|
||||
// 🔥 核心修复:先更新 AppState 为 routePlanning
|
||||
context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
// 🔥 延迟一下,确保状态已更新
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
|
||||
debugPrint('⚙️ 开始类型转换...');
|
||||
@@ -2007,19 +2088,12 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
}).toList();
|
||||
|
||||
final Queue<work_area_model.DeviceAddPathPointModel> pathQueue = Queue.from(
|
||||
typedList,
|
||||
);
|
||||
|
||||
// 步骤 3:调用 Cubit 方法(类型匹配)
|
||||
final Queue<work_area_model.DeviceAddPathPointModel> pathQueue = Queue.from(typedList);
|
||||
await context.read<DevicesCubit>().startRoutePlanning(pathQueue);
|
||||
|
||||
///context.read<DevicesCubit>().updateAppState(AppState.routePlanning);
|
||||
// 可选:显示作业提示
|
||||
_showPageToast(message: "作业已开始", type: ToastType.success);
|
||||
//ToastUtils.showSuccess(context, '作业已开始');
|
||||
_saveDataToLocal();
|
||||
//ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('作业已开始'), backgroundColor: Colors.green));
|
||||
*/
|
||||
}
|
||||
|
||||
/// 暂停作业
|
||||
@@ -2303,7 +2377,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
headingStatus == 0
|
||||
? null
|
||||
: () {
|
||||
_startWork();
|
||||
//@开始作业通过后端接口
|
||||
// _startWork();
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00C853),
|
||||
@@ -2344,6 +2419,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
height: 50,
|
||||
child: ElevatedButton(
|
||||
onPressed: () =>
|
||||
//@暂停工作通过接口、恢复(继续)工作通过接口
|
||||
_workStatus == WorkStatus.working
|
||||
? _pauseWork()
|
||||
: _resumeWork(),
|
||||
@@ -2487,7 +2563,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}).toList();
|
||||
}
|
||||
|
||||
// ========== 抽象:生成路径的核心函数 ==========
|
||||
// ========== 抽象:生成路径的核心函数(打点函数) ==========
|
||||
Future<void> _generatePath({bool showTips = true}) async {
|
||||
if (_currentWorkMode == WorkMode.custom) {
|
||||
setState(() {
|
||||
@@ -2742,10 +2818,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
|
||||
final maxTop = screenHeight - menuHeight;
|
||||
final userState = context.watch<AppUserCubit>().state;
|
||||
// 🔥 V2 适配:使用 RemoteControlCubit 的 targetDevice 保持一致性
|
||||
final deviceId = context
|
||||
.watch<DevicesCubit>()
|
||||
.watch<RemoteControlCubit>()
|
||||
.state
|
||||
.selectedDevice
|
||||
.targetDevice
|
||||
?.deviceName;
|
||||
|
||||
if (deviceId != null &&
|
||||
|
||||
@@ -946,6 +946,9 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
debugPrint('🎯 [RemoteControl] 设备名称: ${device.deviceName}');
|
||||
debugPrint('🎯 [RemoteControl] 当前TCP状态: ${tcpClient.isConnected ? "已连接" : "未连接"}');
|
||||
|
||||
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
|
||||
emit(state.copyWith(targetDevice: device));
|
||||
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
|
||||
// 🔥 关键修改:选择设备时才连接TCP
|
||||
if (!tcpClient.isConnected) {
|
||||
debugPrint('🔌 [RemoteControl] TCP未连接,开始建立连接...');
|
||||
@@ -981,8 +984,6 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
);
|
||||
});
|
||||
|
||||
debugPrint('📦 [RemoteControl] 更新targetDevice状态');
|
||||
emit(state.copyWith(targetDevice: device));
|
||||
debugPrint('🎯 [RemoteControl] ========== 目标设备设置完成 ==========');
|
||||
}
|
||||
|
||||
@@ -992,4 +993,12 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
|
||||
// _logger.logWithLevel('🧹 [RemoteControl] 清除待控制设备');
|
||||
emit(state.copyWith(targetDevice: null));
|
||||
}
|
||||
|
||||
/// 🔥 获取设备
|
||||
|
||||
DeviceEntity? getSelectedDevice() {
|
||||
return state.targetDevice;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -76,12 +76,12 @@ class PositionState extends Equatable {
|
||||
|
||||
/// UAV详情实体(用于详情页面API返回的数据)
|
||||
class UAVDetailEntity extends Equatable {
|
||||
final String deviceSn; // 设备序列号(无人机序列号)
|
||||
final String gatewaySn; // 网关序列号
|
||||
final String callsign; // 机场呼号/名称
|
||||
final String droneCallsign; // 无人机呼号
|
||||
final int onlineStatus; // 机场在线状态 (1:在线, 0:离线)
|
||||
final int droneOnlineStatus; // 无人机在线状态
|
||||
final String deviceSn;
|
||||
final String gatewaySn;
|
||||
final String callsign;
|
||||
final String droneCallsign;
|
||||
final int onlineStatus;
|
||||
final int droneOnlineStatus;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final double? capacityPercent;
|
||||
@@ -173,7 +173,9 @@ class UAVDetailEntity extends Equatable {
|
||||
: null,
|
||||
droneCameraList: json['drone_camera_list'] != null
|
||||
? (json['drone_camera_list'] as List)
|
||||
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
|
||||
.map(
|
||||
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
orgId: json['orgId'],
|
||||
@@ -333,7 +335,9 @@ class DroneStationEntity extends Equatable {
|
||||
: null,
|
||||
droneCameraList: json['drone_camera_list'] != null
|
||||
? (json['drone_camera_list'] as List)
|
||||
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
|
||||
.map(
|
||||
(item) => CameraInfo.fromJson(item as Map<String, dynamic>),
|
||||
)
|
||||
.toList()
|
||||
: null,
|
||||
orgId: json['orgId'] ?? 0,
|
||||
|
||||
@@ -5,7 +5,6 @@ class FlightTaskDetailEntity {
|
||||
final String taskType;
|
||||
final String status;
|
||||
final String sn;
|
||||
final String droneSn; // 无人机序列号
|
||||
final String waylineUuid;
|
||||
final String beginAt;
|
||||
final String endAt;
|
||||
@@ -27,7 +26,6 @@ class FlightTaskDetailEntity {
|
||||
required this.taskType,
|
||||
required this.status,
|
||||
required this.sn,
|
||||
required this.droneSn,
|
||||
required this.waylineUuid,
|
||||
required this.beginAt,
|
||||
required this.endAt,
|
||||
@@ -47,26 +45,18 @@ class FlightTaskDetailEntity {
|
||||
factory FlightTaskDetailEntity.fromJson(Map<String, dynamic> json) {
|
||||
// 安全处理 folder_info 嵌套结构
|
||||
final rawFolderInfo = json['folder_info'];
|
||||
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
|
||||
? Map<String, dynamic>.from(rawFolderInfo)
|
||||
final Map<String, dynamic> folderInfo = (rawFolderInfo is Map)
|
||||
? Map<String, dynamic>.from(rawFolderInfo)
|
||||
: <String, dynamic>{};
|
||||
print(
|
||||
'🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}',
|
||||
);
|
||||
print('🔍 [FlightTaskDetailEntity] folder_info 类型: ${rawFolderInfo.runtimeType}');
|
||||
print('🔍 [FlightTaskDetailEntity] folder_info 值: $rawFolderInfo');
|
||||
|
||||
|
||||
return FlightTaskDetailEntity(
|
||||
name: json['name'] ?? '',
|
||||
uuid: json['uuid'] ?? '',
|
||||
taskType: json['task_type'] ?? '',
|
||||
status: json['status'] ?? '',
|
||||
sn: json['sn'] ?? '',
|
||||
droneSn:
|
||||
json['drone_sn'] ??
|
||||
json['device_sn'] ??
|
||||
json['droneSn'] ??
|
||||
json['deviceSn'] ??
|
||||
'',
|
||||
waylineUuid: json['wayline_uuid'] ?? '',
|
||||
beginAt: json['begin_at'] ?? '',
|
||||
endAt: json['end_at'] ?? '',
|
||||
@@ -99,7 +89,6 @@ class FlightTaskDetailEntity {
|
||||
'task_type': taskType,
|
||||
'status': status,
|
||||
'sn': sn,
|
||||
'drone_sn': droneSn,
|
||||
'wayline_uuid': waylineUuid,
|
||||
'begin_at': beginAt,
|
||||
'end_at': endAt,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/get_device_status_data_usecase.dart';
|
||||
import '../../../../../core/network/error_handler.dart';
|
||||
import 'device_status_event.dart';
|
||||
import 'device_status_state.dart';
|
||||
|
||||
@@ -31,7 +32,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
siteId: event.siteId,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError(e.toString()));
|
||||
emit(DeviceStatusError(
|
||||
message: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,7 +59,10 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
devices: response.devices,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(DeviceStatusError(e.toString()));
|
||||
emit(DeviceStatusError(
|
||||
message: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -54,11 +54,15 @@ class DeviceStatusLoaded extends DeviceStatusState {
|
||||
|
||||
class DeviceStatusError extends DeviceStatusState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const DeviceStatusError(this.message);
|
||||
const DeviceStatusError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
class DeviceStatusEmpty extends DeviceStatusState {
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../domain/usecases/get_drone_station_list_usecase.dart';
|
||||
import '../../domain/usecases/get_video_stream_usecase.dart';
|
||||
import '../../domain/usecases/get_uav_video_stream_usecase.dart';
|
||||
import '../../../../../core/network/error_handler.dart';
|
||||
import 'drone_station_event.dart';
|
||||
import 'drone_station_state.dart';
|
||||
|
||||
@@ -33,7 +34,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
final result = await getDroneStationListUseCase(event.siteId);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DroneStationError(failure.message)),
|
||||
(failure) => emit(DroneStationError(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(stations) => emit(DroneStationLoaded(stations)),
|
||||
);
|
||||
}
|
||||
@@ -46,7 +50,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
final result = await getDroneStationListUseCase(event.siteId);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(DroneStationError(failure.message)),
|
||||
(failure) => emit(DroneStationError(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(stations) => emit(DroneStationLoaded(stations)),
|
||||
);
|
||||
}
|
||||
@@ -61,7 +68,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
final result = await getUAVDetailUseCase(event.gatewaySn, event.deviceSn);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(UAVDetailError(failure.message)),
|
||||
(failure) => emit(UAVDetailError(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(detail) => emit(UAVDetailLoaded(detail)),
|
||||
);
|
||||
}
|
||||
@@ -79,7 +89,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(VideoStreamError(failure.message)),
|
||||
(failure) => emit(VideoStreamError(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
|
||||
);
|
||||
}
|
||||
@@ -100,7 +113,10 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
||||
);
|
||||
|
||||
result.fold(
|
||||
(failure) => emit(UavVideoStreamError(failure.message)),
|
||||
(failure) => emit(UavVideoStreamError(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(videoStream) => emit(UavVideoStreamLoaded(
|
||||
videoStream: videoStream,
|
||||
cameraIndex: event.cameraIndex,
|
||||
|
||||
@@ -29,11 +29,15 @@ class DroneStationLoaded extends DroneStationState {
|
||||
|
||||
class DroneStationError extends DroneStationState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const DroneStationError(this.message);
|
||||
const DroneStationError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
class UAVDetailLoading extends DroneStationState {
|
||||
@@ -51,11 +55,15 @@ class UAVDetailLoaded extends DroneStationState {
|
||||
|
||||
class UAVDetailError extends DroneStationState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const UAVDetailError(this.message);
|
||||
const UAVDetailError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
class VideoStreamLoading extends DroneStationState {
|
||||
@@ -74,11 +82,15 @@ class VideoStreamLoaded extends DroneStationState {
|
||||
|
||||
class VideoStreamError extends DroneStationState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const VideoStreamError(this.message);
|
||||
const VideoStreamError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
/// 无人机实时视频流加载状态
|
||||
@@ -105,9 +117,13 @@ class UavVideoStreamLoaded extends DroneStationState {
|
||||
/// 无人机实时视频流加载失败状态
|
||||
class UavVideoStreamError extends DroneStationState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const UavVideoStreamError(this.message);
|
||||
const UavVideoStreamError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../../../../../core/consts/http_api_consts.dart';
|
||||
import '../../../../../core/network/error_handler.dart';
|
||||
import '../../data/models/robot_data_model.dart';
|
||||
import 'robot_list_event.dart';
|
||||
import 'robot_list_state.dart';
|
||||
@@ -19,7 +20,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
|
||||
Emitter<RobotListState> emit,
|
||||
) async {
|
||||
if (event.siteId == null) {
|
||||
emit(const RobotListError('请先选择场站'));
|
||||
emit(const RobotListError(
|
||||
message: '请先选择场站',
|
||||
shouldShowError: true,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -53,7 +57,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
|
||||
siteId: event.siteId,
|
||||
));
|
||||
} catch (e) {
|
||||
emit(RobotListError(e.toString()));
|
||||
emit(RobotListError(
|
||||
message: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -65,7 +72,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
|
||||
final currentState = state as RobotListLoaded;
|
||||
|
||||
if (currentState.siteId == null) {
|
||||
emit(const RobotListError('请先选择场站'));
|
||||
emit(const RobotListError(
|
||||
message: '请先选择场站',
|
||||
shouldShowError: true,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -94,7 +104,10 @@ class RobotListBloc extends Bloc<RobotListEvent, RobotListState> {
|
||||
|
||||
emit(currentState.copyWith(robots: robots));
|
||||
} catch (e) {
|
||||
emit(RobotListError(e.toString()));
|
||||
emit(RobotListError(
|
||||
message: ErrorHandler.getErrorMessage(e),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,9 +45,13 @@ class RobotListLoaded extends RobotListState {
|
||||
|
||||
class RobotListError extends RobotListState {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const RobotListError(this.message);
|
||||
const RobotListError({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
@@ -1,107 +0,0 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:bloc/bloc.dart';
|
||||
import '../model/robot_status_model.dart';
|
||||
import '../service/robot_status_service.dart';
|
||||
import '../manager/float_bar_manager.dart';
|
||||
|
||||
/// 悬浮条事件
|
||||
abstract class FloatBarEvent {}
|
||||
|
||||
/// 切换折叠/展开状态事件
|
||||
class ToggleExpandEvent extends FloatBarEvent {}
|
||||
|
||||
/// 更新状态数据事件
|
||||
class UpdateStatusEvent extends FloatBarEvent {
|
||||
final RobotStatusModel status;
|
||||
|
||||
UpdateStatusEvent(this.status);
|
||||
}
|
||||
|
||||
/// 悬浮条状态
|
||||
abstract class FloatBarState {}
|
||||
|
||||
/// 折叠状态
|
||||
class FloatBarCollapsedState extends FloatBarState {
|
||||
final RobotStatusModel status;
|
||||
|
||||
FloatBarCollapsedState(this.status);
|
||||
}
|
||||
|
||||
/// 展开状态
|
||||
class FloatBarExpandedState extends FloatBarState {
|
||||
final RobotStatusModel status;
|
||||
|
||||
FloatBarExpandedState(this.status);
|
||||
}
|
||||
|
||||
/// 悬浮条Bloc
|
||||
class FloatBarBloc extends Bloc<FloatBarEvent, FloatBarState> {
|
||||
final RobotStatusService _statusService;
|
||||
final FloatBarManager? _floatBarManager;
|
||||
StreamSubscription? _statusSubscription;
|
||||
|
||||
FloatBarBloc(this._statusService, [this._floatBarManager])
|
||||
: super(FloatBarCollapsedState(_statusService.currentStatus)) {
|
||||
// 监听服务层数据流
|
||||
_startListening();
|
||||
|
||||
on<ToggleExpandEvent>(_handleToggleExpand);
|
||||
on<UpdateStatusEvent>(_handleUpdateStatus);
|
||||
}
|
||||
|
||||
/// 开始监听服务层数据
|
||||
void _startListening() {
|
||||
_statusSubscription?.cancel();
|
||||
_statusSubscription = _statusService.statusStream.listen((status) {
|
||||
add(UpdateStatusEvent(status));
|
||||
});
|
||||
}
|
||||
|
||||
/// 处理切换折叠/展开
|
||||
void _handleToggleExpand(
|
||||
ToggleExpandEvent event,
|
||||
Emitter<FloatBarState> emit,
|
||||
) {
|
||||
final currentState = state;
|
||||
if (currentState is FloatBarCollapsedState) {
|
||||
emit(FloatBarExpandedState(currentState.status));
|
||||
} else if (currentState is FloatBarExpandedState) {
|
||||
emit(FloatBarCollapsedState(currentState.status));
|
||||
}
|
||||
// 触发UI刷新(使用 ?. 处理空安全)
|
||||
_floatBarManager?.refresh();
|
||||
}
|
||||
|
||||
/// 处理状态数据更新
|
||||
void _handleUpdateStatus(
|
||||
UpdateStatusEvent event,
|
||||
Emitter<FloatBarState> emit,
|
||||
) {
|
||||
final currentState = state;
|
||||
if (currentState is FloatBarCollapsedState) {
|
||||
emit(FloatBarCollapsedState(event.status));
|
||||
} else if (currentState is FloatBarExpandedState) {
|
||||
emit(FloatBarExpandedState(event.status));
|
||||
}
|
||||
// 触发UI刷新(使用 ?. 处理空安全)
|
||||
_floatBarManager?.refresh();
|
||||
}
|
||||
|
||||
/// 启动状态服务
|
||||
void startService() {
|
||||
_statusService.start();
|
||||
}
|
||||
|
||||
/// 停止状态服务
|
||||
void stopService() {
|
||||
_statusService.stop();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> close() {
|
||||
_statusSubscription?.cancel();
|
||||
_statusService.stop();
|
||||
return super.close();
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// 悬浮条设置服务 - 简化版
|
||||
/// 使用静态变量存储状态,确保全局同步
|
||||
class FloatBarSettingService {
|
||||
final SharedPreferences _prefs;
|
||||
static const String _key = 'float_bar_enabled';
|
||||
|
||||
/// 🔥 静态实例引用
|
||||
static FloatBarSettingService? _instance;
|
||||
|
||||
/// 🔥 静态状态变量 - 所有组件共享
|
||||
static bool _isEnabled = true;
|
||||
|
||||
/// 🔥 静态 ValueNotifier - 用于通知UI变化
|
||||
static final ValueNotifier<bool> _settingNotifier = ValueNotifier<bool>(true);
|
||||
|
||||
FloatBarSettingService(this._prefs) {
|
||||
_instance = this;
|
||||
// 从持久化读取初始状态
|
||||
_isEnabled = _prefs.getBool(_key) ?? true;
|
||||
_settingNotifier.value = _isEnabled;
|
||||
print('✅ [FloatBarSettingService] 初始化完成,初始状态: $_isEnabled');
|
||||
}
|
||||
|
||||
/// 获取静态实例
|
||||
static FloatBarSettingService? get instance => _instance;
|
||||
|
||||
/// 获取 ValueNotifier
|
||||
static ValueNotifier<bool> get settingNotifier => _settingNotifier;
|
||||
|
||||
/// 获取当前是否启用(直接从静态变量读取)
|
||||
static bool get isEnabled => _isEnabled;
|
||||
|
||||
/// 设置是否启用
|
||||
static Future<void> setEnabled(bool enabled) async {
|
||||
print('🔍 [FloatBarSettingService] setEnabled 被调用,新值: $enabled');
|
||||
|
||||
// 1. 更新静态变量
|
||||
_isEnabled = enabled;
|
||||
print('🔍 [FloatBarSettingService] 静态变量已更新: $_isEnabled');
|
||||
|
||||
// 2. 更新 ValueNotifier(通知所有监听者)
|
||||
_settingNotifier.value = enabled;
|
||||
print(
|
||||
'🔍 [FloatBarSettingService] ValueNotifier 已更新: ${_settingNotifier.value}',
|
||||
);
|
||||
|
||||
// 3. 持久化到 SharedPreferences
|
||||
final instance = _instance;
|
||||
if (instance != null) {
|
||||
await instance._prefs.setBool(_key, enabled);
|
||||
print('🔍 [FloatBarSettingService] 已保存到 SharedPreferences');
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换开关
|
||||
static Future<void> toggle() async {
|
||||
await setEnabled(!_isEnabled);
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
/// 悬浮条控制器 - 极简版
|
||||
/// 使用静态变量管理全局状态
|
||||
class FloatBarController {
|
||||
/// 🔥 是否显示悬浮条
|
||||
static bool isVisible = true;
|
||||
|
||||
/// 🔥 状态变化通知器
|
||||
static final ValueNotifier<bool> visibilityNotifier = ValueNotifier<bool>(true);
|
||||
|
||||
/// 设置显示/隐藏
|
||||
static void setVisible(bool visible) {
|
||||
if (isVisible != visible) {
|
||||
isVisible = visible;
|
||||
visibilityNotifier.value = visible;
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换显示状态
|
||||
static void toggle() {
|
||||
setVisible(!isVisible);
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../view/float_bar_widget.dart';
|
||||
|
||||
/// 全局悬浮条管理器
|
||||
/// 单例模式,负责管理OverlayEntry的创建、显示、隐藏和刷新
|
||||
class FloatBarManager {
|
||||
static final FloatBarManager _instance = FloatBarManager._internal();
|
||||
|
||||
factory FloatBarManager() => _instance;
|
||||
|
||||
FloatBarManager._internal();
|
||||
|
||||
/// OverlayEntry实例
|
||||
OverlayEntry? _overlayEntry;
|
||||
|
||||
/// 是否已初始化
|
||||
bool _isInitialized = false;
|
||||
|
||||
/// 全局上下文
|
||||
BuildContext? _globalContext;
|
||||
|
||||
/// 初始化管理器,保存全局上下文
|
||||
void initialize(BuildContext context) {
|
||||
if (_isInitialized) return;
|
||||
_globalContext = context;
|
||||
_isInitialized = true;
|
||||
}
|
||||
|
||||
/// 显示悬浮条
|
||||
void show() {
|
||||
if (!_isInitialized || _globalContext == null) {
|
||||
throw Exception('FloatBarManager has not been initialized!');
|
||||
}
|
||||
|
||||
if (_overlayEntry != null) {
|
||||
// 已有浮层,先移除再重新创建
|
||||
hide();
|
||||
}
|
||||
|
||||
_overlayEntry = OverlayEntry(
|
||||
builder: (context) => const FloatBarWidget(),
|
||||
);
|
||||
|
||||
Overlay.of(_globalContext!)?.insert(_overlayEntry!);
|
||||
}
|
||||
|
||||
/// 隐藏悬浮条
|
||||
void hide() {
|
||||
if (_overlayEntry != null) {
|
||||
_overlayEntry!.remove();
|
||||
_overlayEntry = null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 强制刷新UI
|
||||
void refresh() {
|
||||
_overlayEntry?.markNeedsBuild();
|
||||
}
|
||||
|
||||
/// 检查浮层是否显示中
|
||||
bool get isVisible => _overlayEntry != null;
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
hide();
|
||||
_globalContext = null;
|
||||
_isInitialized = false;
|
||||
}
|
||||
}
|
||||
@@ -1,104 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// 机器人状态数据模型
|
||||
class RobotStatusModel extends Equatable {
|
||||
/// 任务名称
|
||||
final String taskName;
|
||||
|
||||
/// 电量百分比
|
||||
final int battery;
|
||||
|
||||
/// 设备状态:idle/running/charging/error
|
||||
final String status;
|
||||
|
||||
/// 信号强度
|
||||
final int signal;
|
||||
|
||||
/// 当前位置
|
||||
final String location;
|
||||
|
||||
/// 速度
|
||||
final double speed;
|
||||
|
||||
/// 温度
|
||||
final int temperature;
|
||||
|
||||
/// 运行时间
|
||||
final String runTime;
|
||||
|
||||
const RobotStatusModel({
|
||||
this.taskName = '未知任务',
|
||||
this.battery = 100,
|
||||
this.status = 'idle',
|
||||
this.signal = 100,
|
||||
this.location = '未知位置',
|
||||
this.speed = 0.0,
|
||||
this.temperature = 25,
|
||||
this.runTime = '00:00:00',
|
||||
});
|
||||
|
||||
/// 创建副本
|
||||
RobotStatusModel copyWith({
|
||||
String? taskName,
|
||||
int? battery,
|
||||
String? status,
|
||||
int? signal,
|
||||
String? location,
|
||||
double? speed,
|
||||
int? temperature,
|
||||
String? runTime,
|
||||
}) {
|
||||
return RobotStatusModel(
|
||||
taskName: taskName ?? this.taskName,
|
||||
battery: battery ?? this.battery,
|
||||
status: status ?? this.status,
|
||||
signal: signal ?? this.signal,
|
||||
location: location ?? this.location,
|
||||
speed: speed ?? this.speed,
|
||||
temperature: temperature ?? this.temperature,
|
||||
runTime: runTime ?? this.runTime,
|
||||
);
|
||||
}
|
||||
|
||||
/// 状态描述文本
|
||||
String get statusText {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '运行中';
|
||||
case 'charging':
|
||||
return '充电中';
|
||||
case 'error':
|
||||
return '故障';
|
||||
case 'idle':
|
||||
default:
|
||||
return '待机';
|
||||
}
|
||||
}
|
||||
|
||||
/// 状态颜色
|
||||
String get statusColor {
|
||||
switch (status) {
|
||||
case 'running':
|
||||
return '#00C853';
|
||||
case 'charging':
|
||||
return '#03DAC6';
|
||||
case 'error':
|
||||
return '#FF5252';
|
||||
case 'idle':
|
||||
default:
|
||||
return '#9E9E9E';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
taskName,
|
||||
battery,
|
||||
status,
|
||||
signal,
|
||||
location,
|
||||
speed,
|
||||
temperature,
|
||||
runTime,
|
||||
];
|
||||
}
|
||||
@@ -1,101 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import '../model/robot_status_model.dart';
|
||||
|
||||
/// 机器人状态服务
|
||||
/// 负责模拟设备状态推送,实际项目中应替换为真实的TCP/接口对接
|
||||
class RobotStatusService {
|
||||
static final RobotStatusService _instance = RobotStatusService._internal();
|
||||
|
||||
factory RobotStatusService() => _instance;
|
||||
|
||||
RobotStatusService._internal();
|
||||
|
||||
/// 状态数据流控制器
|
||||
final StreamController<RobotStatusModel> _statusController =
|
||||
StreamController.broadcast();
|
||||
|
||||
/// 当前状态
|
||||
RobotStatusModel _currentStatus = const RobotStatusModel();
|
||||
|
||||
/// 模拟定时器
|
||||
Timer? _timer;
|
||||
|
||||
/// 状态数据流
|
||||
Stream<RobotStatusModel> get statusStream => _statusController.stream;
|
||||
|
||||
/// 获取当前状态
|
||||
RobotStatusModel get currentStatus => _currentStatus;
|
||||
|
||||
/// 启动状态推送
|
||||
void start() {
|
||||
if (_timer != null) return;
|
||||
|
||||
// 立即发送初始状态
|
||||
_statusController.add(_currentStatus);
|
||||
|
||||
// 模拟每3秒更新一次状态
|
||||
_timer = Timer.periodic(const Duration(seconds: 3), (timer) {
|
||||
_simulateStatusUpdate();
|
||||
});
|
||||
}
|
||||
|
||||
/// 停止状态推送
|
||||
void stop() {
|
||||
_timer?.cancel();
|
||||
_timer = null;
|
||||
}
|
||||
|
||||
/// 手动更新状态(用于外部触发更新)
|
||||
void updateStatus(RobotStatusModel status) {
|
||||
_currentStatus = status;
|
||||
_statusController.add(status);
|
||||
}
|
||||
|
||||
/// 模拟状态更新
|
||||
void _simulateStatusUpdate() {
|
||||
final random = Random();
|
||||
final statuses = ['idle', 'running', 'charging', 'error'];
|
||||
|
||||
_currentStatus = _currentStatus.copyWith(
|
||||
battery: max(0, _currentStatus.battery + random.nextInt(3) - 1),
|
||||
status: random.nextDouble() > 0.95 ? statuses[random.nextInt(statuses.length)] : _currentStatus.status,
|
||||
signal: min(100, max(0, _currentStatus.signal + random.nextInt(5) - 2)),
|
||||
speed: _currentStatus.status == 'running' ? random.nextDouble() * 5 : 0,
|
||||
temperature: min(50, max(20, _currentStatus.temperature + random.nextInt(3) - 1)),
|
||||
runTime: _updateRunTime(),
|
||||
);
|
||||
|
||||
_statusController.add(_currentStatus);
|
||||
}
|
||||
|
||||
/// 更新运行时间
|
||||
String _updateRunTime() {
|
||||
if (_currentStatus.status != 'running') {
|
||||
return _currentStatus.runTime;
|
||||
}
|
||||
|
||||
final parts = _currentStatus.runTime.split(':');
|
||||
int hours = int.parse(parts[0]);
|
||||
int minutes = int.parse(parts[1]);
|
||||
int seconds = int.parse(parts[2]);
|
||||
|
||||
seconds++;
|
||||
if (seconds >= 60) {
|
||||
seconds = 0;
|
||||
minutes++;
|
||||
}
|
||||
if (minutes >= 60) {
|
||||
minutes = 0;
|
||||
hours++;
|
||||
}
|
||||
|
||||
return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';
|
||||
}
|
||||
|
||||
/// 释放资源
|
||||
void dispose() {
|
||||
stop();
|
||||
_statusController.close();
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'float_bar_controller.dart';
|
||||
|
||||
/// 简单悬浮条组件
|
||||
class SimpleFloatBar extends StatefulWidget {
|
||||
const SimpleFloatBar({super.key});
|
||||
|
||||
@override
|
||||
State<SimpleFloatBar> createState() => _SimpleFloatBarState();
|
||||
}
|
||||
|
||||
class _SimpleFloatBarState extends State<SimpleFloatBar> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100),
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: _isExpanded ? 200 : 56,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFE8F5E9), Color(0xFFFFFFFF)],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.green.withOpacity(0.2), width: 1),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 1,
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: _isExpanded
|
||||
? SingleChildScrollView(child: _buildExpanded())
|
||||
: _buildCollapsed(),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildCollapsed() {
|
||||
return Row(children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.black87))),
|
||||
const SizedBox(width: 12),
|
||||
Row(children: const [Icon(Icons.battery_full, size: 18, color: Colors.grey), SizedBox(width: 4), Text('85%', style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500))]),
|
||||
const SizedBox(width: 8),
|
||||
const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
_buildCloseButton(),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildExpanded() {
|
||||
return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Row(children: [
|
||||
Container(width: 10, height: 10, decoration: BoxDecoration(color: Colors.green, borderRadius: BorderRadius.circular(5))),
|
||||
const SizedBox(width: 8),
|
||||
Container(padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration(color: Colors.green.withOpacity(0.1), borderRadius: BorderRadius.circular(4)), child: const Text('运行中', style: TextStyle(fontSize: 12, color: Colors.green, fontWeight: FontWeight.w500))),
|
||||
const SizedBox(width: 12),
|
||||
const Expanded(child: Text('设备运行中', style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: Colors.black87))),
|
||||
const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
_buildCloseButton(),
|
||||
]),
|
||||
const SizedBox(height: 16),
|
||||
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
|
||||
_infoItem(Icons.battery_full, '电量', '85%'),
|
||||
_infoItem(Icons.signal_cellular_alt, '信号', '100%'),
|
||||
_infoItem(Icons.speed, '速度', '0.0m/s'),
|
||||
_infoItem(Icons.thermostat, '温度', '25°C'),
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Row(children: const [
|
||||
Icon(Icons.location_on, size: 14, color: Colors.grey),
|
||||
SizedBox(width: 4),
|
||||
Expanded(child: Text('北京市朝阳区', style: TextStyle(fontSize: 12, color: Colors.grey))),
|
||||
SizedBox(width: 12),
|
||||
Icon(Icons.timer, size: 14, color: Colors.grey),
|
||||
SizedBox(width: 4),
|
||||
Text('02:35:18', style: TextStyle(fontSize: 12, color: Colors.grey)),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
Widget _buildCloseButton() {
|
||||
return GestureDetector(
|
||||
onTap: () => FloatBarController.setVisible(false),
|
||||
child: Container(padding: const EdgeInsets.all(4), decoration: BoxDecoration(color: Colors.grey.withOpacity(0.1), borderRadius: BorderRadius.circular(8)), child: const Icon(Icons.close, size: 16, color: Colors.grey)),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _infoItem(IconData icon, String label, String value) {
|
||||
return Column(mainAxisSize: MainAxisSize.min, children: [
|
||||
Icon(icon, size: 20, color: Colors.grey),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
const SizedBox(height: 2),
|
||||
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w600)),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,350 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import '../bloc/float_bar_bloc.dart';
|
||||
import '../model/robot_status_model.dart';
|
||||
import '../service/robot_status_service.dart';
|
||||
import '../cubit/float_bar_setting_cubit.dart';
|
||||
|
||||
/// 悬浮条UI组件
|
||||
/// 内部独立管理Bloc,不依赖外部注入
|
||||
class FloatBarWidget extends StatefulWidget {
|
||||
const FloatBarWidget({super.key});
|
||||
|
||||
@override
|
||||
State<FloatBarWidget> createState() => _FloatBarWidgetState();
|
||||
}
|
||||
|
||||
class _FloatBarWidgetState extends State<FloatBarWidget> {
|
||||
late final FloatBarBloc _bloc;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
debugPrint('🔥 [FloatBarWidget] initState - 开始创建 Bloc');
|
||||
// 内部创建Bloc并启动服务(不需要FloatBarManager,因为我们使用Stack方式)
|
||||
_bloc = FloatBarBloc(RobotStatusService());
|
||||
_bloc.startService();
|
||||
debugPrint('✅ [FloatBarWidget] Bloc 已创建并启动服务');
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
debugPrint('🔥 [FloatBarWidget] dispose - 关闭 Bloc');
|
||||
_bloc.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
debugPrint('🔥 [FloatBarWidget] build - 渲染悬浮条');
|
||||
return BlocProvider.value(value: _bloc, child: const _FloatBarContent());
|
||||
}
|
||||
}
|
||||
|
||||
/// 悬浮条内容组件
|
||||
class _FloatBarContent extends StatelessWidget {
|
||||
const _FloatBarContent();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<FloatBarBloc, FloatBarState>(
|
||||
builder: (context, state) {
|
||||
final isExpanded = state is FloatBarExpandedState;
|
||||
final status = state is FloatBarCollapsedState
|
||||
? state.status
|
||||
: (state as FloatBarExpandedState).status;
|
||||
|
||||
debugPrint(
|
||||
'🔥 [FloatBarContent] build - isExpanded: $isExpanded, status: ${status.taskName}',
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 100), // 底部留出 Tab 栏空间
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('🔥 [FloatBarContent] 点击悬浮条');
|
||||
context.read<FloatBarBloc>().add(ToggleExpandEvent());
|
||||
},
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
child: AnimatedContainer(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeInOut,
|
||||
height: isExpanded ? 180 : 56,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Color(0xFFE8F5E9), // 浅绿色
|
||||
Color(0xFFFFFFFF), // 白色
|
||||
],
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: Colors.green.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.12),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
spreadRadius: 2,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.08),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
spreadRadius: 1,
|
||||
),
|
||||
BoxShadow(
|
||||
color: Colors.white.withOpacity(0.6),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
spreadRadius: -2,
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
child: isExpanded
|
||||
? _buildExpandedContent(status)
|
||||
: _buildCollapsedContent(status),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
/// 折叠态内容
|
||||
Widget _buildCollapsedContent(RobotStatusModel status) {
|
||||
return Row(
|
||||
children: [
|
||||
// 状态指示灯
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: _parseColor(status.statusColor),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 任务名称
|
||||
Expanded(
|
||||
child: Text(
|
||||
status.taskName,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
// 电量
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
status.battery > 20 ? Icons.battery_full : Icons.battery_alert,
|
||||
size: 18,
|
||||
color: status.battery > 20 ? Colors.grey : Colors.red,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'${status.battery}%',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: status.battery > 20 ? Colors.black87 : Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
// 展开箭头
|
||||
const Icon(Icons.keyboard_arrow_up, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
// 关闭按钮
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('🔥 [FloatBarContent] 点击关闭按钮');
|
||||
// 🔥 使用静态方法关闭悬浮条
|
||||
FloatBarSettingService.setEnabled(false);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.close, size: 16, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 展开态内容
|
||||
Widget _buildExpandedContent(RobotStatusModel status) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 顶部:状态+任务名+折叠按钮
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
width: 10,
|
||||
height: 10,
|
||||
decoration: BoxDecoration(
|
||||
color: _parseColor(status.statusColor),
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: _parseColor(status.statusColor).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
status.statusText,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: _parseColor(status.statusColor),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Text(
|
||||
status.taskName,
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Colors.black87,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const Icon(Icons.keyboard_arrow_down, size: 20, color: Colors.grey),
|
||||
const SizedBox(width: 8),
|
||||
// 关闭按钮
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
debugPrint('🔥 [FloatBarContent] 点击关闭按钮(展开态)');
|
||||
// 🔥 使用静态方法关闭悬浮条
|
||||
FloatBarSettingService.setEnabled(false);
|
||||
},
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: const Icon(Icons.close, size: 16, color: Colors.grey),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
// 中间:详细信息网格
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
children: [
|
||||
_buildInfoItem(
|
||||
status.battery > 20 ? Icons.battery_full : Icons.battery_alert,
|
||||
'电量',
|
||||
'${status.battery}%',
|
||||
status.battery > 20 ? Colors.black87 : Colors.red,
|
||||
),
|
||||
_buildInfoItem(
|
||||
Icons.signal_cellular_alt,
|
||||
'信号',
|
||||
'${status.signal}%',
|
||||
Colors.black87,
|
||||
),
|
||||
_buildInfoItem(
|
||||
Icons.speed,
|
||||
'速度',
|
||||
'${status.speed.toStringAsFixed(1)}m/s',
|
||||
Colors.black87,
|
||||
),
|
||||
_buildInfoItem(
|
||||
Icons.thermostat,
|
||||
'温度',
|
||||
'${status.temperature}°C',
|
||||
Colors.black87,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 底部:位置+时间
|
||||
Row(
|
||||
children: [
|
||||
const Icon(Icons.location_on, size: 14, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
status.location,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
const Icon(Icons.timer, size: 14, color: Colors.grey),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
status.runTime,
|
||||
style: const TextStyle(fontSize: 12, color: Colors.grey),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 信息项组件
|
||||
Widget _buildInfoItem(
|
||||
IconData icon,
|
||||
String label,
|
||||
String value,
|
||||
Color valueColor,
|
||||
) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 20, color: Colors.grey),
|
||||
const SizedBox(height: 4),
|
||||
Text(label, style: const TextStyle(fontSize: 10, color: Colors.grey)),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: valueColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 解析颜色字符串
|
||||
Color _parseColor(String colorStr) {
|
||||
try {
|
||||
return Color(int.parse(colorStr.replaceFirst('#', '0xFF')));
|
||||
} catch (_) {
|
||||
return Colors.grey;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import '../widgets/device_item_widget.dart';
|
||||
import '../widgets/drone_station_item_card.dart';
|
||||
import 'robot_list_page.dart';
|
||||
import 'drone_station_detail_page.dart';
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
|
||||
/// 设备状态页面 - 使用 BLoC 模式
|
||||
class DeviceStatusPage extends StatelessWidget {
|
||||
@@ -50,22 +51,35 @@ class DeviceStatusView extends StatelessWidget {
|
||||
child: Scaffold(
|
||||
backgroundColor: const Color(0xFFF7F7F7),
|
||||
body: SafeArea(
|
||||
child:
|
||||
BlocBuilder<
|
||||
DeviceListBloc.DeviceStatusBloc,
|
||||
DeviceListState.DeviceStatusState
|
||||
>(
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAppBar(context),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(child: _buildContent(context, state)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
child: BlocConsumer<
|
||||
DeviceListBloc.DeviceStatusBloc,
|
||||
DeviceListState.DeviceStatusState
|
||||
>(
|
||||
listener: (context, state) {
|
||||
// 🔥 监听错误状态,显示友好提示
|
||||
if (state is DeviceListState.DeviceStatusError &&
|
||||
state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildAppBar(context),
|
||||
_buildSearchBar(context),
|
||||
_buildTypeFilterBar(context),
|
||||
Expanded(child: _buildContent(context, state)),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -264,33 +278,11 @@ class DeviceStatusView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
|
||||
// 页面保持当前内容,用户可以继续操作
|
||||
if (state is DeviceListState.DeviceStatusError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969)),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DeviceListBloc.DeviceStatusBloc>().add(
|
||||
const DeviceListEvent.DeviceStatusLoadData(),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// 如果是从 Loaded 状态变成 Error,返回空容器保持页面
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (state is DeviceListState.DeviceStatusLoaded) {
|
||||
@@ -375,7 +367,20 @@ class DeviceStatusView extends StatelessWidget {
|
||||
return BlocProvider(
|
||||
create: (_) =>
|
||||
sl<DroneStationBloc>()..add(DroneStationLoadData(selectedSite.id)),
|
||||
child: BlocBuilder<DroneStationBloc, DroneStationState>(
|
||||
child: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 监听错误状态,显示友好提示
|
||||
if (state is DroneStationError && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is DroneStationLoading) {
|
||||
return const Center(
|
||||
@@ -383,40 +388,10 @@ class DeviceStatusView extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
|
||||
if (state is DroneStationError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
state.message,
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
context.read<DroneStationBloc>().add(
|
||||
DroneStationLoadData(selectedSite.id),
|
||||
);
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF165DFF),
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// 返回空容器保持页面
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (state is DroneStationLoaded) {
|
||||
@@ -572,8 +547,11 @@ class DeviceStatusView extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(value: context.read<DeviceStatusBloc>()),
|
||||
BlocProvider.value(value: sl<RemoteControlCubit>()),
|
||||
],
|
||||
child: const DeviceStatusModal(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -11,9 +11,8 @@ import '../../domain/usecases/update_flight_task_status_usecase.dart';
|
||||
/// 无人机任务与航线控制页面
|
||||
class DroneMissionControlPage extends StatefulWidget {
|
||||
final List<FlightTaskEntity>? selectedTasks;
|
||||
final String? droneSn; // 无人机序列号
|
||||
|
||||
const DroneMissionControlPage({super.key, this.selectedTasks, this.droneSn});
|
||||
const DroneMissionControlPage({super.key, this.selectedTasks});
|
||||
|
||||
@override
|
||||
State<DroneMissionControlPage> createState() =>
|
||||
@@ -23,12 +22,7 @@ class DroneMissionControlPage extends StatefulWidget {
|
||||
class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
FlightTaskEntity? _listTask; // 列表数据
|
||||
FlightTaskDetailEntity? _detailTask; // 详情数据
|
||||
String? _droneSn; // 无人机序列号
|
||||
bool _isLoading = false;
|
||||
bool _isReturningHome = false; // 是否正在返航
|
||||
bool _isPausing = false; // 是否正在暂停
|
||||
bool _isReturnHomeLoading = false; // 返航命令是否正在执行
|
||||
bool _isPauseLoading = false; // 暂停命令是否正在执行
|
||||
final Dio _dio = Dio();
|
||||
|
||||
@override
|
||||
@@ -38,7 +32,6 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
_listTask = widget.selectedTasks!.first;
|
||||
_loadTaskDetail();
|
||||
}
|
||||
_droneSn = widget.droneSn; // 初始化无人机序列号
|
||||
}
|
||||
|
||||
Future<void> _loadTaskDetail() async {
|
||||
@@ -78,7 +71,6 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
|
||||
final detailData = jsonData['data'];
|
||||
print('🔍 [DroneMissionControl] data 字段类型: ${detailData.runtimeType}');
|
||||
print('🔍 [DroneMissionControl] data 字段完整内容: $detailData');
|
||||
|
||||
// 确保 data 也是 Map
|
||||
final Map<String, dynamic> detailMap = (detailData is Map)
|
||||
@@ -126,7 +118,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
print('❌ [DroneMissionControl] 执行任务失败: ${failure.message}');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
|
||||
).showSnackBar(SnackBar(content: Text(failure.message)));
|
||||
},
|
||||
(data) {
|
||||
print('✅ [DroneMissionControl] 任务执行成功: $data');
|
||||
@@ -139,105 +131,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
print('❌ [DroneMissionControl] 执行任务异常: $e');
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
|
||||
}
|
||||
}
|
||||
|
||||
// 返航/取消返航
|
||||
Future<void> _toggleReturnHome() async {
|
||||
if (_detailTask == null || _detailTask!.sn.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('设备SN不存在')));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isReturnHomeLoading = true);
|
||||
|
||||
try {
|
||||
final command = _isReturningHome ? 'return_home_cancel' : 'return_home';
|
||||
print('🔍 [DroneMissionControl] 发送返航命令: $command, deviceSn: $_droneSn');
|
||||
|
||||
final response = await _dio.post(
|
||||
HttpApiConsts.flightTaskCommand,
|
||||
data: {'command': command, 'deviceSn': _droneSn},
|
||||
);
|
||||
|
||||
print('📦 [DroneMissionControl] 返航响应: ${response.data}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
if (responseData['code'] == 0) {
|
||||
setState(() => _isReturningHome = !_isReturningHome);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(_isReturningHome ? '已下发返航命令' : '已取消返航')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(responseData['message'] ?? '操作失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('请求失败');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ [DroneMissionControl] 返航操作失败: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isReturnHomeLoading = false);
|
||||
}
|
||||
}
|
||||
|
||||
// 暂停/取消暂停
|
||||
Future<void> _togglePause() async {
|
||||
if (_detailTask == null || _detailTask!.sn.isEmpty) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('设备SN不存在')));
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() => _isPauseLoading = true);
|
||||
|
||||
try {
|
||||
final command = _isPausing ? 'flighttask_recovery' : 'flighttask_pause';
|
||||
print('🔍 [DroneMissionControl] 发送暂停命令: $command, deviceSn: $_droneSn');
|
||||
|
||||
final response = await _dio.post(
|
||||
HttpApiConsts.flightTaskCommand,
|
||||
data: {'command': command, 'deviceSn': _droneSn},
|
||||
);
|
||||
|
||||
print('📦 [DroneMissionControl] 暂停响应: ${response.data}');
|
||||
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = response.data;
|
||||
if (responseData['code'] == 0) {
|
||||
setState(() => _isPausing = !_isPausing);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(_isPausing ? '已下发暂停命令' : '已取消暂停')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
throw Exception(responseData['message'] ?? '操作失败');
|
||||
}
|
||||
} else {
|
||||
throw Exception('请求失败');
|
||||
}
|
||||
} catch (e) {
|
||||
print('❌ [DroneMissionControl] 暂停操作失败: $e');
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(const SnackBar(content: Text('下发指令失败')));
|
||||
}
|
||||
} finally {
|
||||
setState(() => _isPauseLoading = false);
|
||||
).showSnackBar(SnackBar(content: Text('执行任务失败: $e')));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,7 +256,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('网关序列号', task.sn),
|
||||
_buildInfoRow('设备序列号', task.sn),
|
||||
const SizedBox(height: 12),
|
||||
_buildInfoRow('任务类型', task.taskType),
|
||||
const SizedBox(height: 12),
|
||||
@@ -628,7 +522,7 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: _isPauseLoading ? null : _togglePause,
|
||||
onPressed: () {},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFFFF7D00),
|
||||
foregroundColor: Colors.white,
|
||||
@@ -638,28 +532,16 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
child: _isPauseLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
_isPausing ? '取消暂停' : '暂停任务',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'暂停任务',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: _isReturnHomeLoading ? null : _toggleReturnHome,
|
||||
onPressed: () {},
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: const Color(0xFF4E5969),
|
||||
side: const BorderSide(color: Color(0xFFC9CDD4)),
|
||||
@@ -668,19 +550,10 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
child: _isReturnHomeLoading
|
||||
? const SizedBox(
|
||||
height: 20,
|
||||
width: 20,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: Text(
|
||||
_isReturningHome ? '取消返航' : '返航降落',
|
||||
style: const TextStyle(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
child: const Text(
|
||||
'返航降落',
|
||||
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -28,8 +28,10 @@ class DroneStationDetailPage extends StatefulWidget {
|
||||
|
||||
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
late DroneStationBloc _bloc;
|
||||
UAVDetailEntity? _detail; // 无人机详情数据
|
||||
String? _droneSn; // 无人机序列号
|
||||
|
||||
// 无人机详情数据
|
||||
UAVDetailEntity? _detail;
|
||||
String? _droneSn;
|
||||
|
||||
// 悬浮视频监控状态
|
||||
bool showFloatingMonitor = false;
|
||||
@@ -55,7 +57,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
// 加载超时计时器
|
||||
Timer? _floatingLoadingTimer;
|
||||
static const _floatingLoadingTimeout = Duration(seconds: 15);
|
||||
|
||||
|
||||
// 无人机状态轮询计时器
|
||||
Timer? _droneStatusPollingTimer;
|
||||
|
||||
@@ -69,7 +71,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// 启动无人机状态轮询(每5秒刷新一次)
|
||||
_startDroneStatusPolling();
|
||||
}
|
||||
@@ -81,15 +83,15 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
debugPrint(' - 是否已显示: $showFloatingMonitor');
|
||||
debugPrint(' - 无人机在线: ${detail.droneOnlineStatus}');
|
||||
debugPrint(' - 机场摄像头: ${detail.gatewayCameraList?.length ?? 0}');
|
||||
|
||||
|
||||
if (!_isFloatingMonitorEnabled || showFloatingMonitor) {
|
||||
debugPrint('❌ 不满足条件,退出检查');
|
||||
return; // 开关关闭或已显示,不执行
|
||||
}
|
||||
|
||||
|
||||
// 无人机在线且有机场摄像头,自动打开悬浮窗
|
||||
if (detail.droneOnlineStatus == 1 &&
|
||||
detail.gatewayCameraList != null &&
|
||||
if (detail.droneOnlineStatus == 1 &&
|
||||
detail.gatewayCameraList != null &&
|
||||
detail.gatewayCameraList!.isNotEmpty) {
|
||||
debugPrint('✅ 检测到无人机在线,自动打开悬浮窗');
|
||||
_loadFloatingVideoStream();
|
||||
@@ -109,13 +111,30 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
|
||||
/// 启动无人机状态轮询
|
||||
void _startDroneStatusPolling() {
|
||||
// 每5秒刷新一次无人机状态
|
||||
_droneStatusPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||
if (!mounted) {
|
||||
timer.cancel();
|
||||
return;
|
||||
_scheduleDroneStatusPoll();
|
||||
}
|
||||
|
||||
/// 根据无人机状态动态调整轮询周期
|
||||
void _scheduleDroneStatusPoll() {
|
||||
if (!mounted) return;
|
||||
|
||||
// 检查当前无人机状态
|
||||
Duration interval = const Duration(seconds: 60); // 默认60秒
|
||||
final currentState = _bloc.state;
|
||||
if (currentState is UAVDetailLoaded) {
|
||||
if (currentState.detail.droneOnlineStatus == 1) {
|
||||
// 无人机在线时,每15秒轮询一次
|
||||
interval = const Duration(seconds: 15);
|
||||
} else {
|
||||
// 无人机离线时,每60秒轮询一次(降低频率)
|
||||
interval = const Duration(seconds: 60);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_droneStatusPollingTimer?.cancel();
|
||||
_droneStatusPollingTimer = Timer(interval, () {
|
||||
if (!mounted) return;
|
||||
|
||||
debugPrint('🔄 定时刷新无人机状态...');
|
||||
_bloc.add(
|
||||
UAVDetailLoad(
|
||||
@@ -123,17 +142,20 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
deviceSn: widget.station.deviceSn,
|
||||
),
|
||||
);
|
||||
|
||||
|
||||
// 如果悬浮窗开启且无人机上线,自动显示悬浮窗
|
||||
if (_isFloatingMonitorEnabled) {
|
||||
final currentState = _bloc.state;
|
||||
if (currentState is UAVDetailLoaded &&
|
||||
currentState.detail.droneOnlineStatus == 1 &&
|
||||
final state = _bloc.state;
|
||||
if (state is UAVDetailLoaded &&
|
||||
state.detail.droneOnlineStatus == 1 &&
|
||||
!showFloatingMonitor) {
|
||||
debugPrint('✅ 无人机已上线,自动打开悬浮窗');
|
||||
_loadFloatingVideoStream();
|
||||
}
|
||||
}
|
||||
|
||||
// 重新调度下一次轮询(动态周期)
|
||||
_scheduleDroneStatusPoll();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -203,10 +225,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
_initFloatingRtcEngine();
|
||||
} else if (state is VideoStreamError) {
|
||||
_floatingLoadingTimer?.cancel();
|
||||
setState(() {
|
||||
_floatingErrorMessage = state.message;
|
||||
_isFloatingLoading = false;
|
||||
});
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
@@ -674,7 +693,6 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
// 悬浮观看功能已禁用
|
||||
// GestureDetector(
|
||||
// onTap: () {
|
||||
@@ -739,10 +757,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => DroneMissionControlPage(
|
||||
selectedTasks: tasks,
|
||||
droneSn: _droneSn,
|
||||
),
|
||||
builder: (context) =>
|
||||
DroneMissionControlPage(selectedTasks: tasks),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -46,7 +46,20 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<RobotListBloc, RobotListState>(
|
||||
return BlocConsumer<RobotListBloc, RobotListState>(
|
||||
listener: (context, state) {
|
||||
// 🔥 监听错误状态,显示友好提示
|
||||
if (state is RobotListError && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
if (state is RobotListLoading) {
|
||||
return const Center(
|
||||
@@ -56,27 +69,10 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
);
|
||||
}
|
||||
|
||||
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
|
||||
if (state is RobotListError) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFFF53F3F),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'加载失败: ${state.message}',
|
||||
style: const TextStyle(
|
||||
fontSize: 14,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
// 返回空容器保持页面
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (state is RobotListLoaded) {
|
||||
@@ -480,9 +476,11 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
status: robot.status,
|
||||
battery: robot.battery,
|
||||
task: robot.task,
|
||||
onTap: () {
|
||||
onTap: () async {
|
||||
// final logger = sl<ILoggerService>();
|
||||
debugPrint('📱 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}, status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
|
||||
debugPrint('🔴🔴🔴 [选中机器人] ========== 点击事件触发 ==========');
|
||||
debugPrint('🔴🔴🔴 [选中机器人] name: ${robot.name}, id: ${robot.id}, type: ${robot.type}');
|
||||
debugPrint('🔴🔴🔴 [选中机器人] status: ${robot.status}, battery: ${robot.battery}, task: ${robot.task}');
|
||||
|
||||
// 1. 将当前机器人设置为全局待控制设备(用 robot.name 作为 deviceName)
|
||||
final device = DeviceEntity(
|
||||
@@ -495,9 +493,11 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
onlineStatus: robot.status == '在线' ? 1 : 0,
|
||||
);
|
||||
|
||||
debugPrint('🎯 [RobotListPage] 准备调用 setTargetDevice...');
|
||||
// 🔥 使用 GetIt 直接获取 RemoteControlCubit 单例
|
||||
final remoteCubit = GetIt.I<RemoteControlCubit>();
|
||||
remoteCubit.setTargetDevice(device);
|
||||
remoteCubit.setTargetDevice(device); // 🔥 TCP连接在后台异步执行
|
||||
debugPrint('✅ [RobotListPage] setTargetDevice 已调用(TCP连接中)');
|
||||
|
||||
// 2. 跳转到机器人控制页面
|
||||
final robotMap = {
|
||||
@@ -509,6 +509,7 @@ class _RobotListViewState extends State<RobotListView> {
|
||||
'task': robot.task,
|
||||
};
|
||||
|
||||
debugPrint('🚀 [RobotListPage] 准备跳转页面...');
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
|
||||
@@ -3,9 +3,7 @@ import '../pages/drone_mission_control_page.dart';
|
||||
|
||||
/// 无人机机场与设备状态组件
|
||||
class DroneStationStatusWidget extends StatelessWidget {
|
||||
final String? droneSn; // 无人机序列号
|
||||
|
||||
const DroneStationStatusWidget({super.key, this.droneSn});
|
||||
const DroneStationStatusWidget({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -222,8 +220,7 @@ class DroneStationStatusWidget extends StatelessWidget {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
DroneMissionControlPage(droneSn: droneSn),
|
||||
builder: (context) => const DroneMissionControlPage(),
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -6,6 +6,7 @@ import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_home_data_
|
||||
import 'package:maibu_satabot_v2/features/v2/home/domain/usecases/get_site_list_usecase.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_state.dart';
|
||||
import '../../../../../core/network/error_handler.dart';
|
||||
|
||||
class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
|
||||
final GetHomeDataUseCase getHomeDataUseCase;
|
||||
@@ -27,7 +28,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
|
||||
|
||||
final user = appUserCubit.state.user;
|
||||
if (user == null) {
|
||||
emit(const HomeV2Error('用户未登录'));
|
||||
emit(const HomeV2Error(
|
||||
message: '用户未登录',
|
||||
shouldShowError: true,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -36,7 +40,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
|
||||
final siteResult = await getSiteListUseCase(user.orgId); // 使用用户的 orgId
|
||||
|
||||
homeResult.fold(
|
||||
(failure) => emit(HomeV2Error(failure.message)),
|
||||
(failure) => emit(HomeV2Error(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(homeData) {
|
||||
List<SiteEntity> sites = [];
|
||||
SiteEntity? selectedSite;
|
||||
@@ -87,7 +94,10 @@ class HomeV2Bloc extends Bloc<HomeV2Event, HomeV2State> {
|
||||
final homeResult = await getHomeDataUseCase(const NoParams());
|
||||
|
||||
homeResult.fold(
|
||||
(failure) => emit(HomeV2Error(failure.message)),
|
||||
(failure) => emit(HomeV2Error(
|
||||
message: ErrorHandler.getErrorMessage(failure.message),
|
||||
shouldShowError: true, // 🔥 标记需要显示弹窗
|
||||
)),
|
||||
(homeData) {
|
||||
emit(HomeV2Loaded(
|
||||
homeData: homeData,
|
||||
|
||||
@@ -50,9 +50,13 @@ class HomeV2Loaded extends HomeV2State {
|
||||
|
||||
class HomeV2Error extends HomeV2State {
|
||||
final String message;
|
||||
final bool shouldShowError; // 🔥 标记是否需要显示错误弹窗
|
||||
|
||||
const HomeV2Error(this.message);
|
||||
const HomeV2Error({
|
||||
required this.message,
|
||||
this.shouldShowError = false, // 默认 false
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => [message, shouldShowError];
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:maibu_satabot_v2/core/di/injection.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_bloc.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/home/presentation/bloc/home_v2_event.dart';
|
||||
@@ -15,6 +16,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_statu
|
||||
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
|
||||
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
|
||||
|
||||
import '../../../../remote_control/presentation/bloc/remote_control_cubit.dart';
|
||||
|
||||
class HomeV2Page extends StatefulWidget {
|
||||
const HomeV2Page({super.key});
|
||||
|
||||
@@ -35,29 +38,26 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: const Color(0xFFF5F7FA),
|
||||
body: BlocBuilder<HomeV2Bloc, HomeV2State>(
|
||||
builder: (context, state) {
|
||||
if (state is HomeV2Error) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
Icons.error_outline,
|
||||
size: 48,
|
||||
color: Color(0xFF86909C),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(state.message),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: () => _bloc.add(const HomeV2LoadData()),
|
||||
child: const Text('重试'),
|
||||
),
|
||||
],
|
||||
body: BlocConsumer<HomeV2Bloc, HomeV2State>(
|
||||
listener: (context, state) {
|
||||
// 🔥 监听错误状态,显示友好提示
|
||||
if (state is HomeV2Error && state.shouldShowError) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(state.message),
|
||||
duration: const Duration(seconds: 2),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
backgroundColor: Colors.orange,
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
// 🔥 错误状态不再显示全屏错误页面,而是通过 listener 显示 SnackBar
|
||||
if (state is HomeV2Error) {
|
||||
// 返回空容器保持页面
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (state is HomeV2Loaded) {
|
||||
return RefreshIndicator(
|
||||
@@ -345,8 +345,15 @@ class _HomeV2PageState extends State<HomeV2Page> {
|
||||
),
|
||||
),
|
||||
builder: (BuildContext context) {
|
||||
return BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
return MultiBlocProvider(
|
||||
providers: [
|
||||
BlocProvider.value(
|
||||
value: context.read<DeviceStatusBloc>(),
|
||||
),
|
||||
BlocProvider.value(
|
||||
value: GetIt.I<RemoteControlCubit>(), // 🔥 注入 RemoteControlCubit
|
||||
),
|
||||
],
|
||||
child: const DeviceStatusModal(),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -86,29 +86,33 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
|
||||
message: tooltip,
|
||||
child: InkWell(
|
||||
onTap: widget.onTap,
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation.value : 1.0;
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
// 🔥 增大点击热区
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: AnimatedBuilder(
|
||||
animation: _animation,
|
||||
builder: (context, child) {
|
||||
final opacity = shouldAnimate ? _animation.value : 1.0;
|
||||
return Container(
|
||||
width: 12,
|
||||
height: 12,
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(opacity),
|
||||
shape: BoxShape.circle,
|
||||
boxShadow: state.status == TcpConnectionStatus.connected
|
||||
? [
|
||||
BoxShadow(
|
||||
color: Colors.green.withOpacity(0.5 * opacity),
|
||||
blurRadius: 6 * opacity,
|
||||
spreadRadius: 2 * opacity,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -8,9 +8,6 @@ import 'package:maibu_satabot_v2/core/router/route_paths.dart';
|
||||
import 'package:maibu_satabot_v2/core/storage/user_storage.dart';
|
||||
import 'package:maibu_satabot_v2/features/auth/presentation/bloc/auth_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/main_container/presentation/cubit/tab_config_cubit.dart';
|
||||
import 'package:maibu_satabot_v2/features/v2/device_list/presentation/float_bar/float_bar_controller.dart';
|
||||
|
||||
final sl = GetIt.instance;
|
||||
|
||||
/// 系统设置综合页面
|
||||
class SystemSettingsPage extends StatefulWidget {
|
||||
@@ -21,17 +18,6 @@ class SystemSettingsPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
/// 悬浮条开关状态
|
||||
bool _floatBarEnabled = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
// 初始化时获取当前状态(使用静态属性)
|
||||
_floatBarEnabled = FloatBarController.isVisible;
|
||||
print('🔍 [设置页面] initState,初始状态: $_floatBarEnabled');
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
@@ -59,9 +45,6 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
// Tab 设置
|
||||
_buildTabSettingsSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 悬浮条设置
|
||||
_buildFloatBarSection(),
|
||||
const SizedBox(height: 12),
|
||||
// 语言设置
|
||||
_buildLanguageSection(),
|
||||
const SizedBox(height: 12),
|
||||
@@ -105,9 +88,9 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
if (state.runtimeType.toString() != 'TabConfigLoaded') {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
|
||||
final tabs = state.config.items;
|
||||
|
||||
|
||||
return ListView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
@@ -181,18 +164,14 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
AppLocalizations.of(context).translate('my.chinese'),
|
||||
'zh',
|
||||
locale.languageCode == 'zh',
|
||||
() => context.read<LocaleCubit>().setLocale(
|
||||
const Locale('zh', 'CN'),
|
||||
),
|
||||
() => context.read<LocaleCubit>().setLocale(const Locale('zh', 'CN')),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildLanguageOption(
|
||||
AppLocalizations.of(context).translate('my.english'),
|
||||
'en',
|
||||
locale.languageCode == 'en',
|
||||
() => context.read<LocaleCubit>().setLocale(
|
||||
const Locale('en', 'US'),
|
||||
),
|
||||
() => context.read<LocaleCubit>().setLocale(const Locale('en', 'US')),
|
||||
),
|
||||
],
|
||||
);
|
||||
@@ -203,25 +182,16 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLanguageOption(
|
||||
String label,
|
||||
String code,
|
||||
bool isSelected,
|
||||
VoidCallback onTap,
|
||||
) {
|
||||
Widget _buildLanguageOption(String label, String code, bool isSelected, VoidCallback onTap) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected
|
||||
? const Color(0xFF165DFF).withOpacity(0.1)
|
||||
: Colors.transparent,
|
||||
color: isSelected ? const Color(0xFF165DFF).withOpacity(0.1) : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(
|
||||
color: isSelected
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFFE5E6EB),
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFFE5E6EB),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
@@ -232,9 +202,7 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
color: isSelected
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF1D2129),
|
||||
color: isSelected ? const Color(0xFF165DFF) : const Color(0xFF1D2129),
|
||||
fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
|
||||
),
|
||||
),
|
||||
@@ -284,94 +252,6 @@ class _SystemSettingsPageState extends State<SystemSettingsPage> {
|
||||
);
|
||||
}
|
||||
|
||||
/// 悬浮条设置
|
||||
Widget _buildFloatBarSection() {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: const [
|
||||
BoxShadow(
|
||||
color: Color(0x0D000000),
|
||||
blurRadius: 8,
|
||||
offset: Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'悬浮条设置',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Color(0xFF1D2129),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_buildFloatBarSwitch(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 悬浮条开关
|
||||
Widget _buildFloatBarSwitch() {
|
||||
print(
|
||||
'🔍 [设置页面] _buildFloatBarSwitch 被调用,_floatBarEnabled: $_floatBarEnabled',
|
||||
);
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: const [
|
||||
Text(
|
||||
'显示悬浮条',
|
||||
style: TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'在Tab页面显示设备状态悬浮条',
|
||||
style: TextStyle(fontSize: 12, color: Color(0xFF8F959E)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Switch(
|
||||
value: _floatBarEnabled,
|
||||
onChanged: (value) {
|
||||
print('🔍 [设置页面] 开关被点击,新值: $value,当前状态: $_floatBarEnabled');
|
||||
|
||||
// 🔥 立即更新本地状态
|
||||
setState(() {
|
||||
_floatBarEnabled = value;
|
||||
});
|
||||
print('🔍 [设置页面] 本地状态已更新为: $_floatBarEnabled');
|
||||
|
||||
// 🔥 使用静态方法设置新值
|
||||
FloatBarController.setVisible(value);
|
||||
print('🔍 [设置页面] setVisible 已调用完成');
|
||||
|
||||
// 显示提示
|
||||
if (context.mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(value ? '悬浮条已开启' : '悬浮条已关闭'),
|
||||
duration: const Duration(seconds: 2),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
activeColor: const Color(0xFF165DFF),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 显示退出登录确认对话框
|
||||
void _showLogoutDialog(BuildContext context) {
|
||||
showDialog(
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import '../../../../../core/network/tcp/tcp_client.dart';
|
||||
import '../../../home/domain/entities/site_entity.dart';
|
||||
|
||||
class SiteState {
|
||||
@@ -48,8 +51,26 @@ class SiteCubit extends Cubit<SiteState> {
|
||||
|
||||
/// 选择场站(持久化)
|
||||
void selectSite(SiteEntity site) {
|
||||
debugPrint('🏭 [SiteCubit] ========== 切换场站 ==========');
|
||||
debugPrint('🏭 [SiteCubit] 从 ${state.selectedSite?.siteName ?? "无"} 切换到 ${site.siteName}');
|
||||
|
||||
// 🔥 关键修复:只有切换到不同场站时才断开TCP
|
||||
if (state.selectedSite?.id != site.id) {
|
||||
final tcpClient = GetIt.I<TcpClient>();
|
||||
if (tcpClient.isConnected) {
|
||||
debugPrint('🛑 [SiteCubit] 检测到TCP已连接,正在断开...');
|
||||
tcpClient.disconnect();
|
||||
debugPrint('✅ [SiteCubit] TCP已断开,防止场站间设备数据混淆');
|
||||
} else {
|
||||
debugPrint('ℹ️ [SiteCubit] TCP未连接,无需断开');
|
||||
}
|
||||
} else {
|
||||
debugPrint('✅ [SiteCubit] 相同场站,保持TCP连接状态');
|
||||
}
|
||||
|
||||
sharedPreferences.setInt(_selectedSiteIdKey, site.id);
|
||||
emit(state.copyWith(selectedSite: site));
|
||||
debugPrint('✅ [SiteCubit] 场站切换完成');
|
||||
}
|
||||
|
||||
/// 清除选中场站
|
||||
|
||||
Reference in New Issue
Block a user