对接路径规划的列表(根据场站的下的)
优化无人机机场的监控视频的显示视频流畅度提升80% 添加首页tcp指示灯的机器状态信息展示功能。(暂不支持在此页面上设备的切换显示)
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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(
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
: [],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user