tcm_card.dart 38 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150
  1. import 'dart:convert';
  2. import 'dart:math';
  3. import 'package:fis_common/index.dart';
  4. import 'package:fis_jsonrpc/rpc.dart';
  5. import 'package:flutter/material.dart';
  6. import 'package:flutter/services.dart';
  7. import 'package:get/get.dart';
  8. import 'package:vitalapp/architecture/storage/text_storage.dart';
  9. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  10. import 'package:vitalapp/components/alert_dialog.dart';
  11. import 'package:vitalapp/components/appbar.dart';
  12. import 'package:vitalapp/components/button.dart';
  13. import 'package:vitalapp/components/dialog_input.dart';
  14. import 'package:vitalapp/components/dialog_select.dart';
  15. import 'package:vitalapp/components/dynamic_drawer.dart';
  16. import 'package:vitalapp/consts/styles.dart';
  17. import 'package:vitalapp/global.dart';
  18. import 'package:vitalapp/managers/interfaces/cachedRecord.dart';
  19. import 'package:vitalapp/managers/interfaces/template.dart';
  20. import 'package:vitalapp/pages/check/models/form.dart';
  21. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_health_guidance_check_box.dart';
  22. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_input.dart';
  23. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_radio.dart';
  24. import 'package:vitalapp/pages/check/widgets/exam_configurable/exam_radio_score.dart';
  25. import 'package:vitalapp/store/store.dart';
  26. class TCMConstitutionModule extends StatefulWidget {
  27. final String cardKey;
  28. final Future<bool> Function(String, String, dynamic) callBack;
  29. final String? patientCode;
  30. final String? examData;
  31. final bool? isEdit;
  32. const TCMConstitutionModule({
  33. super.key,
  34. required this.cardKey,
  35. required this.callBack,
  36. this.patientCode,
  37. this.examData,
  38. this.isEdit = false,
  39. });
  40. @override
  41. State<TCMConstitutionModule> createState() => _ConfigurableFormState();
  42. }
  43. class _ConfigurableFormState extends State<TCMConstitutionModule> {
  44. /// 当前最新的模板的键值对
  45. Map<String, dynamic> templateRelation = {};
  46. bool isFirstEnter = true;
  47. String templateCode = '';
  48. /// 当前模板数据
  49. List<FormObject> currentTemplate = [];
  50. /// 当前title的下标
  51. int currentTitleIndex = 0;
  52. Map<String, dynamic> formValue = {};
  53. var scaffoldKey = GlobalKey<ScaffoldState>();
  54. List<TCMData> resourceInfo = [];
  55. final _templateManager = Get.find<ITemplateManager>();
  56. final _cachedRecordManager = Get.find<ICachedRecordManager>();
  57. Map currentTable = {};
  58. late MapEntry<String, dynamic> currentResult;
  59. late List<MapEntry<String, dynamic>> compatibleResult =
  60. <MapEntry<String, dynamic>>[];
  61. late List<String> unansweredQuestions = <String>[];
  62. List<String> storeTypeList = [
  63. 'Qi_Score',
  64. 'Yang_Score',
  65. 'Yin_Score',
  66. 'Tan_Score',
  67. 'Shi_Score',
  68. 'Xue_Score',
  69. 'Qiyu_Score',
  70. 'Te_Score',
  71. 'Ping_Score'
  72. ];
  73. Map<String, dynamic> storeTypeMap = {
  74. "Qi_Score": '气虚质',
  75. "Yang_Score": '阳虚质',
  76. "Yin_Score": '阴虚质',
  77. "Tan_Score": '痰湿质',
  78. "Shi_Score": '湿热质',
  79. "Xue_Score": '血瘀质',
  80. "Qiyu_Score": '气郁质',
  81. "Te_Score": '特禀质',
  82. "Ping_Score": '平和质',
  83. };
  84. Map<String, dynamic> storeTypeMapDefaultTitle = {
  85. "Qi_Score": [2, 3, 4, 14],
  86. "Yang_Score": [11, 12, 13, 29],
  87. "Yin_Score": [10, 21, 26, 31],
  88. "Tan_Score": [9, 16, 28, 32],
  89. "Shi_Score": [23, 25, 27, 30],
  90. "Xue_Score": [19, 22, 24, 33],
  91. "Qiyu_Score": [5, 6, 7, 8],
  92. "Te_Score": [15, 17, 18, 20],
  93. "Ping_Score": [1, 2, 4, 5, 13],
  94. };
  95. Map<String, String> typeMap = {
  96. 'Peaceful_Quality': "Ping_Score",
  97. "Yang_Deficiency_Substance": "Yang_Score",
  98. "Idiosyncratic_Quality": "Te_Score",
  99. "Qi_Stagnation_Constitution": "Qiyu_Score",
  100. "Blood_Stasis_Substance": "Xue_Score",
  101. "Damp-heat_constitution": "Shi_Score",
  102. "Phlegm-dampness_constitution": "Tan_Score",
  103. "Yin_Deficiency_Substance": "Yin_Score",
  104. "Qi_Deficiency_Constitution": "Qi_Score",
  105. };
  106. String resultConstitution = "";
  107. late int? selectedIndex = -1; // 用于跟踪选中的项目索引
  108. @override
  109. void initState() {
  110. selectedIndex = -1;
  111. super.initState();
  112. initReadCached();
  113. getResourceInfo();
  114. unansweredQuestions.clear();
  115. // compatibleResult?.clear();
  116. WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
  117. if (mounted) {
  118. initTemplate();
  119. }
  120. });
  121. }
  122. Future<void> initReadCached() async {
  123. TextStorage cachedRecord = TextStorage(
  124. fileName: 'ZYTZ',
  125. directory: "patient/${widget.patientCode}",
  126. );
  127. String? value = await cachedRecord.read();
  128. if (value == null) {
  129. formValue = {};
  130. Store.resident.handleSaveMedicalData(jsonEncode(formValue));
  131. return;
  132. }
  133. Store.resident.handleSaveMedicalData(value);
  134. formValue = jsonDecode(value);
  135. }
  136. Future<bool?> saveCachedRecord() async {
  137. // deleteDirectory();
  138. Store.resident.handleSaveMedicalData(jsonEncode(formValue));
  139. TextStorage cachedRecord = TextStorage(
  140. fileName: 'ZYTZ',
  141. directory: "patient/${widget.patientCode}",
  142. );
  143. return cachedRecord.save(jsonEncode(formValue));
  144. }
  145. Future<bool?> deleteDirectory() async {
  146. TextStorage cachedRecord = TextStorage(
  147. fileName: 'ZYTZ',
  148. directory: "patient/${widget.patientCode}",
  149. );
  150. return cachedRecord.deleteDirectory();
  151. }
  152. Future<void> getResourceInfo() async {
  153. if (FPlatform.isWindows) {
  154. return;
  155. }
  156. TextStorage resourceInfoCacheRecord = TextStorage(
  157. fileName: "healthGuidance",
  158. directory: "HealthGuidance/temlpate",
  159. );
  160. String? value = await resourceInfoCacheRecord.read();
  161. if (value != null) {
  162. var json = jsonDecode(value);
  163. resourceInfo =
  164. List<TCMData>.from(json['TCMDesc'].map((x) => TCMData.fromJson(x)));
  165. }
  166. }
  167. @override
  168. Widget build(BuildContext context) {
  169. return Scaffold(
  170. key: scaffoldKey,
  171. appBar: VAppBar(
  172. titleText: "中医体质",
  173. actions: (currentTitleIndex == 0 && !isFirstEnter)
  174. ? [
  175. TextButton(
  176. onPressed: () async {
  177. final result = await VDialogSelect<String, String>(
  178. title: "体质类型",
  179. source: storeTypeList,
  180. valueGetter: (data) => data,
  181. labelGetter: (data) => storeTypeMap[data],
  182. initialValue: formValue["PhysicalConclusion"],
  183. ).show();
  184. if (result != null) {
  185. Random random = Random();
  186. formValue.clear();
  187. formValue["PhysicalConclusion"] = result;
  188. if (result == "Ping_Score") {
  189. for (var i = 1; i < 34; i++) {
  190. if (i == 1) {
  191. int randomNumber = 3 + random.nextInt(3);
  192. formValue["qusition$i"] = randomNumber.toString();
  193. } else {
  194. int randomNumber = 1 + random.nextInt(2);
  195. formValue["qusition$i"] = randomNumber.toString();
  196. }
  197. }
  198. } else {
  199. for (var i = 1; i < 34; i++) {
  200. var questions =
  201. storeTypeMapDefaultTitle[result] as List<int>;
  202. if (questions.contains(i)) {
  203. int randomNumber = 3 + random.nextInt(3);
  204. formValue["qusition$i"] = randomNumber.toString();
  205. } else {
  206. int randomNumber = 1 + random.nextInt(2);
  207. formValue["qusition$i"] = randomNumber.toString();
  208. }
  209. }
  210. }
  211. for (var currentFormObject
  212. in currentTemplate[currentTitleIndex].children!) {
  213. optionUpdate(currentFormObject,
  214. formValue[currentFormObject.key]);
  215. }
  216. var currentFormObject =
  217. currentTemplate[currentTitleIndex].children!.first;
  218. optionUpdate(
  219. currentFormObject, formValue[currentFormObject.key]);
  220. }
  221. setState(() {});
  222. },
  223. child: Text(
  224. "选择体质",
  225. style: TextStyle(
  226. color: Colors.white,
  227. fontSize: 24,
  228. ),
  229. ),
  230. ),
  231. ]
  232. : null,
  233. ),
  234. resizeToAvoidBottomInset: false,
  235. body: (!widget.isEdit!) && isFirstEnter
  236. ? Container(
  237. decoration: const BoxDecoration(
  238. image: DecorationImage(
  239. image:
  240. AssetImage("assets/images/healthGuidanceBackground.png"),
  241. fit: BoxFit.cover,
  242. ),
  243. ),
  244. child: Column(
  245. children: [
  246. const SizedBox(
  247. height: 120,
  248. ),
  249. Center(
  250. child: Container(
  251. decoration: BoxDecoration(
  252. border: Border.all(color: Colors.black, width: 1)),
  253. padding: const EdgeInsets.all(10), // 添加内边距
  254. child: Container(
  255. decoration: BoxDecoration(
  256. border: Border.all(color: Colors.black, width: 1)),
  257. padding: const EdgeInsets.all(10), // 添加内边距
  258. child: const SizedBox(
  259. width: 900,
  260. child: Text(
  261. '《中医体质分类与判定标准》于2009年4月9日,由中华中医药学会正式发布,该标准旨在为体质辨识及中医体质相关疾病的防治、养生保健、健康管理提供依据,使体质分类科学化、规范化,是我国第一部指导和规范中医体质研究及应用的文件。适用于从事中医体质研究的中医临床医生、科研人员及相关管理人员,并可作为临床实践、判定规范及质量评定的重要参考依据。其中将中国人的体质分为九种,是体质分类的一种规范。具体有平和质、阳虚质、阴虚质、气虚质、痰湿质、湿热质、气郁质、血瘀质、特禀质。',
  262. style: TextStyle(fontSize: 24),
  263. ),
  264. ),
  265. ),
  266. ),
  267. ),
  268. const SizedBox(
  269. height: 120,
  270. ),
  271. VButton(
  272. label: "开始检测",
  273. onTap: () {
  274. setState(() {
  275. isFirstEnter = false;
  276. });
  277. },
  278. ),
  279. ],
  280. ),
  281. )
  282. : Column(
  283. mainAxisAlignment: MainAxisAlignment.start,
  284. crossAxisAlignment: CrossAxisAlignment.start,
  285. children: [
  286. const SizedBox(
  287. height: 20,
  288. ),
  289. if (currentTitleIndex == 1)
  290. Column(
  291. mainAxisAlignment: MainAxisAlignment.start,
  292. crossAxisAlignment: CrossAxisAlignment.start,
  293. children: [
  294. Row(
  295. children: [
  296. const SizedBox(
  297. width: 80,
  298. ),
  299. Text(
  300. '当前体质判定为:${storeTypeMap[currentResult.key]} (${jsonDecode(currentResult.value)["Result"]} ${jsonDecode(currentResult.value)["Value"]}分)',
  301. style: const TextStyle(
  302. fontSize: 28,
  303. color: Colors.black,
  304. ),
  305. )
  306. ],
  307. ),
  308. if (compatibleResult.isNotEmpty)
  309. Row(
  310. children: const [
  311. SizedBox(
  312. width: 80,
  313. ),
  314. Text(
  315. '兼夹体质有:',
  316. style: TextStyle(
  317. fontSize: 25,
  318. color: Colors.black,
  319. ),
  320. ),
  321. ],
  322. ),
  323. if (compatibleResult.isNotEmpty)
  324. ListView.builder(
  325. shrinkWrap: true,
  326. itemCount: (compatibleResult.length / 3).ceil(),
  327. itemBuilder: (context, index) {
  328. return Row(
  329. children: [
  330. const SizedBox(width: 120),
  331. for (int i = index * 5;
  332. i < (index + 1) * 5;
  333. i++)
  334. if (i < compatibleResult.length)
  335. Column(
  336. crossAxisAlignment:
  337. CrossAxisAlignment.start,
  338. children: [
  339. Text(
  340. '${storeTypeMap[compatibleResult[i].key]} (${jsonDecode(compatibleResult[i].value)["Result"]} ${jsonDecode(compatibleResult[i].value)["Value"]}分) ',
  341. style: const TextStyle(
  342. fontSize: 20,
  343. color: Colors.black,
  344. ),
  345. softWrap: true,
  346. ),
  347. ],
  348. ),
  349. if ((index + 1) * 3 < compatibleResult.length)
  350. Container(height: 20), // 控制换行间距
  351. ],
  352. );
  353. },
  354. ),
  355. ],
  356. ),
  357. Expanded(
  358. child: Row(
  359. mainAxisAlignment: MainAxisAlignment.start,
  360. crossAxisAlignment: CrossAxisAlignment.start,
  361. children: [
  362. // SizedBox(width: widget.isEdit! ? 90 : 50),
  363. _buildContent(),
  364. // SizedBox(width: widget.isEdit! ? 90 : 50),
  365. ],
  366. ),
  367. ),
  368. Row(
  369. mainAxisAlignment: MainAxisAlignment.center,
  370. crossAxisAlignment: CrossAxisAlignment.center,
  371. children: [
  372. if (currentTitleIndex == 0)
  373. VButton(
  374. onTap: _buildCompute,
  375. child: Row(
  376. mainAxisAlignment: MainAxisAlignment.center,
  377. children: [
  378. Text(
  379. "计算体质辨识结果",
  380. style: const TextStyle(
  381. fontSize: 20,
  382. ),
  383. ),
  384. ],
  385. ),
  386. ),
  387. const SizedBox(
  388. width: 10,
  389. ),
  390. if (currentTitleIndex == 1)
  391. VButton(
  392. child: Row(
  393. mainAxisAlignment: MainAxisAlignment.center,
  394. children: [
  395. Text(widget.isEdit! ? "修改" : "返回",
  396. style: TextStyle(fontSize: 20)),
  397. ],
  398. ),
  399. onTap: () {
  400. setState(() {
  401. // if (!widget.isEdit!) formValue.clear();
  402. currentTitleIndex = 0;
  403. });
  404. },
  405. ),
  406. const SizedBox(
  407. width: 10,
  408. ),
  409. if (currentTitleIndex == 1)
  410. VButton(
  411. child: Row(
  412. mainAxisAlignment: MainAxisAlignment.center,
  413. children: [
  414. Text("提交", style: const TextStyle(fontSize: 20)),
  415. ],
  416. ),
  417. onTap: () async {
  418. final result = await widget.callBack(
  419. widget.cardKey,
  420. templateCode,
  421. jsonEncode(formValue),
  422. );
  423. if (result) {
  424. setState(() {
  425. if (!widget.isEdit!) formValue.clear();
  426. currentTitleIndex = 0;
  427. if (!FPlatform.isWindows) {
  428. deleteDirectory();
  429. }
  430. });
  431. }
  432. Get.back();
  433. },
  434. ),
  435. ],
  436. ),
  437. const SizedBox(
  438. height: 10,
  439. ),
  440. ],
  441. ),
  442. );
  443. }
  444. String getHealthGuidance(String key) {
  445. String? value =
  446. resourceInfo.firstWhereOrNull((element) => element.key == key)?.value;
  447. return value ?? '';
  448. }
  449. /// 主页面
  450. Widget _buildContent() {
  451. return Expanded(
  452. child: flowCardList(),
  453. );
  454. }
  455. @override
  456. void dispose() {
  457. super.dispose();
  458. }
  459. Future<void> initTemplate() async {
  460. Store.app.busy = true;
  461. await fetchTemplateIndex();
  462. await fetchTemplate(widget.cardKey);
  463. await fetchTemplateData();
  464. Store.app.busy = false;
  465. }
  466. /// 读取体检的缓存
  467. Future<String?> readCachedCheck() async {
  468. if (widget.patientCode == null) {
  469. return null;
  470. }
  471. String? value = await _cachedRecordManager.readCachedRecord(
  472. widget.cardKey,
  473. widget.patientCode!,
  474. 'FollowUp',
  475. );
  476. return value;
  477. }
  478. Future<void> fetchTemplateIndex() async {
  479. try {
  480. /// 获取模板的键值对
  481. String? templates;
  482. templates =
  483. await _templateManager.readTemplateRelation('templateRelation');
  484. templateRelation = jsonDecode(templates!);
  485. setState(() {});
  486. } catch (error) {
  487. print('发生错误: $error');
  488. }
  489. }
  490. Future<void> fetchTemplateData() async {
  491. if (widget.examData?.isNotEmpty ?? false) {
  492. formValue = jsonDecode(widget.examData!);
  493. var jsonKey = formValue["PhysicalConclusion"];
  494. currentResult = MapEntry(jsonKey, formValue[jsonKey]);
  495. compatibleResult = calculateAllResylts();
  496. if (widget.isEdit!) {
  497. currentTitleIndex = 1;
  498. }
  499. setState(() {});
  500. return;
  501. }
  502. formValue.forEach(
  503. (key, value) {
  504. if (value is List<String>) {
  505. formValue[key] = List<String>.from(formValue[key]);
  506. } else if (value is List<Map>) {
  507. formValue[key] = List<Map>.from(formValue[key]);
  508. }
  509. },
  510. );
  511. // currentResult = formValue[formValue["PhysicalConclusion"].value];
  512. // if (widget.isEdit!) {
  513. // currentTitleIndex = 1;
  514. // }
  515. setState(() {});
  516. }
  517. Future<void> fetchTemplate(String key) async {
  518. try {
  519. if (templateRelation[key] == null) {
  520. currentTemplate = [];
  521. setState(() {});
  522. if (!FPlatform.isWindows) {
  523. return;
  524. }
  525. }
  526. List<Map<String, dynamic>> list = [];
  527. String? template;
  528. if (templateRelation.containsKey(key)) {
  529. templateCode = templateRelation[key]!;
  530. template = await _templateManager.readTemplate(templateCode);
  531. }
  532. if (template == null) {
  533. var json = await loadJsonFromAssets('assets/templates/${key}.json');
  534. list = jsonDecode(json)["Content"].cast<Map<String, dynamic>>();
  535. } else {
  536. var templateContent =
  537. TemplateDTO.fromJson(jsonDecode(template)).templateContent!;
  538. list = jsonDecode(templateContent).cast<Map<String, dynamic>>();
  539. }
  540. for (var i in list) {
  541. if (i['children'] != null) {
  542. List<FormObject> currentChildren = [];
  543. for (var j in i['children']) {
  544. currentChildren.add(FormObject.fromJson(j));
  545. }
  546. i['children'] = currentChildren;
  547. }
  548. var item = FormObject.fromJson(i);
  549. currentTemplate.add(item);
  550. }
  551. // if (widget.cardKey == 'LNRZYYJKGLFWJL') {
  552. // for (var element in storeTypeList) {
  553. // formValue[element] =
  554. // calculatePhysicalFitnessScore(element, element == 'Ping_Score');
  555. // }
  556. // }
  557. setState(() {});
  558. } catch (error) {
  559. print('发生错误: $error');
  560. }
  561. }
  562. Future<String> loadJsonFromAssets(String filePath) async {
  563. String jsonString = await rootBundle.loadString(filePath);
  564. return jsonString;
  565. }
  566. Widget buildSingleItem(Widget item, int span) {
  567. return Column(
  568. children: [
  569. FractionallySizedBox(
  570. widthFactor: span == 24 ? 1 : 0.5,
  571. child: item,
  572. ),
  573. ],
  574. );
  575. }
  576. Widget buildWidget(FormObject? currentFormObject) {
  577. Map<String, Widget Function(FormObject)> widgetMap = {
  578. 'input': _buildInput,
  579. 'radio': _buildRadio,
  580. 'radioScore': _buildRadioScore,
  581. 'healthGuidanceCheckBox': _buildHealthGuidanceCheckBox,
  582. };
  583. Widget Function(FormObject) builder =
  584. widgetMap[currentFormObject?.type] ?? _buildInput;
  585. return builder(currentFormObject!);
  586. }
  587. Widget flowCardList() {
  588. int itemCount = 0;
  589. bool currentTemplateOptionsIsNotEmpty = false;
  590. if (currentTemplate.isNotEmpty) {
  591. itemCount = currentTemplate[currentTitleIndex].children?.length ?? 0;
  592. currentTemplateOptionsIsNotEmpty =
  593. currentTemplate[currentTitleIndex].options?.isNotEmpty ?? false;
  594. }
  595. List<Widget> items = List.generate(itemCount, (index) {
  596. FormObject? currentFormObject =
  597. currentTemplate[currentTitleIndex].children?[index];
  598. int span = currentFormObject?.span ?? 12;
  599. //父结构的options不等于空或null
  600. //子结构的options若是无值则取父类的options值
  601. if (currentTemplateOptionsIsNotEmpty) {
  602. if (currentFormObject!.options!.isEmpty)
  603. currentFormObject.options =
  604. currentTemplate[currentTitleIndex].options;
  605. }
  606. return buildSingleItem(buildWidget(currentFormObject), span);
  607. });
  608. final ScrollController scrollController = ScrollController();
  609. return Scrollbar(
  610. controller: scrollController,
  611. // thumbVisibility: false,
  612. // isAlwaysShown: false,
  613. child: ListView(
  614. controller: scrollController,
  615. padding: EdgeInsets.symmetric(horizontal: 50),
  616. children: [
  617. Container(
  618. alignment: Alignment.topCenter,
  619. child: Wrap(
  620. runSpacing: currentTitleIndex == 1 ? 5 : 10, // 纵向元素间距
  621. alignment: WrapAlignment.start,
  622. children: items,
  623. ),
  624. ),
  625. if (currentTitleIndex == 1)
  626. Text(
  627. getHealthGuidance(currentResult.key),
  628. style: TextStyle(fontSize: 18),
  629. ),
  630. ],
  631. ),
  632. );
  633. }
  634. /// title标签
  635. _buildCompute() {
  636. var questionsAnsweredNumber = formValue.entries
  637. .where((entry) => entry.key.contains("qusition"))
  638. .length; //获取已完成答题数量
  639. unansweredQuestions.clear();
  640. if (widget.cardKey == 'LNRZYYJKGLFWJL' &&
  641. currentTitleIndex == 0 &&
  642. questionsAnsweredNumber >= 33) {
  643. List<MapEntry<String, dynamic>> result = caculationConclusion();
  644. String? guidanceResult = hasGuidanceBeenProvided();
  645. String message = '';
  646. String title = '';
  647. bool isExistConflict = false;
  648. if (result.length == 1) {
  649. currentResult = result.first;
  650. formValue["PhysicalConclusion"] = currentResult.key;
  651. //直接进入中医药保健指导
  652. setState(() {
  653. compatibleResult = calculateAllResylts();
  654. currentTitleIndex = 1;
  655. });
  656. } else {
  657. if (result.length > 1) {
  658. //弹出选择框
  659. isExistConflict = result
  660. .where((entry) =>
  661. entry.key == "Yang_Score" || entry.key == "Yin_Score")
  662. .length ==
  663. 2;
  664. if (isExistConflict) {
  665. message = '判定结果既是阴虚又是阳虚的矛盾判定结果,需重新填写问题:10、11、12、13、21、26、29、31';
  666. title = '体质冲突';
  667. var list = [10, 11, 12, 13, 21, 26, 29, 31];
  668. for (var element in list) {
  669. unansweredQuestions.add('qusition$element');
  670. }
  671. } else {
  672. message = '根据答题情况,计算出多个体质,请选择其中一个';
  673. title = '体质选择';
  674. }
  675. }
  676. if (result.isEmpty) {
  677. //无法断体质
  678. message = '请重新完成体质问询或2周后重新采集填写';
  679. title = '无法判断体质';
  680. }
  681. Get.dialog(
  682. VAlertDialog(
  683. title: title,
  684. width: 700,
  685. showCancel: false,
  686. content: Container(
  687. padding: const EdgeInsets.symmetric(horizontal: 24),
  688. height: result.length > 1 && !isExistConflict
  689. ? result.length / 3 * 150
  690. : 150,
  691. alignment: Alignment.center,
  692. child: Column(
  693. children: [
  694. Text(
  695. message,
  696. style: const TextStyle(fontSize: 24),
  697. ),
  698. SizedBox(
  699. height: 10,
  700. ),
  701. if (result.length > 1 && !isExistConflict)
  702. Expanded(
  703. child: SelectResult(
  704. result: result,
  705. guidanceResult: guidanceResult,
  706. selectCheckBoxChange: (value) {
  707. selectedIndex = value;
  708. currentResult = result[value];
  709. formValue["PhysicalConclusion"] = currentResult.key;
  710. },
  711. ),
  712. ),
  713. ],
  714. ),
  715. ),
  716. onConfirm: () {
  717. if (selectedIndex != -1) {
  718. compatibleResult = calculateAllResylts();
  719. currentTitleIndex = 1;
  720. }
  721. Get.back();
  722. setState(() {});
  723. },
  724. ),
  725. barrierDismissible: false,
  726. barrierColor: Colors.black.withOpacity(.4),
  727. );
  728. }
  729. } else {
  730. for (var i = 1; i < 34; i++) {
  731. var isExitsQuestion = formValue.entries
  732. .where((element) => element.key == 'qusition$i')
  733. .isNotEmpty;
  734. if (!isExitsQuestion) {
  735. unansweredQuestions.add('qusition$i');
  736. }
  737. }
  738. setState(() {});
  739. PromptBox.toast('题目未答完整,无法计算体质');
  740. }
  741. }
  742. //验证体质
  743. List<MapEntry<String, dynamic>> caculationConclusion() {
  744. var formValues = formValue.entries
  745. .where((entry) =>
  746. storeTypeList.contains(entry.key) && entry.key != "Ping_Score")
  747. .toList();
  748. var pingResult = jsonDecode(formValue["Ping_Score"])["Result"];
  749. // int score = int.parse(formValue["Ping_Score"]["Value"] ?? 0);
  750. // var pingResult = getJudgmentResult('Ping_Score', score);
  751. if (["是", "基本是", "倾向是"].contains(pingResult)) {
  752. return formValue.entries
  753. .where((element) => element.key == 'Ping_Score')
  754. .toList();
  755. } else {
  756. dynamic maxValue = formValues
  757. .map((entry) => jsonDecode(entry.value)["Value"])
  758. .reduce((a, b) =>
  759. int.parse(a.toString()) > int.parse(b.toString()) ? a : b);
  760. if (int.parse(maxValue.toString()) > 8) {
  761. List<MapEntry<String, dynamic>> keyValuePairs = formValues
  762. .where((entry) => jsonDecode(entry.value)["Value"] == maxValue)
  763. .toList();
  764. return keyValuePairs;
  765. } else {
  766. return List.empty();
  767. }
  768. }
  769. }
  770. String? hasGuidanceBeenProvided() {
  771. var formValues = formValue.entries
  772. .where((entry) => entry.key == 'PhysicalConclusion')
  773. .toList();
  774. if (formValues.isNotEmpty) {
  775. return formValues.first.value;
  776. }
  777. return "";
  778. }
  779. Widget _buildInput(FormObject currentFormObject) {
  780. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  781. Future<void> commonInput() async {
  782. String? result = await VDialogInput(
  783. title: currentFormObject.label,
  784. initialValue: formValue[currentFormObject.key],
  785. ).show();
  786. if (result?.isNotEmpty ?? false) {
  787. formValue[currentFormObject.key!] = result;
  788. currentInputValue = formValue[currentFormObject.key!];
  789. setState(() {});
  790. }
  791. }
  792. return ExamInput(
  793. currentInputValue: currentInputValue,
  794. commonInput: commonInput,
  795. currentFormObject: currentFormObject,
  796. );
  797. }
  798. ///计算满足的所有体质类型
  799. List<MapEntry<String, dynamic>> calculateAllResylts() {
  800. List<MapEntry<String, dynamic>> compatibleResults =
  801. <MapEntry<String, dynamic>>[];
  802. for (var key in storeTypeList) {
  803. var result = jsonDecode(formValue[key])["Result"];
  804. if (["是", "基本是", "倾向是"].contains(result)) {
  805. compatibleResults.add(MapEntry(key, formValue[key]));
  806. }
  807. }
  808. compatibleResults = compatibleResults
  809. .where((element) => element.key != currentResult.key)
  810. .toList();
  811. return compatibleResults;
  812. }
  813. ///中医药健康指导多选框组件
  814. Widget _buildHealthGuidanceCheckBox(FormObject currentFormObject) {
  815. List<Option> options = currentFormObject.options ?? [];
  816. List<dynamic> currentSelectedCheckBox = formValue["HealthGuidance"] ?? [];
  817. var currentScoreKey = currentFormObject.groupKeys != null
  818. ? currentFormObject.groupKeys![0]
  819. : '';
  820. if (currentResult.key != currentScoreKey) {
  821. return Container();
  822. }
  823. int score = jsonDecode(formValue[currentScoreKey])["Value"];
  824. String judgmentResult = jsonDecode(formValue[currentScoreKey])["Result"];
  825. void selectCheckBoxChange(Option e) {
  826. if (currentSelectedCheckBox.contains(e.value)) {
  827. currentSelectedCheckBox.remove(e.value);
  828. } else {
  829. currentSelectedCheckBox.add(e.value ?? '');
  830. }
  831. formValue["HealthGuidance"] = currentSelectedCheckBox;
  832. saveCachedRecord();
  833. setState(() {});
  834. }
  835. return ExamHealthGuidanceWidget(
  836. options: options,
  837. currentSelectedCheckBox: currentSelectedCheckBox,
  838. currentFormObject: currentFormObject,
  839. selectCheckBoxChange: selectCheckBoxChange,
  840. score: score,
  841. judgmentResult: judgmentResult,
  842. );
  843. }
  844. ///识别体质
  845. String getJudgmentResult(String storeKey, int score) {
  846. if (storeKey == 'Ping_Score') {
  847. if (score >= 17) {
  848. bool is8 = true;
  849. bool is10 = true;
  850. for (var e in storeTypeList) {
  851. if (formValue[e] != null && e != 'Ping_Score') {
  852. var otherScore = jsonDecode(formValue[e])["Value"];
  853. if (otherScore > 8) {
  854. if (is8) {
  855. is8 = false;
  856. }
  857. if (otherScore > 10) {
  858. is10 = false;
  859. }
  860. }
  861. if (!is10) {
  862. break;
  863. }
  864. }
  865. }
  866. if (is8) {
  867. return '是';
  868. } else if (is10) {
  869. return '基本是';
  870. }
  871. }
  872. return '否';
  873. } else {
  874. if (score >= 11) {
  875. return '是';
  876. } else if (9 == score || score == 10) {
  877. return '倾向是';
  878. } else {
  879. return '否';
  880. }
  881. }
  882. }
  883. /// 单选框组件
  884. Widget _buildRadio(FormObject currentFormObject) {
  885. List<Option> options = currentFormObject.options ?? [];
  886. String currentSelected = formValue[currentFormObject.key!] ?? "";
  887. void selectRaidoChange(Option e) {
  888. currentSelected = e.value ?? '';
  889. setState(() {
  890. optionUpdate(currentFormObject, currentSelected);
  891. });
  892. }
  893. var decoration = unansweredQuestions.contains(currentFormObject.key!)
  894. ? BoxDecoration(
  895. border: Border.all(color: Colors.red, width: 2),
  896. borderRadius: GlobalStyles.borderRadius,
  897. )
  898. : null;
  899. return Container(
  900. decoration: decoration,
  901. child: ExamRadio(
  902. options: options,
  903. currentFormObject: currentFormObject,
  904. selectRaidoChange: selectRaidoChange,
  905. currentSelected: currentSelected,
  906. ),
  907. );
  908. }
  909. Future<void> optionUpdate(
  910. FormObject formObject, String currentSelected) async {
  911. formValue[formObject.key!] = currentSelected;
  912. if (widget.cardKey == 'LNRZYYJKGLFWJL') {
  913. var pingScore = [
  914. 'qusition1',
  915. 'qusition2',
  916. 'qusition4',
  917. 'qusition5',
  918. 'qusition13'
  919. ];
  920. var isPingScore = pingScore.contains(formObject.key);
  921. int store =
  922. calculatePhysicalFitnessScore(formObject.groupKeys?[0], false);
  923. formValue[formObject.groupKeys?[0]] = jsonEncode({
  924. "Value": store,
  925. "Result": getJudgmentResult(formObject.groupKeys?[0], store),
  926. });
  927. if (isPingScore) {
  928. int store = calculatePhysicalFitnessScore('Ping_Score', true);
  929. formValue['Ping_Score'] = jsonEncode({
  930. "Value": store,
  931. "Result": getJudgmentResult('Ping_Score', store),
  932. });
  933. }
  934. }
  935. saveCachedRecord();
  936. }
  937. ///计算得分 currentScoreKey 当前分数的key isPingScore 是否是平和质
  938. int calculatePhysicalFitnessScore(String currentScoreKey, bool isPingScore) {
  939. int score = 0;
  940. var currentTemplateStoreList = currentTemplate[0].children?.where(
  941. (element) =>
  942. element.groupKeys != null &&
  943. element.groupKeys!.contains(currentScoreKey));
  944. if (!isPingScore) {
  945. for (var element in currentTemplateStoreList!) {
  946. if (formValue[element.key!] != null) {
  947. score += int.parse(formValue[element.key!]!);
  948. }
  949. }
  950. } else {
  951. int index = 0;
  952. for (var element in currentTemplateStoreList!) {
  953. if (index == 0) {
  954. if (formValue[element.key!] != null) {
  955. score += int.parse(formValue[element.key!]!);
  956. }
  957. } else {
  958. score += formValue[element.key!] != null
  959. ? (6 - int.parse(formValue[element.key!]!))
  960. : 0;
  961. }
  962. index++;
  963. }
  964. }
  965. return score;
  966. }
  967. Widget _buildRadioScore(FormObject currentFormObject) {
  968. print(currentFormObject.toJson());
  969. List<Option> options = currentFormObject.options ?? [];
  970. String currentSelected =
  971. formValue[currentFormObject.childrenKey!.first] ?? "";
  972. String currentScore = formValue[currentFormObject.childrenKey!.last] ?? "";
  973. void selectRaidoChange(Option e) {
  974. currentSelected = e.value ?? '';
  975. formValue[currentFormObject.childrenKey!.first] = currentSelected;
  976. setState(() {});
  977. }
  978. void changeScore(String? score) {
  979. currentScore = score ?? '';
  980. formValue[currentFormObject.childrenKey!.last] = currentScore;
  981. setState(() {});
  982. }
  983. return ExamRadioScore(
  984. options: options,
  985. currentFormObject: currentFormObject,
  986. selectRaidoChange: selectRaidoChange,
  987. currentSelected: currentSelected,
  988. changeScore: changeScore,
  989. currentScore: currentScore,
  990. );
  991. }
  992. }
  993. ///多个体质时选项
  994. class SelectResult extends StatefulWidget {
  995. final List<MapEntry<String, dynamic>> result;
  996. final Function selectCheckBoxChange;
  997. final String? guidanceResult;
  998. const SelectResult(
  999. {super.key,
  1000. required this.result,
  1001. required this.selectCheckBoxChange,
  1002. this.guidanceResult});
  1003. @override
  1004. State<SelectResult> createState() => _SelectResultState();
  1005. }
  1006. class _SelectResultState extends State<SelectResult> {
  1007. Map<String, dynamic> storeTypeMap = {
  1008. "Qi_Score": '气虚质',
  1009. "Yang_Score": '阳虚质',
  1010. "Yin_Score": '阴虚质',
  1011. "Tan_Score": '痰虚质',
  1012. "Shi_Score": '湿热质',
  1013. "Xue_Score": '血瘀质',
  1014. "Qiyu_Score": '气郁质',
  1015. "Te_Score": '特禀质',
  1016. "Ping_Score": '平和质',
  1017. };
  1018. late int? selectedIndex = -1; // 用于跟踪选中的项目索引
  1019. @override
  1020. void initState() {
  1021. super.initState();
  1022. selectedIndex = -1;
  1023. }
  1024. @override
  1025. Widget build(BuildContext context) {
  1026. return GridView.builder(
  1027. gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
  1028. crossAxisCount: 3,
  1029. crossAxisSpacing: 10,
  1030. mainAxisSpacing: 10,
  1031. childAspectRatio: 3,
  1032. ),
  1033. itemCount: widget.result.length,
  1034. itemBuilder: (context, index) {
  1035. if (widget.result[index].key == widget.guidanceResult) {
  1036. widget.selectCheckBoxChange(index);
  1037. selectedIndex = index;
  1038. }
  1039. return Row(
  1040. children: [
  1041. Radio(
  1042. value: index,
  1043. groupValue: selectedIndex,
  1044. onChanged: (value) {
  1045. widget.selectCheckBoxChange(value);
  1046. setState(() {
  1047. selectedIndex = value;
  1048. });
  1049. }),
  1050. Text(
  1051. "${storeTypeMap[widget.result[index].key]} (${jsonDecode(widget.result[index].value)["Result"]} ${jsonDecode(widget.result[index].value)["Value"]}分)",
  1052. style: TextStyle(fontSize: 22),
  1053. ),
  1054. SizedBox(
  1055. width: 5,
  1056. ),
  1057. ],
  1058. );
  1059. });
  1060. }
  1061. @override
  1062. void dispose() {
  1063. super.dispose();
  1064. }
  1065. }
  1066. class TCMData {
  1067. String key;
  1068. String value;
  1069. TCMData({required this.key, required this.value});
  1070. factory TCMData.fromJson(Map<String, dynamic> json) {
  1071. return TCMData(
  1072. key: json['Key'],
  1073. value: json['Value'],
  1074. );
  1075. }
  1076. }