2026-02-25 13:41:23 +08:00
|
|
|
|
import 'dart:async';
|
2026-02-26 14:08:02 +08:00
|
|
|
|
import 'dart:convert';
|
2026-02-25 13:41:23 +08:00
|
|
|
|
import 'dart:math';
|
|
|
|
|
|
import 'package:flutter/material.dart';
|
2026-02-27 13:17:09 +08:00
|
|
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
2026-02-25 13:41:23 +08:00
|
|
|
|
import 'package:flutter_map/flutter_map.dart';
|
|
|
|
|
|
import 'package:geolocator/geolocator.dart';
|
|
|
|
|
|
import 'package:latlong2/latlong.dart';
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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 09:55:14 +08:00
|
|
|
|
import 'package:maibu_satabot_v2/features/devices/data/repositories/generate_path_repository_Impl.dart';
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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';
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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';
|
2026-02-25 13:41:23 +08:00
|
|
|
|
|
|
|
|
|
|
import '../path_list_pages.dart';
|
|
|
|
|
|
|
2026-02-26 14:08:02 +08:00
|
|
|
|
// 定义轨迹点类型(经纬度)
|
|
|
|
|
|
typedef PlotPoint = LatLng;
|
|
|
|
|
|
|
2026-02-27 13:17:09 +08:00
|
|
|
|
// 保持 PlotData 类不变
|
2026-02-26 10:44:56 +08:00
|
|
|
|
class PlotData {
|
2026-02-27 13:17:09 +08:00
|
|
|
|
final String id;
|
|
|
|
|
|
final String plotName;
|
2026-02-26 10:44:56 +08:00
|
|
|
|
final String imageUrl; // 图片URL(本地/assets/网络都可)
|
2026-02-27 13:17:09 +08:00
|
|
|
|
final String? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
|
|
|
|
|
|
|
|
|
|
|
|
PlotData({
|
|
|
|
|
|
required this.id,
|
|
|
|
|
|
required this.plotName,
|
|
|
|
|
|
required this.imageUrl,
|
|
|
|
|
|
this.jsonData,
|
|
|
|
|
|
});
|
2026-02-26 10:44:56 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 13:20:07 +08:00
|
|
|
|
class PlotDataPath {
|
|
|
|
|
|
final dynamic? jsonData; // 原始数据的JSON字符串(可选,便于调试或后续使用)
|
|
|
|
|
|
|
|
|
|
|
|
PlotDataPath({this.jsonData});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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-25 13:41:23 +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;
|
2026-02-25 13:41:23 +08:00
|
|
|
|
|
|
|
|
|
|
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;
|
2026-02-28 13:20:07 +08:00
|
|
|
|
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; // 当前作业模式
|
2026-02-28 14:02:04 +08:00
|
|
|
|
// 新增:存储转换后的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; // 航线方向角(单位:度)
|
2026-02-25 13:41:23 +08:00
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
|
void initState() {
|
|
|
|
|
|
super.initState();
|
2026-02-26 14:08:02 +08:00
|
|
|
|
_traceManager = TracePoint<PlotPoint>();
|
2026-02-25 13:41:23 +08:00
|
|
|
|
_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
|
|
|
|
}
|
|
|
|
|
|
});
|
2026-02-25 13:41:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
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-27 13:17:09 +08:00
|
|
|
|
|
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('暂无打点可删除')));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
/// ===============================
|
|
|
|
|
|
/// 企业级定位初始化(已修复坐标系)
|
|
|
|
|
|
/// ===============================
|
|
|
|
|
|
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-25 13:41:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/// ===============================
|
|
|
|
|
|
/// 更新位置
|
|
|
|
|
|
/// ===============================
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-26 16:38:10 +08:00
|
|
|
|
// ========== 新增:返回上一级页面的方法 ==========
|
|
|
|
|
|
void _navigateBack() {
|
|
|
|
|
|
// 关闭当前页面,返回上一级
|
|
|
|
|
|
Navigator.of(context).pop();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
@override
|
|
|
|
|
|
void dispose() {
|
|
|
|
|
|
_positionSub?.cancel();
|
2026-02-26 14:08:02 +08:00
|
|
|
|
_traceManager.reset();
|
|
|
|
|
|
_mapController.dispose();
|
2026-02-26 16:38:10 +08:00
|
|
|
|
// 修复:_sub 是 non-null 类型,直接 cancel 无需 ?
|
|
|
|
|
|
_sub.cancel();
|
|
|
|
|
|
_plotNameController.dispose(); // 新增:释放文本控制器资源
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:17:09 +08:00
|
|
|
|
// 🔥 改动2:修改 _buildPlotListItem 方法,增加删除时调用Bloc的逻辑
|
|
|
|
|
|
Widget _buildPlotListItem(PlotData plot, Function(PlotData) onDelete) {
|
2026-02-26 14:08:02 +08:00
|
|
|
|
return GestureDetector(
|
|
|
|
|
|
// 核心:点击列表项触发选中逻辑
|
2026-02-27 13:17:09 +08:00
|
|
|
|
onTap: () async {
|
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');
|
2026-02-27 13:17:09 +08:00
|
|
|
|
|
2026-02-26 14:08:02 +08:00
|
|
|
|
setState(() {
|
|
|
|
|
|
// 1. 选中当前地块
|
|
|
|
|
|
_selectedPlot = plot;
|
|
|
|
|
|
// 2. 关闭列表抽屉
|
|
|
|
|
|
_isListBoxOpen = false;
|
|
|
|
|
|
// 3. 打开底部作业面板
|
|
|
|
|
|
_isWorkPanelOpen = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
child: Container(
|
|
|
|
|
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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),
|
2026-02-27 13:17:09 +08:00
|
|
|
|
child: Image.network(
|
2026-02-26 14:08:02 +08:00
|
|
|
|
plot.imageUrl,
|
2026-02-27 13:17:09 +08:00
|
|
|
|
width: 30,
|
|
|
|
|
|
height: 30,
|
2026-02-26 14:08:02 +08:00
|
|
|
|
fit: BoxFit.cover,
|
|
|
|
|
|
errorBuilder: (context, error, stackTrace) {
|
|
|
|
|
|
return Container(
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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
|
|
|
|
|
2026-02-27 13:17:09 +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(
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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
|
|
|
|
),
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:17:09 +08:00
|
|
|
|
void _showDeleteConfirmDialog(PlotData plot, Function(PlotData) onDelete) {
|
|
|
|
|
|
ConfirmDialog.show(
|
2026-02-26 10:44:56 +08:00
|
|
|
|
context: context,
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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
|
|
|
|
|
2026-02-28 09:55:14 +08:00
|
|
|
|
// 解析路径数据(根据你的实际数据结构调整)
|
|
|
|
|
|
List<Map<String, dynamic>> pathRecords = [];
|
|
|
|
|
|
if (state.pathData is List) {
|
2026-02-28 13:20:07 +08:00
|
|
|
|
// 第一步:安全转换为List,并过滤掉非Map的元素
|
|
|
|
|
|
final List<dynamic> rawList = state.pathData as List<dynamic>;
|
|
|
|
|
|
pathRecords = rawList
|
|
|
|
|
|
.where((item) => item is Map<String, dynamic>)
|
|
|
|
|
|
.cast<Map<String, dynamic>>()
|
|
|
|
|
|
.toList();
|
|
|
|
|
|
|
|
|
|
|
|
if (pathRecords.isNotEmpty) {
|
|
|
|
|
|
_selectedPlotPath = PlotDataPath(jsonData: pathRecords.first);
|
2026-02-28 14:02:04 +08:00
|
|
|
|
// ========== 核心新增:解析并转换坐标 ==========
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 1. 解析嵌套的jsonData
|
|
|
|
|
|
final String nestedJsonStr =
|
|
|
|
|
|
_selectedPlotPath!.jsonData['jsonData'] as String;
|
|
|
|
|
|
final Map<String, dynamic> nestedJson = jsonDecode(nestedJsonStr);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 提取path和outer原始坐标
|
|
|
|
|
|
final List<dynamic> rawPath = nestedJson['path'] as List<dynamic>;
|
|
|
|
|
|
final List<dynamic> rawOuter =
|
|
|
|
|
|
nestedJson['outer'] as List<dynamic>;
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 转换为GCJ02坐标
|
|
|
|
|
|
gcjPathPoints = convertPathToGCJ02(rawPath);
|
|
|
|
|
|
gcjOuterPoints = convertOuterToGCJ02(rawOuter);
|
|
|
|
|
|
//setState(() {});
|
|
|
|
|
|
|
|
|
|
|
|
print('转换后的path坐标数量:${gcjPathPoints.length}');
|
|
|
|
|
|
print('转换后的outer坐标数量:${gcjOuterPoints.length}');
|
|
|
|
|
|
} catch (e) {
|
|
|
|
|
|
print('解析/转换坐标失败:$e');
|
|
|
|
|
|
gcjPathPoints = [];
|
|
|
|
|
|
gcjOuterPoints = [];
|
|
|
|
|
|
}
|
2026-02-28 13:20:07 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
_selectedPlotPath = null; // 空列表时清空,避免残留旧数据
|
2026-02-28 14:02:04 +08:00
|
|
|
|
gcjPathPoints = [];
|
|
|
|
|
|
gcjOuterPoints = [];
|
2026-02-28 13:20:07 +08:00
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
_selectedPlotPath = null; // 非List类型时清空
|
2026-02-28 14:02:04 +08:00
|
|
|
|
gcjPathPoints = [];
|
|
|
|
|
|
gcjOuterPoints = [];
|
2026-02-28 09:55:14 +08:00
|
|
|
|
}
|
2026-02-28 14:02:04 +08:00
|
|
|
|
//print(
|
|
|
|
|
|
// _selectedPlotPath == null
|
|
|
|
|
|
// ? '无数据'
|
|
|
|
|
|
// : 'path坐标: ${jsonDecode(_selectedPlotPath!.jsonData['jsonData'] as String)}',
|
|
|
|
|
|
//);
|
2026-02-28 13:20:07 +08:00
|
|
|
|
String workMode =
|
|
|
|
|
|
jsonDecode(
|
|
|
|
|
|
_selectedPlotPath!.jsonData['jsonData'] as String,
|
|
|
|
|
|
)['planModel'] ==
|
|
|
|
|
|
WorkMode.bow.value
|
|
|
|
|
|
? "弓字模式"
|
|
|
|
|
|
: "自定义模式";
|
2026-02-28 14:02:04 +08:00
|
|
|
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
|
|
|
|
|
if (mounted && _mapController.mapEventStream != null) {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
// 触发地图重绘,确保坐标生效
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
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),
|
2026-02-28 13:20:07 +08:00
|
|
|
|
|
2026-02-28 09:55:14 +08:00
|
|
|
|
Text(
|
|
|
|
|
|
'作业模式:$workMode',
|
|
|
|
|
|
style: const TextStyle(
|
|
|
|
|
|
fontSize: 14,
|
|
|
|
|
|
color: Colors.grey,
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
IconButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
setState(() {
|
|
|
|
|
|
_isWorkPanelOpen = false;
|
|
|
|
|
|
_selectedPlot = null;
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
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,
|
2026-02-28 13:20:07 +08:00
|
|
|
|
height: 50,
|
2026-02-28 09:55:14 +08:00
|
|
|
|
child: ElevatedButton(
|
|
|
|
|
|
onPressed: state.isLoading || pathRecords.isEmpty
|
|
|
|
|
|
? null
|
|
|
|
|
|
: () {
|
|
|
|
|
|
// 作业逻辑:使用加载的pathData
|
|
|
|
|
|
debugPrint(
|
|
|
|
|
|
'开始作业:${_selectedPlot!.plotName},路径数据:$pathRecords',
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
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: 16,
|
|
|
|
|
|
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
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-27 13:17:09 +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();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
@override
|
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
|
final screenHeight = MediaQuery.of(context).size.height;
|
|
|
|
|
|
final menuHeight = 16 * 6; // 假设 VerticalFloatMenu 有 6 个选项,每个高度为 56
|
|
|
|
|
|
final maxTop = screenHeight - menuHeight;
|
2026-02-27 13:17:09 +08:00
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
return Scaffold(
|
2026-02-25 20:00:33 +08:00
|
|
|
|
body: SafeArea(
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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: 15,
|
|
|
|
|
|
maxZoom: 18,
|
2026-02-25 20:09:18 +08:00
|
|
|
|
// 禁止地图点击事件(避免和中心标冲突)
|
|
|
|
|
|
onTap: (_, __) {}, // 空实现,禁用地图点击响应
|
2026-02-25 13:41:23 +08:00
|
|
|
|
),
|
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',
|
2026-02-25 13:41:23 +08:00
|
|
|
|
),
|
2026-02-28 14:02:04 +08:00
|
|
|
|
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 13:41:23 +08:00
|
|
|
|
|
2026-02-25 20:09:18 +08:00
|
|
|
|
/// ========== 新增:中心标与历史打点的虚线连线 ==========
|
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
|
|
|
|
|
|
|
|
|
|
/// ========== 新增:历史打点的绿色标记 ==========
|
|
|
|
|
|
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 13:41:23 +08:00
|
|
|
|
),
|
2026-02-25 20:00:33 +08:00
|
|
|
|
|
2026-02-26 16:38:10 +08:00
|
|
|
|
// ========== 悬浮返回按钮(核心修改) ==========
|
|
|
|
|
|
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:09:18 +08:00
|
|
|
|
/// ========== 核心:地图正中心固定定位标 ==========
|
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 16:38:10 +08:00
|
|
|
|
|
2026-02-26 10:44:56 +08:00
|
|
|
|
//保存按钮
|
|
|
|
|
|
if (_saveBoxOpen)
|
|
|
|
|
|
Positioned(
|
2026-02-26 16:38:10 +08:00
|
|
|
|
right: 80,
|
2026-02-26 10:44:56 +08:00
|
|
|
|
top: maxTop > 10 ? 11 : maxTop,
|
2026-02-26 16:38:10 +08:00
|
|
|
|
child: Container(
|
|
|
|
|
|
width: 40,
|
|
|
|
|
|
height: 40,
|
2026-02-26 10:44:56 +08:00
|
|
|
|
decoration: BoxDecoration(
|
2026-02-26 16:38:10 +08:00
|
|
|
|
color: const Color(0xFF00C853),
|
|
|
|
|
|
shape: BoxShape.circle,
|
2026-02-26 10:44:56 +08:00
|
|
|
|
boxShadow: [
|
|
|
|
|
|
BoxShadow(
|
|
|
|
|
|
color: Colors.black12,
|
2026-02-26 16:38:10 +08:00
|
|
|
|
blurRadius: 4,
|
|
|
|
|
|
offset: const Offset(0, 2),
|
2026-02-26 10:44:56 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
|
|
|
|
|
),
|
|
|
|
|
|
child: IconButton(
|
|
|
|
|
|
onPressed: () {
|
|
|
|
|
|
_showSavePlotDialog();
|
|
|
|
|
|
},
|
2026-02-26 16:38:10 +08:00
|
|
|
|
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-26 16:38:10 +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) {
|
2026-02-27 13:17:09 +08:00
|
|
|
|
// 🔥 改动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;
|
2026-02-28 13:20:07 +08:00
|
|
|
|
_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-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,
|
|
|
|
|
|
onComplete: () {
|
2026-02-26 10:44:56 +08:00
|
|
|
|
debugPrint(
|
|
|
|
|
|
'操作完成回调:当前机器人模式=$_currentRobotMode,当前区域模式=$_currentAreaMode,当前作业模式:$_currentWorkMode,作业区域点:$_markedPoints,作业行距=$_workDistance,航线方向角=$_angle',
|
|
|
|
|
|
);
|
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: () {
|
2026-02-26 16:38:10 +08:00
|
|
|
|
_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) {
|
2026-02-26 16:38:10 +08:00
|
|
|
|
_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
|
2026-02-26 16:38:10 +08:00
|
|
|
|
: 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);
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
2026-02-26 16:38:10 +08:00
|
|
|
|
|
2026-02-27 13:17:09 +08:00
|
|
|
|
// 🔥 改动6:列表面板 - 使用BlocBuilder监听DevicesCubit状态
|
2026-02-26 10:44:56 +08:00
|
|
|
|
if (_isListBoxOpen)
|
2026-02-27 13:17:09 +08:00
|
|
|
|
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
|
|
|
|
),
|
2026-02-27 13:17:09 +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
|
|
|
|
),
|
2026-02-27 13:17:09 +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
|
|
|
|
),
|
2026-02-27 13:17:09 +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
|
|
|
|
),
|
2026-02-27 13:17:09 +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
|
|
|
|
),
|
2026-02-27 13:17:09 +08:00
|
|
|
|
)
|
|
|
|
|
|
: ListView.builder(
|
|
|
|
|
|
padding: const EdgeInsets.symmetric(
|
|
|
|
|
|
vertical: 8,
|
2026-02-26 10:44:56 +08:00
|
|
|
|
),
|
2026-02-27 13:17:09 +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(() {
|
|
|
|
|
|
// 因为Bloc数据更新后会自动重建,这里可省略
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
},
|
|
|
|
|
|
),
|
|
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-02-26 10:44:56 +08:00
|
|
|
|
),
|
2026-02-27 13:17:09 +08:00
|
|
|
|
),
|
|
|
|
|
|
],
|
2026-02-26 10:44:56 +08:00
|
|
|
|
),
|
2026-02-27 13:17:09 +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 13:41:23 +08:00
|
|
|
|
),
|
2026-02-25 20:00:33 +08:00
|
|
|
|
);
|
2026-02-25 13:41:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-28 14:02:04 +08:00
|
|
|
|
// 转换单个WGS84坐标到GCJ02
|
|
|
|
|
|
LatLng convertWGS84ToGCJ02(double lat, double lon) {
|
|
|
|
|
|
return wgs84ToGcj02(lat, lon);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 批量转换path坐标列表(path是lat+lon格式)
|
|
|
|
|
|
List<LatLng> convertPathToGCJ02(List<dynamic> pathList) {
|
|
|
|
|
|
return pathList.map((item) {
|
|
|
|
|
|
double lat = item['lat'] as double;
|
|
|
|
|
|
double lon = item['lon'] as double;
|
|
|
|
|
|
return convertWGS84ToGCJ02(lat, lon);
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 批量转换outer坐标列表(outer是lng+lat格式,注意顺序)
|
|
|
|
|
|
List<LatLng> convertOuterToGCJ02(List<dynamic> outerList) {
|
|
|
|
|
|
return outerList.map((item) {
|
|
|
|
|
|
double lon = item['lng'] as double;
|
|
|
|
|
|
double lat = item['lat'] as double;
|
|
|
|
|
|
return convertWGS84ToGCJ02(lat, lon); // 注意lat在前,lon=lon
|
|
|
|
|
|
}).toList();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-02-25 13:41:23 +08:00
|
|
|
|
/// =======================================================
|
|
|
|
|
|
/// 坐标转换: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);
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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;
|
2026-02-25 13:41:23 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
double _transformLat(double x, double y) {
|
2026-02-25 20:00:33 +08:00
|
|
|
|
double ret =
|
|
|
|
|
|
-100.0 +
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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;
|
2026-02-25 13:41:23 +08:00
|
|
|
|
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;
|
2026-02-25 13:41:23 +08:00
|
|
|
|
return ret;
|
|
|
|
|
|
}
|