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';
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_threshold_config/device_threshold_config.dart';
import 'package:marionette_flutter/marionette_flutter.dart';
Future<void> main(List<String> args) async {
......@@ -77,6 +78,7 @@ class _MyAppState extends State<MyApp> {
late final AuthBloc _authBloc;
BluetoothReadBloc? _bluetoothReadBloc;
DeviceControlBloc? _deviceControlBloc;
DeviceThresholdConfigBloc? _deviceThresholdConfigBloc;
StreamSubscription? _tokenExpiredSubscription;
bool _isInitializing = true;
......@@ -88,6 +90,9 @@ class _MyAppState extends State<MyApp> {
DeviceControlBloc get _deviceControlBlocInstance =>
_deviceControlBloc ??= DeviceControlBloc();
DeviceThresholdConfigBloc get _deviceThresholdConfigBlocInstance =>
_deviceThresholdConfigBloc ??= DeviceThresholdConfigBloc();
@override
void initState() {
super.initState();
......@@ -173,6 +178,7 @@ class _MyAppState extends State<MyApp> {
_authBloc.close();
_bluetoothReadBloc?.close();
_deviceControlBloc?.close();
_deviceThresholdConfigBloc?.close();
super.dispose();
}
......@@ -190,6 +196,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider<DeviceControlBloc>.value(
value: _deviceControlBlocInstance,
),
BlocProvider<DeviceThresholdConfigBloc>.value(
value: _deviceThresholdConfigBlocInstance,
),
// BlocProvider<CounterBloc>(create: (_) => CounterBloc()),
],
child: MultiBlocListener(
......
......@@ -63,11 +63,13 @@ class CameraAlarmBO {
);
}
/// 与 Android 行为编码映射完全一致:
/// 行为编码 → 标题映射:
/// 1 → 宠物抓挠
/// 2 → 异常液体
/// 3 → 检测到排泄物
/// 其他 → 未知异常
String get titleByBehaviorCode {
if (behaviorCode == 1) return '宠物抓挠';
if (behaviorCode == 2) return '异常液体';
if (behaviorCode == 3) 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 {
/// 告警处理状态
final String status;
/// MQTT 下发告警记录 ID,上行 camera/{SN}/up 时使用
final int? alarmId;
const AlertInfoBO({
required this.title,
required this.description,
required this.time,
this.status = '已处理',
this.alarmId,
});
factory AlertInfoBO.fromJson(Map<String, dynamic> json) {
......@@ -168,11 +172,12 @@ class AlertInfoBO extends Equatable {
description: json['description'] as String? ?? '',
time: json['time'] as String? ?? '',
status: json['status'] as String? ?? '已处理',
alarmId: json['alarmId'] as int?,
);
}
@override
List<Object?> get props => [title, description, time, status];
List<Object?> get props => [title, description, time, status, alarmId];
}
/// 用途:首页监护舱右侧设备菜单数据
......
......@@ -15,12 +15,13 @@ class CabinRepository {
Future<ResponseModel<CabinDetailResponse>> getCabinDetail(
String carbinSn,
) {
if (Constants.useMockData) {
return _mockCabinDetail();
}
// if (Constants.useMockData) {
// return _mockCabinDetail();
// }
return DioRequest.instance.get<CabinDetailResponse>(
'/icu/detail',
queryParameters: {'carbinSn': carbinSn},
// queryParameters: {'carbinSn': carbinSn},
queryParameters: {'carbinSn': 'CNA07212L'},
fromJsonT: (data) =>
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:convert';
import 'dart:io';
import 'dart:math';
import 'package:laki_icu_app/utils/bluetooth/index.dart';
import 'package:mqtt_client/mqtt_client.dart';
......@@ -69,11 +70,16 @@ class LakiMqttTopics {
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 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';
......@@ -175,6 +181,11 @@ class LakiMqttClient {
_client = client;
_listenUpdates(client);
_emitState(LakiMqttConnectionState.connected);
// ignore: avoid_print
print(
'[LakiMqttClient] ✅ MQTT 已连接成功, clientId=${_config.clientId} '
'host=${_config.host}',
);
}
void subscribe(String topic, {MqttQos qos = MqttQos.atLeastOnce}) {
......@@ -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 {
await _updatesSub?.cancel();
_updatesSub = null;
......
......@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.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/utils/date_utils.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/services/cabin_service.dart';
......@@ -80,6 +82,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_listenMcuReportChanged();
_listenCareLevelChanged();
_loadBoundBluetoothDevice();
// // todo 测试使用 待删除
_fetchCabinDetailAndConnectVideo("CNA07212L");
_connectMqttForDeviceSn("CNA07212L");
}
/// 监听 WebRTC 连接状态变化并同步到 State
......@@ -155,7 +161,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
).copyWith(sn: reportSn.isEmpty ? null : reportSn);
if (bluetoothReadInfo.hasSn) {
await _storageService.saveBluetoothReadSn(bluetoothReadInfo.sn!);
unawaited(_fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!));
await _connectMqttForDeviceSn(
bluetoothReadInfo.sn!); // stephen 需要保留,选择蓝牙之后获取到sn就需要连接上mqtt
await _fetchCabinDetailAndConnectVideo(bluetoothReadInfo.sn!);
}
eventBus.emit(
BluetoothReadInfoChangedEvent(bluetoothReadInfo),
......@@ -163,13 +171,14 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_publishMqttMonitorReport(latestReport);
if (isClosed) return;
}
final patientInfo = latestReport == null
? state.patientInfo
: _patientInfoWithCareLevel(latestReport);
final careLevel = latestReport == null
? null
: _careLevelTextFromReportValue(latestReport.levelLight);
emit(state.copyWith(
latestBluetoothRawHex: packet.rawHex,
latestMcuReport: latestReport,
patientInfo: patientInfo,
patientInfo: _patientInfoWithCareLevelText(careLevel),
currentCareLevel: careLevel,
metrics: latestReport == null
? state.metrics
: _metricsFromBluetoothReport(latestReport),
......@@ -194,10 +203,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
'🔥🔥🔥 BLE_READ_DEBUG index_metrics_from_report '
'${_metricsLogText(metrics)}',
);
final careLevel = _careLevelTextFromReportValue(event.report.levelLight);
emit(state.copyWith(
latestMcuReport: event.report,
metrics: metrics,
patientInfo: _patientInfoWithCareLevel(event.report),
patientInfo: _patientInfoWithCareLevelText(careLevel),
currentCareLevel: careLevel,
));
});
}
......@@ -206,12 +217,15 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_careLevelChangedSub = eventBus.on<CareLevelChangedEvent>().listen((event) {
if (isClosed) return;
_markCareLevelPending(event.protocolValue);
final careLevel = _careLevelTextFromProtocolValue(event.protocolValue);
final patientInfo = _patientInfoWithCareLevelValue(
event.protocolValue,
fromReport: false,
);
if (patientInfo == null) return;
emit(state.copyWith(patientInfo: patientInfo));
emit(state.copyWith(
patientInfo: patientInfo,
currentCareLevel: careLevel,
));
});
}
......@@ -289,8 +303,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
/// 页面初始化入口。
///
/// 数据不再通过 HTTP 获取:
/// - 宠物信息由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{sn}/to 推送获取
/// - 宠物信息由 MQTT icu/{SN}/pet/bind 推送获取
/// - 摄像头凭证由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{SN}/to 推送获取
/// - 指标数据由蓝牙 MCU 上报实时填充
Future<void> loadData() async {}
......@@ -369,14 +384,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
try {
final detail = await _cabinService.getCabinDetail(carbinSn);
if (isClosed) return;
final careLevel = _careLevelFromCabinDetail(detail);
emit(state.copyWith(
cabinCameraSn: detail.cameraSn,
cabinWifiPwd: detail.wifiPwd,
currentCareLevel: careLevel,
patientInfo: detail.petInfo != null
? _mapPetInfoToPatientInfo(
detail.petInfo!,
careLevel: _careLevelFromCabinDetail(detail),
careLevel: careLevel,
)
: state.patientInfo,
));
......@@ -417,10 +434,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
return _careLevelTextFromProtocolValue(detail.monitoringData?.levelLight);
}
PatientInfoBO? _patientInfoWithCareLevel(McuReport report) {
return _patientInfoWithCareLevelValue(report.levelLight);
}
PatientInfoBO? _patientInfoWithCareLevelValue(
int value, {
bool fromReport = true,
......@@ -439,6 +452,21 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
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) {
final pendingValue = _pendingCareLevelProtocolValue;
final pendingSince = _pendingCareLevelSince;
......@@ -812,6 +840,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_handleCameraAlarmMessage(json);
return;
}
if (json != null &&
message.topic == LakiMqttTopics.icuPetBind(_mqttDeviceSn ?? '')) {
_handlePetBindMessage(json);
return;
}
emit(state.copyWith(
mqttMessage: 'MQTT 收到 ${message.topic}: ${message.payload}',
));
......@@ -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) {
if (json['type'] != 'abnormal_behavior') return;
......@@ -837,13 +878,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_seenAlarmEventIds.add(alarm.eventId);
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(
title: title,
description: title,
description: alarm.behaviorDesc ?? behaviorTitle,
time: timeStr,
status: '待处理',
alarmId: alarm.alarmId,
);
emit(state.copyWith(
......@@ -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 一致)
String _formatTimestamp(int milliseconds) {
final dateTime =
......@@ -1073,26 +1160,65 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
/// 清空宠物信息(出舱)
///
/// 通过 MQTT 上行出舱数据后清空 state 中的宠物信息。
void clearPatientInfo() {
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));
}
/// 将当前宠物信息转为 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() {
if (isClosed) return;
const testPatientInfo = PatientInfoBO(
name: '妮蔻',
type: '猫',
phone: '130-1111-3333',
breed: '长毛三花',
doctor: '张医生',
disease: '胃炎',
checkInDate: '2025-6-15',
careLevel: '特级',
avatarUrl: '',
);
emit(state.copyWith(patientInfo: testPatientInfo));
// todo 平板端暂时不做宠物绑定 stephen
}
/// 获取 WebRTC 渲染器供 UI 层使用
......@@ -1114,37 +1240,57 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
/// 标记告警为已处理
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void markAlertAsProcessed(AlertInfoBO alert) {
if (isClosed) return;
final updatedAlerts = state.alerts.map((item) {
if (item.title == alert.title && item.time == alert.time) {
return AlertInfoBO(
title: item.title,
description: item.description,
time: item.time,
status: '已处理',
);
}
return item;
}).toList();
emit(state.copyWith(alerts: updatedAlerts, selectedAlert: null));
_publishCameraAlarmResult(alert, 'handled');
final removedCameraAlerts =
state.cameraAlerts.where((item) => item != alert).toList();
final removedAlerts = state.alerts.where((item) => item != alert).toList();
emit(state.copyWith(
cameraAlerts: removedCameraAlerts,
alerts: removedAlerts,
selectedAlert: null,
));
}
/// 标记告警为误报
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void markAlertAsFalse(AlertInfoBO alert) {
if (isClosed) return;
final updatedAlerts = state.alerts.map((item) {
if (item.title == alert.title && item.time == alert.time) {
return AlertInfoBO(
title: item.title,
description: item.description,
time: item.time,
status: '误报',
);
}
return item;
}).toList();
emit(state.copyWith(alerts: updatedAlerts, selectedAlert: null));
_publishCameraAlarmResult(alert, 'false_alarm');
final removedCameraAlerts =
state.cameraAlerts.where((item) => item != alert).toList();
final removedAlerts = state.alerts.where((item) => item != alert).toList();
emit(state.copyWith(
cameraAlerts: removedCameraAlerts,
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');
}
}
@override
......
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
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) {
return Column(
children: [
_buildInfoItem(Icons.description_outlined, '宠物病因', info.disease),
_buildInfoItem(
Icons.description_outlined, '宠物病因', _truncate(info.disease)),
SizedBox(height: 20.h),
_buildInfoItem(Icons.home_outlined, '入住时间', info.checkInDate),
SizedBox(height: 20.h),
......
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';
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:laki_icu_app/models/bo/device_threshold_config_bo.dart';
class FactorySettingItem extends Equatable {
const FactorySettingItem({
......@@ -29,47 +30,63 @@ class FactorySettingGroup extends Equatable {
class FactorySettingsIndexState extends Equatable {
const FactorySettingsIndexState({
this.warningText = '非专业人员请勿更改设备各项参数!',
this.leftGroups = const [
FactorySettingGroup(
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'),
],
),
this.editingConfig = DeviceThresholdConfigBO.defaultConfig,
this.isDirty = false,
});
final String warningText;
final List<FactorySettingGroup> leftGroups;
final FactorySettingGroup timeLimitGroup;
final DeviceThresholdConfigBO editingConfig;
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
List<Object?> get props => [
warningText,
leftGroups,
timeLimitGroup,
editingConfig,
isDirty,
];
}
import 'package:flutter/material.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 'widgets/factory_settings_panel.dart';
......@@ -10,7 +11,9 @@ class FactorySettingsIndexView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (_) => FactorySettingsIndexCubit(),
create: (_) => FactorySettingsIndexCubit(
thresholdConfigBloc: context.read<DeviceThresholdConfigBloc>(),
),
child: const FactorySettingsPanel(),
);
}
......
......@@ -38,6 +38,7 @@ class FactorySettingsPanel extends StatelessWidget {
Expanded(
child: _FactoryActionColumn(
group: state.timeLimitGroup,
isDirty: state.isDirty,
),
),
],
......@@ -107,12 +108,17 @@ class _FactorySettingsColumn extends StatelessWidget {
}
class _FactoryActionColumn extends StatelessWidget {
const _FactoryActionColumn({required this.group});
const _FactoryActionColumn({
required this.group,
required this.isDirty,
});
final FactorySettingGroup group;
final bool isDirty;
@override
Widget build(BuildContext context) {
final cubit = context.read<FactorySettingsIndexCubit>();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......@@ -130,9 +136,19 @@ class _FactoryActionColumn extends StatelessWidget {
SizedBox(height: 64.h),
Row(
children: [
const Expanded(child: _FactoryButton(text: '撤销')),
Expanded(
child: _FactoryButton(
text: '撤销',
onPressed: isDirty ? () => cubit.resetEdits() : null,
),
),
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 {
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
Widget build(BuildContext context) {
return Row(
......@@ -192,7 +243,10 @@ class _FactorySettingRow extends StatelessWidget {
),
),
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 {
}
class _FactoryButton extends StatelessWidget {
const _FactoryButton({required this.text});
const _FactoryButton({
required this.text,
this.onPressed,
});
final String text;
final VoidCallback? onPressed;
@override
Widget build(BuildContext context) {
return Container(
height: 70.h,
padding: EdgeInsets.symmetric(horizontal: 28.w),
decoration: settingsButtonDecoration(active: true),
child: Row(
children: [
Text(
'《《',
style: TextStyle(
color: const Color(0xFF67C9F5).withValues(alpha: 0.72),
fontSize: 25.sp,
fontWeight: FontWeight.w700,
),
),
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
final isEnabled = onPressed != null;
return GestureDetector(
onTap: onPressed,
child: Container(
height: 70.h,
padding: EdgeInsets.symmetric(horizontal: 28.w),
decoration: settingsButtonDecoration(active: isEnabled),
child: Row(
children: [
Text(
'《',
style: TextStyle(
color: Colors.white,
color: const Color(0xFF67C9F5).withValues(alpha: isEnabled ? 0.72 : 0.3),
fontSize: 25.sp,
fontWeight: FontWeight.w800,
fontWeight: FontWeight.w700,
),
),
),
Text(
'》》',
style: TextStyle(
color: const Color(0xFF67C9F5).withValues(alpha: 0.72),
fontSize: 25.sp,
fontWeight: FontWeight.w700,
Expanded(
child: Text(
text,
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
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:
mqtt_client: ^10.5.1
marionette_flutter: ^0.5.0
hydrated_bloc: ^9.1.5
intl: ^0.20.2
dev_dependencies:
flutter_test:
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