加上tracepoint版本
This commit is contained in:
@@ -18,6 +18,7 @@ class RunningStatusPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _RunningStatusPageState extends State<RunningStatusPage> {
|
||||
|
||||
final _dispatcher = sl<NetMessageDispatcher>();
|
||||
late StreamSubscription<RawPacket> _sub;
|
||||
|
||||
|
||||
343
lib/features/home/presentation/widgets/common/tracepoint.dart
Normal file
343
lib/features/home/presentation/widgets/common/tracepoint.dart
Normal file
@@ -0,0 +1,343 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:async';
|
||||
|
||||
// ======================== 枚举定义 ========================
|
||||
/// 轨迹模式枚举
|
||||
enum TPMode {
|
||||
NAVIGATION, // 导航模式:绘制规划已完成路径和当前路径
|
||||
LOCATION, // 定位模式:仅绘制当前点
|
||||
TRACK, // 轨迹模式:实时绘制历史轨迹
|
||||
}
|
||||
|
||||
/// 导航模式下的操作动作枚举
|
||||
enum TPAction {
|
||||
UPDATE, // 更新当前点
|
||||
ADD, // 添加完成点
|
||||
}
|
||||
|
||||
// ======================== 尝试锁实现 ========================
|
||||
class TryLock {
|
||||
bool _locked = false;
|
||||
|
||||
/// 尝试获取锁
|
||||
/// return: true-获取成功,false-已被锁定
|
||||
bool tryLock() {
|
||||
if (_locked) return false;
|
||||
_locked = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 释放锁
|
||||
void release() {
|
||||
_locked = false;
|
||||
}
|
||||
|
||||
/// 检查是否已锁定
|
||||
bool get isLocked => _locked;
|
||||
}
|
||||
|
||||
// ======================== 环形队列实现 ========================
|
||||
class CircQueue<T> {
|
||||
final int capacity; // 队列容量
|
||||
final List<T?> buffer; // 存储缓冲区
|
||||
int head = 0; // 队头索引
|
||||
int tail = 0; // 队尾索引
|
||||
bool isFull = false; // 队列满标志
|
||||
final bool deepCopy; // 是否深拷贝元素
|
||||
final int maxMemoryBytes; // 最大内存限制(字节)
|
||||
int currentMemoryBytes = 0; // 当前已使用内存
|
||||
|
||||
CircQueue(
|
||||
this.capacity, {
|
||||
this.deepCopy = true,
|
||||
this.maxMemoryBytes = 1024 * 1024, // 默认1MB
|
||||
}) : buffer = List<T?>.filled(capacity, null);
|
||||
|
||||
/// 清空队列
|
||||
void clear() {
|
||||
head = tail;
|
||||
isFull = false;
|
||||
currentMemoryBytes = 0;
|
||||
}
|
||||
|
||||
/// 判断队列是否为空
|
||||
bool isEmpty() {
|
||||
return head == tail && !isFull;
|
||||
}
|
||||
|
||||
/// 判断队列是否已满
|
||||
bool isFullFn() {
|
||||
return isFull;
|
||||
}
|
||||
|
||||
/// 估算对象大小(JSON序列化后的字节长度)
|
||||
int _estimateSize(T item) {
|
||||
try {
|
||||
return utf8.encode(jsonEncode(item)).length;
|
||||
} catch (e) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// 深拷贝对象
|
||||
T? _deepClone(T? item) {
|
||||
if (!deepCopy || item == null) return item;
|
||||
try {
|
||||
return jsonDecode(jsonEncode(item)) as T;
|
||||
} catch (e) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
/// 入队操作
|
||||
bool enter(T item) {
|
||||
final itemSize = _estimateSize(item);
|
||||
// 检查队列满或内存超限
|
||||
if (isFullFn() || (currentMemoryBytes + itemSize > maxMemoryBytes)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final clone = _deepClone(item);
|
||||
buffer[tail] = clone;
|
||||
tail = (tail + 1) % capacity;
|
||||
currentMemoryBytes += itemSize;
|
||||
|
||||
if (tail == head) isFull = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 出队操作
|
||||
T? out() {
|
||||
if (isEmpty()) return null;
|
||||
|
||||
final item = buffer[head];
|
||||
if (item != null) {
|
||||
currentMemoryBytes -= _estimateSize(item);
|
||||
}
|
||||
head = (head + 1) % capacity;
|
||||
isFull = false;
|
||||
return _deepClone(item);
|
||||
}
|
||||
|
||||
/// 丢弃指定数量的元素
|
||||
bool discard(int len) {
|
||||
for (int i = 0; i < len; i++) {
|
||||
if (isEmpty()) return false;
|
||||
|
||||
final index = (tail - 1 + capacity) % capacity;
|
||||
final item = buffer[index];
|
||||
if (item != null) {
|
||||
currentMemoryBytes -= _estimateSize(item);
|
||||
}
|
||||
tail = index;
|
||||
isFull = false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 获取队列元素个数
|
||||
int getDepth() {
|
||||
return isFull ? capacity : (tail + capacity - head) % capacity;
|
||||
}
|
||||
|
||||
/// 异步入队(支持超时)
|
||||
Future<bool> send(T item, {int timeoutMs = 0}) async {
|
||||
final start = DateTime.now().millisecondsSinceEpoch;
|
||||
while (!enter(item)) {
|
||||
if (timeoutMs > 0 &&
|
||||
DateTime.now().millisecondsSinceEpoch - start >= timeoutMs) {
|
||||
return false;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// 异步出队(支持超时)
|
||||
Future<T?> recv({int timeoutMs = 0}) async {
|
||||
final start = DateTime.now().millisecondsSinceEpoch;
|
||||
while (true) {
|
||||
final item = out();
|
||||
if (item != null) return item;
|
||||
if (timeoutMs > 0 &&
|
||||
DateTime.now().millisecondsSinceEpoch - start >= timeoutMs) {
|
||||
return null;
|
||||
}
|
||||
await Future.delayed(const Duration(milliseconds: 1));
|
||||
}
|
||||
}
|
||||
|
||||
/// 转换为列表(用于调试/序列化)
|
||||
List<T?> toList() {
|
||||
final result = <T?>[];
|
||||
int i = head;
|
||||
int count = getDepth();
|
||||
while (count-- > 0) {
|
||||
result.add(_deepClone(buffer[i]));
|
||||
i = (i + 1) % capacity;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
class TracePoint<T> {
|
||||
// 私有成员变量
|
||||
late final CircQueue<T> _queue;
|
||||
List<T> _tracePoint = [];
|
||||
int _completePointIndex = 0;
|
||||
late final TryLock _lock;
|
||||
TPMode _mode = TPMode.LOCATION;
|
||||
|
||||
// 公开的回调(无下划线,外部可访问)
|
||||
void Function(T)? onCurrentPointUpdated;
|
||||
void Function(T)? onCompletePointAdded;
|
||||
|
||||
// 测试用方法
|
||||
bool tryLock() => _lock.tryLock();
|
||||
void release() => _lock.release();
|
||||
|
||||
/// 构造函数
|
||||
TracePoint({int queueCapacity = 5, int maxMemoryBytes = 1024 * 1024}) {
|
||||
_queue = CircQueue<T>(
|
||||
queueCapacity,
|
||||
deepCopy: true,
|
||||
maxMemoryBytes: maxMemoryBytes,
|
||||
);
|
||||
_lock = TryLock();
|
||||
reset();
|
||||
}
|
||||
|
||||
/// 复位所有状态
|
||||
void reset() {
|
||||
_queue.clear();
|
||||
_tracePoint = [];
|
||||
_completePointIndex = 0;
|
||||
_lock.release();
|
||||
}
|
||||
|
||||
/// 设置工作模式
|
||||
void setMode(TPMode mode) {
|
||||
reset();
|
||||
_mode = mode;
|
||||
}
|
||||
|
||||
/// 添加/更新轨迹点
|
||||
void upsert(T point, [TPAction act = TPAction.UPDATE]) {
|
||||
switch (_mode) {
|
||||
case TPMode.NAVIGATION:
|
||||
act == TPAction.UPDATE
|
||||
? _updateCurrentPoint(point)
|
||||
: _addCompletePoint(point);
|
||||
break;
|
||||
case TPMode.LOCATION:
|
||||
_updateCurrentPoint(point);
|
||||
break;
|
||||
case TPMode.TRACK:
|
||||
_addCompletePoint(point);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/// 获取轨迹点列表(深拷贝,线程安全)
|
||||
List<T>? getTracePoint() {
|
||||
if (_lock.tryLock()) {
|
||||
final trace = List<T>.from(_tracePoint);
|
||||
_lock.release();
|
||||
return trace;
|
||||
} else {
|
||||
print('getTracePoint: lock!!!');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// 更新当前点(私有方法)
|
||||
void _updateCurrentPoint(T point) {
|
||||
if (point == null) {
|
||||
print('updateCurrentPoint: point is NULL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lock.tryLock()) {
|
||||
try {
|
||||
switch (_mode) {
|
||||
case TPMode.NAVIGATION:
|
||||
if (_tracePoint.isEmpty) {
|
||||
print(
|
||||
'updateCurrentPoint: tracePoint is empty , wait first completed point, discard current point: $point',
|
||||
);
|
||||
} else {
|
||||
_tracePoint = _tracePoint.sublist(0, _completePointIndex);
|
||||
_tracePoint.add(point);
|
||||
// 触发公开的回调(无下划线)
|
||||
onCurrentPointUpdated?.call(point);
|
||||
}
|
||||
break;
|
||||
case TPMode.LOCATION:
|
||||
if (_tracePoint.isEmpty) {
|
||||
_tracePoint.add(point);
|
||||
} else {
|
||||
_tracePoint[0] = point;
|
||||
}
|
||||
// 触发公开的回调(无下划线)
|
||||
onCurrentPointUpdated?.call(point);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
_lock.release();
|
||||
}
|
||||
} else {
|
||||
print('updateCurrentPoint: lock!!!, discard current point: $point');
|
||||
}
|
||||
}
|
||||
|
||||
/// 添加完成点(私有方法)
|
||||
void _addCompletePoint(T point) {
|
||||
if (point == null) {
|
||||
print('addCompletePoint: point is NULL');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_lock.tryLock()) {
|
||||
try {
|
||||
while (!_queue.isEmpty()) {
|
||||
final queuedPoint = _queue.out();
|
||||
if (queuedPoint != null) {
|
||||
_addPoint(queuedPoint);
|
||||
}
|
||||
}
|
||||
_addPoint(point);
|
||||
// 触发公开的回调(无下划线)
|
||||
onCompletePointAdded?.call(point);
|
||||
} catch (e) {
|
||||
print('addCompletePoint failure: $e');
|
||||
} finally {
|
||||
_lock.release();
|
||||
}
|
||||
} else {
|
||||
_queue.enter(point);
|
||||
print('addCompletePoint: lock!!!, enter queue, wait disposing: $point');
|
||||
}
|
||||
}
|
||||
|
||||
/// 内部添加点逻辑(私有方法)
|
||||
void _addPoint(T point) {
|
||||
if (point == null) {
|
||||
print('point NULL:');
|
||||
return;
|
||||
}
|
||||
|
||||
final hasCurrentPoint =
|
||||
_tracePoint.isNotEmpty && _completePointIndex < _tracePoint.length;
|
||||
|
||||
if (hasCurrentPoint) {
|
||||
_tracePoint = _tracePoint.sublist(0, _completePointIndex);
|
||||
_tracePoint.insert(_completePointIndex, point);
|
||||
} else {
|
||||
_tracePoint.add(point);
|
||||
}
|
||||
|
||||
_completePointIndex++;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
import 'dart:async';
|
||||
import 'dart:convert';
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_map/flutter_map.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:latlong2/latlong.dart';
|
||||
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';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/BottomDirectionLine.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/enum.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/common/tracepoint.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/CenterLocation.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/map/HeadingPointer.dart';
|
||||
import 'package:maibu_satabot_v2/features/home/presentation/widgets/startpoint_area.dart';
|
||||
|
||||
import '../path_list_pages.dart';
|
||||
|
||||
// 定义轨迹点类型(经纬度)
|
||||
typedef PlotPoint = LatLng;
|
||||
|
||||
class PlotData {
|
||||
final String id; // 唯一ID,用于删除
|
||||
final String plotName; // 地块名称
|
||||
@@ -28,8 +36,13 @@ class MapPageEnterprise extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
final _dispatcher = sl<NetMessageDispatcher>();
|
||||
late StreamSubscription<RawPacket> _sub;
|
||||
|
||||
final MapController _mapController = MapController();
|
||||
final TextEditingController _plotNameController = TextEditingController();
|
||||
// 初始化轨迹管理器(泛型指定为LatLng)
|
||||
late final TracePoint<PlotPoint> _traceManager;
|
||||
|
||||
LatLng? _currentLatLng;
|
||||
StreamSubscription<Position>? _positionSub;
|
||||
@@ -38,6 +51,10 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
bool _directionBoxOpen = false; //航线面板显示/隐藏
|
||||
final bool _saveBoxOpen = true; //保存按钮显示/隐藏
|
||||
bool _isListBoxOpen = false; //列表面板显示/隐藏
|
||||
// 新增:存储选中的地块(null表示未选中)
|
||||
PlotData? _selectedPlot;
|
||||
// 新增:控制底部作业面板显示
|
||||
bool _isWorkPanelOpen = false;
|
||||
|
||||
RobotMode _currentRobotMode = RobotMode.point; //打点模式
|
||||
AreaMode _currentAreaMode = AreaMode.work; //作业区域还是障碍物区域
|
||||
@@ -47,6 +64,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
final List<LatLng> _markedPoints = []; // 存储所有打点坐标
|
||||
LatLng _mapCenter = const LatLng(39.9042, 116.4074);
|
||||
final double _headingAngle = 0.0; // 当前机器航向角(单位:度)
|
||||
List<LatLng>? _currentTracePoints; //收到机器的点坐标
|
||||
|
||||
//打点
|
||||
double _workDistance = 0.0; // 作业行距(单位:米)
|
||||
@@ -62,17 +80,34 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
PlotData(id: '2', plotName: '东地块(二号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '3', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '4', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '5', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '6', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '7', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
PlotData(id: '8', plotName: '西地块(三号田)', imageUrl: ''),
|
||||
];
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_traceManager = TracePoint<PlotPoint>();
|
||||
_initLocationEnterprise();
|
||||
_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);
|
||||
|
||||
// 监听地图移动事件,实时更新连线
|
||||
_mapController.mapEventStream.listen((event) {
|
||||
if (event is MapEventMove || event is MapEventMoveEnd) {
|
||||
@@ -98,6 +133,38 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
});
|
||||
}
|
||||
|
||||
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 (_) {
|
||||
// 解析失败时忽略,避免崩溃
|
||||
}
|
||||
}
|
||||
|
||||
void _showSavePlotDialog() {
|
||||
// 清空上次的输入内容
|
||||
_plotNameController.clear();
|
||||
@@ -290,69 +357,114 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
@override
|
||||
void dispose() {
|
||||
_positionSub?.cancel();
|
||||
_traceManager.reset();
|
||||
_mapController.dispose();
|
||||
_sub?.cancel();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// 模拟添加完成点
|
||||
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);
|
||||
}
|
||||
|
||||
// 在State类中添加列表项构建方法
|
||||
Widget _buildPlotListItem(PlotData plot) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: Colors.grey[100]!),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 地块图片
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
plot.imageUrl,
|
||||
// 网络图片替换为:Image.network(plot.imageUrl, ...)
|
||||
width: 40,
|
||||
height: 40,
|
||||
fit: BoxFit.cover,
|
||||
// 图片加载失败占位
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.image_outlined, color: Colors.grey),
|
||||
);
|
||||
return GestureDetector(
|
||||
// 核心:点击列表项触发选中逻辑
|
||||
onTap: () {
|
||||
setState(() {
|
||||
// 1. 选中当前地块
|
||||
_selectedPlot = plot;
|
||||
// 2. 关闭列表抽屉
|
||||
_isListBoxOpen = false;
|
||||
// 3. 打开底部作业面板
|
||||
_isWorkPanelOpen = true;
|
||||
});
|
||||
|
||||
//// 可选:提示选中成功
|
||||
//ScaffoldMessenger.of(context).showSnackBar(
|
||||
// SnackBar(
|
||||
// content: Text('已选中地块:${plot.plotName}'),
|
||||
// duration: const Duration(seconds: 1),
|
||||
// ),
|
||||
//);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
padding: const EdgeInsets.all(12),
|
||||
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, // 选中项高亮边框
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
// 1. 地块图片(原有代码)
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
plot.imageUrl,
|
||||
width: 60,
|
||||
height: 60,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Container(
|
||||
width: 60,
|
||||
height: 60,
|
||||
color: Colors.grey[200],
|
||||
child: const Icon(Icons.image_outlined, color: Colors.grey),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 2. 地块名称(原有代码)
|
||||
Expanded(
|
||||
child: Text(
|
||||
plot.plotName,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 删除按钮(原有代码)
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
_showDeleteConfirmDialog(plot);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.redAccent,
|
||||
size: 20,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 12),
|
||||
|
||||
// 2. 地块名称(占满剩余空间)
|
||||
Expanded(
|
||||
child: Text(
|
||||
plot.plotName,
|
||||
style: const TextStyle(fontSize: 16, color: Colors.black87),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 3. 删除按钮
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// 弹出确认删除对话框
|
||||
_showDeleteConfirmDialog(plot);
|
||||
},
|
||||
icon: const Icon(
|
||||
Icons.delete_outline,
|
||||
color: Colors.redAccent,
|
||||
size: 20,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: const BoxConstraints(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -398,6 +510,139 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
}
|
||||
|
||||
// 注意:方法名改为 _buildWorkPanel(因为实际是作业面板,不是列表,避免混淆)
|
||||
Widget _buildWorkPanel() {
|
||||
// 核心修复1:空安全判断 - 无选中地块时返回空容器
|
||||
if (_selectedPlot == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min, // 关键:只占用内容高度,不挤压其他组件
|
||||
children: [
|
||||
// 1. 选中地块信息行
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center, // 修复:垂直居中对齐
|
||||
children: [
|
||||
// 地块图片
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Image.asset(
|
||||
_selectedPlot!.imageUrl,
|
||||
width: 80,
|
||||
height: 80,
|
||||
fit: BoxFit.cover,
|
||||
// 核心修复2:图片加载错误占位优化
|
||||
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: Text(
|
||||
_selectedPlot!.plotName,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
|
||||
// 关闭按钮(修复:减少点击区域,避免误触)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: 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,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: () {
|
||||
// 核心修复3:再次空安全校验(双重保险)
|
||||
if (_selectedPlot != null) {
|
||||
//_startWork(_selectedPlot!);
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: const Color(0xFF00C853), // 绿色主题
|
||||
foregroundColor: Colors.white, // 文字颜色(Flutter 3.0+ 推荐)
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
elevation: 2,
|
||||
shadowColor: Colors.black12,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 0,
|
||||
horizontal: 16,
|
||||
),
|
||||
minimumSize: const Size(double.infinity, 48),
|
||||
),
|
||||
child: const Text(
|
||||
'开始作业',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.0,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final screenHeight = MediaQuery.of(context).size.height;
|
||||
@@ -607,6 +852,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
),
|
||||
),
|
||||
//开始作业面板
|
||||
|
||||
// 底部操作面板
|
||||
if (_isPanelOpen)
|
||||
@@ -806,6 +1052,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
if (_isWorkPanelOpen) _buildWorkPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user