blood_pressure.dart 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  1. import 'dart:convert';
  2. import 'package:flutter/material.dart';
  3. import 'package:flutter/services.dart';
  4. import 'package:get/get.dart';
  5. import 'package:vitalapp/components/dialog_number.dart';
  6. import 'package:vitalapp/pages/medical/widgets/device_status_position.dart';
  7. import 'package:vnote_device_plugin/consts/types.dart';
  8. import 'package:vnote_device_plugin/devices/nibp.dart';
  9. import 'package:vnote_device_plugin/models/exams/nibp.dart';
  10. import 'package:vitalapp/components/alert_dialog.dart';
  11. import 'package:vitalapp/managers/interfaces/models/device.dart';
  12. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_card.dart';
  13. import 'package:vitalapp/pages/medical/controller.dart';
  14. import 'package:vitalapp/pages/medical/controllers/nibp.dart';
  15. import 'package:vitalapp/pages/medical/models/item.dart';
  16. import 'package:vitalapp/pages/medical/models/worker.dart';
  17. import 'package:vitalapp/pages/medical/widgets/device_status.dart';
  18. import 'package:vitalapp/pages/medical/widgets/switch_button.dart';
  19. /// TODO 需要优化
  20. /// 小弹窗输入
  21. class VDialogBloodPressure extends StatelessWidget {
  22. /// 标题
  23. final String? title;
  24. /// 描述
  25. final String? description;
  26. /// 输入占位符
  27. final String? placeholder;
  28. /// 初始值
  29. final List<String>? initialValue;
  30. const VDialogBloodPressure({
  31. super.key,
  32. this.title,
  33. this.description,
  34. this.placeholder,
  35. this.initialValue,
  36. });
  37. Future<String?> show<String>() => VAlertDialog.showDialog<String>(this);
  38. @override
  39. Widget build(BuildContext context) {
  40. final controller1 = TextEditingController(text: initialValue?.first);
  41. final controller2 = TextEditingController(text: initialValue?.last);
  42. return VAlertDialog(
  43. title: title,
  44. width: 440,
  45. contentPadding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
  46. content: _buildContent(context, controller1, controller2),
  47. showCancel: true,
  48. onConfirm: () {
  49. Get.back(result: json.encode([controller1.text, controller2.text]));
  50. },
  51. );
  52. }
  53. Widget _buildContent(
  54. BuildContext context,
  55. TextEditingController controller1,
  56. TextEditingController controller2,
  57. ) {
  58. final children = <Widget>[];
  59. if (description != null) {
  60. children.add(
  61. Padding(
  62. padding: const EdgeInsets.symmetric(horizontal: 4),
  63. child: Text(
  64. description!,
  65. style: const TextStyle(color: Colors.black87, fontSize: 18),
  66. ),
  67. ),
  68. );
  69. children.add(const SizedBox(height: 8));
  70. } else {
  71. children.add(const SizedBox(height: 12));
  72. }
  73. children.add(_buildInputWidget(context, controller1, controller2));
  74. return SingleChildScrollView(
  75. child: Column(
  76. mainAxisSize: MainAxisSize.min,
  77. crossAxisAlignment: CrossAxisAlignment.start,
  78. children: children,
  79. ),
  80. );
  81. }
  82. Widget _buildInputWidget(
  83. BuildContext context,
  84. TextEditingController controller1,
  85. TextEditingController controller2,
  86. ) {
  87. const fontSize = 20.0;
  88. return Row(
  89. children: [
  90. _buildItem(context, controller1, '收缩压'),
  91. const Text(
  92. '/',
  93. style: TextStyle(fontSize: fontSize),
  94. ),
  95. _buildItem(context, controller2, '舒张压'),
  96. ],
  97. );
  98. }
  99. Widget _buildItem(
  100. BuildContext context,
  101. TextEditingController controller,
  102. String hintText,
  103. ) {
  104. const fontSize = 20.0;
  105. const height = 56.0;
  106. return Expanded(
  107. child: SizedBox(
  108. height: height,
  109. child: TextField(
  110. controller: controller,
  111. readOnly: false,
  112. autofocus: true,
  113. keyboardType: const TextInputType.numberWithOptions(
  114. decimal: true), // 允许输入数字和小数点
  115. inputFormatters: [
  116. FilteringTextInputFormatter.allow(
  117. RegExp(r'^\d+\.?\d{0,2}'),
  118. ), // 只允许输入数字和小数点,俩位小数
  119. ],
  120. style: const TextStyle(fontSize: fontSize),
  121. decoration: InputDecoration(
  122. border: const UnderlineInputBorder(
  123. borderRadius: BorderRadius.zero,
  124. borderSide: BorderSide(),
  125. ),
  126. enabledBorder: const UnderlineInputBorder(
  127. borderRadius: BorderRadius.zero,
  128. borderSide: BorderSide(
  129. color: Colors.black54,
  130. ),
  131. ),
  132. focusedBorder: UnderlineInputBorder(
  133. borderRadius: BorderRadius.zero,
  134. borderSide: BorderSide(
  135. color: Theme.of(context).primaryColor.withOpacity(.4),
  136. ),
  137. ),
  138. filled: true,
  139. fillColor: Colors.white,
  140. contentPadding: const EdgeInsets.symmetric(
  141. vertical: (height - fontSize * 1.2) / 2,
  142. horizontal: 8,
  143. ),
  144. hintStyle: const TextStyle(fontSize: fontSize),
  145. labelStyle: const TextStyle(fontSize: fontSize),
  146. hintText: hintText,
  147. isCollapsed: true,
  148. ),
  149. onSubmitted: (value) {
  150. print(value);
  151. Get.back(result: value);
  152. },
  153. ),
  154. ),
  155. );
  156. }
  157. }
  158. // ignore: must_be_immutable
  159. class BloodPressure extends StatefulWidget {
  160. const BloodPressure({
  161. super.key,
  162. // required this.currentValue,
  163. // required this.bloodPressure,
  164. });
  165. // Map<String, dynamic> currentValue;
  166. // Function(Map<String, dynamic>) bloodPressure;
  167. @override
  168. State<BloodPressure> createState() => _ExamBloodPressureState();
  169. }
  170. class _ExamBloodPressureState extends State<BloodPressure> {
  171. var controller = Get.find<MedicalController>();
  172. PressureStatus pressureStatus = PressureStatus.left;
  173. late NibpDeviceController nibp;
  174. NibpDeviceWorker? worker;
  175. int liveValue = 0;
  176. int errorCount = 0; //设备重连失败次数
  177. // late NibpExamValue? value = NibpExamValue(
  178. // diastolicPressure: int.parse(
  179. // controller.diagnosisDataValue['NIBP']?['Sbp']?.toString() ?? '',
  180. // ),
  181. // systolicPressure: int.parse(
  182. // controller.diagnosisDataValue['NIBP']?['Dbp']?.toString() ?? '',
  183. // ),
  184. // pulse: 0,
  185. // );
  186. WorkerStatus _connectStatus = WorkerStatus.connecting;
  187. @override
  188. void initState() {
  189. currentDevice();
  190. super.initState();
  191. }
  192. Future<void> currentDevice() async {
  193. DeviceModel? device = await controller.getDevice(DeviceTypes.NIBP);
  194. if (device.isNull) {
  195. _connectStatus = WorkerStatus.unboundDevice;
  196. worker = null;
  197. setState(() {});
  198. return;
  199. }
  200. nibp = NibpDeviceController(device?.model ?? '', device?.mac ?? '');
  201. worker = nibp.worker;
  202. _connectStatus = nibp.connectStatus;
  203. loadListeners();
  204. }
  205. void loadListeners() {
  206. worker!.liveUpdateEvent.addListener(_onLiveUpdate);
  207. worker!.resultUpdateEvent.addListener(_onSuccess);
  208. worker!.connectErrorEvent.addListener(_onConnectFail);
  209. worker!.connectedEvent.addListener(_onConnectSuccess);
  210. worker!.connect();
  211. }
  212. Future<void> disconnect() async {
  213. try {
  214. if (worker != null) {
  215. await worker!.disconnect();
  216. worker!.connectErrorEvent.removeListener(_onConnectFail);
  217. worker!.connectedEvent.removeListener(_onConnectSuccess);
  218. worker!.liveUpdateEvent.removeListener(_onLiveUpdate);
  219. worker!.resultUpdateEvent.removeListener(_onSuccess);
  220. worker!.disconnectedEvent.removeListener(_onDisconnected);
  221. }
  222. } catch (err) {
  223. print(err);
  224. }
  225. }
  226. @override
  227. void dispose() {
  228. disconnect();
  229. worker?.dispose();
  230. super.dispose();
  231. }
  232. void _onLiveUpdate(_, int e) {
  233. setState(() {
  234. liveValue = e;
  235. });
  236. }
  237. /// TODO 需求不清,检测的数据需要传给体检,但是检测又不区分左右侧血压
  238. void _onSuccess(_, NibpExamValue e) {
  239. setState(() {
  240. /// 这是第三方需要的数据
  241. controller.diagnosisDataValue['NIBP'] = {
  242. 'Sbp': e.systolicPressure.toString(),
  243. 'Dbp': e.diastolicPressure.toString(),
  244. 'Pulse_Beat': e.pulse.toString(),
  245. };
  246. controller.saveCachedRecord();
  247. });
  248. }
  249. void _onConnectFail(sender, e) {
  250. print('连接设备失败');
  251. if (errorCount < 3) {
  252. worker?.connect();
  253. }
  254. setState(() {
  255. errorCount++;
  256. _connectStatus = WorkerStatus.connectionFailed;
  257. });
  258. }
  259. void _onDisconnected(sender, e) {
  260. print('设备连接中断');
  261. if (errorCount < 3) {
  262. worker?.connect();
  263. }
  264. setState(() {
  265. errorCount++;
  266. _connectStatus = WorkerStatus.disconnected;
  267. });
  268. }
  269. void _onConnectSuccess(sender, e) {
  270. _connectStatus = WorkerStatus.connected;
  271. setState(() {});
  272. }
  273. Widget _buildValue() {
  274. if (controller.diagnosisDataValue['NIBP']?['Sbp'] != null) {
  275. return _buildResultWidget();
  276. }
  277. return _buildLiveWidget();
  278. }
  279. @override
  280. Widget build(BuildContext context) {
  281. return Stack(
  282. children: [
  283. ExamCard(
  284. title: '血压',
  285. content: Container(
  286. padding: const EdgeInsets.only(top: 50),
  287. child: Column(
  288. children: [
  289. InkWell(
  290. child: _SideBar(
  291. value: _buildValue(),
  292. unit: 'mmHg',
  293. ),
  294. onTap: () async {
  295. String? result = await VDialogBloodPressure(
  296. title: '血压',
  297. initialValue: [
  298. controller.diagnosisDataValue['NIBP']?['Sbp']
  299. .toString() ??
  300. '',
  301. controller.diagnosisDataValue['NIBP']?['Dbp']
  302. .toString() ??
  303. '',
  304. ],
  305. ).show();
  306. if (result != null &&
  307. jsonDecode(result)?.first != '' &&
  308. jsonDecode(result)?.last != '') {
  309. controller.diagnosisDataValue['NIBP'] = {
  310. 'Sbp': jsonDecode(result)?.first,
  311. 'Dbp': jsonDecode(result)?.last,
  312. 'Pulse_Beat': '',
  313. };
  314. print(result);
  315. controller.saveCachedRecord();
  316. }
  317. setState(() {});
  318. },
  319. ),
  320. InkWell(
  321. onTap: () async {
  322. String? result = await VDialogNumber(
  323. title: '脉率',
  324. initialValue: controller.diagnosisDataValue['NIBP']
  325. ?['Pulse_Beat'] ??
  326. '',
  327. ).show();
  328. if (result != null && result.isNotEmpty) {
  329. controller.diagnosisDataValue['NIBP'] = {
  330. 'Sbp': controller.diagnosisDataValue['NIBP']['Sbp'],
  331. 'Dbp': controller.diagnosisDataValue['NIBP']['Dbp'],
  332. 'Pulse_Beat': result
  333. };
  334. controller.saveCachedRecord();
  335. }
  336. setState(() {});
  337. },
  338. child: Container(
  339. padding: const EdgeInsets.symmetric(horizontal: 28),
  340. child: Row(
  341. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  342. children: [
  343. const Text(
  344. '脉率',
  345. style: TextStyle(fontSize: 25),
  346. ),
  347. Text(
  348. controller.diagnosisDataValue['NIBP']?['Pulse_Beat']
  349. .toString() ??
  350. '',
  351. style: const TextStyle(
  352. fontSize: 48,
  353. ),
  354. )
  355. ],
  356. ),
  357. ),
  358. )
  359. ],
  360. ),
  361. ),
  362. ),
  363. if (errorCount < 3)
  364. DeviceStatusPosition(
  365. deviceStatus: DeviceStatus(connectStatus: _connectStatus),
  366. ),
  367. if (errorCount >= 3) _buildErrorButton(),
  368. ],
  369. );
  370. }
  371. /// 需要封装一下
  372. Widget _buildErrorButton() {
  373. return DeviceStatusPosition(
  374. deviceStatus: Row(
  375. children: [
  376. const Text(
  377. '请确认设备是否启动',
  378. style: TextStyle(fontSize: 24, color: Colors.red),
  379. ),
  380. const SizedBox(
  381. width: 8,
  382. ),
  383. IconButton(
  384. onPressed: () {
  385. worker?.connect();
  386. setState(() {
  387. _connectStatus = WorkerStatus.connecting;
  388. errorCount = 0;
  389. });
  390. },
  391. icon: const Icon(Icons.refresh),
  392. iconSize: 32,
  393. ),
  394. const SizedBox(
  395. width: 32,
  396. ),
  397. ],
  398. ),
  399. );
  400. }
  401. Widget _buildLeftOrRightBloodPressure() {
  402. return Positioned(
  403. top: 130,
  404. left: 38,
  405. child: AnimatedToggle(
  406. values: const ['左侧', '右侧'],
  407. onToggleCallback: (value) {
  408. pressureStatus = value;
  409. setState(() {});
  410. },
  411. statusValue: pressureStatus,
  412. buttonColor: Theme.of(context).primaryColor,
  413. backgroundColor: const Color(0xFFB5C1CC),
  414. textColor: const Color(0xFFFFFFFF),
  415. ),
  416. // child: Text('Toggle Value : $_toggleValue'),
  417. );
  418. }
  419. Widget _buildLiveWidget() {
  420. return Center(
  421. child: RichText(
  422. text: TextSpan(
  423. text: liveValue.toString() == '0' ? '--' : liveValue.toString(),
  424. style: const TextStyle(
  425. fontSize: 80,
  426. color: Colors.black,
  427. ),
  428. children: const [
  429. TextSpan(text: ' '),
  430. TextSpan(
  431. text: 'mmHg',
  432. style: TextStyle(fontSize: 25),
  433. )
  434. ],
  435. ),
  436. ),
  437. );
  438. }
  439. Widget _buildResultWidget() {
  440. const textStyle = TextStyle(
  441. fontSize: 48,
  442. );
  443. return Stack(
  444. children: [
  445. Column(
  446. mainAxisAlignment: MainAxisAlignment.center,
  447. mainAxisSize: MainAxisSize.min,
  448. crossAxisAlignment: CrossAxisAlignment.center,
  449. children: [
  450. Align(
  451. alignment: Alignment.centerRight,
  452. child: Text(
  453. controller.diagnosisDataValue['NIBP']['Sbp'],
  454. style: textStyle,
  455. ),
  456. ),
  457. Align(
  458. alignment: Alignment.centerLeft,
  459. child: Text(
  460. controller.diagnosisDataValue['NIBP']['Dbp'],
  461. style: textStyle,
  462. ),
  463. ),
  464. ],
  465. ),
  466. const Positioned.fill(
  467. child: Center(
  468. child: Text(
  469. " /",
  470. style: TextStyle(fontSize: 24),
  471. ),
  472. ),
  473. ),
  474. ],
  475. );
  476. }
  477. }
  478. class _SideBar extends StatelessWidget {
  479. final Widget value;
  480. final String unit;
  481. const _SideBar({
  482. required this.value,
  483. required this.unit,
  484. });
  485. @override
  486. Widget build(BuildContext context) {
  487. return Row(
  488. mainAxisAlignment: MainAxisAlignment.end,
  489. crossAxisAlignment: CrossAxisAlignment.start,
  490. children: [
  491. Container(
  492. alignment: Alignment.bottomRight,
  493. padding: const EdgeInsets.only(
  494. bottom: 20,
  495. right: 30,
  496. left: 40,
  497. ),
  498. child: FittedBox(
  499. child: Row(
  500. mainAxisAlignment: MainAxisAlignment.end,
  501. crossAxisAlignment: CrossAxisAlignment.end,
  502. children: [
  503. value,
  504. ],
  505. ),
  506. ),
  507. ),
  508. ],
  509. );
  510. }
  511. }