From 87609c7dc48b77595ca2e51271397a9cff5d2fe0 Mon Sep 17 00:00:00 2001 From: Songzex <2402265378@qq.com> Date: Mon, 10 Aug 2026 16:26:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96ios?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/presentation/bloc/auth_cubit.dart | 48 +- .../pages/machine_details_page.dart | 2 +- .../presentation/pages/bind_device_page.dart | 16 +- .../pages/drone_station_detail_page.dart | 84 +- .../presentation/widgets/drone_osd_card.dart | 324 +++---- .../widgets/drone_station_osd_card.dart | 866 ++++++++++-------- .../widgets/robot_header_card.dart | 2 +- .../workorder_remote_datasource_impl.dart | 5 + .../presentation/cubit/workorder_cubit.dart | 5 - pubspec.lock | 4 +- pubspec.yaml | 2 +- 11 files changed, 746 insertions(+), 612 deletions(-) diff --git a/lib/features/auth/presentation/bloc/auth_cubit.dart b/lib/features/auth/presentation/bloc/auth_cubit.dart index f07dbd20..56128510 100644 --- a/lib/features/auth/presentation/bloc/auth_cubit.dart +++ b/lib/features/auth/presentation/bloc/auth_cubit.dart @@ -200,31 +200,31 @@ class AuthCubit extends Cubit { await tcp.connect(host: TCPConsts.TCP_IP, port: TCPConsts.TCP_PORT); tcp.startHeartbeat(interval: const Duration(seconds: 4)); - // 🔥 关键:等待 2.5 秒,看是否收到 have_logged_in - bool isKicked = await _waitForLoginVerification(); + // 🔥 已注释:等待 2.5 秒看是否收到 have_logged_in 的验证逻辑 + // bool isKicked = await _waitForLoginVerification(); + // + // if (isKicked) { + // // 🔥 不能进入 APP,必须清除所有登录信息 + // debugPrint('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息...'); + // _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息', shouldLog: true); + // + // // 1. 删除已保存的用户信息 + // await storage.deleteUser(); + // debugPrint('✅ [AUTH] 已删除用户信息'); + // + // // 2. 断开 TCP 连接 + // tcp.forceDisconnect(); + // debugPrint('✅ [AUTH] 已断开 TCP 连接'); + // + // // 3. 显示 Toast + // _showLoginFailedToast("账号已在其他设备登录"); + // debugPrint('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页'); + // _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页', shouldLog: true); + // return; // 停留在登录页 + // } - if (isKicked) { - // 🔥 不能进入 APP,必须清除所有登录信息 - debugPrint('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息...'); - _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,清除登录信息', shouldLog: true); - - // 1. 删除已保存的用户信息 - await storage.deleteUser(); - debugPrint('✅ [AUTH] 已删除用户信息'); - - // 2. 断开 TCP 连接 - tcp.forceDisconnect(); - debugPrint('✅ [AUTH] 已断开 TCP 连接'); - - // 3. 显示 Toast - _showLoginFailedToast("账号已在其他设备登录"); - debugPrint('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页'); - _logger.logWithLevel('>>> [AUTH] ⚠️ 登录验证失败,停留在登录页', shouldLog: true); - return; // 停留在登录页 - } - - debugPrint('✅ [AUTH] TCP连接成功,验证通过'); - _logger.logWithLevel('✅ [AUTH] TCP连接成功,验证通过', shouldLog: true); + debugPrint('✅ [AUTH] TCP连接成功,验证通过(已跳过等待)'); + _logger.logWithLevel('✅ [AUTH] TCP连接成功,验证通过(已跳过等待)', shouldLog: true); } catch (e) { debugPrint('❌ [AUTH] TCP连接失败:$e'); _logger.logWithLevel('❌ [AUTH] TCP连接失败:$e', level: 'ERROR'); diff --git a/lib/features/machine_details/presentation/pages/machine_details_page.dart b/lib/features/machine_details/presentation/pages/machine_details_page.dart index e260a3ca..7cf353e3 100644 --- a/lib/features/machine_details/presentation/pages/machine_details_page.dart +++ b/lib/features/machine_details/presentation/pages/machine_details_page.dart @@ -47,7 +47,7 @@ class _MachineDetailsPageState extends State { {'name': '后视', 'alignment': Alignment.topRight, 'icon': Icons.arrow_downward}, {'name': '左视', 'alignment': Alignment.bottomLeft, 'icon': Icons.arrow_back}, {'name': '右视', 'alignment': Alignment.bottomRight, 'icon': Icons.arrow_forward}, - {'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, + {'name': '全景', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, ]; @override diff --git a/lib/features/v2/device_list/presentation/pages/bind_device_page.dart b/lib/features/v2/device_list/presentation/pages/bind_device_page.dart index b286513b..8c944404 100644 --- a/lib/features/v2/device_list/presentation/pages/bind_device_page.dart +++ b/lib/features/v2/device_list/presentation/pages/bind_device_page.dart @@ -142,12 +142,15 @@ class _BindDevicePageState extends State { children: [ Expanded( child: SizedBox( - height: 44, + height: 48, child: OutlinedButton( onPressed: state.isSubmitting ? null : () => Navigator.of(context).pop(), style: OutlinedButton.styleFrom( + padding: const EdgeInsets.symmetric( + vertical: 10, + ), side: const BorderSide( color: Color(0xFFE5E6EB), ), @@ -158,7 +161,8 @@ class _BindDevicePageState extends State { child: const Text( '取消', style: TextStyle( - fontSize: 15, + fontSize: 14, + height: 1.0, color: Color(0xFF4E5969), ), ), @@ -168,7 +172,7 @@ class _BindDevicePageState extends State { const SizedBox(width: 16), Expanded( child: SizedBox( - height: 44, + height: 48, child: ElevatedButton( onPressed: state.isSubmitting ? null @@ -176,6 +180,9 @@ class _BindDevicePageState extends State { style: ElevatedButton.styleFrom( backgroundColor: const Color(0xFF165DFF), elevation: 0, + padding: const EdgeInsets.symmetric( + vertical: 10, + ), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(8), ), @@ -192,7 +199,8 @@ class _BindDevicePageState extends State { : const Text( '确定', style: TextStyle( - fontSize: 15, + fontSize: 14, + height: 1.0, color: Colors.white, ), ), diff --git a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart index 992ed994..18c32469 100644 --- a/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart +++ b/lib/features/v2/device_list/presentation/pages/drone_station_detail_page.dart @@ -36,8 +36,15 @@ class _DroneStationDetailPageState extends State { late DroneOsdDataSource _osdDataSource; StreamSubscription? _stationOsdSubscription; + + /// 静态缓存:跨页面实例保留上次的 host 数据 + static final Map _cachedHostData = {}; + final ValueNotifier> _stationHostData = - ValueNotifier>({}); + ValueNotifier>( + // 初始化时从缓存加载,进入页面立即显示 + Map.from(_cachedHostData), + ); @override void initState() { @@ -73,47 +80,52 @@ class _DroneStationDetailPageState extends State { final hostData = data['data'] is Map ? (data['data'] as Map)['host'] : null; if (hostData == null || hostData is! Map) return; - final Map parsed = {}; + // 🔥 基于已有数据合并,只更新非 null 字段,避免 Type 2 消息覆盖已有值 + final Map merged = Map.from(_stationHostData.value); - parsed['environment_temperature'] = - (hostData['environment_temperature'] as num?)?.toDouble(); - parsed['humidity'] = (hostData['humidity'] as num?)?.toDouble(); - parsed['wind_speed'] = (hostData['wind_speed'] as num?)?.toDouble(); - parsed['rainfall'] = hostData['rainfall']?.toString(); - parsed['cover_state'] = hostData['cover_state']?.toString(); - parsed['drone_in_dock'] = hostData['drone_in_dock']?.toString(); - parsed['temperature'] = (hostData['temperature'] as num?)?.toDouble(); - parsed['putter_state'] = hostData['putter_state']?.toString(); - parsed['supplement_light_state'] = hostData['supplement_light_state'] - ?.toString(); - parsed['alarm_state'] = hostData['alarm_state']?.toString(); - parsed['emergency_stop_state'] = hostData['emergency_stop_state'] - ?.toString(); - parsed['silent_mode'] = hostData['silent_mode']?.toString(); - parsed['mode_code'] = hostData['mode_code']?.toString(); - parsed['heading'] = (hostData['heading'] as num?)?.toDouble(); - parsed['height'] = (hostData['height'] as num?)?.toDouble(); - parsed['latitude'] = (hostData['latitude'] as num?)?.toDouble(); - parsed['longitude'] = (hostData['longitude'] as num?)?.toDouble(); - parsed['home_position_is_valid'] = hostData['home_position_is_valid'] - ?.toString(); - parsed['battery_store_mode'] = hostData['battery_store_mode']?.toString(); - parsed['first_power_on'] = hostData['first_power_on']?.toString(); - parsed['drone_charge_state'] = hostData['drone_charge_state']; - parsed['air_conditioner'] = hostData['air_conditioner']; - parsed['network_state'] = hostData['network_state']; - parsed['position_state'] = hostData['position_state']; - parsed['storage'] = hostData['storage']; - parsed['sub_device'] = hostData['sub_device']; - parsed['alternate_land_point'] = hostData['alternate_land_point']; + void _put(String key, dynamic value) { + if (value != null) merged[key] = value; + } + + _put('environment_temperature', (hostData['environment_temperature'] as num?)?.toDouble()); + _put('humidity', (hostData['humidity'] as num?)?.toDouble()); + _put('wind_speed', (hostData['wind_speed'] as num?)?.toDouble()); + _put('rainfall', hostData['rainfall']?.toString()); + _put('cover_state', hostData['cover_state']?.toString()); + _put('drone_in_dock', hostData['drone_in_dock']?.toString()); + _put('temperature', (hostData['temperature'] as num?)?.toDouble()); + _put('putter_state', hostData['putter_state']?.toString()); + _put('supplement_light_state', hostData['supplement_light_state']?.toString()); + _put('alarm_state', hostData['alarm_state']?.toString()); + _put('emergency_stop_state', hostData['emergency_stop_state']?.toString()); + _put('silent_mode', hostData['silent_mode']?.toString()); + _put('mode_code', hostData['mode_code']?.toString()); + _put('heading', (hostData['heading'] as num?)?.toDouble()); + _put('height', (hostData['height'] as num?)?.toDouble()); + _put('latitude', (hostData['latitude'] as num?)?.toDouble()); + _put('longitude', (hostData['longitude'] as num?)?.toDouble()); + _put('home_position_is_valid', hostData['home_position_is_valid']?.toString()); + _put('battery_store_mode', hostData['battery_store_mode']?.toString()); + _put('first_power_on', hostData['first_power_on']?.toString()); + _put('drone_charge_state', hostData['drone_charge_state']); + _put('air_conditioner', hostData['air_conditioner']); + _put('network_state', hostData['network_state']); + _put('position_state', hostData['position_state']); + _put('storage', hostData['storage']); + _put('sub_device', hostData['sub_device']); + _put('alternate_land_point', hostData['alternate_land_point']); if (hostData['air_conditioner'] is Map) { final ac = hostData['air_conditioner'] as Map; - parsed['air_conditioner_state'] = ac['air_conditioner_state']?.toString(); - parsed['air_conditioner_switch_time'] = ac['switch_time']?.toString(); + _put('air_conditioner_state', ac['air_conditioner_state']?.toString()); + _put('air_conditioner_switch_time', ac['switch_time']?.toString()); } - _stationHostData.value = Map.from(parsed); + _stationHostData.value = merged; + // 同步写入静态缓存 + _cachedHostData + ..clear() + ..addAll(merged); } /// 🔥 页面重新激活时调用(从其他页面返回时) diff --git a/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart b/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart index e3e831ef..27a2bd2f 100644 --- a/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/drone_osd_card.dart @@ -25,10 +25,73 @@ class DroneOsdCard extends StatefulWidget { State createState() => _DroneOsdCardState(); } +/// 单个字段的静态元数据(图标、标签、key、默认颜色) +class _FieldDef { + final IconData icon; + final String label; + final String key; + final Color defaultColor; + const _FieldDef({ + required this.icon, + required this.label, + required this.key, + required this.defaultColor, + }); +} + class _DroneOsdCardState extends State { StreamSubscription? _subscription; - final List> _osdFields = []; - final Map _cachedValues = {}; + + /// 字段静态定义(顺序即展示顺序) + static const _fieldDefs = <_FieldDef>[ + _FieldDef( + icon: Icons.battery_full_rounded, + label: '电量', + key: 'battery', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.height_rounded, + label: '高度', + key: 'height', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.trending_flat_rounded, + label: '水平速度', + key: 'horizontalSpeed', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.trending_up_rounded, + label: '垂直速度', + key: 'verticalSpeed', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.navigation_rounded, + label: '航向角', + key: 'heading', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.satellite_rounded, + label: 'GPS', + key: 'gps', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.satellite_alt_rounded, + label: 'RTK', + key: 'rtk', + defaultColor: Color(0xFF00B42A), + ), + ]; + + /// 每个字段的值和颜色用独立 ValueNotifier + /// 数据更新时只重建对应字段的 Text,不再 setState 整个卡片 + final Map> _valueNotifiers = {}; + final Map> _colorNotifiers = {}; /// 是否正在等待任务下发后的无人机推送数据 bool _isWaitingForPush = false; @@ -43,7 +106,7 @@ class _DroneOsdCardState extends State { @override void initState() { super.initState(); - _initFields(); + _initNotifiers(); _isWaitingForPush = droneTaskStateManager.isWaitingForOsdPush.value; droneTaskStateManager.isWaitingForOsdPush.addListener(_onWaitingChanged); if (_isWaitingForPush) { @@ -52,6 +115,13 @@ class _DroneOsdCardState extends State { _subscribeStream(); } + void _initNotifiers() { + for (final def in _fieldDefs) { + _valueNotifiers[def.key] = ValueNotifier('--'); + _colorNotifiers[def.key] = ValueNotifier(def.defaultColor); + } + } + void _onWaitingChanged() { if (!mounted) return; final waiting = droneTaskStateManager.isWaitingForOsdPush.value; @@ -105,63 +175,15 @@ class _DroneOsdCardState extends State { droneTaskStateManager.isWaitingForOsdPush.removeListener(_onWaitingChanged); _localWaitTimeoutTimer?.cancel(); _subscription?.cancel(); + for (final n in _valueNotifiers.values) { + n.dispose(); + } + for (final n in _colorNotifiers.values) { + n.dispose(); + } super.dispose(); } - void _initFields() { - _osdFields.addAll([ - { - 'icon': Icons.battery_full_rounded, - 'label': '电量', - 'key': 'battery', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.height_rounded, - 'label': '高度', - 'key': 'height', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.trending_flat_rounded, - 'label': '水平速度', - 'key': 'horizontalSpeed', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.trending_up_rounded, - 'label': '垂直速度', - 'key': 'verticalSpeed', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.navigation_rounded, - 'label': '航向角', - 'key': 'heading', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.satellite_rounded, - 'label': 'GPS', - 'key': 'gps', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.satellite_alt_rounded, - 'label': 'RTK', - 'key': 'rtk', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - ]); - } - void _subscribeStream() { if (widget.deviceSn.isEmpty) return; @@ -171,6 +193,20 @@ class _DroneOsdCardState extends State { }); } + /// 更新单个字段的值和颜色(仅当值变化时才触发监听者重建) + void _updateField(String key, String value, [Color? color]) { + final vNotifier = _valueNotifiers[key]; + if (vNotifier != null && vNotifier.value != value) { + vNotifier.value = value; + } + if (color != null) { + final cNotifier = _colorNotifiers[key]; + if (cNotifier != null && cNotifier.value != color) { + cNotifier.value = color; + } + } + } + void _parseOsdFields(DroneOsdEntity osd) { final data = osd.rawData; final dataMap = data['data'] is Map ? data['data'] as Map : null; @@ -186,22 +222,24 @@ class _DroneOsdCardState extends State { if (droneData == null) return; // 收到有效的无人机推送数据,标记已收到数据 - // 即使接口返回 isDroneOnline=false,MQTT 推送了数据说明无人机实际已在线 + // 首次收到数据需要 setState 切换卡片显示,之后数据更新通过 ValueNotifier 局部刷新 + final wasFirstData = !_hasReceivedData; _hasReceivedData = true; - // 收到有效的无人机推送数据,停止本地“推送信息检测中”转圈 + // 收到有效的无人机推送数据,停止本地"推送信息检测中"转圈 // 注意:只清除本地状态,不清除全局状态 // 因为本卡片在用户离开详情页期间仍可能收到旧 OSD 数据, // 提前清除全局状态会导致用户返回后看不到转圈效果 - if (_isWaitingForPush) { + final needStopWaiting = _isWaitingForPush; + if (needStopWaiting) { _localWaitTimeoutTimer?.cancel(); - setState(() { - _isWaitingForPush = false; - }); + _isWaitingForPush = false; } - final parsedValues = {}; - final parsedColors = {}; + // 仅在首次切换卡片显示 / 停止等待转圈时调用 setState(很低频) + if (wasFirstData || needStopWaiting) { + if (mounted) setState(() {}); + } double? height = (droneData['height'] as num?)?.toDouble() ?? @@ -235,77 +273,41 @@ class _DroneOsdCardState extends State { final int? rtkSatellites = droneData['rtk_satellites'] as int?; if (batteryPercent != null) { - parsedValues['battery'] = '$batteryPercent%'; - parsedColors['battery'] = batteryPercent > 50 - ? const Color(0xFF00B42A) - : batteryPercent > 20 - ? const Color(0xFFFF7D00) - : const Color(0xFFF53F3F); + _updateField('battery', '$batteryPercent%'); } if (height != null) { - parsedValues['height'] = '${height.toStringAsFixed(1)}m'; - parsedColors['height'] = const Color(0xFF165DFF); + _updateField('height', '${height.toStringAsFixed(1)}m'); } if (horizontalSpeed != null) { - parsedValues['horizontalSpeed'] = - '${horizontalSpeed.toStringAsFixed(1)}m/s'; + _updateField( + 'horizontalSpeed', + '${horizontalSpeed.toStringAsFixed(1)}m/s', + ); } if (verticalSpeed != null) { - parsedValues['verticalSpeed'] = '${verticalSpeed.toStringAsFixed(1)}m/s'; - parsedColors['verticalSpeed'] = verticalSpeed > 5 - ? const Color(0xFFF53F3F) - : const Color(0xFF00B42A); + _updateField('verticalSpeed', '${verticalSpeed.toStringAsFixed(1)}m/s'); } if (heading != null) { - parsedValues['heading'] = '${heading.toStringAsFixed(0)}°'; + _updateField('heading', '${heading.toStringAsFixed(0)}°'); } if (gpsSatellites != null) { - parsedValues['gps'] = '$gpsSatellites颗'; - parsedColors['gps'] = gpsSatellites >= 6 - ? const Color(0xFF00B42A) - : const Color(0xFFFF7D00); + _updateField('gps', '$gpsSatellites颗'); } // RTK:优先展示定位状态(is_fixed),其次回退到卫星数 if (rtkFixed != null) { - String rtkStatus; - Color rtkColor; switch (rtkFixed) { case 2: - rtkStatus = '固定解'; - rtkColor = const Color(0xFF00B42A); + _updateField('rtk', '固定解'); break; case 1: - rtkStatus = '浮点解'; - rtkColor = const Color(0xFFFF7D00); + _updateField('rtk', '浮点解'); break; default: - rtkStatus = '未定位'; - rtkColor = const Color(0xFF86909C); + _updateField('rtk', '未定位'); } - parsedValues['rtk'] = rtkStatus; - parsedColors['rtk'] = rtkColor; } else if (rtkSatellites != null) { - parsedValues['rtk'] = '$rtkSatellites颗'; - parsedColors['rtk'] = rtkSatellites >= 4 - ? const Color(0xFF00B42A) - : const Color(0xFFFF7D00); + _updateField('rtk', '$rtkSatellites颗'); } - - if (!mounted) return; - setState(() { - for (var field in _osdFields) { - final key = field['key'] as String; - if (parsedValues.containsKey(key)) { - _cachedValues[key] = parsedValues[key]!; - field['value'] = parsedValues[key]; - } else if (_cachedValues.containsKey(key)) { - field['value'] = _cachedValues[key]; - } - if (parsedColors.containsKey(key)) { - field['color'] = parsedColors[key]; - } - } - }); } @override @@ -516,8 +518,8 @@ class _DroneOsdCardState extends State { const columns = 3; final itemWidth = (constraints.maxWidth - spacing * (columns - 1)) / columns; - final items = _osdFields.map((field) { - return _buildOsdGridItem(field, itemWidth); + final items = _fieldDefs.map((def) { + return _buildOsdGridItem(def, itemWidth); }).toList(); return Wrap( spacing: spacing, @@ -531,53 +533,63 @@ class _DroneOsdCardState extends State { ); } - Widget _buildOsdGridItem(Map field, double width) { - final color = field['color'] as Color; + /// 每个字段用 ValueListenableBuilder 包裹,数据变化时只重建这一格 + Widget _buildOsdGridItem(_FieldDef def, double width) { return SizedBox( width: width, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), - decoration: BoxDecoration( - color: color.withOpacity(0.08), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color.withOpacity(0.2), width: 1), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + child: ValueListenableBuilder( + valueListenable: _colorNotifiers[def.key]!, + builder: (context, color, _) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.withOpacity(0.2), width: 1), + ), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Icon(field['icon'] as IconData, color: color, size: 14), - const SizedBox(width: 2), - Expanded( - child: Text( - field['label'] as String, - style: const TextStyle( - fontSize: 9, - color: Color(0xFF86909C), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(def.icon, color: color, size: 14), + const SizedBox(width: 2), + Expanded( + child: Text( + def.label, + style: const TextStyle( + fontSize: 9, + color: Color(0xFF86909C), + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ], + ), + const SizedBox(height: 3), + ValueListenableBuilder( + valueListenable: _valueNotifiers[def.key]!, + builder: (context, value, _) { + return Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: color, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, ), ], ), - const SizedBox(height: 3), - Text( - field['value'] as String, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: color, - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + ); + }, ), ); } diff --git a/lib/features/v2/device_list/presentation/widgets/drone_station_osd_card.dart b/lib/features/v2/device_list/presentation/widgets/drone_station_osd_card.dart index 99224d8a..6dc6cc2f 100644 --- a/lib/features/v2/device_list/presentation/widgets/drone_station_osd_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/drone_station_osd_card.dart @@ -18,19 +18,200 @@ class DroneStationOsdCard extends StatefulWidget { State createState() => _DroneStationOsdCardState(); } +/// 单个字段的静态元数据(图标、标签、key、默认颜色) +class _FieldDef { + final IconData icon; + final String label; + final String key; + final Color defaultColor; + const _FieldDef({ + required this.icon, + required this.label, + required this.key, + required this.defaultColor, + }); +} + class _DroneStationOsdCardState extends State { StreamSubscription? _subscription; - final List> _osdFields = []; - final Map _cachedValues = {}; - bool _initialized = false; + + /// 字段静态定义(顺序即展示顺序) + static const _fieldDefs = <_FieldDef>[ + _FieldDef( + icon: Icons.ac_unit_rounded, + label: '空调状态', + key: 'acState', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.timer_rounded, + label: '空调计时', + key: 'acTime', + defaultColor: Color(0xFF722ED1), + ), + _FieldDef( + icon: Icons.door_sliding_rounded, + label: '机库舱门', + key: 'coverState', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.push_pin_rounded, + label: '推板状态', + key: 'putterState', + defaultColor: Color(0xFF722ED1), + ), + _FieldDef( + icon: Icons.lightbulb_rounded, + label: '补光灯', + key: 'lightState', + defaultColor: Color(0xFFFF7D00), + ), + _FieldDef( + icon: Icons.thermostat_rounded, + label: '机库温度', + key: 'cargoTemp', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.flight_rounded, + label: '无人机位置', + key: 'droneDock', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.battery_charging_full_rounded, + label: '充电状态', + key: 'chargeState', + defaultColor: Color(0xFFFF7D00), + ), + _FieldDef( + icon: Icons.water_drop_rounded, + label: '湿度', + key: 'humidity', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.air_rounded, + label: '风速', + key: 'windSpeed', + defaultColor: Color(0xFF722ED1), + ), + _FieldDef( + icon: Icons.umbrella_rounded, + label: '降雨', + key: 'rainfall', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.cloud_rounded, + label: '外部温度', + key: 'externalTemp', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.warning_amber_rounded, + label: '告警状态', + key: 'alarmState', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.sensors_rounded, + label: '急停状态', + key: 'emergencyStop', + defaultColor: Color(0xFF00B42A), + ), + _FieldDef( + icon: Icons.volume_off_rounded, + label: '静音模式', + key: 'silentMode', + defaultColor: Color(0xFF86909C), + ), + _FieldDef( + icon: Icons.mode_standby_rounded, + label: '运行模式', + key: 'modeCode', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.network_check_rounded, + label: '网络状态', + key: 'networkState', + defaultColor: Color(0xFF86909C), + ), + _FieldDef( + icon: Icons.gps_fixed_rounded, + label: '定位状态', + key: 'positionState', + defaultColor: Color(0xFF86909C), + ), + _FieldDef( + icon: Icons.storage_rounded, + label: '存储状态', + key: 'storageState', + defaultColor: Color(0xFF86909C), + ), + _FieldDef( + icon: Icons.north_rounded, + label: '航向角', + key: 'heading', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.height_rounded, + label: '海拔高度', + key: 'altitude', + defaultColor: Color(0xFF165DFF), + ), + _FieldDef( + icon: Icons.phone_android_rounded, + label: '设备型号', + key: 'deviceModel', + defaultColor: Color(0xFF86909C), + ), + _FieldDef( + icon: Icons.battery_saver_rounded, + label: '维护模式', + key: 'batteryStoreMode', + defaultColor: Color(0xFF86909C), + ), + ]; + + /// 每个字段的值和颜色用独立 ValueNotifier + /// 数据更新时只重建对应字段的 Text,不再 setState 整个卡片 + final Map> _valueNotifiers = {}; + final Map> _colorNotifiers = {}; + + /// 静态缓存:跨页面实例保留上次收到的值,进入页面立即显示 + static final Map _cachedValues = {}; + static final Map _cachedColors = {}; + + /// 是否已收到过有效数据(用于首次标记) + bool _hasReceivedData = false; @override void initState() { super.initState(); - _initFields(); + _initNotifiers(); _subscribeStream(); } + void _initNotifiers() { + for (final def in _fieldDefs) { + // 优先用缓存值,没有缓存则显示'未知' + _valueNotifiers[def.key] = ValueNotifier( + _cachedValues[def.key] ?? '未知', + ); + _colorNotifiers[def.key] = ValueNotifier( + _cachedColors[def.key] ?? def.defaultColor, + ); + } + // 如果有缓存数据,标记为已收到 + if (_cachedValues.isNotEmpty) { + _hasReceivedData = true; + } + } + @override void didUpdateWidget(DroneStationOsdCard oldWidget) { super.didUpdateWidget(oldWidget); @@ -44,176 +225,15 @@ class _DroneStationOsdCardState extends State { @override void dispose() { _subscription?.cancel(); + for (final n in _valueNotifiers.values) { + n.dispose(); + } + for (final n in _colorNotifiers.values) { + n.dispose(); + } super.dispose(); } - void _initFields() { - _osdFields.addAll([ - { - 'icon': Icons.ac_unit_rounded, - 'label': '空调状态', - 'key': 'acState', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.timer_rounded, - 'label': '空调计时', - 'key': 'acTime', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, - { - 'icon': Icons.door_sliding_rounded, - 'label': '机库舱门', - 'key': 'coverState', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.push_pin_rounded, - 'label': '推板状态', - 'key': 'putterState', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, - { - 'icon': Icons.lightbulb_rounded, - 'label': '补光灯', - 'key': 'lightState', - 'value': '未知', - 'color': const Color(0xFFFF7D00), - }, - { - 'icon': Icons.thermostat_rounded, - 'label': '机库温度', - 'key': 'cargoTemp', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.flight_rounded, - 'label': '无人机位置', - 'key': 'droneDock', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.battery_charging_full_rounded, - 'label': '充电状态', - 'key': 'chargeState', - 'value': '未知', - 'color': const Color(0xFFFF7D00), - }, - { - 'icon': Icons.water_drop_rounded, - 'label': '湿度', - 'key': 'humidity', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.air_rounded, - 'label': '风速', - 'key': 'windSpeed', - 'value': '未知', - 'color': const Color(0xFF722ED1), - }, - { - 'icon': Icons.umbrella_rounded, - 'label': '降雨', - 'key': 'rainfall', - 'value': '未知', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.cloud_rounded, - 'label': '外部温度', - 'key': 'externalTemp', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.warning_amber_rounded, - 'label': '告警状态', - 'key': 'alarmState', - 'value': '无告警', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.sensors_rounded, - 'label': '急停状态', - 'key': 'emergencyStop', - 'value': '未触发', - 'color': const Color(0xFF00B42A), - }, - { - 'icon': Icons.volume_off_rounded, - 'label': '静音模式', - 'key': 'silentMode', - 'value': '关闭', - 'color': const Color(0xFF86909C), - }, - { - 'icon': Icons.mode_standby_rounded, - 'label': '运行模式', - 'key': 'modeCode', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.network_check_rounded, - 'label': '网络状态', - 'key': 'networkState', - 'value': '未知', - 'color': const Color(0xFF86909C), - }, - { - 'icon': Icons.gps_fixed_rounded, - 'label': '定位状态', - 'key': 'positionState', - 'value': '未知', - 'color': const Color(0xFF86909C), - }, - { - 'icon': Icons.storage_rounded, - 'label': '存储状态', - 'key': 'storageState', - 'value': '未知', - 'color': const Color(0xFF86909C), - }, - { - 'icon': Icons.north_rounded, - 'label': '航向角', - 'key': 'heading', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.height_rounded, - 'label': '海拔高度', - 'key': 'altitude', - 'value': '未知', - 'color': const Color(0xFF165DFF), - }, - { - 'icon': Icons.phone_android_rounded, - 'label': '设备型号', - 'key': 'deviceModel', - 'value': '未知', - 'color': const Color(0xFF86909C), - }, - { - 'icon': Icons.battery_saver_rounded, - 'label': '维护模式', - 'key': 'batteryStoreMode', - 'value': '关闭', - 'color': const Color(0xFF86909C), - }, - ]); - _initialized = true; - } - void _subscribeStream() { if (!widget.isOnline) return; @@ -223,181 +243,257 @@ class _DroneStationOsdCardState extends State { }); } + /// 更新单个字段的值和颜色(仅当值变化时才触发 ValueNotifier 重建) + /// 同时写入静态缓存,下次进入页面可立即显示 + void _updateField(String key, String value, [Color? color]) { + // 写入缓存 + _cachedValues[key] = value; + if (color != null) { + _cachedColors[key] = color; + } + final vNotifier = _valueNotifiers[key]; + if (vNotifier != null && vNotifier.value != value) { + vNotifier.value = value; + } + if (color != null) { + final cNotifier = _colorNotifiers[key]; + if (cNotifier != null && cNotifier.value != color) { + cNotifier.value = color; + } + } + } + + + void _parseOsdFields(DroneOsdEntity osd) { final data = osd.rawData; final hostData = data['data'] is Map ? (data['data'] as Map)['host'] : null; - if (hostData == null || hostData is! Map) return; + if (hostData == null || hostData is! Map) { + debugPrint('⚠️ [StationOsdCard] hostData 为空,rawData keys: ${data.keys.toList()}'); + return; + } - final parsedValues = {}; - final parsedColors = {}; + // 标记已收到数据 + _hasReceivedData = true; + + // 🔥 调试日志:对比卡片期望的字段 vs 实际推送中存在的字段 + final expectedKeys = [ + 'air_conditioner', 'cover_state', 'putter_state', 'supplement_light_state', + 'temperature', 'drone_in_dock', 'drone_charge_state', 'humidity', + 'wind_speed', 'rainfall', 'environment_temperature', 'alarm_state', + 'emergency_stop_state', 'silent_mode', 'mode_code', 'network_state', + 'position_state', 'storage', 'heading', 'height', 'sub_device', + 'battery_store_mode', + ]; + final hostMap = hostData as Map; + final found = expectedKeys.where((k) => hostMap.containsKey(k)).toList(); + final missing = expectedKeys.where((k) => !hostMap.containsKey(k)).toList(); + debugPrint('[StationOsdCard] host keys(${hostMap.length}): ${hostMap.keys.join(", ")}'); + debugPrint('[StationOsdCard] matched: ${found.join(", ")}'); + if (missing.isNotEmpty) { + debugPrint('[StationOsdCard] MISSING: ${missing.join(", ")}'); + } // 空调信息 final acData = hostData['air_conditioner']; if (acData is Map) { final acState = int.tryParse(acData['air_conditioner_state']?.toString() ?? '') ?? -1; - parsedValues['acState'] = acState == 1 - ? '开启' - : acState == 0 - ? '关闭' - : '未知'; - parsedColors['acState'] = acState == 1 - ? const Color(0xFF00B42A) - : acState == 0 - ? const Color(0xFF86909C) - : const Color(0xFF86909C); + if (acState == 0 || acState == 1) { + _updateField( + 'acState', + acState == 1 ? '开启' : '关闭', + acState == 1 + ? const Color(0xFF00B42A) + : const Color(0xFF86909C), + ); + } final switchTime = acData['switch_time']; if (switchTime != null) { - parsedValues['acTime'] = '${switchTime}s'; + _updateField('acTime', '${switchTime}s'); } } - // 机库相关 + // 机库舱门 final coverState = int.tryParse(hostData['cover_state']?.toString() ?? '') ?? -1; - parsedValues['coverState'] = coverState == 1 - ? '开启' - : coverState == 0 - ? '关闭' - : '未知'; - parsedColors['coverState'] = coverState == 1 - ? const Color(0xFFFF7D00) - : coverState == 0 - ? const Color(0xFF00B42A) - : const Color(0xFF86909C); - - final putterState = - int.tryParse(hostData['putter_state']?.toString() ?? '') ?? -1; - parsedValues['putterState'] = putterState == 1 - ? '动作中' - : putterState == 0 - ? '归位' - : '未知'; - parsedColors['putterState'] = putterState == 1 - ? const Color(0xFFFF7D00) - : putterState == 0 - ? const Color(0xFF00B42A) - : const Color(0xFF86909C); - - final lightState = - int.tryParse(hostData['supplement_light_state']?.toString() ?? '') ?? - -1; - parsedValues['lightState'] = lightState == 1 - ? '开启' - : lightState == 0 - ? '关闭' - : '未知'; - parsedColors['lightState'] = lightState == 1 - ? const Color(0xFFFF7D00) - : lightState == 0 - ? const Color(0xFF86909C) - : const Color(0xFF86909C); - - final cargoTemp = (hostData['temperature'] as num?)?.toDouble(); - if (cargoTemp != null) { - parsedValues['cargoTemp'] = '${cargoTemp.toStringAsFixed(1)}°C'; - parsedColors['cargoTemp'] = cargoTemp > 60 - ? const Color(0xFFF53F3F) - : cargoTemp > 40 - ? const Color(0xFFFF7D00) - : const Color(0xFF165DFF); + if (coverState == 0 || coverState == 1) { + _updateField( + 'coverState', + coverState == 1 ? '开启' : '关闭', + coverState == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF00B42A), + ); } + // 推板状态 + final putterState = + int.tryParse(hostData['putter_state']?.toString() ?? '') ?? -1; + if (putterState == 0 || putterState == 1) { + _updateField( + 'putterState', + putterState == 1 ? '动作中' : '归位', + putterState == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF00B42A), + ); + } + + // 补光灯 + final lightState = + int.tryParse(hostData['supplement_light_state']?.toString() ?? '') ?? + -1; + if (lightState == 0 || lightState == 1) { + _updateField( + 'lightState', + lightState == 1 ? '开启' : '关闭', + lightState == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF86909C), + ); + } + + // 机库温度 + final cargoTemp = (hostData['temperature'] as num?)?.toDouble(); + if (cargoTemp != null) { + _updateField( + 'cargoTemp', + '${cargoTemp.toStringAsFixed(1)}°C', + cargoTemp > 60 + ? const Color(0xFFF53F3F) + : cargoTemp > 40 + ? const Color(0xFFFF7D00) + : const Color(0xFF165DFF), + ); + } + + // 无人机位置 final droneDock = int.tryParse(hostData['drone_in_dock']?.toString() ?? '') ?? -1; - parsedValues['droneDock'] = droneDock == 1 - ? '在库内' - : droneDock == 0 - ? '出库' - : '未知'; - parsedColors['droneDock'] = droneDock == 1 - ? const Color(0xFF00B42A) - : droneDock == 0 - ? const Color(0xFFFF7D00) - : const Color(0xFF86909C); + if (droneDock == 0 || droneDock == 1) { + _updateField( + 'droneDock', + droneDock == 1 ? '在库内' : '出库', + droneDock == 1 + ? const Color(0xFF00B42A) + : const Color(0xFFFF7D00), + ); + } - // 无人机电量和充电状态 + // 充电状态 final chargeStateData = hostData['drone_charge_state']; if (chargeStateData is Map) { final chargeState = int.tryParse(chargeStateData['state']?.toString() ?? '') ?? -1; - parsedValues['chargeState'] = chargeState == 1 - ? '充电中' - : chargeState == 0 - ? '未充电' - : '未知'; - parsedColors['chargeState'] = chargeState == 1 - ? const Color(0xFFFF7D00) - : const Color(0xFF86909C); + if (chargeState == 0 || chargeState == 1) { + _updateField( + 'chargeState', + chargeState == 1 ? '充电中' : '未充电', + chargeState == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF86909C), + ); + } } - // 环境信息 + // 湿度 final humidity = (hostData['humidity'] as num?)?.toDouble(); if (humidity != null) { - parsedValues['humidity'] = '${humidity.toStringAsFixed(0)}%'; - parsedColors['humidity'] = const Color(0xFF00B42A); + _updateField( + 'humidity', + '${humidity.toStringAsFixed(0)}%', + const Color(0xFF00B42A), + ); } + // 风速 final windSpeed = (hostData['wind_speed'] as num?)?.toDouble(); if (windSpeed != null) { - parsedValues['windSpeed'] = '${windSpeed.toStringAsFixed(1)} m/s'; - parsedColors['windSpeed'] = windSpeed > 10 - ? const Color(0xFFF53F3F) - : windSpeed > 5 - ? const Color(0xFFFF7D00) - : const Color(0xFF722ED1); + _updateField( + 'windSpeed', + '${windSpeed.toStringAsFixed(1)} m/s', + windSpeed > 10 + ? const Color(0xFFF53F3F) + : windSpeed > 5 + ? const Color(0xFFFF7D00) + : const Color(0xFF722ED1), + ); } - final rainfall = int.tryParse(hostData['rainfall']?.toString() ?? '') ?? -1; - parsedValues['rainfall'] = rainfall == 1 - ? '降雨中' - : rainfall == 0 - ? '无降雨' - : '未知'; - parsedColors['rainfall'] = rainfall == 1 - ? const Color(0xFF165DFF) - : const Color(0xFF00B42A); + // 降雨 + final rainfall = + int.tryParse(hostData['rainfall']?.toString() ?? '') ?? -1; + if (rainfall >= 0) { + _updateField( + 'rainfall', + rainfall == 1 ? '降雨中' : '无降雨', + rainfall == 1 + ? const Color(0xFF165DFF) + : const Color(0xFF00B42A), + ); + } - final externalTemp = (hostData['environment_temperature'] as num?) - ?.toDouble(); + // 外部温度 + final externalTemp = + (hostData['environment_temperature'] as num?)?.toDouble(); if (externalTemp != null) { - parsedValues['externalTemp'] = '${externalTemp.toStringAsFixed(1)}°C'; - parsedColors['externalTemp'] = externalTemp > 40 - ? const Color(0xFFF53F3F) - : externalTemp > 25 - ? const Color(0xFFFF7D00) - : const Color(0xFF165DFF); + _updateField( + 'externalTemp', + '${externalTemp.toStringAsFixed(1)}°C', + externalTemp > 40 + ? const Color(0xFFF53F3F) + : externalTemp > 25 + ? const Color(0xFFFF7D00) + : const Color(0xFF165DFF), + ); } // 告警状态 - final alarmState = - int.tryParse(hostData['alarm_state']?.toString() ?? '') ?? 0; - parsedValues['alarmState'] = alarmState != 0 ? '告警中' : '无告警'; - parsedColors['alarmState'] = alarmState != 0 - ? const Color(0xFFF53F3F) - : const Color(0xFF00B42A); + if (hostData.containsKey('alarm_state')) { + final alarmState = + int.tryParse(hostData['alarm_state']?.toString() ?? '') ?? 0; + _updateField( + 'alarmState', + alarmState != 0 ? '告警中' : '无告警', + alarmState != 0 + ? const Color(0xFFF53F3F) + : const Color(0xFF00B42A), + ); + } // 急停状态 - final emergencyStop = - int.tryParse(hostData['emergency_stop_state']?.toString() ?? '') ?? 0; - parsedValues['emergencyStop'] = emergencyStop == 1 ? '已触发' : '未触发'; - parsedColors['emergencyStop'] = emergencyStop == 1 - ? const Color(0xFFF53F3F) - : const Color(0xFF00B42A); + if (hostData.containsKey('emergency_stop_state')) { + final emergencyStop = + int.tryParse(hostData['emergency_stop_state']?.toString() ?? '') ?? + 0; + _updateField( + 'emergencyStop', + emergencyStop == 1 ? '已触发' : '未触发', + emergencyStop == 1 + ? const Color(0xFFF53F3F) + : const Color(0xFF00B42A), + ); + } // 静音模式 - final silentMode = - int.tryParse(hostData['silent_mode']?.toString() ?? '') ?? 0; - parsedValues['silentMode'] = silentMode == 1 ? '开启' : '关闭'; - parsedColors['silentMode'] = silentMode == 1 - ? const Color(0xFFFF7D00) - : const Color(0xFF86909C); + if (hostData.containsKey('silent_mode')) { + final silentMode = + int.tryParse(hostData['silent_mode']?.toString() ?? '') ?? 0; + _updateField( + 'silentMode', + silentMode == 1 ? '开启' : '关闭', + silentMode == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF86909C), + ); + } // 运行模式 final modeCode = hostData['mode_code']; if (modeCode != null) { - parsedValues['modeCode'] = '模式 $modeCode'; + _updateField('modeCode', '模式 $modeCode'); } // 网络状态 @@ -406,13 +502,15 @@ class _DroneStationOsdCardState extends State { final type = networkState['type']; final quality = networkState['quality']; final typeStr = type == 2 ? '4G' : '网络'; - parsedValues['networkState'] = '$typeStr Q$quality'; - parsedColors['networkState'] = - (quality != null && - int.tryParse(quality.toString()) != null && - int.parse(quality.toString()) > 3) - ? const Color(0xFF00B42A) - : const Color(0xFF86909C); + _updateField( + 'networkState', + '$typeStr Q$quality', + (quality != null && + int.tryParse(quality.toString()) != null && + int.parse(quality.toString()) > 3) + ? const Color(0xFF00B42A) + : const Color(0xFF86909C), + ); } // 定位状态 @@ -420,10 +518,13 @@ class _DroneStationOsdCardState extends State { if (positionState is Map) { final isFixed = positionState['is_fixed']; final gpsNum = positionState['gps_number']; - parsedValues['positionState'] = 'GPS:$gpsNum RTK:$isFixed'; - parsedColors['positionState'] = isFixed == '2' - ? const Color(0xFF00B42A) - : const Color(0xFFFF7D00); + _updateField( + 'positionState', + 'GPS:$gpsNum RTK:$isFixed', + isFixed == '2' || isFixed == 2 + ? const Color(0xFF00B42A) + : const Color(0xFFFF7D00), + ); } // 存储状态 @@ -433,24 +534,26 @@ class _DroneStationOsdCardState extends State { final used = storage['used']; if (total != null && used != null) { final usedPercent = (used / total * 100).toStringAsFixed(0); - parsedValues['storageState'] = '$usedPercent%'; - parsedColors['storageState'] = - int.tryParse(usedPercent) != null && int.parse(usedPercent) > 80 - ? const Color(0xFFFF7D00) - : const Color(0xFF00B42A); + _updateField( + 'storageState', + '$usedPercent%', + int.tryParse(usedPercent) != null && int.parse(usedPercent) > 80 + ? const Color(0xFFFF7D00) + : const Color(0xFF00B42A), + ); } } // 航向角 final heading = (hostData['heading'] as num?)?.toDouble(); if (heading != null) { - parsedValues['heading'] = '${heading.toStringAsFixed(1)}°'; + _updateField('heading', '${heading.toStringAsFixed(1)}°'); } // 海拔高度 final altitude = (hostData['height'] as num?)?.toDouble(); if (altitude != null) { - parsedValues['altitude'] = '${altitude.toStringAsFixed(1)}m'; + _updateField('altitude', '${altitude.toStringAsFixed(1)}m'); } // 设备型号 @@ -458,39 +561,28 @@ class _DroneStationOsdCardState extends State { if (subDevice is Map) { final modelKey = subDevice['device_model_key']; if (modelKey != null) { - parsedValues['deviceModel'] = modelKey.toString(); + _updateField('deviceModel', modelKey.toString()); } } // 维护模式 - final batteryStoreMode = - int.tryParse(hostData['battery_store_mode']?.toString() ?? '') ?? 0; - parsedValues['batteryStoreMode'] = batteryStoreMode == 1 ? '开启' : '关闭'; - parsedColors['batteryStoreMode'] = batteryStoreMode == 1 - ? const Color(0xFFFF7D00) - : const Color(0xFF86909C); + if (hostData.containsKey('battery_store_mode')) { + final batteryStoreMode = + int.tryParse(hostData['battery_store_mode']?.toString() ?? '') ?? 0; + _updateField( + 'batteryStoreMode', + batteryStoreMode == 1 ? '开启' : '关闭', + batteryStoreMode == 1 + ? const Color(0xFFFF7D00) + : const Color(0xFF86909C), + ); + } - // 应用更新到字段列表 - if (!mounted) return; - setState(() { - for (var field in _osdFields) { - final key = field['key'] as String; - if (parsedValues.containsKey(key)) { - _cachedValues[key] = parsedValues[key]!; - field['value'] = parsedValues[key]; - } else if (_cachedValues.containsKey(key)) { - field['value'] = _cachedValues[key]; - } - if (parsedColors.containsKey(key)) { - field['color'] = parsedColors[key]; - } - } - }); } @override Widget build(BuildContext context) { - if (!widget.isOnline) { + if (!widget.isOnline && !_hasReceivedData) { return _buildOfflineCard(); } return _buildOsdGridCard(); @@ -518,10 +610,10 @@ class _DroneStationOsdCardState extends State { color: Color(0xFF86909C), ), const SizedBox(width: 16), - Expanded( + const Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ Text( '机场离线', style: TextStyle( @@ -597,8 +689,8 @@ class _DroneStationOsdCardState extends State { const columns = 3; final itemWidth = (constraints.maxWidth - spacing * (columns - 1)) / columns; - final items = _osdFields.map((field) { - return _buildOsdGridItem(field, itemWidth); + final items = _fieldDefs.map((def) { + return _buildOsdGridItem(def, itemWidth); }).toList(); return Wrap( spacing: spacing, @@ -612,53 +704,63 @@ class _DroneStationOsdCardState extends State { ); } - Widget _buildOsdGridItem(Map field, double width) { - final color = field['color'] as Color; + /// 每个字段用 ValueListenableBuilder 包裹,数据变化时只重建这一格 + Widget _buildOsdGridItem(_FieldDef def, double width) { return SizedBox( width: width, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), - decoration: BoxDecoration( - color: color.withOpacity(0.08), - borderRadius: BorderRadius.circular(6), - border: Border.all(color: color.withOpacity(0.2), width: 1), - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.center, + child: ValueListenableBuilder( + valueListenable: _colorNotifiers[def.key]!, + builder: (context, color, _) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6), + decoration: BoxDecoration( + color: color.withOpacity(0.08), + borderRadius: BorderRadius.circular(6), + border: Border.all(color: color.withOpacity(0.2), width: 1), + ), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Icon(field['icon'] as IconData, color: color, size: 14), - const SizedBox(width: 2), - Expanded( - child: Text( - field['label'] as String, - style: const TextStyle( - fontSize: 9, - color: Color(0xFF86909C), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(def.icon, color: color, size: 14), + const SizedBox(width: 2), + Expanded( + child: Text( + def.label, + style: const TextStyle( + fontSize: 9, + color: Color(0xFF86909C), + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), + ], + ), + const SizedBox(height: 3), + ValueListenableBuilder( + valueListenable: _valueNotifiers[def.key]!, + builder: (context, value, _) { + return Text( + value, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: color, + ), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ); + }, ), ], ), - const SizedBox(height: 3), - Text( - field['value'] as String, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: color, - ), - textAlign: TextAlign.center, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), + ); + }, ), ); } diff --git a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart index 60282ec5..81189972 100644 --- a/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart +++ b/lib/features/v2/device_list/presentation/widgets/robot_header_card.dart @@ -41,7 +41,7 @@ class _RobotHeaderCardState extends State { 'alignment': Alignment.bottomRight, 'icon': Icons.arrow_forward, }, - {'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, + {'name': '全景', 'alignment': Alignment.center, 'icon': Icons.view_agenda}, ]; @override diff --git a/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart index 4a0cf00c..8a3111b5 100644 --- a/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart +++ b/lib/features/v2/workorder/data/datasources/workorder_remote_datasource_impl.dart @@ -23,18 +23,23 @@ class WorkOrderRemoteDataSourceImpl implements WorkOrderRemoteDataSource { if (orgId != null) { queryParameters['orgId'] = orgId; } + print('[WorkOrder] 请求URL: ${HttpApiConsts.workOrderList}'); + print('[WorkOrder] 请求参数: $queryParameters'); final response = await _dio.get( HttpApiConsts.workOrderList, queryParameters: queryParameters, ); final responseData = response.data; + print('[WorkOrder] 响应状态码: ${response.statusCode}'); + print('[WorkOrder] 响应数据: $responseData'); final int code = responseData['code'] ?? -1; if (code != 0 && code != 200) { throw Exception('获取工单列表失败: code=$code'); } final List rows = responseData['rows'] ?? []; + print('[WorkOrder] 返回数据条数: ${rows.length}'); return rows .map((item) => WorkOrderModel.fromJson(item as Map)) .toList(); diff --git a/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart b/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart index 9e4b69bc..52d8bb95 100644 --- a/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart +++ b/lib/features/v2/workorder/presentation/cubit/workorder_cubit.dart @@ -3,7 +3,6 @@ import 'package:equatable/equatable.dart'; import 'package:flutter/foundation.dart'; import 'package:get_it/get_it.dart'; import 'package:maibu_satabot_v2/features/v2/site/presentation/cubit/site_cubit.dart'; -import '../../../../../core/app/app_user_cubit.dart'; import '../../../../../core/consts/workorder_consts.dart'; import '../../../../../core/error/failure.dart'; import '../../../../../core/error/workorder_failure.dart'; @@ -115,7 +114,6 @@ class WorkOrderCubit extends Cubit { try { final siteId = GetIt.I().state.selectedSite?.id; - final orgId = GetIt.I().state.user?.orgId; List allWorkOrders = []; String? errorMessage; @@ -124,7 +122,6 @@ class WorkOrderCubit extends Cubit { page: 1, pageSize: _pageSize, siteId: siteId, - orgId: orgId, ); listResult.fold((Failure failure) { @@ -169,12 +166,10 @@ class WorkOrderCubit extends Cubit { try { final siteId = GetIt.I().state.selectedSite?.id; - final orgId = GetIt.I().state.user?.orgId; final result = await getWorkOrderListUseCase.execute( page: currentState.currentPage + 1, pageSize: _pageSize, siteId: siteId, - orgId: orgId, ); result.fold( diff --git a/pubspec.lock b/pubspec.lock index 251afdf1..8e83f20c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -984,10 +984,10 @@ packages: dependency: "direct main" description: name: mobile_scanner - sha256: "1b60b8f9d4ce0cb0e7d7bc223c955d083a0737bee66fa1fcfe5de48225e0d5b3" + sha256: ce3f059ebd6dbfab7292bba0e893e354b46730636820d3c9ef69005ce2d55bce url: "https://pub.flutter-io.cn" source: hosted - version: "3.5.7" + version: "7.4.0" mqtt_client: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 6e9a56b9..70f91f8b 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -139,7 +139,7 @@ dependencies: markdown_widget: ^2.0.0 # 产品参数用的 markdown 渲染 flutter_markdown: ^0.7.1 - mobile_scanner: ^3.4.1 + mobile_scanner: ^7.4.0 vibration: ^3.1.8 flutter_patcher: ^0.1.2 # Add flutter_patcher here open_file: ^3.3.2 # 打开文件(安装 APK)