Commit 0f9cb2e3 authored by akari's avatar akari

feat: 中控页功能联调修复

parent 4dff6ccd
export 'device_calibration_config_bloc.dart';
export 'device_calibration_config_event.dart';
export 'device_calibration_config_state.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:laki_icu_app/models/bo/device_calibration_config_bo.dart';
import 'device_calibration_config_event.dart';
import 'device_calibration_config_state.dart';
class DeviceCalibrationConfigBloc extends HydratedBloc<
DeviceCalibrationConfigEvent, DeviceCalibrationConfigState> {
DeviceCalibrationConfigBloc() : super(const DeviceCalibrationConfigState()) {
on<DeviceCalibrationConfigLoaded>(_onLoaded);
on<DeviceCalibrationConfigItemUpdated>(_onItemUpdated);
on<DeviceCalibrationConfigUpdated>(_onConfigUpdated);
on<DeviceCalibrationConfigResetToDefault>(_onResetToDefault);
}
Future<void> _onLoaded(
DeviceCalibrationConfigLoaded event,
Emitter<DeviceCalibrationConfigState> 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 _onItemUpdated(
DeviceCalibrationConfigItemUpdated event,
Emitter<DeviceCalibrationConfigState> emit,
) {
emit(state.copyWith(
config: state.config.updateItem(event.key, event.value),
clearError: true,
));
}
void _onConfigUpdated(
DeviceCalibrationConfigUpdated event,
Emitter<DeviceCalibrationConfigState> emit,
) {
emit(state.copyWith(
config: event.config,
clearError: true,
));
}
void _onResetToDefault(
DeviceCalibrationConfigResetToDefault event,
Emitter<DeviceCalibrationConfigState> emit,
) {
emit(const DeviceCalibrationConfigState(
config: DeviceCalibrationConfigBO.defaultConfig,
));
}
void updateItem(DeviceCalibrationItemKey key, int value) {
add(DeviceCalibrationConfigItemUpdated(key: key, value: value));
}
void updateConfig(DeviceCalibrationConfigBO config) {
add(DeviceCalibrationConfigUpdated(config));
}
void resetToDefault() {
add(const DeviceCalibrationConfigResetToDefault());
}
@override
DeviceCalibrationConfigState? fromJson(Map<String, dynamic> json) {
try {
return DeviceCalibrationConfigState(
config: DeviceCalibrationConfigBO.fromJson(json),
);
} catch (e) {
return null;
}
}
@override
Map<String, dynamic>? toJson(DeviceCalibrationConfigState state) {
return state.config.toJson();
}
}
/// 全局设备校准配置 Bloc 引用。
///
/// 由 main.dart 在创建时写入,页面外需要直接读取校准参数时可通过此引用获取。
DeviceCalibrationConfigBloc? globalDeviceCalibrationConfigBloc;
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_calibration_config_bo.dart';
abstract class DeviceCalibrationConfigEvent extends Equatable {
const DeviceCalibrationConfigEvent();
@override
List<Object?> get props => [];
}
class DeviceCalibrationConfigLoaded extends DeviceCalibrationConfigEvent {
const DeviceCalibrationConfigLoaded();
}
class DeviceCalibrationConfigItemUpdated extends DeviceCalibrationConfigEvent {
const DeviceCalibrationConfigItemUpdated({
required this.key,
required this.value,
});
final DeviceCalibrationItemKey key;
final int value;
@override
List<Object?> get props => [key, value];
}
class DeviceCalibrationConfigUpdated extends DeviceCalibrationConfigEvent {
const DeviceCalibrationConfigUpdated(this.config);
final DeviceCalibrationConfigBO config;
@override
List<Object?> get props => [config];
}
class DeviceCalibrationConfigResetToDefault
extends DeviceCalibrationConfigEvent {
const DeviceCalibrationConfigResetToDefault();
}
import 'package:equatable/equatable.dart';
import 'package:laki_icu_app/models/bo/device_calibration_config_bo.dart';
class DeviceCalibrationConfigState extends Equatable {
const DeviceCalibrationConfigState({
this.config = DeviceCalibrationConfigBO.defaultConfig,
this.isLoading = false,
this.error,
});
final DeviceCalibrationConfigBO config;
final bool isLoading;
final String? error;
DeviceCalibrationConfigState copyWith({
DeviceCalibrationConfigBO? config,
bool? isLoading,
String? error,
bool clearError = false,
}) {
return DeviceCalibrationConfigState(
config: config ?? this.config,
isLoading: isLoading ?? this.isLoading,
error: clearError ? null : error ?? this.error,
);
}
@override
List<Object?> get props => [config, isLoading, error];
}
......@@ -129,6 +129,7 @@ class DeviceControlBloc
DateTime? _pendingCareLevelSince;
late final StreamSubscription<McuReportChangedEvent> _mcuReportSub;
late final StreamSubscription<BluetoothDataPacket> _bluetoothDataSub;
Timer? _oxygenTimer;
/// 上次设备告警分发时间(30s 防抖)
DateTime? _lastAlarmDispatchTime;
......@@ -269,6 +270,7 @@ class DeviceControlBloc
),
);
emit(state.copyWith(data: newData, clearError: true));
_syncLocalOxygenTimer(report.oxygenConcentration);
// 异步分发:触发设备异常告警(MQTT 上行 + UI 通知)
_dispatchDeviceAlarm(newData);
......@@ -903,6 +905,11 @@ class DeviceControlBloc
DeviceControlOxygenTimerUpdated event,
Emitter<DeviceControlState> emit,
) {
final totalSeconds = _oxygenTimerSecondsFromParts(
event.hours,
event.minutes,
event.seconds,
);
final newData = state.data.copyWith(
oxygenTimer: state.data.oxygenTimer.copyWith(
hours: event.hours,
......@@ -910,7 +917,14 @@ class DeviceControlBloc
seconds: event.seconds,
),
);
emit(state.copyWith(data: newData, clearError: true));
final newUserSettings = state.userSettings.copyWith(
oxygenTimerSeconds: totalSeconds,
);
emit(state.copyWith(
data: newData,
userSettings: newUserSettings,
clearError: true,
));
}
/// [用户操作] 清零供氧计时
......@@ -925,7 +939,14 @@ class DeviceControlBloc
seconds: '00',
),
);
emit(state.copyWith(data: newData, clearError: true));
final newUserSettings = state.userSettings.copyWith(
oxygenTimerSeconds: 0,
);
emit(state.copyWith(
data: newData,
userSettings: newUserSettings,
clearError: true,
));
await _writeControlValueToBluetooth(
identifier: 0x29,
setValue: '0',
......@@ -1045,13 +1066,128 @@ class DeviceControlBloc
emit(state.copyWith(data: newData, clearError: true));
}
/// [用户操作] 重置设备设置为默认值 —— 清除持久化
void _onResetSettings(
/// [用户操作] 关闭所有可关闭控制并按 300ms 间隔下发
Future<void> _onResetSettings(
DeviceControlResetSettings event,
Emitter<DeviceControlState> emit,
) {
emit(const DeviceControlState());
// TODO: 发送重置指令到MCU
) async {
final newData = state.data.copyWith(
cabinTemp: state.data.cabinTemp.copyWith(isOn: false),
cabinHumidity: state.data.cabinHumidity.copyWith(isOn: false),
oxygenConcentration: state.data.oxygenConcentration.copyWith(
isOn: false,
setValue: _clampIntString(
state.data.oxygenConcentration.setValue,
0,
60,
),
),
openOxygen: state.data.openOxygen.copyWith(isOn: false),
windSpeed: state.data.windSpeed.copyWith(currentMode: WindSpeedMode.off),
nebulizationTime: state.data.nebulizationTime.copyWith(setMinutes: '0'),
disinfectionTime: state.data.disinfectionTime.copyWith(setMinutes: '0'),
careLevel: state.data.careLevel.copyWith(currentLevel: CareLevel.off),
lightControl: state.data.lightControl.copyWith(
checkLightOn: false,
illuminationOn: false,
blueLightOn: false,
redLightOn: false,
),
);
final newUserSettings = state.userSettings.copyWith(
cabinTempIsOn: false,
cabinHumidityIsOn: false,
oxygenConcentrationIsOn: false,
oxygenConcentrationSetValue: newData.oxygenConcentration.setValue,
openOxygenIsOn: false,
windSpeedModeValue: WindSpeedMode.off,
nebulizationTimeMinutes: '0',
disinfectionTimeMinutes: '0',
careLevelValue: CareLevel.off,
checkLightOn: false,
illuminationOn: false,
blueLightOn: false,
redLightOn: false,
);
emit(state.copyWith(
data: newData,
userSettings: newUserSettings,
clearError: true,
));
await _writeResetCommandWithDelay(
identifier: 0x02,
setValue: '10',
errorPrefix: '舱内温度关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0x04,
setValue: '100',
errorPrefix: '舱内湿度关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0x05,
setValue: '20',
errorPrefix: '氧气浓度关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0x26,
setValue: '0',
errorPrefix: '开放供氧关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0xB3,
setValue: '${_windSpeedProtocolValue(WindSpeedMode.off)}',
errorPrefix: '风速关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0x0C,
setValue: '0',
errorPrefix: '雾化关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0x0D,
setValue: '0',
errorPrefix: '消毒关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: 0xA8,
setValue: '${_careLevelProtocolValue(CareLevel.off)}',
errorPrefix: '护理等级关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: _lightIdentifier(_LightTarget.check),
setValue: '0',
errorPrefix: '检查灯关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: _lightIdentifier(_LightTarget.illumination),
setValue: '0',
errorPrefix: '照明灯关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: _lightIdentifier(_LightTarget.blue),
setValue: '0',
errorPrefix: '蓝光关闭',
emit: emit,
);
await _writeResetCommandWithDelay(
identifier: _lightIdentifier(_LightTarget.red),
setValue: '0',
errorPrefix: '红光关闭',
emit: emit,
delayAfter: false,
);
}
int get _oxygenSetValueMax => state.data.openOxygen.isOn ? 80 : 60;
......@@ -1099,6 +1235,60 @@ class DeviceControlBloc
return '${parsed.clamp(min, max)}';
}
void _syncLocalOxygenTimer(int oxygenConcentration) {
if (oxygenConcentration >= 30) {
_oxygenTimer ??= Timer.periodic(const Duration(seconds: 1), (_) {
if (isClosed) return;
final totalSeconds = _currentOxygenTimerSeconds() + 1;
final timer = _oxygenTimerFromSeconds(totalSeconds);
add(DeviceControlOxygenTimerUpdated(
timer.hours,
timer.minutes,
timer.seconds,
));
});
return;
}
_stopLocalOxygenTimer();
}
int _currentOxygenTimerSeconds() {
return state.userSettings.oxygenTimerSeconds ??
_oxygenTimerSecondsFromParts(
state.data.oxygenTimer.hours,
state.data.oxygenTimer.minutes,
state.data.oxygenTimer.seconds,
);
}
int _oxygenTimerSecondsFromParts(
String hours,
String minutes,
String seconds,
) {
final parsedHours = int.tryParse(hours) ?? 0;
final parsedMinutes = int.tryParse(minutes) ?? 0;
final parsedSeconds = int.tryParse(seconds) ?? 0;
return parsedHours * 3600 + parsedMinutes * 60 + parsedSeconds;
}
OxygenTimerBO _oxygenTimerFromSeconds(int totalSeconds) {
final safeSeconds = totalSeconds < 0 ? 0 : totalSeconds;
final hours = safeSeconds ~/ 3600;
final minutes = (safeSeconds % 3600) ~/ 60;
final seconds = safeSeconds % 60;
return OxygenTimerBO(
hours: hours.toString().padLeft(2, '0'),
minutes: minutes.toString().padLeft(2, '0'),
seconds: seconds.toString().padLeft(2, '0'),
);
}
void _stopLocalOxygenTimer() {
_oxygenTimer?.cancel();
_oxygenTimer = null;
}
Future<void> _setExclusiveLight({
required _LightTarget target,
required bool isOn,
......@@ -1306,6 +1496,24 @@ class DeviceControlBloc
);
}
Future<void> _writeResetCommandWithDelay({
required int identifier,
required String setValue,
required String errorPrefix,
required Emitter<DeviceControlState> emit,
bool delayAfter = true,
}) async {
await _writeControlValueToBluetooth(
identifier: identifier,
setValue: setValue,
errorPrefix: errorPrefix,
emit: emit,
);
if (delayAfter && !isClosed) {
await Future<void>.delayed(const Duration(milliseconds: 300));
}
}
int _windSpeedProtocolValue(WindSpeedMode mode) {
switch (mode) {
case WindSpeedMode.off:
......@@ -1421,6 +1629,7 @@ class DeviceControlBloc
@override
Future<void> close() async {
_stopLocalOxygenTimer();
await _mcuReportSub.cancel();
await _bluetoothDataSub.cancel();
return super.close();
......
......@@ -29,10 +29,18 @@ 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_calibration_config/device_calibration_config.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 {
if (kDebugMode) {
// MarionetteBinding 继承自 WidgetsFlutterBinding,必须最先且唯一初始化
MarionetteBinding.ensureInitialized();
} else {
WidgetsFlutterBinding.ensureInitialized();
}
// 初始化 HydratedBloc 存储(用于持久化设备控制用户设置等)
final storageDirectory = await getApplicationDocumentsDirectory();
HydratedBloc.storage = await HydratedStorage.build(
......@@ -41,9 +49,6 @@ Future<void> main(List<String> args) async {
// UME 调试工具(仅 Debug 模式启用)
if (kDebugMode) {
// MarionetteBinding 继承自 WidgetsFlutterBinding,必须最先且唯一初始化
MarionetteBinding.ensureInitialized();
PluginManager.instance
..register(WidgetInfoInspector())
..register(WidgetDetailInspector())
......@@ -54,8 +59,6 @@ Future<void> main(List<String> args) async {
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
runApp(UMEWidget(enable: true, child: const MyApp()));
} else {
WidgetsFlutterBinding.ensureInitialized();
// 全局全屏:隐藏系统状态栏和底部导航栏
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual, overlays: []);
runApp(const MyApp());
......@@ -78,6 +81,7 @@ class _MyAppState extends State<MyApp> {
late final AuthBloc _authBloc;
BluetoothReadBloc? _bluetoothReadBloc;
DeviceControlBloc? _deviceControlBloc;
DeviceCalibrationConfigBloc? _deviceCalibrationConfigBloc;
DeviceThresholdConfigBloc? _deviceThresholdConfigBloc;
StreamSubscription? _tokenExpiredSubscription;
bool _isInitializing = true;
......@@ -101,6 +105,15 @@ class _MyAppState extends State<MyApp> {
return _deviceThresholdConfigBloc!;
}
DeviceCalibrationConfigBloc get _deviceCalibrationConfigBlocInstance {
if (_deviceCalibrationConfigBloc != null) {
return _deviceCalibrationConfigBloc!;
}
_deviceCalibrationConfigBloc = DeviceCalibrationConfigBloc();
globalDeviceCalibrationConfigBloc = _deviceCalibrationConfigBloc;
return _deviceCalibrationConfigBloc!;
}
@override
void initState() {
super.initState();
......@@ -169,6 +182,7 @@ class _MyAppState extends State<MyApp> {
_authBloc.close();
_bluetoothReadBloc?.close();
_deviceControlBloc?.close();
_deviceCalibrationConfigBloc?.close();
_deviceThresholdConfigBloc?.close();
super.dispose();
}
......@@ -187,6 +201,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider<DeviceControlBloc>.value(
value: _deviceControlBlocInstance,
),
BlocProvider<DeviceCalibrationConfigBloc>.value(
value: _deviceCalibrationConfigBlocInstance,
),
BlocProvider<DeviceThresholdConfigBloc>.value(
value: _deviceThresholdConfigBlocInstance,
),
......@@ -200,8 +217,8 @@ class _MyAppState extends State<MyApp> {
debugShowCheckedModeBanner: false,
builder: (context, child) {
// 每次页面重建时重新隐藏状态栏,防止路由切换后状态栏重新出现
SystemChrome.setEnabledSystemUIMode(
SystemUiMode.manual, overlays: []);
SystemChrome.setEnabledSystemUIMode(SystemUiMode.manual,
overlays: []);
return child ?? const SizedBox.shrink();
},
),
......
import 'package:equatable/equatable.dart';
/// 用途:设备校准参数配置
/// 涉及页面:工厂设置-设备校准页面
class DeviceCalibrationConfigBO extends Equatable {
/// 温度系数%S1(上)
final int tempCoefficientS1Upper;
/// 温度系数%S1(下)
final int tempCoefficientS1Lower;
/// 环境温度冬夏临界值T0
final int ambientTempSeasonThresholdT0;
/// 摄像头循环重启(分钟)
final int cameraLoopRestartMinutes;
/// 摄像头定时重启(小时)
final int cameraScheduledRestartHours;
/// CO2最低设置值C1
final int co2MinSettingC1;
/// CO2差值C2
final int co2DifferenceC2;
/// CO2基准值C3
final int co2BaseValueC3;
/// CO2系数C4
final int co2CoefficientC4;
/// 密闭舱氧气修正点X0
final int closedCabinOxygenCorrectionPointX0;
/// 密闭舱氧气修正幅度X1
final int closedCabinOxygenCorrectionRangeX1;
/// 密闭舱氧气修正步进值X4
final int closedCabinOxygenCorrectionStepX4;
/// 开放式供氧氧气修正点S0
final int openOxygenCorrectionPointS0;
/// 开放式供氧氧气修正幅度S1
final int openOxygenCorrectionRangeS1;
/// 开放式供氧氧气修正步进值S4
final int openOxygenCorrectionStepS4;
const DeviceCalibrationConfigBO({
required this.tempCoefficientS1Upper,
required this.tempCoefficientS1Lower,
required this.ambientTempSeasonThresholdT0,
required this.cameraLoopRestartMinutes,
required this.cameraScheduledRestartHours,
required this.co2MinSettingC1,
required this.co2DifferenceC2,
required this.co2BaseValueC3,
required this.co2CoefficientC4,
required this.closedCabinOxygenCorrectionPointX0,
required this.closedCabinOxygenCorrectionRangeX1,
required this.closedCabinOxygenCorrectionStepX4,
required this.openOxygenCorrectionPointS0,
required this.openOxygenCorrectionRangeS1,
required this.openOxygenCorrectionStepS4,
});
static const DeviceCalibrationConfigBO defaultConfig =
DeviceCalibrationConfigBO(
tempCoefficientS1Upper: 17,
tempCoefficientS1Lower: 34,
ambientTempSeasonThresholdT0: 17,
cameraLoopRestartMinutes: 34,
cameraScheduledRestartHours: 17,
co2MinSettingC1: 4000,
co2DifferenceC2: 2000,
co2BaseValueC3: 1500,
co2CoefficientC4: 34,
closedCabinOxygenCorrectionPointX0: 17,
closedCabinOxygenCorrectionRangeX1: 34,
closedCabinOxygenCorrectionStepX4: 17,
openOxygenCorrectionPointS0: 34,
openOxygenCorrectionRangeS1: 17,
openOxygenCorrectionStepS4: 17,
);
List<DeviceCalibrationSectionBO> get sections => [
DeviceCalibrationSectionBO(
title: '主温度调节系数',
items: [
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.tempCoefficientS1Upper,
label: '温度系数%S1(上):',
value: tempCoefficientS1Upper,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.tempCoefficientS1Lower,
label: '温度系数%S1(下):',
value: tempCoefficientS1Lower,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.ambientTempSeasonThresholdT0,
label: '环境温度冬夏临界值T0:',
value: ambientTempSeasonThresholdT0,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.cameraLoopRestartMinutes,
label: '摄像头循环重启(分钟):',
value: cameraLoopRestartMinutes,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.cameraScheduledRestartHours,
label: '摄像头定时重启(小时):',
value: cameraScheduledRestartHours,
),
],
),
DeviceCalibrationSectionBO(
title: 'CO₂报警及净化修正值',
items: [
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.co2MinSettingC1,
label: 'CO₂最低设置值C1:',
value: co2MinSettingC1,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.co2DifferenceC2,
label: 'CO₂差值C2:',
value: co2DifferenceC2,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.co2BaseValueC3,
label: 'CO₂基准值C3:',
value: co2BaseValueC3,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.co2CoefficientC4,
label: 'CO₂系数C4:',
value: co2CoefficientC4,
),
],
),
DeviceCalibrationSectionBO(
title: '密闭舱氧气修正值',
items: [
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.closedCabinOxygenCorrectionPointX0,
label: '密闭舱氧气修正点X0:',
value: closedCabinOxygenCorrectionPointX0,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.closedCabinOxygenCorrectionRangeX1,
label: '密闭舱氧气修正幅度X1:',
value: closedCabinOxygenCorrectionRangeX1,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.closedCabinOxygenCorrectionStepX4,
label: '密闭舱氧气修正步进值X4:',
value: closedCabinOxygenCorrectionStepX4,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.openOxygenCorrectionPointS0,
label: '开放式供氧氧气修正点S0:',
value: openOxygenCorrectionPointS0,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.openOxygenCorrectionRangeS1,
label: '开放式供氧氧气修正幅度S1:',
value: openOxygenCorrectionRangeS1,
),
DeviceCalibrationItemBO(
key: DeviceCalibrationItemKey.openOxygenCorrectionStepS4,
label: '开放式供氧氧气修正步进值S4:',
value: openOxygenCorrectionStepS4,
),
],
),
];
DeviceCalibrationConfigBO copyWith({
int? tempCoefficientS1Upper,
int? tempCoefficientS1Lower,
int? ambientTempSeasonThresholdT0,
int? cameraLoopRestartMinutes,
int? cameraScheduledRestartHours,
int? co2MinSettingC1,
int? co2DifferenceC2,
int? co2BaseValueC3,
int? co2CoefficientC4,
int? closedCabinOxygenCorrectionPointX0,
int? closedCabinOxygenCorrectionRangeX1,
int? closedCabinOxygenCorrectionStepX4,
int? openOxygenCorrectionPointS0,
int? openOxygenCorrectionRangeS1,
int? openOxygenCorrectionStepS4,
}) {
return DeviceCalibrationConfigBO(
tempCoefficientS1Upper:
tempCoefficientS1Upper ?? this.tempCoefficientS1Upper,
tempCoefficientS1Lower:
tempCoefficientS1Lower ?? this.tempCoefficientS1Lower,
ambientTempSeasonThresholdT0:
ambientTempSeasonThresholdT0 ?? this.ambientTempSeasonThresholdT0,
cameraLoopRestartMinutes:
cameraLoopRestartMinutes ?? this.cameraLoopRestartMinutes,
cameraScheduledRestartHours:
cameraScheduledRestartHours ?? this.cameraScheduledRestartHours,
co2MinSettingC1: co2MinSettingC1 ?? this.co2MinSettingC1,
co2DifferenceC2: co2DifferenceC2 ?? this.co2DifferenceC2,
co2BaseValueC3: co2BaseValueC3 ?? this.co2BaseValueC3,
co2CoefficientC4: co2CoefficientC4 ?? this.co2CoefficientC4,
closedCabinOxygenCorrectionPointX0: closedCabinOxygenCorrectionPointX0 ??
this.closedCabinOxygenCorrectionPointX0,
closedCabinOxygenCorrectionRangeX1: closedCabinOxygenCorrectionRangeX1 ??
this.closedCabinOxygenCorrectionRangeX1,
closedCabinOxygenCorrectionStepX4: closedCabinOxygenCorrectionStepX4 ??
this.closedCabinOxygenCorrectionStepX4,
openOxygenCorrectionPointS0:
openOxygenCorrectionPointS0 ?? this.openOxygenCorrectionPointS0,
openOxygenCorrectionRangeS1:
openOxygenCorrectionRangeS1 ?? this.openOxygenCorrectionRangeS1,
openOxygenCorrectionStepS4:
openOxygenCorrectionStepS4 ?? this.openOxygenCorrectionStepS4,
);
}
DeviceCalibrationConfigBO updateItem(
DeviceCalibrationItemKey key,
int value,
) {
switch (key) {
case DeviceCalibrationItemKey.tempCoefficientS1Upper:
return copyWith(tempCoefficientS1Upper: value);
case DeviceCalibrationItemKey.tempCoefficientS1Lower:
return copyWith(tempCoefficientS1Lower: value);
case DeviceCalibrationItemKey.ambientTempSeasonThresholdT0:
return copyWith(ambientTempSeasonThresholdT0: value);
case DeviceCalibrationItemKey.cameraLoopRestartMinutes:
return copyWith(cameraLoopRestartMinutes: value);
case DeviceCalibrationItemKey.cameraScheduledRestartHours:
return copyWith(cameraScheduledRestartHours: value);
case DeviceCalibrationItemKey.co2MinSettingC1:
return copyWith(co2MinSettingC1: value);
case DeviceCalibrationItemKey.co2DifferenceC2:
return copyWith(co2DifferenceC2: value);
case DeviceCalibrationItemKey.co2BaseValueC3:
return copyWith(co2BaseValueC3: value);
case DeviceCalibrationItemKey.co2CoefficientC4:
return copyWith(co2CoefficientC4: value);
case DeviceCalibrationItemKey.closedCabinOxygenCorrectionPointX0:
return copyWith(closedCabinOxygenCorrectionPointX0: value);
case DeviceCalibrationItemKey.closedCabinOxygenCorrectionRangeX1:
return copyWith(closedCabinOxygenCorrectionRangeX1: value);
case DeviceCalibrationItemKey.closedCabinOxygenCorrectionStepX4:
return copyWith(closedCabinOxygenCorrectionStepX4: value);
case DeviceCalibrationItemKey.openOxygenCorrectionPointS0:
return copyWith(openOxygenCorrectionPointS0: value);
case DeviceCalibrationItemKey.openOxygenCorrectionRangeS1:
return copyWith(openOxygenCorrectionRangeS1: value);
case DeviceCalibrationItemKey.openOxygenCorrectionStepS4:
return copyWith(openOxygenCorrectionStepS4: value);
}
}
factory DeviceCalibrationConfigBO.fromJson(Map<String, dynamic> json) {
return DeviceCalibrationConfigBO(
tempCoefficientS1Upper: json['tempCoefficientS1Upper'] as int? ?? 17,
tempCoefficientS1Lower: json['tempCoefficientS1Lower'] as int? ?? 34,
ambientTempSeasonThresholdT0:
json['ambientTempSeasonThresholdT0'] as int? ?? 17,
cameraLoopRestartMinutes: json['cameraLoopRestartMinutes'] as int? ?? 34,
cameraScheduledRestartHours:
json['cameraScheduledRestartHours'] as int? ?? 17,
co2MinSettingC1: json['co2MinSettingC1'] as int? ?? 4000,
co2DifferenceC2: json['co2DifferenceC2'] as int? ?? 2000,
co2BaseValueC3: json['co2BaseValueC3'] as int? ?? 1500,
co2CoefficientC4: json['co2CoefficientC4'] as int? ?? 34,
closedCabinOxygenCorrectionPointX0:
json['closedCabinOxygenCorrectionPointX0'] as int? ?? 17,
closedCabinOxygenCorrectionRangeX1:
json['closedCabinOxygenCorrectionRangeX1'] as int? ?? 34,
closedCabinOxygenCorrectionStepX4:
json['closedCabinOxygenCorrectionStepX4'] as int? ?? 17,
openOxygenCorrectionPointS0:
json['openOxygenCorrectionPointS0'] as int? ?? 34,
openOxygenCorrectionRangeS1:
json['openOxygenCorrectionRangeS1'] as int? ?? 17,
openOxygenCorrectionStepS4:
json['openOxygenCorrectionStepS4'] as int? ?? 17,
);
}
Map<String, dynamic> toJson() {
return {
'tempCoefficientS1Upper': tempCoefficientS1Upper,
'tempCoefficientS1Lower': tempCoefficientS1Lower,
'ambientTempSeasonThresholdT0': ambientTempSeasonThresholdT0,
'cameraLoopRestartMinutes': cameraLoopRestartMinutes,
'cameraScheduledRestartHours': cameraScheduledRestartHours,
'co2MinSettingC1': co2MinSettingC1,
'co2DifferenceC2': co2DifferenceC2,
'co2BaseValueC3': co2BaseValueC3,
'co2CoefficientC4': co2CoefficientC4,
'closedCabinOxygenCorrectionPointX0': closedCabinOxygenCorrectionPointX0,
'closedCabinOxygenCorrectionRangeX1': closedCabinOxygenCorrectionRangeX1,
'closedCabinOxygenCorrectionStepX4': closedCabinOxygenCorrectionStepX4,
'openOxygenCorrectionPointS0': openOxygenCorrectionPointS0,
'openOxygenCorrectionRangeS1': openOxygenCorrectionRangeS1,
'openOxygenCorrectionStepS4': openOxygenCorrectionStepS4,
};
}
@override
List<Object?> get props => [
tempCoefficientS1Upper,
tempCoefficientS1Lower,
ambientTempSeasonThresholdT0,
cameraLoopRestartMinutes,
cameraScheduledRestartHours,
co2MinSettingC1,
co2DifferenceC2,
co2BaseValueC3,
co2CoefficientC4,
closedCabinOxygenCorrectionPointX0,
closedCabinOxygenCorrectionRangeX1,
closedCabinOxygenCorrectionStepX4,
openOxygenCorrectionPointS0,
openOxygenCorrectionRangeS1,
openOxygenCorrectionStepS4,
];
}
class DeviceCalibrationSectionBO extends Equatable {
const DeviceCalibrationSectionBO({
required this.title,
required this.items,
});
final String title;
final List<DeviceCalibrationItemBO> items;
@override
List<Object?> get props => [title, items];
}
class DeviceCalibrationItemBO extends Equatable {
const DeviceCalibrationItemBO({
required this.key,
required this.label,
required this.value,
});
final DeviceCalibrationItemKey key;
final String label;
final int value;
@override
List<Object?> get props => [key, label, value];
}
enum DeviceCalibrationItemKey {
tempCoefficientS1Upper,
tempCoefficientS1Lower,
ambientTempSeasonThresholdT0,
cameraLoopRestartMinutes,
cameraScheduledRestartHours,
co2MinSettingC1,
co2DifferenceC2,
co2BaseValueC3,
co2CoefficientC4,
closedCabinOxygenCorrectionPointX0,
closedCabinOxygenCorrectionRangeX1,
closedCabinOxygenCorrectionStepX4,
openOxygenCorrectionPointS0,
openOxygenCorrectionRangeS1,
openOxygenCorrectionStepS4,
}
......@@ -801,6 +801,9 @@ class DeviceControlUserSettings extends Equatable {
/// 开放供氧开关
final bool? openOxygenIsOn;
/// 本地供氧计时秒数
final int? oxygenTimerSeconds;
/// 循环模式
final CirculationMode? circulationModeValue;
......@@ -831,6 +834,7 @@ class DeviceControlUserSettings extends Equatable {
this.nebulizationTimeMinutes,
this.disinfectionTimeMinutes,
this.openOxygenIsOn,
this.oxygenTimerSeconds,
this.circulationModeValue,
this.checkLightOn,
this.illuminationOn,
......@@ -853,6 +857,7 @@ class DeviceControlUserSettings extends Equatable {
String? nebulizationTimeMinutes,
String? disinfectionTimeMinutes,
bool? openOxygenIsOn,
int? oxygenTimerSeconds,
CirculationMode? circulationModeValue,
bool? checkLightOn,
bool? illuminationOn,
......@@ -879,6 +884,7 @@ class DeviceControlUserSettings extends Equatable {
disinfectionTimeMinutes:
disinfectionTimeMinutes ?? this.disinfectionTimeMinutes,
openOxygenIsOn: openOxygenIsOn ?? this.openOxygenIsOn,
oxygenTimerSeconds: oxygenTimerSeconds ?? this.oxygenTimerSeconds,
circulationModeValue: circulationModeValue ?? this.circulationModeValue,
checkLightOn: checkLightOn ?? this.checkLightOn,
illuminationOn: illuminationOn ?? this.illuminationOn,
......@@ -926,6 +932,9 @@ class DeviceControlUserSettings extends Equatable {
openOxygen: openOxygenIsOn != null
? bo.openOxygen.copyWith(isOn: openOxygenIsOn)
: null,
oxygenTimer: oxygenTimerSeconds != null
? _oxygenTimerFromSeconds(oxygenTimerSeconds!)
: null,
circulationMode: circulationModeValue != null
? bo.circulationMode.copyWith(currentMode: circulationModeValue)
: null,
......@@ -970,6 +979,7 @@ class DeviceControlUserSettings extends Equatable {
nebulizationTimeMinutes: json['nebulizationTimeMinutes'] as String?,
disinfectionTimeMinutes: json['disinfectionTimeMinutes'] as String?,
openOxygenIsOn: json['openOxygenIsOn'] as bool?,
oxygenTimerSeconds: json['oxygenTimerSeconds'] as int?,
circulationModeValue: json['circulationModeValue'] != null
? CirculationMode.values.firstWhere(
(e) => e.name == json['circulationModeValue'] as String,
......@@ -1006,6 +1016,7 @@ class DeviceControlUserSettings extends Equatable {
if (disinfectionTimeMinutes != null)
'disinfectionTimeMinutes': disinfectionTimeMinutes,
if (openOxygenIsOn != null) 'openOxygenIsOn': openOxygenIsOn,
if (oxygenTimerSeconds != null) 'oxygenTimerSeconds': oxygenTimerSeconds,
if (circulationModeValue != null)
'circulationModeValue': circulationModeValue!.name,
if (checkLightOn != null) 'checkLightOn': checkLightOn,
......@@ -1031,6 +1042,7 @@ class DeviceControlUserSettings extends Equatable {
nebulizationTimeMinutes,
disinfectionTimeMinutes,
openOxygenIsOn,
oxygenTimerSeconds,
circulationModeValue,
checkLightOn,
illuminationOn,
......@@ -1038,3 +1050,15 @@ class DeviceControlUserSettings extends Equatable {
redLightOn,
];
}
OxygenTimerBO _oxygenTimerFromSeconds(int totalSeconds) {
final safeSeconds = totalSeconds < 0 ? 0 : totalSeconds;
final hours = safeSeconds ~/ 3600;
final minutes = (safeSeconds % 3600) ~/ 60;
final seconds = safeSeconds % 60;
return OxygenTimerBO(
hours: hours.toString().padLeft(2, '0'),
minutes: minutes.toString().padLeft(2, '0'),
seconds: seconds.toString().padLeft(2, '0'),
);
}
......@@ -18,6 +18,8 @@ class NumericKeyboard extends StatefulWidget {
super.key,
required this.initialValue,
this.decimalEnabled = false,
this.showCloseKey = false,
this.closeKeyValue,
this.onChanged,
this.onConfirm,
});
......@@ -28,6 +30,12 @@ class NumericKeyboard extends StatefulWidget {
/// 是否允许输入小数点(温度模式为 true)
final bool decimalEnabled;
/// 是否在键盘右下角显示关闭按钮
final bool showCloseKey;
/// 点击关闭按钮时返回的值;为空则只关闭弹窗
final String? closeKeyValue;
/// 输入变化回调(可选)
final ValueChanged<String>? onChanged;
......@@ -45,6 +53,8 @@ class NumericKeyboard extends StatefulWidget {
BuildContext context, {
String initialValue = '',
bool decimalEnabled = false,
bool showCloseKey = false,
String? closeKeyValue,
}) {
return showModalBottomSheet<String>(
context: context,
......@@ -54,6 +64,8 @@ class NumericKeyboard extends StatefulWidget {
builder: (_) => NumericKeyboard(
initialValue: initialValue,
decimalEnabled: decimalEnabled,
showCloseKey: showCloseKey,
closeKeyValue: closeKeyValue,
),
);
}
......@@ -92,6 +104,9 @@ class _NumericKeyboardState extends State<NumericKeyboard> {
Navigator.of(context).pop(_text);
}
return; // 不触发 onChanged 和后续 setState 中的 pop
case '关闭':
Navigator.of(context).pop(widget.closeKeyValue);
return;
case '.':
if (!widget.decimalEnabled || _hasDecimal) return;
if (_text.isEmpty) {
......@@ -249,7 +264,7 @@ class _NumericKeyboardState extends State<NumericKeyboard> {
Widget _buildKeypad() {
// 按键定义:行列表,每行包含(标签, 宽度比例, 颜色类型)
final keys = <List<_KeyDef>>[
final topRows = <List<_KeyDef>>[
[
const _KeyDef('1', 1),
const _KeyDef('2', 1),
......@@ -262,46 +277,94 @@ class _NumericKeyboardState extends State<NumericKeyboard> {
const _KeyDef('6', 1),
const _KeyDef('C', 1, isAction: true),
],
[
const _KeyDef('7', 1),
const _KeyDef('8', 1),
const _KeyDef('9', 1),
const _KeyDef('', 1),
],
[
if (widget.decimalEnabled) const _KeyDef('.', 1),
_KeyDef('0', widget.decimalEnabled ? 2 : 3),
const _KeyDef('', 1),
],
];
return Padding(
padding: const EdgeInsets.only(top: 2),
child: Column(
children: keys.map((row) {
return Row(
children: row.map((keyDef) {
if (keyDef.label.isEmpty) {
return const Spacer(flex: 1);
}
return Expanded(
flex: keyDef.flex,
children: [
...topRows.map((row) {
return Row(
children: row.map((keyDef) {
if (keyDef.label.isEmpty) {
return const Spacer(flex: 1);
}
return Expanded(
flex: keyDef.flex,
child: Padding(
padding: const EdgeInsets.all(5),
child: _buildKeyButton(keyDef),
),
);
}).toList(),
);
}),
Row(
children: [
Expanded(
flex: 3,
child: Column(
children: [
Row(
children: [
const _KeyDef('7', 1),
const _KeyDef('8', 1),
const _KeyDef('9', 1),
].map((keyDef) {
return Expanded(
flex: keyDef.flex,
child: Padding(
padding: const EdgeInsets.all(5),
child: _buildKeyButton(keyDef),
),
);
}).toList(),
),
Row(
children: [
if (widget.decimalEnabled) const _KeyDef('.', 1),
_KeyDef('0', widget.decimalEnabled ? 2 : 3),
].map((keyDef) {
return Expanded(
flex: keyDef.flex,
child: Padding(
padding: const EdgeInsets.all(5),
child: _buildKeyButton(keyDef),
),
);
}).toList(),
),
],
),
),
Expanded(
child: Padding(
padding: const EdgeInsets.all(5),
child: _buildKeyButton(keyDef),
child: widget.showCloseKey
? _buildKeyButton(
const _KeyDef(
'关闭',
1,
isAction: true,
isDanger: true,
),
height: 130,
)
: const SizedBox.shrink(),
),
);
}).toList(),
);
}).toList(),
),
],
),
],
),
);
}
Widget _buildKeyButton(_KeyDef keyDef) {
Widget _buildKeyButton(_KeyDef keyDef, {double height = 60}) {
final isAction = keyDef.isAction;
final isDanger = keyDef.isDanger;
return SizedBox(
height: 60,
height: height,
child: Material(
color: Colors.transparent,
borderRadius: BorderRadius.circular(8),
......@@ -310,14 +373,18 @@ class _NumericKeyboardState extends State<NumericKeyboard> {
onTap: () => _onKeyTap(keyDef.label),
child: Ink(
decoration: BoxDecoration(
color: isAction
? const Color(0xFF10385D)
: const Color(0xFF092849),
color: isDanger
? const Color(0xFF7A1D1D)
: isAction
? const Color(0xFF10385D)
: const Color(0xFF092849),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: isAction
? const Color(0xFF5D9EC5)
: const Color(0xFF3F9ED2),
color: isDanger
? const Color(0xFFFF6B6B)
: isAction
? const Color(0xFF5D9EC5)
: const Color(0xFF3F9ED2),
width: 1,
),
boxShadow: const [
......@@ -334,9 +401,11 @@ class _NumericKeyboardState extends State<NumericKeyboard> {
style: TextStyle(
fontSize: isAction ? 20 : 25,
fontWeight: isAction ? FontWeight.w600 : FontWeight.w700,
color: isAction
? const Color(0xFFB8E7FF)
: const Color(0xFFEAF9FF),
color: isDanger
? const Color(0xFFFFE7E7)
: isAction
? const Color(0xFFB8E7FF)
: const Color(0xFFEAF9FF),
),
),
),
......@@ -352,5 +421,11 @@ class _KeyDef {
final String label;
final int flex;
final bool isAction;
const _KeyDef(this.label, this.flex, {this.isAction = false});
final bool isDanger;
const _KeyDef(
this.label,
this.flex, {
this.isAction = false,
this.isDanger = false,
});
}
......@@ -61,15 +61,22 @@ class DeviceControlCarePanel extends StatelessWidget {
}
}
class _CareLevelBar extends StatelessWidget {
class _CareLevelBar extends StatefulWidget {
const _CareLevelBar({required this.currentLevel});
final CareLevel currentLevel;
@override
State<_CareLevelBar> createState() => _CareLevelBarState();
}
class _CareLevelBarState extends State<_CareLevelBar> {
int? _lastDragIndex;
@override
Widget build(BuildContext context) {
final activeIndex = _careLevelItems.indexWhere(
(item) => item.level == currentLevel,
(item) => item.level == widget.currentLevel,
);
return LayoutBuilder(
......@@ -83,29 +90,38 @@ class _CareLevelBar extends StatelessWidget {
segmentWidth -
knobSize / 2;
return SizedBox(
height: knobSize,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.centerLeft,
children: [
Positioned(
left: 0,
right: 0,
top: (knobSize - 20.h) / 2,
child: Row(
children: [
for (var index = 0;
index < _careLevelItems.length;
index++) ...[
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context.read<DeviceControlBloc>().add(
DeviceControlCareLevelChanged(
_careLevelItems[index].level,
),
),
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTapDown: (details) => _setLevelFromDx(
context,
details.localPosition.dx,
constraints.maxWidth,
activeIndex,
),
onHorizontalDragUpdate: (details) => _setLevelFromDx(
context,
details.localPosition.dx,
constraints.maxWidth,
activeIndex,
),
onHorizontalDragEnd: (_) => _lastDragIndex = null,
onHorizontalDragCancel: () => _lastDragIndex = null,
child: SizedBox(
height: knobSize,
child: Stack(
clipBehavior: Clip.none,
alignment: Alignment.centerLeft,
children: [
Positioned(
left: 0,
right: 0,
top: (knobSize - 20.h) / 2,
child: Row(
children: [
for (var index = 0;
index < _careLevelItems.length;
index++) ...[
Expanded(
child: Container(
height: 20.h,
decoration: BoxDecoration(
......@@ -126,37 +142,57 @@ class _CareLevelBar extends StatelessWidget {
),
),
),
),
if (index < _careLevelItems.length - 1)
SizedBox(width: segmentGap),
if (index < _careLevelItems.length - 1)
SizedBox(width: segmentGap),
],
],
],
),
),
),
Positioned(
left: knobLeft.clamp(0, constraints.maxWidth - knobSize),
child: Container(
width: knobSize,
height: knobSize,
decoration: BoxDecoration(
color: const Color(0xFFF7F7F7),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: 0.72),
blurRadius: 10.r,
),
],
Positioned(
left: knobLeft.clamp(0, constraints.maxWidth - knobSize),
child: Container(
width: knobSize,
height: knobSize,
decoration: BoxDecoration(
color: const Color(0xFFF7F7F7),
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.white.withValues(alpha: 0.72),
blurRadius: 10.r,
),
],
),
),
),
),
],
],
),
),
);
},
);
}
void _setLevelFromDx(
BuildContext context,
double dx,
double width,
int activeIndex,
) {
if (width <= 0) return;
final index = (dx / width * _careLevelItems.length)
.floor()
.clamp(0, _careLevelItems.length - 1)
.toInt();
if (index == activeIndex || index == _lastDragIndex) return;
_lastDragIndex = index;
context.read<DeviceControlBloc>().add(
DeviceControlCareLevelChanged(_careLevelItems[index].level),
);
}
BorderRadius _segmentRadius(int index) {
final radius = Radius.circular(12.r);
if (index == 0) {
......
......@@ -155,23 +155,32 @@ class DeviceControlAdjustText extends StatelessWidget {
/// 大号操作按钮
class DeviceControlBigActionButton extends StatelessWidget {
const DeviceControlBigActionButton({super.key, required this.text});
const DeviceControlBigActionButton({
super.key,
required this.text,
this.onTap,
});
final String text;
final VoidCallback? onTap;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
height: 64.h,
decoration: deviceControlButtonDecoration(backgroundAsset: assetButton),
alignment: Alignment.center,
child: Text(
'《《 $text 》》',
style: TextStyle(
color: const Color(0xFFC9F8FF),
fontSize: 28.sp,
fontWeight: FontWeight.w700,
return GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: Container(
width: double.infinity,
height: 64.h,
decoration: deviceControlButtonDecoration(backgroundAsset: assetButton),
alignment: Alignment.center,
child: Text(
'《《 $text 》》',
style: TextStyle(
color: const Color(0xFFC9F8FF),
fontSize: 28.sp,
fontWeight: FontWeight.w700,
),
),
),
);
......
......@@ -89,69 +89,86 @@ class DeviceControlMetricCard extends StatelessWidget {
isHumidity: isHumidity,
isOxygen: isOxygen,
);
final displaySetValue = !isCo2 && !isSwitchOn ? '--' : setValue;
final displaySetValue = setValue;
final VoidCallback? switchTap = switchEventBuilder == null
? null
: () {
confirmDeviceControlChange(
context,
() => context.read<DeviceControlBloc>().add(
switchEventBuilder(!isSwitchOn),
),
);
};
final isCardActive = !isCo2 && isSwitchOn;
return DeviceControlPanelFrame(
active: isOxygen,
backgroundAsset: isOxygen ? assetMetricGreen : assetMetricBlue,
active: isCardActive,
backgroundAsset: isCardActive ? assetMetricGreen : assetMetricBlue,
padding: EdgeInsets.fromLTRB(32.w, 26.h, 32.w, 28.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Expanded(
child: DeviceControlPanelTitle(_title(metric.label)),
),
DeviceControlPill(
text: isCo2 ? 'PPM' : (isSwitchOn ? 'ON' : 'Off'),
active: !isCo2 && isSwitchOn,
onTap: switchEventBuilder == null
? null
: () {
confirmDeviceControlChange(
context,
() => context.read<DeviceControlBloc>().add(
switchEventBuilder(!isSwitchOn),
),
);
},
),
],
),
const Spacer(),
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currentValue,
style: TextStyle(
color: Colors.white,
fontSize: isCo2 ? 60.sp : 82.sp,
height: 0.9,
fontWeight: FontWeight.bold,
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: switchTap,
child: Row(
children: [
Expanded(
child: DeviceControlPanelTitle(_title(metric.label)),
),
),
SizedBox(width: 12.w),
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
isCo2 ? '' : metric.unit,
DeviceControlPill(
text: isCo2 ? 'PPM' : (isSwitchOn ? 'ON' : 'Off'),
active: !isCo2 && isSwitchOn,
onTap: switchTap,
),
],
),
),
Expanded(
child: GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: switchTap,
child: const SizedBox.expand(),
),
),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: switchTap,
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currentValue,
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
fontSize: isCo2 ? 60.sp : 82.sp,
height: 0.9,
fontWeight: FontWeight.bold,
),
),
),
const Spacer(),
Image.asset(
deviceControlIconAsset(metric.label),
width: 72.w,
height: 72.w,
fit: BoxFit.contain,
),
],
SizedBox(width: 12.w),
Padding(
padding: EdgeInsets.only(bottom: 8.h),
child: Text(
isCo2 ? '' : metric.unit,
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
),
),
),
const Spacer(),
Image.asset(
deviceControlIconAsset(metric.label),
width: 72.w,
height: 72.w,
fit: BoxFit.contain,
),
],
),
),
Divider(
color: Colors.white.withValues(alpha: 0.18), height: 28.h),
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_bloc.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_event.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_state.dart';
import 'device_control_common.dart';
......@@ -9,42 +13,64 @@ class DeviceControlOxygenTimerPanel extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DeviceControlPanelFrame(
backgroundAsset: assetPanelWide,
padding: EdgeInsets.fromLTRB(30.w, 26.h, 30.w, 28.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DeviceControlPanelTitle('供氧计时'),
const Spacer(),
Row(
return BlocBuilder<DeviceControlBloc, DeviceControlState>(
buildWhen: (previous, current) =>
previous.data.oxygenTimer != current.data.oxygenTimer,
builder: (context, state) {
final oxygenTimer = state.data.oxygenTimer;
final timeText =
'${oxygenTimer.hours}:${oxygenTimer.minutes}:${oxygenTimer.seconds}';
return DeviceControlPanelFrame(
backgroundAsset: assetPanelWide,
padding: EdgeInsets.fromLTRB(30.w, 26.h, 30.w, 28.h),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const DeviceControlPanelTitle('供氧计时'),
const Spacer(),
Row(
children: [
Text(
timeText,
style: TextStyle(
color: Colors.white,
fontSize: 42.sp,
letterSpacing: 2,
fontWeight: FontWeight.w500,
),
),
const Spacer(),
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: () => context
.read<DeviceControlBloc>()
.add(const DeviceControlOxygenTimerCleared()),
child: const DeviceControlPill(text: '清零'),
),
],
),
Divider(
color: Colors.white.withValues(alpha: 0.16), height: 38.h),
Text(
'00:00:00',
'ICU舱设置重置',
style: TextStyle(
color: Colors.white,
fontSize: 42.sp,
letterSpacing: 2,
fontWeight: FontWeight.w500,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
const DeviceControlPill(text: '清零'),
DeviceControlBigActionButton(
text: '初始化设置',
onTap: () => context
.read<DeviceControlBloc>()
.add(const DeviceControlResetSettings()),
),
],
),
Divider(color: Colors.white.withValues(alpha: 0.16), height: 38.h),
Text(
'ICU舱设置重置',
style: TextStyle(
color: Colors.white,
fontSize: 28.sp,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
const DeviceControlBigActionButton(text: '初始化设置'),
],
),
);
},
);
}
}
......@@ -76,6 +76,8 @@ class DeviceControlTimePanel extends StatelessWidget {
context,
initialValue: initialValue == '--' ? '' : initialValue,
decimalEnabled: false,
showCloseKey: true,
closeKeyValue: '0',
);
if (!context.mounted || result == null) return;
......
......@@ -651,6 +651,18 @@ class MonitoringIndexCubit extends HydratedCubit<MonitoringIndexState> {
try {
await stopBluetoothScan();
final boundDeviceId = state.boundBluetoothDeviceId;
if (boundDeviceId != null &&
boundDeviceId.isNotEmpty &&
boundDeviceId != device.remoteId) {
await unbindBluetoothDevice();
if (isClosed) return;
emit(state.copyWith(
isBluetoothBinding: true,
bindingBluetoothDeviceId: device.remoteId,
bluetoothMessage: '正在绑定蓝牙设备: ${device.name}',
));
}
await _bluetoothManager.connectToDevice(device);
if (!_bluetoothManager.isConnected) {
throw StateError('蓝牙连接未就绪');
......
......@@ -4,6 +4,8 @@ import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_bloc.dart';
import 'package:laki_icu_app/blocs/device_control/device_control_state.dart';
import 'package:laki_icu_app/enums/video_stream_mode_enum.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/utils/event_bus.dart';
......@@ -214,25 +216,52 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
Widget _buildMetrics(List<MonitoringMetricBO> metrics) {
final displayMetrics = _environmentMetrics(metrics);
final children = <Widget>[];
for (int i = 0; i < displayMetrics.length; i++) {
children.add(
Expanded(
child: MonitoringMetricCard(
metric: displayMetrics[i],
onTap: () => _openDeviceControl(displayMetrics[i]),
),
),
);
if (i < displayMetrics.length - 1) {
children.add(SizedBox(width: 20.w));
}
}
return Row(
children: children,
return BlocBuilder<DeviceControlBloc, DeviceControlState>(
buildWhen: (previous, current) =>
previous.data.cabinTemp.isOn != current.data.cabinTemp.isOn ||
previous.data.cabinHumidity.isOn != current.data.cabinHumidity.isOn ||
previous.data.oxygenConcentration.isOn !=
current.data.oxygenConcentration.isOn,
builder: (context, controlState) {
final children = <Widget>[];
for (int i = 0; i < displayMetrics.length; i++) {
final metric = displayMetrics[i];
children.add(
Expanded(
child: MonitoringMetricCard(
metric: metric,
switchValue: _metricSwitchValue(metric, controlState),
onTap: () => _openDeviceControl(metric),
),
),
);
if (i < displayMetrics.length - 1) {
children.add(SizedBox(width: 20.w));
}
}
return Row(
children: children,
);
},
);
}
bool _metricSwitchValue(
MonitoringMetricBO metric,
DeviceControlState controlState,
) {
if (metric.label.contains('温度')) {
return controlState.data.cabinTemp.isOn;
}
if (metric.label.contains('湿度')) {
return controlState.data.cabinHumidity.isOn;
}
if (metric.label.contains('氧气')) {
return controlState.data.oxygenConcentration.isOn;
}
return false;
}
void _openDeviceControl(MonitoringMetricBO metric) {
context.tabsRouter.setActiveIndex(7);
}
......
......@@ -139,11 +139,12 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
}
Widget _buildPanelStatus(BuildContext context, MonitoringIndexState state) {
final devices = _cnaDevices(state);
final message = state.isBluetoothScanning
? '自动发现附近可配对的监护舱设备,搜索中...'
: state.bluetoothDevices.isEmpty
: devices.isEmpty
? '未发现设备,可重新搜索附近蓝牙设备'
: '蓝牙扫描结束,共发现 ${state.bluetoothDevices.length} 个设备';
: '蓝牙扫描结束,共发现 ${devices.length} 个设备';
return Row(
children: [
......@@ -187,7 +188,9 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
}
Widget _buildDeviceList(BuildContext context, MonitoringIndexState state) {
if (state.bluetoothDevices.isEmpty) {
final devices = _sortDevicesForDisplay(state);
if (devices.isEmpty) {
return Center(
child: Text(
state.isBluetoothScanning ? '正在扫描附近设备...' : '暂无设备',
......@@ -199,8 +202,6 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
);
}
final devices = _sortDevicesForDisplay(state);
return ListView.separated(
padding: EdgeInsets.zero,
itemCount: devices.length,
......@@ -227,12 +228,13 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
}
List<BluetoothScanDevice> _sortDevicesForDisplay(MonitoringIndexState state) {
final devices = _cnaDevices(state);
final boundDeviceId = state.boundBluetoothDeviceId;
if (boundDeviceId == null || boundDeviceId.isEmpty) {
return state.bluetoothDevices;
return devices;
}
return List<BluetoothScanDevice>.from(state.bluetoothDevices)
return devices
..sort((left, right) {
final leftIsBound = left.remoteId == boundDeviceId;
final rightIsBound = right.remoteId == boundDeviceId;
......@@ -243,6 +245,14 @@ class _BluetoothBindDialogState extends State<BluetoothBindDialog> {
});
}
bool _isCnaDevice(BluetoothScanDevice device) {
return device.name.trim().toUpperCase().startsWith('CNA');
}
List<BluetoothScanDevice> _cnaDevices(MonitoringIndexState state) {
return state.bluetoothDevices.where(_isCnaDevice).toList();
}
Future<void> _confirmUnbindBluetoothDevice(
BuildContext context,
BluetoothScanDevice device,
......
......@@ -76,16 +76,22 @@ class MonitoringMetricCard extends StatelessWidget {
height: 41.h,
alignment: AlignmentDirectional.center,
decoration: BoxDecoration(
color: Color(0xFF0A1B34),
color: switchValue
? const Color(0xFF063F1A)
: const Color(0xFF0A1B34),
border: BoxBorder.all(
width: 1.w,
color: Color(0xFF5A8BB0),
color: switchValue
? const Color(0xFF3EFF5F)
: const Color(0xFF5A8BB0),
),
borderRadius: BorderRadius.circular(20)),
child: Text(
"ON",
switchValue ? "ON" : "Off",
style: TextStyle(
color: Color(0xFF3C6082),
color: switchValue
? const Color(0xFF52FFBB)
: const Color(0xFF3C6082),
fontSize: 24.sp,
fontWeight: FontWeight.bold),
),
......
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart';
import 'package:laki_icu_app/models/bo/device_threshold_config_bo.dart';
import 'package:laki_icu_app/utils/toast_utils.dart';
import 'factory_settings_index_state.dart';
......@@ -14,6 +15,10 @@ class FactorySettingsIndexCubit extends Cubit<FactorySettingsIndexState> {
final DeviceThresholdConfigBloc _thresholdConfigBloc;
void _saveEditingConfig(DeviceThresholdConfigBO config) {
_thresholdConfigBloc.updateConfig(config);
}
void showCalibrationPage() {
emit(state.copyWith(isCalibrationPage: true));
}
......@@ -23,66 +28,92 @@ class FactorySettingsIndexCubit extends Cubit<FactorySettingsIndexState> {
}
void editTempMax(int value) {
final editingConfig = state.editingConfig.copyWith(tempMax: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(tempMax: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editTempMin(int value) {
final editingConfig = state.editingConfig.copyWith(tempMin: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(tempMin: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editOxygenMax(int value) {
final editingConfig = state.editingConfig.copyWith(oxygenMax: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(oxygenMax: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editOxygenMin(int value) {
final editingConfig = state.editingConfig.copyWith(oxygenMin: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(oxygenMin: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editHumidityMax(int value) {
final editingConfig = state.editingConfig.copyWith(humidityMax: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(humidityMax: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editHumidityMin(int value) {
final editingConfig = state.editingConfig.copyWith(humidityMin: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(humidityMin: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editUvTimeMax(int value) {
final editingConfig = state.editingConfig.copyWith(uvTimeMax: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(uvTimeMax: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editIrTimeMax(int value) {
final editingConfig = state.editingConfig.copyWith(irTimeMax: value);
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(irTimeMax: value),
isDirty: true,
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void editAtomizeTimeMax(int value) {
final editingConfig = state.editingConfig.copyWith(atomizeTimeMax: value);
emit(state.copyWith(
editingConfig: editingConfig,
isDirty: false,
));
_saveEditingConfig(editingConfig);
}
void resetToDefault() {
emit(state.copyWith(
editingConfig: state.editingConfig.copyWith(atomizeTimeMax: value),
isDirty: true,
editingConfig: DeviceThresholdConfigBO.defaultConfig,
isDirty: false,
));
_saveEditingConfig(DeviceThresholdConfigBO.defaultConfig);
}
void applyChanges() {
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/blocs/device_calibration_config/device_calibration_config.dart';
import 'package:laki_icu_app/models/bo/device_calibration_config_bo.dart';
import 'package:laki_icu_app/utils/numeric_keyboard.dart';
import 'package:laki_icu_app/utils/toast_utils.dart';
import '../../widgets/settings_panel_frame.dart';
import '../cubit/factory_settings_index_cubit.dart';
......@@ -20,42 +23,57 @@ class FactoryDeviceCalibrationPanel extends StatefulWidget {
class _FactoryDeviceCalibrationPanelState
extends State<FactoryDeviceCalibrationPanel> {
late List<_CalibrationSection> _sections;
late DeviceCalibrationConfigBO _config;
@override
void initState() {
super.initState();
_sections = _defaultSections();
_config = context.read<DeviceCalibrationConfigBloc>().state.config;
}
Future<void> _editItem(_CalibrationItem item) async {
Future<void> _editItem(DeviceCalibrationItemBO item) async {
final result = await NumericKeyboard.show(
context,
initialValue: item.value,
initialValue: '${item.value}',
decimalEnabled: false,
);
if (!mounted || result == null) return;
final value = result.trim();
if (value.isEmpty) return;
final parsedValue = int.tryParse(value);
if (parsedValue == null) return;
setState(() {
item.value = value;
_config = _config.updateItem(item.key, parsedValue);
});
_saveConfig();
}
void _resetDefaults() {
setState(() {
_sections = _defaultSections();
_config = DeviceCalibrationConfigBO.defaultConfig;
});
_saveConfig();
}
void _saveConfig() {
context.read<DeviceCalibrationConfigBloc>().updateConfig(_config);
}
void _applyChanges() {
// UI placeholder only. Real calibration persistence will be wired later.
try {
_saveConfig();
showToast('操作成功');
} catch (e) {
showToast('操作失败');
}
}
@override
Widget build(BuildContext context) {
final sections = _config.sections;
return Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
......@@ -66,12 +84,12 @@ class _FactoryDeviceCalibrationPanelState
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CalibrationSectionView(
section: _sections[0],
section: sections[0],
onItemTap: _enableCalibrationEditing ? _editItem : null,
),
SizedBox(height: 28.h),
_CalibrationSectionView(
section: _sections[1],
section: sections[1],
onItemTap: _enableCalibrationEditing ? _editItem : null,
),
],
......@@ -88,7 +106,7 @@ class _FactoryDeviceCalibrationPanelState
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_CalibrationSectionView(
section: _sections[2],
section: sections[2],
onItemTap: _enableCalibrationEditing ? _editItem : null,
),
const Spacer(),
......@@ -142,8 +160,8 @@ class _CalibrationSectionView extends StatelessWidget {
required this.onItemTap,
});
final _CalibrationSection section;
final ValueChanged<_CalibrationItem>? onItemTap;
final DeviceCalibrationSectionBO section;
final ValueChanged<DeviceCalibrationItemBO>? onItemTap;
@override
Widget build(BuildContext context) {
......@@ -179,7 +197,7 @@ class _CalibrationRow extends StatelessWidget {
required this.onTap,
});
final _CalibrationItem item;
final DeviceCalibrationItemBO item;
final VoidCallback? onTap;
@override
......@@ -202,7 +220,7 @@ class _CalibrationRow extends StatelessWidget {
GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: onTap,
child: _CalibrationValueBox(value: item.value),
child: _CalibrationValueBox(value: '${item.value}'),
),
],
);
......@@ -277,58 +295,3 @@ class _CalibrationButton extends StatelessWidget {
);
}
}
class _CalibrationSection {
const _CalibrationSection({
required this.title,
required this.items,
});
final String title;
final List<_CalibrationItem> items;
}
class _CalibrationItem {
_CalibrationItem({
required this.label,
required this.value,
});
final String label;
String value;
}
List<_CalibrationSection> _defaultSections() {
return [
_CalibrationSection(
title: '主温度调节系数',
items: [
_CalibrationItem(label: '温度系数%S1(上):', value: '17'),
_CalibrationItem(label: '温度系数%S1(下):', value: '34'),
_CalibrationItem(label: '环境温度冬夏临界值T0:', value: '17'),
_CalibrationItem(label: '摄像头循环重启(分钟):', value: '34'),
_CalibrationItem(label: '摄像头定时重启(小时):', value: '17'),
],
),
_CalibrationSection(
title: 'CO₂报警及净化修正值',
items: [
_CalibrationItem(label: 'CO₂最低设置值C1:', value: '4000'),
_CalibrationItem(label: 'CO₂差值C2:', value: '2000'),
_CalibrationItem(label: 'CO₂基准值C3:', value: '1500'),
_CalibrationItem(label: 'CO₂系数C4:', value: '34'),
],
),
_CalibrationSection(
title: '密闭舱氧气修正值',
items: [
_CalibrationItem(label: '密闭舱氧气修正点X0:', value: '17'),
_CalibrationItem(label: '密闭舱氧气修正幅度X1:', value: '34'),
_CalibrationItem(label: '密闭舱氧气修正步进值X4:', value: '17'),
_CalibrationItem(label: '开放式供氧氧气修正点S0:', value: '34'),
_CalibrationItem(label: '开放式供氧氧气修正幅度S1:', value: '17'),
_CalibrationItem(label: '开放式供氧氧气修正步进值S4:', value: '17'),
],
),
];
}
......@@ -143,7 +143,10 @@ class _FactoryActionColumn extends StatelessWidget {
if (_showFactoryCalibrationEntry && _showFactoryInitEntry)
SizedBox(height: 28.h),
if (_showFactoryInitEntry)
const _WideFactoryButton(text: '初始化设置'),
_WideFactoryButton(
text: '初始化设置',
onPressed: () => cubit.resetToDefault(),
),
],
),
),
......
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