修复高清地图(贴图)不支持手动滑动地理位置显示问题;修复地图显示上次设备位置的异常情况;修复打点保存少携带mode +orgid参数问题。

This commit is contained in:
2026-09-04 08:43:43 +08:00
parent 5141d6e83f
commit db81816b78
7 changed files with 112 additions and 18 deletions

View File

@@ -52,6 +52,9 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
// 🔥 当前监听的设备ID
String? _currentDeviceId;
// 🔥 当前 MQTT 机器状态监听的设备 SN(用于换设备时清空旧缓存)
String? _currentListeningSn;
// 🔥 MQTT机器状态收帧计数(用于链路日志)
int _mqttVehicleFrameCount = 0;
int _mqttLocationFrameCount = 0;
@@ -222,6 +225,20 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
debugPrint('${LogTime.now()} 🚀 [DeviceStatusBloc] 启动MQTT机器状态监听: $deviceSn');
// 🔥 换设备兜底:SN 变化时清空上一个设备的缓存与状态,
// 防止旧坐标通过 copyWith(base) 残留污染新设备数据(即使入口漏调 Reset 也能兜住)
if (_currentListeningSn != deviceSn) {
_throttleTimer?.cancel();
_throttleTimer = null;
_cachedStatus = null;
_cachedGps = null;
_currentListeningSn = deviceSn;
if (!isClosed) {
emit(DeviceStatusInitial());
}
debugPrint('${LogTime.now()} 🧹 [DeviceStatusBloc] 切换设备,已清空旧缓存: $deviceSn');
}
// 按SN订阅(数据源内部自动退旧订新、同SN去重)
_mowerRealtimeRepo.startListening(deviceSn: deviceSn).then((result) {
result.fold(
@@ -434,9 +451,18 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
'🔄 [DeviceStatusBloc] 收到重置事件:清空状态 - 当前状态:${state.runtimeType}',
);
// 🔥 关键修复:清空缓存与节流器,避免上一个设备的坐标/状态残留,
// 并通过 copyWith(base) 污染新设备的数据
_throttleTimer?.cancel();
_throttleTimer = null;
_cachedStatus = null;
_cachedGps = null;
_mqttVehicleFrameCount = 0;
_mqttLocationFrameCount = 0;
// 只 emit 初始状态,让 UI 清除旧设备的数据
emit(DeviceStatusInitial());
debugPrint('⚠️ [DeviceStatusBloc] 已emit DeviceStatusInitial');
debugPrint('⚠️ [DeviceStatusBloc] 已emit DeviceStatusInitial,缓存已清空');
}
Future<void> _handleDeviceStatusLoaded(
@@ -552,6 +578,7 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
// 🔥 重置设备ID
_currentDeviceId = null;
_currentListeningSn = null;
return Future.value();
}

View File

@@ -1,11 +1,13 @@
enum WorkMode {
bow(0), // 作业模式:bow,值为0
custom(2); // 作业模式:custom,值为2
bow(0, 'coverage'), // 作业模式:bow,值为0;后端 mode="coverage"(覆盖/区域)
custom(2, 'single_point'); // 作业模式:custom,值为2;后端 mode="single_point"(自定义/单点)
// 定义枚举的数值属性
final int value;
// 🔥 对齐 Web 端:workRecord.mode 字段(Web 注释"区域还是单点")
final String modeCode;
// 枚举构造函数(必须是const)
const WorkMode(this.value);
const WorkMode(this.value, this.modeCode);
}
enum RobotMode { point, robot }

View File

@@ -184,3 +184,23 @@ class GeoSuzhouOverlay extends StatelessWidget {
);
}
}
/// 全部场站高清叠加图层(一次性构建、复用实例,避免每次重建重复请求瓦片)。
/// 用法:在 FlutterMap 的 children 里 spread 进去 —— ...kAllStationTileLayers
/// 效果:滑到任意场站区域(zoom>=16)自动叠加对应高清图;每层各自 tileBounds,框外不请求。
final List<TileLayer> kAllStationTileLayers = kStationLayerBounds.entries
.map(
(entry) => TileLayer(
tileProvider: GeoWmsProvider(
layerName: entry.key,
bounds: entry.value,
maxLevel: 21,
),
userAgentPackageName: 'com.example.app',
minZoom: 16,
maxZoom: 21,
maxNativeZoom: 21,
tileBounds: entry.value,
),
)
.toList();

View File

@@ -936,6 +936,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
required String workRecordJson,
required String plotName,
required Object? siteId,
Object? orgId,
String? mode,
}) {
// debugPrint 单条过长会被 logcat 截断,长文本统一按行拆开输出
void printLines(String text) {
@@ -960,11 +962,24 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('📤 [请求头] $safeHeaders');
debugPrint(
'📤 [上下文] workName=$plotName, siteId=$siteId, '
'📤 [上下文] workName=$plotName, siteId=$siteId, orgId=$orgId, mode=$mode, '
'workMode=$_currentWorkMode, robotMode=$_currentRobotMode, '
'areaMode=$_currentAreaMode, planModel=${savePath['planModel']}',
);
// 🔥 mode / planModel 一致性自检:两者必须由同一个 _currentWorkMode 推导出来
final String expectMode = savePath['planModel'] == 0
? WorkMode.bow.modeCode
: WorkMode.custom.modeCode;
debugPrint(
'📤 [模式校验] planModel=${savePath['planModel']} → 期望 mode="$expectMode",'
'实际 mode="$mode" → ${expectMode == mode ? "✅ 一致" : "❌ 不一致,检查 _currentWorkMode"}',
);
debugPrint(
'📤 [场站校验] siteId=$siteId, orgId=$orgId → '
'${orgId != null ? "✅ orgId 已带上" : "⚠️ orgId 为 null,未写入请求体(selectedSite 为空?)"}',
);
// multipart 各 part 的元信息(不消费流)
debugPrint('📤 [multipart] part 数量: ${request.files.length}');
for (var i = 0; i < request.files.length; i++) {
@@ -1079,12 +1094,17 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
debugPrint('🔍 [保存地块] ====== _savePlotData 被调用, plotName=$plotName ======');
final loc = AppLocalizations.of(context);
// 🔥 V2 适配:使用 SiteCubit 获取站点ID(从 v2 首页场站选择获取,有默认值)
final siteId = sl<SiteCubit>().state.selectedSite?.id;
// selectedSite 就是各页面左上角下拉(SiteSelectorWidget)全局维护的那个场站
final selectedSite = sl<SiteCubit>().state.selectedSite;
final siteId = selectedSite?.id;
if (siteId == null) {
debugPrint('❌ [保存地块] siteId为空,提前返回!');
_showPageToast(message: '请先在场站列表中选择一个场站!', type: ToastType.error);
return;
}
// 🔥 对齐 Web 端 orgId: currentStation?.orgId
// ⚠️ 必须和 siteId 同源(selectedSite),不要取 AppUserCubit 里登录用户的 orgId
final orgId = selectedSite?.orgId;
for (var i = 0; i < typedPathList.length; i++) {
debugPrint(typedPathList[i].toString());
}
@@ -1126,6 +1146,19 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
// 🔥 jsonData 必须是对象,不能是字符串(后端 PlanPath 类型要求)
'jsonData': savePath,
};
// 🔥 对齐 Web 端 orgId(场站所属机构),与 siteId 同源于 SiteCubit.selectedSite
// Web 是 currentStation?.orgId,为 undefined 时 JSON.stringify 会丢掉该键,这里同样只在非空时加
if (orgId != null) {
workRecord['orgId'] = orgId;
}
// 🔥 对齐 Web 端 mode 字段(Web 注释"区域还是单点")
// 覆盖模式 → "coverage",自定义模式 → "single_point"
// ⚠️ 判定表达式必须和上面 planModel 用同一个,保证两字段永远同源不打架
final String modeCode = _currentWorkMode == WorkMode.bow
? WorkMode.bow.modeCode
: WorkMode.custom.modeCode;
workRecord['mode'] = modeCode;
debugPrint('📤 [保存地块] workRecord 附加字段: orgId=$orgId, mode=$modeCode');
final String workRecordJson = jsonEncode(workRecord);
final http.MultipartRequest request = http.MultipartRequest(
'POST',
@@ -1195,6 +1228,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
workRecordJson: workRecordJson,
plotName: plotName,
siteId: siteId,
orgId: orgId,
mode: modeCode,
);
// 7. 发送请求
@@ -2142,13 +2177,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
maxZoom: 19,
),
/// GeoServer 场站无人机高清影像叠加层(WGS84 基准,无偏移)
/// 按当前选中场站的 siteMapName 动态加载对应图层;需 zoom >= 16 才能看到
/// 当前 WMS 兜底(原生 EPSG:4326);公司补配 GWC EPSG:3857 后切 WMTS
/// 切换位置:geoserver_suzhou_layer.dart 的 kUseWmtsTile 开关
GeoSuzhouOverlay(
siteMapName: sl<SiteCubit>().state.selectedSite?.siteMapName,
),
/// GeoServer 全部场站无人机高清影像叠加层(WGS84 基准,无偏移)
/// 一次性叠加所有场站图层:滑到哪个场站区域(zoom>=16)就自动显示哪个高清图
/// 每层各自 tileBounds,框外瓦片不请求,开销极小;苏州等手动滑过去即可叠加
/// 当前 WMS 兜底(原生 EPSG:4326);公司补配 GWC EPSG:3857 后改 geoserver_suzhou_layer.dart
...kAllStationTileLayers,
//if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController),
// 绘制path折线(Line模式)

View File

@@ -13,6 +13,7 @@ import 'package:maibu_satabot_v2/core/network/tcp/tcp_client.dart';
import 'package:maibu_satabot_v2/features/devices/domain/entities/device_entity.dart';
import 'package:maibu_satabot_v2/features/devices/domain/repositories/device_repository.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_bloc.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_event.dart';
import 'package:maibu_satabot_v2/features/devices/presentation/bloc/device_status_state.dart';
import 'package:maibu_satabot_v2/features/remote_control/data/models/running_status_model.dart';
import 'package:maibu_satabot_v2/features/remote_control/domain/usecase/request_control_permission_usecase.dart';
@@ -1029,6 +1030,10 @@ class RemoteControlCubit extends Cubit<RemoteControlState> {
emit(state.copyWith(targetDevice: device));
debugPrint('✅ [RemoteControl] targetDevice状态已更新');
// 🔥 关键修复:切换设备时重置 DeviceStatusBloc 状态并清空缓存,
// 清除上一个设备的坐标/状态,防止路径规划显示旧车位置(与 switchDevice 对齐)
deviceStatusBloc.add(DeviceStatusReset());
// 🔥 MQTT机器状态订阅:切换设备时订阅新设备的topic(与DevicesCubit.switchDevice相同逻辑)
debugPrint('📡 [RemoteControl] 开始MQTT机器状态订阅: ${device.deviceName}');
deviceStatusBloc.startMqttRealtimeListening(device.deviceName);

View File

@@ -41,10 +41,15 @@ class RobotStatusBar extends StatelessWidget {
);
},
child: StreamBuilder<DeviceStatusState>(
stream: GetIt.I<DeviceStatusBloc>().stream,
initialData: GetIt.I<DeviceStatusBloc>().state,
// 🔥 MQTT 数据源(mower/{sn}/property/realtime/post)尚未推送,
// 暂时不订阅 DeviceStatusBloc 的状态流,下面四个字段固定显示 '--'。
// 数据源确认后把 stream/initialData 两行换回下面的注释版本即可。
stream: null,
initialData: null,
// stream: GetIt.I<DeviceStatusBloc>().stream,
// initialData: GetIt.I<DeviceStatusBloc>().state,
builder: (context, snapshot) {
final state = snapshot.data;
// 🔥 数据源接入后恢复:final state = snapshot.data;
String speed = '--';
String mode = '--';
@@ -53,6 +58,7 @@ class RobotStatusBar extends StatelessWidget {
Color signalColor = context.appColors.textTertiary;
Color batteryColor = context.appColors.textTertiary;
/* 🔥 数据源接入后恢复:从 DeviceStatusUpdated 中解析实际字段
if (state is DeviceStatusUpdated) {
final s = state.status;
speed = s.leftMeasureSpeed.toStringAsFixed(0);
@@ -83,6 +89,7 @@ class RobotStatusBar extends StatelessWidget {
batteryColor = context.appColors.danger;
}
}
*/
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),