Commit 65c9e3b0 authored by akari's avatar akari
parents 45154e91 83539172
{
"APP_ENV": "dev",
"SERVICE_URL": "http://121.41.173.189/laki-merchant-service"
"SERVICE_URL": "http://192.168.112.129:8089"
}
This diff is collapsed.
......@@ -38,9 +38,34 @@ class BluetoothReadBloc extends Bloc<BluetoothReadEvent, BluetoothReadState> {
) async {
emit(state.copyWith(isLoading: true, clearError: true));
try {
final info = await _storageService.getBluetoothReadInfo();
Log.warn('**************sn************: ${info.sn}');
emit(state.copyWith(info: info, isLoading: false, clearError: true));
BluetoothReadModel? info;
const maxRetries = 5;
const retryInterval = Duration(seconds: 10);
for (int i = 0; i < maxRetries; i++) {
try {
info = await _storageService.getBluetoothReadInfo();
Log.warn(
'**************sn 第${i + 1}次获取************: ${info.sn}');
if (info.hasSn) {
Log.warn('**************sn 获取成功,停止重试************');
break;
}
} catch (e) {
Log.warn('**************sn 第${i + 1}次获取失败: $e************');
}
// 如果还没获取到sn且不是最后一次,则等待后重试
if (i < maxRetries - 1 && (info == null || !info.hasSn)) {
await Future.delayed(retryInterval);
}
}
emit(state.copyWith(
info: info ?? BluetoothReadModel.empty,
isLoading: false,
clearError: true,
));
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}
......
This diff is collapsed.
/// MQTT 摄像头告警消息数据模型
/// 对应 topic: camera/{cabinSn}/to 中 type="abnormal_behavior" 的消息
///
/// 与 Android 项目 qisuanfa_0621 MqttManager.processControlCommand() 保持一致:
/// - 只提取 behaviorCode 和 timestamp 两个字段用于标题映射
/// - 其余字段保留解析,方便后续扩展
class CameraAlarmBO {
/// 事件唯一标识(UUID 去横线),用于去重
final String eventId;
/// 仓 SN
final String deviceId;
/// 设备类型,固定 "camera"
final String deviceType;
/// 毫秒时间戳
final int timestamp;
/// 消息类型,如 "abnormal_behavior"
final String type;
/// 告警 ID
final int alarmId;
/// 告警类别
final String alarmType;
/// 行为编码
final int? behaviorCode;
/// 行为描述
final String? behaviorDesc;
/// 宠物名称
final String? petName;
const CameraAlarmBO({
required this.eventId,
required this.deviceId,
required this.deviceType,
required this.timestamp,
required this.type,
required this.alarmId,
required this.alarmType,
this.behaviorCode,
this.behaviorDesc,
this.petName,
});
factory CameraAlarmBO.fromJson(Map<String, dynamic> json) {
return CameraAlarmBO(
eventId: json['eventId'] as String? ?? '',
deviceId: json['deviceId'] as String? ?? '',
deviceType: json['deviceType'] as String? ?? '',
timestamp: json['timestamp'] as int? ?? 0,
type: json['type'] as String? ?? '',
alarmId: json['alarmId'] as int? ?? 0,
alarmType: json['alarmType'] as String? ?? '',
behaviorCode: json['behaviorCode'] as int?,
behaviorDesc: json['behaviorDesc'] as String?,
petName: json['petName'] as String?,
);
}
/// 与 Android 行为编码映射完全一致:
/// 2 → 异常液体
/// 3 → 检测到排泄物
/// 其他 → 未知异常
String get titleByBehaviorCode {
if (behaviorCode == 2) return '异常液体';
if (behaviorCode == 3) return '检测到排泄物';
return '未知异常';
}
}
import 'mock/cabin_mock_data.dart';
import 'package:laki_icu_app/models/bo/cabin_detail_response.dart';
import 'package:laki_icu_app/utils/constants.dart';
import 'package:laki_icu_app/utils/http/dio_request.dart';
import 'package:laki_icu_app/utils/http/response_model.dart';
class CabinRepository {
CabinRepository._();
static final CabinRepository instance = CabinRepository._();
/// 获取舱详情
///
/// [carbinSn] 舱序列号(设备 SN)
Future<ResponseModel<CabinDetailResponse>> getCabinDetail(
String carbinSn,
) {
return DioRequest.instance.get<CabinDetailResponse>(
'/icu/detail',
queryParameters: {'carbinSn': carbinSn},
fromJsonT: (data) =>
CabinDetailResponse.fromJson(data as Map<String, dynamic>),
);
}
Future<ResponseModel<CabinDetailResponse>> _mockCabinDetail() async {
await Future.delayed(const Duration(milliseconds: 300));
return ResponseModel<CabinDetailResponse>(
code: 200,
msg: '获取舱详情成功(mock)',
data: CabinMockData.getMockCabinDetail(),
uuid: 'mock_uuid_${DateTime.now().millisecondsSinceEpoch}',
success: true,
timestamp: DateTime.now().millisecondsSinceEpoch,
);
}
}
import '../../models/bo/cabin_detail_response.dart';
/// 舱详情模块本地 mock 数据。
///
/// 所有 mock 数据直接使用 models 层构造函数创建,
/// 确保数据结构与 models 定义完全一致。
/// 后续接入真实接口时,只需修改 Repository 层开关即可。
class CabinMockData {
CabinMockData._();
static CabinDetailResponse getMockCabinDetail() {
return CabinDetailResponse(
pageTitle: 'ICU 监护舱',
deviceId: 1001,
deviceName: 'TSAAS ICU 设备',
cabinId: 1,
cabinName: '1号舱',
cabinType: 'ICU',
carbinSn: 'VE10085871QOXG',
cameraSn: 'VE10085871QOXG',
wifiPwd: '143548',
wifiName: 'TSAAS-ICU-WiFi',
petInfo: PetInfoResponse(
petId: 1,
name: '妮蔻',
avatar: '',
type: '猫',
breedTag: '长毛三花',
disease: '胃炎',
ownerName: '张先生',
ownerPhone: '130-1111-3333',
basicInfo: '3岁 雌性 4.5kg',
petName: '妮蔻',
petType: '猫',
monitoringStatus: 'monitoring',
),
cameraInfo: CameraInfoResponse(
cameraId: 1,
cameraName: '主摄像头',
sn: 'VE10085871QOXG',
cameraPwd: '143548',
status: 'online',
streamUrl: '',
),
monitoringData: MonitoringDataResponse(
totalDuration: 7200,
elapsedDuration: 3600,
status: 'monitoring',
careLevel: '特级',
planId: 1,
planName: '标准监护方案',
planType: 'standard',
temperatureSetting: 25,
mainTemperature: 24,
humidity: 55,
humiditySetting: 50,
oxygenConcentration: 21,
oxygenSetting: 20,
co2Measurement: 400,
co2Setting: 500,
),
cabinControls: CabinControlsResponse(
temperature: 25.0,
oxygen: 21.0,
humidity: 55.0,
co2: 400.0,
nebulizer: 0,
sterilize: 0,
fanMode: 'auto',
airMode: 'cycle',
newFan: true,
openOxygen: false,
checkLight: false,
light: true,
blueLight: false,
redLight: false,
),
currentPlanId: 1,
currentPetId: 1,
status: 'occupied',
state: 'normal',
);
}
}
import 'package:laki_icu_app/models/bo/cabin_detail_response.dart';
import 'package:laki_icu_app/repositories/cabin_repository.dart';
class CabinService {
final CabinRepository _repository;
CabinService() : _repository = CabinRepository.instance;
/// 获取舱详情
///
/// [carbinSn] 舱序列号(设备 SN)
/// 返回 [CabinDetailResponse],包含 cameraSn、wifiPwd、petInfo 等
Future<CabinDetailResponse> getCabinDetail(String carbinSn) async {
final result = await _repository.getCabinDetail(carbinSn);
if (result.success && result.data != null) {
return result.data!;
}
throw Exception(result.msg);
}
}
......@@ -71,6 +71,18 @@ class MonitoringIndexState extends Equatable {
/// 当前 MQTT 连接使用的设备 SN
final String? mqttDeviceSn;
/// 设备摄像头序列号(从舱详情接口 /cabin/detail 获取)
final String? cabinCameraSn;
/// 设备 WiFi 密码(从舱详情接口 /cabin/detail 获取)
final String? cabinWifiPwd;
/// MQTT 实时推送的摄像头告警列表(来自 camera/{sn}/to)
final List<AlertInfoBO> cameraAlerts;
/// 当前选中的告警(用于弹窗显示)
final AlertInfoBO? selectedAlert;
const MonitoringIndexState({
this.status = MonitoringIndexStatus.initial,
this.isLoading = false,
......@@ -78,6 +90,7 @@ class MonitoringIndexState extends Equatable {
this.metrics = const [],
this.patientInfo,
this.alerts = const [],
this.cameraAlerts = const [],
this.videoConnectionState = WebrtcConnectionState.disconnected,
this.videoStreamMode = VideoStreamMode.p2p,
this.isSwitchingMode = false,
......@@ -95,6 +108,9 @@ class MonitoringIndexState extends Equatable {
this.mqttConnectionState = LakiMqttConnectionState.disconnected,
this.mqttMessage,
this.mqttDeviceSn,
this.cabinCameraSn,
this.cabinWifiPwd,
this.selectedAlert,
});
MonitoringIndexState copyWith({
......@@ -104,6 +120,7 @@ class MonitoringIndexState extends Equatable {
List<MonitoringMetricBO>? metrics,
PatientInfoBO? patientInfo,
List<AlertInfoBO>? alerts,
List<AlertInfoBO>? cameraAlerts,
WebrtcConnectionState? videoConnectionState,
VideoStreamMode? videoStreamMode,
bool? isSwitchingMode,
......@@ -121,6 +138,9 @@ class MonitoringIndexState extends Equatable {
LakiMqttConnectionState? mqttConnectionState,
String? mqttMessage,
String? mqttDeviceSn,
String? cabinCameraSn,
String? cabinWifiPwd,
AlertInfoBO? selectedAlert,
bool clearBoundBluetoothDevice = false,
bool clearLatestMcuReport = false,
bool clearPatientInfo = false,
......@@ -133,6 +153,7 @@ class MonitoringIndexState extends Equatable {
metrics: metrics ?? this.metrics,
patientInfo: clearPatientInfo ? null : patientInfo ?? this.patientInfo,
alerts: alerts ?? this.alerts,
cameraAlerts: cameraAlerts ?? this.cameraAlerts,
videoConnectionState: videoConnectionState ?? this.videoConnectionState,
videoStreamMode: videoStreamMode ?? this.videoStreamMode,
isSwitchingMode: isSwitchingMode ?? this.isSwitchingMode,
......@@ -159,6 +180,9 @@ class MonitoringIndexState extends Equatable {
mqttMessage: mqttMessage ?? this.mqttMessage,
mqttDeviceSn:
clearMqttDeviceSn ? null : mqttDeviceSn ?? this.mqttDeviceSn,
cabinCameraSn: cabinCameraSn ?? this.cabinCameraSn,
cabinWifiPwd: cabinWifiPwd ?? this.cabinWifiPwd,
selectedAlert: selectedAlert ?? this.selectedAlert,
);
}
......@@ -170,6 +194,7 @@ class MonitoringIndexState extends Equatable {
metrics,
patientInfo,
alerts,
cameraAlerts,
videoConnectionState,
videoStreamMode,
isSwitchingMode,
......@@ -187,5 +212,8 @@ class MonitoringIndexState extends Equatable {
mqttConnectionState,
mqttMessage,
mqttDeviceSn,
cabinCameraSn,
cabinWifiPwd,
selectedAlert,
];
}
......@@ -13,6 +13,7 @@ import 'cubit/monitoring_index_cubit.dart';
import 'cubit/monitoring_index_state.dart';
import 'widgets/bluetooth_bind_dialog.dart';
import 'widgets/monitoring_alert_card.dart';
import 'widgets/monitoring_alert_detail_dialog.dart';
import 'widgets/monitoring_metric_card.dart';
import 'widgets/monitoring_pet_info_card.dart';
import 'widgets/video_player_widget.dart';
......@@ -59,7 +60,13 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
@override
Widget build(BuildContext context) {
return BlocBuilder<MonitoringIndexCubit, MonitoringIndexState>(
return BlocListener<MonitoringIndexCubit, MonitoringIndexState>(
listener: (context, state) {
if (state.selectedAlert != null) {
_showAlertDetailDialog(state.selectedAlert!);
}
},
child: BlocBuilder<MonitoringIndexCubit, MonitoringIndexState>(
builder: (context, state) {
if (state.isLoading && state.metrics.isEmpty) {
return const Center(
......@@ -72,6 +79,7 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
}
return _buildMainArea(state);
},
),
);
}
......@@ -121,7 +129,12 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
children: [
Expanded(
flex: 10,
child: MonitoringAlertCard(alerts: state.alerts),
child: MonitoringAlertCard(
alerts: [
...state.cameraAlerts,
...state.alerts,
],
),
),
SizedBox(width: 10.w),
Expanded(
......@@ -244,6 +257,18 @@ class _MonitoringIndexContentState extends State<MonitoringIndexContent> {
);
}
void _showAlertDetailDialog(AlertInfoBO alert) {
showDialog<void>(
context: context,
builder: (_) => BlocProvider.value(
value: _monitoringIndexCubit,
child: MonitoringAlertDetailDialog(alert: alert),
),
).then((_) {
_monitoringIndexCubit.clearSelectedAlert();
});
}
List<MonitoringMetricBO> _environmentMetrics(
List<MonitoringMetricBO> metrics) {
final matched = metrics
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/views/monitoring/index/cubit/monitoring_index_cubit.dart';
class MonitoringAlertCard extends StatelessWidget {
final List<AlertInfoBO> alerts;
......@@ -73,13 +75,16 @@ class MonitoringAlertCard extends StatelessWidget {
itemBuilder: (context, index) {
final item = alerts[index];
final pending = item.status.contains('待处理');
return _buildAlertItem(item, pending);
return _buildAlertItem(context, item, pending);
},
);
}
Widget _buildAlertItem(AlertInfoBO item, bool pending) {
return IntrinsicHeight(
Widget _buildAlertItem(BuildContext context, AlertInfoBO item, bool pending) {
return InkWell(
onTap: () => context.read<MonitoringIndexCubit>().selectAlert(item),
borderRadius: BorderRadius.circular(16.r),
child: IntrinsicHeight(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
......@@ -121,6 +126,7 @@ class MonitoringAlertCard extends StatelessWidget {
),
],
),
),
);
}
......
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:laki_icu_app/models/bo/monitoring_bo.dart';
import 'package:laki_icu_app/views/monitoring/index/cubit/monitoring_index_cubit.dart';
class MonitoringAlertDetailDialog extends StatelessWidget {
final AlertInfoBO alert;
const MonitoringAlertDetailDialog({
super.key,
required this.alert,
});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: EdgeInsets.zero,
child: _buildDialogContent(context),
);
}
Widget _buildDialogContent(BuildContext context) {
return Container(
width: 1300.w,
height: 800.h,
margin: EdgeInsets.fromLTRB(100.w, 100.h, 100.w, 100.h),
decoration: BoxDecoration(
image: const DecorationImage(
image: AssetImage('lib/assets/monitoring/dialog_alert_bg.png'),
fit: BoxFit.fill,
),
borderRadius: BorderRadius.circular(20.r),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildHeader(),
SizedBox(height: 30.h),
_buildContent(context),
SizedBox(height: 40.h),
_buildBottomButtons(context),
],
),
);
}
Widget _buildHeader() {
return Container(
padding: EdgeInsets.fromLTRB(40.w, 24.h, 40.w, 24.h),
decoration: const BoxDecoration(
border: Border(
bottom: BorderSide(
color: Color(0xFF00D4FF),
width: 2,
),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'告警详情',
style: TextStyle(
color: const Color(0xFF00D4FF),
fontSize: 36.sp,
fontWeight: FontWeight.bold,
),
),
],
),
);
}
Widget _buildContent(BuildContext context) {
return Expanded(
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 40.w),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildImagePlaceholder(),
SizedBox(width: 30.w),
Expanded(
child: _buildAlertInfo(),
),
],
),
),
);
}
Widget _buildImagePlaceholder() {
return Container(
width: 280.w,
height: 200.h,
decoration: BoxDecoration(
color: const Color(0xFF1A2332),
borderRadius: BorderRadius.circular(12.r),
),
);
}
Widget _buildAlertInfo() {
final isPending = alert.status.contains('待处理');
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
alert.title,
style: TextStyle(
color: const Color(0xFFFFC14A),
fontSize: 34.sp,
fontWeight: FontWeight.bold,
),
),
Text(
alert.time,
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
),
),
],
),
SizedBox(height: 20.h),
_buildInfoRow('处理状态', isPending ? '待处理' : '已处理', isPending),
_buildInfoRow('处理状态', isPending ? '待处理' : '已处理', isPending),
_buildInfoRow('处理时间', '', false),
SizedBox(height: 30.h),
Expanded(
child: SingleChildScrollView(
child: Text(
alert.description,
style: TextStyle(
color: Colors.white,
fontSize: 24.sp,
height: 1.5,
),
),
),
),
],
);
}
Widget _buildInfoRow(String label, String value, bool isHighlight) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 8.h),
child: Row(
children: [
SizedBox(
width: 140.w,
child: Text(
label,
style: TextStyle(
color: Colors.white.withValues(alpha: 153),
fontSize: 26.sp,
),
),
),
SizedBox(width: 20.w),
Text(
value,
style: TextStyle(
color: isHighlight ? const Color(0xFFFFC14A) : Colors.white,
fontSize: 26.sp,
fontWeight: isHighlight ? FontWeight.bold : FontWeight.normal,
),
),
],
),
);
}
Widget _buildBottomButtons(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(40.w, 20.h, 40.w, 30.h),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildButton(
'关闭窗口',
const Color(0xFF2A2E3B),
Colors.white,
const Color(0xFF4A4E5B),
() => Navigator.of(context).pop(),
),
SizedBox(width: 30.w),
_buildButton(
'标记误报',
const Color(0xFF2A2E3B),
Colors.white,
const Color(0xFF4A4E5B),
() => _handleMarkAsFalse(context),
),
SizedBox(width: 30.w),
_buildButton(
'已处理',
const Color(0xFF00D4FF),
Colors.black,
const Color(0xFF00D4FF),
() => _handleMarkAsProcessed(context),
isActive: true,
),
],
),
);
}
Widget _buildButton(
String text,
Color backgroundColor,
Color textColor,
Color borderColor,
VoidCallback onPressed, {
bool isActive = false,
}) {
return GestureDetector(
onTap: onPressed,
child: Container(
width: 220.w,
height: 72.h,
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(
color: borderColor,
width: 2.w,
),
borderRadius: BorderRadius.circular(8.r),
boxShadow: isActive
? [
BoxShadow(
color: borderColor.withValues(alpha: 128),
blurRadius: 10.r,
offset: const Offset(0, 0),
),
]
: null,
),
child: Center(
child: Text(
text,
style: TextStyle(
color: textColor,
fontSize: 28.sp,
fontWeight: FontWeight.bold,
),
),
),
),
);
}
void _handleMarkAsFalse(BuildContext context) {
context.read<MonitoringIndexCubit>().markAlertAsFalse(alert);
Navigator.of(context).pop();
}
void _handleMarkAsProcessed(BuildContext context) {
context.read<MonitoringIndexCubit>().markAlertAsProcessed(alert);
Navigator.of(context).pop();
}
}
\ No newline at end of file
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