ecg_result_view.dart 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. import 'dart:convert';
  2. import 'package:ecg_list_view/ecg_list/widgets/conclusion_dialog.dart';
  3. import 'package:fis_common/index.dart';
  4. import 'package:flutter/foundation.dart';
  5. import 'package:get/get.dart';
  6. import 'package:http/http.dart' as http;
  7. import 'package:fis_jsonrpc/rpc.dart';
  8. import 'package:flutter/cupertino.dart';
  9. import 'package:flutter/material.dart';
  10. import 'package:vitalapp/architecture/storage/storage.dart';
  11. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  12. import 'package:vitalapp/architecture/utils/upload.dart';
  13. import 'package:vitalapp/components/appbar.dart';
  14. import 'package:vitalapp/components/dropdown_button.dart';
  15. import 'package:vitalapp/components/no_data_view.dart';
  16. import 'package:vitalapp/components/select.dart';
  17. import 'package:vitalapp/managers/interfaces/exam.dart';
  18. import 'package:vitalapp/managers/interfaces/report.dart';
  19. import 'package:vitalapp/managers/interfaces/report_template.dart';
  20. import 'package:vitalapp/pages/medical/controller.dart';
  21. import 'package:vitalapp/pages/medical/models/worker.dart';
  22. import 'package:vitalapp/pages/medical/widgets/device_status_position.dart';
  23. import 'package:vitalapp/pages/medical/widgets/twelve_ecg.dart';
  24. import 'package:vitalapp/pages/medical/widgets/twelve_ecg_view/controller.dart';
  25. import 'package:vitalapp/pages/medical/widgets/twelve_ecg_view/view.dart';
  26. import 'package:vitalapp/pages/medical/widgets/twelve_ecg_view/widgets/ecg_device_status.dart';
  27. import 'package:vitalapp/pages/medical/widgets/twelve_ecg_view/widgets/full_screen_ecg_data_dialog.dart';
  28. import 'package:vitalapp/rpc.dart';
  29. import 'package:vnote_device_plugin/models/exams/twelve_heart.dart';
  30. import 'ecg_term_selection.dart';
  31. class EcgResultView extends StatefulWidget {
  32. final ElectrocardiogramRecord recordInfo;
  33. final String reportCode;
  34. EcgResultView(
  35. this.recordInfo, {
  36. this.reportCode = "",
  37. });
  38. @override
  39. State<StatefulWidget> createState() {
  40. return EcgResultViewState();
  41. }
  42. }
  43. class EcgResultViewState extends State<EcgResultView> {
  44. TwelveHeartResultEntity? resultConclusion;
  45. final MedicalController medicalController = Get.find<MedicalController>();
  46. final GlobalKey<ScaffoldState> scaffoldKey = GlobalKey<ScaffoldState>();
  47. final _promptWordsController = TextEditingController();
  48. final _hrController = TextEditingController();
  49. final _qRSAxisController = TextEditingController();
  50. final _pRController = TextEditingController();
  51. final _qTDurController = TextEditingController();
  52. final _qTCDurController = TextEditingController();
  53. final _pAxisController = TextEditingController();
  54. final _qRSController = TextEditingController();
  55. final _tAxisController = TextEditingController();
  56. final _pDurController = TextEditingController();
  57. final _tDurController = TextEditingController();
  58. double _paperSpeed = 25.0;
  59. int _gain = 5;
  60. List<ReportTemplateDTO> _templates = [];
  61. ReportTemplateDTO _currentSelectedTemplate = ReportTemplateDTO();
  62. /// 初始时的心电初始数据
  63. List<int> _initEcgDatas = [];
  64. @override
  65. void initState() {
  66. String examData = widget.recordInfo.examData ?? '';
  67. if (examData.isNotEmpty) {
  68. Get.find<IReportTemplateManager>()
  69. .getReportTemplatePage(
  70. businessType: ReportTemplateBusinessTypeEnum.HEIECG)
  71. .then((result) {
  72. setState(() {
  73. _templates = result.pageData ?? [];
  74. if (_templates.isNotEmpty) {
  75. _currentSelectedTemplate = _templates.first;
  76. }
  77. });
  78. });
  79. Map<String, dynamic> examDatas = jsonDecode(examData);
  80. if (examDatas.containsKey("ECG_POINT12")) {
  81. String exgPoint12Url = examDatas["ECG_POINT12"].toString();
  82. if (exgPoint12Url.startsWith('https://') ||
  83. exgPoint12Url.startsWith("http://")) {
  84. http.get(Uri.parse(exgPoint12Url)).then((value) {
  85. var initEcgData = jsonDecode(value.body).cast<int>();
  86. //Get.lazyPut(() => TwelveEcgViewController(initPoints: initEcgData));
  87. setState(() {
  88. _initEcgDatas = initEcgData;
  89. });
  90. });
  91. }
  92. }
  93. if (examDatas.containsKey("Analyse12")) {
  94. String analyse12 = examDatas["Analyse12"].toString();
  95. if (analyse12.isNotEmpty) {
  96. resultConclusion =
  97. TwelveHeartResultEntity.fromJson(jsonDecode(analyse12));
  98. if (resultConclusion != null) {
  99. _promptWordsController.text = resultConclusion!.advice;
  100. _qRSAxisController.text = resultConclusion!.QRSAxis;
  101. _pRController.text = resultConclusion!.PR;
  102. _qTDurController.text = resultConclusion!.QTDur;
  103. _qTCDurController.text = resultConclusion!.QTCDur;
  104. _pAxisController.text = resultConclusion!.PAxis;
  105. _qRSController.text = resultConclusion!.QRSDur;
  106. _tAxisController.text = resultConclusion!.TAxis;
  107. _pDurController.text = resultConclusion!.PDur;
  108. _tDurController.text = resultConclusion!.TDur;
  109. }
  110. }
  111. }
  112. }
  113. var startDate =
  114. medicalController.diagnosisDataValue['TwelveHeart']?['StartDate'] ?? "";
  115. var endDate =
  116. medicalController.diagnosisDataValue['TwelveHeart']?['EndDate'] ?? '';
  117. if (startDate.toString() != "" && endDate.toString() != "")
  118. Get.find<TwelveEcgViewController>().state.rangeValues = RangeValues(
  119. double.parse(startDate.toString()), double.parse(endDate.toString()));
  120. super.initState();
  121. }
  122. @override
  123. Widget build(BuildContext context) {
  124. final double ecgViewWidth = 950;
  125. return Scaffold(
  126. key: scaffoldKey,
  127. resizeToAvoidBottomInset: false, // 防止内容自动调整以避免被键盘遮挡
  128. appBar: VAppBar(
  129. titleText: "体检心电",
  130. actions: [
  131. Container(
  132. margin: EdgeInsets.only(right: 10),
  133. child: Row(
  134. children: [
  135. Text(
  136. widget.recordInfo.patientName ?? '',
  137. style: TextStyle(color: Colors.white, fontSize: 24),
  138. ),
  139. SizedBox(width: 38),
  140. ElevatedButton(
  141. onPressed: () async {
  142. String examData = widget.recordInfo.examData ?? '';
  143. if (examData.isNotEmpty) {
  144. await Get.find<TwelveEcgViewController>()
  145. .updateEcgImage();
  146. String base64Image = medicalController
  147. .diagnosisDataValue['TwelveHeart']?['ECG12'];
  148. Map<String, dynamic> examDatas = jsonDecode(examData);
  149. if (examDatas.containsKey("ECG12")) {
  150. if (kIsWeb) {
  151. var file =
  152. UploadUtils.convertBase64ToFile(base64Image);
  153. String? url = await rpc.storage.webUpload(file!);
  154. examDatas['ECG12'] = url ?? '';
  155. } else {
  156. final imageFile =
  157. UploadUtils.convertBase64ToXFile(base64Image);
  158. String? imageUrl = await rpc.storage
  159. .upload(imageFile!, fileType: "jpg");
  160. examDatas['ECG12'] = imageUrl ?? '';
  161. }
  162. }
  163. if (examDatas.containsKey("Analyse12")) {
  164. resultConclusion!.advice =
  165. _promptWordsController.text;
  166. resultConclusion!.QRSAxis = _qRSAxisController.text;
  167. resultConclusion!.PR = _pRController.text;
  168. resultConclusion!.QTDur = _qTDurController.text;
  169. resultConclusion!.QTCDur = _qTCDurController.text;
  170. resultConclusion!.PAxis = _pAxisController.text;
  171. resultConclusion!.QRSDur = _qRSController.text;
  172. resultConclusion!.TAxis = _tAxisController.text;
  173. resultConclusion!.PDur = _pDurController.text;
  174. resultConclusion!.TDur = _tDurController.text;
  175. resultConclusion!.paperSpeed =
  176. _paperSpeed.toString();
  177. resultConclusion!.gain = _gain.toString();
  178. var newData = resultConclusion?.toJson();
  179. examDatas["Analyse12"] = jsonEncode(newData);
  180. examDatas["HEART12"] = _hrController.text;
  181. var exam = jsonEncode(examDatas);
  182. if (widget.reportCode.isEmpty) {
  183. final String result =
  184. await Get.find<IReportManager>().addReport(
  185. recordCode: widget.recordInfo.code ?? '',
  186. reportDatasJson: exam,
  187. name: widget.recordInfo.patientName ?? '',
  188. reportTemplateCode:
  189. _currentSelectedTemplate.reportTemplateCode,
  190. );
  191. if (result.isNotEmpty) {
  192. PromptBox.toast('提交成功');
  193. Get.back();
  194. }
  195. } else {
  196. final bool result =
  197. await Get.find<IReportManager>().modifyReport(
  198. recordCode: widget.recordInfo.code ?? '',
  199. reportInfoJson: exam,
  200. name: widget.recordInfo.patientName ?? '',
  201. reportTemplateCode: _currentSelectedTemplate
  202. .reportTemplateCode ??
  203. '',
  204. reportCode: widget.reportCode,
  205. );
  206. if (result) {
  207. PromptBox.toast('提交成功');
  208. Get.back();
  209. }
  210. }
  211. }
  212. }
  213. },
  214. child: Text(
  215. "提交",
  216. style: TextStyle(
  217. fontSize: 20,
  218. ),
  219. ))
  220. ],
  221. )),
  222. ],
  223. ),
  224. drawer: Drawer(
  225. width: 550,
  226. child: ECGTermSelection(
  227. onTab: (v) {
  228. _promptWordsController.text += "\r\n" + v;
  229. },
  230. ),
  231. ),
  232. body: widget.recordInfo.examData.isNullOrEmpty
  233. ? VNoDataView()
  234. : Container(
  235. color: Colors.white,
  236. child: Row(
  237. children: [
  238. SizedBox(width: 5),
  239. if (_initEcgDatas.isNotEmpty) ...[
  240. Expanded(
  241. flex: 10,
  242. child: Column(
  243. crossAxisAlignment: CrossAxisAlignment.start,
  244. children: [
  245. Expanded(
  246. child: _buildHeader(context),
  247. ),
  248. Stack(
  249. children: [
  250. GetBuilder(
  251. init: TwelveEcgViewController(
  252. initPoints: _initEcgDatas),
  253. id: "twelve_ecg_view",
  254. builder: (_) {
  255. return FullScreenEcgDataDialog(
  256. height: kIsWeb ? 520 : 610,
  257. showHeader: false,
  258. widthScale: 1,
  259. hintFontSize: 14,
  260. );
  261. }),
  262. Positioned(
  263. top: 70,
  264. left: 20,
  265. child: Row(
  266. children: [
  267. SizedBox(width: 25),
  268. _buildKeyValue(
  269. "",
  270. "1.6Hz",
  271. fontSize: 14,
  272. ),
  273. SizedBox(width: 15),
  274. _buildKeyValue(
  275. "",
  276. "53 Hz",
  277. fontSize: 14,
  278. ),
  279. SizedBox(width: 15),
  280. _buildKeyValue(
  281. "",
  282. "AC",
  283. fontSize: 14,
  284. ),
  285. SizedBox(width: 15),
  286. _buildKeyValue(
  287. "",
  288. "50 Hz",
  289. fontSize: 14,
  290. ),
  291. SizedBox(width: 15),
  292. ],
  293. ),
  294. )
  295. ],
  296. )
  297. ],
  298. ),
  299. ),
  300. ] else ...[
  301. SizedBox(
  302. width: ecgViewWidth,
  303. ),
  304. ],
  305. Expanded(
  306. flex: 3,
  307. child: _buildAnalysisResults(),
  308. ),
  309. ],
  310. ),
  311. ),
  312. );
  313. }
  314. Widget _buildHeader(BuildContext buildContext) {
  315. String heartRate12 = "";
  316. String examData = widget.recordInfo.examData ?? '';
  317. if (examData.isNotEmpty) {
  318. Map<String, dynamic> examDatas = jsonDecode(examData);
  319. if (examDatas.containsKey("HEART12")) {
  320. heartRate12 = examDatas["HEART12"].toString();
  321. _hrController.text = heartRate12;
  322. }
  323. }
  324. return Row(
  325. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  326. children: [
  327. Container(
  328. child: Column(
  329. crossAxisAlignment: CrossAxisAlignment.start,
  330. children: [
  331. Row(
  332. children: [
  333. SizedBox(width: 10),
  334. Row(
  335. children: [
  336. Text(
  337. "走速:",
  338. style: TextStyle(fontSize: 20),
  339. ),
  340. GenericDropdownButton<double>(
  341. items: [12.5, 25, 50],
  342. itemToString: (v) {
  343. return v.toString();
  344. },
  345. initialValue: _paperSpeed,
  346. onChanged: (value) {
  347. _paperSpeed = value;
  348. Get.find<TwelveEcgViewController>()
  349. .changeHorizontalRatio(_paperSpeed / 25);
  350. },
  351. ),
  352. Text(
  353. "mm/s",
  354. style: TextStyle(fontSize: 20),
  355. ),
  356. ],
  357. ),
  358. // _buildKeyValue("走速:", "25 mm/s"),
  359. SizedBox(width: 10),
  360. Row(
  361. children: [
  362. Text(
  363. "增益:",
  364. style: TextStyle(fontSize: 20),
  365. ),
  366. GenericDropdownButton<int>(
  367. items: [5, 10, 20],
  368. itemToString: (v) {
  369. return v.toString();
  370. },
  371. initialValue: _gain,
  372. onChanged: (value) {
  373. _gain = value;
  374. int radio = _gain ~/ 5;
  375. List<int> tempDatas = [];
  376. for (int p in _initEcgDatas) {
  377. tempDatas.add(p * radio);
  378. }
  379. Get.find<TwelveEcgViewController>()
  380. .changeVerticalRatio(tempDatas);
  381. },
  382. ),
  383. Text(
  384. "mm/mV",
  385. style: TextStyle(fontSize: 20),
  386. ),
  387. ],
  388. ),
  389. //_buildKeyValue("增益:", "5 mm/mV"),
  390. SizedBox(width: 10),
  391. _buildKeyValue(
  392. "RV5/SV1:", "${resultConclusion?.Rv5_Sv1_1} Hz"),
  393. SizedBox(width: 10),
  394. _buildKeyValue(
  395. "RV5+SV1:", "${resultConclusion?.Rv5_Sv1_2} Hz"),
  396. SizedBox(width: 10),
  397. ],
  398. ),
  399. Row(
  400. children: [
  401. SizedBox(width: 10),
  402. _buildKeyValue("姓名:", widget.recordInfo.patientName ?? ''),
  403. SizedBox(width: 10),
  404. _buildKeyValue("性别:",
  405. patientGenderConvertAge(widget.recordInfo.patientGender)),
  406. SizedBox(width: 10),
  407. _buildKeyValue(
  408. "年龄:", birthDayConvertAge(widget.recordInfo.birthday)),
  409. SizedBox(width: 10),
  410. if (_templates.isNotEmpty) ...[
  411. Text(
  412. "模板:",
  413. style: TextStyle(fontSize: 20),
  414. ),
  415. GenericDropdownButton<ReportTemplateDTO>(
  416. items: _templates,
  417. initialValue: _currentSelectedTemplate,
  418. itemToString: (t) {
  419. return t.reportTemplateName ?? '';
  420. },
  421. onChanged: (v) {
  422. _currentSelectedTemplate = v;
  423. },
  424. ),
  425. ],
  426. SizedBox(
  427. width: 40,
  428. ),
  429. SizedBox(
  430. child: Text(
  431. "若需要更改图像截取范围,请滑动下方的滑动条",
  432. style: TextStyle(
  433. fontSize: 15,
  434. ),
  435. ),
  436. ),
  437. ],
  438. ),
  439. //SizedBox(height: 5,)
  440. ],
  441. ),
  442. ),
  443. Column(
  444. crossAxisAlignment: CrossAxisAlignment.end,
  445. children: [
  446. Text(
  447. "诊断提示:",
  448. style: TextStyle(fontSize: 16),
  449. ),
  450. ],
  451. ),
  452. ],
  453. );
  454. }
  455. String patientGenderConvertAge(GenderEnum patientGender) {
  456. switch (patientGender) {
  457. case GenderEnum.Male:
  458. return "男";
  459. case GenderEnum.Female:
  460. return "女";
  461. default:
  462. return "";
  463. }
  464. }
  465. String birthDayConvertAge(DateTime? birthday) {
  466. if (birthday == null) {
  467. return "";
  468. }
  469. // 获取当前日期
  470. DateTime now = DateTime.now();
  471. // 计算年龄
  472. int age = calculateAge(birthday, now);
  473. return age.toString();
  474. }
  475. int calculateAge(DateTime birthDate, DateTime now) {
  476. // 计算年份差
  477. int age = now.year - birthDate.year;
  478. // 检查生日是否已经过了当前年份
  479. if (now.month < birthDate.month ||
  480. (now.month == birthDate.month && now.day < birthDate.day)) {
  481. age--;
  482. }
  483. return age;
  484. }
  485. Widget _buildKeyValue(
  486. String key,
  487. String value, {
  488. double fontSize = 20,
  489. }) {
  490. return Row(
  491. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  492. children: [
  493. Text(
  494. key,
  495. style: TextStyle(fontSize: fontSize),
  496. ),
  497. Text(
  498. value,
  499. style: TextStyle(fontSize: fontSize),
  500. ),
  501. ],
  502. );
  503. }
  504. Widget _buildKeyInput(
  505. String key, TextEditingController controller, String? unit) {
  506. final textStyle = const TextStyle(fontSize: 20);
  507. return Container(
  508. margin: EdgeInsets.symmetric(vertical: 2),
  509. child: Row(
  510. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  511. children: [
  512. SizedBox(
  513. width: 110,
  514. child: Text(
  515. key,
  516. style: textStyle,
  517. ),
  518. ),
  519. Expanded(
  520. child: TextField(
  521. decoration: InputDecoration(
  522. contentPadding: EdgeInsets.only(
  523. top: 2.0, bottom: 2.0, left: 10, right: 10), // 设置上下内边距
  524. border: OutlineInputBorder(
  525. borderSide:
  526. BorderSide(color: Colors.black, width: 1.0), // 设置边框颜色和宽度
  527. borderRadius:
  528. BorderRadius.all(Radius.circular(8.0)), // 可选:设置边框圆角
  529. ),
  530. ),
  531. controller: controller,
  532. style: textStyle,
  533. ),
  534. ),
  535. if (unit.isNotNullOrEmpty) ...[
  536. SizedBox(
  537. width: 50,
  538. child: Text(
  539. unit!,
  540. style: textStyle,
  541. ),
  542. ),
  543. ],
  544. ],
  545. ),
  546. );
  547. }
  548. Widget _buildAnalysisResults() {
  549. if (resultConclusion == null) {
  550. return SizedBox();
  551. }
  552. return Container(
  553. margin: EdgeInsets.symmetric(horizontal: 10),
  554. child: Column(
  555. crossAxisAlignment: CrossAxisAlignment.start,
  556. children: [
  557. SizedBox(
  558. height: 5,
  559. ),
  560. Expanded(child: _buildPromptWords()),
  561. ElevatedButton(
  562. onPressed: () {
  563. scaffoldKey.currentState?.openDrawer();
  564. },
  565. child: Text("词条选择"),
  566. ),
  567. _buildKeyInput("心率:", _hrController, " bpm"),
  568. _buildKeyInput("P时限:", _pDurController, " ms"),
  569. _buildKeyInput("QRS时限:", _qRSController, " ms"),
  570. _buildKeyInput("T时限:", _tDurController, " ms"),
  571. _buildKeyInput("PR间期:", _pRController, " ms"),
  572. _buildKeyInput("QT间期:", _qTDurController, " ms"),
  573. _buildKeyInput("QTc间期:", _qTCDurController, " ms"),
  574. _buildKeyInput("P轴:", _pAxisController, " °"),
  575. _buildKeyInput("QRS轴:", _qRSController, " °"),
  576. _buildKeyInput("T轴:", _tAxisController, " °"),
  577. ],
  578. ),
  579. );
  580. }
  581. Widget _buildPromptWords() {
  582. return SizedBox(
  583. child: TextField(
  584. controller: _promptWordsController,
  585. maxLines: 5,
  586. decoration: InputDecoration(
  587. hintText: '请输入提示词',
  588. border: OutlineInputBorder(
  589. borderSide:
  590. BorderSide(color: Colors.black, width: 1.0), // 设置边框颜色和宽度
  591. borderRadius: BorderRadius.all(Radius.circular(8.0)), // 可选:设置边框圆角
  592. ),
  593. ),
  594. ),
  595. );
  596. }
  597. }