优化ios
This commit is contained in:
@@ -200,31 +200,31 @@ class AuthCubit extends Cubit<AuthState> {
|
||||
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');
|
||||
|
||||
@@ -47,7 +47,7 @@ class _MachineDetailsPageState extends State<MachineDetailsPage> {
|
||||
{'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
|
||||
|
||||
@@ -142,12 +142,15 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
||||
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<BindDevicePage> {
|
||||
child: const Text(
|
||||
'取消',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontSize: 14,
|
||||
height: 1.0,
|
||||
color: Color(0xFF4E5969),
|
||||
),
|
||||
),
|
||||
@@ -168,7 +172,7 @@ class _BindDevicePageState extends State<BindDevicePage> {
|
||||
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<BindDevicePage> {
|
||||
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<BindDevicePage> {
|
||||
: const Text(
|
||||
'确定',
|
||||
style: TextStyle(
|
||||
fontSize: 15,
|
||||
fontSize: 14,
|
||||
height: 1.0,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -36,8 +36,15 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
|
||||
late DroneOsdDataSource _osdDataSource;
|
||||
StreamSubscription<DroneOsdEntity>? _stationOsdSubscription;
|
||||
|
||||
/// 静态缓存:跨页面实例保留上次的 host 数据
|
||||
static final Map<String, dynamic> _cachedHostData = {};
|
||||
|
||||
final ValueNotifier<Map<String, dynamic>> _stationHostData =
|
||||
ValueNotifier<Map<String, dynamic>>({});
|
||||
ValueNotifier<Map<String, dynamic>>(
|
||||
// 初始化时从缓存加载,进入页面立即显示
|
||||
Map<String, dynamic>.from(_cachedHostData),
|
||||
);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -73,47 +80,52 @@ class _DroneStationDetailPageState extends State<DroneStationDetailPage> {
|
||||
final hostData = data['data'] is Map ? (data['data'] as Map)['host'] : null;
|
||||
if (hostData == null || hostData is! Map) return;
|
||||
|
||||
final Map<String, dynamic> parsed = {};
|
||||
// 🔥 基于已有数据合并,只更新非 null 字段,避免 Type 2 消息覆盖已有值
|
||||
final Map<String, dynamic> merged = Map<String, dynamic>.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<String, dynamic>.from(parsed);
|
||||
_stationHostData.value = merged;
|
||||
// 同步写入静态缓存
|
||||
_cachedHostData
|
||||
..clear()
|
||||
..addAll(merged);
|
||||
}
|
||||
|
||||
/// 🔥 页面重新激活时调用(从其他页面返回时)
|
||||
|
||||
@@ -25,10 +25,73 @@ class DroneOsdCard extends StatefulWidget {
|
||||
State<DroneOsdCard> 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<DroneOsdCard> {
|
||||
StreamSubscription<DroneOsdEntity>? _subscription;
|
||||
final List<Map<String, dynamic>> _osdFields = [];
|
||||
final Map<String, String> _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<String, ValueNotifier<String>> _valueNotifiers = {};
|
||||
final Map<String, ValueNotifier<Color>> _colorNotifiers = {};
|
||||
|
||||
/// 是否正在等待任务下发后的无人机推送数据
|
||||
bool _isWaitingForPush = false;
|
||||
@@ -43,7 +106,7 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
@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<DroneOsdCard> {
|
||||
_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,61 +175,13 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
droneTaskStateManager.isWaitingForOsdPush.removeListener(_onWaitingChanged);
|
||||
_localWaitTimeoutTimer?.cancel();
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
for (final n in _valueNotifiers.values) {
|
||||
n.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),
|
||||
},
|
||||
]);
|
||||
for (final n in _colorNotifiers.values) {
|
||||
n.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _subscribeStream() {
|
||||
@@ -171,6 +193,20 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 更新单个字段的值和颜色(仅当值变化时才触发监听者重建)
|
||||
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<DroneOsdCard> {
|
||||
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;
|
||||
});
|
||||
}
|
||||
|
||||
final parsedValues = <String, String>{};
|
||||
final parsedColors = <String, Color>{};
|
||||
// 仅在首次切换卡片显示 / 停止等待转圈时调用 setState(很低频)
|
||||
if (wasFirstData || needStopWaiting) {
|
||||
if (mounted) setState(() {});
|
||||
}
|
||||
|
||||
double? height =
|
||||
(droneData['height'] as num?)?.toDouble() ??
|
||||
@@ -235,77 +273,41 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
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<DroneOsdCard> {
|
||||
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,11 +533,14 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOsdGridItem(Map<String, dynamic> field, double width) {
|
||||
final color = field['color'] as Color;
|
||||
/// 每个字段用 ValueListenableBuilder 包裹,数据变化时只重建这一格
|
||||
Widget _buildOsdGridItem(_FieldDef def, double width) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Container(
|
||||
child: ValueListenableBuilder<Color>(
|
||||
valueListenable: _colorNotifiers[def.key]!,
|
||||
builder: (context, color, _) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
@@ -548,11 +553,11 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(field['icon'] as IconData, color: color, size: 14),
|
||||
Icon(def.icon, color: color, size: 14),
|
||||
const SizedBox(width: 2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
field['label'] as String,
|
||||
def.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Color(0xFF86909C),
|
||||
@@ -565,8 +570,11 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
field['value'] as String,
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _valueNotifiers[def.key]!,
|
||||
builder: (context, value, _) {
|
||||
return Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -575,9 +583,13 @@ class _DroneOsdCardState extends State<DroneOsdCard> {
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,19 +18,200 @@ class DroneStationOsdCard extends StatefulWidget {
|
||||
State<DroneStationOsdCard> 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<DroneStationOsdCard> {
|
||||
StreamSubscription<DroneOsdEntity>? _subscription;
|
||||
final List<Map<String, dynamic>> _osdFields = [];
|
||||
final Map<String, String> _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<String, ValueNotifier<String>> _valueNotifiers = {};
|
||||
final Map<String, ValueNotifier<Color>> _colorNotifiers = {};
|
||||
|
||||
/// 静态缓存:跨页面实例保留上次收到的值,进入页面立即显示
|
||||
static final Map<String, String> _cachedValues = {};
|
||||
static final Map<String, Color> _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,174 +225,13 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
@override
|
||||
void dispose() {
|
||||
_subscription?.cancel();
|
||||
super.dispose();
|
||||
for (final n in _valueNotifiers.values) {
|
||||
n.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;
|
||||
for (final n in _colorNotifiers.values) {
|
||||
n.dispose();
|
||||
}
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _subscribeStream() {
|
||||
@@ -223,181 +243,257 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
});
|
||||
}
|
||||
|
||||
/// 更新单个字段的值和颜色(仅当值变化时才触发 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 = <String, String>{};
|
||||
final parsedColors = <String, Color>{};
|
||||
// 标记已收到数据
|
||||
_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
|
||||
if (acState == 0 || acState == 1) {
|
||||
_updateField(
|
||||
'acState',
|
||||
acState == 1 ? '开启' : '关闭',
|
||||
acState == 1
|
||||
? const Color(0xFF00B42A)
|
||||
: acState == 0
|
||||
? const Color(0xFF86909C)
|
||||
: const Color(0xFF86909C);
|
||||
: 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
|
||||
if (coverState == 0 || coverState == 1) {
|
||||
_updateField(
|
||||
'coverState',
|
||||
coverState == 1 ? '开启' : '关闭',
|
||||
coverState == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: coverState == 0
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFF86909C);
|
||||
: const Color(0xFF00B42A),
|
||||
);
|
||||
}
|
||||
|
||||
// 推板状态
|
||||
final putterState =
|
||||
int.tryParse(hostData['putter_state']?.toString() ?? '') ?? -1;
|
||||
parsedValues['putterState'] = putterState == 1
|
||||
? '动作中'
|
||||
: putterState == 0
|
||||
? '归位'
|
||||
: '未知';
|
||||
parsedColors['putterState'] = putterState == 1
|
||||
if (putterState == 0 || putterState == 1) {
|
||||
_updateField(
|
||||
'putterState',
|
||||
putterState == 1 ? '动作中' : '归位',
|
||||
putterState == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: putterState == 0
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFF86909C);
|
||||
: const Color(0xFF00B42A),
|
||||
);
|
||||
}
|
||||
|
||||
// 补光灯
|
||||
final lightState =
|
||||
int.tryParse(hostData['supplement_light_state']?.toString() ?? '') ??
|
||||
-1;
|
||||
parsedValues['lightState'] = lightState == 1
|
||||
? '开启'
|
||||
: lightState == 0
|
||||
? '关闭'
|
||||
: '未知';
|
||||
parsedColors['lightState'] = lightState == 1
|
||||
if (lightState == 0 || lightState == 1) {
|
||||
_updateField(
|
||||
'lightState',
|
||||
lightState == 1 ? '开启' : '关闭',
|
||||
lightState == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: lightState == 0
|
||||
? const Color(0xFF86909C)
|
||||
: 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
|
||||
_updateField(
|
||||
'cargoTemp',
|
||||
'${cargoTemp.toStringAsFixed(1)}°C',
|
||||
cargoTemp > 60
|
||||
? const Color(0xFFF53F3F)
|
||||
: cargoTemp > 40
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF165DFF);
|
||||
: const Color(0xFF165DFF),
|
||||
);
|
||||
}
|
||||
|
||||
// 无人机位置
|
||||
final droneDock =
|
||||
int.tryParse(hostData['drone_in_dock']?.toString() ?? '') ?? -1;
|
||||
parsedValues['droneDock'] = droneDock == 1
|
||||
? '在库内'
|
||||
: droneDock == 0
|
||||
? '出库'
|
||||
: '未知';
|
||||
parsedColors['droneDock'] = droneDock == 1
|
||||
if (droneDock == 0 || droneDock == 1) {
|
||||
_updateField(
|
||||
'droneDock',
|
||||
droneDock == 1 ? '在库内' : '出库',
|
||||
droneDock == 1
|
||||
? const Color(0xFF00B42A)
|
||||
: droneDock == 0
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF86909C);
|
||||
: 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
|
||||
if (chargeState == 0 || chargeState == 1) {
|
||||
_updateField(
|
||||
'chargeState',
|
||||
chargeState == 1 ? '充电中' : '未充电',
|
||||
chargeState == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF86909C);
|
||||
: 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
|
||||
_updateField(
|
||||
'windSpeed',
|
||||
'${windSpeed.toStringAsFixed(1)} m/s',
|
||||
windSpeed > 10
|
||||
? const Color(0xFFF53F3F)
|
||||
: windSpeed > 5
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF722ED1);
|
||||
: const Color(0xFF722ED1),
|
||||
);
|
||||
}
|
||||
|
||||
final rainfall = int.tryParse(hostData['rainfall']?.toString() ?? '') ?? -1;
|
||||
parsedValues['rainfall'] = rainfall == 1
|
||||
? '降雨中'
|
||||
: rainfall == 0
|
||||
? '无降雨'
|
||||
: '未知';
|
||||
parsedColors['rainfall'] = rainfall == 1
|
||||
// 降雨
|
||||
final rainfall =
|
||||
int.tryParse(hostData['rainfall']?.toString() ?? '') ?? -1;
|
||||
if (rainfall >= 0) {
|
||||
_updateField(
|
||||
'rainfall',
|
||||
rainfall == 1 ? '降雨中' : '无降雨',
|
||||
rainfall == 1
|
||||
? const Color(0xFF165DFF)
|
||||
: const Color(0xFF00B42A);
|
||||
: 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
|
||||
_updateField(
|
||||
'externalTemp',
|
||||
'${externalTemp.toStringAsFixed(1)}°C',
|
||||
externalTemp > 40
|
||||
? const Color(0xFFF53F3F)
|
||||
: externalTemp > 25
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF165DFF);
|
||||
: const Color(0xFF165DFF),
|
||||
);
|
||||
}
|
||||
|
||||
// 告警状态
|
||||
if (hostData.containsKey('alarm_state')) {
|
||||
final alarmState =
|
||||
int.tryParse(hostData['alarm_state']?.toString() ?? '') ?? 0;
|
||||
parsedValues['alarmState'] = alarmState != 0 ? '告警中' : '无告警';
|
||||
parsedColors['alarmState'] = alarmState != 0
|
||||
_updateField(
|
||||
'alarmState',
|
||||
alarmState != 0 ? '告警中' : '无告警',
|
||||
alarmState != 0
|
||||
? const Color(0xFFF53F3F)
|
||||
: const Color(0xFF00B42A);
|
||||
: const Color(0xFF00B42A),
|
||||
);
|
||||
}
|
||||
|
||||
// 急停状态
|
||||
if (hostData.containsKey('emergency_stop_state')) {
|
||||
final emergencyStop =
|
||||
int.tryParse(hostData['emergency_stop_state']?.toString() ?? '') ?? 0;
|
||||
parsedValues['emergencyStop'] = emergencyStop == 1 ? '已触发' : '未触发';
|
||||
parsedColors['emergencyStop'] = emergencyStop == 1
|
||||
int.tryParse(hostData['emergency_stop_state']?.toString() ?? '') ??
|
||||
0;
|
||||
_updateField(
|
||||
'emergencyStop',
|
||||
emergencyStop == 1 ? '已触发' : '未触发',
|
||||
emergencyStop == 1
|
||||
? const Color(0xFFF53F3F)
|
||||
: const Color(0xFF00B42A);
|
||||
: const Color(0xFF00B42A),
|
||||
);
|
||||
}
|
||||
|
||||
// 静音模式
|
||||
if (hostData.containsKey('silent_mode')) {
|
||||
final silentMode =
|
||||
int.tryParse(hostData['silent_mode']?.toString() ?? '') ?? 0;
|
||||
parsedValues['silentMode'] = silentMode == 1 ? '开启' : '关闭';
|
||||
parsedColors['silentMode'] = silentMode == 1
|
||||
_updateField(
|
||||
'silentMode',
|
||||
silentMode == 1 ? '开启' : '关闭',
|
||||
silentMode == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF86909C);
|
||||
: 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<DroneStationOsdCard> {
|
||||
final type = networkState['type'];
|
||||
final quality = networkState['quality'];
|
||||
final typeStr = type == 2 ? '4G' : '网络';
|
||||
parsedValues['networkState'] = '$typeStr Q$quality';
|
||||
parsedColors['networkState'] =
|
||||
_updateField(
|
||||
'networkState',
|
||||
'$typeStr Q$quality',
|
||||
(quality != null &&
|
||||
int.tryParse(quality.toString()) != null &&
|
||||
int.parse(quality.toString()) > 3)
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFF86909C);
|
||||
: const Color(0xFF86909C),
|
||||
);
|
||||
}
|
||||
|
||||
// 定位状态
|
||||
@@ -420,10 +518,13 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
if (positionState is Map) {
|
||||
final isFixed = positionState['is_fixed'];
|
||||
final gpsNum = positionState['gps_number'];
|
||||
parsedValues['positionState'] = 'GPS:$gpsNum RTK:$isFixed';
|
||||
parsedColors['positionState'] = isFixed == '2'
|
||||
_updateField(
|
||||
'positionState',
|
||||
'GPS:$gpsNum RTK:$isFixed',
|
||||
isFixed == '2' || isFixed == 2
|
||||
? const Color(0xFF00B42A)
|
||||
: const Color(0xFFFF7D00);
|
||||
: const Color(0xFFFF7D00),
|
||||
);
|
||||
}
|
||||
|
||||
// 存储状态
|
||||
@@ -433,24 +534,26 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
final used = storage['used'];
|
||||
if (total != null && used != null) {
|
||||
final usedPercent = (used / total * 100).toStringAsFixed(0);
|
||||
parsedValues['storageState'] = '$usedPercent%';
|
||||
parsedColors['storageState'] =
|
||||
_updateField(
|
||||
'storageState',
|
||||
'$usedPercent%',
|
||||
int.tryParse(usedPercent) != null && int.parse(usedPercent) > 80
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF00B42A);
|
||||
: 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<DroneStationOsdCard> {
|
||||
if (subDevice is Map) {
|
||||
final modelKey = subDevice['device_model_key'];
|
||||
if (modelKey != null) {
|
||||
parsedValues['deviceModel'] = modelKey.toString();
|
||||
_updateField('deviceModel', modelKey.toString());
|
||||
}
|
||||
}
|
||||
|
||||
// 维护模式
|
||||
if (hostData.containsKey('battery_store_mode')) {
|
||||
final batteryStoreMode =
|
||||
int.tryParse(hostData['battery_store_mode']?.toString() ?? '') ?? 0;
|
||||
parsedValues['batteryStoreMode'] = batteryStoreMode == 1 ? '开启' : '关闭';
|
||||
parsedColors['batteryStoreMode'] = batteryStoreMode == 1
|
||||
_updateField(
|
||||
'batteryStoreMode',
|
||||
batteryStoreMode == 1 ? '开启' : '关闭',
|
||||
batteryStoreMode == 1
|
||||
? const Color(0xFFFF7D00)
|
||||
: const Color(0xFF86909C);
|
||||
: 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<DroneStationOsdCard> {
|
||||
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<DroneStationOsdCard> {
|
||||
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,11 +704,14 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildOsdGridItem(Map<String, dynamic> field, double width) {
|
||||
final color = field['color'] as Color;
|
||||
/// 每个字段用 ValueListenableBuilder 包裹,数据变化时只重建这一格
|
||||
Widget _buildOsdGridItem(_FieldDef def, double width) {
|
||||
return SizedBox(
|
||||
width: width,
|
||||
child: Container(
|
||||
child: ValueListenableBuilder<Color>(
|
||||
valueListenable: _colorNotifiers[def.key]!,
|
||||
builder: (context, color, _) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withOpacity(0.08),
|
||||
@@ -629,11 +724,11 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(field['icon'] as IconData, color: color, size: 14),
|
||||
Icon(def.icon, color: color, size: 14),
|
||||
const SizedBox(width: 2),
|
||||
Expanded(
|
||||
child: Text(
|
||||
field['label'] as String,
|
||||
def.label,
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Color(0xFF86909C),
|
||||
@@ -646,8 +741,11 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
field['value'] as String,
|
||||
ValueListenableBuilder<String>(
|
||||
valueListenable: _valueNotifiers[def.key]!,
|
||||
builder: (context, value, _) {
|
||||
return Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
@@ -656,9 +754,13 @@ class _DroneStationOsdCardState extends State<DroneStationOsdCard> {
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ class _RobotHeaderCardState extends State<RobotHeaderCard> {
|
||||
'alignment': Alignment.bottomRight,
|
||||
'icon': Icons.arrow_forward,
|
||||
},
|
||||
{'name': '俯视', 'alignment': Alignment.center, 'icon': Icons.view_agenda},
|
||||
{'name': '全景', 'alignment': Alignment.center, 'icon': Icons.view_agenda},
|
||||
];
|
||||
|
||||
@override
|
||||
|
||||
@@ -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<dynamic> rows = responseData['rows'] ?? [];
|
||||
print('[WorkOrder] 返回数据条数: ${rows.length}');
|
||||
return rows
|
||||
.map((item) => WorkOrderModel.fromJson(item as Map<String, dynamic>))
|
||||
.toList();
|
||||
|
||||
@@ -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<WorkOrderState> {
|
||||
|
||||
try {
|
||||
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
|
||||
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId;
|
||||
|
||||
List<WorkOrderEntity> allWorkOrders = [];
|
||||
String? errorMessage;
|
||||
@@ -124,7 +122,6 @@ class WorkOrderCubit extends Cubit<WorkOrderState> {
|
||||
page: 1,
|
||||
pageSize: _pageSize,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
);
|
||||
|
||||
listResult.fold((Failure failure) {
|
||||
@@ -169,12 +166,10 @@ class WorkOrderCubit extends Cubit<WorkOrderState> {
|
||||
|
||||
try {
|
||||
final siteId = GetIt.I<SiteCubit>().state.selectedSite?.id;
|
||||
final orgId = GetIt.I<AppUserCubit>().state.user?.orgId;
|
||||
final result = await getWorkOrderListUseCase.execute(
|
||||
page: currentState.currentPage + 1,
|
||||
pageSize: _pageSize,
|
||||
siteId: siteId,
|
||||
orgId: orgId,
|
||||
);
|
||||
|
||||
result.fold(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user