import 'dart:async'; import 'package:EasyDartModule/EasyDartModule.dart' as edm; import 'package:ef/ef.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_svg/svg.dart'; import 'package:flutterflow_ui/flutterflow_ui.dart'; import 'package:vbvs_app/common/color/appConstants.dart'; import 'package:vbvs_app/common/util/CommonVariables.dart'; import 'package:vbvs_app/common/util/DailyLogUtils.dart'; import 'package:vbvs_app/common/util/FitTool.dart'; import 'package:vbvs_app/common/util/MyUtils.dart'; import 'package:vbvs_app/component/tool/ClickableContainer.dart'; import 'package:vbvs_app/controller/device/blueteeth_bind_controller.dart'; import 'package:vbvs_app/controller/device/device_type_controller.dart'; import 'package:vbvs_app/controller/main_bottom/global_controller.dart'; import 'package:vbvs_app/controller/theme_controller/ThemeController.dart'; import 'package:vbvs_app/controller/user_info_controller.dart'; import 'package:vbvs_app/model/WebSocketMessage.dart'; import 'package:vbvs_app/pages/device/component/DeviceStatusInfoWidget.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'dart:ui' as ui; class InstantBodyPage extends StatefulWidget { var personInfo; InstantBodyPage({super.key, required this.personInfo}); @override State createState() => _InstantBodyPageState(); } class _InstantBodyPageState extends State with WidgetsBindingObserver { GlobalController globalController = Get.find(); UserInfoController userInfoController = Get.find(); BlueteethBindController blueteethBindController = Get.find(); ThemeController themeController = Get.find(); DeviceTypeController deviceTypeController = Get.find(); int maxBodyMotion = 1; String breathState = "-"; String inBed = "-"; String onlineState = "离线".tr; Timer? _onlineTimer; // 添加 Timer 引用 int bodyMotion = -1; int breathrate = -1; String snores = "-"; int heartrate = -1; @override void initState() { WidgetsBinding.instance.addObserver(this); // 添加生命周期观察者 // edm.EasyDartModule.websocket.sendData(jsonEncode(WebSocketMessage( // path: "/vsbs/web/rt/marttress", // type: 1, // data: {"mac": widget.personInfo['mac']}))); // _startOnlineTimer(); // 初始化时启动定时器 try { Future.delayed(Duration(seconds: 0), () { CommonVariables.callMap["/vsbs/web/rt/marttress"] = (data) { edm.EasyDartModule.logger.info("[websocket]实时体征页面数据-->${data}]"); ef.log("[websocket]实时体征页面数据-->${data}]"); if (data['status'] == "离线") { inBed = "-"; bodyMotion = -1; heartrate = -1; snores = "-"; breathrate = -1; breathState = "-"; onlineState = "离线".tr; return; } inBed = data["inBed"]; // 心率 呼吸 体动 呼吸暂停 if ("离床".tr == inBed) { breathState = "否".tr; bodyMotion = 0; breathrate = 0; heartrate = 0; snores = "否".tr; } else { onlineState = "在线".tr; // 接收到数据,设置为在线 breathState = data["breathState"] == null || data["breathState"] == "" ? "-" : data["breathState"].toString().tr; bodyMotion = data['bodyMotion'] == null ? -1 : data['bodyMotion']; breathrate = data["breathRate"] == null ? -1 : data["breathRate"]; heartrate = data['heartRate'] == null ? -1 : data['heartRate']; snores = data['snores'] == null || data['snores'] == "" || data['snores'] == "否".tr ? "否".tr : "${data['snores']}".tr; } if (mounted) { setState(() { onlineState = "在线".tr; // 接收到数据,设置为在线 }); } _startOnlineTimer(); // 重置定时器 }; }); } catch (e) { print(e); edm.EasyDartModule.logger .error("[webscoekt]格式化数据错误-->${{"mac": widget.personInfo['mac']}}"); } _initWebSocket(); super.initState(); } void _initWebSocket() { // 发送WebSocket请求 if (widget.personInfo['status'] != null) { try { onlineState = widget.personInfo['status']['status'] == 1 ? "在线".tr : "离线".tr; if (widget.personInfo['status']['status'] != 0) { inBed = widget.personInfo['status']['inBed'] == 1 ? "在床".tr : "离床".tr; } } catch (e) { edm.EasyDartModule.logger .error("[webscoekt]格式化数据错误-->${{"mac": widget.personInfo['mac']}}"); } } edm.EasyDartModule.logger .info("[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}"); DailyLogUtils.writeLog( "[webscoekt]发送请求:数据-->${{"mac": widget.personInfo['mac']}}"); edm.EasyDartModule.websocket.sendData(jsonEncode(WebSocketMessage( path: "/vsbs/web/rt/marttress", type: 1, data: {"mac": widget.personInfo['mac']}))); _startOnlineTimer(); } //y @override void dispose() { WidgetsBinding.instance.removeObserver(this); // 移除生命周期观察者 _onlineTimer?.cancel(); _closeWebSocket(); CommonVariables.callMap.remove("/vsbs/web/rt/marttress"); super.dispose(); } // 监听应用生命周期变化 @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.resumed) { // 应用回到前台时重新连接WebSocket edm.EasyDartModule.logger.info("app切回页面,重连websocket"); _initWebSocket(); } else if (state == AppLifecycleState.paused) { // 应用进入后台时关闭WebSocket _closeWebSocket(); } } void _startOnlineTimer() { _onlineTimer?.cancel(); // 取消之前的定时器 _onlineTimer = Timer.periodic(Duration(seconds: 60), (timer) { if (mounted) { setState(() { edm.EasyDartModule.logger.info("60 秒内没有接收到数据,设置为离线"); onlineState = "离线".tr; // 30 秒内没有接收到数据,设置为离线 inBed = "-"; bodyMotion = -1; heartrate = -1; snores = "-"; breathrate = -1; breathState = "-"; }); } }); } @override Widget build(BuildContext context) { Map device = widget.personInfo; return LayoutBuilder( builder: (context, bodySize) => GestureDetector( // onTap: () => FocusScope.of(context).unfocus(),, child: Container( decoration: BoxDecoration( image: DecorationImage( image: AssetImage('assets/img/bgNoImg.png'), fit: BoxFit.fill, ), ), child: Scaffold( backgroundColor: Colors.transparent, appBar: AppBar( backgroundColor: themeController.currentColor.sc17, automaticallyImplyLeading: false, iconTheme: IconThemeData(color: themeController.currentColor.sc3), titleSpacing: 0.rpx, title: Container( width: double.infinity, height: 180.rpx, child: Stack( alignment: Alignment.center, children: [ RichText( text: TextSpan( style: TextStyle( fontFamily: 'Readex Pro', color: themeController.currentColor.sc3, fontSize: 30.rpx, letterSpacing: 0.0, ), children: [ TextSpan( text: '实时体征.标题'.tr, ), TextSpan( text: "(${onlineState})", style: TextStyle( color: onlineState == '在线'.tr ? themeController.currentColor.sc2 : themeController .currentColor.sc9, // 👈 单独设置颜色 ), ), ], ), ), Positioned( left: 0.rpx, child: returnIconButtom, ), ], ), ), actions: [], centerTitle: false, ), body: SafeArea( top: true, child: Stack( children: [ Align( alignment: Alignment.center, child: FractionallySizedBox( heightFactor: 0.5, widthFactor: 0.5, child: Opacity( opacity: 0.7, child: (onlineState == "离线".tr || inBed == '离床'.tr) ? Image.asset( 'assets/img/black_body_still.png', fit: BoxFit.contain, ) : SpeedControlledGif( 'assets/img/body_black.gif', speedFactor: 2, // 2.0 for 2x speed, 0.5 for half speed fit: BoxFit.contain, ), ), ), ), // 其余内容放在上层 Positioned.fill( child: Padding( padding: EdgeInsetsDirectional.fromSTEB( 0.rpx, 29.rpx, 0.rpx, 0.rpx), child: Column( mainAxisSize: MainAxisSize.max, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0.rpx, 30.rpx, 100.rpx), child: ClickableContainer( backgroundColor: themeController.currentColor.sc5, highlightColor: Colors.transparent, // 点击涟漪颜色 borderRadius: AppConstants() .normal_container_radius, // 如果你想加圆角可以设置 eg. 12.rpx padding: EdgeInsets.zero, onTap: () { print('点击了体征卡片'); }, child: Row( mainAxisSize: MainAxisSize.max, children: [ Flexible( flex: 2, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '实时体征.姓名'.tr, style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController .currentColor.sc4, ), ), // Text( // '实时体征.年龄'.tr, // style: TextStyle( // fontFamily: 'Inter', // fontSize: 26.rpx, // letterSpacing: 0.0, // color: themeController // .currentColor.sc4, // ), // ), ].divide( SizedBox(height: 34.rpx)), ), Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Container( width: MediaQuery.sizeOf(context) .width * 0.2, child: Text( '${device['person']?['name'] ?? '未命名'.tr}', style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController .currentColor.sc3, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), ), // Text( // '${MyUtils.getAgeByDate(MyUtils.formatBirthdayTime(device['person']?['birthday'])) ?? '未知数据'.tr}', // style: TextStyle( // fontFamily: 'Inter', // fontSize: 26.rpx, // letterSpacing: 0.0, // color: themeController // .currentColor.sc3, // ), // ), ].divide( SizedBox(height: 34.rpx)), ), ] .divide(SizedBox(width: 33.rpx)) .addToStart( SizedBox(width: 37.rpx)), ), ] .addToStart(SizedBox(height: 36.rpx)) .addToEnd(SizedBox(height: 36.rpx)), ), ), Flexible( flex: 3, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Column( crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( '实时体征.设备ID'.tr, style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController .currentColor.sc4, ), ), // Text( // '实时体征.体重'.tr, // style: TextStyle( // fontFamily: 'Inter', // fontSize: 26.rpx, // letterSpacing: 0.0, // color: themeController // .currentColor.sc4, // ), // ), ].divide( SizedBox(height: 34.rpx)), ), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( '${device['code'] ?? '未知数据'.tr}', // "D11250300003", style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController .currentColor.sc3, ), maxLines: 1, overflow: TextOverflow.ellipsis, ), // Text( // '${device['person']?['weight'] ?? '未知数据'.tr}kg', // style: TextStyle( // fontFamily: 'Inter', // fontSize: 26.rpx, // letterSpacing: 0.0, // color: themeController // .currentColor.sc3, // ), // ), ].divide( SizedBox(height: 34.rpx)), ), ), ] .divide(SizedBox(width: 33.rpx)) .addToStart( SizedBox(width: 37.rpx)), ), ] .addToStart(SizedBox(height: 36.rpx)) .addToEnd(SizedBox(height: 36.rpx)), ), ), ], ), ), ), Expanded( child: SingleChildScrollView( child: Column( children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0, 30.rpx, 0), child: Container( child: Column( children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DeviceStatusInfoWidget( title: "在离床".tr, iconAsset: "assets/img/icon/bed_status.svg", value: inBed, ), DeviceStatusInfoWidget( title: "体动".tr, iconAsset: "assets/img/icon/bodymotion.svg", value: inBed == "离床".tr ? ("-") : (bodyMotion == null || bodyMotion == -1) ? "-" : "$bodyMotion", ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DeviceStatusInfoWidget( title: "心率".tr, iconAsset: "assets/img/icon/heart.svg", value: inBed == "离床".tr ? "-" : ((heartrate == null || heartrate == -1) ? "-" : "$heartrate"), ), DeviceStatusInfoWidget( title: "打鼾".tr, iconAsset: "assets/img/icon/snore.svg", value: inBed == "离床".tr ? "-" : ('${snores}'.tr), ), ], ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ DeviceStatusInfoWidget( title: "呼吸".tr, iconAsset: "assets/img/icon/breathe.svg", value: inBed == "离床".tr ? ("-") : ((breathrate == null || breathrate == -1) ? "-" : "$breathrate"), ), DeviceStatusInfoWidget( title: "呼吸暂停".tr, iconAsset: "assets/img/icon/breathe_pause.svg", value: inBed == "离床".tr ? "-" : ('${breathState}'), ), ], ), ].divide(SizedBox(height: 49.rpx)), ), ), ), Padding( padding: EdgeInsetsDirectional.fromSTEB( 0.rpx, 67.rpx, 0.rpx, 0.rpx), child: Container( height: 40.rpx, child: Text( bodyMotion >= maxBodyMotion ? '请保持静止'.tr : "", style: TextStyle( fontFamily: 'Inter', fontSize: 26.rpx, letterSpacing: 0.0, color: themeController.currentColor.sc9, ), ), ), ), // SizedBox( // height: 207.rpx, // ), ], ), )), ClickableContainer( backgroundColor: Colors.transparent, // 可自定义背景色 highlightColor: Colors.transparent, // 点击涟漪颜色 borderRadius: 16.rpx, // 圆角大小,可按需调整 padding: EdgeInsetsDirectional.fromSTEB( 30.rpx, 0.rpx, 30.rpx, 0.rpx), onTap: () {}, child: Container( padding: EdgeInsetsDirectional.fromSTEB( 26.rpx, 26.rpx, 26.rpx, 26.rpx), decoration: BoxDecoration( // color: FlutterFlowTheme.of(context) // .primaryBackground // .withOpacity(0.6), // 半透明背景 borderRadius: BorderRadius.circular(16.rpx), border: Border.all( color: themeController.currentColor.sc4 .withOpacity(0.5), width: 0.5.rpx, ), ), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ Padding( padding: EdgeInsetsDirectional.fromSTEB( 0, 8.rpx, 0, 0), child: Container( width: 23.rpx, height: 23.rpx, // width: double.infinity, decoration: BoxDecoration(), child: SvgPicture.asset( 'assets/img/icon/tips.svg', fit: BoxFit.cover, color: themeController.currentColor.sc4, ), ), ), Expanded( child: Text( '实时体征.提示'.tr, style: TextStyle( fontFamily: 'Inter', letterSpacing: 0.0, color: themeController.currentColor.sc4, ), ), ), ].divide(SizedBox(width: 23.rpx)), ), ), ), SizedBox( height: 40.rpx, ), Padding( padding: EdgeInsets.fromLTRB(46.rpx, 0, 46.rpx, 0), child: Column( children: [ Text( "MAC号".tr + ": ${widget.personInfo['mac'] ?? '未知数据'.tr}", style: TextStyle( color: themeController.currentColor.sc4, fontSize: AppConstants().middler_text_fontSize, ), ), SizedBox( height: 4.rpx, ), Text( "睡眠报告提示".tr, style: TextStyle( color: themeController.currentColor.sc4, fontSize: AppConstants().middler_text_fontSize, ), ), ], ), ), SizedBox( height: 40.rpx, ), SizedBox( height: 26.rpx, ), ], ), ), ), ], ), ), ), ), ), ); } void _closeWebSocket() { // 取消WebSocket订阅 edm.EasyDartModule.websocket.sendData( jsonEncode(WebSocketMessage(path: "/vsbs/web/rt/marttress", type: 2))); _onlineTimer?.cancel(); } } class SpeedControlledGif extends StatefulWidget { final String assetPath; final double speedFactor; final BoxFit? fit; /// [speedFactor] 播放速度倍数,默认1.0, /// 大于1表示加速播放,小于1表示减速播放 const SpeedControlledGif( this.assetPath, { Key? key, this.speedFactor = 1.0, this.fit, }) : super(key: key); @override _SpeedControlledGifState createState() => _SpeedControlledGifState(); } class _SpeedControlledGifState extends State { ui.Codec? _codec; ui.FrameInfo? _currentFrame; Timer? _timer; bool _isDisposed = false; @override void initState() { super.initState(); _loadGif(); } Future _loadGif() async { final data = await rootBundle.load(widget.assetPath); _codec = await ui.instantiateImageCodec(data.buffer.asUint8List()); if (_isDisposed) return; _showNextFrame(); } Future _showNextFrame() async { if (_isDisposed || _codec == null) return; _currentFrame = await _codec!.getNextFrame(); if (_isDisposed) return; if (mounted) { setState(() {}); } // 取当前帧持续时间,单位毫秒 final baseDuration = _currentFrame?.duration.inMilliseconds ?? 100; // 限制最小帧间隔,防止刷新过快 const minFrameDuration = 50; // 计算实际播放帧间隔,speedFactor越大速度越快 final adjustedDuration = (baseDuration / widget.speedFactor) .round() .clamp(minFrameDuration, 10000); _timer = Timer(Duration(milliseconds: adjustedDuration), _showNextFrame); } @override void dispose() { _isDisposed = true; _timer?.cancel(); _codec?.dispose(); super.dispose(); } @override Widget build(BuildContext context) { if (_currentFrame == null) { // 加载中或无帧时显示空容器或占位 return Container(); } return RawImage( image: _currentFrame!.image, fit: widget.fit, ); } }