Files
tuiche/lib/pages/device/instant_body_page.dart
2026-01-12 15:16:36 +08:00

773 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<InstantBodyPage> createState() => _InstantBodyPageState();
}
class _InstantBodyPageState extends State<InstantBodyPage>
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'] != null &&
device['person']
['name'] !=
null &&
device['person']
['name']
.toString()
.trim()
.isNotEmpty
? device['person']
['name']
.toString()
: '体征检测设备'.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().smaller_text_fontSize,
),
),
SizedBox(
height: 4.rpx,
),
Text(
"睡眠报告提示".tr,
style: TextStyle(
color: themeController.currentColor.sc4,
fontSize:
AppConstants().smaller_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<SpeedControlledGif> {
ui.Codec? _codec;
ui.FrameInfo? _currentFrame;
Timer? _timer;
bool _isDisposed = false;
@override
void initState() {
super.initState();
_loadGif();
}
Future<void> _loadGif() async {
final data = await rootBundle.load(widget.assetPath);
_codec = await ui.instantiateImageCodec(data.buffer.asUint8List());
if (_isDisposed) return;
_showNextFrame();
}
Future<void> _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,
);
}
}