Commit c701cce4 authored by akari's avatar akari
parents 862904c4 fb9b8d79
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,
];
}
...@@ -155,11 +155,15 @@ class AlertInfoBO extends Equatable { ...@@ -155,11 +155,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) {
...@@ -168,11 +172,12 @@ class AlertInfoBO extends Equatable { ...@@ -168,11 +172,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];
} }
/// 用途:首页监护舱右侧设备菜单数据 /// 用途:首页监护舱右侧设备菜单数据
......
...@@ -15,12 +15,13 @@ class CabinRepository { ...@@ -15,12 +15,13 @@ class CabinRepository {
Future<ResponseModel<CabinDetailResponse>> getCabinDetail( Future<ResponseModel<CabinDetailResponse>> getCabinDetail(
String carbinSn, String carbinSn,
) { ) {
if (Constants.useMockData) { // if (Constants.useMockData) {
return _mockCabinDetail(); // return _mockCabinDetail();
} // }
return DioRequest.instance.get<CabinDetailResponse>( return DioRequest.instance.get<CabinDetailResponse>(
'/icu/detail', '/icu/detail',
queryParameters: {'carbinSn': carbinSn}, // queryParameters: {'carbinSn': carbinSn},
queryParameters: {'carbinSn': 'CNA07212L'},
fromJsonT: (data) => fromJsonT: (data) =>
CabinDetailResponse.fromJson(data as Map<String, dynamic>), CabinDetailResponse.fromJson(data as Map<String, dynamic>),
); );
......
/// 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;
}
}
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:laki_icu_app/utils/bluetooth/index.dart'; import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:mqtt_client/mqtt_client.dart'; import 'package:mqtt_client/mqtt_client.dart';
...@@ -69,11 +70,16 @@ class LakiMqttTopics { ...@@ -69,11 +70,16 @@ class LakiMqttTopics {
static String icuUp(String deviceSn) => 'icu/$deviceSn/up'; static String icuUp(String deviceSn) => 'icu/$deviceSn/up';
static String cameraUp(String deviceSn) => 'camera/$deviceSn/up'; // stephen 上行 宠物异常行为处理结果 camera/{SN}/up → CameraResultHandler
static String icuAlarm(String deviceSn) => 'icu/$deviceSn/alarm'; // stephen 上行 传感器(设备)异常告警 icu/{SN}/alarm → DeviceAlarmHandler
static String icuTo(String deviceSn) => 'icu/$deviceSn/to'; static String icuTo(String deviceSn) => 'icu/$deviceSn/to';
static String icuPetBind(String deviceSn) => 'icu/$deviceSn/pet/bind'; static String icuPetBind(String deviceSn) => 'icu/$deviceSn/pet/bind'; // stephen 下行 宠物 绑定、出舱
static String cameraTo(String cabinSn) => 'camera/$cabinSn/to'; // 获取摄像头通道的告警信息 static String cameraTo(String cabinSn) => 'camera/$cabinSn/to'; // stephen 下行 获取摄像头通道的告警信息
static const String clientConnected = r'$events/client_connected'; static const String clientConnected = r'$events/client_connected';
...@@ -175,6 +181,11 @@ class LakiMqttClient { ...@@ -175,6 +181,11 @@ class LakiMqttClient {
_client = client; _client = client;
_listenUpdates(client); _listenUpdates(client);
_emitState(LakiMqttConnectionState.connected); _emitState(LakiMqttConnectionState.connected);
// ignore: avoid_print
print(
'[LakiMqttClient] ✅ MQTT 已连接成功, clientId=${_config.clientId} '
'host=${_config.host}',
);
} }
void subscribe(String topic, {MqttQos qos = MqttQos.atLeastOnce}) { void subscribe(String topic, {MqttQos qos = MqttQos.atLeastOnce}) {
...@@ -263,6 +274,64 @@ class LakiMqttClient { ...@@ -263,6 +274,64 @@ 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
/// [deviceSn] 设备 SN,[type] 操作类型(unbindPet),
/// [petData] 宠物信息,为 null 时发送空对象。
// TODO: 服务端尚未确定具体数据结构,等确定后再调整
int publishPetUnBind({
required String deviceSn,
required String type,
required Map<String, dynamic>? petData,
MqttQos qos = MqttQos.atLeastOnce,
}) {
return publishJson(
LakiMqttTopics.icuPetBind(deviceSn),
{
'eventId': '${DateTime.now().millisecondsSinceEpoch}_${_randomHex(4)}',
'deviceId': deviceSn,
'deviceType': 'ICU',
'timestamp': DateTime.now().millisecondsSinceEpoch,
'type': type,
'data': petData ?? {},
},
qos: qos,
);
}
/// 生成指定长度的随机 hex 字符串
String _randomHex(int length) {
final random = Random();
return List.generate(length, (_) => random.nextInt(16).toRadixString(16)).join();
}
Future<void> disconnect() async { Future<void> disconnect() async {
await _updatesSub?.cancel(); await _updatesSub?.cancel();
_updatesSub = null; _updatesSub = null;
......
...@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart'; ...@@ -4,6 +4,8 @@ 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/models/bo/cabin_detail_response.dart'; import 'package:laki_icu_app/models/bo/cabin_detail_response.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';
import 'package:laki_icu_app/services/cabin_service.dart'; import 'package:laki_icu_app/services/cabin_service.dart';
...@@ -80,6 +82,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -80,6 +82,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_listenMcuReportChanged(); _listenMcuReportChanged();
_listenCareLevelChanged(); _listenCareLevelChanged();
_loadBoundBluetoothDevice(); _loadBoundBluetoothDevice();
// // todo 测试使用 待删除
_fetchCabinDetailAndConnectVideo("CNA07212L");
_connectMqttForDeviceSn("CNA07212L");
} }
/// 监听 WebRTC 连接状态变化并同步到 State /// 监听 WebRTC 连接状态变化并同步到 State
...@@ -155,7 +161,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -155,7 +161,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
).copyWith(sn: reportSn.isEmpty ? null : reportSn); ).copyWith(sn: reportSn.isEmpty ? null : reportSn);
if (bluetoothReadInfo.hasSn) { if (bluetoothReadInfo.hasSn) {
await _storageService.saveBluetoothReadSn(bluetoothReadInfo.sn!); await _storageService.saveBluetoothReadSn(bluetoothReadInfo.sn!);
unawaited(_fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!)); await _connectMqttForDeviceSn(
bluetoothReadInfo.sn!); // stephen 需要保留,选择蓝牙之后获取到sn就需要连接上mqtt
await _fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!);
} }
eventBus.emit( eventBus.emit(
BluetoothReadInfoChangedEvent(bluetoothReadInfo), BluetoothReadInfoChangedEvent(bluetoothReadInfo),
...@@ -163,13 +171,14 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -163,13 +171,14 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_publishMqttMonitorReport(latestReport); _publishMqttMonitorReport(latestReport);
if (isClosed) return; if (isClosed) return;
} }
final patientInfo = latestReport == null final careLevel = latestReport == null
? state.patientInfo ? null
: _patientInfoWithCareLevel(latestReport); : _careLevelTextFromReportValue(latestReport.levelLight);
emit(state.copyWith( emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex, latestBluetoothRawHex: packet.rawHex,
latestMcuReport: latestReport, latestMcuReport: latestReport,
patientInfo: patientInfo, patientInfo: _patientInfoWithCareLevelText(careLevel),
currentCareLevel: careLevel,
metrics: latestReport == null metrics: latestReport == null
? state.metrics ? state.metrics
: _metricsFromBluetoothReport(latestReport), : _metricsFromBluetoothReport(latestReport),
...@@ -194,10 +203,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -194,10 +203,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
'🔥🔥🔥 BLE_READ_DEBUG index_metrics_from_report ' '🔥🔥🔥 BLE_READ_DEBUG index_metrics_from_report '
'${_metricsLogText(metrics)}', '${_metricsLogText(metrics)}',
); );
final careLevel = _careLevelTextFromReportValue(event.report.levelLight);
emit(state.copyWith( emit(state.copyWith(
latestMcuReport: event.report, latestMcuReport: event.report,
metrics: metrics, metrics: metrics,
patientInfo: _patientInfoWithCareLevel(event.report), patientInfo: _patientInfoWithCareLevelText(careLevel),
currentCareLevel: careLevel,
)); ));
}); });
} }
...@@ -206,12 +217,15 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -206,12 +217,15 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_careLevelChangedSub = eventBus.on<CareLevelChangedEvent>().listen((event) { _careLevelChangedSub = eventBus.on<CareLevelChangedEvent>().listen((event) {
if (isClosed) return; if (isClosed) return;
_markCareLevelPending(event.protocolValue); _markCareLevelPending(event.protocolValue);
final careLevel = _careLevelTextFromProtocolValue(event.protocolValue);
final patientInfo = _patientInfoWithCareLevelValue( final patientInfo = _patientInfoWithCareLevelValue(
event.protocolValue, event.protocolValue,
fromReport: false, fromReport: false,
); );
if (patientInfo == null) return; emit(state.copyWith(
emit(state.copyWith(patientInfo: patientInfo)); patientInfo: patientInfo,
currentCareLevel: careLevel,
));
}); });
} }
...@@ -289,8 +303,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -289,8 +303,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
/// 页面初始化入口。 /// 页面初始化入口。
/// ///
/// 数据不再通过 HTTP 获取: /// 数据不再通过 HTTP 获取:
/// - 宠物信息由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取 /// - 宠物信息由 MQTT icu/{SN}/pet/bind 推送获取
/// - 告警信息由 MQTT camera/{sn}/to 推送获取 /// - 摄像头凭证由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{SN}/to 推送获取
/// - 指标数据由蓝牙 MCU 上报实时填充 /// - 指标数据由蓝牙 MCU 上报实时填充
Future<void> loadData() async {} Future<void> loadData() async {}
...@@ -369,14 +384,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -369,14 +384,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
try { try {
final detail = await _cabinService.getCabinDetail(carbinSn); final detail = await _cabinService.getCabinDetail(carbinSn);
if (isClosed) return; if (isClosed) return;
final careLevel = _careLevelFromCabinDetail(detail);
emit(state.copyWith( emit(state.copyWith(
cabinCameraSn: detail.cameraSn, cabinCameraSn: detail.cameraSn,
cabinWifiPwd: detail.wifiPwd, cabinWifiPwd: detail.wifiPwd,
currentCareLevel: careLevel,
patientInfo: detail.petInfo != null patientInfo: detail.petInfo != null
? _mapPetInfoToPatientInfo( ? _mapPetInfoToPatientInfo(
detail.petInfo!, detail.petInfo!,
careLevel: _careLevelFromCabinDetail(detail), careLevel: careLevel,
) )
: state.patientInfo, : state.patientInfo,
)); ));
...@@ -417,10 +434,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -417,10 +434,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
return _careLevelTextFromProtocolValue(detail.monitoringData?.levelLight); return _careLevelTextFromProtocolValue(detail.monitoringData?.levelLight);
} }
PatientInfoBO? _patientInfoWithCareLevel(McuReport report) {
return _patientInfoWithCareLevelValue(report.levelLight);
}
PatientInfoBO? _patientInfoWithCareLevelValue( PatientInfoBO? _patientInfoWithCareLevelValue(
int value, { int value, {
bool fromReport = true, bool fromReport = true,
...@@ -439,6 +452,21 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -439,6 +452,21 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
return current.copyWith(careLevel: careLevel); return current.copyWith(careLevel: careLevel);
} }
PatientInfoBO? _patientInfoWithCareLevelText(String? careLevel) {
final current = state.patientInfo;
if (current == null) return null;
if (careLevel == null || careLevel == current.careLevel) {
return current;
}
return current.copyWith(careLevel: careLevel);
}
String? _careLevelTextFromReportValue(int value) {
final resolvedValue = _resolveCareLevelReportValue(value);
if (resolvedValue == null) return null;
return _careLevelTextFromProtocolValue(resolvedValue);
}
int? _resolveCareLevelReportValue(int value) { int? _resolveCareLevelReportValue(int value) {
final pendingValue = _pendingCareLevelProtocolValue; final pendingValue = _pendingCareLevelProtocolValue;
final pendingSince = _pendingCareLevelSince; final pendingSince = _pendingCareLevelSince;
...@@ -812,6 +840,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -812,6 +840,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_handleCameraAlarmMessage(json); _handleCameraAlarmMessage(json);
return; return;
} }
if (json != null &&
message.topic == LakiMqttTopics.icuPetBind(_mqttDeviceSn ?? '')) {
_handlePetBindMessage(json);
return;
}
emit(state.copyWith( emit(state.copyWith(
mqttMessage: 'MQTT 收到 ${message.topic}: ${message.payload}', mqttMessage: 'MQTT 收到 ${message.topic}: ${message.payload}',
)); ));
...@@ -823,10 +856,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -823,10 +856,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;
...@@ -837,13 +878,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -837,13 +878,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(
...@@ -851,6 +897,47 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -851,6 +897,47 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
)); ));
} }
/// 处理 MQTT pet/bind 宠物绑定消息
///
/// 服务端推送的宠物绑定数据格式:
/// ```json
/// {
/// "type": "bindPet",
/// "data": { "name": "旺财", "type": "dog", "breedTag": "金毛", ... }
/// }
/// ```
/// [type] 为 `bindPet` 时设置 petInfo,为 `unbindPet` 时清空 petInfo
void _handlePetBindMessage(Map<String, dynamic> json) {
final type = json['type'] as String?;
if (type == null) return;
if (type == 'unbindPet') {
// 出舱:清空宠物信息
emit(state.copyWith(clearPatientInfo: true));
return;
}
if (type != 'bindPet') return;
final data = json['data'];
if (data is! Map<String, dynamic>) return;
final patientInfo = PatientInfoBO(
name: (data['name'] as String?) ?? '',
type: (data['type'] as String?) ?? '',
phone: (data['ownerPhone'] as String?) ?? '',
breed: (data['breedTag'] as String?) ?? '',
doctor: '缺失字段',
avatarUrl: (data['avatar'] as String?) ?? '',
disease: (data['disease'] as String?) ?? '',
checkInDate:
formatIsoTime(data['createTime'] as String?, fallback: '缺失字段'),
careLevel: '缺失字段',
);
emit(state.copyWith(patientInfo: patientInfo));
}
/// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致) /// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致)
String _formatTimestamp(int milliseconds) { String _formatTimestamp(int milliseconds) {
final dateTime = final dateTime =
...@@ -1073,26 +1160,65 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -1073,26 +1160,65 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
} }
/// 清空宠物信息(出舱) /// 清空宠物信息(出舱)
///
/// 通过 MQTT 上行出舱数据后清空 state 中的宠物信息。
void clearPatientInfo() { void clearPatientInfo() {
if (isClosed) return; if (isClosed) return;
final deviceSn = state.mqttDeviceSn;
final client = _mqttClient;
debugPrint(
'[出舱] deviceSn=$deviceSn mqttClient=${client != null} isConnected=${client?.isConnected ?? false} patientInfo=${state.patientInfo?.name}');
if (deviceSn != null &&
deviceSn.isNotEmpty &&
client != null &&
client.isConnected) {
try {
final msgId = client.publishPetUnBind(
deviceSn: deviceSn,
type: 'unbindPet', // TODO: 出舱 type 后续等服务端确定后再调整
petData: _patientInfoToMap(),
);
debugPrint(
'[出舱] MQTT publishPetUnBind 成功 msgId=$msgId topic=icu/$deviceSn/pet/bind');
} catch (e) {
debugPrint('[出舱] MQTT publishPetUnBind 失败: $e');
if (!isClosed) {
emit(state.copyWith(mqttMessage: 'MQTT 出舱上报失败: $e'));
}
}
} else {
debugPrint('[出舱] 跳过 MQTT 上行: '
'deviceSn=${deviceSn ?? "null"} '
'hasClient=${client != null} '
'isConnected=${client?.isConnected ?? false}');
}
emit(state.copyWith(clearPatientInfo: true)); emit(state.copyWith(clearPatientInfo: true));
} }
/// 将当前宠物信息转为 Map,供 MQTT 上行使用
Map<String, dynamic>? _patientInfoToMap() {
final info = state.patientInfo;
if (info == null) return null;
return {
'name': info.name,
'petType': info.type,
'phone': info.phone,
'breed': info.breed,
'doctor': info.doctor,
'disease': info.disease,
'checkInDate': info.checkInDate,
'careLevel': info.careLevel,
};
}
/// 设置测试宠物信息(绑定宠物) /// 设置测试宠物信息(绑定宠物)
void setTestPatientInfo() { void setTestPatientInfo() {
if (isClosed) return; if (isClosed) return;
const testPatientInfo = PatientInfoBO( // todo 平板端暂时不做宠物绑定 stephen
name: '妮蔻',
type: '猫',
phone: '130-1111-3333',
breed: '长毛三花',
doctor: '张医生',
disease: '胃炎',
checkInDate: '2025-6-15',
careLevel: '特级',
avatarUrl: '',
);
emit(state.copyWith(patientInfo: testPatientInfo));
} }
/// 获取 WebRTC 渲染器供 UI 层使用 /// 获取 WebRTC 渲染器供 UI 层使用
...@@ -1114,37 +1240,57 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> { ...@@ -1114,37 +1240,57 @@ 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 = state.alerts.where((item) => item != alert).toList();
description: item.description, emit(state.copyWith(
time: item.time, cameraAlerts: removedCameraAlerts,
status: '已处理', 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 = state.alerts.where((item) => item != alert).toList();
description: item.description, emit(state.copyWith(
time: item.time, cameraAlerts: removedCameraAlerts,
status: '误报', alerts: removedAlerts,
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');
} }
return item;
}).toList();
emit(state.copyWith(alerts: updatedAlerts, selectedAlert: null));
} }
@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';
class MonitoringPetInfoCard extends StatelessWidget { class MonitoringPetInfoCard extends StatelessWidget {
...@@ -287,10 +288,16 @@ class MonitoringPetInfoCard extends StatelessWidget { ...@@ -287,10 +288,16 @@ class MonitoringPetInfoCard extends StatelessWidget {
); );
} }
String _truncate(String text, {int maxChars = 10}) {
if (text.length <= maxChars) return text;
return '${text.substring(0, maxChars)}...';
}
Widget _buildInfoBlock(PatientInfoBO info) { Widget _buildInfoBlock(PatientInfoBO info) {
return Column( return Column(
children: [ children: [
_buildInfoItem(Icons.description_outlined, '宠物病因', info.disease), _buildInfoItem(
Icons.description_outlined, '宠物病因', _truncate(info.disease)),
SizedBox(height: 20.h), SizedBox(height: 20.h),
_buildInfoItem(Icons.home_outlined, '入住时间', info.checkInDate), _buildInfoItem(Icons.home_outlined, '入住时间', info.checkInDate),
SizedBox(height: 20.h), SizedBox(height: 20.h),
......
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,
this.isDirty = false,
});
final String warningText;
final DeviceThresholdConfigBO editingConfig;
final bool isDirty;
List<FactorySettingGroup> get leftGroups => [
FactorySettingGroup( FactorySettingGroup(
title: '温度范围设定(℃)', title: '温度范围设定(℃)',
items: [ items: [
FactorySettingItem(label: '温度设定上限:', value: '34'), FactorySettingItem(label: '温度设定上限:', value: '${editingConfig.tempMax}'),
FactorySettingItem(label: '温度设定下限:', value: '17'), FactorySettingItem(label: '温度设定下限:', value: '${editingConfig.tempMin}'),
], ],
), ),
FactorySettingGroup( FactorySettingGroup(
title: '氧浓度范围设定(%)', title: '氧浓度范围设定(%)',
items: [ items: [
FactorySettingItem(label: '氧浓度设定上限:', value: '34'), FactorySettingItem(label: '氧浓度设定上限:', value: '${editingConfig.oxygenMax}'),
FactorySettingItem(label: '氧浓度设定下限:', value: '17'), FactorySettingItem(label: '氧浓度设定下限:', value: '${editingConfig.oxygenMin}'),
], ],
), ),
FactorySettingGroup( FactorySettingGroup(
title: '湿度范围设定(%)', title: '湿度范围设定(%)',
items: [ items: [
FactorySettingItem(label: '湿度设定上限:', value: '34'), FactorySettingItem(label: '湿度设定上限:', value: '${editingConfig.humidityMax}'),
FactorySettingItem(label: '湿度设定下限:', value: '17'), FactorySettingItem(label: '湿度设定下限:', value: '${editingConfig.humidityMin}'),
], ],
), ),
], ];
this.timeLimitGroup = const FactorySettingGroup(
FactorySettingGroup get timeLimitGroup => FactorySettingGroup(
title: '时间上限设定(分钟)', title: '时间上限设定(分钟)',
items: [ items: [
FactorySettingItem(label: 'UV时间设定上限:', value: '34'), FactorySettingItem(label: 'UV时间设定上限:', value: '${editingConfig.uvTimeMax}'),
FactorySettingItem(label: '红外时间设定上限:', value: '17'), FactorySettingItem(label: '红外时间设定上限:', value: '${editingConfig.irTimeMax}'),
FactorySettingItem(label: '雾化时间设定上限:', value: '17'), FactorySettingItem(label: '雾化时间设定上限:', value: '${editingConfig.atomizeTimeMax}'),
], ],
), );
});
final String warningText; FactorySettingsIndexState copyWith({
final List<FactorySettingGroup> leftGroups; String? warningText,
final FactorySettingGroup timeLimitGroup; 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,22 +296,29 @@ class _WideFactoryButton extends StatelessWidget { ...@@ -242,22 +296,29 @@ 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;
return GestureDetector(
onTap: onPressed,
child: Container(
height: 70.h, height: 70.h,
padding: EdgeInsets.symmetric(horizontal: 28.w), padding: EdgeInsets.symmetric(horizontal: 28.w),
decoration: settingsButtonDecoration(active: true), decoration: settingsButtonDecoration(active: isEnabled),
child: Row( child: Row(
children: [ children: [
Text( Text(
'《《', '《',
style: TextStyle( style: TextStyle(
color: const Color(0xFF67C9F5).withValues(alpha: 0.72), color: const Color(0xFF67C9F5).withValues(alpha: isEnabled ? 0.72 : 0.3),
fontSize: 25.sp, fontSize: 25.sp,
fontWeight: FontWeight.w700, fontWeight: FontWeight.w700,
), ),
...@@ -269,22 +330,84 @@ class _FactoryButton extends StatelessWidget { ...@@ -269,22 +330,84 @@ class _FactoryButton extends StatelessWidget {
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: Colors.white, color: isEnabled ? Colors.white : Colors.white.withValues(alpha: 0.5),
fontSize: 25.sp, fontSize: 25.sp,
fontWeight: FontWeight.w800, fontWeight: FontWeight.w800,
), ),
), ),
), ),
Text( Text(
'》》', '》',
style: TextStyle( style: TextStyle(
color: const Color(0xFF67C9F5).withValues(alpha: 0.72), color: const Color(0xFF67C9F5).withValues(alpha: isEnabled ? 0.72 : 0.3),
fontSize: 25.sp, fontSize: 25.sp,
fontWeight: FontWeight.w700, 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