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