蓝牙绑定设备+场站切换支持高清图层的一对一显示+设备实时状态的部分显示
This commit is contained in:
@@ -231,8 +231,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.target_speed') +
|
||||
"\n(rpm)",
|
||||
),
|
||||
_buildTableCell(status.leftTargetSpeed.toStringAsFixed(2)),
|
||||
_buildTableCell(status.rightTargetSpeed.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -243,8 +243,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.measure_speed') +
|
||||
"\n(rpm)",
|
||||
),
|
||||
_buildTableCell(status.leftMeasureSpeed.toStringAsFixed(2)),
|
||||
_buildTableCell(status.rightMeasureSpeed.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -255,8 +255,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.current') +
|
||||
"(A)",
|
||||
),
|
||||
_buildTableCell(status.leftCurrent.toStringAsFixed(2)),
|
||||
_buildTableCell(status.rightCurrent.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -267,8 +267,8 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.motor_temp') +
|
||||
"(°C)",
|
||||
),
|
||||
_buildTableCell(status.leftMotorTemp.toStringAsFixed(2)),
|
||||
_buildTableCell(status.rightMotorTemp.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
],
|
||||
@@ -318,7 +318,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.pitch_angle') +
|
||||
"(°)",
|
||||
),
|
||||
_buildTableCell(status.pitch.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -329,7 +329,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.roll_angle') +
|
||||
"(°)",
|
||||
),
|
||||
_buildTableCell(status.roll.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -351,7 +351,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.battery') +
|
||||
"(%)",
|
||||
),
|
||||
_buildTableCell(status.battery),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -362,7 +362,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.chip_temp') +
|
||||
"(°C)",
|
||||
),
|
||||
_buildTableCell(status.chipTemp.toStringAsFixed(2)),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -373,7 +373,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
).translate('running_status.knife_speed') +
|
||||
"(rpm)",
|
||||
),
|
||||
_buildTableCell(status.knifeCuttingSpeed),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -383,11 +383,7 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
context,
|
||||
).translate('running_status.control_mode'),
|
||||
),
|
||||
_buildTableCell(
|
||||
AppLocalizations.of(context).translate(
|
||||
_getControlModeKey(int.parse(status.controlMode)),
|
||||
),
|
||||
),
|
||||
_buildTableCell('--'),
|
||||
],
|
||||
),
|
||||
TableRow(
|
||||
@@ -524,28 +520,13 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
// Status Bar
|
||||
BlocBuilder<DeviceStatusBloc, DeviceStatusState>(
|
||||
builder: (context, state) {
|
||||
String qual = _isDataTimeout ? '--' : '--';
|
||||
String satelliteCnt = _isDataTimeout ? '--' : '--';
|
||||
String headingStatus = _isDataTimeout ? "--" : "--";
|
||||
|
||||
// 🔥 headingStatus/satelliteCnt 来自 realtime/post(尚未推送)
|
||||
String satelliteCnt = '--';
|
||||
String headingStatus = '--';
|
||||
// 🔥 定位质量来自 location/post 的 fix_status
|
||||
String qual = '--';
|
||||
if (state is DeviceStatusUpdated) {
|
||||
headingStatus = state.status.headingStatus == 0
|
||||
? AppLocalizations.of(
|
||||
context,
|
||||
).translate('running_status.not_initialized')
|
||||
: AppLocalizations.of(
|
||||
context,
|
||||
).translate('running_status.initialized');
|
||||
int qualValue = 0;
|
||||
try {
|
||||
qualValue = int.parse(state.status.qual.toString());
|
||||
} catch (e) {
|
||||
qualValue = 0;
|
||||
}
|
||||
qual = AppLocalizations.of(
|
||||
context,
|
||||
).translate(_getLocationQualityKey(qualValue));
|
||||
satelliteCnt = state.status.satelliteCnt.toString();
|
||||
qual = _getLocationQualityText(state.status.qual);
|
||||
}
|
||||
|
||||
return Container(
|
||||
@@ -1173,15 +1154,20 @@ class _DeviceStatusModalState extends State<DeviceStatusModal> {
|
||||
);
|
||||
}
|
||||
|
||||
String _getLocationQualityKey(int qualValue) {
|
||||
/// 🔥 fix_status 枚举翻译(来自 MQTT location/post)
|
||||
String _getLocationQualityText(int qualValue) {
|
||||
switch (qualValue) {
|
||||
case 0:
|
||||
return 'running_status.invalid';
|
||||
case 4:
|
||||
case 5:
|
||||
return 'running_status.valid';
|
||||
default:
|
||||
return 'common.unknown';
|
||||
case 0: return '无定位';
|
||||
case 1: return '单点定位';
|
||||
case 2: return '差分定位';
|
||||
case 3: return '惯性导航';
|
||||
case 4: return 'RTK固定解';
|
||||
case 5: return 'RTK浮点解';
|
||||
case 6: return '单点(无航向)';
|
||||
case 7: return '差分(无航向)';
|
||||
case 8: return 'RTK固定(无航向)';
|
||||
case 9: return 'RTK浮点(无航向)';
|
||||
default: return '--';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
lib/core/env/env_config.dart
vendored
2
lib/core/env/env_config.dart
vendored
@@ -1,7 +1,7 @@
|
||||
class EnvConfig {
|
||||
static const String environment = String.fromEnvironment(
|
||||
'ENV',
|
||||
defaultValue: 'prod',
|
||||
defaultValue: 'test',
|
||||
);
|
||||
|
||||
static String get sentryDsn {
|
||||
|
||||
@@ -14,6 +14,10 @@ class MowerLocationEntity extends Equatable {
|
||||
|
||||
/// 速度
|
||||
final double? speed;
|
||||
|
||||
/// 定位质量(fix_status)
|
||||
final String? fixStatus;
|
||||
|
||||
final String? timestamp;
|
||||
|
||||
/// 原始报文,便于解析格式确认后补充字段
|
||||
@@ -25,6 +29,7 @@ class MowerLocationEntity extends Equatable {
|
||||
this.altitude,
|
||||
this.heading,
|
||||
this.speed,
|
||||
this.fixStatus,
|
||||
this.timestamp,
|
||||
this.rawData = const {},
|
||||
});
|
||||
@@ -38,6 +43,7 @@ class MowerLocationEntity extends Equatable {
|
||||
altitude: _toDouble(json['altitude'] ?? json['height']),
|
||||
heading: _toDouble(json['heading'] ?? json['yaw']),
|
||||
speed: _toDouble(json['speed']),
|
||||
fixStatus: json['fix_status']?.toString(),
|
||||
timestamp: json['timestamp']?.toString(),
|
||||
rawData: json,
|
||||
);
|
||||
@@ -50,5 +56,5 @@ class MowerLocationEntity extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [latitude, longitude, altitude, heading, speed, timestamp];
|
||||
List<Object?> get props => [latitude, longitude, altitude, heading, speed, fixStatus, timestamp];
|
||||
}
|
||||
|
||||
@@ -262,12 +262,13 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
// 更新 _cachedGps
|
||||
_cachedGps = GPSEntity(location.latitude!, location.longitude!);
|
||||
|
||||
// 更新 _cachedStatus 中的航向角和坐标(复用已有缓存或新建空壳)
|
||||
// 更新 _cachedStatus 中的航向角、坐标、定位质量(复用已有缓存或新建空壳)
|
||||
final base = _cachedStatus ?? RunningStatusEntity();
|
||||
_cachedStatus = base.copyWith(
|
||||
latitude: location.latitude!,
|
||||
longitude: location.longitude!,
|
||||
yaw: location.heading ?? base.yaw,
|
||||
qual: _fixStatusToQual(location.fixStatus, base.qual),
|
||||
);
|
||||
|
||||
// 500ms节流:与 TCP 0x02 时代逻辑一致
|
||||
@@ -397,6 +398,24 @@ class DeviceStatusBloc extends Bloc<DeviceStatusEvent, DeviceStatusState> {
|
||||
_currentDeviceId = deviceId;
|
||||
}
|
||||
|
||||
/// 🔥 将 MQTT location/post 的 fix_status 字符串映射为 qual 整数值
|
||||
int _fixStatusToQual(String? fixStatus, int fallback) {
|
||||
if (fixStatus == null) return fallback;
|
||||
switch (fixStatus) {
|
||||
case 'no_fix': return 0;
|
||||
case 'single': return 1;
|
||||
case 'dgnss': return 2;
|
||||
case 'ins': return 3;
|
||||
case 'rtk_fixed': return 4;
|
||||
case 'rtk_float': return 5;
|
||||
case 'single_no_heading': return 6;
|
||||
case 'dgnss_no_heading': return 7;
|
||||
case 'rtk_fixed_no_heading': return 8;
|
||||
case 'rtk_float_no_heading': return 9;
|
||||
default: return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
// 🔥 节流发射:500ms到期后发射缓存的最新数据
|
||||
void _emitCachedStatus() {
|
||||
if (_cachedStatus != null && _cachedGps != null && !isClosed) {
|
||||
|
||||
@@ -7,17 +7,46 @@ import 'package:latlong2/latlong.dart';
|
||||
/// GeoServer 基地址(公司端)
|
||||
const String kGeoserverBase = 'http://1.95.137.212:7900/geoserver';
|
||||
|
||||
/// 首期苏州区域图层标识
|
||||
const String kSuzhouLayerName = 'workSpace:suzhou';
|
||||
/// 各场站高清图层的真实范围(取自 GeoServer GetCapabilities,原生 EPSG:4326)
|
||||
/// key = 后端下发的 siteMapName(形如 workSpace:suzhou)
|
||||
final Map<String, LatLngBounds> kStationLayerBounds = {
|
||||
'workSpace:liaoding': LatLngBounds(
|
||||
const LatLng(41.19998369457442, 120.59074514010273),
|
||||
const LatLng(41.202002066385475, 120.59343722827124),
|
||||
),
|
||||
'workSpace:loudi': LatLngBounds(
|
||||
const LatLng(27.846682995656035, 111.52814279952669),
|
||||
const LatLng(27.847939704917945, 111.52928081352842),
|
||||
),
|
||||
'workSpace:neimeng': LatLngBounds(
|
||||
const LatLng(41.321581124009896, 112.62468603332228),
|
||||
const LatLng(41.325573311689986, 112.63034797059984),
|
||||
),
|
||||
'workSpace:result': LatLngBounds(
|
||||
const LatLng(27.36490549568922, 116.84490756003203),
|
||||
const LatLng(27.36673908366302, 116.84661654295478),
|
||||
),
|
||||
'workSpace:sanxiamen': LatLngBounds(
|
||||
const LatLng(34.732748259670494, 111.43051478686061),
|
||||
const LatLng(34.734205267863345, 111.43201022647355),
|
||||
),
|
||||
'workSpace:suzhou': LatLngBounds(
|
||||
const LatLng(31.082450793459877, 120.83906484264836),
|
||||
const LatLng(31.084141983135456, 120.84164596446476),
|
||||
),
|
||||
'workSpace:zhongke': LatLngBounds(
|
||||
const LatLng(32.0406442971753, 120.8057183175354),
|
||||
const LatLng(32.045144217672636, 120.81100452381844),
|
||||
),
|
||||
};
|
||||
|
||||
/// 苏州图层实际覆盖范围(取自 GetCapabilities,约 250m × 190m 的小地块)
|
||||
final LatLngBounds kSuzhouBounds = LatLngBounds(
|
||||
const LatLng(31.082450793459877, 120.83906484264836),
|
||||
const LatLng(31.084141983135456, 120.84164596446476),
|
||||
);
|
||||
|
||||
/// 场站中心点(真实地块中心)
|
||||
const LatLng kSuzhouCenter = LatLng(31.083296, 120.840355);
|
||||
/// 取某场站图层的范围中心点(用作进场默认定位);未知/为空返回 null
|
||||
LatLng? stationLayerCenter(String? siteMapName) {
|
||||
if (siteMapName == null) return null;
|
||||
final b = kStationLayerBounds[siteMapName];
|
||||
if (b == null) return null;
|
||||
return LatLng((b.south + b.north) / 2, (b.west + b.east) / 2);
|
||||
}
|
||||
|
||||
/// 叠加层取瓦片方式开关:
|
||||
/// - false(当前):WMS GetMap 兜底,不依赖 GWC 缓存
|
||||
@@ -29,8 +58,14 @@ const bool kUseWmtsTile = false;
|
||||
/// 请求 EPSG:3857(WMS 1.1.1 / 1.3.0 均验证过)返回纯白图。
|
||||
/// 因此这里按瓦片 XYZ 反算经纬度 bbox,以 EPSG:4326 请求 WMS。
|
||||
class GeoWmsProvider extends TileProvider {
|
||||
GeoWmsProvider({this.maxLevel = 21});
|
||||
GeoWmsProvider({
|
||||
required this.layerName,
|
||||
required this.bounds,
|
||||
this.maxLevel = 21,
|
||||
});
|
||||
|
||||
final String layerName;
|
||||
final LatLngBounds bounds;
|
||||
final int maxLevel;
|
||||
|
||||
/// Web 墨卡托地球周长(米)
|
||||
@@ -64,10 +99,10 @@ class GeoWmsProvider extends TileProvider {
|
||||
final northLat = _mercatorYToLat(northM);
|
||||
|
||||
// 与图层实际范围不相交的瓦片直接返回透明图,不发无谓请求
|
||||
final intersects = westLng < kSuzhouBounds.east &&
|
||||
eastLng > kSuzhouBounds.west &&
|
||||
southLat < kSuzhouBounds.north &&
|
||||
northLat > kSuzhouBounds.south;
|
||||
final intersects = westLng < bounds.east &&
|
||||
eastLng > bounds.west &&
|
||||
southLat < bounds.north &&
|
||||
northLat > bounds.south;
|
||||
if (!intersects) {
|
||||
return MemoryImage(TileProvider.transparentImage);
|
||||
}
|
||||
@@ -77,7 +112,7 @@ class GeoWmsProvider extends TileProvider {
|
||||
'SERVICE': 'WMS',
|
||||
'REQUEST': 'GetMap',
|
||||
'VERSION': '1.1.1',
|
||||
'LAYERS': kSuzhouLayerName,
|
||||
'LAYERS': layerName,
|
||||
'BBOX': '${westLng.toStringAsFixed(6)},${southLat.toStringAsFixed(6)},'
|
||||
'${eastLng.toStringAsFixed(6)},${northLat.toStringAsFixed(6)}',
|
||||
'WIDTH': '256',
|
||||
@@ -92,11 +127,18 @@ class GeoWmsProvider extends TileProvider {
|
||||
}
|
||||
}
|
||||
|
||||
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,覆盖在 ESRI 底图上)
|
||||
///
|
||||
/// 地块很小(约 250m),zoom 16 以上才有意义,默认 minZoom = 16
|
||||
/// GeoServer 场站无人机高清影像叠加层(WGS84 基准,覆盖在 ESRI 底图上)
|
||||
/// 按当前选中场站的 siteMapName 动态加载对应图层;地块很小,默认 minZoom = 16
|
||||
class GeoSuzhouOverlay extends StatelessWidget {
|
||||
const GeoSuzhouOverlay({super.key, this.minZoom = 16, this.maxZoom = 21});
|
||||
const GeoSuzhouOverlay({
|
||||
super.key,
|
||||
this.siteMapName,
|
||||
this.minZoom = 16,
|
||||
this.maxZoom = 21,
|
||||
});
|
||||
|
||||
/// 当前选中场站对应的 GeoServer 图层名(形如 workSpace:suzhou)
|
||||
final String? siteMapName;
|
||||
|
||||
/// 小于该级别不请求瓦片(地块太小,低层级看不见)
|
||||
final double minZoom;
|
||||
@@ -106,28 +148,39 @@ class GeoSuzhouOverlay extends StatelessWidget {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final layer = siteMapName;
|
||||
final bounds = layer == null ? null : kStationLayerBounds[layer];
|
||||
// 未选中场站或图层未知 → 不渲染叠加层
|
||||
if (layer == null || bounds == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
if (kUseWmtsTile) {
|
||||
// === WMTS 版:公司把 EPSG:3857 gridset 加入 GWC 并 Seed 后生效 ===
|
||||
return TileLayer(
|
||||
urlTemplate: '$kGeoserverBase/gwc/service/wmts'
|
||||
'?SERVICE=WMTS&REQUEST=GetTile&VERSION=1.0.0'
|
||||
'&LAYER=$kSuzhouLayerName&TILEMATRIXSET=EPSG:3857'
|
||||
'&LAYER=$layer&TILEMATRIXSET=EPSG:3857'
|
||||
'&TILEMATRIX={z}&TILEROW={y}&TILECOL={x}&FORMAT=image/png',
|
||||
userAgentPackageName: 'com.example.app',
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
maxNativeZoom: maxZoom.toInt(),
|
||||
tileBounds: kSuzhouBounds,
|
||||
tileBounds: bounds,
|
||||
);
|
||||
}
|
||||
// === WMS 版(当前兜底):按原生 EPSG:4326 请求,GeoServer 实时渲染 ===
|
||||
return TileLayer(
|
||||
tileProvider: GeoWmsProvider(maxLevel: maxZoom.toInt()),
|
||||
tileProvider: GeoWmsProvider(
|
||||
layerName: layer,
|
||||
bounds: bounds,
|
||||
maxLevel: maxZoom.toInt(),
|
||||
),
|
||||
userAgentPackageName: 'com.example.app',
|
||||
minZoom: minZoom,
|
||||
maxZoom: maxZoom,
|
||||
maxNativeZoom: maxZoom.toInt(),
|
||||
tileBounds: kSuzhouBounds,
|
||||
tileBounds: bounds,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -492,7 +492,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
// ==================== ✅ 核心修复 ====================
|
||||
// 1. 从缓存恢复后,强制让 _markedPoints 同步给外边框(解决不显示)
|
||||
if (_markedPoints.isNotEmpty && gcjOuterPoints.isEmpty) {
|
||||
gcjOuterPoints = List.from(_markedPoints);
|
||||
// 🔥 覆盖模式:兜底恢复时同样闭合边界,避免重启后 outer 缺一边
|
||||
gcjOuterPoints = _closeBowRing(_markedPoints);
|
||||
}
|
||||
|
||||
// 2. 关键:同步机器人模式坐标(保存时边框不丢失)
|
||||
@@ -924,6 +925,156 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🔥 保存地块:打印即将 POST 给后端的完整请求详情
|
||||
/// 用途:验证覆盖模式下 jsonData.outer / jsonData.path 是否已首尾闭合(A B C → A B C A)
|
||||
/// ⚠️ 只读取已构造好的 Dart 对象和 MultipartFile 的元信息(field/filename/contentType/length),
|
||||
/// 绝不调用 MultipartFile.finalize()——那会消费 ByteStream,导致后续 request.send() 失败。
|
||||
/// (旁边的 _debugPrintMultipartRequest 正是踩了这个坑,故未启用)
|
||||
void _logSaveRequestDetail({
|
||||
required http.MultipartRequest request,
|
||||
required Map<String, dynamic> savePath,
|
||||
required String workRecordJson,
|
||||
required String plotName,
|
||||
required Object? siteId,
|
||||
}) {
|
||||
// debugPrint 单条过长会被 logcat 截断,长文本统一按行拆开输出
|
||||
void printLines(String text) {
|
||||
for (final line in text.split('\n')) {
|
||||
debugPrint(line);
|
||||
}
|
||||
}
|
||||
|
||||
debugPrint('📤 [保存地块-请求详情] ================ BEGIN ================');
|
||||
debugPrint('📤 [请求行] ${request.method} ${request.url}');
|
||||
|
||||
// 请求头:Authorization 脱敏,避免 token 明文落日志
|
||||
final Map<String, String> safeHeaders = Map<String, String>.from(
|
||||
request.headers,
|
||||
);
|
||||
final String? auth = safeHeaders['Authorization'];
|
||||
if (auth != null) {
|
||||
safeHeaders['Authorization'] = auth.length > 18
|
||||
? '${auth.substring(0, 14)}...${auth.substring(auth.length - 4)} (已脱敏, 原长${auth.length})'
|
||||
: '(已脱敏)';
|
||||
}
|
||||
debugPrint('📤 [请求头] $safeHeaders');
|
||||
|
||||
debugPrint(
|
||||
'📤 [上下文] workName=$plotName, siteId=$siteId, '
|
||||
'workMode=$_currentWorkMode, robotMode=$_currentRobotMode, '
|
||||
'areaMode=$_currentAreaMode, planModel=${savePath['planModel']}',
|
||||
);
|
||||
|
||||
// multipart 各 part 的元信息(不消费流)
|
||||
debugPrint('📤 [multipart] part 数量: ${request.files.length}');
|
||||
for (var i = 0; i < request.files.length; i++) {
|
||||
final http.MultipartFile f = request.files[i];
|
||||
debugPrint(
|
||||
'📤 [multipart] part[$i] field=${f.field}, '
|
||||
'filename="${f.filename ?? ''}", contentType=${f.contentType}, '
|
||||
'大小=${f.length} 字节 (${(f.length / 1024).toStringAsFixed(1)} KB)',
|
||||
);
|
||||
}
|
||||
|
||||
// 逐点打印 outer / path,并标注首尾,肉眼即可确认是否闭合
|
||||
void printPoints(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
debugPrint('📤 [jsonData.$field] 点数=${list.length}');
|
||||
for (var i = 0; i < list.length; i++) {
|
||||
final Map<dynamic, dynamic> p = list[i] as Map;
|
||||
final String tag = list.length == 1
|
||||
? ' ← 唯一的点'
|
||||
: (i == 0
|
||||
? ' ← 第一个点'
|
||||
: (i == list.length - 1 ? ' ← 最后一个点' : ''));
|
||||
debugPrint(
|
||||
'📤 [jsonData.$field] [$i] lat=${p['lat']}, lng=${p['lng']}$tag',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
printPoints('outer');
|
||||
printPoints('path');
|
||||
|
||||
// 闭环校验结论
|
||||
bool isClosed(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
if (list.length < 4) return false;
|
||||
final Map<dynamic, dynamic> first = list.first as Map;
|
||||
final Map<dynamic, dynamic> last = list.last as Map;
|
||||
return first['lat'] == last['lat'] && first['lng'] == last['lng'];
|
||||
}
|
||||
|
||||
// 🔥 首尾并排比对:把 [0] 和 [最后一个] 的实际坐标上下两行打出来,
|
||||
// 直接肉眼确认“第一个点被当作最后一个点追加到了末尾”
|
||||
void printHeadTail(String field) {
|
||||
final List<dynamic> list = (savePath[field] as List?) ?? <dynamic>[];
|
||||
if (list.isEmpty) {
|
||||
debugPrint('🔒 [首尾比对] $field:空数组,无比对对象');
|
||||
return;
|
||||
}
|
||||
if (list.length == 1) {
|
||||
debugPrint('🔒 [首尾比对] $field:只有 1 个点,不构成环');
|
||||
return;
|
||||
}
|
||||
final Map<dynamic, dynamic> f = list.first as Map;
|
||||
final Map<dynamic, dynamic> l = list.last as Map;
|
||||
final int lastIdx = list.length - 1;
|
||||
final bool same = f['lat'] == l['lat'] && f['lng'] == l['lng'];
|
||||
debugPrint('🔒 [首尾比对] ---- $field (共 ${list.length} 个点) ----');
|
||||
debugPrint('🔒 [首尾比对] $field[0] lat=${f['lat']}, lng=${f['lng']} ← 第一个点');
|
||||
debugPrint('🔒 [首尾比对] $field[$lastIdx] lat=${l['lat']}, lng=${l['lng']} ← 最后一个点');
|
||||
debugPrint(
|
||||
'🔒 [首尾比对] 结论:$field[$lastIdx] ${same ? "==" : "!="} $field[0] → '
|
||||
'${same ? "✅ 最后一个点就是第一个点(已闭环 ABCA)" : "❌ 两点不同(未闭环 ABC)"}',
|
||||
);
|
||||
if (same) {
|
||||
debugPrint(
|
||||
'🔒 [首尾比对] 即:用户实际打了 $lastIdx 个不同顶点,'
|
||||
'末尾第 $lastIdx 位是自动追加的闭合点(与[0]完全相同)',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (_currentWorkMode == WorkMode.bow) {
|
||||
printHeadTail('outer');
|
||||
printHeadTail('path');
|
||||
|
||||
final bool outerOk = isClosed('outer');
|
||||
final bool pathOk = isClosed('path');
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] 覆盖模式要求首尾同点 → '
|
||||
'outer=${outerOk ? "✅已闭合" : "❌未闭合"}, '
|
||||
'path=${pathOk ? "✅已闭合" : "❌未闭合"}',
|
||||
);
|
||||
if (!outerOk || !pathOk) {
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] ⚠️ 未闭合原因排查:打点数=${_markedPoints.length}(需≥3), '
|
||||
'robotMode打点数=${_robotModeWgsPoints.length}, '
|
||||
'workMode=$_currentWorkMode(需为 WorkMode.bow)'
|
||||
'${outerOk || pathOk ? "" : ";若两者都❌,说明 _closeBowRing 未执行追加,往上翻 🔗 [闭合] 日志看跳过原因"}',
|
||||
);
|
||||
}
|
||||
} else {
|
||||
debugPrint(
|
||||
'🔒 [闭环校验] 当前 workMode=$_currentWorkMode 非覆盖模式,'
|
||||
'outer 应为空、path 不闭合(符合预期)',
|
||||
);
|
||||
}
|
||||
|
||||
// 完整 JSON:这就是后端 /iot/workRecord/add 收到的 workRecord 内容
|
||||
debugPrint('📤 [workRecord 完整JSON] ↓↓↓ 后端实际接收内容 ↓↓↓');
|
||||
try {
|
||||
printLines(
|
||||
const JsonEncoder.withIndent(' ').convert(jsonDecode(workRecordJson)),
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint('📤 [workRecord 完整JSON] 格式化失败($e),输出原始串:');
|
||||
printLines(workRecordJson);
|
||||
}
|
||||
debugPrint('📤 [保存地块-请求详情] ================= END =================');
|
||||
}
|
||||
|
||||
Future<void> _savePlotData(String plotName, String? imgBase64) async {
|
||||
debugPrint('🔍 [保存地块] ====== _savePlotData 被调用, plotName=$plotName ======');
|
||||
final loc = AppLocalizations.of(context);
|
||||
@@ -944,15 +1095,14 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
|
||||
// 辅助方法:将打点转为 WGS84 格式 {lng, lat}
|
||||
List<Map<String, double>> _markedPointsToWgs() {
|
||||
if (_currentRobotMode == RobotMode.robot) {
|
||||
return _robotModeWgsPoints
|
||||
.map((p) => {'lng': p.longitude, 'lat': p.latitude})
|
||||
.toList();
|
||||
}
|
||||
return _markedPoints.map((point) {
|
||||
final List<LatLng> source = _currentRobotMode == RobotMode.robot
|
||||
? _robotModeWgsPoints
|
||||
: _markedPoints;
|
||||
// 🔥 覆盖模式:上传前闭合边界,首尾同点(A B C → A B C A)
|
||||
// 下位机与后端要求第一个点与最后一个点相同
|
||||
return _closeBowRing(source).map((point) {
|
||||
// ⚠️ 地图已切换为 WGS84,直接使用原始坐标
|
||||
final wgs84 = LatLng(point.latitude, point.longitude);
|
||||
return {'lng': wgs84.longitude, 'lat': wgs84.latitude};
|
||||
return {'lng': point.longitude, 'lat': point.latitude};
|
||||
}).toList();
|
||||
}
|
||||
|
||||
@@ -1036,21 +1186,19 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
);
|
||||
|
||||
debugPrint('🔍 [保存地块] 构造请求完成,即将进入try发送...');
|
||||
|
||||
// 🔥 发送前打印完整请求详情:URL、请求头、multipart 各 part、
|
||||
// outer/path 逐点坐标(标注首尾)、闭环校验结论、workRecord 完整 JSON
|
||||
_logSaveRequestDetail(
|
||||
request: request,
|
||||
savePath: savePath,
|
||||
workRecordJson: workRecordJson,
|
||||
plotName: plotName,
|
||||
siteId: siteId,
|
||||
);
|
||||
|
||||
// 7. 发送请求
|
||||
try {
|
||||
// debugPrint('📤 [保存地块] ========== 请求参数 BEGIN ==========');
|
||||
// debugPrint('📤 [保存地块] plotName: $plotName');
|
||||
// debugPrint('📤 [保存地块] siteId: $siteId');
|
||||
// debugPrint('📤 [保存地块] workMode: $_currentWorkMode');
|
||||
// debugPrint('📤 [保存地块] planModel: ${savePath['planModel']}');
|
||||
// debugPrint('📤 [保存地块] savePath.name: ${savePath['name']}');
|
||||
// debugPrint('📤 [保存地块] path长度: ${(savePath['path'] as List?)?.length ?? 0}');
|
||||
// debugPrint('📤 [保存地块] outer长度: ${(savePath['outer'] as List?)?.length ?? 0}');
|
||||
// debugPrint('📤 [保存地块] 是否有图片文件: ${imgBase64 != null && imgBase64.isNotEmpty}');
|
||||
// debugPrint('📤 [保存地块] 请求URL: ${HttpApiConsts.baseUrl}/iot/workRecord/add');
|
||||
// debugPrint('📤 [保存地块] workRecord(完整JSON): $workRecordJson');
|
||||
// debugPrint('📤 [保存地块] savePath(原始对象): $savePath');
|
||||
// debugPrint('📤 [保存地块] ========== 请求参数 END ==========');
|
||||
_showPageToast(
|
||||
message: loc.translate('route_planning.saving') + "「$plotName」...",
|
||||
type: ToastType.loading,
|
||||
@@ -1928,7 +2076,11 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}
|
||||
|
||||
LatLng _getDefaultValidCenter() {
|
||||
return const LatLng(30.279651, 120.154871);
|
||||
// 优先进场定位到“当前选中场站”的高清图层中心;无有效场站时兜底到苏州地块中心
|
||||
final center = stationLayerCenter(
|
||||
sl<SiteCubit>().state.selectedSite?.siteMapName,
|
||||
);
|
||||
return center ?? const LatLng(31.083296, 120.840355);
|
||||
}
|
||||
|
||||
Widget _buildMap(
|
||||
@@ -1990,11 +2142,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
maxZoom: 19,
|
||||
),
|
||||
|
||||
/// GeoServer 苏州无人机高清影像叠加层(WGS84 基准,无偏移)
|
||||
/// 实际地块很小(约 250m,中心 31.0833/120.8404),需 zoom >= 16 才能看到
|
||||
/// GeoServer 场站无人机高清影像叠加层(WGS84 基准,无偏移)
|
||||
/// 按当前选中场站的 siteMapName 动态加载对应图层;需 zoom >= 16 才能看到
|
||||
/// 当前 WMS 兜底(原生 EPSG:4326);公司补配 GWC EPSG:3857 后切 WMTS
|
||||
/// 切换位置:geoserver_suzhou_layer.dart 的 kUseWmtsTile 开关
|
||||
GeoSuzhouOverlay(),
|
||||
GeoSuzhouOverlay(
|
||||
siteMapName: sl<SiteCubit>().state.selectedSite?.siteMapName,
|
||||
),
|
||||
//if (isDjMapShow) DJITileLayer(stationName: _currentStation.name, mapController: _mapController),
|
||||
|
||||
// 绘制path折线(Line模式)
|
||||
@@ -3776,6 +3930,50 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
return result;
|
||||
}
|
||||
|
||||
/// 覆盖模式下闭合打点集合:把第一个点追加到末尾(A B C → A B C A)
|
||||
/// 仅覆盖模式(WorkMode.bow)生效;点数不足 3 或首尾已重合时原样返回,避免重复追加
|
||||
/// ⚠️ 只用于派生数据/出口数据,禁止写入 _markedPoints、_robotModeWgsPoints 源集合
|
||||
/// 🔥 每个分支都打日志,便于确认“第一个点到底有没有被追加到末尾”
|
||||
List<LatLng> _closeBowRing(List<LatLng> points) {
|
||||
if (_currentWorkMode != WorkMode.bow) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:workMode=$_currentWorkMode 不是覆盖模式,'
|
||||
'点数=${points.length} 原样返回(不闭合)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
if (points.length < 3) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:点数=${points.length} 不足 3 个,无法成环,'
|
||||
'原样返回(避免造出退化多边形)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
final LatLng first = points.first;
|
||||
final LatLng last = points.last;
|
||||
if (first.latitude == last.latitude && first.longitude == last.longitude) {
|
||||
debugPrint(
|
||||
'🔗 [闭合] ⏭️ 跳过追加:首尾已经重合'
|
||||
'(lat=${first.latitude}, lng=${first.longitude}),'
|
||||
'点数=${points.length} 保持不变(幂等保护,不重复叠加)',
|
||||
);
|
||||
return List<LatLng>.from(points);
|
||||
}
|
||||
// ✅ 真正执行追加的地方
|
||||
debugPrint(
|
||||
'🔗 [闭合] ✅ 已把第一个点追加到末尾!'
|
||||
'追加的坐标=第[0]个点(lat=${first.latitude}, lng=${first.longitude});'
|
||||
'它成为新的第[${points.length}]号点(即最后一个点);'
|
||||
'点数 ${points.length} → ${points.length + 1};'
|
||||
'原本的第[${points.length - 1}]号点(lat=${last.latitude}, lng=${last.longitude}) 往前让一位',
|
||||
);
|
||||
debugPrint(
|
||||
'🔗 [闭合] ✅ 追加后首尾关系:新[0]=(lat=${first.latitude}, lng=${first.longitude}) '
|
||||
'== 新[${points.length}]=(lat=${first.latitude}, lng=${first.longitude}) → 完全相同',
|
||||
);
|
||||
return [...points, LatLng(first.latitude, first.longitude)];
|
||||
}
|
||||
|
||||
// ========== 抽象:生成路径的核心函数(打点函数) ==========
|
||||
Future<void> _generatePath({bool showTips = true}) async {
|
||||
debugPrint(
|
||||
@@ -3847,7 +4045,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
List<work_area_model.Position> outerPositions = [];
|
||||
if (_currentRobotMode == RobotMode.robot &&
|
||||
_robotModeWgsPoints.isNotEmpty) {
|
||||
outerPositions = _robotModeWgsPoints.map((latLng) {
|
||||
outerPositions = _closeBowRing(_robotModeWgsPoints).map((latLng) {
|
||||
return work_area_model.Position(
|
||||
lat: latLng.latitude,
|
||||
lon: latLng.longitude,
|
||||
@@ -3855,7 +4053,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
}).toList();
|
||||
} else if (_markedPoints.isNotEmpty) {
|
||||
// robot模式但_robotModeWgsPoints为空时,用_markedPoints(已是WGS84)
|
||||
outerPositions = _markedPoints.map((latLng) {
|
||||
outerPositions = _closeBowRing(_markedPoints).map((latLng) {
|
||||
return work_area_model.Position(
|
||||
lat: latLng.latitude,
|
||||
lon: latLng.longitude,
|
||||
@@ -3988,7 +4186,8 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
_isWorkAreaCompleted = true;
|
||||
_currentObstaclePoints.clear();
|
||||
_robotModeObsWgsPoints.clear();
|
||||
gcjPathPoints = List.from(_markedPoints);
|
||||
// 🔥 覆盖模式:闭合边界(A B C → A B C A),保证地图显示与保存截图一致
|
||||
gcjPathPoints = _closeBowRing(_markedPoints);
|
||||
typedPathList = [];
|
||||
});
|
||||
_saveDataToLocal();
|
||||
@@ -4276,12 +4475,13 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
),
|
||||
|
||||
/// 悬浮"回到苏州"按钮:一键跳转到 GeoServer 苏州地块(zoom 18 才能看清 250m 地块)
|
||||
/// 悬浮“回到当前场站”按钮:一键跳转到选中场站高清图层(zoom 18 才看得清小地块)
|
||||
Positioned(
|
||||
top: 60,
|
||||
left: 10,
|
||||
child: GestureDetector(
|
||||
onTap: () => _mapController.move(kSuzhouCenter, 18),
|
||||
onTap: () =>
|
||||
_mapController.move(_getDefaultValidCenter(), 18),
|
||||
child: Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -4298,7 +4498,7 @@ class _MapPageEnterpriseState extends State<MapPageEnterprise> {
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: const Text(
|
||||
'苏',
|
||||
'站',
|
||||
style: TextStyle(
|
||||
color: Colors.black87,
|
||||
fontSize: 16,
|
||||
|
||||
@@ -338,7 +338,7 @@ class _BleDeviceDetailPageState extends State<BleDeviceDetailPage> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BindDevicePage(scanResult: matchedDevice!.deviceId),
|
||||
builder: (_) => BindDevicePage(scanResult: matchedDevice!.name),
|
||||
),
|
||||
);
|
||||
} catch (e) {
|
||||
|
||||
@@ -125,6 +125,26 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
/// 🔥 机器人类型中文名关键字(接口 deviceTypeName 可能是纯中文)
|
||||
static const _robotTypeKeywords = ['除草', '巡检', '清洗', '割草', '机器人'];
|
||||
|
||||
/// 🔥 类型 tab 的 code → 别名集合。
|
||||
/// 筛选条用英文 code(inverter),但接口 deviceTypeName 可能返回中文名(逆变器),
|
||||
/// 只做 `device.type == code` 的精确比较会让这四个 tab 恒为空,所以两套写法都认。
|
||||
static const Map<String, Set<String>> _typeAliases = {
|
||||
'inverter': {'inverter', '逆变器'},
|
||||
'combiner_box': {'combiner_box', '汇流箱'},
|
||||
'module': {'module', '组件'},
|
||||
'monitor': {'monitor', '监控'},
|
||||
};
|
||||
|
||||
/// 设备类型是否命中当前 tab(大小写与首尾空格不敏感)
|
||||
static bool _matchTypeTab(String deviceType, String selectedType) {
|
||||
final normalized = deviceType.trim().toLowerCase();
|
||||
final aliases = _typeAliases[selectedType];
|
||||
if (aliases != null) {
|
||||
return aliases.any((alias) => alias.toLowerCase() == normalized);
|
||||
}
|
||||
return normalized == selectedType.toLowerCase();
|
||||
}
|
||||
|
||||
/// 🔥 通用判断:是否机器人(型号前缀 + 中文关键字双重匹配,新型号也能自动识别)
|
||||
static bool _isRobotDevice(String type, String name, String deviceId) {
|
||||
final haystacks = [
|
||||
@@ -506,24 +526,15 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
List filteredByType = state.devices;
|
||||
if (state.selectedType != 'all') {
|
||||
filteredByType = state.devices.where((device) {
|
||||
return device.type == state.selectedType;
|
||||
return _matchTypeTab(device.type, state.selectedType);
|
||||
}).toList();
|
||||
} else {
|
||||
// 🔥 "全部"模式下,排除已由专用组件渲染的类型,避免重复展示
|
||||
// drone_station 已通过 _buildDroneStationList 渲染
|
||||
// 机器人通过下面的 _isRobotDevice 通用匹配识别,已由 _buildEmbeddedRobotList 渲染
|
||||
filteredByType = state.devices.where((device) {
|
||||
if (device.type == 'drone_station') return false;
|
||||
final isRobot = _isRobotDevice(
|
||||
device.type,
|
||||
device.name,
|
||||
device.deviceId,
|
||||
);
|
||||
debugPrint(
|
||||
'🔍 [全部-过滤] deviceId=${device.deviceId} type=${device.type} name=${device.name} → isRobot=$isRobot ${isRobot ? '✅排除' : ''}',
|
||||
);
|
||||
return !isRobot;
|
||||
}).toList();
|
||||
// 🔥 "全部"模式下不渲染通用设备段:
|
||||
// 机器人段(RobotListBloc → /iot/device/getSiteList)零过滤,已渲染该接口全量设备,
|
||||
// 通用段数据源是同一个接口,恒为机器人段的子集 → 必然重复渲染。
|
||||
// 原先靠 _isRobotDevice 字符串猜测来排除,后端 deviceTypeName 缺失时判据即失效。
|
||||
// 类型 tab(逆变器/汇流箱/组件/监控)走上面的 if 分支,不受影响。
|
||||
filteredByType = const [];
|
||||
}
|
||||
|
||||
// 🔥 再根据搜索关键词过滤
|
||||
@@ -550,11 +561,21 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
_buildStatusCard(context, state),
|
||||
const SizedBox(height: 20.0),
|
||||
// 🔥 机器人列表(直接复用 RobotListPage 的布局)
|
||||
if (state.selectedType == 'all') _buildEmbeddedRobotList(context),
|
||||
// 🔥 无人机机场列表(直接复用 _buildDroneStationList 的布局)
|
||||
// 传入搜索关键词:通用段移除后,"全部"模式的搜索由机器人段承接
|
||||
if (state.selectedType == 'all')
|
||||
_buildDroneStationList(context, embedded: true),
|
||||
// 🔥 普通设备列表
|
||||
_buildEmbeddedRobotList(
|
||||
context,
|
||||
searchKeyword: state.searchKeyword,
|
||||
),
|
||||
// 🔥 无人机机场列表(直接复用 _buildDroneStationList 的布局)
|
||||
// 传 key:电站切换时强制重建 BlocProvider,否则不会用新 siteId 重新加载
|
||||
if (state.selectedType == 'all')
|
||||
_buildDroneStationList(
|
||||
context,
|
||||
embedded: true,
|
||||
key: ValueKey('embedded_drone_station_$_currentSiteId'),
|
||||
),
|
||||
// 🔥 普通设备列表(仅类型 tab 生效,"全部"模式下 filteredByType 为空)
|
||||
...filteredDevices
|
||||
.map(
|
||||
(device) => DeviceItemWidget(
|
||||
@@ -576,7 +597,10 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
}
|
||||
|
||||
/// 🔥 嵌入式机器人列表(直接复用 RobotListPage 的布局)
|
||||
Widget _buildEmbeddedRobotList(BuildContext context) {
|
||||
Widget _buildEmbeddedRobotList(
|
||||
BuildContext context, {
|
||||
String searchKeyword = '',
|
||||
}) {
|
||||
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||
final siteId = selectedSite?.id;
|
||||
|
||||
@@ -619,11 +643,25 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
'✅ [EmbeddedRobotList] 加载成功,机器人数量: ${state.robots.length}',
|
||||
);
|
||||
|
||||
// 🔥 搜索联动:按 设备名 / deviceId / 别名 过滤
|
||||
final keyword = searchKeyword.trim().toLowerCase();
|
||||
final robots = keyword.isEmpty
|
||||
? state.robots
|
||||
: state.robots.where((robot) {
|
||||
return robot.name.toLowerCase().contains(keyword) ||
|
||||
robot.id.toLowerCase().contains(keyword) ||
|
||||
(robot.alias ?? '').toLowerCase().contains(keyword);
|
||||
}).toList();
|
||||
|
||||
if (robots.isEmpty) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
|
||||
// 🔥 直接复用 RobotListPage 的布局结构(不显示标题)
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
...state.robots.map((robot) {
|
||||
...robots.map((robot) {
|
||||
debugPrint(
|
||||
'📍 [EmbeddedRobotList] 机器人 - name: ${robot.name}, id: ${robot.id}',
|
||||
);
|
||||
@@ -701,6 +739,8 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
final selectedSite = sl<SiteCubit>().state.selectedSite;
|
||||
|
||||
if (selectedSite == null) {
|
||||
// 🔥 嵌入式分组不输出占位内容,避免"全部"列表中间夹一段提示
|
||||
if (embedded) return const SizedBox.shrink();
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
@@ -762,6 +802,9 @@ class _DeviceStatusViewState extends State<DeviceStatusView> {
|
||||
final stations = state.stations;
|
||||
|
||||
if (stations.isEmpty) {
|
||||
// 🔥 "全部"标签下的嵌入式分组为空时不输出文案,
|
||||
// 避免列表中间夹一行"该场站暂无无人机机场"
|
||||
if (embedded) return const SizedBox.shrink();
|
||||
return Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(
|
||||
|
||||
@@ -19,6 +19,9 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
// 🔥 卡片外边距:与 RobotItemCard 保持一致。
|
||||
// 外层 ListView 只有 bottom padding,缺了这里卡片就会通栏贴边、左右圆角被裁掉。
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 6.0),
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
decoration: BoxDecoration(
|
||||
color: context.appColors.cardBackground,
|
||||
@@ -81,24 +84,32 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
IconData icon;
|
||||
Color color;
|
||||
|
||||
// 🔥 同一个 type 字段有两套取值体系:英文 code(inverter)与中文名(逆变器)。
|
||||
// 类型 tab 用英文 code 过滤,所以到达这里的设备大多是英文 code;
|
||||
// 两套写法都认,否则全部落到 default 的通用设备图标。
|
||||
switch (type) {
|
||||
case '机器人':
|
||||
case 'robot':
|
||||
icon = Icons.smart_toy;
|
||||
color = const Color(0xFF165DFF);
|
||||
break;
|
||||
case '逆变器':
|
||||
case 'inverter':
|
||||
icon = Icons.power;
|
||||
color = const Color(0xFF165DFF);
|
||||
break;
|
||||
case '汇流箱':
|
||||
case 'combiner_box':
|
||||
icon = Icons.electrical_services;
|
||||
color = const Color(0xFF722ED1);
|
||||
break;
|
||||
case '组件':
|
||||
case 'module':
|
||||
icon = Icons.solar_power;
|
||||
color = const Color(0xFF13C2C2);
|
||||
break;
|
||||
case '监控':
|
||||
case 'monitor':
|
||||
icon = Icons.videocam;
|
||||
color = const Color(0xFFEB2F96);
|
||||
break;
|
||||
@@ -153,7 +164,9 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
Widget _buildDeviceData(BuildContext context, DeviceEntity device) {
|
||||
final dataItems = <Widget>[];
|
||||
|
||||
if (device.type == 'inverter') {
|
||||
// 🔥 与 _buildDeviceIcon 同理:英文 code 与中文名都认,
|
||||
// 否则匹配不上会落到 else 显示 "-- 数据"。
|
||||
if (device.type == 'inverter' || device.type == '逆变器') {
|
||||
dataItems.add(
|
||||
_buildDataItem(
|
||||
context,
|
||||
@@ -169,7 +182,7 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
'${AppLocalizations.of(context).translate('device_list_v2.today_energy')}',
|
||||
),
|
||||
);
|
||||
} else if (device.type == 'combiner_box') {
|
||||
} else if (device.type == 'combiner_box' || device.type == '汇流箱') {
|
||||
if (device.current != null) {
|
||||
dataItems.add(
|
||||
_buildDataItem(
|
||||
@@ -189,7 +202,7 @@ class DeviceItemWidget extends StatelessWidget {
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (device.type == 'module') {
|
||||
} else if (device.type == 'module' || device.type == '组件') {
|
||||
dataItems.add(
|
||||
_buildDataItem(
|
||||
context,
|
||||
|
||||
@@ -4,6 +4,7 @@ class SiteEntity extends Equatable {
|
||||
final int id;
|
||||
final String siteName;
|
||||
final String? siteCode;
|
||||
final String? siteMapName; // GeoServer 图层名,形如 workSpace:suzhou
|
||||
final int orgId;
|
||||
final double? longitude;
|
||||
final double? latitude;
|
||||
@@ -14,6 +15,7 @@ class SiteEntity extends Equatable {
|
||||
required this.id,
|
||||
required this.siteName,
|
||||
this.siteCode,
|
||||
this.siteMapName,
|
||||
required this.orgId,
|
||||
this.longitude,
|
||||
this.latitude,
|
||||
@@ -26,6 +28,7 @@ class SiteEntity extends Equatable {
|
||||
id: json['id'] as int,
|
||||
siteName: json['name'] as String? ?? json['siteName'] as String? ?? '',
|
||||
siteCode: json['siteCode'] as String?,
|
||||
siteMapName: json['siteMapName'] as String?,
|
||||
orgId: json['orgId'] as int? ?? 0,
|
||||
longitude: (json['longitude'] as num?)?.toDouble(),
|
||||
latitude: (json['latitude'] as num?)?.toDouble(),
|
||||
@@ -39,6 +42,7 @@ class SiteEntity extends Equatable {
|
||||
'id': id,
|
||||
'siteName': siteName,
|
||||
'siteCode': siteCode,
|
||||
'siteMapName': siteMapName,
|
||||
'orgId': orgId,
|
||||
'longitude': longitude,
|
||||
'latitude': latitude,
|
||||
@@ -48,5 +52,6 @@ class SiteEntity extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [id, siteName, siteCode, orgId, longitude, latitude, address, status];
|
||||
List<Object?> get props =>
|
||||
[id, siteName, siteCode, siteMapName, orgId, longitude, latitude, address, status];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user