Commit fb9b8d79 authored by 张宏's avatar 张宏

6666666666666

parent 646029b6
export 'device_threshold_config_bloc.dart';
export 'device_threshold_config_event.dart';
export 'device_threshold_config_state.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:laki_icu_app/models/bo/device_threshold_config_bo.dart';
import 'device_threshold_config_event.dart';
import 'device_threshold_config_state.dart';
class DeviceThresholdConfigBloc extends HydratedBloc<DeviceThresholdConfigEvent, DeviceThresholdConfigState> {
DeviceThresholdConfigBloc() : super(const DeviceThresholdConfigState()) {
on<DeviceThresholdConfigLoaded>(_onLoaded);
on<DeviceThresholdConfigTempMaxUpdated>(_onTempMaxUpdated);
on<DeviceThresholdConfigTempMinUpdated>(_onTempMinUpdated);
on<DeviceThresholdConfigOxygenMaxUpdated>(_onOxygenMaxUpdated);
on<DeviceThresholdConfigOxygenMinUpdated>(_onOxygenMinUpdated);
on<DeviceThresholdConfigHumidityMaxUpdated>(_onHumidityMaxUpdated);
on<DeviceThresholdConfigHumidityMinUpdated>(_onHumidityMinUpdated);
on<DeviceThresholdConfigUvTimeMaxUpdated>(_onUvTimeMaxUpdated);
on<DeviceThresholdConfigIrTimeMaxUpdated>(_onIrTimeMaxUpdated);
on<DeviceThresholdConfigAtomizeTimeMaxUpdated>(_onAtomizeTimeMaxUpdated);
on<DeviceThresholdConfigUpdated>(_onConfigUpdated);
on<DeviceThresholdConfigResetToDefault>(_onResetToDefault);
}
Future<void> _onLoaded(
DeviceThresholdConfigLoaded event,
Emitter<DeviceThresholdConfigState> emit,
) async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
emit(state.copyWith(isLoading: false, clearError: true));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
}
void _onTempMaxUpdated(
DeviceThresholdConfigTempMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(tempMax: event.value),
clearError: true,
));
}
void _onTempMinUpdated(
DeviceThresholdConfigTempMinUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(tempMin: event.value),
clearError: true,
));
}
void _onOxygenMaxUpdated(
DeviceThresholdConfigOxygenMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(oxygenMax: event.value),
clearError: true,
));
}
void _onOxygenMinUpdated(
DeviceThresholdConfigOxygenMinUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(oxygenMin: event.value),
clearError: true,
));
}
void _onHumidityMaxUpdated(
DeviceThresholdConfigHumidityMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(humidityMax: event.value),
clearError: true,
));
}
void _onHumidityMinUpdated(
DeviceThresholdConfigHumidityMinUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(humidityMin: event.value),
clearError: true,
));
}
void _onUvTimeMaxUpdated(
DeviceThresholdConfigUvTimeMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(uvTimeMax: event.value),
clearError: true,
));
}
void _onIrTimeMaxUpdated(
DeviceThresholdConfigIrTimeMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(irTimeMax: event.value),
clearError: true,
));
}
void _onAtomizeTimeMaxUpdated(
DeviceThresholdConfigAtomizeTimeMaxUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: state.config.copyWith(atomizeTimeMax: event.value),
clearError: true,
));
}
void _onConfigUpdated(
DeviceThresholdConfigUpdated event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(state.copyWith(
config: event.config,
clearError: true,
));
}
void _onResetToDefault(
DeviceThresholdConfigResetToDefault event,
Emitter<DeviceThresholdConfigState> emit,
) {
emit(const DeviceThresholdConfigState(
config: DeviceThresholdConfigBO.defaultConfig,
));
}
bool isTemperatureOutOfRange(int currentTemp) {
return currentTemp < state.config.tempMin || currentTemp > state.config.tempMax;
}
bool isTemperatureTooLow(int currentTemp) {
return currentTemp < state.config.tempMin;
}
bool isTemperatureTooHigh(int currentTemp) {
return currentTemp > state.config.tempMax;
}
bool isOxygenOutOfRange(int currentOxygen) {
return currentOxygen < state.config.oxygenMin || currentOxygen > state.config.oxygenMax;
}
bool isOxygenTooLow(int currentOxygen) {
return currentOxygen < state.config.oxygenMin;
}
bool isOxygenTooHigh(int currentOxygen) {
return currentOxygen > state.config.oxygenMax;
}
bool isHumidityOutOfRange(int currentHumidity) {
return currentHumidity < state.config.humidityMin || currentHumidity > state.config.humidityMax;
}
bool isHumidityTooLow(int currentHumidity) {
return currentHumidity < state.config.humidityMin;
}
bool isHumidityTooHigh(int currentHumidity) {
return currentHumidity > state.config.humidityMax;
}
bool isUvTimeOverLimit(int currentTime) {
return currentTime > state.config.uvTimeMax;
}
bool isIrTimeOverLimit(int currentTime) {
return currentTime > state.config.irTimeMax;
}
bool isAtomizeTimeOverLimit(int currentTime) {
return currentTime > state.config.atomizeTimeMax;
}
String getTempRangeDescription() {
return '${state.config.tempMin}℃ - ${state.config.tempMax}℃';
}
String getOxygenRangeDescription() {
return '${state.config.oxygenMin}% - ${state.config.oxygenMax}%';
}
String getHumidityRangeDescription() {
return '${state.config.humidityMin}% - ${state.config.humidityMax}%';
}
Map<String, String> getAllThresholdDescriptions() {
return {
'temp': getTempRangeDescription(),
'oxygen': getOxygenRangeDescription(),
'humidity': getHumidityRangeDescription(),
'uvTime': '≤${state.config.uvTimeMax}分钟',
'irTime': '≤${state.config.irTimeMax}分钟',
'atomizeTime': '≤${state.config.atomizeTimeMax}分钟',
};
}
List<String> checkOutOfRangeIndicators({
int? temp,
int? oxygen,
int? humidity,
}) {
final List<String> outOfRange = [];
if (temp != null && isTemperatureOutOfRange(temp)) {
outOfRange.add('温度');
}
if (oxygen != null && isOxygenOutOfRange(oxygen)) {
outOfRange.add('氧浓度');
}
if (humidity != null && isHumidityOutOfRange(humidity)) {
outOfRange.add('湿度');
}
return outOfRange;
}
void updateTempMax(int value) => add(DeviceThresholdConfigTempMaxUpdated(value));
void updateTempMin(int value) => add(DeviceThresholdConfigTempMinUpdated(value));
void updateOxygenMax(int value) => add(DeviceThresholdConfigOxygenMaxUpdated(value));
void updateOxygenMin(int value) => add(DeviceThresholdConfigOxygenMinUpdated(value));
void updateHumidityMax(int value) => add(DeviceThresholdConfigHumidityMaxUpdated(value));
void updateHumidityMin(int value) => add(DeviceThresholdConfigHumidityMinUpdated(value));
void updateUvTimeMax(int value) => add(DeviceThresholdConfigUvTimeMaxUpdated(value));
void updateIrTimeMax(int value) => add(DeviceThresholdConfigIrTimeMaxUpdated(value));
void updateAtomizeTimeMax(int value) => add(DeviceThresholdConfigAtomizeTimeMaxUpdated(value));
void updateConfig(DeviceThresholdConfigBO config) => add(DeviceThresholdConfigUpdated(config));
void resetToDefault() => add(const DeviceThresholdConfigResetToDefault());
@override
DeviceThresholdConfigState? fromJson(Map<String, dynamic> json) {
try {
return DeviceThresholdConfigState(
config: DeviceThresholdConfigBO(
tempMax: json['tempMax'] as int? ?? 34,
tempMin: json['tempMin'] as int? ?? 17,
oxygenMax: json['oxygenMax'] as int? ?? 34,
oxygenMin: json['oxygenMin'] as int? ?? 17,
humidityMax: json['humidityMax'] as int? ?? 34,
humidityMin: json['humidityMin'] as int? ?? 17,
uvTimeMax: json['uvTimeMax'] as int? ?? 34,
irTimeMax: json['irTimeMax'] as int? ?? 17,
atomizeTimeMax: json['atomizeTimeMax'] as int? ?? 17,
),
);
} catch (e) {
return null;
}
}
@override
Map<String, dynamic>? toJson(DeviceThresholdConfigState state) {
return {
'tempMax': state.config.tempMax,
'tempMin': state.config.tempMin,
'oxygenMax': state.config.oxygenMax,
'oxygenMin': state.config.oxygenMin,
'humidityMax': state.config.humidityMax,
'humidityMin': state.config.humidityMin,
'uvTimeMax': state.config.uvTimeMax,
'irTimeMax': state.config.irTimeMax,
'atomizeTimeMax': state.config.atomizeTimeMax,
};
}
}
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_threshold_config_bo.dart';
abstract class DeviceThresholdConfigEvent extends Equatable {
const DeviceThresholdConfigEvent();
@override
List<Object?> get props => [];
}
class DeviceThresholdConfigLoaded extends DeviceThresholdConfigEvent {
const DeviceThresholdConfigLoaded();
}
class DeviceThresholdConfigTempMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigTempMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigTempMinUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigTempMinUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigOxygenMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigOxygenMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigOxygenMinUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigOxygenMinUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigHumidityMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigHumidityMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigHumidityMinUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigHumidityMinUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigUvTimeMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigUvTimeMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigIrTimeMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigIrTimeMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigAtomizeTimeMaxUpdated extends DeviceThresholdConfigEvent {
final int value;
const DeviceThresholdConfigAtomizeTimeMaxUpdated(this.value);
@override
List<Object?> get props => [value];
}
class DeviceThresholdConfigUpdated extends DeviceThresholdConfigEvent {
final DeviceThresholdConfigBO config;
const DeviceThresholdConfigUpdated(this.config);
@override
List<Object?> get props => [config];
}
class DeviceThresholdConfigResetToDefault extends DeviceThresholdConfigEvent {
const DeviceThresholdConfigResetToDefault();
}
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_threshold_config_bo.dart';
class DeviceThresholdConfigState extends Equatable {
const DeviceThresholdConfigState({
this.config = DeviceThresholdConfigBO.defaultConfig,
this.isLoading = false,
this.error,
});
final DeviceThresholdConfigBO config;
final bool isLoading;
final String? error;
DeviceThresholdConfigState copyWith({
DeviceThresholdConfigBO? config,
bool? isLoading,
String? error,
bool clearError = false,
}) {
return DeviceThresholdConfigState(
config: config ?? this.config,
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : error ?? this.error,
);
}
@override
List<Object?> get props => [config, isLoading, error];
}
...@@ -29,6 +29,7 @@ import 'package:flutter_ume_kit_dio_plus/flutter_ume_kit_dio_plus.dart'; ...@@ -29,6 +29,7 @@ import 'package:flutter_ume_kit_dio_plus/flutter_ume_kit_dio_plus.dart';
import 'package:flutter_ume_kit_ui_plus/flutter_ume_kit_ui_plus.dart'; import 'package:flutter_ume_kit_ui_plus/flutter_ume_kit_ui_plus.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_bloc.dart'; import 'package:laki_icu_app/blocs/device_control/device_control_bloc.dart';
import 'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart';
import 'package:marionette_flutter/marionette_flutter.dart'; import 'package:marionette_flutter/marionette_flutter.dart';
Future<void> main(List<String> args) async { Future<void> main(List<String> args) async {
...@@ -77,6 +78,7 @@ class _MyAppState extends State<MyApp> { ...@@ -77,6 +78,7 @@ class _MyAppState extends State<MyApp> {
late final AuthBloc _authBloc; late final AuthBloc _authBloc;
BluetoothReadBloc? _bluetoothReadBloc; BluetoothReadBloc? _bluetoothReadBloc;
DeviceControlBloc? _deviceControlBloc; DeviceControlBloc? _deviceControlBloc;
DeviceThresholdConfigBloc? _deviceThresholdConfigBloc;
StreamSubscription? _tokenExpiredSubscription; StreamSubscription? _tokenExpiredSubscription;
bool _isInitializing = true; bool _isInitializing = true;
...@@ -88,6 +90,9 @@ class _MyAppState extends State<MyApp> { ...@@ -88,6 +90,9 @@ class _MyAppState extends State<MyApp> {
DeviceControlBloc get _deviceControlBlocInstance => DeviceControlBloc get _deviceControlBlocInstance =>
_deviceControlBloc ??= DeviceControlBloc(); _deviceControlBloc ??= DeviceControlBloc();
DeviceThresholdConfigBloc get _deviceThresholdConfigBlocInstance =>
_deviceThresholdConfigBloc ??= DeviceThresholdConfigBloc();
@override @override
void initState() { void initState() {
super.initState(); super.initState();
...@@ -173,6 +178,7 @@ class _MyAppState extends State<MyApp> { ...@@ -173,6 +178,7 @@ class _MyAppState extends State<MyApp> {
_authBloc.close(); _authBloc.close();
_bluetoothReadBloc?.close(); _bluetoothReadBloc?.close();
_deviceControlBloc?.close(); _deviceControlBloc?.close();
_deviceThresholdConfigBloc?.close();
super.dispose(); super.dispose();
} }
...@@ -190,6 +196,9 @@ class _MyAppState extends State<MyApp> { ...@@ -190,6 +196,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider<DeviceControlBloc>.value( BlocProvider<DeviceControlBloc>.value(
value: _deviceControlBlocInstance, value: _deviceControlBlocInstance,
), ),
BlocProvider<DeviceThresholdConfigBloc>.value(
value: _deviceThresholdConfigBlocInstance,
),
// BlocProvider<CounterBloc>(create: (_) => CounterBloc()), // BlocProvider<CounterBloc>(create: (_) => CounterBloc()),
], ],
child: MultiBlocListener( child: MultiBlocListener(
......
...@@ -63,11 +63,13 @@ class CameraAlarmBO { ...@@ -63,11 +63,13 @@ class CameraAlarmBO {
); );
} }
/// 与 Android 行为编码映射完全一致: /// 行为编码 → 标题映射:
/// 1 → 宠物抓挠
/// 2 → 异常液体 /// 2 → 异常液体
/// 3 → 检测到排泄物 /// 3 → 检测到排泄物
/// 其他 → 未知异常 /// 其他 → 未知异常
String get titleByBehaviorCode { String get titleByBehaviorCode {
if (behaviorCode == 1) return '宠物抓挠';
if (behaviorCode == 2) return '异常液体'; if (behaviorCode == 2) return '异常液体';
if (behaviorCode == 3) return '检测到排泄物'; if (behaviorCode == 3) return '检测到排泄物';
return '未知异常'; return '未知异常';
......
import 'package:equatable/equatable.dart';
/// 用途:设备阈值配置数据
/// 涉及页面:设备阈值配置页面
class DeviceThresholdConfigBO extends Equatable {
/// 温度上限 ℃
final int tempMax;
/// 温度下限 ℃
final int tempMin;
/// 氧浓度上限 %
final int oxygenMax;
/// 氧浓度下限 %
final int oxygenMin;
/// 湿度上限 %
final int humidityMax;
/// 湿度下限 %
final int humidityMin;
/// 紫外线时间上限 单位:分钟
final int uvTimeMax;
/// 红外线时间上限 单位:分钟
final int irTimeMax;
/// 雾化时间上限 单位:分钟
final int atomizeTimeMax;
const DeviceThresholdConfigBO({
required this.tempMax,
required this.tempMin,
required this.oxygenMax,
required this.oxygenMin,
required this.humidityMax,
required this.humidityMin,
required this.uvTimeMax,
required this.irTimeMax,
required this.atomizeTimeMax,
});
/// UI 默认初始值(截图默认上限34,下限17)
static const DeviceThresholdConfigBO defaultConfig = DeviceThresholdConfigBO(
tempMax: 34,
tempMin: 17,
oxygenMax: 34,
oxygenMin: 17,
humidityMax: 34,
humidityMin: 17,
uvTimeMax: 34,
irTimeMax: 17,
atomizeTimeMax: 17,
);
/// 拷贝修改(用于表单更新)
DeviceThresholdConfigBO copyWith({
int? tempMax,
int? tempMin,
int? oxygenMax,
int? oxygenMin,
int? humidityMax,
int? humidityMin,
int? uvTimeMax,
int? irTimeMax,
int? atomizeTimeMax,
}) {
return DeviceThresholdConfigBO(
tempMax: tempMax ?? this.tempMax,
tempMin: tempMin ?? this.tempMin,
oxygenMax: oxygenMax ?? this.oxygenMax,
oxygenMin: oxygenMin ?? this.oxygenMin,
humidityMax: humidityMax ?? this.humidityMax,
humidityMin: humidityMin ?? this.humidityMin,
uvTimeMax: uvTimeMax ?? this.uvTimeMax,
irTimeMax: irTimeMax ?? this.irTimeMax,
atomizeTimeMax: atomizeTimeMax ?? this.atomizeTimeMax,
);
}
/// 从 JSON 解析模型
factory DeviceThresholdConfigBO.fromJson(Map<String, dynamic> json) {
return DeviceThresholdConfigBO(
tempMax: json['tempMax'] as int? ?? 34,
tempMin: json['tempMin'] as int? ?? 17,
oxygenMax: json['oxygenMax'] as int? ?? 34,
oxygenMin: json['oxygenMin'] as int? ?? 17,
humidityMax: json['humidityMax'] as int? ?? 34,
humidityMin: json['humidityMin'] as int? ?? 17,
uvTimeMax: json['uvTimeMax'] as int? ?? 34,
irTimeMax: json['irTimeMax'] as int? ?? 17,
atomizeTimeMax: json['atomizeTimeMax'] as int? ?? 17,
);
}
/// 转 JSON(接口请求/本地存储)
Map<String, dynamic> toJson() {
return {
'tempMax': tempMax,
'tempMin': tempMin,
'oxygenMax': oxygenMax,
'oxygenMin': oxygenMin,
'humidityMax': humidityMax,
'humidityMin': humidityMin,
'uvTimeMax': uvTimeMax,
'irTimeMax': irTimeMax,
'atomizeTimeMax': atomizeTimeMax,
};
}
@override
List<Object?> get props => [
tempMax,
tempMin,
oxygenMax,
oxygenMin,
humidityMax,
humidityMin,
uvTimeMax,
irTimeMax,
atomizeTimeMax,
];
}
...@@ -122,11 +122,15 @@ class AlertInfoBO extends Equatable { ...@@ -122,11 +122,15 @@ class AlertInfoBO extends Equatable {
/// 告警处理状态 /// 告警处理状态
final String status; final String status;
/// MQTT 下发告警记录 ID,上行 camera/{SN}/up 时使用
final int? alarmId;
const AlertInfoBO({ const AlertInfoBO({
required this.title, required this.title,
required this.description, required this.description,
required this.time, required this.time,
this.status = '已处理', this.status = '已处理',
this.alarmId,
}); });
factory AlertInfoBO.fromJson(Map<String, dynamic> json) { factory AlertInfoBO.fromJson(Map<String, dynamic> json) {
...@@ -135,11 +139,12 @@ class AlertInfoBO extends Equatable { ...@@ -135,11 +139,12 @@ class AlertInfoBO extends Equatable {
description: json['description'] as String? ?? '', description: json['description'] as String? ?? '',
time: json['time'] as String? ?? '', time: json['time'] as String? ?? '',
status: json['status'] as String? ?? '已处理', status: json['status'] as String? ?? '已处理',
alarmId: json['alarmId'] as int?,
); );
} }
@override @override
List<Object?> get props => [title, description, time, status]; List<Object?> get props => [title, description, time, status, alarmId];
} }
/// 用途:首页监护舱右侧设备菜单数据 /// 用途:首页监护舱右侧设备菜单数据
......
/// ISO 时间字符串 → yyyy-MM-dd HH:mm:ss
///
/// 输入格式: "2026-06-25T17:08:11.234234234"
/// 输出格式: "2026-06-25 17:08:11"
String formatIsoTime(String? isoTime, {String fallback = ''}) {
if (isoTime == null || isoTime.isEmpty) return fallback;
try {
final cleaned =
isoTime.contains('T') ? isoTime.replaceFirst('T', ' ') : isoTime;
final dotIndex = cleaned.indexOf('.');
return dotIndex >= 0 ? cleaned.substring(0, dotIndex) : cleaned;
} catch (_) {
return isoTime;
}
}
...@@ -274,6 +274,33 @@ class LakiMqttClient { ...@@ -274,6 +274,33 @@ class LakiMqttClient {
); );
} }
/// 上行 摄像头宠物异常行为处理结果
///
/// 对应 topic: camera/{SN}/up
/// [status] "handled" = 已处理, "false_alarm" = 误报
int publishCameraAlarmResult({
required String deviceSn,
required int alarmId,
required String status,
MqttQos qos = MqttQos.atLeastOnce,
}) {
return publishJson(
LakiMqttTopics.cameraUp(deviceSn),
{
'eventId': '${DateTime.now().millisecondsSinceEpoch}_${_randomHex(8)}',
'deviceVersionNo': '',
'deviceType': 'ICU',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'type': 'alarm',
'data': {
'alarmId': '$alarmId',
'status': status,
},
},
qos: qos,
);
}
/// 上行 出舱数据 /// 上行 出舱数据
/// create by stephen /// create by stephen
/// [deviceSn] 设备 SN,[type] 操作类型(unbindPet), /// [deviceSn] 设备 SN,[type] 操作类型(unbindPet),
......
...@@ -3,6 +3,7 @@ import 'dart:async'; ...@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart'; import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/utils/date_utils.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart'; import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/models/bo/camera_alarm_bo.dart'; import 'package:laki_icu_app/models/bo/camera_alarm_bo.dart';
...@@ -76,7 +77,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -76,7 +77,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_listenMcuReportChanged(); _listenMcuReportChanged();
_loadBoundBluetoothDevice(); _loadBoundBluetoothDevice();
// todo 测试使用 待删除 // // todo 测试使用 待删除
_fetchCabinDetailAndConnectVideo("CNA07212L"); _fetchCabinDetailAndConnectVideo("CNA07212L");
_connectMqttForDeviceSn("CNA07212L"); _connectMqttForDeviceSn("CNA07212L");
} }
...@@ -712,10 +713,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -712,10 +713,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}); });
} }
/// 处理摄像头告警消息(与 Android qisuanfa_0621 逻辑保持一致) /// 处理 MQTT camera/{SN}/to 摄像头告警消息
/// ///
/// 仅处理 type == "abnormal_behavior" 的消息, /// 服务端推送格式:
/// 按 behaviorCode 映射标题:2→异常液体、3→检测到排泄物、其他→未知异常 /// ```json
/// {
/// "type": "abnormal_behavior",
/// "behaviorCode": 1,
/// "behaviorDesc": "宠物抓挠",
/// "petName": "旺财",
/// ...
/// }
/// ```
void _handleCameraAlarmMessage(Map<String, dynamic> json) { void _handleCameraAlarmMessage(Map<String, dynamic> json) {
if (json['type'] != 'abnormal_behavior') return; if (json['type'] != 'abnormal_behavior') return;
...@@ -726,13 +735,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -726,13 +735,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_seenAlarmEventIds.add(alarm.eventId); _seenAlarmEventIds.add(alarm.eventId);
final timeStr = _formatTimestamp(alarm.timestamp); final timeStr = _formatTimestamp(alarm.timestamp);
final title = alarm.titleByBehaviorCode; final behaviorTitle = alarm.titleByBehaviorCode;
final petName = alarm.petName;
final title = petName != null && petName.isNotEmpty
? '$petName - $behaviorTitle'
: behaviorTitle;
final alert = AlertInfoBO( final alert = AlertInfoBO(
title: title, title: title,
description: title, description: alarm.behaviorDesc ?? behaviorTitle,
time: timeStr, time: timeStr,
status: '待处理', status: '待处理',
alarmId: alarm.alarmId,
); );
emit(state.copyWith( emit(state.copyWith(
...@@ -773,7 +787,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -773,7 +787,7 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
doctor: '缺失字段', doctor: '缺失字段',
avatarUrl: (data['avatar'] as String?) ?? '', avatarUrl: (data['avatar'] as String?) ?? '',
disease: (data['disease'] as String?) ?? '', disease: (data['disease'] as String?) ?? '',
checkInDate: (data['createTime'] as String?) ?? '缺失字段', checkInDate: formatIsoTime(data['createTime'] as String?, fallback: '缺失字段'),
careLevel: '缺失字段', careLevel: '缺失字段',
); );
...@@ -1077,37 +1091,56 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -1077,37 +1091,56 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
} }
/// 标记告警为已处理 /// 标记告警为已处理
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void markAlertAsProcessed(AlertInfoBO alert) { void markAlertAsProcessed(AlertInfoBO alert) {
if (isClosed) return; if (isClosed) return;
final updatedAlerts = state.alerts.map((item) { _publishCameraAlarmResult(alert, 'handled');
if (item.title == alert.title && item.time == alert.time) { final removedCameraAlerts =
return AlertInfoBO( state.cameraAlerts.where((item) => item != alert).toList();
title: item.title, final removedAlerts =
description: item.description, state.alerts.where((item) => item != alert).toList();
time: item.time, emit(state.copyWith(
status: '已处理', cameraAlerts: removedCameraAlerts,
); alerts: removedAlerts,
} selectedAlert: null,
return item; ));
}).toList();
emit(state.copyWith(alerts: updatedAlerts, selectedAlert: null));
} }
/// 标记告警为误报 /// 标记告警为误报
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void markAlertAsFalse(AlertInfoBO alert) { void markAlertAsFalse(AlertInfoBO alert) {
if (isClosed) return; if (isClosed) return;
final updatedAlerts = state.alerts.map((item) { _publishCameraAlarmResult(alert, 'false_alarm');
if (item.title == alert.title && item.time == alert.time) { final removedCameraAlerts =
return AlertInfoBO( state.cameraAlerts.where((item) => item != alert).toList();
title: item.title, final removedAlerts =
description: item.description, state.alerts.where((item) => item != alert).toList();
time: item.time, emit(state.copyWith(
status: '误报', cameraAlerts: removedCameraAlerts,
); alerts: removedAlerts,
} selectedAlert: null,
return item; ));
}).toList(); }
emit(state.copyWith(alerts: updatedAlerts, selectedAlert: null));
/// 上行 MQTT camera/{SN}/up 告警处理结果
void _publishCameraAlarmResult(AlertInfoBO alert, String status) {
final client = _mqttClient;
final deviceSn = _mqttDeviceSn;
if (client == null || !client.isConnected || deviceSn == null || deviceSn.isEmpty) return;
if (alert.alarmId == null) return;
try {
client.publishCameraAlarmResult(
deviceSn: deviceSn,
alarmId: alert.alarmId!,
status: status,
);
debugPrint('[告警处理] MQTT 上行成功 alarmId=${alert.alarmId} status=$status');
} catch (e) {
debugPrint('[告警处理] MQTT 上行失败: $e');
}
} }
@override @override
......
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart'; import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
......
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart';
import 'factory_settings_index_state.dart'; import 'factory_settings_index_state.dart';
class FactorySettingsIndexCubit extends Cubit<FactorySettingsIndexState> { class FactorySettingsIndexCubit extends Cubit<FactorySettingsIndexState> {
FactorySettingsIndexCubit() : super(const FactorySettingsIndexState()); FactorySettingsIndexCubit({
required DeviceThresholdConfigBloc thresholdConfigBloc,
}) : _thresholdConfigBloc = thresholdConfigBloc,
super(FactorySettingsIndexState(
editingConfig: thresholdConfigBloc.state.config,
));
final DeviceThresholdConfigBloc _thresholdConfigBloc;
void editTempMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(tempMax: value),
isDirty: true,
));
}
void editTempMin(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(tempMin: value),
isDirty: true,
));
}
void editOxygenMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(oxygenMax: value),
isDirty: true,
));
}
void editOxygenMin(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(oxygenMin: value),
isDirty: true,
));
}
void editHumidityMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(humidityMax: value),
isDirty: true,
));
}
void editHumidityMin(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(humidityMin: value),
isDirty: true,
));
}
void editUvTimeMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(uvTimeMax: value),
isDirty: true,
));
}
void editIrTimeMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(irTimeMax: value),
isDirty: true,
));
}
void editAtomizeTimeMax(int value) {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(atomizeTimeMax: value),
isDirty: true,
));
}
void applyChanges() {
_thresholdConfigBloc.updateConfig(state.editingConfig);
emit(state.copyWith(isDirty: false));
}
void resetEdits() {
emit(state.copyWith(
editingConfig: _thresholdConfigBloc.state.config,
isDirty: false,
));
}
} }
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_threshold_config_bo.dart';
class FactorySettingItem extends Equatable { class FactorySettingItem extends Equatable {
const FactorySettingItem({ const FactorySettingItem({
...@@ -29,47 +30,63 @@ class FactorySettingGroup extends Equatable { ...@@ -29,47 +30,63 @@ class FactorySettingGroup extends Equatable {
class FactorySettingsIndexState extends Equatable { class FactorySettingsIndexState extends Equatable {
const FactorySettingsIndexState({ const FactorySettingsIndexState({
this.warningText = '非专业人员请勿更改设备各项参数!', this.warningText = '非专业人员请勿更改设备各项参数!',
this.leftGroups = const [ this.editingConfig = DeviceThresholdConfigBO.defaultConfig,
FactorySettingGroup( this.isDirty = false,
title: '温度范围设定(℃)',
items: [
FactorySettingItem(label: '温度设定上限:', value: '34'),
FactorySettingItem(label: '温度设定下限:', value: '17'),
],
),
FactorySettingGroup(
title: '氧浓度范围设定(%)',
items: [
FactorySettingItem(label: '氧浓度设定上限:', value: '34'),
FactorySettingItem(label: '氧浓度设定下限:', value: '17'),
],
),
FactorySettingGroup(
title: '湿度范围设定(%)',
items: [
FactorySettingItem(label: '湿度设定上限:', value: '34'),
FactorySettingItem(label: '湿度设定下限:', value: '17'),
],
),
],
this.timeLimitGroup = const FactorySettingGroup(
title: '时间上限设定(分钟)',
items: [
FactorySettingItem(label: 'UV时间设定上限:', value: '34'),
FactorySettingItem(label: '红外时间设定上限:', value: '17'),
FactorySettingItem(label: '雾化时间设定上限:', value: '17'),
],
),
}); });
final String warningText; final String warningText;
final List<FactorySettingGroup> leftGroups; final DeviceThresholdConfigBO editingConfig;
final FactorySettingGroup timeLimitGroup; final bool isDirty;
List<FactorySettingGroup> get leftGroups => [
FactorySettingGroup(
title: '温度范围设定(℃)',
items: [
FactorySettingItem(label: '温度设定上限:', value: '${editingConfig.tempMax}'),
FactorySettingItem(label: '温度设定下限:', value: '${editingConfig.tempMin}'),
],
),
FactorySettingGroup(
title: '氧浓度范围设定(%)',
items: [
FactorySettingItem(label: '氧浓度设定上限:', value: '${editingConfig.oxygenMax}'),
FactorySettingItem(label: '氧浓度设定下限:', value: '${editingConfig.oxygenMin}'),
],
),
FactorySettingGroup(
title: '湿度范围设定(%)',
items: [
FactorySettingItem(label: '湿度设定上限:', value: '${editingConfig.humidityMax}'),
FactorySettingItem(label: '湿度设定下限:', value: '${editingConfig.humidityMin}'),
],
),
];
FactorySettingGroup get timeLimitGroup => FactorySettingGroup(
title: '时间上限设定(分钟)',
items: [
FactorySettingItem(label: 'UV时间设定上限:', value: '${editingConfig.uvTimeMax}'),
FactorySettingItem(label: '红外时间设定上限:', value: '${editingConfig.irTimeMax}'),
FactorySettingItem(label: '雾化时间设定上限:', value: '${editingConfig.atomizeTimeMax}'),
],
);
FactorySettingsIndexState copyWith({
String? warningText,
DeviceThresholdConfigBO? editingConfig,
bool? isDirty,
}) {
return FactorySettingsIndexState(
warningText: warningText ?? this.warningText,
editingConfig: editingConfig ?? this.editingConfig,
isDirty: isDirty ?? this.isDirty,
);
}
@override @override
List<Object?> get props => [ List<Object?> get props => [
warningText, warningText,
leftGroups, editingConfig,
timeLimitGroup, isDirty,
]; ];
} }
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart';
import 'cubit/factory_settings_index_cubit.dart'; import 'cubit/factory_settings_index_cubit.dart';
import 'widgets/factory_settings_panel.dart'; import 'widgets/factory_settings_panel.dart';
...@@ -10,7 +11,9 @@ class FactorySettingsIndexView extends StatelessWidget { ...@@ -10,7 +11,9 @@ class FactorySettingsIndexView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider( return BlocProvider(
create: (_) => FactorySettingsIndexCubit(), create: (_) => FactorySettingsIndexCubit(
thresholdConfigBloc: context.read<DeviceThresholdConfigBloc>(),
),
child: const FactorySettingsPanel(), child: const FactorySettingsPanel(),
); );
} }
......
...@@ -38,6 +38,7 @@ class FactorySettingsPanel extends StatelessWidget { ...@@ -38,6 +38,7 @@ class FactorySettingsPanel extends StatelessWidget {
Expanded( Expanded(
child: _FactoryActionColumn( child: _FactoryActionColumn(
group: state.timeLimitGroup, group: state.timeLimitGroup,
isDirty: state.isDirty,
), ),
), ),
], ],
...@@ -107,12 +108,17 @@ class _FactorySettingsColumn extends StatelessWidget { ...@@ -107,12 +108,17 @@ class _FactorySettingsColumn extends StatelessWidget {
} }
class _FactoryActionColumn extends StatelessWidget { class _FactoryActionColumn extends StatelessWidget {
const _FactoryActionColumn({required this.group}); const _FactoryActionColumn({
required this.group,
required this.isDirty,
});
final FactorySettingGroup group; final FactorySettingGroup group;
final bool isDirty;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final cubit = context.read<FactorySettingsIndexCubit>();
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
...@@ -130,9 +136,19 @@ class _FactoryActionColumn extends StatelessWidget { ...@@ -130,9 +136,19 @@ class _FactoryActionColumn extends StatelessWidget {
SizedBox(height: 64.h), SizedBox(height: 64.h),
Row( Row(
children: [ children: [
const Expanded(child: _FactoryButton(text: '撤销')), Expanded(
child: _FactoryButton(
text: '撤销',
onPressed: isDirty ? () => cubit.resetEdits() : null,
),
),
SizedBox(width: 34.w), SizedBox(width: 34.w),
const Expanded(child: _FactoryButton(text: '应用更改')), Expanded(
child: _FactoryButton(
text: '应用更改',
onPressed: isDirty ? () => cubit.applyChanges() : null,
),
),
], ],
), ),
], ],
...@@ -175,6 +191,41 @@ class _FactorySettingRow extends StatelessWidget { ...@@ -175,6 +191,41 @@ class _FactorySettingRow extends StatelessWidget {
final FactorySettingItem item; final FactorySettingItem item;
void _onTap(BuildContext context) {
final cubit = context.read<FactorySettingsIndexCubit>();
final currentValue = int.tryParse(item.value) ?? 0;
showDialog<int>(
context: context,
builder: (context) => _EditDialog(
label: item.label,
initialValue: currentValue,
),
).then((value) {
if (value != null) {
if (item.label == '温度设定上限:') {
cubit.editTempMax(value);
} else if (item.label == '温度设定下限:') {
cubit.editTempMin(value);
} else if (item.label == '氧浓度设定上限:') {
cubit.editOxygenMax(value);
} else if (item.label == '氧浓度设定下限:') {
cubit.editOxygenMin(value);
} else if (item.label == '湿度设定上限:') {
cubit.editHumidityMax(value);
} else if (item.label == '湿度设定下限:') {
cubit.editHumidityMin(value);
} else if (item.label == 'UV时间设定上限:') {
cubit.editUvTimeMax(value);
} else if (item.label == '红外时间设定上限:') {
cubit.editIrTimeMax(value);
} else if (item.label == '雾化时间设定上限:') {
cubit.editAtomizeTimeMax(value);
}
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Row( return Row(
...@@ -192,7 +243,10 @@ class _FactorySettingRow extends StatelessWidget { ...@@ -192,7 +243,10 @@ class _FactorySettingRow extends StatelessWidget {
), ),
), ),
SizedBox(width: 16.w), SizedBox(width: 16.w),
_ValueBox(value: item.value), GestureDetector(
onTap: () => _onTap(context),
child: _ValueBox(value: item.value),
),
], ],
); );
} }
...@@ -242,49 +296,118 @@ class _WideFactoryButton extends StatelessWidget { ...@@ -242,49 +296,118 @@ class _WideFactoryButton extends StatelessWidget {
} }
class _FactoryButton extends StatelessWidget { class _FactoryButton extends StatelessWidget {
const _FactoryButton({required this.text}); const _FactoryButton({
required this.text,
this.onPressed,
});
final String text; final String text;
final VoidCallback? onPressed;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Container( final isEnabled = onPressed != null;
height: 70.h, return GestureDetector(
padding: EdgeInsets.symmetric(horizontal: 28.w), onTap: onPressed,
decoration: settingsButtonDecoration(active: true), child: Container(
child: Row( height: 70.h,
children: [ padding: EdgeInsets.symmetric(horizontal: 28.w),
Text( decoration: settingsButtonDecoration(active: isEnabled),
'《《', child: Row(
style: TextStyle( children: [
color: const Color(0xFF67C9F5).withValues(alpha: 0.72), Text(
fontSize: 25.sp, '《',
fontWeight: FontWeight.w700,
),
),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: const Color(0xFF67C9F5).withValues(alpha: isEnabled ? 0.72 : 0.3),
fontSize: 25.sp, fontSize: 25.sp,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w700,
), ),
), ),
), Expanded(
Text( child: Text(
'》》', text,
style: TextStyle( maxLines: 1,
color: const Color(0xFF67C9F5).withValues(alpha: 0.72), overflow: TextOverflow.ellipsis,
fontSize: 25.sp, textAlign: TextAlign.center,
fontWeight: FontWeight.w700, style: TextStyle(
color: isEnabled ? Colors.white : Colors.white.withValues(alpha: 0.5),
fontSize: 25.sp,
fontWeight: FontWeight.w800,
),
),
), ),
), Text(
], '》',
style: TextStyle(
color: const Color(0xFF67C9F5).withValues(alpha: isEnabled ? 0.72 : 0.3),
fontSize: 25.sp,
fontWeight: FontWeight.w700,
),
),
],
),
),
);
}
}
class _EditDialog extends StatefulWidget {
const _EditDialog({
required this.label,
required this.initialValue,
});
final String label;
final int initialValue;
@override
State<_EditDialog> createState() => _EditDialogState();
}
class _EditDialogState extends State<_EditDialog> {
late final TextEditingController _controller;
@override
void initState() {
super.initState();
_controller = TextEditingController(text: '${widget.initialValue}');
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _onSave() {
final value = int.tryParse(_controller.text);
if (value != null) {
Navigator.of(context).pop(value);
}
}
@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text(widget.label),
content: TextField(
controller: _controller,
keyboardType: TextInputType.number,
decoration: const InputDecoration(
hintText: '请输入数值',
),
autofocus: true,
), ),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('取消'),
),
TextButton(
onPressed: _onSave,
child: const Text('确定'),
),
],
); );
} }
} }
...@@ -61,6 +61,7 @@ dependencies: ...@@ -61,6 +61,7 @@ dependencies:
mqtt_client: ^10.5.1 mqtt_client: ^10.5.1
marionette_flutter: ^0.5.0 marionette_flutter: ^0.5.0
hydrated_bloc: ^9.1.5 hydrated_bloc: ^9.1.5
intl: ^0.20.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment