修复高清地图(贴图)不支持手动滑动地理位置显示问题;修复地图显示上次设备位置的异常情况;修复打点保存少携带mode +orgid参数问题。
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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模式)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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),
|
||||
|
||||
Reference in New Issue
Block a user