Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
L
laki_icu_app
Project
Project
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
张宏
laki_icu_app
Commits
c701cce4
Commit
c701cce4
authored
Jun 26, 2026
by
akari
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'master' of
http://git.ruanyiit.com/zhanghong/laki_icu_app
parents
862904c4
fb9b8d79
Hide whitespace changes
Inline
Side-by-side
Showing
18 changed files
with
1164 additions
and
138 deletions
+1164
-138
device_threshold_config.dart
...locs/device_threshold_config/device_threshold_config.dart
+3
-0
device_threshold_config_bloc.dart
...device_threshold_config/device_threshold_config_bloc.dart
+280
-0
device_threshold_config_event.dart
...evice_threshold_config/device_threshold_config_event.dart
+107
-0
device_threshold_config_state.dart
...evice_threshold_config/device_threshold_config_state.dart
+30
-0
main.dart
lib/main.dart
+9
-0
camera_alarm_bo.dart
lib/models/bo/camera_alarm_bo.dart
+3
-1
device_threshold_config_bo.dart
lib/models/bo/device_threshold_config_bo.dart
+125
-0
monitoring_bo.dart
lib/models/bo/monitoring_bo.dart
+6
-1
cabin_repository.dart
lib/repositories/cabin_repository.dart
+5
-4
date_utils.dart
lib/utils/date_utils.dart
+15
-0
laki_mqtt_client.dart
lib/utils/mqtt/laki_mqtt_client.dart
+71
-2
monitoring_index_cubit.dart
lib/views/monitoring/index/cubit/monitoring_index_cubit.dart
+202
-56
monitoring_pet_info_card.dart
...ws/monitoring/index/widgets/monitoring_pet_info_card.dart
+8
-1
factory_settings_index_cubit.dart
.../factory_settings/cubit/factory_settings_index_cubit.dart
+84
-1
factory_settings_index_state.dart
.../factory_settings/cubit/factory_settings_index_state.dart
+52
-35
factory_settings_index_view.dart
...ettings/factory_settings/factory_settings_index_view.dart
+4
-1
factory_settings_panel.dart
...ings/factory_settings/widgets/factory_settings_panel.dart
+159
-36
pubspec.yaml
pubspec.yaml
+1
-0
No files found.
lib/blocs/device_threshold_config/device_threshold_config.dart
0 → 100644
View file @
c701cce4
export
'device_threshold_config_bloc.dart'
;
export
'device_threshold_config_event.dart'
;
export
'device_threshold_config_state.dart'
;
lib/blocs/device_threshold_config/device_threshold_config_bloc.dart
0 → 100644
View file @
c701cce4
import
'package:hydrated_bloc/hydrated_bloc.dart'
;
import
'package:laki_icu_app/models/bo/device_threshold_config_bo.dart'
;
import
'device_threshold_config_event.dart'
;
import
'device_threshold_config_state.dart'
;
class
DeviceThresholdConfigBloc
extends
HydratedBloc
<
DeviceThresholdConfigEvent
,
DeviceThresholdConfigState
>
{
DeviceThresholdConfigBloc
()
:
super
(
const
DeviceThresholdConfigState
())
{
on
<
DeviceThresholdConfigLoaded
>(
_onLoaded
);
on
<
DeviceThresholdConfigTempMaxUpdated
>(
_onTempMaxUpdated
);
on
<
DeviceThresholdConfigTempMinUpdated
>(
_onTempMinUpdated
);
on
<
DeviceThresholdConfigOxygenMaxUpdated
>(
_onOxygenMaxUpdated
);
on
<
DeviceThresholdConfigOxygenMinUpdated
>(
_onOxygenMinUpdated
);
on
<
DeviceThresholdConfigHumidityMaxUpdated
>(
_onHumidityMaxUpdated
);
on
<
DeviceThresholdConfigHumidityMinUpdated
>(
_onHumidityMinUpdated
);
on
<
DeviceThresholdConfigUvTimeMaxUpdated
>(
_onUvTimeMaxUpdated
);
on
<
DeviceThresholdConfigIrTimeMaxUpdated
>(
_onIrTimeMaxUpdated
);
on
<
DeviceThresholdConfigAtomizeTimeMaxUpdated
>(
_onAtomizeTimeMaxUpdated
);
on
<
DeviceThresholdConfigUpdated
>(
_onConfigUpdated
);
on
<
DeviceThresholdConfigResetToDefault
>(
_onResetToDefault
);
}
Future
<
void
>
_onLoaded
(
DeviceThresholdConfigLoaded
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
async
{
emit
(
state
.
copyWith
(
isLoading:
true
,
clearError:
true
));
try
{
emit
(
state
.
copyWith
(
isLoading:
false
,
clearError:
true
));
}
catch
(
e
)
{
emit
(
state
.
copyWith
(
isLoading:
false
,
error:
e
.
toString
()));
}
}
void
_onTempMaxUpdated
(
DeviceThresholdConfigTempMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
tempMax:
event
.
value
),
clearError:
true
,
));
}
void
_onTempMinUpdated
(
DeviceThresholdConfigTempMinUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
tempMin:
event
.
value
),
clearError:
true
,
));
}
void
_onOxygenMaxUpdated
(
DeviceThresholdConfigOxygenMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
oxygenMax:
event
.
value
),
clearError:
true
,
));
}
void
_onOxygenMinUpdated
(
DeviceThresholdConfigOxygenMinUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
oxygenMin:
event
.
value
),
clearError:
true
,
));
}
void
_onHumidityMaxUpdated
(
DeviceThresholdConfigHumidityMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
humidityMax:
event
.
value
),
clearError:
true
,
));
}
void
_onHumidityMinUpdated
(
DeviceThresholdConfigHumidityMinUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
humidityMin:
event
.
value
),
clearError:
true
,
));
}
void
_onUvTimeMaxUpdated
(
DeviceThresholdConfigUvTimeMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
uvTimeMax:
event
.
value
),
clearError:
true
,
));
}
void
_onIrTimeMaxUpdated
(
DeviceThresholdConfigIrTimeMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
irTimeMax:
event
.
value
),
clearError:
true
,
));
}
void
_onAtomizeTimeMaxUpdated
(
DeviceThresholdConfigAtomizeTimeMaxUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
state
.
config
.
copyWith
(
atomizeTimeMax:
event
.
value
),
clearError:
true
,
));
}
void
_onConfigUpdated
(
DeviceThresholdConfigUpdated
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
state
.
copyWith
(
config:
event
.
config
,
clearError:
true
,
));
}
void
_onResetToDefault
(
DeviceThresholdConfigResetToDefault
event
,
Emitter
<
DeviceThresholdConfigState
>
emit
,
)
{
emit
(
const
DeviceThresholdConfigState
(
config:
DeviceThresholdConfigBO
.
defaultConfig
,
));
}
bool
isTemperatureOutOfRange
(
int
currentTemp
)
{
return
currentTemp
<
state
.
config
.
tempMin
||
currentTemp
>
state
.
config
.
tempMax
;
}
bool
isTemperatureTooLow
(
int
currentTemp
)
{
return
currentTemp
<
state
.
config
.
tempMin
;
}
bool
isTemperatureTooHigh
(
int
currentTemp
)
{
return
currentTemp
>
state
.
config
.
tempMax
;
}
bool
isOxygenOutOfRange
(
int
currentOxygen
)
{
return
currentOxygen
<
state
.
config
.
oxygenMin
||
currentOxygen
>
state
.
config
.
oxygenMax
;
}
bool
isOxygenTooLow
(
int
currentOxygen
)
{
return
currentOxygen
<
state
.
config
.
oxygenMin
;
}
bool
isOxygenTooHigh
(
int
currentOxygen
)
{
return
currentOxygen
>
state
.
config
.
oxygenMax
;
}
bool
isHumidityOutOfRange
(
int
currentHumidity
)
{
return
currentHumidity
<
state
.
config
.
humidityMin
||
currentHumidity
>
state
.
config
.
humidityMax
;
}
bool
isHumidityTooLow
(
int
currentHumidity
)
{
return
currentHumidity
<
state
.
config
.
humidityMin
;
}
bool
isHumidityTooHigh
(
int
currentHumidity
)
{
return
currentHumidity
>
state
.
config
.
humidityMax
;
}
bool
isUvTimeOverLimit
(
int
currentTime
)
{
return
currentTime
>
state
.
config
.
uvTimeMax
;
}
bool
isIrTimeOverLimit
(
int
currentTime
)
{
return
currentTime
>
state
.
config
.
irTimeMax
;
}
bool
isAtomizeTimeOverLimit
(
int
currentTime
)
{
return
currentTime
>
state
.
config
.
atomizeTimeMax
;
}
String
getTempRangeDescription
()
{
return
'
${state.config.tempMin}
℃ -
${state.config.tempMax}
℃'
;
}
String
getOxygenRangeDescription
()
{
return
'
${state.config.oxygenMin}
% -
${state.config.oxygenMax}
%'
;
}
String
getHumidityRangeDescription
()
{
return
'
${state.config.humidityMin}
% -
${state.config.humidityMax}
%'
;
}
Map
<
String
,
String
>
getAllThresholdDescriptions
()
{
return
{
'temp'
:
getTempRangeDescription
(),
'oxygen'
:
getOxygenRangeDescription
(),
'humidity'
:
getHumidityRangeDescription
(),
'uvTime'
:
'≤
${state.config.uvTimeMax}
分钟'
,
'irTime'
:
'≤
${state.config.irTimeMax}
分钟'
,
'atomizeTime'
:
'≤
${state.config.atomizeTimeMax}
分钟'
,
};
}
List
<
String
>
checkOutOfRangeIndicators
({
int
?
temp
,
int
?
oxygen
,
int
?
humidity
,
})
{
final
List
<
String
>
outOfRange
=
[];
if
(
temp
!=
null
&&
isTemperatureOutOfRange
(
temp
))
{
outOfRange
.
add
(
'温度'
);
}
if
(
oxygen
!=
null
&&
isOxygenOutOfRange
(
oxygen
))
{
outOfRange
.
add
(
'氧浓度'
);
}
if
(
humidity
!=
null
&&
isHumidityOutOfRange
(
humidity
))
{
outOfRange
.
add
(
'湿度'
);
}
return
outOfRange
;
}
void
updateTempMax
(
int
value
)
=>
add
(
DeviceThresholdConfigTempMaxUpdated
(
value
));
void
updateTempMin
(
int
value
)
=>
add
(
DeviceThresholdConfigTempMinUpdated
(
value
));
void
updateOxygenMax
(
int
value
)
=>
add
(
DeviceThresholdConfigOxygenMaxUpdated
(
value
));
void
updateOxygenMin
(
int
value
)
=>
add
(
DeviceThresholdConfigOxygenMinUpdated
(
value
));
void
updateHumidityMax
(
int
value
)
=>
add
(
DeviceThresholdConfigHumidityMaxUpdated
(
value
));
void
updateHumidityMin
(
int
value
)
=>
add
(
DeviceThresholdConfigHumidityMinUpdated
(
value
));
void
updateUvTimeMax
(
int
value
)
=>
add
(
DeviceThresholdConfigUvTimeMaxUpdated
(
value
));
void
updateIrTimeMax
(
int
value
)
=>
add
(
DeviceThresholdConfigIrTimeMaxUpdated
(
value
));
void
updateAtomizeTimeMax
(
int
value
)
=>
add
(
DeviceThresholdConfigAtomizeTimeMaxUpdated
(
value
));
void
updateConfig
(
DeviceThresholdConfigBO
config
)
=>
add
(
DeviceThresholdConfigUpdated
(
config
));
void
resetToDefault
()
=>
add
(
const
DeviceThresholdConfigResetToDefault
());
@override
DeviceThresholdConfigState
?
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
try
{
return
DeviceThresholdConfigState
(
config:
DeviceThresholdConfigBO
(
tempMax:
json
[
'tempMax'
]
as
int
?
??
34
,
tempMin:
json
[
'tempMin'
]
as
int
?
??
17
,
oxygenMax:
json
[
'oxygenMax'
]
as
int
?
??
34
,
oxygenMin:
json
[
'oxygenMin'
]
as
int
?
??
17
,
humidityMax:
json
[
'humidityMax'
]
as
int
?
??
34
,
humidityMin:
json
[
'humidityMin'
]
as
int
?
??
17
,
uvTimeMax:
json
[
'uvTimeMax'
]
as
int
?
??
34
,
irTimeMax:
json
[
'irTimeMax'
]
as
int
?
??
17
,
atomizeTimeMax:
json
[
'atomizeTimeMax'
]
as
int
?
??
17
,
),
);
}
catch
(
e
)
{
return
null
;
}
}
@override
Map
<
String
,
dynamic
>?
toJson
(
DeviceThresholdConfigState
state
)
{
return
{
'tempMax'
:
state
.
config
.
tempMax
,
'tempMin'
:
state
.
config
.
tempMin
,
'oxygenMax'
:
state
.
config
.
oxygenMax
,
'oxygenMin'
:
state
.
config
.
oxygenMin
,
'humidityMax'
:
state
.
config
.
humidityMax
,
'humidityMin'
:
state
.
config
.
humidityMin
,
'uvTimeMax'
:
state
.
config
.
uvTimeMax
,
'irTimeMax'
:
state
.
config
.
irTimeMax
,
'atomizeTimeMax'
:
state
.
config
.
atomizeTimeMax
,
};
}
}
lib/blocs/device_threshold_config/device_threshold_config_event.dart
0 → 100644
View file @
c701cce4
import
'package:equatable/equatable.dart'
;
import
'package:laki_icu_app/models/bo/device_threshold_config_bo.dart'
;
abstract
class
DeviceThresholdConfigEvent
extends
Equatable
{
const
DeviceThresholdConfigEvent
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
DeviceThresholdConfigLoaded
extends
DeviceThresholdConfigEvent
{
const
DeviceThresholdConfigLoaded
();
}
class
DeviceThresholdConfigTempMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigTempMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigTempMinUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigTempMinUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigOxygenMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigOxygenMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigOxygenMinUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigOxygenMinUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigHumidityMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigHumidityMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigHumidityMinUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigHumidityMinUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigUvTimeMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigUvTimeMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigIrTimeMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigIrTimeMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigAtomizeTimeMaxUpdated
extends
DeviceThresholdConfigEvent
{
final
int
value
;
const
DeviceThresholdConfigAtomizeTimeMaxUpdated
(
this
.
value
);
@override
List
<
Object
?>
get
props
=>
[
value
];
}
class
DeviceThresholdConfigUpdated
extends
DeviceThresholdConfigEvent
{
final
DeviceThresholdConfigBO
config
;
const
DeviceThresholdConfigUpdated
(
this
.
config
);
@override
List
<
Object
?>
get
props
=>
[
config
];
}
class
DeviceThresholdConfigResetToDefault
extends
DeviceThresholdConfigEvent
{
const
DeviceThresholdConfigResetToDefault
();
}
lib/blocs/device_threshold_config/device_threshold_config_state.dart
0 → 100644
View file @
c701cce4
import
'package:equatable/equatable.dart'
;
import
'package:laki_icu_app/models/bo/device_threshold_config_bo.dart'
;
class
DeviceThresholdConfigState
extends
Equatable
{
const
DeviceThresholdConfigState
({
this
.
config
=
DeviceThresholdConfigBO
.
defaultConfig
,
this
.
isLoading
=
false
,
this
.
error
,
});
final
DeviceThresholdConfigBO
config
;
final
bool
isLoading
;
final
String
?
error
;
DeviceThresholdConfigState
copyWith
({
DeviceThresholdConfigBO
?
config
,
bool
?
isLoading
,
String
?
error
,
bool
clearError
=
false
,
})
{
return
DeviceThresholdConfigState
(
config:
config
??
this
.
config
,
isLoading:
isLoading
??
this
.
isLoading
,
error:
clearError
?
null
:
error
??
this
.
error
,
);
}
@override
List
<
Object
?>
get
props
=>
[
config
,
isLoading
,
error
];
}
lib/main.dart
View file @
c701cce4
...
...
@@ -29,6 +29,7 @@ import 'package:flutter_ume_kit_dio_plus/flutter_ume_kit_dio_plus.dart';
import
'package:flutter_ume_kit_ui_plus/flutter_ume_kit_ui_plus.dart'
;
import
'package:laki_icu_app/blocs/device_control/device_control_bloc.dart'
;
import
'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart'
;
import
'package:marionette_flutter/marionette_flutter.dart'
;
Future
<
void
>
main
(
List
<
String
>
args
)
async
{
...
...
@@ -77,6 +78,7 @@ class _MyAppState extends State<MyApp> {
late
final
AuthBloc
_authBloc
;
BluetoothReadBloc
?
_bluetoothReadBloc
;
DeviceControlBloc
?
_deviceControlBloc
;
DeviceThresholdConfigBloc
?
_deviceThresholdConfigBloc
;
StreamSubscription
?
_tokenExpiredSubscription
;
bool
_isInitializing
=
true
;
...
...
@@ -88,6 +90,9 @@ class _MyAppState extends State<MyApp> {
DeviceControlBloc
get
_deviceControlBlocInstance
=>
_deviceControlBloc
??=
DeviceControlBloc
();
DeviceThresholdConfigBloc
get
_deviceThresholdConfigBlocInstance
=>
_deviceThresholdConfigBloc
??=
DeviceThresholdConfigBloc
();
@override
void
initState
()
{
super
.
initState
();
...
...
@@ -173,6 +178,7 @@ class _MyAppState extends State<MyApp> {
_authBloc
.
close
();
_bluetoothReadBloc
?.
close
();
_deviceControlBloc
?.
close
();
_deviceThresholdConfigBloc
?.
close
();
super
.
dispose
();
}
...
...
@@ -190,6 +196,9 @@ class _MyAppState extends State<MyApp> {
BlocProvider
<
DeviceControlBloc
>.
value
(
value:
_deviceControlBlocInstance
,
),
BlocProvider
<
DeviceThresholdConfigBloc
>.
value
(
value:
_deviceThresholdConfigBlocInstance
,
),
// BlocProvider<CounterBloc>(create: (_) => CounterBloc()),
],
child:
MultiBlocListener
(
...
...
lib/models/bo/camera_alarm_bo.dart
View file @
c701cce4
...
...
@@ -63,11 +63,13 @@ class CameraAlarmBO {
);
}
/// 与 Android 行为编码映射完全一致:
/// 行为编码 → 标题映射:
/// 1 → 宠物抓挠
/// 2 → 异常液体
/// 3 → 检测到排泄物
/// 其他 → 未知异常
String
get
titleByBehaviorCode
{
if
(
behaviorCode
==
1
)
return
'宠物抓挠'
;
if
(
behaviorCode
==
2
)
return
'异常液体'
;
if
(
behaviorCode
==
3
)
return
'检测到排泄物'
;
return
'未知异常'
;
...
...
lib/models/bo/device_threshold_config_bo.dart
0 → 100644
View file @
c701cce4
import
'package:equatable/equatable.dart'
;
/// 用途:设备阈值配置数据
/// 涉及页面:设备阈值配置页面
class
DeviceThresholdConfigBO
extends
Equatable
{
/// 温度上限 ℃
final
int
tempMax
;
/// 温度下限 ℃
final
int
tempMin
;
/// 氧浓度上限 %
final
int
oxygenMax
;
/// 氧浓度下限 %
final
int
oxygenMin
;
/// 湿度上限 %
final
int
humidityMax
;
/// 湿度下限 %
final
int
humidityMin
;
/// 紫外线时间上限 单位:分钟
final
int
uvTimeMax
;
/// 红外线时间上限 单位:分钟
final
int
irTimeMax
;
/// 雾化时间上限 单位:分钟
final
int
atomizeTimeMax
;
const
DeviceThresholdConfigBO
({
required
this
.
tempMax
,
required
this
.
tempMin
,
required
this
.
oxygenMax
,
required
this
.
oxygenMin
,
required
this
.
humidityMax
,
required
this
.
humidityMin
,
required
this
.
uvTimeMax
,
required
this
.
irTimeMax
,
required
this
.
atomizeTimeMax
,
});
/// UI 默认初始值(截图默认上限34,下限17)
static
const
DeviceThresholdConfigBO
defaultConfig
=
DeviceThresholdConfigBO
(
tempMax:
34
,
tempMin:
17
,
oxygenMax:
34
,
oxygenMin:
17
,
humidityMax:
34
,
humidityMin:
17
,
uvTimeMax:
34
,
irTimeMax:
17
,
atomizeTimeMax:
17
,
);
/// 拷贝修改(用于表单更新)
DeviceThresholdConfigBO
copyWith
({
int
?
tempMax
,
int
?
tempMin
,
int
?
oxygenMax
,
int
?
oxygenMin
,
int
?
humidityMax
,
int
?
humidityMin
,
int
?
uvTimeMax
,
int
?
irTimeMax
,
int
?
atomizeTimeMax
,
})
{
return
DeviceThresholdConfigBO
(
tempMax:
tempMax
??
this
.
tempMax
,
tempMin:
tempMin
??
this
.
tempMin
,
oxygenMax:
oxygenMax
??
this
.
oxygenMax
,
oxygenMin:
oxygenMin
??
this
.
oxygenMin
,
humidityMax:
humidityMax
??
this
.
humidityMax
,
humidityMin:
humidityMin
??
this
.
humidityMin
,
uvTimeMax:
uvTimeMax
??
this
.
uvTimeMax
,
irTimeMax:
irTimeMax
??
this
.
irTimeMax
,
atomizeTimeMax:
atomizeTimeMax
??
this
.
atomizeTimeMax
,
);
}
/// 从 JSON 解析模型
factory
DeviceThresholdConfigBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
DeviceThresholdConfigBO
(
tempMax:
json
[
'tempMax'
]
as
int
?
??
34
,
tempMin:
json
[
'tempMin'
]
as
int
?
??
17
,
oxygenMax:
json
[
'oxygenMax'
]
as
int
?
??
34
,
oxygenMin:
json
[
'oxygenMin'
]
as
int
?
??
17
,
humidityMax:
json
[
'humidityMax'
]
as
int
?
??
34
,
humidityMin:
json
[
'humidityMin'
]
as
int
?
??
17
,
uvTimeMax:
json
[
'uvTimeMax'
]
as
int
?
??
34
,
irTimeMax:
json
[
'irTimeMax'
]
as
int
?
??
17
,
atomizeTimeMax:
json
[
'atomizeTimeMax'
]
as
int
?
??
17
,
);
}
/// 转 JSON(接口请求/本地存储)
Map
<
String
,
dynamic
>
toJson
()
{
return
{
'tempMax'
:
tempMax
,
'tempMin'
:
tempMin
,
'oxygenMax'
:
oxygenMax
,
'oxygenMin'
:
oxygenMin
,
'humidityMax'
:
humidityMax
,
'humidityMin'
:
humidityMin
,
'uvTimeMax'
:
uvTimeMax
,
'irTimeMax'
:
irTimeMax
,
'atomizeTimeMax'
:
atomizeTimeMax
,
};
}
@override
List
<
Object
?>
get
props
=>
[
tempMax
,
tempMin
,
oxygenMax
,
oxygenMin
,
humidityMax
,
humidityMin
,
uvTimeMax
,
irTimeMax
,
atomizeTimeMax
,
];
}
lib/models/bo/monitoring_bo.dart
View file @
c701cce4
...
...
@@ -155,11 +155,15 @@ class AlertInfoBO extends Equatable {
/// 告警处理状态
final
String
status
;
/// MQTT 下发告警记录 ID,上行 camera/{SN}/up 时使用
final
int
?
alarmId
;
const
AlertInfoBO
({
required
this
.
title
,
required
this
.
description
,
required
this
.
time
,
this
.
status
=
'已处理'
,
this
.
alarmId
,
});
factory
AlertInfoBO
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
...
...
@@ -168,11 +172,12 @@ class AlertInfoBO extends Equatable {
description:
json
[
'description'
]
as
String
?
??
''
,
time:
json
[
'time'
]
as
String
?
??
''
,
status:
json
[
'status'
]
as
String
?
??
'已处理'
,
alarmId:
json
[
'alarmId'
]
as
int
?,
);
}
@override
List
<
Object
?>
get
props
=>
[
title
,
description
,
time
,
status
];
List
<
Object
?>
get
props
=>
[
title
,
description
,
time
,
status
,
alarmId
];
}
/// 用途:首页监护舱右侧设备菜单数据
...
...
lib/repositories/cabin_repository.dart
View file @
c701cce4
...
...
@@ -15,12 +15,13 @@ class CabinRepository {
Future
<
ResponseModel
<
CabinDetailResponse
>>
getCabinDetail
(
String
carbinSn
,
)
{
if
(
Constants
.
useMockData
)
{
return
_mockCabinDetail
();
}
//
if (Constants.useMockData) {
//
return _mockCabinDetail();
//
}
return
DioRequest
.
instance
.
get
<
CabinDetailResponse
>(
'/icu/detail'
,
queryParameters:
{
'carbinSn'
:
carbinSn
},
// queryParameters: {'carbinSn': carbinSn},
queryParameters:
{
'carbinSn'
:
'CNA07212L'
},
fromJsonT:
(
data
)
=>
CabinDetailResponse
.
fromJson
(
data
as
Map
<
String
,
dynamic
>),
);
...
...
lib/utils/date_utils.dart
0 → 100644
View file @
c701cce4
/// ISO 时间字符串 → yyyy-MM-dd HH:mm:ss
///
/// 输入格式: "2026-06-25T17:08:11.234234234"
/// 输出格式: "2026-06-25 17:08:11"
String
formatIsoTime
(
String
?
isoTime
,
{
String
fallback
=
''
})
{
if
(
isoTime
==
null
||
isoTime
.
isEmpty
)
return
fallback
;
try
{
final
cleaned
=
isoTime
.
contains
(
'T'
)
?
isoTime
.
replaceFirst
(
'T'
,
' '
)
:
isoTime
;
final
dotIndex
=
cleaned
.
indexOf
(
'.'
);
return
dotIndex
>=
0
?
cleaned
.
substring
(
0
,
dotIndex
)
:
cleaned
;
}
catch
(
_
)
{
return
isoTime
;
}
}
lib/utils/mqtt/laki_mqtt_client.dart
View file @
c701cce4
import
'dart:async'
;
import
'dart:convert'
;
import
'dart:io'
;
import
'dart:math'
;
import
'package:laki_icu_app/utils/bluetooth/index.dart'
;
import
'package:mqtt_client/mqtt_client.dart'
;
...
...
@@ -69,11 +70,16 @@ class LakiMqttTopics {
static
String
icuUp
(
String
deviceSn
)
=>
'icu/
$deviceSn
/up'
;
static
String
cameraUp
(
String
deviceSn
)
=>
'camera/
$deviceSn
/up'
;
// stephen 上行 宠物异常行为处理结果 camera/{SN}/up → CameraResultHandler
static
String
icuAlarm
(
String
deviceSn
)
=>
'icu/
$deviceSn
/alarm'
;
// stephen 上行 传感器(设备)异常告警 icu/{SN}/alarm → DeviceAlarmHandler
static
String
icuTo
(
String
deviceSn
)
=>
'icu/
$deviceSn
/to'
;
static
String
icuPetBind
(
String
deviceSn
)
=>
'icu/
$deviceSn
/pet/bind'
;
static
String
icuPetBind
(
String
deviceSn
)
=>
'icu/
$deviceSn
/pet/bind'
;
// stephen 下行 宠物 绑定、出舱
static
String
cameraTo
(
String
cabinSn
)
=>
'camera/
$cabinSn
/to'
;
// 获取摄像头通道的告警信息
static
String
cameraTo
(
String
cabinSn
)
=>
'camera/
$cabinSn
/to'
;
//
stephen 下行
获取摄像头通道的告警信息
static
const
String
clientConnected
=
r'$events/client_connected'
;
...
...
@@ -175,6 +181,11 @@ class LakiMqttClient {
_client
=
client
;
_listenUpdates
(
client
);
_emitState
(
LakiMqttConnectionState
.
connected
);
// ignore: avoid_print
print
(
'[LakiMqttClient] ✅ MQTT 已连接成功, clientId=
${_config.clientId}
'
'host=
${_config.host}
'
,
);
}
void
subscribe
(
String
topic
,
{
MqttQos
qos
=
MqttQos
.
atLeastOnce
})
{
...
...
@@ -263,6 +274,64 @@ class LakiMqttClient {
);
}
/// 上行 摄像头宠物异常行为处理结果
///
/// 对应 topic: camera/{SN}/up
/// [status] "handled" = 已处理, "false_alarm" = 误报
int
publishCameraAlarmResult
({
required
String
deviceSn
,
required
int
alarmId
,
required
String
status
,
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
})
{
return
publishJson
(
LakiMqttTopics
.
cameraUp
(
deviceSn
),
{
'eventId'
:
'
${DateTime.now().millisecondsSinceEpoch}
_
${_randomHex(8)}
'
,
'deviceVersionNo'
:
''
,
'deviceType'
:
'ICU'
,
'timestamp'
:
DateTime
.
now
().
millisecondsSinceEpoch
,
'type'
:
'alarm'
,
'data'
:
{
'alarmId'
:
'
$alarmId
'
,
'status'
:
status
,
},
},
qos:
qos
,
);
}
/// 上行 出舱数据
/// create by stephen
/// [deviceSn] 设备 SN,[type] 操作类型(unbindPet),
/// [petData] 宠物信息,为 null 时发送空对象。
// TODO: 服务端尚未确定具体数据结构,等确定后再调整
int
publishPetUnBind
({
required
String
deviceSn
,
required
String
type
,
required
Map
<
String
,
dynamic
>?
petData
,
MqttQos
qos
=
MqttQos
.
atLeastOnce
,
})
{
return
publishJson
(
LakiMqttTopics
.
icuPetBind
(
deviceSn
),
{
'eventId'
:
'
${DateTime.now().millisecondsSinceEpoch}
_
${_randomHex(4)}
'
,
'deviceId'
:
deviceSn
,
'deviceType'
:
'ICU'
,
'timestamp'
:
DateTime
.
now
().
millisecondsSinceEpoch
,
'type'
:
type
,
'data'
:
petData
??
{},
},
qos:
qos
,
);
}
/// 生成指定长度的随机 hex 字符串
String
_randomHex
(
int
length
)
{
final
random
=
Random
();
return
List
.
generate
(
length
,
(
_
)
=>
random
.
nextInt
(
16
).
toRadixString
(
16
)).
join
();
}
Future
<
void
>
disconnect
()
async
{
await
_updatesSub
?.
cancel
();
_updatesSub
=
null
;
...
...
lib/views/monitoring/index/cubit/monitoring_index_cubit.dart
View file @
c701cce4
...
...
@@ -4,6 +4,8 @@ import 'package:flutter/foundation.dart';
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:laki_icu_app/enums/video_stream_mode_enum.dart'
;
import
'package:laki_icu_app/models/bo/cabin_detail_response.dart'
;
import
'package:laki_icu_app/utils/date_utils.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.dart'
;
import
'package:laki_icu_app/models/bo/camera_alarm_bo.dart'
;
import
'package:laki_icu_app/services/cabin_service.dart'
;
...
...
@@ -80,6 +82,10 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_listenMcuReportChanged
();
_listenCareLevelChanged
();
_loadBoundBluetoothDevice
();
// // todo 测试使用 待删除
_fetchCabinDetailAndConnectVideo
(
"CNA07212L"
);
_connectMqttForDeviceSn
(
"CNA07212L"
);
}
/// 监听 WebRTC 连接状态变化并同步到 State
...
...
@@ -155,7 +161,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
).
copyWith
(
sn:
reportSn
.
isEmpty
?
null
:
reportSn
);
if
(
bluetoothReadInfo
.
hasSn
)
{
await
_storageService
.
saveBluetoothReadSn
(
bluetoothReadInfo
.
sn
!);
unawaited
(
_fetchCabinDetailAndConnectVideo
(
bluetoothReadInfo
.
sn
!));
await
_connectMqttForDeviceSn
(
bluetoothReadInfo
.
sn
!);
// stephen 需要保留,选择蓝牙之后获取到sn就需要连接上mqtt
await
_fetchCabinDetailAndConnectVideo
(
bluetoothReadInfo
.
sn
!);
}
eventBus
.
emit
(
BluetoothReadInfoChangedEvent
(
bluetoothReadInfo
),
...
...
@@ -163,13 +171,14 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_publishMqttMonitorReport
(
latestReport
);
if
(
isClosed
)
return
;
}
final
patientInfo
=
latestReport
==
null
?
state
.
patientInfo
:
_
patientInfoWithCareLevel
(
latestRepor
t
);
final
careLevel
=
latestReport
==
null
?
null
:
_
careLevelTextFromReportValue
(
latestReport
.
levelLigh
t
);
emit
(
state
.
copyWith
(
latestBluetoothRawHex:
packet
.
rawHex
,
latestMcuReport:
latestReport
,
patientInfo:
patientInfo
,
patientInfo:
_patientInfoWithCareLevelText
(
careLevel
),
currentCareLevel:
careLevel
,
metrics:
latestReport
==
null
?
state
.
metrics
:
_metricsFromBluetoothReport
(
latestReport
),
...
...
@@ -194,10 +203,12 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
'🔥🔥🔥 BLE_READ_DEBUG index_metrics_from_report '
'
${_metricsLogText(metrics)}
'
,
);
final
careLevel
=
_careLevelTextFromReportValue
(
event
.
report
.
levelLight
);
emit
(
state
.
copyWith
(
latestMcuReport:
event
.
report
,
metrics:
metrics
,
patientInfo:
_patientInfoWithCareLevel
(
event
.
report
),
patientInfo:
_patientInfoWithCareLevelText
(
careLevel
),
currentCareLevel:
careLevel
,
));
});
}
...
...
@@ -206,12 +217,15 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_careLevelChangedSub
=
eventBus
.
on
<
CareLevelChangedEvent
>().
listen
((
event
)
{
if
(
isClosed
)
return
;
_markCareLevelPending
(
event
.
protocolValue
);
final
careLevel
=
_careLevelTextFromProtocolValue
(
event
.
protocolValue
);
final
patientInfo
=
_patientInfoWithCareLevelValue
(
event
.
protocolValue
,
fromReport:
false
,
);
if
(
patientInfo
==
null
)
return
;
emit
(
state
.
copyWith
(
patientInfo:
patientInfo
));
emit
(
state
.
copyWith
(
patientInfo:
patientInfo
,
currentCareLevel:
careLevel
,
));
});
}
...
...
@@ -289,8 +303,9 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
/// 页面初始化入口。
///
/// 数据不再通过 HTTP 获取:
/// - 宠物信息由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{sn}/to 推送获取
/// - 宠物信息由 MQTT icu/{SN}/pet/bind 推送获取
/// - 摄像头凭证由蓝牙 SN → /icu/detail 接口([_fetchCabinDetailAndConnectVideo])获取
/// - 告警信息由 MQTT camera/{SN}/to 推送获取
/// - 指标数据由蓝牙 MCU 上报实时填充
Future
<
void
>
loadData
()
async
{}
...
...
@@ -369,14 +384,16 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
try
{
final
detail
=
await
_cabinService
.
getCabinDetail
(
carbinSn
);
if
(
isClosed
)
return
;
final
careLevel
=
_careLevelFromCabinDetail
(
detail
);
emit
(
state
.
copyWith
(
cabinCameraSn:
detail
.
cameraSn
,
cabinWifiPwd:
detail
.
wifiPwd
,
currentCareLevel:
careLevel
,
patientInfo:
detail
.
petInfo
!=
null
?
_mapPetInfoToPatientInfo
(
detail
.
petInfo
!,
careLevel:
_careLevelFromCabinDetail
(
detail
)
,
careLevel:
careLevel
,
)
:
state
.
patientInfo
,
));
...
...
@@ -417,10 +434,6 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
return
_careLevelTextFromProtocolValue
(
detail
.
monitoringData
?.
levelLight
);
}
PatientInfoBO
?
_patientInfoWithCareLevel
(
McuReport
report
)
{
return
_patientInfoWithCareLevelValue
(
report
.
levelLight
);
}
PatientInfoBO
?
_patientInfoWithCareLevelValue
(
int
value
,
{
bool
fromReport
=
true
,
...
...
@@ -439,6 +452,21 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
return
current
.
copyWith
(
careLevel:
careLevel
);
}
PatientInfoBO
?
_patientInfoWithCareLevelText
(
String
?
careLevel
)
{
final
current
=
state
.
patientInfo
;
if
(
current
==
null
)
return
null
;
if
(
careLevel
==
null
||
careLevel
==
current
.
careLevel
)
{
return
current
;
}
return
current
.
copyWith
(
careLevel:
careLevel
);
}
String
?
_careLevelTextFromReportValue
(
int
value
)
{
final
resolvedValue
=
_resolveCareLevelReportValue
(
value
);
if
(
resolvedValue
==
null
)
return
null
;
return
_careLevelTextFromProtocolValue
(
resolvedValue
);
}
int
?
_resolveCareLevelReportValue
(
int
value
)
{
final
pendingValue
=
_pendingCareLevelProtocolValue
;
final
pendingSince
=
_pendingCareLevelSince
;
...
...
@@ -812,6 +840,11 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_handleCameraAlarmMessage
(
json
);
return
;
}
if
(
json
!=
null
&&
message
.
topic
==
LakiMqttTopics
.
icuPetBind
(
_mqttDeviceSn
??
''
))
{
_handlePetBindMessage
(
json
);
return
;
}
emit
(
state
.
copyWith
(
mqttMessage:
'MQTT 收到
${message.topic}
:
${message.payload}
'
,
));
...
...
@@ -823,10 +856,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
});
}
/// 处理
摄像头告警消息(与 Android qisuanfa_0621 逻辑保持一致)
/// 处理
MQTT camera/{SN}/to 摄像头告警消息
///
/// 仅处理 type == "abnormal_behavior" 的消息,
/// 按 behaviorCode 映射标题:2→异常液体、3→检测到排泄物、其他→未知异常
/// 服务端推送格式:
/// ```json
/// {
/// "type": "abnormal_behavior",
/// "behaviorCode": 1,
/// "behaviorDesc": "宠物抓挠",
/// "petName": "旺财",
/// ...
/// }
/// ```
void
_handleCameraAlarmMessage
(
Map
<
String
,
dynamic
>
json
)
{
if
(
json
[
'type'
]
!=
'abnormal_behavior'
)
return
;
...
...
@@ -837,13 +878,18 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
_seenAlarmEventIds
.
add
(
alarm
.
eventId
);
final
timeStr
=
_formatTimestamp
(
alarm
.
timestamp
);
final
title
=
alarm
.
titleByBehaviorCode
;
final
behaviorTitle
=
alarm
.
titleByBehaviorCode
;
final
petName
=
alarm
.
petName
;
final
title
=
petName
!=
null
&&
petName
.
isNotEmpty
?
'
$petName
-
$behaviorTitle
'
:
behaviorTitle
;
final
alert
=
AlertInfoBO
(
title:
title
,
description:
t
itle
,
description:
alarm
.
behaviorDesc
??
behaviorT
itle
,
time:
timeStr
,
status:
'待处理'
,
alarmId:
alarm
.
alarmId
,
);
emit
(
state
.
copyWith
(
...
...
@@ -851,6 +897,47 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
));
}
/// 处理 MQTT pet/bind 宠物绑定消息
///
/// 服务端推送的宠物绑定数据格式:
/// ```json
/// {
/// "type": "bindPet",
/// "data": { "name": "旺财", "type": "dog", "breedTag": "金毛", ... }
/// }
/// ```
/// [type] 为 `bindPet` 时设置 petInfo,为 `unbindPet` 时清空 petInfo
void
_handlePetBindMessage
(
Map
<
String
,
dynamic
>
json
)
{
final
type
=
json
[
'type'
]
as
String
?;
if
(
type
==
null
)
return
;
if
(
type
==
'unbindPet'
)
{
// 出舱:清空宠物信息
emit
(
state
.
copyWith
(
clearPatientInfo:
true
));
return
;
}
if
(
type
!=
'bindPet'
)
return
;
final
data
=
json
[
'data'
];
if
(
data
is
!
Map
<
String
,
dynamic
>)
return
;
final
patientInfo
=
PatientInfoBO
(
name:
(
data
[
'name'
]
as
String
?)
??
''
,
type:
(
data
[
'type'
]
as
String
?)
??
''
,
phone:
(
data
[
'ownerPhone'
]
as
String
?)
??
''
,
breed:
(
data
[
'breedTag'
]
as
String
?)
??
''
,
doctor:
'缺失字段'
,
avatarUrl:
(
data
[
'avatar'
]
as
String
?)
??
''
,
disease:
(
data
[
'disease'
]
as
String
?)
??
''
,
checkInDate:
formatIsoTime
(
data
[
'createTime'
]
as
String
?,
fallback:
'缺失字段'
),
careLevel:
'缺失字段'
,
);
emit
(
state
.
copyWith
(
patientInfo:
patientInfo
));
}
/// 毫秒时间戳 → HH:mm:ss 格式(与 Android 一致)
String
_formatTimestamp
(
int
milliseconds
)
{
final
dateTime
=
...
...
@@ -1073,26 +1160,65 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
/// 清空宠物信息(出舱)
///
/// 通过 MQTT 上行出舱数据后清空 state 中的宠物信息。
void
clearPatientInfo
()
{
if
(
isClosed
)
return
;
final
deviceSn
=
state
.
mqttDeviceSn
;
final
client
=
_mqttClient
;
debugPrint
(
'[出舱] deviceSn=
$deviceSn
mqttClient=
${client != null}
isConnected=
${client?.isConnected ?? false}
patientInfo=
${state.patientInfo?.name}
'
);
if
(
deviceSn
!=
null
&&
deviceSn
.
isNotEmpty
&&
client
!=
null
&&
client
.
isConnected
)
{
try
{
final
msgId
=
client
.
publishPetUnBind
(
deviceSn:
deviceSn
,
type:
'unbindPet'
,
// TODO: 出舱 type 后续等服务端确定后再调整
petData:
_patientInfoToMap
(),
);
debugPrint
(
'[出舱] MQTT publishPetUnBind 成功 msgId=
$msgId
topic=icu/
$deviceSn
/pet/bind'
);
}
catch
(
e
)
{
debugPrint
(
'[出舱] MQTT publishPetUnBind 失败:
$e
'
);
if
(!
isClosed
)
{
emit
(
state
.
copyWith
(
mqttMessage:
'MQTT 出舱上报失败:
$e
'
));
}
}
}
else
{
debugPrint
(
'[出舱] 跳过 MQTT 上行: '
'deviceSn=
${deviceSn ?? "null"}
'
'hasClient=
${client != null}
'
'isConnected=
${client?.isConnected ?? false}
'
);
}
emit
(
state
.
copyWith
(
clearPatientInfo:
true
));
}
/// 将当前宠物信息转为 Map,供 MQTT 上行使用
Map
<
String
,
dynamic
>?
_patientInfoToMap
()
{
final
info
=
state
.
patientInfo
;
if
(
info
==
null
)
return
null
;
return
{
'name'
:
info
.
name
,
'petType'
:
info
.
type
,
'phone'
:
info
.
phone
,
'breed'
:
info
.
breed
,
'doctor'
:
info
.
doctor
,
'disease'
:
info
.
disease
,
'checkInDate'
:
info
.
checkInDate
,
'careLevel'
:
info
.
careLevel
,
};
}
/// 设置测试宠物信息(绑定宠物)
void
setTestPatientInfo
()
{
if
(
isClosed
)
return
;
const
testPatientInfo
=
PatientInfoBO
(
name:
'妮蔻'
,
type:
'猫'
,
phone:
'130-1111-3333'
,
breed:
'长毛三花'
,
doctor:
'张医生'
,
disease:
'胃炎'
,
checkInDate:
'2025-6-15'
,
careLevel:
'特级'
,
avatarUrl:
''
,
);
emit
(
state
.
copyWith
(
patientInfo:
testPatientInfo
));
// todo 平板端暂时不做宠物绑定 stephen
}
/// 获取 WebRTC 渲染器供 UI 层使用
...
...
@@ -1114,37 +1240,57 @@ class MonitoringIndexCubit extends Cubit<MonitoringIndexState> {
}
/// 标记告警为已处理
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void
markAlertAsProcessed
(
AlertInfoBO
alert
)
{
if
(
isClosed
)
return
;
final
updatedAlerts
=
state
.
alerts
.
map
((
item
)
{
if
(
item
.
title
==
alert
.
title
&&
item
.
time
==
alert
.
time
)
{
return
AlertInfoBO
(
title:
item
.
title
,
description:
item
.
description
,
time:
item
.
time
,
status:
'已处理'
,
);
}
return
item
;
}).
toList
();
emit
(
state
.
copyWith
(
alerts:
updatedAlerts
,
selectedAlert:
null
));
_publishCameraAlarmResult
(
alert
,
'handled'
);
final
removedCameraAlerts
=
state
.
cameraAlerts
.
where
((
item
)
=>
item
!=
alert
).
toList
();
final
removedAlerts
=
state
.
alerts
.
where
((
item
)
=>
item
!=
alert
).
toList
();
emit
(
state
.
copyWith
(
cameraAlerts:
removedCameraAlerts
,
alerts:
removedAlerts
,
selectedAlert:
null
,
));
}
/// 标记告警为误报
///
/// 通过 MQTT camera/{SN}/up 上行处理结果,并从列表中移除。
void
markAlertAsFalse
(
AlertInfoBO
alert
)
{
if
(
isClosed
)
return
;
final
updatedAlerts
=
state
.
alerts
.
map
((
item
)
{
if
(
item
.
title
==
alert
.
title
&&
item
.
time
==
alert
.
time
)
{
return
AlertInfoBO
(
title:
item
.
title
,
description:
item
.
description
,
time:
item
.
time
,
status:
'误报'
,
);
}
return
item
;
}).
toList
();
emit
(
state
.
copyWith
(
alerts:
updatedAlerts
,
selectedAlert:
null
));
_publishCameraAlarmResult
(
alert
,
'false_alarm'
);
final
removedCameraAlerts
=
state
.
cameraAlerts
.
where
((
item
)
=>
item
!=
alert
).
toList
();
final
removedAlerts
=
state
.
alerts
.
where
((
item
)
=>
item
!=
alert
).
toList
();
emit
(
state
.
copyWith
(
cameraAlerts:
removedCameraAlerts
,
alerts:
removedAlerts
,
selectedAlert:
null
,
));
}
/// 上行 MQTT camera/{SN}/up 告警处理结果
void
_publishCameraAlarmResult
(
AlertInfoBO
alert
,
String
status
)
{
final
client
=
_mqttClient
;
final
deviceSn
=
_mqttDeviceSn
;
if
(
client
==
null
||
!
client
.
isConnected
||
deviceSn
==
null
||
deviceSn
.
isEmpty
)
return
;
if
(
alert
.
alarmId
==
null
)
return
;
try
{
client
.
publishCameraAlarmResult
(
deviceSn:
deviceSn
,
alarmId:
alert
.
alarmId
!,
status:
status
,
);
debugPrint
(
'[告警处理] MQTT 上行成功 alarmId=
${alert.alarmId}
status=
$status
'
);
}
catch
(
e
)
{
debugPrint
(
'[告警处理] MQTT 上行失败:
$e
'
);
}
}
@override
...
...
lib/views/monitoring/index/widgets/monitoring_pet_info_card.dart
View file @
c701cce4
import
'package:flutter/material.dart'
;
import
'package:flutter_screenutil/flutter_screenutil.dart'
;
import
'package:laki_icu_app/models/bo/monitoring_bo.dart'
;
class
MonitoringPetInfoCard
extends
StatelessWidget
{
...
...
@@ -287,10 +288,16 @@ class MonitoringPetInfoCard extends StatelessWidget {
);
}
String
_truncate
(
String
text
,
{
int
maxChars
=
10
})
{
if
(
text
.
length
<=
maxChars
)
return
text
;
return
'
${text.substring(0, maxChars)}
...'
;
}
Widget
_buildInfoBlock
(
PatientInfoBO
info
)
{
return
Column
(
children:
[
_buildInfoItem
(
Icons
.
description_outlined
,
'宠物病因'
,
info
.
disease
),
_buildInfoItem
(
Icons
.
description_outlined
,
'宠物病因'
,
_truncate
(
info
.
disease
)),
SizedBox
(
height:
20
.
h
),
_buildInfoItem
(
Icons
.
home_outlined
,
'入住时间'
,
info
.
checkInDate
),
SizedBox
(
height:
20
.
h
),
...
...
lib/views/settings/factory_settings/cubit/factory_settings_index_cubit.dart
View file @
c701cce4
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart'
;
import
'factory_settings_index_state.dart'
;
class
FactorySettingsIndexCubit
extends
Cubit
<
FactorySettingsIndexState
>
{
FactorySettingsIndexCubit
()
:
super
(
const
FactorySettingsIndexState
());
FactorySettingsIndexCubit
({
required
DeviceThresholdConfigBloc
thresholdConfigBloc
,
})
:
_thresholdConfigBloc
=
thresholdConfigBloc
,
super
(
FactorySettingsIndexState
(
editingConfig:
thresholdConfigBloc
.
state
.
config
,
));
final
DeviceThresholdConfigBloc
_thresholdConfigBloc
;
void
editTempMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
tempMax:
value
),
isDirty:
true
,
));
}
void
editTempMin
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
tempMin:
value
),
isDirty:
true
,
));
}
void
editOxygenMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
oxygenMax:
value
),
isDirty:
true
,
));
}
void
editOxygenMin
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
oxygenMin:
value
),
isDirty:
true
,
));
}
void
editHumidityMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
humidityMax:
value
),
isDirty:
true
,
));
}
void
editHumidityMin
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
humidityMin:
value
),
isDirty:
true
,
));
}
void
editUvTimeMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
uvTimeMax:
value
),
isDirty:
true
,
));
}
void
editIrTimeMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
irTimeMax:
value
),
isDirty:
true
,
));
}
void
editAtomizeTimeMax
(
int
value
)
{
emit
(
state
.
copyWith
(
editingConfig:
state
.
editingConfig
.
copyWith
(
atomizeTimeMax:
value
),
isDirty:
true
,
));
}
void
applyChanges
()
{
_thresholdConfigBloc
.
updateConfig
(
state
.
editingConfig
);
emit
(
state
.
copyWith
(
isDirty:
false
));
}
void
resetEdits
()
{
emit
(
state
.
copyWith
(
editingConfig:
_thresholdConfigBloc
.
state
.
config
,
isDirty:
false
,
));
}
}
lib/views/settings/factory_settings/cubit/factory_settings_index_state.dart
View file @
c701cce4
import
'package:equatable/equatable.dart'
;
import
'package:laki_icu_app/models/bo/device_threshold_config_bo.dart'
;
class
FactorySettingItem
extends
Equatable
{
const
FactorySettingItem
({
...
...
@@ -29,47 +30,63 @@ class FactorySettingGroup extends Equatable {
class
FactorySettingsIndexState
extends
Equatable
{
const
FactorySettingsIndexState
({
this
.
warningText
=
'非专业人员请勿更改设备各项参数!'
,
this
.
leftGroups
=
const
[
FactorySettingGroup
(
title:
'温度范围设定(℃)'
,
items:
[
FactorySettingItem
(
label:
'温度设定上限:'
,
value:
'34'
),
FactorySettingItem
(
label:
'温度设定下限:'
,
value:
'17'
),
],
),
FactorySettingGroup
(
title:
'氧浓度范围设定(%)'
,
items:
[
FactorySettingItem
(
label:
'氧浓度设定上限:'
,
value:
'34'
),
FactorySettingItem
(
label:
'氧浓度设定下限:'
,
value:
'17'
),
],
),
FactorySettingGroup
(
title:
'湿度范围设定(%)'
,
items:
[
FactorySettingItem
(
label:
'湿度设定上限:'
,
value:
'34'
),
FactorySettingItem
(
label:
'湿度设定下限:'
,
value:
'17'
),
],
),
],
this
.
timeLimitGroup
=
const
FactorySettingGroup
(
title:
'时间上限设定(分钟)'
,
items:
[
FactorySettingItem
(
label:
'UV时间设定上限:'
,
value:
'34'
),
FactorySettingItem
(
label:
'红外时间设定上限:'
,
value:
'17'
),
FactorySettingItem
(
label:
'雾化时间设定上限:'
,
value:
'17'
),
],
),
this
.
editingConfig
=
DeviceThresholdConfigBO
.
defaultConfig
,
this
.
isDirty
=
false
,
});
final
String
warningText
;
final
List
<
FactorySettingGroup
>
leftGroups
;
final
FactorySettingGroup
timeLimitGroup
;
final
DeviceThresholdConfigBO
editingConfig
;
final
bool
isDirty
;
List
<
FactorySettingGroup
>
get
leftGroups
=>
[
FactorySettingGroup
(
title:
'温度范围设定(℃)'
,
items:
[
FactorySettingItem
(
label:
'温度设定上限:'
,
value:
'
${editingConfig.tempMax}
'
),
FactorySettingItem
(
label:
'温度设定下限:'
,
value:
'
${editingConfig.tempMin}
'
),
],
),
FactorySettingGroup
(
title:
'氧浓度范围设定(%)'
,
items:
[
FactorySettingItem
(
label:
'氧浓度设定上限:'
,
value:
'
${editingConfig.oxygenMax}
'
),
FactorySettingItem
(
label:
'氧浓度设定下限:'
,
value:
'
${editingConfig.oxygenMin}
'
),
],
),
FactorySettingGroup
(
title:
'湿度范围设定(%)'
,
items:
[
FactorySettingItem
(
label:
'湿度设定上限:'
,
value:
'
${editingConfig.humidityMax}
'
),
FactorySettingItem
(
label:
'湿度设定下限:'
,
value:
'
${editingConfig.humidityMin}
'
),
],
),
];
FactorySettingGroup
get
timeLimitGroup
=>
FactorySettingGroup
(
title:
'时间上限设定(分钟)'
,
items:
[
FactorySettingItem
(
label:
'UV时间设定上限:'
,
value:
'
${editingConfig.uvTimeMax}
'
),
FactorySettingItem
(
label:
'红外时间设定上限:'
,
value:
'
${editingConfig.irTimeMax}
'
),
FactorySettingItem
(
label:
'雾化时间设定上限:'
,
value:
'
${editingConfig.atomizeTimeMax}
'
),
],
);
FactorySettingsIndexState
copyWith
({
String
?
warningText
,
DeviceThresholdConfigBO
?
editingConfig
,
bool
?
isDirty
,
})
{
return
FactorySettingsIndexState
(
warningText:
warningText
??
this
.
warningText
,
editingConfig:
editingConfig
??
this
.
editingConfig
,
isDirty:
isDirty
??
this
.
isDirty
,
);
}
@override
List
<
Object
?>
get
props
=>
[
warningText
,
leftGroups
,
timeLimitGroup
,
editingConfig
,
isDirty
,
];
}
lib/views/settings/factory_settings/factory_settings_index_view.dart
View file @
c701cce4
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:laki_icu_app/blocs/device_threshold_config/device_threshold_config.dart'
;
import
'cubit/factory_settings_index_cubit.dart'
;
import
'widgets/factory_settings_panel.dart'
;
...
...
@@ -10,7 +11,9 @@ class FactorySettingsIndexView extends StatelessWidget {
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
(
create:
(
_
)
=>
FactorySettingsIndexCubit
(),
create:
(
_
)
=>
FactorySettingsIndexCubit
(
thresholdConfigBloc:
context
.
read
<
DeviceThresholdConfigBloc
>(),
),
child:
const
FactorySettingsPanel
(),
);
}
...
...
lib/views/settings/factory_settings/widgets/factory_settings_panel.dart
View file @
c701cce4
...
...
@@ -38,6 +38,7 @@ class FactorySettingsPanel extends StatelessWidget {
Expanded
(
child:
_FactoryActionColumn
(
group:
state
.
timeLimitGroup
,
isDirty:
state
.
isDirty
,
),
),
],
...
...
@@ -107,12 +108,17 @@ class _FactorySettingsColumn extends StatelessWidget {
}
class
_FactoryActionColumn
extends
StatelessWidget
{
const
_FactoryActionColumn
({
required
this
.
group
});
const
_FactoryActionColumn
({
required
this
.
group
,
required
this
.
isDirty
,
});
final
FactorySettingGroup
group
;
final
bool
isDirty
;
@override
Widget
build
(
BuildContext
context
)
{
final
cubit
=
context
.
read
<
FactorySettingsIndexCubit
>();
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
...
...
@@ -130,9 +136,19 @@ class _FactoryActionColumn extends StatelessWidget {
SizedBox
(
height:
64
.
h
),
Row
(
children:
[
const
Expanded
(
child:
_FactoryButton
(
text:
'撤销'
)),
Expanded
(
child:
_FactoryButton
(
text:
'撤销'
,
onPressed:
isDirty
?
()
=>
cubit
.
resetEdits
()
:
null
,
),
),
SizedBox
(
width:
34
.
w
),
const
Expanded
(
child:
_FactoryButton
(
text:
'应用更改'
)),
Expanded
(
child:
_FactoryButton
(
text:
'应用更改'
,
onPressed:
isDirty
?
()
=>
cubit
.
applyChanges
()
:
null
,
),
),
],
),
],
...
...
@@ -175,6 +191,41 @@ class _FactorySettingRow extends StatelessWidget {
final
FactorySettingItem
item
;
void
_onTap
(
BuildContext
context
)
{
final
cubit
=
context
.
read
<
FactorySettingsIndexCubit
>();
final
currentValue
=
int
.
tryParse
(
item
.
value
)
??
0
;
showDialog
<
int
>(
context:
context
,
builder:
(
context
)
=>
_EditDialog
(
label:
item
.
label
,
initialValue:
currentValue
,
),
).
then
((
value
)
{
if
(
value
!=
null
)
{
if
(
item
.
label
==
'温度设定上限:'
)
{
cubit
.
editTempMax
(
value
);
}
else
if
(
item
.
label
==
'温度设定下限:'
)
{
cubit
.
editTempMin
(
value
);
}
else
if
(
item
.
label
==
'氧浓度设定上限:'
)
{
cubit
.
editOxygenMax
(
value
);
}
else
if
(
item
.
label
==
'氧浓度设定下限:'
)
{
cubit
.
editOxygenMin
(
value
);
}
else
if
(
item
.
label
==
'湿度设定上限:'
)
{
cubit
.
editHumidityMax
(
value
);
}
else
if
(
item
.
label
==
'湿度设定下限:'
)
{
cubit
.
editHumidityMin
(
value
);
}
else
if
(
item
.
label
==
'UV时间设定上限:'
)
{
cubit
.
editUvTimeMax
(
value
);
}
else
if
(
item
.
label
==
'红外时间设定上限:'
)
{
cubit
.
editIrTimeMax
(
value
);
}
else
if
(
item
.
label
==
'雾化时间设定上限:'
)
{
cubit
.
editAtomizeTimeMax
(
value
);
}
}
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
Row
(
...
...
@@ -192,7 +243,10 @@ class _FactorySettingRow extends StatelessWidget {
),
),
SizedBox
(
width:
16
.
w
),
_ValueBox
(
value:
item
.
value
),
GestureDetector
(
onTap:
()
=>
_onTap
(
context
),
child:
_ValueBox
(
value:
item
.
value
),
),
],
);
}
...
...
@@ -242,49 +296,118 @@ class _WideFactoryButton extends StatelessWidget {
}
class
_FactoryButton
extends
StatelessWidget
{
const
_FactoryButton
({
required
this
.
text
});
const
_FactoryButton
({
required
this
.
text
,
this
.
onPressed
,
});
final
String
text
;
final
VoidCallback
?
onPressed
;
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
height:
70
.
h
,
padding:
EdgeInsets
.
symmetric
(
horizontal:
28
.
w
),
decoration:
settingsButtonDecoration
(
active:
true
),
child:
Row
(
children:
[
Text
(
'《《'
,
style:
TextStyle
(
color:
const
Color
(
0xFF67C9F5
).
withValues
(
alpha:
0.72
),
fontSize:
25
.
sp
,
fontWeight:
FontWeight
.
w700
,
),
),
Expanded
(
child:
Text
(
text
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
textAlign:
TextAlign
.
center
,
final
isEnabled
=
onPressed
!=
null
;
return
GestureDetector
(
onTap:
onPressed
,
child:
Container
(
height:
70
.
h
,
padding:
EdgeInsets
.
symmetric
(
horizontal:
28
.
w
),
decoration:
settingsButtonDecoration
(
active:
isEnabled
),
child:
Row
(
children:
[
Text
(
'《'
,
style:
TextStyle
(
color:
Colors
.
white
,
color:
const
Color
(
0xFF67C9F5
).
withValues
(
alpha:
isEnabled
?
0.72
:
0.3
)
,
fontSize:
25
.
sp
,
fontWeight:
FontWeight
.
w
8
00
,
fontWeight:
FontWeight
.
w
7
00
,
),
),
),
Text
(
'》》'
,
style:
TextStyle
(
color:
const
Color
(
0xFF67C9F5
).
withValues
(
alpha:
0.72
),
fontSize:
25
.
sp
,
fontWeight:
FontWeight
.
w700
,
Expanded
(
child:
Text
(
text
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
textAlign:
TextAlign
.
center
,
style:
TextStyle
(
color:
isEnabled
?
Colors
.
white
:
Colors
.
white
.
withValues
(
alpha:
0.5
),
fontSize:
25
.
sp
,
fontWeight:
FontWeight
.
w800
,
),
),
),
),
],
Text
(
'》'
,
style:
TextStyle
(
color:
const
Color
(
0xFF67C9F5
).
withValues
(
alpha:
isEnabled
?
0.72
:
0.3
),
fontSize:
25
.
sp
,
fontWeight:
FontWeight
.
w700
,
),
),
],
),
),
);
}
}
class
_EditDialog
extends
StatefulWidget
{
const
_EditDialog
({
required
this
.
label
,
required
this
.
initialValue
,
});
final
String
label
;
final
int
initialValue
;
@override
State
<
_EditDialog
>
createState
()
=>
_EditDialogState
();
}
class
_EditDialogState
extends
State
<
_EditDialog
>
{
late
final
TextEditingController
_controller
;
@override
void
initState
()
{
super
.
initState
();
_controller
=
TextEditingController
(
text:
'
${widget.initialValue}
'
);
}
@override
void
dispose
()
{
_controller
.
dispose
();
super
.
dispose
();
}
void
_onSave
()
{
final
value
=
int
.
tryParse
(
_controller
.
text
);
if
(
value
!=
null
)
{
Navigator
.
of
(
context
).
pop
(
value
);
}
}
@override
Widget
build
(
BuildContext
context
)
{
return
AlertDialog
(
title:
Text
(
widget
.
label
),
content:
TextField
(
controller:
_controller
,
keyboardType:
TextInputType
.
number
,
decoration:
const
InputDecoration
(
hintText:
'请输入数值'
,
),
autofocus:
true
,
),
actions:
[
TextButton
(
onPressed:
()
=>
Navigator
.
of
(
context
).
pop
(),
child:
const
Text
(
'取消'
),
),
TextButton
(
onPressed:
_onSave
,
child:
const
Text
(
'确定'
),
),
],
);
}
}
pubspec.yaml
View file @
c701cce4
...
...
@@ -61,6 +61,7 @@ dependencies:
mqtt_client
:
^10.5.1
marionette_flutter
:
^0.5.0
hydrated_bloc
:
^9.1.5
intl
:
^0.20.2
dev_dependencies
:
flutter_test
:
sdk
:
flutter
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment