优化和路径规划新适配

This commit is contained in:
2026-07-11 16:46:53 +08:00
parent 2124f3bc02
commit 1a4d0aedfd
43 changed files with 2454 additions and 685 deletions

View File

@@ -204,22 +204,53 @@ class _CreateTaskPageState extends State<CreateTaskPage> {
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFE5E6EB)),
minimumSize: const Size(80, 36),
// 取消按钮
SizedBox(
width: 100,
height: 44,
child: OutlinedButton(
onPressed: () => Navigator.pop(context),
style: OutlinedButton.styleFrom(
side: const BorderSide(color: Color(0xFFE5E6EB)),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'取消',
style: TextStyle(
fontSize: 14,
color: Color(0xFF4E5969),
fontWeight: FontWeight.w500,
),
),
),
child: const Text('取消'),
),
const SizedBox(width: 12),
ElevatedButton(
onPressed: _onCreateTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
minimumSize: const Size(100, 36),
// 确认创建按钮
SizedBox(
width: 140,
height: 44,
child: ElevatedButton(
onPressed: _onCreateTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF165DFF),
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 0,
),
child: const Text(
'确认创建',
style: TextStyle(
fontSize: 14,
color: Colors.white,
fontWeight: FontWeight.w500,
),
),
),
child: const Text('确认创建'),
),
],
),

View File

@@ -9,6 +9,8 @@ import '../../domain/entities/flight_task_entity.dart';
import '../../domain/entities/flight_task_detail_entity.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../../domain/usecases/update_flight_task_status_usecase.dart';
import '../../domain/usecases/pause_flight_task_usecase.dart';
import '../../domain/usecases/return_home_usecase.dart';
import '../bloc/drone_station_bloc.dart';
import '../float_bar/view/float_bar_widget.dart';
@@ -28,6 +30,10 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
FlightTaskDetailEntity? _detailTask; // 详情数据
bool _isLoading = false;
final Dio _dio = Dio();
// 🔥 任务状态管理
bool _isPaused = false; // 是否已暂停(用于切换暂停/恢复按钮)
bool _isReturning = false; // 是否正在返航中
@override
void initState() {
@@ -201,6 +207,147 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
}
}
// 🔥 暂停任务
Future<void> _pauseTask() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备序列号不存在')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始暂停任务, deviceSn: ${_detailTask!.sn}');
final useCase = GetIt.I<PauseFlightTaskUseCase>();
final result = await useCase.execute(
deviceSn: _detailTask!.sn,
);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 暂停任务失败: ${failure.message}');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('暂停任务失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 任务暂停成功: $data');
setState(() {
_isPaused = true; // 更新状态为已暂停
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务已暂停')));
},
);
} catch (e) {
print('❌ [DroneMissionControl] 暂停任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('暂停任务失败: $e')));
}
}
// 🔥 恢复任务
Future<void> _resumeTask() async {
if (_detailTask == null || _detailTask!.uuid.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务ID不存在')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始恢复任务: ${_detailTask!.uuid}');
final useCase = GetIt.I<UpdateFlightTaskStatusUseCase>();
final result = await useCase.execute(
taskId: _detailTask!.uuid,
status: 'restored',
);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 恢复任务失败: ${failure.message}');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('恢复任务失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 任务恢复成功: $data');
setState(() {
_isPaused = false; // 更新状态为执行中
});
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('任务已恢复执行')));
},
);
} catch (e) {
print('❌ [DroneMissionControl] 恢复任务异常: $e');
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('恢复任务失败: $e')));
}
}
// 🔥 返航降落
Future<void> _returnHome() async {
if (_detailTask == null || _detailTask!.sn.isEmpty) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('设备序列号不存在')));
return;
}
// 防止重复点击
if (_isReturning) {
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已在返航中...')));
return;
}
try {
print('🔍 [DroneMissionControl] 开始返航, deviceSn: ${_detailTask!.sn}');
setState(() {
_isReturning = true; // 标记正在返航
});
final useCase = GetIt.I<ReturnHomeUseCase>();
final result = await useCase.execute(deviceSn: _detailTask!.sn);
result.fold(
(failure) {
print('❌ [DroneMissionControl] 返航失败: ${failure.message}');
setState(() {
_isReturning = false; // 重置状态
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('返航失败: ${failure.message}')));
},
(data) {
print('✅ [DroneMissionControl] 返航指令发送成功: $data');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已在返航')));
// 注意:不重置 _isReturning,因为返航是一个持续过程
},
);
} catch (e) {
print('❌ [DroneMissionControl] 返航异常: $e');
setState(() {
_isReturning = false; // 重置状态
});
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('返航失败: $e')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
@@ -588,9 +735,9 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {},
onPressed: _isPaused ? _resumeTask : _pauseTask,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFFF7D00),
backgroundColor: _isPaused ? const Color(0xFF165DFF) : const Color(0xFFFF7D00),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
@@ -598,27 +745,27 @@ class _DroneMissionControlPageState extends State<DroneMissionControlPage> {
),
elevation: 0,
),
child: const Text(
'暂停任务',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
child: Text(
_isPaused ? '恢复任务' : '暂停任务',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),
const SizedBox(width: 12),
Expanded(
child: OutlinedButton(
onPressed: () {},
onPressed: _isReturning ? null : _returnHome,
style: OutlinedButton.styleFrom(
foregroundColor: const Color(0xFF4E5969),
side: const BorderSide(color: Color(0xFFC9CDD4)),
foregroundColor: _isReturning ? const Color(0xFF86909C) : const Color(0xFF4E5969),
side: BorderSide(color: _isReturning ? const Color(0xFFE5E6EB) : const Color(0xFFC9CDD4)),
padding: const EdgeInsets.symmetric(vertical: 14),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: const Text(
'返航降落',
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
child: Text(
_isReturning ? '已在返航' : '返航降落',
style: const TextStyle(fontSize: 15, fontWeight: FontWeight.w500),
),
),
),

View File

@@ -32,6 +32,9 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
// 无人机状态轮询计时器
Timer? _droneStatusPollingTimer;
// 🔥 标记是否已经初始化过(用于判断是否从其他页面返回)
bool _hasInitialized = false;
@override
void initState() {
@@ -43,12 +46,45 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 🔥 标记已初始化
_hasInitialized = true;
// 启动无人机状态轮询(每5秒刷新一次)
// 🔥 已禁用自动轮询,改为手动下拉刷新
// _startDroneStatusPolling();
}
/// 🔥 页面重新激活时调用(从其他页面返回时)
@override
void didChangeDependencies() {
super.didChangeDependencies();
// 🔥 只有在已经初始化后才执行刷新(避免首次加载时重复刷新)
if (_hasInitialized && _bloc.state is UAVDetailLoaded) {
debugPrint('🔄 [DroneStationDetailPage] 从其他页面返回,刷新数据');
// 延迟一下再刷新,避免与 Bloc 状态更新冲突
Future.delayed(const Duration(milliseconds: 300), () {
if (mounted) {
_refreshData();
}
});
}
}
/// 🔥 刷新数据(无人机详情 + OSD数据会自动通过MQTT更新)
void _refreshData() {
if (!mounted) return;
debugPrint('📡 [DroneStationDetailPage] 刷新无人机详情数据');
_bloc.add(
UAVDetailLoad(
gatewaySn: widget.station.gatewaySn,
deviceSn: widget.station.deviceSn,
),
);
}
@override
void dispose() {
_bloc.close();
@@ -216,6 +252,20 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
/// 处理下拉刷新
Future<void> _handleRefresh() async {
debugPrint('🔄 [DroneStationDetailPage] 开始下拉刷新');
// 🔥 创建一个 Completer 来等待 Bloc 状态更新
final completer = Completer<void>();
// 监听 Bloc 状态变化
final subscription = _bloc.stream.listen((state) {
if (state is UAVDetailLoaded || state is UAVDetailError) {
if (!completer.isCompleted) {
completer.complete();
}
}
});
// 重新加载无人机详情
_bloc.add(
UAVDetailLoad(
@@ -223,9 +273,19 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
deviceSn: widget.station.deviceSn,
),
);
// 重置轮询计时器,使用新的状态
// 🔥 已禁用自动轮询,无需重置
// _scheduleDroneStatusPoll();
// 🔥 等待数据加载完成(最多等待5秒)
await completer.future.timeout(
const Duration(seconds: 5),
onTimeout: () {
debugPrint('⚠️ [DroneStationDetailPage] 下拉刷新超时');
},
);
// 取消订阅
subscription.cancel();
debugPrint('✅ [DroneStationDetailPage] 下拉刷新完成');
}
Widget _buildMonitorCard() {

View File

@@ -1,7 +1,13 @@
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:volc_engine_rtc/volc_engine_rtc.dart' as volc;
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../../../../core/di/injection.dart';
import '../../../../../core/network/mqtt/data/datasources/drone_osd_datasource.dart';
import '../../../../../core/network/mqtt/domain/entities/drone_osd_entity.dart';
import '../../domain/entities/uav_video_stream_entity.dart';
import '../../domain/entities/drone_station_entity.dart';
import '../bloc/drone_station_bloc.dart';
@@ -53,11 +59,23 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
final volc.IRTCRoomEventHandler _roomEventHandler =
volc.IRTCRoomEventHandler();
// 🔥 实时轨迹相关状态
MapController? _mapController;
List<LatLng> _trajectoryPoints = [];
LatLng? _currentPosition;
double? _currentHeading;
StreamSubscription<DroneOsdEntity>? _osdSubscription;
DroneOsdDataSource? _droneOsdDataSource;
@override
void initState() {
super.initState();
_bloc = sl<DroneStationBloc>();
// 🔥 初始化 MQTT OSD 数据源
_droneOsdDataSource = sl<DroneOsdDataSource>();
_startOsdListening();
// 初始化事件处理器
_initVolcEventHandlers();
@@ -110,6 +128,9 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
@override
void dispose() {
_osdSubscription?.cancel();
_droneOsdDataSource?.dispose();
_mapController?.dispose();
_destroyRtcEngine();
_bloc.close();
super.dispose();
@@ -219,6 +240,147 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
}
}
/// 🔥 开始监听 MQTT OSD 数据
void _startOsdListening() async {
if (_droneOsdDataSource == null) return;
debugPrint('🛸 [DroneVideoControlPage] 开始监听 OSD 数据');
debugPrint(' droneSn: ${widget.droneSn}');
debugPrint(' gatewaySn: ${widget.gatewaySn}');
await _droneOsdDataSource!.startListening(
deviceSn: widget.droneSn,
gatewaySn: widget.gatewaySn,
);
_osdSubscription = _droneOsdDataSource!.droneOsdStream.listen((osdData) {
if (!mounted) return;
debugPrint('📡 [DroneVideoControlPage] 收到 droneOsdStream 事件');
_handleOsdUpdate(osdData);
}, onError: (error) {
debugPrint('❌ [DroneVideoControlPage] OSD 监听错误: $error');
});
debugPrint('✅ [DroneVideoControlPage] OSD 监听已启动');
}
/// 🔥 处理 OSD 数据更新,绘制轨迹
void _handleOsdUpdate(DroneOsdEntity osdData) {
// 从 rawData 中提取位置信息
final rawData = osdData.rawData;
// 🔥 尝试从嵌套结构中获取经纬度
double? lat;
double? lng;
double? heading;
// 路径1: rawData['data']['host']['99-0-0']['measure_target_latitude'] (无人机)
if (rawData['data'] is Map &&
(rawData['data'] as Map)['host'] is Map) {
final host = (rawData['data'] as Map)['host'] as Map;
// 尝试从 99-0-0 载荷获取(无人机)
if (host.containsKey('99-0-0') && host['99-0-0'] is Map) {
final payload = host['99-0-0'] as Map;
lat = (payload['measure_target_latitude'] as num?)?.toDouble();
lng = (payload['measure_target_longitude'] as num?)?.toDouble();
debugPrint('✅ [DroneVideoControlPage] 从 99-0-0 获取位置: lat=$lat, lng=$lng');
}
// 如果 99-0-0 中没有,尝试从 host 直接获取(机场)
if (lat == null || lng == null) {
lat = (host['latitude'] as num?)?.toDouble();
lng = (host['longitude'] as num?)?.toDouble();
if (lat != null && lng != null) {
debugPrint('✅ [DroneVideoControlPage] 从 host 获取位置: lat=$lat, lng=$lng');
}
}
// 获取航向角
heading = (host['attitude_head'] as num?)?.toDouble() ??
(host['heading'] as num?)?.toDouble();
}
// 兼容旧格式:直接从 rawData 获取
if (lat == null || lng == null) {
lat = lat ?? (rawData['latitude'] as num?)?.toDouble() ??
(rawData['lat'] as num?)?.toDouble();
lng = lng ?? (rawData['longitude'] as num?)?.toDouble() ??
(rawData['lng'] as num?)?.toDouble() ??
(rawData['lon'] as num?)?.toDouble();
heading = heading ?? (rawData['heading'] as num?)?.toDouble() ??
(rawData['attitudeHeading'] as num?)?.toDouble();
}
debugPrint('🛰️ [DroneVideoControlPage] 收到 OSD 数据');
debugPrint(' lat=$lat, lng=$lng, heading=$heading');
debugPrint(' 当前轨迹点数: ${_trajectoryPoints.length}');
debugPrint(' 当前位置: $_currentPosition');
// 验证位置有效性
if (lat != null && lng != null && lat.abs() <= 90 && lng.abs() <= 180) {
final newPos = LatLng(lat, lng);
setState(() {
// 更新当前位置(驱动飞机 Marker)
_currentPosition = newPos;
_currentHeading = heading;
// 轨迹"拉烟"逻辑:距离过滤(防止 GPS 抖动导致轨迹像乱麻)
if (_trajectoryPoints.isEmpty) {
_trajectoryPoints.add(newPos);
debugPrint('✅ [DroneVideoControlPage] 添加第一个轨迹点: $newPos');
// 🔥 重要:第一个点添加后,等待 UI 构建完成再移动地图
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mapController != null && mounted) {
_mapController!.move(newPos, 18); // 缩放到 18 级
debugPrint('🗺️ [DroneVideoControlPage] 首次定位到: $newPos');
}
});
} else {
final distance = _calculateDistance(
_trajectoryPoints.last.latitude,
_trajectoryPoints.last.longitude,
lat!, // ✅ 空安全:已经通过 if 检查,使用 ! 断言
lng!, // ✅ 空安全:已经通过 if 检查,使用 ! 断言
);
debugPrint(' 📏 距离上一个点: ${distance.toStringAsFixed(2)} 米');
// 只有移动超过 0.5 米才画线,否则认为是原地漂移
if (distance > 0.5) {
_trajectoryPoints.add(newPos);
debugPrint('✅ [DroneVideoControlPage] 添加新轨迹点,当前总数: ${_trajectoryPoints.length}');
// 性能优化:只保留最近 1000 个点
if (_trajectoryPoints.length > 1000) {
_trajectoryPoints.removeAt(0);
}
} else {
debugPrint('⚠️ [DroneVideoControlPage] 距离不足0.5米(${distance.toStringAsFixed(2)}m),跳过此点');
}
}
});
// 地图跟随:后续点也移动地图(保持飞机在视野中)
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_mapController != null && mounted) {
_mapController!.move(newPos, _mapController!.camera.zoom);
debugPrint('🗺️ [DroneVideoControlPage] 地图已移动到: $newPos');
}
});
} else {
debugPrint('⚠️ [DroneVideoControlPage] 位置数据无效: lat=$lat, lng=$lng');
}
}
/// 🔥 计算两点之间的距离(米)
double _calculateDistance(double lat1, double lon1, double lat2, double lon2) {
const p = 0.017453292519943295; // Math.PI / 180
final a = 0.5 -
cos((lat2 - lat1) * p) / 2 +
cos(lat1 * p) * cos(lat2 * p) * (1 - cos((lon2 - lon1) * p)) / 2;
return 12742 * asin(sqrt(a)) * 1000; // 2 * R * asin(sqrt(a)) * 1000 (R = 6371km)
}
// 初始化火山引擎事件处理器
void _initVolcEventHandlers() {
_engineEventHandler.onWarning = (volc.WarningCode code) {
@@ -522,7 +684,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
}
} else if (state is UavVideoStreamError) {
setState(() {
_errorMessage = state.message;
_errorMessage = '暂无视频';
_isLoading = false;
});
}
@@ -537,11 +699,15 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
children: [
_buildVideoPlayer(),
const SizedBox(height: 12),
// 🔥 实时轨迹地图(放在视频和飞行数据之间)
_buildTrajectoryMap(),
const SizedBox(height: 12),
_buildFlightData(),
const SizedBox(height: 12),
_buildAIResults(),
const SizedBox(height: 12),
_buildMapAndJoystick(),
// 🔥 摇杆控制(单独一行)
_buildJoystickControl(),
const SizedBox(height: 16),
_buildBottomToolbar(),
],
@@ -901,29 +1067,351 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
);
}
/// 🔥 实时轨迹地图(放在视频和飞行数据之间)
Widget _buildTrajectoryMap() {
debugPrint('🗺️ [DroneVideoControlPage] 构建地图轨迹组件');
debugPrint(' currentPosition: $_currentPosition');
debugPrint(' currentHeading: $_currentHeading');
debugPrint(' trajectoryPoints.length: ${_trajectoryPoints.length}');
return Container(
height: 200,
decoration: BoxDecoration(
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Stack(
children: [
// 🔥 地图
FlutterMap(
mapController: _mapController ??= MapController(), // ✅ 懒加载初始化
options: MapOptions(
initialCenter: _currentPosition ?? const LatLng(39.9042, 116.4074),
initialZoom: 18,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
// 高德地图瓦片(最底层)
TileLayer(
urlTemplate: 'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
userAgentPackageName: 'com.example.app',
),
// 🔥 轨迹线(中间层)
if (_trajectoryPoints.length >= 1)
PolylineLayer(
polylines: [
Polyline(
points: _trajectoryPoints,
color: const Color(0xFF00B42A).withOpacity(0.9),
strokeWidth: 4,
borderColor: Colors.white,
borderStrokeWidth: 2,
),
],
),
// 🔥 无人机当前位置标记(最上层)
if (_currentPosition != null)
MarkerLayer(
markers: [
Marker(
key: const ValueKey('drone_marker'),
point: _currentPosition!,
width: 40,
height: 40,
child: Transform.rotate(
angle: (_currentHeading ?? 0) * pi / 180,
alignment: Alignment.center,
child: const Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 32,
),
),
),
],
),
],
),
// 🔥 GPS定位标签
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'GPS定位',
style: TextStyle(fontSize: 10, color: Colors.white),
),
],
),
),
),
// 🔥 轨迹点数
if (_trajectoryPoints.isNotEmpty)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'轨迹点: ${_trajectoryPoints.length}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
// 🔥 等待定位提示
if (_currentPosition == null)
const Positioned(
bottom: 8,
right: 8,
child: Text(
'等待定位...',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
),
],
),
),
);
}
/// 🔥 摇杆控制(单独一行)
Widget _buildJoystickControl() {
return Container(
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
),
child: Stack(
alignment: Alignment.center,
children: [
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
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: () {},
),
),
],
),
);
}
Widget _buildMapAndJoystick() {
return Row(
children: [
Expanded(
child: Container(
height: 160,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
color: const Color(0xFFF7F8FA),
borderRadius: BorderRadius.circular(12),
boxShadow: const [
BoxShadow(
color: Color(0x0D000000),
blurRadius: 8,
offset: Offset(0, 2),
),
],
border: Border.all(color: Colors.grey.withOpacity(0.2)),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(12),
child: Image.asset(
'assets/images/xunjian.png',
fit: BoxFit.cover,
width: double.infinity,
child: Stack(
children: [
// 🔥 地图
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter: _currentPosition ?? const LatLng(39.9042, 116.4074),
initialZoom: 18,
interactionOptions: const InteractionOptions(
flags: InteractiveFlag.all & ~InteractiveFlag.rotate,
),
),
children: [
// 高德地图瓦片(最底层)
TileLayer(
urlTemplate: 'https://webst0{s}.is.autonavi.com/appmaptile?style=6&x={x}&y={y}&z={z}',
subdomains: ['1', '2', '3', '4'],
userAgentPackageName: 'com.example.app',
),
// 🔥 轨迹线(中间层)
if (_trajectoryPoints.length >= 1)
PolylineLayer(
polylines: [
Polyline(
points: _trajectoryPoints,
color: const Color(0xFF00B42A).withOpacity(0.9),
strokeWidth: 4,
borderColor: Colors.white,
borderStrokeWidth: 2,
),
],
),
// 🔥 无人机当前位置标记(最上层)
if (_currentPosition != null)
MarkerLayer(
markers: [
Marker(
key: const ValueKey('drone_marker'),
point: _currentPosition!,
width: 40,
height: 40,
child: Transform.rotate(
angle: (_currentHeading ?? 0) * pi / 180,
alignment: Alignment.center,
child: const Icon(
Icons.flight,
color: Color(0xFF165DFF),
size: 32,
),
),
),
],
),
],
),
// 🔥 GPS定位标签
Positioned(
top: 8,
left: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(4),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 6,
height: 6,
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
),
),
const SizedBox(width: 4),
const Text(
'GPS定位',
style: TextStyle(fontSize: 10, color: Colors.white),
),
],
),
),
),
// 🔥 轨迹点数
if (_trajectoryPoints.isNotEmpty)
Positioned(
top: 8,
right: 8,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
'轨迹点: ${_trajectoryPoints.length}',
style: const TextStyle(fontSize: 10, color: Colors.white),
),
),
),
// 🔥 等待定位提示
if (_currentPosition == null)
const Positioned(
bottom: 8,
right: 8,
child: Text(
'等待定位...',
style: TextStyle(fontSize: 10, color: Colors.black54),
),
),
],
),
),
),
@@ -931,7 +1419,7 @@ class _DroneVideoControlPageState extends State<DroneVideoControlPage> {
const SizedBox(width: 12),
Expanded(
child: Container(
height: 160,
height: 200,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),

View File

@@ -25,13 +25,32 @@ class RobotControlPage extends StatelessWidget {
icon: const Icon(Icons.arrow_back, color: Color(0xFF1D2129)),
onPressed: () => Navigator.pop(context),
),
title: Text(
'${robot['type']}控制',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
title: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
robot['alias'] != null && (robot['alias'] as String).isNotEmpty
? robot['alias']
: '暂无别名',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Color(0xFF1D2129),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 2),
Text(
robot['name'] ?? '', // 🔥 使用 name 字段(长序列号 deviceName)
style: const TextStyle(
fontSize: 11,
color: Color(0xFF86909C),
),
textAlign: TextAlign.center,
maxLines: 2,
softWrap: true,
),
],
),
centerTitle: true,
actions: [

View File

@@ -112,7 +112,8 @@ class _RobotListViewState extends State<RobotListView> {
.where((r) => r.status == '在线')
.length;
final onlineCleaning = cleaningRobots.where((r) => r.status == '在线').length;
final onlineWeeding = weedingRobots.where((r) => r.status == '在线').length;
// 暂且所有设备都视为除草机器人,在线数使用总数
final onlineWeeding = allRobots.where((r) => r.status == '在线').length;
return Container(
margin: const EdgeInsets.fromLTRB(16, 16, 16, 12),
padding: const EdgeInsets.symmetric(vertical: 16),
@@ -351,7 +352,7 @@ class _RobotListViewState extends State<RobotListView> {
),
const SizedBox(width: 8),
Text(
'${weedingRobots.length}',
'${allRobots.length}',
style: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,