Files
flutterApp/lib/features/home/presentation/widgets/map/testmap_pages.dart

1671 lines
63 KiB
Dart
Raw Normal View History

import 'dart:async';
2026-02-26 14:08:02 +08:00
import 'dart:convert';
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:geolocator/geolocator.dart';
import 'package:latlong2/latlong.dart';
import 'package:maibu_satabot_v2/components/confrim_dialog.dart';
import 'package:maibu_satabot_v2/core/app/app_user_cubit.dart';
2026-02-26 14:08:02 +08:00
import 'package:maibu_satabot_v2/core/di/injection.dart';
import 'package:maibu_satabot_v2/core/network/net_message_dispatcher.dart';
import 'package:maibu_satabot_v2/core/network/protocol_decoder.dart';
2026-02-28 14:58:25 +08:00
import 'package:maibu_satabot_v2/features/devices/data/models/device_work_area_param_model.dart'
as work_area_model;
2026-02-28 09:55:14 +08:00
import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart';
import 'package:maibu_satabot_v2/features/devices/domain/usecases/get_work_record_usecase.dart';
2026-02-28 09:55:14 +08:00
import 'package:maibu_satabot_v2/features/devices/domain/usecases/select_work_record_usecase.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_cubit.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/devices_state.dart';
2026-02-25 20:00:33 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
2026-02-25 17:22:59 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
2026-02-26 14:08:02 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart';
2026-02-25 20:36:46 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart';
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart';
2026-02-25 17:22:59 +08:00
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
import '../path_list_pages.dart';
2026-02-26 14:08:02 +08:00
// 定义轨迹点类型(经纬度)
typedef PlotPoint = LatLng;
// 保持 PlotData 类不变
2026-02-26 10:44:56 +08:00
class PlotData {
final String id;
final String plotName;
2026-02-26 10:44:56 +08:00
final String imageUrl; // 图片URL(本地/assets/网络都可)
final String? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
PlotData({
required this.id,
required this.plotName,
required this.imageUrl,
this.jsonData,
});
2026-02-26 10:44:56 +08:00
}
class PlotDataPath {
final dynamic? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
PlotDataPath({this.jsonData});
}
class MapPageEnterprise extends StatefulWidget {
const MapPageEnterprise({Key? key}) : super(key: key);
@override
State<MapPageEnterprise> createState() => _MapPageEnterpriseState();
}
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
2026-02-26 14:08:02 +08:00
final _dispatcher = sl<NetMessageDispatcher>();
late StreamSubscription<RawPacket> _sub;
2026-02-28 14:33:55 +08:00
bool _isRefreshing = false; // 新增:页面刷新状态标志
2026-02-26 14:08:02 +08:00
final MapController _mapController = MapController();
2026-02-26 10:44:56 +08:00
final TextEditingController _plotNameController = TextEditingController();
2026-02-26 14:08:02 +08:00
// 初始化轨迹管理器(泛型指定为LatLng)
late final TracePoint<PlotPoint> _traceManager;
LatLng? _currentLatLng;
StreamSubscription<Position>? _positionSub;
bool _hasMovedOnce = false;
2026-02-25 17:22:59 +08:00
bool _isPanelOpen = false; // 控制面板显示/隐藏
2026-02-25 20:00:33 +08:00
bool _directionBoxOpen = false; //航线面板显示/隐藏
2026-02-26 10:44:56 +08:00
final bool _saveBoxOpen = true; //保存按钮显示/隐藏
bool _isListBoxOpen = false; //列表面板显示/隐藏
2026-02-26 14:08:02 +08:00
// 新增:存储选中的地块(null表示未选中)
PlotData? _selectedPlot;
PlotDataPath? _selectedPlotPath; // 存储选中地块的路径数据(可选)
2026-02-26 14:08:02 +08:00
// 新增:控制底部作业面板显示
bool _isWorkPanelOpen = false;
2026-02-25 20:00:33 +08:00
RobotMode _currentRobotMode = RobotMode.point; //打点模式
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
WorkMode? _currentWorkMode = WorkMode.bow; // 当前作业模式
// 新增:存储转换后的path和outer坐标
List<LatLng> gcjPathPoints = [];
List<LatLng> gcjOuterPoints = [];
2026-02-25 20:00:33 +08:00
2026-02-25 20:09:18 +08:00
// ========== 核心新增状态 ==========
2026-02-26 09:17:14 +08:00
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
2026-02-26 10:44:56 +08:00
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
2026-02-26 09:17:14 +08:00
final double _headingAngle = 0.0; // 当前机器航向角(单位:度)
2026-02-26 14:08:02 +08:00
List<LatLng>? _currentTracePoints; //收到机器的点坐标
2026-02-26 09:17:14 +08:00
//打点
double _workDistance = 0.0; // 作业行距(单位:米)
double _angle = 0.0; // 航线方向角(单位:度)
@override
void initState() {
super.initState();
2026-02-26 14:08:02 +08:00
_traceManager = TracePoint<PlotPoint>();
_initLocationEnterprise();
2026-02-26 14:08:02 +08:00
_sub = _dispatcher.onCommand(0x12).listen(_onDeviceData);
// 设置事件回调(更新UI)
_traceManager.onCurrentPointUpdated = (point) {
setState(() {
_currentTracePoints = _traceManager.getTracePoint();
});
};
_traceManager.onCompletePointAdded = (point) {
setState(() {
_currentTracePoints = _traceManager.getTracePoint();
});
};
// 示例:切换到导航模式
_traceManager.setMode(TPMode.LOCATION);
2026-02-25 20:09:18 +08:00
// 监听地图移动事件,实时更新连线
_mapController.mapEventStream.listen((event) {
if (event is MapEventMove || event is MapEventMoveEnd) {
2026-02-26 10:44:56 +08:00
if (mounted && _mapController.camera != null) {
final originalCenter = _mapController.camera!.center;
final highPrecisionCenter = LatLng(
originalCenter.latitude, // 原始纬度(保留全部小数位)
originalCenter.longitude, // 原始经度(保留全部小数位)
);
setState(() {
_mapCenter = highPrecisionCenter;
});
// 可选:打印验证精度(保留15位小数)
debugPrint(
'地图中心更新(高精度):纬度=${_mapCenter.latitude.toStringAsFixed(15)}, 经度=${_mapCenter.longitude.toStringAsFixed(15)}',
);
}
2026-02-25 20:09:18 +08:00
}
});
}
// ========== 新增:计算坐标列表的边界范围 ==========
LatLngBounds? calculateBounds(List<LatLng> points) {
if (points.isEmpty) return null;
// 使用静态方法 fromPoints 创建边界(所有latlong2版本都支持)
return LatLngBounds.fromPoints(points);
}
// ========== 新增:移动地图到坐标中心 ==========
// ========== 移动地图到坐标中心(无需修改) ==========
void moveMapToPointsCenter(List<LatLng> points) {
if (points.isEmpty || !mounted) return;
// 计算边界
final bounds = calculateBounds(points);
if (bounds == null) return;
// 移动地图到边界中心(自动适配缩放级别)
_mapController.fitBounds(
bounds,
options: FitBoundsOptions(
padding: const EdgeInsets.all(50), // 边缘留白(避免内容贴边)
maxZoom: 18, // 最大缩放级别(防止过度放大)
),
);
print('地图已移动到绘制内容中心,边界范围:$bounds');
}
// ========== 新增:安全转换任意类型为double ==========
double? safeToDouble(dynamic value) {
if (value == null) return null;
if (value is double) return value;
if (value is int) return value.toDouble();
if (value is String) {
return double.tryParse(value); // 字符串转数字(失败返回null)
}
return null;
}
2026-02-26 14:08:02 +08:00
void _onDeviceData(RawPacket packet) {
try {
debugPrint('>>> 收到0x12设备数据: ${packet}');
debugPrint('>>> 收到设备数据: ${utf8.decode(packet.payload)}');
final csv = utf8.decode(packet.payload);
final fields = csv.split(',');
// 至少需 16 个字段(索引 10=航向角, 13=卫星数, 14=定位质量)
if (fields.length >= 16) {
//setState(() {
// _voltage = fields[0]; // 电压
// _leftSpeed = fields[1]; // 左轮目标速度
// _rightSpeed = fields[2]; // 右轮目标速度
// _pitch = fields[11]; // 俯仰角
// _roll = fields[12]; // 翻滚角
// _latitude = fields[16]; // 纬度
// _longitude = fields[17]; // 经度
// _timeStamp = '${fields[16]} ${fields[17]}'; // 时间戳(合并字段)
// _knifeCuttingSpeed = fields[19]; // 割刀速度
// _controlMode = fields[20]; // 控制模式
// _workingArea = fields[21]; // 作业面积
// _battery = fields[22]; // 电量
// _obstacleFlag = fields[23]; // 障碍物标志
// _yaw = fields[10]; // 航向角
// _satelliteCnt = fields[13]; // 卫星数
// _qual = fields[14]; // 定位质量
//});
}
} catch (_) {
// 解析失败时忽略,避免崩溃
}
}
2026-02-26 10:44:56 +08:00
void _showSavePlotDialog() {
// 清空上次的输入内容
_plotNameController.clear();
// 显示弹窗
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('保存地块'),
content: TextField(
controller: _plotNameController,
decoration: const InputDecoration(
hintText: '请输入地块名称(如:北地块、一号田)',
border: OutlineInputBorder(),
labelText: '地块名称',
),
),
actions: [
// 取消按钮
TextButton(
onPressed: () {
Navigator.of(context).pop(); // 关闭弹窗
},
child: const Text('取消'),
),
// 确认保存按钮
TextButton(
onPressed: () {
// 获取输入的地块名称并去除首尾空格
String plotName = _plotNameController.text.trim();
// 验证输入是否为空
if (plotName.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('地块名称不能为空!'),
backgroundColor: Colors.red,
),
);
return;
}
// 执行保存逻辑
_savePlotData(plotName);
// 关闭弹窗
Navigator.of(context).pop();
// 保存成功提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('地块「$plotName」保存成功!'),
backgroundColor: Colors.green,
),
);
},
child: const Text('保存'),
),
],
);
},
);
}
// 3. 保存地块数据的核心方法(这里写你的实际保存逻辑)
void _savePlotData(String plotName) {
// 示例:打印保存的信息(实际开发中替换为存数据库/本地存储/接口调用)
2026-02-26 10:44:56 +08:00
debugPrint('===== 保存地块数据 =====');
debugPrint('地块名称:$plotName');
debugPrint('打点数量:${_markedPoints.length}');
debugPrint('打点坐标:$_markedPoints');
debugPrint('作业模式:$_currentWorkMode');
}
List<LatLng> _getPolygonPoints() {
if (_markedPoints.isEmpty || _mapController.camera == null) return [];
List<LatLng> polygonPoints = List.from(_markedPoints);
polygonPoints.add(_mapCenter);
return polygonPoints;
}
2026-02-26 09:17:14 +08:00
void _undoLastPoint() {
if (_markedPoints.isNotEmpty) {
setState(() {
_markedPoints.removeLast(); // 删除最后一个打点
});
debugPrint('撤回成功,剩余打点数量:${_markedPoints.length}');
} else {
debugPrint('无打点可撤回');
// 可选:提示用户
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('暂无打点可撤回')));
}
}
// ========== 核心:清空所有打点和轨迹 ==========
void _deleteAllPoints() {
if (_markedPoints.isNotEmpty) {
setState(() {
_markedPoints.clear(); // 清空所有打点
});
debugPrint('删除成功,已清空所有打点和轨迹');
// 可选:提示用户
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('已清空所有打点和轨迹')));
} else {
debugPrint('无打点可删除');
ScaffoldMessenger.of(
context,
).showSnackBar(const SnackBar(content: Text('暂无打点可删除')));
}
}
/// ===============================
/// 企业级定位初始化(已修复坐标系)
/// ===============================
Future<void> _initLocationEnterprise() async {
final serviceEnabled = await Geolocator.isLocationServiceEnabled();
if (!serviceEnabled) return;
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (permission != LocationPermission.whileInUse &&
permission != LocationPermission.always) {
return;
}
// 1️⃣ 首次强制获取定位
final position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.bestForNavigation,
);
final gcj = wgs84ToGcj02(position.latitude, position.longitude);
_updateLocation(gcj, moveMap: true);
// 2️⃣ 实时监听(不再强制移动地图)
2026-02-25 20:00:33 +08:00
_positionSub =
Geolocator.getPositionStream(
locationSettings: const LocationSettings(
accuracy: LocationAccuracy.bestForNavigation,
distanceFilter: 2,
),
).listen((pos) {
final gcj = wgs84ToGcj02(pos.latitude, pos.longitude);
_updateLocation(gcj, moveMap: false);
});
}
2026-02-28 14:33:55 +08:00
// ========== 新增:页面刷新初始化方法 ==========
Future<void> _handleRefresh() async {
// 防止重复刷新
if (_isRefreshing) return;
setState(() {
_isRefreshing = true; // 标记开始刷新
});
try {
// 1. 取消旧的定位监听
await _positionSub?.cancel();
// 2. 重置所有页面状态(恢复初始值)
setState(() {
// 清空打点和轨迹数据
_markedPoints.clear();
gcjPathPoints.clear();
gcjOuterPoints.clear();
_currentTracePoints = null;
// 重置面板状态
_isPanelOpen = false;
_directionBoxOpen = false;
_isListBoxOpen = false;
_isWorkPanelOpen = false;
// 重置选中状态
_selectedPlot = null;
_selectedPlotPath = null;
// 重置作业模式
_currentRobotMode = RobotMode.point;
_currentAreaMode = AreaMode.work;
_currentWorkMode = WorkMode.bow;
// 重置参数
_workDistance = 0.0;
_angle = 0.0;
_mapCenter = const LatLng(39.9042, 116.4074);
_hasMovedOnce = false;
_currentLatLng = null;
});
// 3. 重新初始化定位
await _initLocationEnterprise();
// 4. 重置轨迹管理器
_traceManager.reset();
// 5. 重新加载作业记录(保持数据最新)
final userId = context.read<AppUserCubit>().state.user?.userId ?? "";
if (userId.isNotEmpty) {
await context.read<DevicesCubit>().loadWorkRecords(userId);
}
// 6. 重置地图视角到初始位置
_mapController.move(const LatLng(39.9042, 116.4074), 17);
// 刷新成功提示
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('页面已刷新完成'),
backgroundColor: Colors.green,
duration: Duration(seconds: 2),
),
);
}
} catch (e) {
// 错误处理
debugPrint('刷新失败:$e');
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('刷新失败:${e.toString().substring(0, 50)}'),
backgroundColor: Colors.red,
duration: Duration(seconds: 3),
),
);
}
} finally {
// 标记刷新结束
if (mounted) {
setState(() {
_isRefreshing = false;
});
}
}
}
/// ===============================
/// 更新位置
/// ===============================
void _updateLocation(LatLng latLng, {bool moveMap = false}) {
setState(() {
_currentLatLng = latLng;
});
if (moveMap && !_hasMovedOnce) {
_hasMovedOnce = true;
_mapController.move(latLng, 17);
}
}
/// 手动回到当前位置
void _moveToCurrentLocation() {
if (_currentLatLng == null) return;
_mapController.move(_currentLatLng!, 17);
}
2026-02-25 20:09:18 +08:00
// ========== 核心方法:打点逻辑 ==========
void _addMarkedPoint() {
setState(() {
_markedPoints.add(_mapCenter); // 在地图中心打点
});
debugPrint(
2026-02-26 10:44:56 +08:00
'$_markedPoints,新增打点:第${_markedPoints.length}个点,经纬度:${_mapCenter.latitude.toStringAsFixed(20)}, ${_mapCenter.longitude.toStringAsFixed(20)}',
2026-02-25 20:09:18 +08:00
);
}
// ========== 新增:返回上一级页面的方法 ==========
void _navigateBack() {
// 关闭当前页面,返回上一级
Navigator.of(context).pop();
}
@override
void dispose() {
_positionSub?.cancel();
2026-02-26 14:08:02 +08:00
_traceManager.reset();
_mapController.dispose();
// 修复:_sub 是 non-null 类型,直接 cancel 无需 ?
_sub.cancel();
_plotNameController.dispose(); // 新增:释放文本控制器资源
super.dispose();
}
2026-02-26 14:08:02 +08:00
// 模拟添加完成点
void _addCompletePoint() {
final newPoint = LatLng(
39.9042 + (DateTime.now().millisecond / 10000),
116.4074 + (DateTime.now().millisecond / 10000),
);
_traceManager.upsert(newPoint, TPAction.ADD);
}
// 模拟更新当前点
void _updateCurrentPoint() {
final newPoint = LatLng(
39.9042 + (DateTime.now().millisecond / 10000),
116.4074 + (DateTime.now().millisecond / 10000),
);
_traceManager.upsert(newPoint);
}
// 🔥 改动2:修改 _buildPlotListItem 方法,增加删除时调用Bloc的逻辑
Widget _buildPlotListItem(PlotData plot, Function(PlotData) onDelete) {
2026-02-26 14:08:02 +08:00
return GestureDetector(
// 核心:点击列表项触发选中逻辑
onTap: () async {
// 1. 清空之前的轨迹数据
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
_currentTracePoints = null;
});
2026-02-28 09:55:14 +08:00
await context.read<DevicesCubit>().loadSelectedPath(plot.plotName);
final cubitState = context.read<DevicesCubit>().state;
final _loadedPlot = cubitState.pathData;
print('加载地块「${plot.plotName}」的路径数据: $_loadedPlot');
// 2. 立即解析并更新状态(在事件回调中,不在 build 中)
if (_loadedPlot is List) {
final List<dynamic> rawList = _loadedPlot as List<dynamic>;
final pathRecords = rawList
.where((item) => item is Map<String, dynamic>)
.cast<Map<String, dynamic>>()
.toList();
if (pathRecords.isNotEmpty) {
final selectedPlotPath = PlotDataPath(jsonData: pathRecords.first);
try {
final dynamic nestedJsonRaw =
selectedPlotPath.jsonData['jsonData'];
if (nestedJsonRaw != null && nestedJsonRaw is String) {
final String nestedJsonStr = nestedJsonRaw;
final dynamic parsedJson = jsonDecode(nestedJsonStr);
if (parsedJson is Map<String, dynamic>) {
final Map<String, dynamic> nestedJson = parsedJson;
final dynamic rawPath = nestedJson['path'];
final dynamic rawOuter = nestedJson['outer'];
List<dynamic> pathList = rawPath is List ? rawPath : [];
List<dynamic> outerList = rawOuter is List ? rawOuter : [];
List<LatLng> newPathPoints = [];
List<LatLng> newOuterPoints = [];
for (var item in pathList) {
double? lat = safeToDouble(item['lat']);
double? lon = safeToDouble(item['lon']);
if (lat != null && lon != null) {
newPathPoints.add(convertWGS84ToGCJ02(lat, lon));
}
}
for (var item in outerList) {
double? lng = safeToDouble(item['lng']);
double? lat = safeToDouble(item['lat']);
if (lng != null && lat != null) {
newOuterPoints.add(convertWGS84ToGCJ02(lat, lng));
}
}
// 在这里更新状态(安全,因为在事件回调中)
setState(() {
gcjPathPoints = newPathPoints;
gcjOuterPoints = newOuterPoints;
_selectedPlot = plot;
_isListBoxOpen = false;
_isWorkPanelOpen = true;
});
// 移动地图到中心
List<LatLng> allPoints = [];
allPoints.addAll(gcjPathPoints);
allPoints.addAll(gcjOuterPoints);
if (allPoints.isNotEmpty) {
moveMapToPointsCenter(allPoints);
}
}
}
} catch (e) {
print('解析/转换坐标失败:$e');
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
_selectedPlot = plot;
_isListBoxOpen = false;
_isWorkPanelOpen = true;
});
}
} else {
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
_selectedPlot = plot;
_isListBoxOpen = false;
_isWorkPanelOpen = true;
});
}
} else {
setState(() {
gcjPathPoints = [];
gcjOuterPoints = [];
_selectedPlot = plot;
_isListBoxOpen = false;
_isWorkPanelOpen = true;
});
}
2026-02-26 14:08:02 +08:00
},
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
2026-02-26 14:08:02 +08:00
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _selectedPlot?.id == plot.id
? Colors.blue
: Colors.grey[100]!,
width: _selectedPlot?.id == plot.id ? 2 : 1, // 选中项高亮边框
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
),
child: Row(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
2026-02-26 14:08:02 +08:00
plot.imageUrl,
width: 30,
height: 30,
2026-02-26 14:08:02 +08:00
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 30,
height: 30,
2026-02-26 14:08:02 +08:00
color: Colors.grey[200],
child: const Icon(Icons.image_outlined, color: Colors.grey),
);
},
),
),
2026-02-26 10:44:56 +08:00
const SizedBox(width: 10),
2026-02-26 10:44:56 +08:00
2026-02-26 14:08:02 +08:00
Expanded(
child: Text(
plot.plotName,
style: const TextStyle(fontSize: 16, color: Colors.black87),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
IconButton(
onPressed: () => _showDeleteConfirmDialog(plot, onDelete),
2026-02-26 14:08:02 +08:00
icon: const Icon(
Icons.delete_outline,
color: Colors.redAccent,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
],
),
2026-02-26 10:44:56 +08:00
),
);
}
void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) {
ConfirmDialog.show(
2026-02-26 10:44:56 +08:00
context: context,
title: '确认删除',
content: '是否删除地块「${plot.plotName}」?删除后不可恢复。',
confirmText: '删除',
confirmTextColor: Colors.red,
onConfirm: () {
onDelete(plot);
if (context.mounted) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text('已删除地块「${plot.plotName}」')));
}
2026-02-26 10:44:56 +08:00
},
);
}
2026-02-26 14:08:02 +08:00
Widget _buildWorkPanel() {
2026-02-28 09:55:14 +08:00
return BlocBuilder<DevicesCubit, DevicesState>(
builder: (context, state) {
// 无选中地块或无路径数据时返回空
if (_selectedPlot == null || state.pathData == null) {
return const SizedBox.shrink();
}
2026-02-26 14:08:02 +08:00
String workMode = "未知模式";
if (_selectedPlotPath != null) {
try {
final String nestedJsonStr =
_selectedPlotPath!.jsonData['jsonData'] as String;
final Map<String, dynamic> nestedJson = jsonDecode(nestedJsonStr);
workMode = nestedJson['planModel'] == WorkMode.bow.value
? "弓字模式"
: "自定义模式";
} catch (e) {
workMode = "未知模式";
}
2026-02-28 09:55:14 +08:00
}
return Positioned(
left: 0,
right: 0,
bottom: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
2026-02-26 14:08:02 +08:00
),
2026-02-28 09:55:14 +08:00
child: Column(
mainAxisSize: MainAxisSize.min,
2026-02-26 14:08:02 +08:00
children: [
2026-02-28 09:55:14 +08:00
// 1. 地块基础信息
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.network(
_selectedPlot!.imageUrl,
2026-02-26 14:08:02 +08:00
width: 80,
height: 80,
2026-02-28 09:55:14 +08:00
fit: BoxFit.cover,
errorBuilder: (context, error, stackTrace) {
return Container(
width: 80,
height: 80,
color: Colors.grey[200],
child: const Icon(
Icons.image_outlined,
color: Colors.grey,
size: 32,
),
);
},
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_selectedPlot!.plotName,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
'作业模式:$workMode',
style: const TextStyle(
fontSize: 14,
color: Colors.grey,
),
),
],
),
),
IconButton(
onPressed: () {
setState(() {
_isWorkPanelOpen = false;
_selectedPlot = null;
gcjPathPoints = [];
gcjOuterPoints = [];
2026-02-28 09:55:14 +08:00
});
},
icon: const Icon(
Icons.close,
color: Colors.grey,
size: 20,
),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(
minWidth: 24,
minHeight: 24,
),
),
],
2026-02-26 14:08:02 +08:00
),
2026-02-28 09:55:14 +08:00
const SizedBox(height: 16),
2026-02-26 14:08:02 +08:00
2026-02-28 09:55:14 +08:00
// 2. 错误提示(如果加载失败)
if (state.errorMessage != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
margin: const EdgeInsets.only(bottom: 12),
decoration: BoxDecoration(
color: Colors.red[50],
borderRadius: BorderRadius.circular(6),
border: Border.all(color: Colors.red[200]!),
),
child: Row(
children: [
const Icon(
Icons.error_outline,
color: Colors.redAccent,
size: 16,
),
const SizedBox(width: 8),
Expanded(
child: Text(
state.errorMessage!,
style: const TextStyle(
fontSize: 14,
color: Colors.redAccent,
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
),
],
2026-02-26 14:08:02 +08:00
),
),
2026-02-28 09:55:14 +08:00
// 3. 开始作业按钮(加载中禁用)
SizedBox(
width: double.infinity,
height: 50,
2026-02-28 09:55:14 +08:00
child: ElevatedButton(
onPressed: state.isLoading || gcjPathPoints.isEmpty
2026-02-28 09:55:14 +08:00
? null
: () {
// 作业逻辑:使用加载的pathData
debugPrint(
'开始作业:${_selectedPlot!.plotName},路径数据:$gcjPathPoints',
2026-02-28 09:55:14 +08:00
);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF00C853),
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
elevation: 2,
disabledBackgroundColor: Colors.grey[300],
disabledForegroundColor: Colors.grey[600],
2026-02-26 14:08:02 +08:00
),
2026-02-28 09:55:14 +08:00
child: state.isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: const Text(
'开始作业',
style: TextStyle(
fontSize: 14,
2026-02-28 09:55:14 +08:00
fontWeight: FontWeight.bold,
),
),
2026-02-26 14:08:02 +08:00
),
),
],
),
2026-02-28 09:55:14 +08:00
),
);
},
2026-02-26 14:08:02 +08:00
);
}
// 🔥 改动4:新增方法 - 将Bloc的workRecords转换为PlotData列表
List<PlotData> _convertWorkRecordsToPlotData(
List<Map<String, dynamic>> records,
) {
return records.map((record) {
return PlotData(
id:
record['id']?.toString() ??
DateTime.now().microsecondsSinceEpoch.toString(), // 唯一ID
plotName: record['workName'] ?? '未命名地块', // 地块名称(从接口字段取)
imageUrl: record['imgUrl'] ?? '', // 图片URL(从接口字段取,无则为空)
jsonData: record['jsonData'], // 原始数据的JSON字符串(可选,便于调试或后续使用)
);
}).toList();
}
@override
Widget build(BuildContext context) {
final screenHeight = MediaQuery.of(context).size.height;
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
final maxTop = screenHeight - menuHeight;
return Scaffold(
2026-02-25 20:00:33 +08:00
body: SafeArea(
child: Stack(
2026-02-25 20:00:33 +08:00
children: [
2026-02-25 20:09:18 +08:00
// 地图核心组件
2026-02-25 20:00:33 +08:00
FlutterMap(
mapController: _mapController,
options: MapOptions(
initialCenter:
_currentLatLng ?? const LatLng(39.9042, 116.4074),
initialZoom: 20,
maxZoom: 22,
2026-02-25 20:09:18 +08:00
// 禁止地图点击事件(避免和中心标冲突)
onTap: (_, __) {}, // 空实现,禁用地图点击响应
),
2026-02-25 20:00:33 +08:00
children: [
/// 高德瓦片(GCJ-02)
TileLayer(
urlTemplate:
'https://webrd02.is.autonavi.com/appmaptile'
'?style=8&x={x}&y={y}&z={z}&lang=zh_cn&size=1&scale=1'
'&key=bbb1f0f20eed6bf679eddf2625630aba',
),
// 绘制path折线(Line模式)
if (gcjPathPoints.isNotEmpty)
PolylineLayer(
polylines: [
Polyline(
points: gcjPathPoints, // 转换后的GCJ02坐标
color: Colors.blueAccent, // 折线颜色(可自定义)
strokeWidth: 3.0, // 折线宽度
isDotted: false, // 非虚线(Line模式)
borderColor: Colors.white, // 可选:添加白色描边,提升辨识度
borderStrokeWidth: 0.5,
),
],
),
// 绘制outer边框(Polygon模式)
if (gcjOuterPoints.isNotEmpty)
PolygonLayer(
polygons: [
Polygon(
points: gcjOuterPoints, // 转换后的GCJ02坐标
color: Colors.green.withOpacity(0.1), // 内部填充色(透明)
borderColor: Colors.green, // 边框颜色
borderStrokeWidth: 2.0, // 边框宽度
isFilled: true, // 开启填充(即使透明,也需要开启才能显示边框)
),
],
),
/// 中心标与历史打点的虚线连线
2026-02-25 20:36:46 +08:00
PolylineLayer(
polylines: [
for (int i = 0; i < _markedPoints.length - 1; i++)
Polyline(
points: [_markedPoints[i], _markedPoints[i + 1]],
color: Colors.orange.withOpacity(0.5),
strokeWidth: 1.5,
),
],
),
2026-02-25 20:09:18 +08:00
if (_markedPoints.isNotEmpty)
PolylineLayer(
polylines: [
2026-02-25 20:36:46 +08:00
Polyline(
points: [_mapCenter, _markedPoints.last],
color: Colors.blue.withOpacity(0.5),
strokeWidth: 1.5,
),
2026-02-25 20:09:18 +08:00
],
),
2026-02-25 20:00:33 +08:00
/// 当前定位 Marker
if (_currentLatLng != null)
MarkerLayer(
markers: [
Marker(
point: _currentLatLng!,
2026-02-25 20:36:46 +08:00
width: 40,
height: 40,
child: CustomPaint(
size: const Size(40, 40),
painter: HeadingMarkerPainter(
headingAngle: _headingAngle,
),
2026-02-25 20:00:33 +08:00
),
),
],
),
2026-02-26 10:44:56 +08:00
if (_currentWorkMode == WorkMode.bow &&
_markedPoints.isNotEmpty)
PolygonLayer(
polygons: [
Polygon(
points: _getPolygonPoints(),
color: Colors.green.withOpacity(0.2),
borderColor: Colors.green.withOpacity(0.5),
borderStrokeWidth: 1,
isFilled: true,
),
],
),
2026-02-25 20:09:18 +08:00
/// 历史打点的绿色标记
2026-02-25 20:09:18 +08:00
MarkerLayer(
markers: _markedPoints.asMap().entries.map((entry) {
int index = entry.key + 1; // 打点序号(从1开始)
LatLng point = entry.value;
return Marker(
point: point,
width: 80,
height: 40,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
2026-02-25 20:36:46 +08:00
const SizedBox(height: 8), // 向下偏移8px(抵消默认的顶部对齐)
2026-02-25 20:09:18 +08:00
// 绿色打点标记
Container(
width: 16,
height: 16,
decoration: const BoxDecoration(
color: Color(0xFF00C853), // 绿色主题色
shape: BoxShape.circle,
boxShadow: [
BoxShadow(color: Colors.black12, blurRadius: 2),
],
),
child: Center(
child: Text(
'$index',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold,
),
),
),
),
],
),
);
}).toList(),
),
2026-02-25 20:00:33 +08:00
],
),
2026-02-25 20:00:33 +08:00
2026-02-28 14:33:55 +08:00
if (_isRefreshing)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.3),
child: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(
color: Colors.white,
strokeWidth: 3,
),
],
),
),
),
),
// 悬浮返回按钮(核心修改)
Positioned(
top: 10,
left: 10,
child: GestureDetector(
// 自定义点击事件
onTap: _navigateBack,
// 自定义点击反馈(替代 IconButton 的高亮/水波纹)
child: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.8),
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 3,
offset: const Offset(0, 2),
),
],
),
// 核心:用 Stack 实现点击高亮层 + 图标层
child: Stack(
alignment: Alignment.center, // 所有子组件居中
children: [
// 1. 点击高亮层(默认隐藏,点击时显示)
Positioned.fill(
child: Container(
decoration: BoxDecoration(
color: Colors.transparent, // 默认透明
borderRadius: BorderRadius.circular(20),
),
),
),
// 2. 图标层(绝对居中)
const Icon(
Icons.arrow_back_ios,
color: Colors.black87,
size: 20,
),
],
),
),
),
),
/// 核心:地图正中心固定定位标
2026-02-25 20:36:46 +08:00
Positioned(
2026-02-25 20:09:18 +08:00
left: 0,
right: 0,
top: 0,
bottom: 0,
child: Center(
2026-02-25 20:36:46 +08:00
// 自定义十字标(中心精准对齐地图中心)
child: SizedBox(
width: 16, // 十字整体宽度
height: 16, // 十字整体高度
child: CustomPaint(
painter: CrosshairPainter(), // 自定义十字画笔
),
2026-02-25 20:09:18 +08:00
),
),
),
2026-02-26 10:44:56 +08:00
//保存按钮
if (_saveBoxOpen)
Positioned(
right: 80,
2026-02-26 10:44:56 +08:00
top: maxTop > 10 ? 11 : maxTop,
child: Container(
width: 40,
height: 40,
2026-02-26 10:44:56 +08:00
decoration: BoxDecoration(
color: const Color(0xFF00C853),
shape: BoxShape.circle,
2026-02-26 10:44:56 +08:00
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 4,
offset: const Offset(0, 2),
2026-02-26 10:44:56 +08:00
),
],
),
child: IconButton(
onPressed: () {
_showSavePlotDialog();
},
icon: const Icon(Icons.save, color: Colors.white, size: 18),
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
2026-02-26 10:44:56 +08:00
),
),
),
2026-02-25 20:09:18 +08:00
// 右侧悬浮菜单
2026-02-25 17:22:59 +08:00
Positioned(
2026-02-25 20:00:33 +08:00
height: 336,
right: 16,
2026-02-25 20:09:18 +08:00
top: maxTop > 10 ? 11 : maxTop,
2026-02-25 20:00:33 +08:00
child: FloatingActionButton(
onPressed: _moveToCurrentLocation,
child: new VerticalFloatMenu(
onEditTap: (bool isOpen) {
setState(() {
_isPanelOpen = isOpen;
});
},
2026-02-26 10:44:56 +08:00
onListBox: (bool isOpen) {
// 🔥 改动5:加载作业记录(原有逻辑保留)
final userId =
context.read<AppUserCubit>().state.user?.userId ?? "";
print('加载作业记录,当前用户ID:$userId');
context.read<DevicesCubit>().loadWorkRecords(userId);
2026-02-26 10:44:56 +08:00
setState(() {
_isListBoxOpen = isOpen;
_isWorkPanelOpen = false;
2026-02-26 10:44:56 +08:00
});
},
2026-02-25 20:00:33 +08:00
onWorkModeSelected: (mode) {
_currentWorkMode = mode;
debugPrint('外部收到作业模式:$mode');
},
2026-02-28 14:33:55 +08:00
onRefreshTap: _handleRefresh, // 推荐用局部刷新
2026-02-25 20:00:33 +08:00
),
2026-02-25 17:22:59 +08:00
),
),
2026-02-25 20:09:18 +08:00
// 底部操作面板
2026-02-25 20:00:33 +08:00
if (_isPanelOpen)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: BottomOperationPanel(
initialRobotMode: RobotMode.point,
initialAreaMode: AreaMode.work,
2026-02-28 14:58:25 +08:00
onComplete: () async {
2026-02-26 10:44:56 +08:00
debugPrint(
'操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle',
);
2026-02-28 14:58:25 +08:00
// 1. 校验必要条件(至少需要3个打点才能形成闭合区域)
if (_markedPoints.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请先添加作业区域打点!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_markedPoints.length < 3) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('作业区域至少需要3个打点才能生成路径!'),
backgroundColor: Colors.red,
),
);
return;
}
if (_currentWorkMode == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('请先选择作业模式!'),
backgroundColor: Colors.red,
),
);
return;
}
try {
// 2. 转换页面数据为 generatePath 所需的参数格式
// 2.1 参考点(取第一个打点,转换为 WGS84)
final firstPoint = _markedPoints.first;
final wgs84FirstPoint = gcj02ToWgs84(
firstPoint.latitude,
firstPoint.longitude,
);
final referencePoint = work_area_model.ReferencePoint(
lat: wgs84FirstPoint.latitude,
lon: wgs84FirstPoint.longitude,
);
// 2.2 航向角(转换为整型,-1表示最优航向则取-1)
final heading = _angle == -1 ? -1 : _angle.toInt();
// 2.3 外边界(转换为 OuterBoundary 格式,所有点转 WGS84)
final outerBoundary = work_area_model.OuterBoundary(
// 将 LatLng 打点转换为 Position 列表(先转 WGS84)
position: _markedPoints.map((latLng) {
// 关键:每个点都转换为 WGS84 坐标系
final wgs84Point = gcj02ToWgs84(
latLng.latitude,
latLng.longitude,
);
return work_area_model.Position(
lat: wgs84Point.latitude,
lon: wgs84Point.longitude,
);
}).toList(),
sideWidth: _workDistance,
);
final holes = <work_area_model.HoleBoundary>[];
final workType = _currentWorkMode == WorkMode.bow
? 0
: 2; // 注意:这里弓字模式是0,自定义是2(确认和后端一致)
// 3. 调用 Cubit 的 generatePath 方法生成路径
await context.read<DevicesCubit>().generatePath(
reference: referencePoint,
heading: heading,
outer: outerBoundary,
holes: holes,
workType: workType,
);
// 4. 获取生成结果并处理
final cubitState = context.read<DevicesCubit>().state;
if (cubitState.errorMessage != null) {
// 生成失败
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('路径生成失败:${cubitState.errorMessage}'),
backgroundColor: Colors.red,
),
);
} else if (cubitState.generatedPath != null) {
// 生成成功
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('路径生成成功!'),
backgroundColor: Colors.green,
),
);
debugPrint('生成的路径数据:${cubitState.generatedPath}');
// 可选:将生成的路径转换为地图可显示的坐标(WGS84 → GCJ02)
if (cubitState.generatedPath is List) {
setState(() {
// 关键:生成的路径是 WGS84,需转回 GCJ02 才能在高德地图正确显示
gcjPathPoints = (cubitState.generatedPath as List)
.map((point) {
// 先取 WGS84 坐标
double wgs84Lat =
safeToDouble(point['lat']) ?? 0.0;
double wgs84Lon =
safeToDouble(point['lon']) ?? 0.0;
// 转换为 GCJ02
final gcjPoint = wgs84ToGcj02(
wgs84Lat,
wgs84Lon,
);
return LatLng(
gcjPoint.latitude,
gcjPoint.longitude,
);
})
.toList();
// 外边界也转回 GCJ02 显示(可选,保持和地图坐标系一致)
gcjOuterPoints = _markedPoints;
});
// 移动地图到生成的路径中心
moveMapToPointsCenter(gcjPathPoints);
}
}
} catch (e) {
// 异常处理
debugPrint('路径生成异常:$e');
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'路径生成异常:${e.toString().substring(0, 50)}',
),
backgroundColor: Colors.red,
),
);
}
// 5. 关闭操作面板
2026-02-25 20:00:33 +08:00
setState(() => _isPanelOpen = false);
},
2026-02-26 09:17:14 +08:00
onUndoTap: _undoLastPoint,
onDeleteTap: _deleteAllPoints,
2026-02-25 20:00:33 +08:00
onRobotModeChanged: (mode) {
setState(() => _currentRobotMode = mode);
debugPrint('外部收到机器人模式:$mode');
},
onAreaModeChanged: (mode) {
setState(() => _currentAreaMode = mode);
debugPrint('外部收到区域模式:$mode');
},
onAddTap: () {
_addMarkedPoint();
2026-02-25 20:09:18 +08:00
debugPrint('外部处理加号按钮点击,已添加打点');
2026-02-25 20:00:33 +08:00
},
2026-02-26 09:17:14 +08:00
onDistanceTap: (distance) {
_workDistance = distance;
2026-02-26 09:17:14 +08:00
debugPrint(
'外部处理作业行距距离设置,当前距离:${distance.toStringAsFixed(1)}米',
);
},
2026-02-25 20:00:33 +08:00
onSettingTap: () {
setState(() {
_directionBoxOpen = true;
});
2026-02-25 20:09:18 +08:00
debugPrint('外部处理设置按钮点击');
2026-02-25 20:00:33 +08:00
},
onLandTap: () {
debugPrint('外部处理地块标签点击');
},
onRouteTap: () {
debugPrint('外部处理航线标签点击');
},
),
),
2026-02-25 20:09:18 +08:00
// 航线方向面板
2026-02-25 20:00:33 +08:00
if (_directionBoxOpen)
Positioned(
left: 0,
right: 0,
bottom: 0,
child: RouteDirectionPanel(
initialOptimalHeading: true,
initialDirection: 0.0,
onConfirm: (result) {
debugPrint(
'最优航向:${result['optimalHeading']},角度:${result['direction']}',
);
2026-02-26 09:17:14 +08:00
_angle = result['optimalHeading'] == true
? -1
: result['direction'];
2026-02-26 09:17:14 +08:00
debugPrint('外部处理航线方向设置,当前角度:${_angle}');
2026-02-25 20:00:33 +08:00
setState(() => _directionBoxOpen = false);
},
onCancel: () {
setState(() => _directionBoxOpen = false);
},
),
),
// 列表面板 - 使用BlocBuilder监听DevicesCubit状态
2026-02-26 10:44:56 +08:00
if (_isListBoxOpen)
BlocBuilder<DevicesCubit, DevicesState>(
builder: (context, state) {
final plotList = _convertWorkRecordsToPlotData(
state.workRecords ?? [],
);
return Positioned.fill(
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
_isListBoxOpen = false;
});
},
child: Container(
color: Colors.black.withOpacity(0.3),
),
2026-02-26 10:44:56 +08:00
),
),
Container(
width: double.infinity,
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.vertical(
top: Radius.circular(16),
2026-02-26 10:44:56 +08:00
),
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
offset: Offset(0, -2),
),
],
),
constraints: const BoxConstraints(
maxHeight: 600,
minHeight: 200,
),
child: Column(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 12,
2026-02-26 10:44:56 +08:00
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: [
Text(
'地块列表(共${plotList.length}条)',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
IconButton(
onPressed: () {
setState(() {
_isListBoxOpen = false;
});
},
icon: const Icon(
Icons.close,
color: Colors.grey,
size: 20,
),
),
],
2026-02-26 10:44:56 +08:00
),
),
const Divider(height: 1, color: Colors.grey),
Expanded(
child: plotList.isEmpty
? const Center(
child: Column(
mainAxisAlignment:
MainAxisAlignment.center,
children: [
Icon(
Icons.inbox_outlined,
color: Colors.grey,
size: 48,
),
SizedBox(height: 16),
Text(
'暂无地块数据',
style: TextStyle(
color: Colors.grey,
fontSize: 16,
),
),
],
2026-02-26 10:44:56 +08:00
),
)
: ListView.builder(
padding: const EdgeInsets.symmetric(
vertical: 8,
2026-02-26 10:44:56 +08:00
),
itemCount: plotList.length,
itemBuilder: (context, index) {
final plot = plotList[index];
// 传入删除回调(调用Bloc的删除方法)
return _buildPlotListItem(plot, (
deletedPlot,
) async {
await context
.read<DevicesCubit>()
.deleteWorkRecord(
deletedPlot.plotName,
);
final userId =
context
.read<AppUserCubit>()
.state
.user
?.userId ??
"";
await context
.read<DevicesCubit>()
.loadWorkRecords(userId);
setState(() {});
});
},
),
),
],
2026-02-26 10:44:56 +08:00
),
),
],
2026-02-26 10:44:56 +08:00
),
);
},
2026-02-26 10:44:56 +08:00
),
2026-02-26 14:08:02 +08:00
if (_isWorkPanelOpen) _buildWorkPanel(),
2026-02-25 20:00:33 +08:00
],
),
),
2026-02-25 20:00:33 +08:00
);
}
}
// 转换单个WGS84坐标到GCJ02
LatLng convertWGS84ToGCJ02(double lat, double lon) {
return wgs84ToGcj02(lat, lon);
}
/// =======================================================
/// 坐标转换:WGS84 -> GCJ02(国内高德/腾讯通用)
/// =======================================================
const double _pi = 3.14159265358979324;
const double _a = 6378245.0;
const double _ee = 0.00669342162296594323;
LatLng wgs84ToGcj02(double lat, double lon) {
if (_outOfChina(lat, lon)) {
return LatLng(lat, lon);
}
double dLat = _transformLat(lon - 105.0, lat - 35.0);
double dLon = _transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * _pi;
double magic = sin(radLat);
magic = 1 - _ee * magic * magic;
double sqrtMagic = sqrt(magic);
2026-02-25 20:00:33 +08:00
dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi);
dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi);
double mgLat = lat + dLat;
double mgLon = lon + dLon;
return LatLng(mgLat, mgLon);
}
2026-02-28 14:58:25 +08:00
LatLng gcj02ToWgs84(double lat, double lon) {
if (_outOfChina(lat, lon)) {
return LatLng(lat, lon);
}
double dLat = _transformLat(lon - 105.0, lat - 35.0);
double dLon = _transformLon(lon - 105.0, lat - 35.0);
double radLat = lat / 180.0 * _pi;
double magic = sin(radLat);
magic = 1 - _ee * magic * magic;
double sqrtMagic = sqrt(magic);
dLat = (dLat * 180.0) / ((_a * (1 - _ee)) / (magic * sqrtMagic) * _pi);
dLon = (dLon * 180.0) / (_a / sqrtMagic * cos(radLat) * _pi);
double mgLat = lat - dLat;
double mgLon = lon - dLon;
return LatLng(mgLat, mgLon);
}
bool _outOfChina(double lat, double lon) {
2026-02-25 20:00:33 +08:00
return lon < 72.004 || lon > 137.8347 || lat < 0.8293 || lat > 55.8271;
}
double _transformLat(double x, double y) {
2026-02-25 20:00:33 +08:00
double ret =
-100.0 +
2.0 * x +
3.0 * y +
0.2 * y * y +
0.1 * x * y +
0.2 * sqrt(x.abs());
2026-02-25 20:00:33 +08:00
ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0;
ret += (20.0 * sin(y * _pi) + 40.0 * sin(y / 3.0 * _pi)) * 2.0 / 3.0;
ret += (160.0 * sin(y / 12.0 * _pi) + 320 * sin(y * _pi / 30.0)) * 2.0 / 3.0;
return ret;
}
double _transformLon(double x, double y) {
2026-02-25 20:00:33 +08:00
double ret =
300.0 + x + 2.0 * y + 0.1 * x * x + 0.1 * x * y + 0.1 * sqrt(x.abs());
ret += (20.0 * sin(6.0 * x * _pi) + 20.0 * sin(2.0 * x * _pi)) * 2.0 / 3.0;
ret += (20.0 * sin(x * _pi) + 40.0 * sin(x / 3.0 * _pi)) * 2.0 / 3.0;
ret +=
(150.0 * sin(x / 12.0 * _pi) + 300.0 * sin(x / 30.0 * _pi)) * 2.0 / 3.0;
return ret;
}