无人机视频对接+支持无人机未在线时候5S执行一下查询操作。便于视频正常展示。
无人机视频的摄像头切换功能和参数切换功能(广角和红外 和等) 无人机航线任务创建页面的开发和接口对接 无人机航线任务搜索接口(支持时间过滤) 无人机执行航线任务功能开发和接口对接 无人机暂停执行任务功能开发和接口对接 无人机返航任务和取消返航功能开发和接口对接 无人机机场详情页中的展示信息的更正。
This commit is contained in:
@@ -39,4 +39,22 @@ class HttpApiConsts {
|
|||||||
|
|
||||||
// 获取飞行任务列表
|
// 获取飞行任务列表
|
||||||
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
|
static const String getFlightTask = "$baseUrl/iot/UAV/getFlightTask";
|
||||||
|
|
||||||
|
// 获取飞行任务详情
|
||||||
|
static const String getFlightTaskDetail = "$baseUrl/iot/UAV/getFlightTaskDetail";
|
||||||
|
|
||||||
|
// 获取航线列表
|
||||||
|
static const String getWayline = "$baseUrl/iot/UAV/getWayline";
|
||||||
|
|
||||||
|
// 创建飞行任务
|
||||||
|
static const String createFlightTask = "$baseUrl/iot/UAV/createFlightTask";
|
||||||
|
|
||||||
|
// 更新飞行任务状态
|
||||||
|
static const String updateFlightTaskStatus = "$baseUrl/iot/UAV/updateFlightTaskStatus";
|
||||||
|
|
||||||
|
// 返航、暂停等命令
|
||||||
|
static const String flightTaskCommand = "$baseUrl/iot/UAV/flightTaskCommand";
|
||||||
|
|
||||||
|
// 切换无人机镜头获取视频流
|
||||||
|
static const String changeUAVLens = "$baseUrl/iot/UAV/changeLens";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -85,6 +85,8 @@ import '../../features/v2/device_list/data/repositories/drone_station_repository
|
|||||||
import '../../features/v2/device_list/domain/repositories/drone_station_repository.dart';
|
import '../../features/v2/device_list/domain/repositories/drone_station_repository.dart';
|
||||||
import '../../features/v2/device_list/domain/usecases/get_drone_station_list_usecase.dart';
|
import '../../features/v2/device_list/domain/usecases/get_drone_station_list_usecase.dart';
|
||||||
import '../../features/v2/device_list/domain/usecases/get_video_stream_usecase.dart';
|
import '../../features/v2/device_list/domain/usecases/get_video_stream_usecase.dart';
|
||||||
|
import '../../features/v2/device_list/domain/usecases/get_uav_video_stream_usecase.dart';
|
||||||
|
import '../../features/v2/device_list/domain/usecases/update_flight_task_status_usecase.dart';
|
||||||
import '../../features/v2/device_list/presentation/bloc/drone_station_bloc.dart';
|
import '../../features/v2/device_list/presentation/bloc/drone_station_bloc.dart';
|
||||||
import '../../features/v2/device_list/presentation/bloc/robot_list_bloc.dart';
|
import '../../features/v2/device_list/presentation/bloc/robot_list_bloc.dart';
|
||||||
import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart';
|
import '../../features/v2/waring_center/data/datasources/alarm_remote_datasource.dart';
|
||||||
@@ -301,8 +303,14 @@ Future<void> init() async {
|
|||||||
sl.registerLazySingleton<GetVideoStreamUseCase>(
|
sl.registerLazySingleton<GetVideoStreamUseCase>(
|
||||||
() => GetVideoStreamUseCase(sl()),
|
() => GetVideoStreamUseCase(sl()),
|
||||||
);
|
);
|
||||||
|
sl.registerLazySingleton<GetUavVideoStreamUseCase>(
|
||||||
|
() => GetUavVideoStreamUseCase(sl()),
|
||||||
|
);
|
||||||
|
sl.registerLazySingleton<UpdateFlightTaskStatusUseCase>(
|
||||||
|
() => UpdateFlightTaskStatusUseCase(sl()),
|
||||||
|
);
|
||||||
sl.registerFactory<DroneStationBloc>(
|
sl.registerFactory<DroneStationBloc>(
|
||||||
() => DroneStationBloc(sl(), sl(), sl()),
|
() => DroneStationBloc(sl(), sl(), sl(), sl()),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Robot List V2
|
/// Robot List V2
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import '../../domain/entities/drone_station_entity.dart';
|
import '../../domain/entities/drone_station_entity.dart';
|
||||||
import '../../domain/entities/video_stream_entity.dart';
|
import '../../domain/entities/video_stream_entity.dart';
|
||||||
import '../../domain/entities/flight_task_entity.dart';
|
import '../../domain/entities/flight_task_entity.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
|
||||||
abstract class DroneStationDataSource {
|
abstract class DroneStationDataSource {
|
||||||
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
|
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
|
||||||
@@ -17,4 +18,20 @@ abstract class DroneStationDataSource {
|
|||||||
required int beginAt,
|
required int beginAt,
|
||||||
required int endAt,
|
required int endAt,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
Future<FlightTaskEntity> getFlightTaskDetail(String taskId);
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> updateFlightTaskStatus({
|
||||||
|
required String taskId,
|
||||||
|
required String status,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 切换无人机镜头获取实时视频流
|
||||||
|
Future<UavVideoStreamEntity> getUavVideoStream({
|
||||||
|
required String sn,
|
||||||
|
required String cameraIndex,
|
||||||
|
UavLensType? lensType,
|
||||||
|
VideoQualityType qualityType = VideoQualityType.adaptive,
|
||||||
|
int videoExpire = 720000000,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import 'dart:convert';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
|
||||||
import '../datasources/drone_station_datasource.dart';
|
import '../datasources/drone_station_datasource.dart';
|
||||||
import '../../domain/entities/drone_station_entity.dart';
|
import '../../domain/entities/drone_station_entity.dart';
|
||||||
import '../../domain/entities/video_stream_entity.dart';
|
import '../../domain/entities/video_stream_entity.dart';
|
||||||
import '../../domain/entities/flight_task_entity.dart';
|
import '../../domain/entities/flight_task_entity.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
|
||||||
class DroneStationDataSourceImpl implements DroneStationDataSource {
|
class DroneStationDataSourceImpl implements DroneStationDataSource {
|
||||||
final Dio dio;
|
final Dio dio;
|
||||||
@@ -134,4 +136,198 @@ class DroneStationDataSourceImpl implements DroneStationDataSource {
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
@override
|
||||||
|
Future<FlightTaskEntity> getFlightTaskDetail(String taskId) async {
|
||||||
|
final response = await dio.get(
|
||||||
|
HttpApiConsts.getFlightTaskDetail,
|
||||||
|
queryParameters: {'taskId': taskId},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('网络请求失败: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final responseData = response.data;
|
||||||
|
|
||||||
|
if (responseData['code'] != 0 && responseData['code'] != 200) {
|
||||||
|
throw Exception(responseData['message'] ?? '业务异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
return FlightTaskEntity.fromJson(responseData['data']);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Map<String, dynamic>> updateFlightTaskStatus({
|
||||||
|
required String taskId,
|
||||||
|
required String status,
|
||||||
|
}) async {
|
||||||
|
final response = await dio.post(
|
||||||
|
HttpApiConsts.updateFlightTaskStatus,
|
||||||
|
queryParameters: {
|
||||||
|
'taskId': taskId,
|
||||||
|
'status': status,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('网络请求失败: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
final responseData = response.data;
|
||||||
|
|
||||||
|
if (responseData['code'] != 0) {
|
||||||
|
throw Exception(responseData['message'] ?? '业务异常');
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<UavVideoStreamEntity> getUavVideoStream({
|
||||||
|
required String sn,
|
||||||
|
required String cameraIndex,
|
||||||
|
UavLensType? lensType,
|
||||||
|
VideoQualityType qualityType = VideoQualityType.adaptive,
|
||||||
|
int videoExpire = 720000000,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
print('=== 开始请求无人机视频流 ===');
|
||||||
|
print('URL: ${HttpApiConsts.changeUAVLens}');
|
||||||
|
print('📤 请求参数:');
|
||||||
|
print(' sn: $sn');
|
||||||
|
print(' cameraIndex: $cameraIndex ⬅️ 重点检查这个');
|
||||||
|
print(' lensType: ${_lensTypeToString(lensType)}');
|
||||||
|
print(' qualityType: ${_qualityTypeToString(qualityType)}');
|
||||||
|
print(' videoExpire: $videoExpire');
|
||||||
|
|
||||||
|
final response = await dio.post(
|
||||||
|
HttpApiConsts.changeUAVLens,
|
||||||
|
data: {
|
||||||
|
'sn': sn,
|
||||||
|
'lensType': lensType != null ? _lensTypeToString(lensType) : '',
|
||||||
|
'cameraIndex': cameraIndex,
|
||||||
|
'qualityType': _qualityTypeToString(qualityType),
|
||||||
|
'videoExpire': videoExpire,
|
||||||
|
},
|
||||||
|
options: Options(
|
||||||
|
receiveTimeout: const Duration(seconds: 30),
|
||||||
|
sendTimeout: const Duration(seconds: 30),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
print('响应状态码: ${response.statusCode}');
|
||||||
|
print('响应数据类型: ${response.data.runtimeType}');
|
||||||
|
|
||||||
|
if (response.statusCode != 200) {
|
||||||
|
throw Exception('网络请求失败: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
|
||||||
|
var responseData = response.data;
|
||||||
|
|
||||||
|
// 打印完整响应,方便调试
|
||||||
|
print('完整响应: $responseData');
|
||||||
|
|
||||||
|
// 检查是否为字符串类型,需要手动解析
|
||||||
|
if (responseData is String) {
|
||||||
|
print('检测到响应为字符串类型,尝试 JSON 解析...');
|
||||||
|
try {
|
||||||
|
final parsed = jsonDecode(responseData);
|
||||||
|
if (parsed is Map<String, dynamic>) {
|
||||||
|
print('JSON 解析成功');
|
||||||
|
responseData = parsed;
|
||||||
|
} else {
|
||||||
|
throw Exception('JSON 解析结果不是 Map 类型');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('JSON 解析失败: $e');
|
||||||
|
throw Exception('响应数据格式错误: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
print('响应数据键列表: ${(responseData as Map).keys?.toList()}');
|
||||||
|
|
||||||
|
if (responseData['code'] != 200) {
|
||||||
|
final errorMsg = responseData['msg'] ?? '业务异常';
|
||||||
|
print('业务错误: $errorMsg');
|
||||||
|
throw Exception(errorMsg);
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = responseData['data'];
|
||||||
|
print('\n=== data 字段内容 ===');
|
||||||
|
print('data: $data');
|
||||||
|
print('data 类型: ${data.runtimeType}');
|
||||||
|
|
||||||
|
if (data == null) {
|
||||||
|
print('错误: data 字段为 null!');
|
||||||
|
throw Exception('服务器返回的 data 字段为空');
|
||||||
|
}
|
||||||
|
|
||||||
|
print('data.sn: ${data['sn']}');
|
||||||
|
print('data.camera_index: ${data['camera_index']}');
|
||||||
|
print('data.url: ${data['url']}');
|
||||||
|
print('data.expire_ts: ${data['expire_ts']}');
|
||||||
|
print('data.url_type: ${data['url_type']}');
|
||||||
|
print('=========================\n');
|
||||||
|
|
||||||
|
return UavVideoStreamEntity.fromJson(data);
|
||||||
|
} on DioException catch (e) {
|
||||||
|
print('=== Dio 异常捕获 ===');
|
||||||
|
print('异常类型: ${e.type}');
|
||||||
|
print('异常消息: ${e.message}');
|
||||||
|
print('异常详情: $e');
|
||||||
|
|
||||||
|
String errorMessage;
|
||||||
|
switch (e.type) {
|
||||||
|
case DioExceptionType.connectionTimeout:
|
||||||
|
case DioExceptionType.sendTimeout:
|
||||||
|
case DioExceptionType.receiveTimeout:
|
||||||
|
errorMessage = '网络连接超时,请检查网络';
|
||||||
|
break;
|
||||||
|
case DioExceptionType.connectionError:
|
||||||
|
errorMessage = '网络连接失败,请检查网络设置';
|
||||||
|
break;
|
||||||
|
case DioExceptionType.badResponse:
|
||||||
|
errorMessage = '服务器响应错误: ${e.response?.statusCode}';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
errorMessage = '网络请求失败: ${e.message}';
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(errorMessage);
|
||||||
|
} catch (e) {
|
||||||
|
print('=== 未知异常 ===');
|
||||||
|
print('异常类型: ${e.runtimeType}');
|
||||||
|
print('异常消息: $e');
|
||||||
|
rethrow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将镜头类型枚举转换为字符串
|
||||||
|
String _lensTypeToString(UavLensType? type) {
|
||||||
|
switch (type) {
|
||||||
|
case UavLensType.wide:
|
||||||
|
return 'wide';
|
||||||
|
case UavLensType.zoom:
|
||||||
|
return 'zoom';
|
||||||
|
case UavLensType.ir:
|
||||||
|
return 'ir';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 将质量类型枚举转换为字符串
|
||||||
|
String _qualityTypeToString(VideoQualityType type) {
|
||||||
|
switch (type) {
|
||||||
|
case VideoQualityType.adaptive:
|
||||||
|
return 'adaptive';
|
||||||
|
case VideoQualityType.low:
|
||||||
|
return 'low';
|
||||||
|
case VideoQualityType.medium:
|
||||||
|
return 'medium';
|
||||||
|
case VideoQualityType.high:
|
||||||
|
return 'high';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ import '../../../../../core/error/failure.dart';
|
|||||||
import '../datasources/drone_station_datasource.dart';
|
import '../datasources/drone_station_datasource.dart';
|
||||||
import '../../domain/entities/drone_station_entity.dart';
|
import '../../domain/entities/drone_station_entity.dart';
|
||||||
import '../../domain/entities/video_stream_entity.dart';
|
import '../../domain/entities/video_stream_entity.dart';
|
||||||
|
import '../../domain/entities/flight_task_entity.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
import '../../domain/repositories/drone_station_repository.dart';
|
import '../../domain/repositories/drone_station_repository.dart';
|
||||||
|
|
||||||
class DroneStationRepositoryImpl implements DroneStationRepository {
|
class DroneStationRepositoryImpl implements DroneStationRepository {
|
||||||
@@ -52,4 +54,70 @@ class DroneStationRepositoryImpl implements DroneStationRepository {
|
|||||||
return Left(Failure(e.toString()));
|
return Left(Failure(e.toString()));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Map<String, List<FlightTaskEntity>?>>> getFlightTasks({
|
||||||
|
required List<String> sns,
|
||||||
|
required int beginAt,
|
||||||
|
required int endAt,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final tasks = await dataSource.getFlightTasks(
|
||||||
|
sns: sns,
|
||||||
|
beginAt: beginAt,
|
||||||
|
endAt: endAt,
|
||||||
|
);
|
||||||
|
return Right(tasks);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(Failure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, FlightTaskEntity>> getFlightTaskDetail(String taskId) async {
|
||||||
|
try {
|
||||||
|
final detail = await dataSource.getFlightTaskDetail(taskId);
|
||||||
|
return Right(detail);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(Failure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, Map<String, dynamic>>> updateFlightTaskStatus({
|
||||||
|
required String taskId,
|
||||||
|
required String status,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final result = await dataSource.updateFlightTaskStatus(
|
||||||
|
taskId: taskId,
|
||||||
|
status: status,
|
||||||
|
);
|
||||||
|
return Right(result);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(Failure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Either<Failure, UavVideoStreamEntity>> getUavVideoStream({
|
||||||
|
required String sn,
|
||||||
|
required String cameraIndex,
|
||||||
|
UavLensType? lensType,
|
||||||
|
VideoQualityType qualityType = VideoQualityType.adaptive,
|
||||||
|
int videoExpire = 720000000,
|
||||||
|
}) async {
|
||||||
|
try {
|
||||||
|
final videoStream = await dataSource.getUavVideoStream(
|
||||||
|
sn: sn,
|
||||||
|
cameraIndex: cameraIndex,
|
||||||
|
lensType: lensType,
|
||||||
|
qualityType: qualityType,
|
||||||
|
videoExpire: videoExpire,
|
||||||
|
);
|
||||||
|
return Right(videoStream);
|
||||||
|
} catch (e) {
|
||||||
|
return Left(Failure(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -76,12 +76,12 @@ class PositionState extends Equatable {
|
|||||||
|
|
||||||
/// UAV详情实体(用于详情页面API返回的数据)
|
/// UAV详情实体(用于详情页面API返回的数据)
|
||||||
class UAVDetailEntity extends Equatable {
|
class UAVDetailEntity extends Equatable {
|
||||||
final String deviceSn;
|
final String deviceSn; // 设备序列号(无人机序列号)
|
||||||
final String gatewaySn;
|
final String gatewaySn; // 网关序列号
|
||||||
final String callsign;
|
final String callsign; // 机场呼号/名称
|
||||||
final String droneCallsign;
|
final String droneCallsign; // 无人机呼号
|
||||||
final int onlineStatus;
|
final int onlineStatus; // 机场在线状态 (1:在线, 0:离线)
|
||||||
final int droneOnlineStatus;
|
final int droneOnlineStatus; // 无人机在线状态
|
||||||
final double? latitude;
|
final double? latitude;
|
||||||
final double? longitude;
|
final double? longitude;
|
||||||
final double? capacityPercent;
|
final double? capacityPercent;
|
||||||
@@ -94,7 +94,7 @@ class UAVDetailEntity extends Equatable {
|
|||||||
final double? liveCapacity;
|
final double? liveCapacity;
|
||||||
final String? rainfall;
|
final String? rainfall;
|
||||||
final List<CameraInfo>? gatewayCameraList;
|
final List<CameraInfo>? gatewayCameraList;
|
||||||
final List<dynamic>? droneCameraList;
|
final List<CameraInfo>? droneCameraList;
|
||||||
final int? orgId;
|
final int? orgId;
|
||||||
final int? siteId;
|
final int? siteId;
|
||||||
final int? userId;
|
final int? userId;
|
||||||
@@ -126,7 +126,12 @@ class UAVDetailEntity extends Equatable {
|
|||||||
|
|
||||||
factory UAVDetailEntity.fromJson(Map<String, dynamic> json) {
|
factory UAVDetailEntity.fromJson(Map<String, dynamic> json) {
|
||||||
return UAVDetailEntity(
|
return UAVDetailEntity(
|
||||||
deviceSn: json['device_sn'] ?? '',
|
deviceSn:
|
||||||
|
json['device_sn'] ??
|
||||||
|
json['drone_sn'] ??
|
||||||
|
json['droneSn'] ??
|
||||||
|
json['droneDeviceSn'] ??
|
||||||
|
'',
|
||||||
gatewaySn: json['gateway_sn'] ?? '',
|
gatewaySn: json['gateway_sn'] ?? '',
|
||||||
callsign: json['callsign'] ?? '',
|
callsign: json['callsign'] ?? '',
|
||||||
droneCallsign: json['drone_callsign'] ?? '',
|
droneCallsign: json['drone_callsign'] ?? '',
|
||||||
@@ -166,7 +171,11 @@ class UAVDetailEntity extends Equatable {
|
|||||||
.map((item) => CameraInfo.fromJson(item))
|
.map((item) => CameraInfo.fromJson(item))
|
||||||
.toList()
|
.toList()
|
||||||
: null,
|
: null,
|
||||||
droneCameraList: json['drone_camera_list'],
|
droneCameraList: json['drone_camera_list'] != null
|
||||||
|
? (json['drone_camera_list'] as List)
|
||||||
|
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
orgId: json['orgId'],
|
orgId: json['orgId'],
|
||||||
siteId: json['siteId'],
|
siteId: json['siteId'],
|
||||||
userId: json['userId'],
|
userId: json['userId'],
|
||||||
@@ -322,7 +331,11 @@ class DroneStationEntity extends Equatable {
|
|||||||
.map((item) => CameraInfo.fromJson(item))
|
.map((item) => CameraInfo.fromJson(item))
|
||||||
.toList()
|
.toList()
|
||||||
: null,
|
: null,
|
||||||
droneCameraList: json['drone_camera_list'],
|
droneCameraList: json['drone_camera_list'] != null
|
||||||
|
? (json['drone_camera_list'] as List)
|
||||||
|
.map((item) => CameraInfo.fromJson(item as Map<String, dynamic>))
|
||||||
|
.toList()
|
||||||
|
: null,
|
||||||
orgId: json['orgId'] ?? 0,
|
orgId: json['orgId'] ?? 0,
|
||||||
siteId: json['siteId'] ?? 0,
|
siteId: json['siteId'] ?? 0,
|
||||||
userId: json['userId'],
|
userId: json['userId'],
|
||||||
@@ -387,4 +400,4 @@ class DroneStationEntity extends Equatable {
|
|||||||
siteId,
|
siteId,
|
||||||
userId,
|
userId,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
/// 飞行任务详情实体(专门用于详情接口)
|
||||||
|
class FlightTaskDetailEntity {
|
||||||
|
final String name;
|
||||||
|
final String uuid;
|
||||||
|
final String taskType;
|
||||||
|
final String status;
|
||||||
|
final String sn;
|
||||||
|
final String droneSn; // 无人机序列号
|
||||||
|
final String waylineUuid;
|
||||||
|
final String beginAt;
|
||||||
|
final String endAt;
|
||||||
|
final String outOfControlAction; // 失控动作
|
||||||
|
final int rthAltitude; // 返航高度(米)
|
||||||
|
final String rthMode; // 返航模式
|
||||||
|
final String resumableStatus; // 断点续飞状态
|
||||||
|
final int folderId; // 文件夹ID(从folder_info中获取)
|
||||||
|
final int expectedFileCount; // 期望文件数
|
||||||
|
final int uploadedFileCount; // 已上传文件数
|
||||||
|
final String repeatType; // 重复类型
|
||||||
|
final int interval; // 间隔
|
||||||
|
final int weekOfMonth; // 每月第几周
|
||||||
|
final int minBatteryCapacity; // 最低电量(百分比)
|
||||||
|
|
||||||
|
FlightTaskDetailEntity({
|
||||||
|
required this.name,
|
||||||
|
required this.uuid,
|
||||||
|
required this.taskType,
|
||||||
|
required this.status,
|
||||||
|
required this.sn,
|
||||||
|
required this.droneSn,
|
||||||
|
required this.waylineUuid,
|
||||||
|
required this.beginAt,
|
||||||
|
required this.endAt,
|
||||||
|
required this.outOfControlAction,
|
||||||
|
required this.rthAltitude,
|
||||||
|
required this.rthMode,
|
||||||
|
required this.resumableStatus,
|
||||||
|
required this.folderId,
|
||||||
|
required this.expectedFileCount,
|
||||||
|
required this.uploadedFileCount,
|
||||||
|
required this.repeatType,
|
||||||
|
required this.interval,
|
||||||
|
required this.weekOfMonth,
|
||||||
|
required this.minBatteryCapacity,
|
||||||
|
});
|
||||||
|
|
||||||
|
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)
|
||||||
|
: <String, dynamic>{};
|
||||||
|
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'] ?? '',
|
||||||
|
outOfControlAction: json['out_of_control_action_in_flight'] ?? '',
|
||||||
|
rthAltitude: _parseInt(json['rth_altitude']),
|
||||||
|
rthMode: json['rth_mode'] ?? '',
|
||||||
|
resumableStatus: json['resumable_status'] ?? '',
|
||||||
|
folderId: _parseInt(folderInfo['folder_id']),
|
||||||
|
expectedFileCount: _parseInt(folderInfo['expected_file_count']),
|
||||||
|
uploadedFileCount: _parseInt(folderInfo['uploaded_file_count']),
|
||||||
|
repeatType: json['repeat_type'] ?? '',
|
||||||
|
interval: _parseInt(json['interval']),
|
||||||
|
weekOfMonth: _parseInt(json['week_of_month']),
|
||||||
|
minBatteryCapacity: _parseInt(json['min_battery_capacity']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:安全解析整数
|
||||||
|
static int _parseInt(dynamic value) {
|
||||||
|
if (value == null) return 0;
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is String) return int.tryParse(value) ?? 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
return {
|
||||||
|
'name': name,
|
||||||
|
'uuid': uuid,
|
||||||
|
'task_type': taskType,
|
||||||
|
'status': status,
|
||||||
|
'sn': sn,
|
||||||
|
'drone_sn': droneSn,
|
||||||
|
'wayline_uuid': waylineUuid,
|
||||||
|
'begin_at': beginAt,
|
||||||
|
'end_at': endAt,
|
||||||
|
'out_of_control_action_in_flight': outOfControlAction,
|
||||||
|
'rth_altitude': rthAltitude,
|
||||||
|
'rth_mode': rthMode,
|
||||||
|
'resumable_status': resumableStatus,
|
||||||
|
'folder_info': {
|
||||||
|
'folder_id': folderId,
|
||||||
|
'expected_file_count': expectedFileCount,
|
||||||
|
'uploaded_file_count': uploadedFileCount,
|
||||||
|
},
|
||||||
|
'repeat_type': repeatType,
|
||||||
|
'interval': interval,
|
||||||
|
'week_of_month': weekOfMonth,
|
||||||
|
'min_battery_capacity': minBatteryCapacity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/// 飞行任务列表实体(专门用于列表接口)
|
||||||
class FlightTaskEntity {
|
class FlightTaskEntity {
|
||||||
final String name;
|
final String name;
|
||||||
final String uuid;
|
final String uuid;
|
||||||
@@ -54,9 +55,9 @@ class FlightTaskEntity {
|
|||||||
runAt: json['run_at'] ?? '',
|
runAt: json['run_at'] ?? '',
|
||||||
completedAt: json['completed_at'] ?? '',
|
completedAt: json['completed_at'] ?? '',
|
||||||
waylineUuid: json['wayline_uuid'] ?? '',
|
waylineUuid: json['wayline_uuid'] ?? '',
|
||||||
folderId: json['folder_id'] ?? 0,
|
folderId: _parseInt(json['folder_id']),
|
||||||
currentWaypointIndex: json['current_waypoint_index'] ?? 0,
|
currentWaypointIndex: _parseInt(json['current_waypoint_index']),
|
||||||
totalWaypoints: json['total_waypoints'] ?? 0,
|
totalWaypoints: _parseInt(json['total_waypoints']),
|
||||||
mediaUploadStatus: json['media_upload_status'] ?? '',
|
mediaUploadStatus: json['media_upload_status'] ?? '',
|
||||||
resumableStatus: json['resumable_status'] ?? '',
|
resumableStatus: json['resumable_status'] ?? '',
|
||||||
isBreakPointResume: json['is_break_point_resume'] ?? false,
|
isBreakPointResume: json['is_break_point_resume'] ?? false,
|
||||||
@@ -65,6 +66,14 @@ class FlightTaskEntity {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 辅助方法:安全解析整数
|
||||||
|
static int _parseInt(dynamic value) {
|
||||||
|
if (value == null) return 0;
|
||||||
|
if (value is int) return value;
|
||||||
|
if (value is String) return int.tryParse(value) ?? 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
return {
|
return {
|
||||||
'name': name,
|
'name': name,
|
||||||
|
|||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
/// 无人机镜头类型枚举
|
||||||
|
enum UavLensType {
|
||||||
|
wide, // 广角镜头
|
||||||
|
zoom, // 变焦镜头
|
||||||
|
ir, // 红外镜头
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 视频质量类型枚举
|
||||||
|
enum VideoQualityType {
|
||||||
|
adaptive, // 自适应
|
||||||
|
low, // 低清晰度
|
||||||
|
medium, // 中清晰度
|
||||||
|
high, // 高清晰度
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 无人机实时视频流实体
|
||||||
|
class UavVideoStreamEntity extends Equatable {
|
||||||
|
final String sn; // 设备序列号
|
||||||
|
final String cameraIndex; // 摄像头编号
|
||||||
|
final String url; // 视频流URL(含RTC参数)
|
||||||
|
final int expireTs; // Token过期时间戳
|
||||||
|
final String urlType; // URL类型:volc(火山引擎)或 agora
|
||||||
|
|
||||||
|
const UavVideoStreamEntity({
|
||||||
|
required this.sn,
|
||||||
|
required this.cameraIndex,
|
||||||
|
required this.url,
|
||||||
|
required this.expireTs,
|
||||||
|
required this.urlType,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory UavVideoStreamEntity.fromJson(Map<String, dynamic> json) {
|
||||||
|
return UavVideoStreamEntity(
|
||||||
|
sn: json['sn'] ?? '',
|
||||||
|
cameraIndex: json['camera_index'] ?? '',
|
||||||
|
url: json['url'] ?? '',
|
||||||
|
expireTs: json['expire_ts'] ?? 0,
|
||||||
|
urlType: json['url_type'] ?? 'volc',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> toMap() {
|
||||||
|
return {
|
||||||
|
'sn': sn,
|
||||||
|
'cameraIndex': cameraIndex,
|
||||||
|
'url': url,
|
||||||
|
'expireTs': expireTs.toString(),
|
||||||
|
'urlType': urlType,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 解析RTC参数(从URL中提取)
|
||||||
|
Map<String, String> parseRtcParams() {
|
||||||
|
final params = <String, String>{};
|
||||||
|
final pairs = url.split('&');
|
||||||
|
for (final pair in pairs) {
|
||||||
|
final kv = pair.split('=');
|
||||||
|
if (kv.length == 2) {
|
||||||
|
params[kv[0]] = Uri.decodeComponent(kv[1]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 AppId
|
||||||
|
String get appId {
|
||||||
|
final params = parseRtcParams();
|
||||||
|
return params['app_id'] ?? params['appid'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 RoomId
|
||||||
|
String get roomId {
|
||||||
|
final params = parseRtcParams();
|
||||||
|
return params['room_id'] ?? params['roomid'] ?? params['channel'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 Token
|
||||||
|
String get token {
|
||||||
|
final params = parseRtcParams();
|
||||||
|
return params['token'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 获取 UserId
|
||||||
|
String get userId {
|
||||||
|
final params = parseRtcParams();
|
||||||
|
return params['user_id'] ?? params['uid'] ?? '';
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sn, cameraIndex, url, expireTs, urlType];
|
||||||
|
}
|
||||||
@@ -2,6 +2,8 @@ import 'package:fpdart/fpdart.dart';
|
|||||||
import '../../../../../core/error/failure.dart';
|
import '../../../../../core/error/failure.dart';
|
||||||
import '../entities/drone_station_entity.dart';
|
import '../entities/drone_station_entity.dart';
|
||||||
import '../entities/video_stream_entity.dart';
|
import '../entities/video_stream_entity.dart';
|
||||||
|
import '../entities/flight_task_entity.dart';
|
||||||
|
import '../entities/uav_video_stream_entity.dart';
|
||||||
|
|
||||||
abstract class DroneStationRepository {
|
abstract class DroneStationRepository {
|
||||||
Future<Either<Failure, List<DroneStationEntity>>> getDroneStationList(
|
Future<Either<Failure, List<DroneStationEntity>>> getDroneStationList(
|
||||||
@@ -16,4 +18,23 @@ abstract class DroneStationRepository {
|
|||||||
required String cameraIndex,
|
required String cameraIndex,
|
||||||
required String cameraPosition,
|
required String cameraPosition,
|
||||||
});
|
});
|
||||||
|
Future<Either<Failure, Map<String, List<FlightTaskEntity>?>>> getFlightTasks({
|
||||||
|
required List<String> sns,
|
||||||
|
required int beginAt,
|
||||||
|
required int endAt,
|
||||||
|
});
|
||||||
|
Future<Either<Failure, FlightTaskEntity>> getFlightTaskDetail(String taskId);
|
||||||
|
Future<Either<Failure, Map<String, dynamic>>> updateFlightTaskStatus({
|
||||||
|
required String taskId,
|
||||||
|
required String status,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// 获取无人机实时视频流
|
||||||
|
Future<Either<Failure, UavVideoStreamEntity>> getUavVideoStream({
|
||||||
|
required String sn,
|
||||||
|
required String cameraIndex,
|
||||||
|
UavLensType? lensType,
|
||||||
|
VideoQualityType qualityType,
|
||||||
|
int videoExpire,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../../core/error/failure.dart';
|
||||||
|
import '../entities/uav_video_stream_entity.dart';
|
||||||
|
import '../repositories/drone_station_repository.dart';
|
||||||
|
|
||||||
|
/// 获取无人机实时视频流用例
|
||||||
|
class GetUavVideoStreamUseCase {
|
||||||
|
final DroneStationRepository repository;
|
||||||
|
|
||||||
|
GetUavVideoStreamUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<Either<Failure, UavVideoStreamEntity>> call({
|
||||||
|
required String sn,
|
||||||
|
required String cameraIndex,
|
||||||
|
UavLensType? lensType,
|
||||||
|
VideoQualityType qualityType = VideoQualityType.adaptive,
|
||||||
|
int videoExpire = 720000000,
|
||||||
|
}) async {
|
||||||
|
return await repository.getUavVideoStream(
|
||||||
|
sn: sn,
|
||||||
|
cameraIndex: cameraIndex,
|
||||||
|
lensType: lensType,
|
||||||
|
qualityType: qualityType,
|
||||||
|
videoExpire: videoExpire,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import 'package:fpdart/fpdart.dart';
|
||||||
|
import '../../../../../core/error/failure.dart';
|
||||||
|
import '../repositories/drone_station_repository.dart';
|
||||||
|
|
||||||
|
class UpdateFlightTaskStatusUseCase {
|
||||||
|
final DroneStationRepository repository;
|
||||||
|
|
||||||
|
UpdateFlightTaskStatusUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<Either<Failure, Map<String, dynamic>>> execute({
|
||||||
|
required String taskId,
|
||||||
|
required String status,
|
||||||
|
}) async {
|
||||||
|
return await repository.updateFlightTaskStatus(
|
||||||
|
taskId: taskId,
|
||||||
|
status: status,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import '../../domain/usecases/get_drone_station_list_usecase.dart';
|
import '../../domain/usecases/get_drone_station_list_usecase.dart';
|
||||||
import '../../domain/usecases/get_video_stream_usecase.dart';
|
import '../../domain/usecases/get_video_stream_usecase.dart';
|
||||||
|
import '../../domain/usecases/get_uav_video_stream_usecase.dart';
|
||||||
import 'drone_station_event.dart';
|
import 'drone_station_event.dart';
|
||||||
import 'drone_station_state.dart';
|
import 'drone_station_state.dart';
|
||||||
|
|
||||||
@@ -8,16 +9,19 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
|||||||
final GetDroneStationListUseCase getDroneStationListUseCase;
|
final GetDroneStationListUseCase getDroneStationListUseCase;
|
||||||
final GetUAVDetailUseCase getUAVDetailUseCase;
|
final GetUAVDetailUseCase getUAVDetailUseCase;
|
||||||
final GetVideoStreamUseCase getVideoStreamUseCase;
|
final GetVideoStreamUseCase getVideoStreamUseCase;
|
||||||
|
final GetUavVideoStreamUseCase getUavVideoStreamUseCase;
|
||||||
|
|
||||||
DroneStationBloc(
|
DroneStationBloc(
|
||||||
this.getDroneStationListUseCase,
|
this.getDroneStationListUseCase,
|
||||||
this.getUAVDetailUseCase,
|
this.getUAVDetailUseCase,
|
||||||
this.getVideoStreamUseCase,
|
this.getVideoStreamUseCase,
|
||||||
|
this.getUavVideoStreamUseCase,
|
||||||
) : super(const DroneStationInitial()) {
|
) : super(const DroneStationInitial()) {
|
||||||
on<DroneStationLoadData>(_onLoadData);
|
on<DroneStationLoadData>(_onLoadData);
|
||||||
on<DroneStationRefresh>(_onRefresh);
|
on<DroneStationRefresh>(_onRefresh);
|
||||||
on<UAVDetailLoad>(_onUAVDetailLoad);
|
on<UAVDetailLoad>(_onUAVDetailLoad);
|
||||||
on<VideoStreamLoad>(_onVideoStreamLoad);
|
on<VideoStreamLoad>(_onVideoStreamLoad);
|
||||||
|
on<UavVideoStreamLoad>(_onUavVideoStreamLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoadData(
|
Future<void> _onLoadData(
|
||||||
@@ -79,4 +83,29 @@ class DroneStationBloc extends Bloc<DroneStationEvent, DroneStationState> {
|
|||||||
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
|
(videoStream) => emit(VideoStreamLoaded(videoStream, event.cameraPosition)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 处理无人机实时视频流加载事件
|
||||||
|
Future<void> _onUavVideoStreamLoad(
|
||||||
|
UavVideoStreamLoad event,
|
||||||
|
Emitter<DroneStationState> emit,
|
||||||
|
) async {
|
||||||
|
emit(const UavVideoStreamLoading());
|
||||||
|
|
||||||
|
final result = await getUavVideoStreamUseCase(
|
||||||
|
sn: event.sn,
|
||||||
|
cameraIndex: event.cameraIndex,
|
||||||
|
lensType: event.lensType,
|
||||||
|
qualityType: event.qualityType,
|
||||||
|
videoExpire: event.videoExpire,
|
||||||
|
);
|
||||||
|
|
||||||
|
result.fold(
|
||||||
|
(failure) => emit(UavVideoStreamError(failure.message)),
|
||||||
|
(videoStream) => emit(UavVideoStreamLoaded(
|
||||||
|
videoStream: videoStream,
|
||||||
|
cameraIndex: event.cameraIndex,
|
||||||
|
lensType: event.lensType,
|
||||||
|
)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
|
||||||
abstract class DroneStationEvent extends Equatable {
|
abstract class DroneStationEvent extends Equatable {
|
||||||
const DroneStationEvent();
|
const DroneStationEvent();
|
||||||
@@ -49,3 +50,23 @@ class VideoStreamLoad extends DroneStationEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [sn, cameraIndex, cameraPosition];
|
List<Object?> get props => [sn, cameraIndex, cameraPosition];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 加载无人机实时视频流事件
|
||||||
|
class UavVideoStreamLoad extends DroneStationEvent {
|
||||||
|
final String sn;
|
||||||
|
final String cameraIndex;
|
||||||
|
final UavLensType? lensType;
|
||||||
|
final VideoQualityType qualityType;
|
||||||
|
final int videoExpire;
|
||||||
|
|
||||||
|
const UavVideoStreamLoad({
|
||||||
|
required this.sn,
|
||||||
|
required this.cameraIndex,
|
||||||
|
this.lensType,
|
||||||
|
this.qualityType = VideoQualityType.adaptive,
|
||||||
|
this.videoExpire = 720000000,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [sn, cameraIndex, lensType, qualityType, videoExpire];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import '../../domain/entities/drone_station_entity.dart';
|
import '../../domain/entities/drone_station_entity.dart';
|
||||||
import '../../domain/entities/video_stream_entity.dart';
|
import '../../domain/entities/video_stream_entity.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
|
||||||
abstract class DroneStationState extends Equatable {
|
abstract class DroneStationState extends Equatable {
|
||||||
const DroneStationState();
|
const DroneStationState();
|
||||||
@@ -76,6 +77,37 @@ class VideoStreamError extends DroneStationState {
|
|||||||
|
|
||||||
const VideoStreamError(this.message);
|
const VideoStreamError(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 无人机实时视频流加载状态
|
||||||
|
class UavVideoStreamLoading extends DroneStationState {
|
||||||
|
const UavVideoStreamLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 无人机实时视频流加载成功状态
|
||||||
|
class UavVideoStreamLoaded extends DroneStationState {
|
||||||
|
final UavVideoStreamEntity videoStream;
|
||||||
|
final String cameraIndex;
|
||||||
|
final UavLensType? lensType;
|
||||||
|
|
||||||
|
const UavVideoStreamLoaded({
|
||||||
|
required this.videoStream,
|
||||||
|
required this.cameraIndex,
|
||||||
|
this.lensType,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [videoStream, cameraIndex, lensType];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 无人机实时视频流加载失败状态
|
||||||
|
class UavVideoStreamError extends DroneStationState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const UavVideoStreamError(this.message);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [message];
|
List<Object?> get props => [message];
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,627 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:dio/dio.dart';
|
||||||
|
import '../../../../../core/consts/http_api_consts.dart';
|
||||||
|
|
||||||
|
class CreateTaskPage extends StatefulWidget {
|
||||||
|
final String sn;
|
||||||
|
|
||||||
|
const CreateTaskPage({super.key, required this.sn});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CreateTaskPage> createState() => _CreateTaskPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CreateTaskPageState extends State<CreateTaskPage> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
|
||||||
|
// 表单字段控制器
|
||||||
|
final TextEditingController _taskNameController = TextEditingController();
|
||||||
|
final TextEditingController _rthAltitudeController = TextEditingController(
|
||||||
|
text: '10',
|
||||||
|
);
|
||||||
|
final TextEditingController _minBatteryController = TextEditingController(
|
||||||
|
text: '50',
|
||||||
|
);
|
||||||
|
|
||||||
|
// 下拉选择值
|
||||||
|
String? _selectedWaylineName; // 航线名称(显示用)
|
||||||
|
String? _selectedWaylineUuid; // 航线UUID(接口用)
|
||||||
|
String _rthMode = '智能'; // 返航模式:智能/预设
|
||||||
|
String _precision = 'GPS'; // 航线精度:GPS/RTK
|
||||||
|
String _breakPointResume = '自动'; // 断点续飞:自动/手动
|
||||||
|
String _taskType = '立即'; // 任务类型:立即
|
||||||
|
|
||||||
|
// 航线列表(name + uuid)
|
||||||
|
List<Map<String, dynamic>> _waylineList = [];
|
||||||
|
bool _isLoadingWaylines = false;
|
||||||
|
final Dio _dio = Dio();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_loadWaylines();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_taskNameController.dispose();
|
||||||
|
_rthAltitudeController.dispose();
|
||||||
|
_minBatteryController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF5F6F8),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'创建航线任务',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
),
|
||||||
|
body: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// 第一行:航线名称 + 选择航线
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTextField(
|
||||||
|
label: '航线名称 *',
|
||||||
|
controller: _taskNameController,
|
||||||
|
hint: '请输入',
|
||||||
|
validator: (value) =>
|
||||||
|
value == null || value.isEmpty ? '请输入航线名称' : null,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildSelectableField(
|
||||||
|
label: '选择航线 *',
|
||||||
|
value: _selectedWaylineName,
|
||||||
|
hint: '请选择航线',
|
||||||
|
onTap: _showWaylineSelector,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 第二行:机场SN + 降落地场SN
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTextField(
|
||||||
|
label: '机场SN *',
|
||||||
|
controller: TextEditingController(text: widget.sn),
|
||||||
|
enabled: false,
|
||||||
|
hint: '固定就是当前机场的sn',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildTextField(
|
||||||
|
label: '降落地场SN',
|
||||||
|
controller: TextEditingController(),
|
||||||
|
hint: '请输入',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 第三行:返航高度 + 返航模式
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTextField(
|
||||||
|
label: '返航高度(m)',
|
||||||
|
controller: _rthAltitudeController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
hint: '默认是10,可以上下调',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildDropdownField(
|
||||||
|
label: '返航模式',
|
||||||
|
value: _rthMode,
|
||||||
|
items: ['智能', '预设'],
|
||||||
|
onChanged: (value) => setState(() => _rthMode = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 第四行:航线精度 + 断点续飞
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildDropdownField(
|
||||||
|
label: '航线精度',
|
||||||
|
value: _precision,
|
||||||
|
items: ['GPS', 'RTK'],
|
||||||
|
onChanged: (value) => setState(() => _precision = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildDropdownField(
|
||||||
|
label: '断点续飞',
|
||||||
|
value: _breakPointResume,
|
||||||
|
items: ['自动', '手动'],
|
||||||
|
onChanged: (value) =>
|
||||||
|
setState(() => _breakPointResume = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
|
// 第五行:最低电量 + 任务类型
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: _buildTextField(
|
||||||
|
label: '最低电量(%)',
|
||||||
|
controller: _minBatteryController,
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
hint: '手动输入',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: _buildDropdownField(
|
||||||
|
label: '任务类型',
|
||||||
|
value: _taskType,
|
||||||
|
items: ['立即'],
|
||||||
|
onChanged: (value) => setState(() => _taskType = value!),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
|
// 底部按钮
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
OutlinedButton(
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
side: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
|
minimumSize: const Size(80, 36),
|
||||||
|
),
|
||||||
|
child: const Text('取消'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _onCreateTask,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF165DFF),
|
||||||
|
minimumSize: const Size(100, 36),
|
||||||
|
),
|
||||||
|
child: const Text('确认创建'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildTextField({
|
||||||
|
required String label,
|
||||||
|
required TextEditingController controller,
|
||||||
|
String? hint,
|
||||||
|
bool enabled = true,
|
||||||
|
TextInputType? keyboardType,
|
||||||
|
String? Function(String?)? validator,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
TextFormField(
|
||||||
|
controller: controller,
|
||||||
|
enabled: enabled,
|
||||||
|
keyboardType: keyboardType,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
hintText: hint,
|
||||||
|
hintStyle: const TextStyle(fontSize: 13, color: Color(0xFFC9CDD4)),
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF165DFF)),
|
||||||
|
),
|
||||||
|
disabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFF2F3F5)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
validator: validator,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildDropdownField({
|
||||||
|
required String label,
|
||||||
|
required String? value,
|
||||||
|
required List<String> items,
|
||||||
|
required void Function(String?) onChanged,
|
||||||
|
String? hint,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
DropdownButtonFormField<String>(
|
||||||
|
value: value,
|
||||||
|
hint: hint != null
|
||||||
|
? Text(
|
||||||
|
hint,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: Color(0xFFC9CDD4),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
items: items.map((item) {
|
||||||
|
return DropdownMenuItem(value: item, child: Text(item));
|
||||||
|
}).toList(),
|
||||||
|
onChanged: onChanged,
|
||||||
|
decoration: InputDecoration(
|
||||||
|
border: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
|
),
|
||||||
|
enabledBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFFE5E6EB)),
|
||||||
|
),
|
||||||
|
focusedBorder: OutlineInputBorder(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
borderSide: const BorderSide(color: Color(0xFF165DFF)),
|
||||||
|
),
|
||||||
|
contentPadding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCreateTask() {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// 验证航线是否选择
|
||||||
|
if (_selectedWaylineUuid == null) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('请选择航线')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_submitCreateTask();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提交创建任务
|
||||||
|
Future<void> _submitCreateTask() async {
|
||||||
|
try {
|
||||||
|
print('🔍 [CreateTask] 开始创建任务');
|
||||||
|
|
||||||
|
// 构建请求参数
|
||||||
|
final requestData = {
|
||||||
|
'name': _taskNameController.text,
|
||||||
|
'sn': widget.sn,
|
||||||
|
'landing_dock_sn': '', // 降落地场SN(可选)
|
||||||
|
'rth_altitude': int.parse(_rthAltitudeController.text),
|
||||||
|
'rth_mode': _rthMode == '智能' ? 'optimal' : 'straight_line',
|
||||||
|
'wayline_precision_type': _precision.toLowerCase(),
|
||||||
|
'resumable_status': _breakPointResume == '自动' ? 'auto' : 'manual',
|
||||||
|
'min_battery_capacity': int.parse(_minBatteryController.text),
|
||||||
|
'task_type': 'immediate',
|
||||||
|
'repeat_type': 'nonrepeating',
|
||||||
|
'time_zone': 'Asia/Shanghai',
|
||||||
|
'begin_at': null,
|
||||||
|
'end_at': null,
|
||||||
|
'recurring_task_start_time_list': [],
|
||||||
|
'wayline_uuid': _selectedWaylineUuid,
|
||||||
|
};
|
||||||
|
|
||||||
|
print('📦 [CreateTask] 请求参数: $requestData');
|
||||||
|
|
||||||
|
// 调用接口
|
||||||
|
final response = await _dio.post(
|
||||||
|
HttpApiConsts.createFlightTask,
|
||||||
|
data: requestData,
|
||||||
|
);
|
||||||
|
|
||||||
|
print('📡 [CreateTask] 响应状态码: ${response.statusCode}');
|
||||||
|
print('📦 [CreateTask] 响应数据: ${response.data}');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
final Map<String, dynamic> jsonData = (response.data is String)
|
||||||
|
? json.decode(response.data)
|
||||||
|
: Map<String, dynamic>.from(response.data);
|
||||||
|
|
||||||
|
if (jsonData['code'] == 0 || jsonData['code'] == 200) {
|
||||||
|
final taskUuid = jsonData['data']['task_uuid'];
|
||||||
|
print('✅ [CreateTask] 任务创建成功, task_uuid: $taskUuid');
|
||||||
|
|
||||||
|
Navigator.pop(context);
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('任务创建成功,任务ID: $taskUuid')));
|
||||||
|
} else {
|
||||||
|
throw Exception(jsonData['message'] ?? '创建任务失败');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw Exception('网络请求失败');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [CreateTask] 创建任务失败: $e');
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(SnackBar(content: Text('创建任务失败: $e')));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载航线列表
|
||||||
|
Future<void> _loadWaylines() async {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingWaylines = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
print('🔍 [CreateTask] 开始加载航线列表, sn: ${widget.sn}');
|
||||||
|
final response = await _dio.get(
|
||||||
|
HttpApiConsts.getWayline,
|
||||||
|
queryParameters: {'sn': widget.sn},
|
||||||
|
);
|
||||||
|
|
||||||
|
print('📡 [CreateTask] 响应状态码: ${response.statusCode}');
|
||||||
|
print('📦 [CreateTask] 响应数据类型: ${response.data.runtimeType}');
|
||||||
|
print('📦 [CreateTask] 响应数据: ${response.data}');
|
||||||
|
|
||||||
|
if (response.statusCode == 200) {
|
||||||
|
// 如果 response.data 是 String,手动解析 JSON
|
||||||
|
final Map<String, dynamic> jsonData = (response.data is String)
|
||||||
|
? json.decode(response.data)
|
||||||
|
: Map<String, dynamic>.from(response.data);
|
||||||
|
|
||||||
|
if (jsonData['code'] == 200 || jsonData['code'] == 0) {
|
||||||
|
final dataField = jsonData['data'];
|
||||||
|
print('🔍 [CreateTask] data 类型: ${dataField.runtimeType}');
|
||||||
|
print('🔍 [CreateTask] data 值: $dataField');
|
||||||
|
|
||||||
|
// data 可能是 List 或包含 list 字段的对象
|
||||||
|
List<dynamic>? waylinesData;
|
||||||
|
|
||||||
|
if (dataField is List) {
|
||||||
|
waylinesData = dataField;
|
||||||
|
} else if (dataField is Map && dataField.containsKey('list')) {
|
||||||
|
waylinesData = dataField['list'];
|
||||||
|
print('🔍 [CreateTask] 从 data.list 获取航线列表');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waylinesData != null && waylinesData is List) {
|
||||||
|
setState(() {
|
||||||
|
_waylineList = waylinesData!.map((item) {
|
||||||
|
if (item is Map) {
|
||||||
|
return Map<String, dynamic>.from(item);
|
||||||
|
}
|
||||||
|
return {'name': item.toString()};
|
||||||
|
}).toList();
|
||||||
|
});
|
||||||
|
print('✅ [CreateTask] 加载航线列表成功,共 ${_waylineList.length} 条');
|
||||||
|
} else {
|
||||||
|
print('❌ [CreateTask] 未找到航线列表数据');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print(
|
||||||
|
'❌ [CreateTask] 业务异常, code: ${jsonData["code"]}, message: ${jsonData["message"]}',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('❌ [CreateTask] HTTP 错误, status: ${response.statusCode}');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('❌ [CreateTask] 加载航线列表失败: $e');
|
||||||
|
} finally {
|
||||||
|
setState(() {
|
||||||
|
_isLoadingWaylines = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示航线选择器
|
||||||
|
void _showWaylineSelector() {
|
||||||
|
if (_waylineList.isEmpty) {
|
||||||
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(const SnackBar(content: Text('暂无可用航线')));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showModalBottomSheet(
|
||||||
|
context: context,
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
shape: const RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
|
||||||
|
),
|
||||||
|
builder: (context) {
|
||||||
|
return Container(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'选择航线',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.close, size: 20),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Flexible(
|
||||||
|
child: ListView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemCount: _waylineList.length,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final wayline = _waylineList[index];
|
||||||
|
final waylineName = wayline['name'] ?? '未命名航线';
|
||||||
|
// 兼容多种字段名
|
||||||
|
final waylineUuid =
|
||||||
|
wayline['uuid'] ??
|
||||||
|
wayline['waylineUuid'] ??
|
||||||
|
wayline['id'] ??
|
||||||
|
'';
|
||||||
|
// print('🔍 [CreateTask] 航线数据: $wayline');
|
||||||
|
// print('🔍 [CreateTask] 解析的UUID: $waylineUuid');
|
||||||
|
return ListTile(
|
||||||
|
title: Text(waylineName),
|
||||||
|
subtitle: waylineUuid.isNotEmpty
|
||||||
|
? Text(
|
||||||
|
waylineUuid,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Color(0xFF86909C),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: null,
|
||||||
|
trailing: _selectedWaylineName == waylineName
|
||||||
|
? const Icon(Icons.check, color: Color(0xFF165DFF))
|
||||||
|
: null,
|
||||||
|
onTap: () {
|
||||||
|
setState(() {
|
||||||
|
_selectedWaylineName = waylineName;
|
||||||
|
_selectedWaylineUuid = waylineUuid;
|
||||||
|
});
|
||||||
|
print(
|
||||||
|
'✅ [CreateTask] 已选择航线: $_selectedWaylineName, UUID: $_selectedWaylineUuid',
|
||||||
|
);
|
||||||
|
Navigator.pop(context);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 可点击的选择字段
|
||||||
|
Widget _buildSelectableField({
|
||||||
|
required String label,
|
||||||
|
required String? value,
|
||||||
|
required String hint,
|
||||||
|
required VoidCallback onTap,
|
||||||
|
}) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
GestureDetector(
|
||||||
|
onTap: onTap,
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
border: Border.all(color: const Color(0xFFE5E6EB)),
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
value ?? hint,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
color: value != null
|
||||||
|
? const Color(0xFF1D2129)
|
||||||
|
: const Color(0xFFC9CDD4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Icon(Icons.arrow_drop_down, color: Color(0xFF86909C)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -28,6 +28,8 @@ class DroneStationDetailPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||||
late DroneStationBloc _bloc;
|
late DroneStationBloc _bloc;
|
||||||
|
UAVDetailEntity? _detail; // 无人机详情数据
|
||||||
|
String? _droneSn; // 无人机序列号
|
||||||
|
|
||||||
// 悬浮视频监控状态
|
// 悬浮视频监控状态
|
||||||
bool showFloatingMonitor = false;
|
bool showFloatingMonitor = false;
|
||||||
@@ -52,6 +54,9 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
// 加载超时计时器
|
// 加载超时计时器
|
||||||
Timer? _floatingLoadingTimer;
|
Timer? _floatingLoadingTimer;
|
||||||
static const _floatingLoadingTimeout = Duration(seconds: 15);
|
static const _floatingLoadingTimeout = Duration(seconds: 15);
|
||||||
|
|
||||||
|
// 无人机状态轮询计时器
|
||||||
|
Timer? _droneStatusPollingTimer;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -63,6 +68,9 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
deviceSn: widget.station.deviceSn,
|
deviceSn: widget.station.deviceSn,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 启动无人机状态轮询(每5秒刷新一次)
|
||||||
|
_startDroneStatusPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -70,9 +78,29 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
_bloc.close();
|
_bloc.close();
|
||||||
_destroyFloatingRtcEngine();
|
_destroyFloatingRtcEngine();
|
||||||
_floatingLoadingTimer?.cancel();
|
_floatingLoadingTimer?.cancel();
|
||||||
|
_droneStatusPollingTimer?.cancel(); // 停止轮询
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 启动无人机状态轮询
|
||||||
|
void _startDroneStatusPolling() {
|
||||||
|
// 每5秒刷新一次无人机状态
|
||||||
|
_droneStatusPollingTimer = Timer.periodic(const Duration(seconds: 5), (timer) {
|
||||||
|
if (!mounted) {
|
||||||
|
timer.cancel();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('🔄 定时刷新无人机状态...');
|
||||||
|
_bloc.add(
|
||||||
|
UAVDetailLoad(
|
||||||
|
gatewaySn: widget.station.gatewaySn,
|
||||||
|
deviceSn: widget.station.deviceSn,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// 销毁悬浮窗的 RTC 引擎
|
// 销毁悬浮窗的 RTC 引擎
|
||||||
void _destroyFloatingRtcEngine() async {
|
void _destroyFloatingRtcEngine() async {
|
||||||
// 销毁火山引擎 RTC
|
// 销毁火山引擎 RTC
|
||||||
@@ -202,6 +230,8 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildContent(UAVDetailEntity detail) {
|
Widget _buildContent(UAVDetailEntity detail) {
|
||||||
|
_detail = detail; // 保存详情数据供其他方法使用
|
||||||
|
_droneSn = detail.deviceSn; // 保存无人机序列号
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
ListView(
|
ListView(
|
||||||
@@ -371,7 +401,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const DroneVideoControlPage(),
|
builder: (context) => DroneVideoControlPage(
|
||||||
|
droneSn: detail.deviceSn,
|
||||||
|
// 使用无人机摄像头列表,如果为null则传空列表
|
||||||
|
cameraList: detail.droneCameraList ?? [],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -667,8 +701,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) =>
|
builder: (context) => DroneMissionControlPage(
|
||||||
DroneMissionControlPage(selectedTasks: tasks),
|
selectedTasks: tasks,
|
||||||
|
droneSn: _droneSn,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,75 +1,416 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
|
||||||
|
import '../../../../../core/di/injection.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
import '../../domain/entities/drone_station_entity.dart';
|
||||||
|
import '../bloc/drone_station_bloc.dart';
|
||||||
|
import '../bloc/drone_station_event.dart';
|
||||||
|
import '../bloc/drone_station_state.dart';
|
||||||
|
|
||||||
/// 无人机视频回传/远程控制页面
|
/// 无人机视频回传/远程控制页面
|
||||||
class DroneVideoControlPage extends StatefulWidget {
|
class DroneVideoControlPage extends StatefulWidget {
|
||||||
const DroneVideoControlPage({super.key});
|
final String droneSn; // 无人机设备序列号
|
||||||
|
final List<CameraInfo>? cameraList; // 摄像头列表
|
||||||
|
|
||||||
|
const DroneVideoControlPage({
|
||||||
|
super.key,
|
||||||
|
required this.droneSn,
|
||||||
|
this.cameraList,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<DroneVideoControlPage> createState() => _DroneVideoControlPageState();
|
State<DroneVideoControlPage> createState() => _DroneVideoControlPageState();
|
||||||
}
|
}
|
||||||
|
|
||||||
class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
||||||
|
late DroneStationBloc _bloc;
|
||||||
int _selectedTab = 0; // 0: 视频画面, 1: 热成像, 2: 抓拍记录
|
int _selectedTab = 0; // 0: 视频画面, 1: 热成像, 2: 抓拍记录
|
||||||
|
UavVideoStreamEntity? _videoStream;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
UavLensType? _currentLensType;
|
||||||
|
CameraInfo? _currentCamera; // 当前选中的摄像头
|
||||||
|
|
||||||
|
// 火山引擎 RTC
|
||||||
|
volc.RTCEngine? _rtcEngine;
|
||||||
|
volc.RTCRoom? _rtcRoom;
|
||||||
|
volc.RTCViewContext? _remoteRenderContext;
|
||||||
|
String? _remoteUserId;
|
||||||
|
|
||||||
|
// 事件处理器(注意:使用 I 前缀的接口)
|
||||||
|
final volc.IRTCEngineEventHandler _engineEventHandler = volc.IRTCEngineEventHandler();
|
||||||
|
final volc.IRTCRoomEventHandler _roomEventHandler = volc.IRTCRoomEventHandler();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
void initState() {
|
||||||
return Scaffold(
|
super.initState();
|
||||||
backgroundColor: const Color(0xFFF5F6F8),
|
_bloc = sl<DroneStationBloc>();
|
||||||
appBar: AppBar(
|
|
||||||
backgroundColor: Colors.white,
|
// 初始化事件处理器
|
||||||
elevation: 0,
|
_initVolcEventHandlers();
|
||||||
leading: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
// 打印接收到的参数
|
||||||
onPressed: () => Navigator.pop(context),
|
debugPrint('=== 视频控制页面接收参数 ===');
|
||||||
),
|
debugPrint('droneSn: ${widget.droneSn}');
|
||||||
title: const Text(
|
debugPrint('cameraList: ${widget.cameraList}');
|
||||||
'无人机视频回传 / 远程控制',
|
if (widget.cameraList != null) {
|
||||||
style: TextStyle(
|
for (var camera in widget.cameraList!) {
|
||||||
fontSize: 18,
|
debugPrint(' - cameraIndex: ${camera.cameraIndex}');
|
||||||
fontWeight: FontWeight.bold,
|
}
|
||||||
color: Color(0xFF1D2129),
|
}
|
||||||
),
|
debugPrint('===========================\n');
|
||||||
),
|
|
||||||
centerTitle: true,
|
// 默认选择第一个摄像头
|
||||||
actions: [
|
_currentCamera = widget.cameraList?.isNotEmpty == true
|
||||||
IconButton(
|
? widget.cameraList!.first
|
||||||
icon: const Icon(Icons.settings, color: Color(0xFF1D2129)),
|
: null;
|
||||||
onPressed: () {},
|
|
||||||
),
|
debugPrint('默认选择的摄像头: ${_currentCamera?.cameraIndex}');
|
||||||
],
|
|
||||||
),
|
// 默认加载广角镜头
|
||||||
body: Column(
|
if (_currentCamera != null) {
|
||||||
children: [
|
_loadVideoStream(UavLensType.wide);
|
||||||
// Tab切换
|
}
|
||||||
_buildTabBar(),
|
}
|
||||||
// 内容区域
|
|
||||||
Expanded(
|
@override
|
||||||
child: ListView(
|
void dispose() {
|
||||||
padding: const EdgeInsets.all(16),
|
_destroyRtcEngine();
|
||||||
children: [
|
_bloc.close();
|
||||||
// 视频画面
|
super.dispose();
|
||||||
_buildVideoPlayer(),
|
}
|
||||||
const SizedBox(height: 12),
|
|
||||||
// 飞行数据
|
// 销毁 RTC 引擎
|
||||||
_buildFlightData(),
|
Future<void> _destroyRtcEngine() async {
|
||||||
const SizedBox(height: 12),
|
if (_rtcRoom != null) {
|
||||||
// AI识别结果
|
try {
|
||||||
_buildAIResults(),
|
await _rtcRoom?.leaveRoom();
|
||||||
const SizedBox(height: 12),
|
} catch (_) {}
|
||||||
// 地图和摇杆
|
_rtcRoom = null;
|
||||||
_buildMapAndJoystick(),
|
}
|
||||||
const SizedBox(height: 16),
|
if (_rtcEngine != null) {
|
||||||
// 底部工具栏
|
try {
|
||||||
_buildBottomToolbar(),
|
_rtcEngine?.destroy(); // 注意:destroy() 返回 void,不能用 await
|
||||||
],
|
} catch (_) {}
|
||||||
),
|
_rtcEngine = null;
|
||||||
),
|
}
|
||||||
],
|
_remoteRenderContext = null;
|
||||||
|
_remoteUserId = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载视频流
|
||||||
|
void _loadVideoStream(UavLensType lensType) {
|
||||||
|
if (_currentCamera == null) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = '没有可用的摄像头';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
debugPrint('=== 🎥 镜头切换 ===');
|
||||||
|
debugPrint('当前摄像头: ${_currentCamera!.cameraIndex}');
|
||||||
|
debugPrint('新镜头类型: ${lensType.name} (${_getLensTypeName(lensType)})');
|
||||||
|
debugPrint('旧镜头类型: ${_currentLensType?.name ?? "none"}');
|
||||||
|
debugPrint('==================\n');
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
_currentLensType = lensType;
|
||||||
|
});
|
||||||
|
|
||||||
|
_bloc.add(
|
||||||
|
UavVideoStreamLoad(
|
||||||
|
sn: widget.droneSn,
|
||||||
|
cameraIndex: _currentCamera!.cameraIndex,
|
||||||
|
lensType: lensType,
|
||||||
|
qualityType: VideoQualityType.adaptive,
|
||||||
|
videoExpire: 720000000,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Tab切换栏
|
/// 切换摄像头
|
||||||
|
void _switchCamera(CameraInfo camera) {
|
||||||
|
if (camera.cameraIndex == _currentCamera?.cameraIndex) {
|
||||||
|
return; // 如果选择的摄像头与当前相同,不执行操作
|
||||||
|
}
|
||||||
|
|
||||||
|
setState(() {
|
||||||
|
_currentCamera = camera;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重新加载视频流
|
||||||
|
_loadVideoStream(_currentLensType ?? UavLensType.wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化火山引擎事件处理器
|
||||||
|
void _initVolcEventHandlers() {
|
||||||
|
_engineEventHandler.onWarning = (volc.WarningCode code) {
|
||||||
|
debugPrint('⚠️ Volc Warning: $code');
|
||||||
|
};
|
||||||
|
|
||||||
|
_engineEventHandler.onError = (volc.ErrorCode code) {
|
||||||
|
debugPrint('❌ Volc Error: $code');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = '视频错误: $code';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_engineEventHandler.onFirstRemoteVideoFrameDecoded = (
|
||||||
|
String streamId,
|
||||||
|
volc.StreamInfo streamInfo,
|
||||||
|
volc.VideoFrameInfo frameInfo,
|
||||||
|
) {
|
||||||
|
debugPrint('✅✅✅ Volc 第一帧视频解码完成!');
|
||||||
|
debugPrint(' streamId: $streamId, userId: ${streamInfo.userId}');
|
||||||
|
|
||||||
|
if (streamInfo.userId.isNotEmpty && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_remoteUserId = streamInfo.userId;
|
||||||
|
_remoteRenderContext = volc.RTCViewContext.remoteContext(
|
||||||
|
roomId: _videoStream?.roomId ?? '',
|
||||||
|
userId: streamInfo.userId,
|
||||||
|
streamId: streamId,
|
||||||
|
);
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_roomEventHandler.onUserPublishStreamVideo = (
|
||||||
|
String userId,
|
||||||
|
volc.StreamInfo streamInfo,
|
||||||
|
bool isPublish,
|
||||||
|
) {
|
||||||
|
debugPrint('📹 Volc 远端用户 $userId 视频流状态: $isPublish');
|
||||||
|
if (isPublish && mounted && _remoteUserId == null) {
|
||||||
|
debugPrint('⏳ 检测到 Volc 视频流推送,等待第一帧解码...');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_roomEventHandler.onUserLeave = (String userId, int reason) {
|
||||||
|
debugPrint('👋 Volc 用户离开: $userId');
|
||||||
|
if (userId == _remoteUserId && mounted) {
|
||||||
|
setState(() {
|
||||||
|
_remoteRenderContext = null;
|
||||||
|
_remoteUserId = null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化火山引擎 RTC
|
||||||
|
Future<void> _initRtcEngine(UavVideoStreamEntity stream) async {
|
||||||
|
final appId = stream.appId;
|
||||||
|
final roomId = stream.roomId;
|
||||||
|
final token = stream.token;
|
||||||
|
final userId = stream.userId.isNotEmpty
|
||||||
|
? stream.userId
|
||||||
|
: 'user_${DateTime.now().millisecondsSinceEpoch}';
|
||||||
|
|
||||||
|
debugPrint('=== VolcEngine 参数 ===');
|
||||||
|
debugPrint('AppId: "$appId"');
|
||||||
|
debugPrint('RoomId: "$roomId"');
|
||||||
|
debugPrint('UserId: "$userId"');
|
||||||
|
|
||||||
|
if (appId.isEmpty || roomId.isEmpty || token.isEmpty) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'VolcEngine 参数缺失';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
debugPrint('🚀 创建 VolcEngine 引擎...');
|
||||||
|
|
||||||
|
_rtcEngine = await volc.RTCEngine.createRTCEngine(
|
||||||
|
volc.RTCVideoContext(appId: appId, eventHandler: _engineEventHandler),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (_rtcEngine == null) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'VolcEngine 引擎创建失败';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint('✅ VolcEngine 引擎创建成功');
|
||||||
|
|
||||||
|
_rtcRoom = await _rtcEngine?.createRTCRoom(roomId);
|
||||||
|
if (_rtcRoom == null) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'VolcEngine 房间创建失败';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
debugPrint('✅ VolcEngine 房间创建成功');
|
||||||
|
|
||||||
|
await _rtcRoom?.setRTCRoomEventHandler(_roomEventHandler);
|
||||||
|
|
||||||
|
debugPrint('🔑 加入 VolcEngine 房间...');
|
||||||
|
|
||||||
|
await _rtcRoom?.joinRoom(
|
||||||
|
token: token,
|
||||||
|
userInfo: volc.UserInfo(userId: userId, extraInfo: ''),
|
||||||
|
userVisibility: true,
|
||||||
|
roomConfig: volc.RoomConfig(
|
||||||
|
isPublishAudio: false,
|
||||||
|
isPublishVideo: false,
|
||||||
|
isAutoSubscribeAudio: false,
|
||||||
|
isAutoSubscribeVideo: true,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
debugPrint('✅ 成功加入 VolcEngine 房间: $roomId');
|
||||||
|
debugPrint('⏳ 等待视频流推送...');
|
||||||
|
} catch (e) {
|
||||||
|
debugPrint('❌ VolcEngine 初始化失败: $e');
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = 'VolcEngine 加入失败: $e';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _bloc,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: const Color(0xFFF5F6F8),
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'无人机视频回传 / 远程控制',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Color(0xFF1D2129),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (widget.cameraList != null && widget.cameraList!.isNotEmpty)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 4),
|
||||||
|
child: DropdownButtonHideUnderline(
|
||||||
|
child: DropdownButton<CameraInfo>(
|
||||||
|
value: _currentCamera,
|
||||||
|
hint: const Text(
|
||||||
|
'选择摄像头',
|
||||||
|
style: TextStyle(fontSize: 12, color: Color(0xFF86909C)),
|
||||||
|
),
|
||||||
|
items: widget.cameraList!.map((camera) {
|
||||||
|
return DropdownMenuItem<CameraInfo>(
|
||||||
|
value: camera,
|
||||||
|
child: Text(
|
||||||
|
camera.cameraIndex,
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
onChanged: (CameraInfo? newCamera) {
|
||||||
|
if (newCamera != null) {
|
||||||
|
_switchCamera(newCamera);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isDense: true,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
color: Color(0xFF165DFF),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
centerTitle: false,
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.settings, color: Color(0xFF1D2129)),
|
||||||
|
onPressed: () {},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is UavVideoStreamLoaded) {
|
||||||
|
setState(() {
|
||||||
|
_videoStream = state.videoStream;
|
||||||
|
});
|
||||||
|
debugPrint('=== 视频流加载成功 ===');
|
||||||
|
debugPrint('URL Type: ${state.videoStream.urlType}');
|
||||||
|
|
||||||
|
if (state.videoStream.urlType == 'volc') {
|
||||||
|
_destroyRtcEngine();
|
||||||
|
_initRtcEngine(state.videoStream);
|
||||||
|
} else {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = '不支持的 URL 类型: ${state.videoStream.urlType}';
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (state is UavVideoStreamError) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = state.message;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
_buildTabBar(),
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
children: [
|
||||||
|
_buildVideoPlayer(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildFlightData(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildAIResults(),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildMapAndJoystick(),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
_buildBottomToolbar(),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getLensTypeName(UavLensType lensType) {
|
||||||
|
switch (lensType) {
|
||||||
|
case UavLensType.wide:
|
||||||
|
return '广角';
|
||||||
|
case UavLensType.zoom:
|
||||||
|
return '变焦';
|
||||||
|
case UavLensType.ir:
|
||||||
|
return '红外';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildTabBar() {
|
Widget _buildTabBar() {
|
||||||
return Container(
|
return Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -115,7 +456,6 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 视频播放器
|
|
||||||
Widget _buildVideoPlayer() {
|
Widget _buildVideoPlayer() {
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
@@ -127,76 +467,122 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.asset(
|
child: _isLoading
|
||||||
'assets/images/xunjian.png',
|
? Center(
|
||||||
fit: BoxFit.cover,
|
child: Column(
|
||||||
width: double.infinity,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
),
|
children: [
|
||||||
|
const CircularProgressIndicator(
|
||||||
|
valueColor: AlwaysStoppedAnimation<Color>(Colors.white),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text(
|
||||||
|
'正在加载视频流...',
|
||||||
|
style: TextStyle(fontSize: 14, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _errorMessage != null
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.error_outline, color: Colors.white, size: 48),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||||
|
child: Text(
|
||||||
|
_errorMessage!,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () => _loadVideoStream(_currentLensType ?? UavLensType.wide),
|
||||||
|
style: ElevatedButton.styleFrom(backgroundColor: const Color(0xFF165DFF)),
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: _videoStream != null && _remoteRenderContext != null
|
||||||
|
? volc.RTCSurfaceView(
|
||||||
|
context: _remoteRenderContext!,
|
||||||
|
renderMode: volc.VideoRenderMode.fit,
|
||||||
|
)
|
||||||
|
: _videoStream != null
|
||||||
|
? Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(Icons.videocam_off, size: 48, color: Colors.grey),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
const Text('等待视频流推送...', style: TextStyle(fontSize: 14, color: Colors.white)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: Image.asset('assets/images/xunjian.png', fit: BoxFit.cover, width: double.infinity),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 录制标识
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 12,
|
top: 12,
|
||||||
left: 12,
|
left: 12,
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Container(width: 8, height: 8, decoration: const BoxDecoration(color: Color(0xFFF53F3F), shape: BoxShape.circle)),
|
||||||
width: 8,
|
|
||||||
height: 8,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Color(0xFFF53F3F),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
const Text(
|
const Text('REC 00:12:36', style: TextStyle(fontSize: 12, color: Colors.white, fontWeight: FontWeight.w500)),
|
||||||
'REC 00:12:36',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Colors.white,
|
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 缩放标识
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 12,
|
bottom: 12,
|
||||||
right: 12,
|
right: 12,
|
||||||
child: Container(
|
child: PopupMenuButton<UavLensType>(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
icon: Container(
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||||
color: Colors.black.withOpacity(0.5),
|
decoration: BoxDecoration(
|
||||||
borderRadius: BorderRadius.circular(4),
|
color: Colors.black.withOpacity(0.5),
|
||||||
),
|
borderRadius: BorderRadius.circular(4),
|
||||||
child: const Text(
|
),
|
||||||
'1.0x',
|
child: Row(
|
||||||
style: TextStyle(
|
mainAxisSize: MainAxisSize.min,
|
||||||
fontSize: 12,
|
children: [
|
||||||
color: Colors.white,
|
Text(_getLensTypeName(_currentLensType ?? UavLensType.wide), style: const TextStyle(fontSize: 12, color: Colors.white)),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
const Icon(Icons.arrow_drop_down, color: Colors.white, size: 16),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
onSelected: (UavLensType lensType) {
|
||||||
|
debugPrint('👆 用户点击了镜头切换: ${lensType.name}');
|
||||||
|
if (lensType != _currentLensType) {
|
||||||
|
debugPrint('✅ 镜头类型不同,开始加载...');
|
||||||
|
_loadVideoStream(lensType);
|
||||||
|
} else {
|
||||||
|
debugPrint('⚠️ 镜头类型相同,忽略操作');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemBuilder: (BuildContext context) => <PopupMenuEntry<UavLensType>>[
|
||||||
|
const PopupMenuItem<UavLensType>(value: UavLensType.wide, child: Text('广角')),
|
||||||
|
const PopupMenuItem<UavLensType>(value: UavLensType.zoom, child: Text('变焦')),
|
||||||
|
const PopupMenuItem<UavLensType>(value: UavLensType.ir, child: Text('红外')),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 飞行数据
|
|
||||||
Widget _buildFlightData() {
|
Widget _buildFlightData() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x0D000000),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -217,53 +603,26 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF86909C))),
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(value, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||||
value,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF1D2129),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// AI识别结果
|
|
||||||
Widget _buildAIResults() {
|
Widget _buildAIResults() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x0D000000),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text('AI 识别结果', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Color(0xFF1D2129))),
|
||||||
'AI 识别结果',
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: Color(0xFF1D2129),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildAIResultItem('热斑疑似', '3 处'),
|
_buildAIResultItem('热斑疑似', '3 处'),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
@@ -278,128 +637,51 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
onTap: () {},
|
onTap: () {},
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(Icons.warning_amber_rounded, color: Color(0xFFFF7D00), size: 20),
|
||||||
Icons.warning_amber_rounded,
|
|
||||||
color: Color(0xFFFF7D00),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(child: Text(label, style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)))),
|
||||||
child: Text(
|
Text(count, style: const TextStyle(fontSize: 14, color: Color(0xFF4E5969))),
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Color(0xFF1D2129),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
count,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 14,
|
|
||||||
color: Color(0xFF4E5969),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(
|
const Icon(Icons.chevron_right, color: Color(0xFF86909C), size: 20),
|
||||||
Icons.chevron_right,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 地图和摇杆
|
|
||||||
Widget _buildMapAndJoystick() {
|
Widget _buildMapAndJoystick() {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
// 地图
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 160,
|
height: 160,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x0D000000),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
child: Image.asset(
|
child: Image.asset('assets/images/xunjian.png', fit: BoxFit.cover, width: double.infinity),
|
||||||
'assets/images/xunjian.png',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
width: double.infinity,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
// 摇杆控制器
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 160,
|
height: 160,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x0D000000),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
// 中心圆点
|
Container(width: 40, height: 40, decoration: BoxDecoration(color: const Color(0xFFC9CDD4), shape: BoxShape.circle)),
|
||||||
Container(
|
Positioned(top: 16, child: IconButton(icon: const Icon(Icons.arrow_drop_up, size: 32, color: Color(0xFF4E5969)), onPressed: () {})),
|
||||||
width: 40,
|
Positioned(bottom: 16, child: IconButton(icon: const Icon(Icons.arrow_drop_down, size: 32, color: Color(0xFF4E5969)), onPressed: () {})),
|
||||||
height: 40,
|
Positioned(left: 16, child: IconButton(icon: const Icon(Icons.arrow_left, size: 32, color: Color(0xFF4E5969)), onPressed: () {})),
|
||||||
decoration: BoxDecoration(
|
Positioned(right: 16, child: IconButton(icon: const Icon(Icons.arrow_right, size: 32, color: Color(0xFF4E5969)), onPressed: () {})),
|
||||||
color: const Color(0xFFC9CDD4),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 上
|
|
||||||
Positioned(
|
|
||||||
top: 16,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_drop_up, size: 32, color: Color(0xFF4E5969)),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 下
|
|
||||||
Positioned(
|
|
||||||
bottom: 16,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_drop_down, size: 32, color: Color(0xFF4E5969)),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 左
|
|
||||||
Positioned(
|
|
||||||
left: 16,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_left, size: 32, color: Color(0xFF4E5969)),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// 右
|
|
||||||
Positioned(
|
|
||||||
right: 16,
|
|
||||||
child: IconButton(
|
|
||||||
icon: const Icon(Icons.arrow_right, size: 32, color: Color(0xFF4E5969)),
|
|
||||||
onPressed: () {},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -408,20 +690,13 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 底部工具栏
|
|
||||||
Widget _buildBottomToolbar() {
|
Widget _buildBottomToolbar() {
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
boxShadow: const [
|
boxShadow: const [BoxShadow(color: Color(0x0D000000), blurRadius: 8, offset: Offset(0, 2))],
|
||||||
BoxShadow(
|
|
||||||
color: Color(0x0D000000),
|
|
||||||
blurRadius: 8,
|
|
||||||
offset: Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||||
@@ -442,15 +717,10 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
|
|||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 28, color: const Color(0xFF4E5969)),
|
Icon(icon, size: 28, color: const Color(0xFF4E5969)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(label, style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969))),
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 12,
|
|
||||||
color: Color(0xFF4E5969),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import '../../../../../core/di/injection.dart';
|
||||||
|
import '../../domain/entities/uav_video_stream_entity.dart';
|
||||||
|
import '../bloc/drone_station_bloc.dart';
|
||||||
|
import '../bloc/drone_station_event.dart';
|
||||||
|
import '../bloc/drone_station_state.dart';
|
||||||
|
|
||||||
|
/// 无人机实时视频监控页面(使用新的 changeLens 接口)
|
||||||
|
class UavLiveVideoPage extends StatefulWidget {
|
||||||
|
final String droneSn; // 无人机设备序列号
|
||||||
|
final String cameraIndex; // 摄像头编号
|
||||||
|
|
||||||
|
const UavLiveVideoPage({
|
||||||
|
super.key,
|
||||||
|
required this.droneSn,
|
||||||
|
required this.cameraIndex,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<UavLiveVideoPage> createState() => _UavLiveVideoPageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _UavLiveVideoPageState extends State<UavLiveVideoPage> {
|
||||||
|
late DroneStationBloc _bloc;
|
||||||
|
UavVideoStreamEntity? _videoStream;
|
||||||
|
bool _isLoading = false;
|
||||||
|
String? _errorMessage;
|
||||||
|
UavLensType? _currentLensType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_bloc = sl<DroneStationBloc>();
|
||||||
|
// 默认加载广角镜头
|
||||||
|
_loadVideoStream(UavLensType.wide);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_bloc.close();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 加载视频流
|
||||||
|
void _loadVideoStream(UavLensType lensType) {
|
||||||
|
setState(() {
|
||||||
|
_isLoading = true;
|
||||||
|
_errorMessage = null;
|
||||||
|
_currentLensType = lensType;
|
||||||
|
});
|
||||||
|
|
||||||
|
_bloc.add(
|
||||||
|
UavVideoStreamLoad(
|
||||||
|
sn: widget.droneSn,
|
||||||
|
cameraIndex: widget.cameraIndex,
|
||||||
|
lensType: lensType,
|
||||||
|
qualityType: VideoQualityType.adaptive,
|
||||||
|
videoExpire: 720000000,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return BlocProvider.value(
|
||||||
|
value: _bloc,
|
||||||
|
child: Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
appBar: AppBar(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
elevation: 0,
|
||||||
|
leading: IconButton(
|
||||||
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
|
onPressed: () => Navigator.pop(context),
|
||||||
|
),
|
||||||
|
title: const Text(
|
||||||
|
'无人机实时视频',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
centerTitle: true,
|
||||||
|
actions: [
|
||||||
|
// 镜头切换按钮
|
||||||
|
PopupMenuButton<UavLensType>(
|
||||||
|
icon: const Icon(Icons.videocam, color: Colors.white),
|
||||||
|
tooltip: '切换镜头',
|
||||||
|
onSelected: (lensType) {
|
||||||
|
_loadVideoStream(lensType);
|
||||||
|
},
|
||||||
|
itemBuilder: (context) => [
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: UavLensType.wide,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.videocam, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('广角镜头'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: UavLensType.zoom,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.zoom_in, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('变焦镜头'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const PopupMenuItem(
|
||||||
|
value: UavLensType.ir,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(Icons.thermostat, size: 20),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('红外镜头'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: BlocConsumer<DroneStationBloc, DroneStationState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state is UavVideoStreamLoaded) {
|
||||||
|
setState(() {
|
||||||
|
_videoStream = state.videoStream;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
debugPrint('=== 视频流加载成功 ===');
|
||||||
|
debugPrint('URL Type: ${state.videoStream.urlType}');
|
||||||
|
debugPrint('AppId: ${state.videoStream.appId}');
|
||||||
|
debugPrint('RoomId: ${state.videoStream.roomId}');
|
||||||
|
debugPrint('UserId: ${state.videoStream.userId}');
|
||||||
|
// TODO: 这里可以初始化 RTC 引擎并显示视频
|
||||||
|
} else if (state is UavVideoStreamError) {
|
||||||
|
setState(() {
|
||||||
|
_errorMessage = state.message;
|
||||||
|
_isLoading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
builder: (context, state) {
|
||||||
|
if (_isLoading) {
|
||||||
|
return const Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(color: Colors.white),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'正在加载视频流...',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_errorMessage != null) {
|
||||||
|
return Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.error_outline,
|
||||||
|
size: 48,
|
||||||
|
color: Colors.red,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
_errorMessage!,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_currentLensType != null) {
|
||||||
|
_loadVideoStream(_currentLensType!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: const Text('重试'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_videoStream == null) {
|
||||||
|
return const Center(
|
||||||
|
child: Text(
|
||||||
|
'暂无视频信号',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: 这里根据 _videoStream 的 urlType 初始化对应的 RTC 引擎
|
||||||
|
// - 如果是 volc,使用火山引擎 RTC SDK
|
||||||
|
// - 如果是 agora,使用声网 RTC SDK
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
height: double.infinity,
|
||||||
|
color: Colors.black,
|
||||||
|
child: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
Icons.videocam_off,
|
||||||
|
size: 64,
|
||||||
|
color: Colors.grey,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'视频流已获取\nURL Type: ${_videoStream!.urlType}\nCamera: $_currentLensType',
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'请集成 RTC SDK 后在此处显示视频画面',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.grey[600],
|
||||||
|
fontSize: 12,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,9 @@ import '../pages/drone_mission_control_page.dart';
|
|||||||
|
|
||||||
/// 无人机机场与设备状态组件
|
/// 无人机机场与设备状态组件
|
||||||
class DroneStationStatusWidget extends StatelessWidget {
|
class DroneStationStatusWidget extends StatelessWidget {
|
||||||
const DroneStationStatusWidget({super.key});
|
final String? droneSn; // 无人机序列号
|
||||||
|
|
||||||
|
const DroneStationStatusWidget({super.key, this.droneSn});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -220,7 +222,8 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const DroneMissionControlPage(),
|
builder: (context) =>
|
||||||
|
DroneMissionControlPage(droneSn: droneSn),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -252,19 +255,12 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
color: color.withOpacity(0.1),
|
color: color.withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: BorderRadius.circular(12),
|
||||||
),
|
),
|
||||||
child: Icon(
|
child: Icon(icon, color: color, size: 24),
|
||||||
icon,
|
|
||||||
color: color,
|
|
||||||
size: 24,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 12, color: Color(0xFF4E5969)),
|
||||||
fontSize: 12,
|
|
||||||
color: Color(0xFF4E5969),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -277,10 +273,7 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
@@ -314,15 +307,16 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// 带进度条的状态行
|
/// 带进度条的状态行
|
||||||
Widget _buildStatusRowWithProgress(String label, String value, String progress) {
|
Widget _buildStatusRowWithProgress(
|
||||||
|
String label,
|
||||||
|
String value,
|
||||||
|
String progress,
|
||||||
|
) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
label,
|
label,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Text(
|
Text(
|
||||||
@@ -343,11 +337,7 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
const Icon(
|
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFF86909C)),
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -358,10 +348,7 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'气象条件',
|
'气象条件',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
Row(
|
Row(
|
||||||
@@ -379,18 +366,12 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'25°C',
|
'25°C',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Color(0xFF1D2129)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF1D2129),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'东南风 2级',
|
'东南风 2级',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Color(0xFF1D2129)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF1D2129),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -404,10 +385,7 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
const Text(
|
const Text(
|
||||||
'电量',
|
'电量',
|
||||||
style: TextStyle(
|
style: TextStyle(fontSize: 13, color: Color(0xFF86909C)),
|
||||||
fontSize: 13,
|
|
||||||
color: Color(0xFF86909C),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -416,7 +394,9 @@ class DroneStationStatusWidget extends StatelessWidget {
|
|||||||
child: LinearProgressIndicator(
|
child: LinearProgressIndicator(
|
||||||
value: 0.78,
|
value: 0.78,
|
||||||
backgroundColor: const Color(0xFFF2F3F5),
|
backgroundColor: const Color(0xFFF2F3F5),
|
||||||
valueColor: const AlwaysStoppedAnimation<Color>(Color(0xFF00B42A)),
|
valueColor: const AlwaysStoppedAnimation<Color>(
|
||||||
|
Color(0xFF00B42A),
|
||||||
|
),
|
||||||
minHeight: 6,
|
minHeight: 6,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import '../../../../../core/consts/http_api_consts.dart';
|
import '../../../../../core/consts/http_api_consts.dart';
|
||||||
import '../../domain/entities/flight_task_entity.dart';
|
import '../../domain/entities/flight_task_entity.dart';
|
||||||
|
import '../pages/create_task_page.dart';
|
||||||
|
|
||||||
class FlightTaskSelectorModal extends StatefulWidget {
|
class FlightTaskSelectorModal extends StatefulWidget {
|
||||||
final String currentGatewaySn;
|
final String currentGatewaySn;
|
||||||
@@ -40,16 +41,49 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _selectDateRange(BuildContext context) async {
|
Future<void> _selectDateRange(BuildContext context) async {
|
||||||
final picked = await showDateRangePicker(
|
// 先选择日期范围
|
||||||
|
final pickedDate = await showDateRangePicker(
|
||||||
context: context,
|
context: context,
|
||||||
firstDate: DateTime(2020),
|
firstDate: DateTime(2020),
|
||||||
lastDate: DateTime(2030),
|
lastDate: DateTime(2030),
|
||||||
initialDateRange: _selectedDateRange,
|
initialDateRange: _selectedDateRange,
|
||||||
);
|
);
|
||||||
if (picked != null) {
|
|
||||||
setState(() {
|
if (pickedDate != null) {
|
||||||
_selectedDateRange = picked;
|
// 选择开始时间(时分秒)
|
||||||
});
|
final startTime = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: TimeOfDay.fromDateTime(pickedDate.start),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (startTime != null) {
|
||||||
|
// 选择结束时间(时分秒)
|
||||||
|
final endTime = await showTimePicker(
|
||||||
|
context: context,
|
||||||
|
initialTime: const TimeOfDay(hour: 23, minute: 59),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (endTime != null) {
|
||||||
|
setState(() {
|
||||||
|
_selectedDateRange = DateTimeRange(
|
||||||
|
start: DateTime(
|
||||||
|
pickedDate.start.year,
|
||||||
|
pickedDate.start.month,
|
||||||
|
pickedDate.start.day,
|
||||||
|
startTime.hour,
|
||||||
|
startTime.minute,
|
||||||
|
),
|
||||||
|
end: DateTime(
|
||||||
|
pickedDate.end.year,
|
||||||
|
pickedDate.end.month,
|
||||||
|
pickedDate.end.day,
|
||||||
|
endTime.hour,
|
||||||
|
endTime.minute,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +129,7 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
data.forEach((sn, value) {
|
data.forEach((sn, value) {
|
||||||
if (value != null && value['list'] != null) {
|
if (value != null && value['list'] != null) {
|
||||||
final list = value['list'] as List<dynamic>;
|
final list = value['list'] as List<dynamic>;
|
||||||
|
print(' [FlightTask] API返回的原始数据: $list');
|
||||||
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
|
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
|
||||||
} else {
|
} else {
|
||||||
result[sn] = null;
|
result[sn] = null;
|
||||||
@@ -129,15 +164,30 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
_buildDeviceSelector(),
|
_buildDeviceSelector(),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
ElevatedButton(
|
Row(
|
||||||
onPressed: _isLoading ? null : _loadTasks,
|
children: [
|
||||||
style: ElevatedButton.styleFrom(
|
Expanded(
|
||||||
backgroundColor: const Color(0xFF165DFF),
|
child: ElevatedButton(
|
||||||
minimumSize: const Size(double.infinity, 48),
|
onPressed: _isLoading ? null : _loadTasks,
|
||||||
),
|
style: ElevatedButton.styleFrom(
|
||||||
child: _isLoading
|
backgroundColor: const Color(0xFF165DFF),
|
||||||
? const CircularProgressIndicator(color: Colors.white)
|
minimumSize: const Size(double.infinity, 48),
|
||||||
: const Text('查询任务'),
|
),
|
||||||
|
child: _isLoading
|
||||||
|
? const CircularProgressIndicator(color: Colors.white)
|
||||||
|
: const Text('查询任务'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: _showCreateTaskDialog,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: const Color(0xFF00B42A),
|
||||||
|
minimumSize: const Size(48, 48),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.add, color: Colors.white),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
if (_errorMessage != null) ...[
|
if (_errorMessage != null) ...[
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -173,7 +223,7 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
_selectedDateRange != null
|
_selectedDateRange != null
|
||||||
? '${_formatDate(_selectedDateRange!.start)} - ${_formatDate(_selectedDateRange!.end)}'
|
? '${_formatDateTimeFull(_selectedDateRange!.start)} -\n${_formatDateTimeFull(_selectedDateRange!.end)}'
|
||||||
: '选择时间范围',
|
: '选择时间范围',
|
||||||
style: const TextStyle(fontSize: 14),
|
style: const TextStyle(fontSize: 14),
|
||||||
),
|
),
|
||||||
@@ -307,6 +357,11 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatDateTimeFull(DateTime dateTime) {
|
||||||
|
return '${dateTime.year}-${dateTime.month.toString().padLeft(2, '0')}-${dateTime.day.toString().padLeft(2, '0')} '
|
||||||
|
'${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
String _formatDateTime(String dateTimeStr) {
|
String _formatDateTime(String dateTimeStr) {
|
||||||
try {
|
try {
|
||||||
final dateTime = DateTime.parse(dateTimeStr);
|
final dateTime = DateTime.parse(dateTimeStr);
|
||||||
@@ -315,4 +370,15 @@ class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
|
|||||||
return dateTimeStr;
|
return dateTimeStr;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
// 显示创建任务对话框
|
||||||
|
void _showCreateTaskDialog() {
|
||||||
|
Navigator.pop(context); // 先关闭当前模态页
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(
|
||||||
|
builder: (context) => CreateTaskPage(sn: widget.currentGatewaySn),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user