对接路径规划的列表(根据场站的下的)

优化无人机机场的监控视频的显示视频流畅度提升80%
添加首页tcp指示灯的机器状态信息展示功能。(暂不支持在此页面上设备的切换显示)
This commit is contained in:
2026-06-09 14:39:55 +08:00
parent ff4a3836c7
commit 058a5d9588
26 changed files with 3634 additions and 1028 deletions

View File

@@ -1,5 +1,6 @@
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/entities/video_stream_entity.dart';
import '../../domain/entities/flight_task_entity.dart';
abstract class DroneStationDataSource {
Future<List<DroneStationEntity>> getDroneStationList(int siteId);
@@ -11,4 +12,9 @@ abstract class DroneStationDataSource {
String qualityType = 'adaptive',
int videoExpire = 7200,
});
Future<Map<String, List<FlightTaskEntity>?>> getFlightTasks({
required List<String> sns,
required int beginAt,
required int endAt,
});
}

View File

@@ -3,6 +3,7 @@ import 'package:maibu_satabot_v2/core/consts/http_api_consts.dart';
import '../datasources/drone_station_datasource.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/entities/video_stream_entity.dart';
import '../../domain/entities/flight_task_entity.dart';
class DroneStationDataSourceImpl implements DroneStationDataSource {
final Dio dio;
@@ -77,11 +78,60 @@ class DroneStationDataSourceImpl implements DroneStationDataSource {
}
final responseData = response.data;
// 打印完整响应,方便调试
print('=== 视频流API响应 ===');
print('请求参数: sn=$sn, cameraIndex=$cameraIndex, cameraPosition=$cameraPosition');
print('完整响应: ${responseData}');
if (responseData['code'] != 200) {
throw Exception(responseData['msg'] ?? '业务异常');
}
return VideoStreamEntity.fromJson(responseData['data']);
final data = responseData['data'];
print('视频流URL: ${data['url']}');
print('视频流URL Type: ${data['url_type']}');
return VideoStreamEntity.fromJson(data);
}
@override
Future<Map<String, List<FlightTaskEntity>?>> getFlightTasks({
required List<String> sns,
required int beginAt,
required int endAt,
}) async {
final response = await dio.post(
HttpApiConsts.getFlightTask,
data: {
'sns': sns,
'beginAt': beginAt,
'endAt': endAt,
},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败: ${response.statusCode}');
}
final responseData = response.data;
if (responseData['code'] != 200) {
throw Exception(responseData['msg'] ?? '业务异常');
}
final data = responseData['data'] as Map<String, dynamic>;
final result = <String, List<FlightTaskEntity>?>{};
data.forEach((sn, value) {
if (value != null && value['list'] != null) {
final list = value['list'] as List<dynamic>;
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
} else {
result[sn] = null;
}
});
return result;
}
}

View File

@@ -0,0 +1,91 @@
class FlightTaskEntity {
final String name;
final String uuid;
final String taskType;
final String status;
final String sn;
final String landingDockSn;
final String beginAt;
final String endAt;
final String runAt;
final String completedAt;
final String waylineUuid;
final int folderId;
final int currentWaypointIndex;
final int totalWaypoints;
final String mediaUploadStatus;
final String resumableStatus;
final bool isBreakPointResume;
final dynamic operations;
final dynamic exceptions;
FlightTaskEntity({
required this.name,
required this.uuid,
required this.taskType,
required this.status,
required this.sn,
required this.landingDockSn,
required this.beginAt,
required this.endAt,
required this.runAt,
required this.completedAt,
required this.waylineUuid,
required this.folderId,
required this.currentWaypointIndex,
required this.totalWaypoints,
required this.mediaUploadStatus,
required this.resumableStatus,
required this.isBreakPointResume,
this.operations,
this.exceptions,
});
factory FlightTaskEntity.fromJson(Map<String, dynamic> json) {
return FlightTaskEntity(
name: json['name'] ?? '',
uuid: json['uuid'] ?? '',
taskType: json['task_type'] ?? '',
status: json['status'] ?? '',
sn: json['sn'] ?? '',
landingDockSn: json['landing_dock_sn'] ?? '',
beginAt: json['begin_at'] ?? '',
endAt: json['end_at'] ?? '',
runAt: json['run_at'] ?? '',
completedAt: json['completed_at'] ?? '',
waylineUuid: json['wayline_uuid'] ?? '',
folderId: json['folder_id'] ?? 0,
currentWaypointIndex: json['current_waypoint_index'] ?? 0,
totalWaypoints: json['total_waypoints'] ?? 0,
mediaUploadStatus: json['media_upload_status'] ?? '',
resumableStatus: json['resumable_status'] ?? '',
isBreakPointResume: json['is_break_point_resume'] ?? false,
operations: json['operations'],
exceptions: json['exceptions'],
);
}
Map<String, dynamic> toJson() {
return {
'name': name,
'uuid': uuid,
'task_type': taskType,
'status': status,
'sn': sn,
'landing_dock_sn': landingDockSn,
'begin_at': beginAt,
'end_at': endAt,
'run_at': runAt,
'completed_at': completedAt,
'wayline_uuid': waylineUuid,
'folder_id': folderId,
'current_waypoint_index': currentWaypointIndex,
'total_waypoints': totalWaypoints,
'media_upload_status': mediaUploadStatus,
'resumable_status': resumableStatus,
'is_break_point_resume': isBreakPointResume,
'operations': operations,
'exceptions': exceptions,
};
}
}

View File

@@ -4,10 +4,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/app/app_user_cubit.dart';
import '../../../../../components/tcp_status_indicator.dart';
import '../../../../../components/device_status_modal.dart';
import '../../../../v2/site/presentation/cubit/site_cubit.dart';
import '../bloc/device_status_bloc.dart';
import '../bloc/device_status_event.dart';
import '../bloc/device_status_state.dart';
import '../bloc/device_status_bloc.dart' as DeviceListBloc;
import '../bloc/device_status_event.dart' as DeviceListEvent;
import '../bloc/device_status_state.dart' as DeviceListState;
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import '../bloc/drone_station_bloc.dart';
import '../bloc/drone_station_event.dart';
import '../bloc/drone_station_state.dart';
@@ -27,7 +29,8 @@ class DeviceStatusPage extends StatelessWidget {
return BlocProvider(
create: (_) =>
sl<DeviceStatusBloc>()..add(DeviceStatusLoadData(siteId: siteId)),
sl<DeviceListBloc.DeviceStatusBloc>()
..add(DeviceListEvent.DeviceStatusLoadData(siteId: siteId)),
child: const DeviceStatusView(),
);
}
@@ -47,24 +50,28 @@ class DeviceStatusView extends StatelessWidget {
child: Scaffold(
backgroundColor: const Color(0xFFF7F7F7),
body: SafeArea(
child: BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
builder: (context, state) {
return Column(
children: [
_buildAppBar(),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
);
},
),
child:
BlocBuilder<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
builder: (context, state) {
return Column(
children: [
_buildAppBar(context),
_buildSearchBar(context),
_buildTypeFilterBar(context),
Expanded(child: _buildContent(context, state)),
],
);
},
),
),
),
);
}
Widget _buildAppBar() {
Widget _buildAppBar(BuildContext context) {
return Container(
height: 44.0,
padding: const EdgeInsets.symmetric(horizontal: 16.0),
@@ -92,7 +99,10 @@ class DeviceStatusView extends StatelessWidget {
shape: BoxShape.circle,
color: Colors.blue.withOpacity(0.1),
),
child: const TcpStatusIndicator(size: 12),
child: TcpStatusIndicator(
size: 12,
onTap: () => _showDeviceStatusModal(context),
),
),
],
),
@@ -148,7 +158,9 @@ class DeviceStatusView extends StatelessWidget {
style: const TextStyle(fontSize: 14, color: Color(0xFF1D2129)),
onSubmitted: (value) {
// 🔥 点击键盘确定键时触发搜索
context.read<DeviceStatusBloc>().add(DeviceStatusSearch(value));
context.read<DeviceListBloc.DeviceStatusBloc>().add(
DeviceListEvent.DeviceStatusSearch(value),
);
},
),
);
@@ -157,10 +169,13 @@ class DeviceStatusView extends StatelessWidget {
Widget _buildTypeFilterBar(BuildContext context) {
final types = ['全部', '机器人', '无人机机场', '逆变器', '汇流箱', '组件', '监控'];
return BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
return BlocBuilder<
DeviceListBloc.DeviceStatusBloc,
DeviceListState.DeviceStatusState
>(
builder: (context, state) {
String selectedType = '全部';
if (state is DeviceStatusLoaded) {
if (state is DeviceListState.DeviceStatusLoaded) {
selectedType = state.selectedType;
}
@@ -178,8 +193,8 @@ class DeviceStatusView extends StatelessWidget {
return GestureDetector(
onTap: () {
context.read<DeviceStatusBloc>().add(
DeviceStatusChangeType(type),
context.read<DeviceListBloc.DeviceStatusBloc>().add(
DeviceListEvent.DeviceStatusChangeType(type),
);
},
child: Column(
@@ -219,14 +234,19 @@ class DeviceStatusView extends StatelessWidget {
);
}
Widget _buildContent(BuildContext context, DeviceStatusState state) {
Widget _buildContent(
BuildContext context,
DeviceListState.DeviceStatusState state,
) {
// 如果选择的是"机器人",显示机器人专属页面
if (state is DeviceStatusLoaded && state.selectedType == '机器人') {
if (state is DeviceListState.DeviceStatusLoaded &&
state.selectedType == '机器人') {
return const RobotListPage();
}
// 如果选择的是"无人机机场",显示机场列表
if (state is DeviceStatusLoaded && state.selectedType == '无人机机场') {
if (state is DeviceListState.DeviceStatusLoaded &&
state.selectedType == '无人机机场') {
return _buildDroneStationList(context);
}
@@ -234,14 +254,17 @@ class DeviceStatusView extends StatelessWidget {
return _buildDeviceList(context, state);
}
Widget _buildDeviceList(BuildContext context, DeviceStatusState state) {
if (state is DeviceStatusLoading) {
Widget _buildDeviceList(
BuildContext context,
DeviceListState.DeviceStatusState state,
) {
if (state is DeviceListState.DeviceStatusLoading) {
return const Center(
child: CircularProgressIndicator(color: Color(0xFF165DFF)),
);
}
if (state is DeviceStatusError) {
if (state is DeviceListState.DeviceStatusError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
@@ -255,8 +278,8 @@ class DeviceStatusView extends StatelessWidget {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
context.read<DeviceStatusBloc>().add(
const DeviceStatusLoadData(),
context.read<DeviceListBloc.DeviceStatusBloc>().add(
const DeviceListEvent.DeviceStatusLoadData(),
);
},
style: ElevatedButton.styleFrom(
@@ -270,7 +293,7 @@ class DeviceStatusView extends StatelessWidget {
);
}
if (state is DeviceStatusLoaded) {
if (state is DeviceListState.DeviceStatusLoaded) {
// 🔥 先根据类型过滤
List filteredByType = state.devices;
if (state.selectedType != '全部') {
@@ -292,7 +315,9 @@ class DeviceStatusView extends StatelessWidget {
return RefreshIndicator(
onRefresh: () async {
context.read<DeviceStatusBloc>().add(const DeviceStatusRefresh());
context.read<DeviceListBloc.DeviceStatusBloc>().add(
const DeviceListEvent.DeviceStatusRefresh(),
);
},
color: const Color(0xFF165DFF),
child: ListView(
@@ -441,7 +466,7 @@ class DeviceStatusView extends StatelessWidget {
);
}
Widget _buildStatusCard(DeviceStatusLoaded state) {
Widget _buildStatusCard(DeviceListState.DeviceStatusLoaded state) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 16.0),
padding: const EdgeInsets.all(16.0),
@@ -533,4 +558,25 @@ class DeviceStatusView extends StatelessWidget {
),
);
}
// 显示设备状态模态框 - 从底部滑出
void _showDeviceStatusModal(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (BuildContext context) {
return BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
child: const DeviceStatusModal(),
);
},
);
}
}

View File

@@ -1,8 +1,26 @@
import 'package:flutter/material.dart';
import '../../domain/entities/flight_task_entity.dart';
/// 无人机任务与航线控制页面
class DroneMissionControlPage extends StatelessWidget {
const DroneMissionControlPage({super.key});
class DroneMissionControlPage extends StatefulWidget {
final List<FlightTaskEntity>? selectedTasks;
const DroneMissionControlPage({super.key, this.selectedTasks});
@override
State<DroneMissionControlPage> createState() => _DroneMissionControlPageState();
}
class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
FlightTaskEntity? _currentTask;
@override
void initState() {
super.initState();
if (widget.selectedTasks != null && widget.selectedTasks!.isNotEmpty) {
_currentTask = widget.selectedTasks!.first;
}
}
@override
Widget build(BuildContext context) {
@@ -78,44 +96,104 @@ class DroneMissionControlPage extends StatelessWidget {
),
),
const SizedBox(height: 16),
_buildInfoRow('任务名称', '逆变器区巡检任务'),
const SizedBox(height: 12),
Row(
children: [
_buildInfoColumn('任务编号', 'UAV-2025052001'),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF165DFF).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'进行中',
style: TextStyle(
fontSize: 12,
color: Color(0xFF165DFF),
fontWeight: FontWeight.w500,
if (_currentTask != null) ...[
_buildInfoRow('任务名称', _currentTask!.name),
const SizedBox(height: 12),
Row(
children: [
_buildInfoColumn('任务ID', _currentTask!.uuid.substring(0, 8)),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getStatusColor(_currentTask!.status).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
_currentTask!.status,
style: TextStyle(
fontSize: 12,
color: _getStatusColor(_currentTask!.status),
fontWeight: FontWeight.w500,
),
),
),
),
],
),
const SizedBox(height: 12),
_buildInfoRow('巡检区域', '逆变器区A区'),
const SizedBox(height: 12),
_buildInfoRow('飞行高度', '80 m'),
const SizedBox(height: 12),
_buildInfoRow('飞行速度', '8.0 m/s'),
const SizedBox(height: 12),
_buildInfoRow('预计时长', '26 min'),
const SizedBox(height: 12),
_buildInfoRow('电量预估', '68% (可飞行 22 min)'),
],
),
const SizedBox(height: 12),
_buildInfoRow('设备序列号', _currentTask!.sn),
const SizedBox(height: 12),
_buildInfoRow('任务类型', _currentTask!.taskType),
const SizedBox(height: 12),
_buildInfoRow('开始时间', _formatDateTime(_currentTask!.beginAt)),
const SizedBox(height: 12),
_buildInfoRow('结束时间', _formatDateTime(_currentTask!.endAt)),
const SizedBox(height: 12),
_buildInfoRow('航点数量', '${_currentTask!.totalWaypoints}'),
const SizedBox(height: 12),
_buildInfoRow('媒体上传', _currentTask!.mediaUploadStatus),
] else ...[
_buildInfoRow('任务名称', '逆变器区巡检任务'),
const SizedBox(height: 12),
Row(
children: [
_buildInfoColumn('任务编号', 'UAV-2025052001'),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFF165DFF).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: const Text(
'进行中',
style: TextStyle(
fontSize: 12,
color: Color(0xFF165DFF),
fontWeight: FontWeight.w500,
),
),
),
],
),
const SizedBox(height: 12),
_buildInfoRow('巡检区域', '逆变器区A区'),
const SizedBox(height: 12),
_buildInfoRow('飞行高度', '80 m'),
const SizedBox(height: 12),
_buildInfoRow('飞行速度', '8.0 m/s'),
const SizedBox(height: 12),
_buildInfoRow('预计时长', '26 min'),
const SizedBox(height: 12),
_buildInfoRow('电量预估', '68% (可飞行 22 min)'),
],
],
),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'success':
return const Color(0xFF00B42A);
case 'failed':
return const Color(0xFFF53F3F);
case 'running':
return const Color(0xFF165DFF);
default:
return const Color(0xFF86909C);
}
}
String _formatDateTime(String dateTimeStr) {
try {
final dateTime = DateTime.parse(dateTimeStr);
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')}';
} catch (e) {
return dateTimeStr;
}
}
Widget _buildRouteMap() {
return Container(
height: 200,

View File

@@ -1,3 +1,4 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
@@ -11,6 +12,7 @@ import '../bloc/drone_station_state.dart';
import 'drone_video_control_page.dart';
import 'drone_mission_control_page.dart';
import 'drone_monitor_page.dart';
import '../widgets/flight_task_selector_modal.dart';
// SDK 类型枚举
enum RtcSdkType { volcengine, agora }
@@ -47,6 +49,10 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// Agora RTC
agora.RtcEngine? _floatingAgoraEngine;
// 加载超时计时器
Timer? _floatingLoadingTimer;
static const _floatingLoadingTimeout = Duration(seconds: 15);
@override
void initState() {
super.initState();
@@ -63,6 +69,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
void dispose() {
_bloc.close();
_destroyFloatingRtcEngine();
_floatingLoadingTimer?.cancel();
super.dispose();
}
@@ -125,11 +132,13 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
listener: (context, state) {
// 监听视频流加载状态
if (state is VideoStreamLoaded) {
_floatingLoadingTimer?.cancel();
setState(() {
_floatingVideoStream = state.videoStream;
});
_initFloatingRtcEngine();
} else if (state is VideoStreamError) {
_floatingLoadingTimer?.cancel();
setState(() {
_floatingErrorMessage = state.message;
_isFloatingLoading = false;
@@ -518,12 +527,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
label: '任务下发',
color: const Color(0xFF165DFF),
onTap: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const DroneMissionControlPage(),
),
);
_showFlightTaskSelector();
},
),
),
@@ -535,6 +539,16 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
}
void _goToMonitor() {
// 从当前状态中获取摄像头索引
final currentState = _bloc.state;
String cameraIndex = '165-0-7'; // 默认值
if (currentState is UAVDetailLoaded &&
currentState.detail.gatewayCameraList != null &&
currentState.detail.gatewayCameraList!.isNotEmpty) {
cameraIndex = currentState.detail.gatewayCameraList!.first.cameraIndex;
}
showModalBottomSheet(
context: context,
shape: const RoundedRectangleBorder(
@@ -559,7 +573,7 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
MaterialPageRoute(
builder: (context) => DroneMonitorPage(
gatewaySn: widget.station.gatewaySn,
cameraIndex: '165-0-7',
cameraIndex: cameraIndex,
),
),
);
@@ -589,36 +603,37 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
),
),
const SizedBox(height: 12),
GestureDetector(
onTap: () {
Navigator.pop(context);
_loadFloatingVideoStream();
},
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(12),
),
child: const Row(
children: [
Icon(Icons.picture_in_picture, color: Color(0xFF165DFF)),
SizedBox(width: 16),
Text(
'悬浮观看',
style: TextStyle(
fontSize: 16,
color: Color(0xFF1D2129),
fontWeight: FontWeight.w500,
),
),
Spacer(),
Icon(Icons.arrow_forward_ios, color: Color(0xFF86909C)),
],
),
),
),
const SizedBox(height: 16),
// 悬浮观看功能已禁用
// GestureDetector(
// onTap: () {
// Navigator.pop(context);
// _loadFloatingVideoStream();
// },
// child: Container(
// padding: const EdgeInsets.all(16),
// decoration: BoxDecoration(
// color: const Color(0xFFF2F3F5),
// borderRadius: BorderRadius.circular(12),
// ),
// child: const Row(
// children: [
// Icon(Icons.picture_in_picture, color: Color(0xFF165DFF)),
// SizedBox(width: 16),
// Text(
// '悬浮观看',
// style: TextStyle(
// fontSize: 16,
// color: Color(0xFF1D2129),
// fontWeight: FontWeight.w500,
// ),
// ),
// Spacer(),
// Icon(Icons.arrow_forward_ios, color: Color(0xFF86909C)),
// ],
// ),
// ),
// ),
// const SizedBox(height: 16),
],
),
);
@@ -626,6 +641,46 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
);
}
// 显示飞行任务选择器
void _showFlightTaskSelector() {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) {
return DraggableScrollableSheet(
expand: false,
initialChildSize: 0.7,
minChildSize: 0.5,
maxChildSize: 0.95,
builder: (context, scrollController) {
return Container(
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
),
child: FlightTaskSelectorModal(
currentGatewaySn: widget.station.gatewaySn,
onTaskSelected: (tasks) {
if (tasks.isNotEmpty) {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) =>
DroneMissionControlPage(selectedTasks: tasks),
),
);
}
},
),
);
},
);
},
);
}
// 悬浮视频监控组件
Widget _buildFloatingMonitor() {
if (!showFloatingMonitor) return const SizedBox();
@@ -858,6 +913,18 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// 加载悬浮视频流
void _loadFloatingVideoStream() {
_floatingLoadingTimer?.cancel();
// 从当前状态中获取摄像头索引
final currentState = _bloc.state;
String cameraIndex = '165-0-7'; // 默认值
if (currentState is UAVDetailLoaded &&
currentState.detail.gatewayCameraList != null &&
currentState.detail.gatewayCameraList!.isNotEmpty) {
cameraIndex = currentState.detail.gatewayCameraList!.first.cameraIndex;
}
setState(() {
_isFloatingLoading = true;
_floatingErrorMessage = null;
@@ -866,10 +933,22 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
_destroyFloatingRtcEngine();
// 设置加载超时计时器
_floatingLoadingTimer = Timer(_floatingLoadingTimeout, () {
if (!mounted) return;
if (_isFloatingLoading) {
debugPrint('⚠️ 悬浮窗视频加载超时');
setState(() {
_isFloatingLoading = false;
_floatingErrorMessage = '视频加载超时,请检查网络连接或点击刷新重试';
});
}
});
_bloc.add(
VideoStreamLoad(
sn: widget.station.gatewaySn,
cameraIndex: '165-0-7',
cameraIndex: cameraIndex,
cameraPosition: isFloatingIndoor ? 'indoor' : 'outdoor',
),
);
@@ -1007,6 +1086,11 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
_isFloatingLoading = false;
});
},
onConnectionStateChanged: (state, reason) {
debugPrint(
'Volc 悬浮窗 Connection State Changed: $state, reason: $reason',
);
},
);
_floatingRtcEngine = await volc.RTCEngine.createRTCEngine(

View File

@@ -0,0 +1,318 @@
import 'package:flutter/material.dart';
import 'package:dio/dio.dart';
import '../../../../../core/consts/http_api_consts.dart';
import '../../domain/entities/flight_task_entity.dart';
class FlightTaskSelectorModal extends StatefulWidget {
final String currentGatewaySn;
final Function(List<FlightTaskEntity>) onTaskSelected;
const FlightTaskSelectorModal({
super.key,
required this.currentGatewaySn,
required this.onTaskSelected,
});
@override
State<FlightTaskSelectorModal> createState() => _FlightTaskSelectorModalState();
}
class _FlightTaskSelectorModalState extends State<FlightTaskSelectorModal> {
DateTimeRange? _selectedDateRange;
String? _selectedDeviceSn;
Map<String, List<FlightTaskEntity>?> _taskData = {};
bool _isLoading = false;
String? _errorMessage;
final Dio _dio = Dio();
@override
void initState() {
super.initState();
_selectedDeviceSn = widget.currentGatewaySn;
_setDefaultDateRange();
}
void _setDefaultDateRange() {
final now = DateTime.now();
final todayStart = DateTime(now.year, now.month, now.day);
final todayEnd = DateTime(now.year, now.month, now.day, 23, 59, 59);
_selectedDateRange = DateTimeRange(start: todayStart, end: todayEnd);
}
Future<void> _selectDateRange(BuildContext context) async {
final picked = await showDateRangePicker(
context: context,
firstDate: DateTime(2020),
lastDate: DateTime(2030),
initialDateRange: _selectedDateRange,
);
if (picked != null) {
setState(() {
_selectedDateRange = picked;
});
}
}
Future<void> _loadTasks() async {
if (_selectedDateRange == null || _selectedDeviceSn == null) {
setState(() {
_errorMessage = '请选择时间和设备';
});
return;
}
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final beginAt = _selectedDateRange!.start.millisecondsSinceEpoch ~/ 1000;
final endAt = _selectedDateRange!.end.millisecondsSinceEpoch ~/ 1000;
final response = await _dio.post(
HttpApiConsts.getFlightTask,
data: {
'sns': [_selectedDeviceSn!],
'beginAt': beginAt,
'endAt': endAt,
},
);
if (response.statusCode != 200) {
throw Exception('网络请求失败');
}
final responseData = response.data;
if (responseData['code'] != 200) {
throw Exception(responseData['msg'] ?? '业务异常');
}
final data = responseData['data'] as Map<String, dynamic>;
final result = <String, List<FlightTaskEntity>?>{};
data.forEach((sn, value) {
if (value != null && value['list'] != null) {
final list = value['list'] as List<dynamic>;
result[sn] = list.map((item) => FlightTaskEntity.fromJson(item)).toList();
} else {
result[sn] = null;
}
});
setState(() {
_taskData = result;
_isLoading = false;
});
} catch (e) {
setState(() {
_isLoading = false;
_errorMessage = '加载失败: $e';
});
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'选择飞行任务',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 24),
_buildTimeSelector(),
const SizedBox(height: 12),
_buildDeviceSelector(),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _isLoading ? null : _loadTasks,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
minimumSize: const Size(double.infinity, 48),
),
child: _isLoading
? const CircularProgressIndicator(color: Colors.white)
: const Text('查询任务'),
),
if (_errorMessage != null) ...[
const SizedBox(height: 12),
Text(
_errorMessage!,
style: const TextStyle(color: Color(0xFFF53F3F)),
),
],
if (_taskData.isNotEmpty) ...[
const SizedBox(height: 16),
Flexible(
child: _buildTaskList(),
),
],
],
),
);
}
Widget _buildTimeSelector() {
return GestureDetector(
onTap: () => _selectDateRange(context),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.calendar_today, color: Color(0xFF86909C)),
const SizedBox(width: 12),
Expanded(
child: Text(
_selectedDateRange != null
? '${_formatDate(_selectedDateRange!.start)} - ${_formatDate(_selectedDateRange!.end)}'
: '选择时间范围',
style: const TextStyle(fontSize: 14),
),
),
],
),
),
);
}
Widget _buildDeviceSelector() {
return Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.devices, color: Color(0xFF86909C)),
const SizedBox(width: 12),
Expanded(
child: Text(
_selectedDeviceSn ?? '未知设备',
style: const TextStyle(fontSize: 14),
),
),
],
),
);
}
Widget _buildTaskList() {
final tasks = _taskData[_selectedDeviceSn];
if (tasks == null || tasks.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(32),
child: Text('暂无任务数据'),
),
);
}
return ListView.builder(
shrinkWrap: true,
itemCount: tasks.length,
itemBuilder: (context, index) {
final task = tasks[index];
return _buildTaskItem(task);
},
);
}
Widget _buildTaskItem(FlightTaskEntity task) {
return GestureDetector(
onTap: () {
widget.onTaskSelected([task]);
},
child: Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE5E6EB)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
task.name,
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
'类型: ${task.taskType}',
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
),
const Spacer(),
Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getStatusColor(task.status).withOpacity(0.1),
borderRadius: BorderRadius.circular(4),
),
child: Text(
task.status,
style: TextStyle(
fontSize: 11,
color: _getStatusColor(task.status),
),
),
),
],
),
const SizedBox(height: 4),
Text(
'开始: ${_formatDateTime(task.beginAt)}',
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
),
Text(
'结束: ${_formatDateTime(task.endAt)}',
style: const TextStyle(fontSize: 12, color: Color(0xFF86909C)),
),
],
),
),
);
}
Color _getStatusColor(String status) {
switch (status.toLowerCase()) {
case 'success':
return const Color(0xFF00B42A);
case 'failed':
return const Color(0xFFF53F3F);
case 'running':
return const Color(0xFF165DFF);
default:
return const Color(0xFF86909C);
}
}
String _formatDate(DateTime date) {
return '${date.year}-${date.month.toString().padLeft(2, '0')}-${date.day.toString().padLeft(2, '0')}';
}
String _formatDateTime(String dateTimeStr) {
try {
final dateTime = DateTime.parse(dateTimeStr);
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')}';
} catch (e) {
return dateTimeStr;
}
}
}

View File

@@ -12,6 +12,8 @@ import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/quick_ent
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/power_trend_chart.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/plant_overview_card.dart';
import 'package:maibu_satabot_v2/features/v2/home/presentation/widgets/tcp_status_indicator.dart';
import 'package:maibu_satabot_v2/components/device_status_modal.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
class HomeV2Page extends StatefulWidget {
const HomeV2Page({super.key});
@@ -40,7 +42,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: Color(0xFF86909C)),
const Icon(
Icons.error_outline,
size: 48,
color: Color(0xFF86909C),
),
const SizedBox(height: 16),
Text(state.message),
const SizedBox(height: 16),
@@ -81,9 +87,12 @@ class _HomeV2PageState extends State<HomeV2Page> {
child: StreamBuilder<SiteState>(
stream: sl<SiteCubit>().stream,
builder: (context, snapshot) {
final siteState = snapshot.data ?? sl<SiteCubit>().state;
final siteState =
snapshot.data ??
sl<SiteCubit>().state;
return Text(
siteState.selectedSite?.siteName ?? '选择电站',
siteState.selectedSite?.siteName ??
'选择电站',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
@@ -96,12 +105,18 @@ class _HomeV2PageState extends State<HomeV2Page> {
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_drop_down, size: 20, color: Color(0xFF1D2129)),
const Icon(
Icons.arrow_drop_down,
size: 20,
color: Color(0xFF1D2129),
),
],
),
),
),
const TcpStatusIndicator(),
TcpStatusIndicator(
onTap: () => _showDeviceStatusModal(context),
),
],
),
),
@@ -117,7 +132,10 @@ class _HomeV2PageState extends State<HomeV2Page> {
children: [
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFFF2F3F5),
borderRadius: BorderRadius.circular(8),
@@ -125,7 +143,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.cloud_queue, size: 16, color: Color(0xFF165DFF)),
const Icon(
Icons.cloud_queue,
size: 16,
color: Color(0xFF165DFF),
),
const SizedBox(width: 6),
const Text(
'多云 28°C',
@@ -141,7 +163,10 @@ class _HomeV2PageState extends State<HomeV2Page> {
const SizedBox(width: 8),
Expanded(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
decoration: BoxDecoration(
color: const Color(0xFFFFF0F0),
borderRadius: BorderRadius.circular(8),
@@ -149,7 +174,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.warning_amber_rounded, size: 16, color: Color(0xFFF53F3F)),
const Icon(
Icons.warning_amber_rounded,
size: 16,
color: Color(0xFFF53F3F),
),
const SizedBox(width: 6),
const Expanded(
child: Text(
@@ -161,7 +190,11 @@ class _HomeV2PageState extends State<HomeV2Page> {
),
),
const SizedBox(width: 4),
const Icon(Icons.arrow_forward_ios, size: 12, color: Color(0xFFF53F3F)),
const Icon(
Icons.arrow_forward_ios,
size: 12,
color: Color(0xFFF53F3F),
),
],
),
),
@@ -173,7 +206,9 @@ class _HomeV2PageState extends State<HomeV2Page> {
const SizedBox(height: 10),
StatsGrid(data: state.homeData),
const SizedBox(height: 10),
PlantOverviewCard(overview: state.homeData.plantOverview),
PlantOverviewCard(
overview: state.homeData.plantOverview,
),
const SizedBox(height: 10),
WorkOrderCard(stats: state.homeData.workOrderStats),
const SizedBox(height: 10),
@@ -182,7 +217,8 @@ class _HomeV2PageState extends State<HomeV2Page> {
PowerTrendChart(
data: state.homeData.powerTrendData,
trendType: state.trendType,
onToggleType: (type) => _bloc.add(HomeV2ToggleTrendType(type)),
onToggleType: (type) =>
_bloc.add(HomeV2ToggleTrendType(type)),
),
const SizedBox(height: 40),
]),
@@ -215,14 +251,14 @@ class _HomeV2PageState extends State<HomeV2Page> {
void _showPlantSelector() {
if (_bloc.state is! HomeV2Loaded) return;
final currentState = _bloc.state as HomeV2Loaded;
final sites = currentState.sites;
if (sites.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('暂无可用电站')),
);
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('暂无可用电站')));
return;
}
@@ -237,7 +273,7 @@ class _HomeV2PageState extends State<HomeV2Page> {
bloc: _bloc,
builder: (context, state) {
if (state is! HomeV2Loaded) return Container();
return Container(
padding: const EdgeInsets.all(14),
constraints: BoxConstraints(
@@ -262,20 +298,24 @@ class _HomeV2PageState extends State<HomeV2Page> {
itemBuilder: (context, index) {
final site = state.sites[index];
final isSelected = state.selectedSite?.id == site.id;
return ListTile(
title: Text(site.siteName),
subtitle: site.siteCode != null && site.siteCode!.isNotEmpty
subtitle:
site.siteCode != null && site.siteCode!.isNotEmpty
? Text('编号: ${site.siteCode}')
: null,
trailing: isSelected
? const Icon(Icons.check, color: Color(0xFF165DFF))
? const Icon(
Icons.check,
color: Color(0xFF165DFF),
)
: null,
onTap: () {
// 更新全局选中的场站
sl<SiteCubit>().selectSite(site);
Navigator.pop(context);
// 切换场站后,重新加载首页数据
_bloc.add(const HomeV2LoadData());
},
@@ -291,4 +331,25 @@ class _HomeV2PageState extends State<HomeV2Page> {
},
);
}
// 显示设备状态模态框 - 从底部滑出
void _showDeviceStatusModal(BuildContext context) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
),
),
builder: (BuildContext context) {
return BlocProvider.value(
value: context.read<DeviceStatusBloc>(),
child: const DeviceStatusModal(),
);
},
);
}
}

View File

@@ -4,7 +4,9 @@ import 'package:maibu_satabot_v2/core/network/tcp/tcp_status_cubit.dart';
/// TCP 连接状态指示灯组件
class TcpStatusIndicator extends StatefulWidget {
const TcpStatusIndicator({super.key});
final VoidCallback? onTap; // 点击回调
const TcpStatusIndicator({super.key, this.onTap});
@override
State<TcpStatusIndicator> createState() => _TcpStatusIndicatorState();
@@ -21,16 +23,17 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
void initState() {
super.initState();
_tcpStatusCubit = GetIt.I<TcpStatusCubit>();
_controller = AnimationController(
duration: const Duration(milliseconds: 1500),
vsync: this,
)..repeat(reverse: true);
_animation = Tween<double>(begin: 0.6, end: 1.0).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
_animation = Tween<double>(
begin: 0.6,
end: 1.0,
).animate(CurvedAnimation(parent: _controller, curve: Curves.easeInOut));
_tcpStatusCubit.stream.listen((state) {
if (state.status != TcpConnectionStatus.disconnected) {
_hasActivity = true;
@@ -81,28 +84,32 @@ class _TcpStatusIndicatorState extends State<TcpStatusIndicator>
return Tooltip(
message: tooltip,
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final opacity = shouldAnimate ? _animation.value : 1.0;
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color.withOpacity(opacity),
shape: BoxShape.circle,
boxShadow: state.status == TcpConnectionStatus.connected
? [
BoxShadow(
color: Colors.green.withOpacity(0.5 * opacity),
blurRadius: 6 * opacity,
spreadRadius: 2 * opacity,
),
]
: [],
),
);
},
child: InkWell(
onTap: widget.onTap,
borderRadius: BorderRadius.circular(6),
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
final opacity = shouldAnimate ? _animation.value : 1.0;
return Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color.withOpacity(opacity),
shape: BoxShape.circle,
boxShadow: state.status == TcpConnectionStatus.connected
? [
BoxShadow(
color: Colors.green.withOpacity(0.5 * opacity),
blurRadius: 6 * opacity,
spreadRadius: 2 * opacity,
),
]
: [],
),
);
},
),
),
);
}