configurable_card.dart 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678
  1. import 'dart:convert';
  2. import 'package:fis_jsonrpc/rpc.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:get/get.dart';
  5. import 'package:vnoteapp/components/dialog_input.dart';
  6. import 'package:vnoteapp/components/dialog_number.dart';
  7. import 'package:vnoteapp/components/dynamic_drawer.dart';
  8. import 'package:vnoteapp/managers/interfaces/template.dart';
  9. import 'package:vnoteapp/pages/check/models/form.dart';
  10. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_blood_pressure.dart';
  11. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_body_temperature.dart';
  12. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_check_box.dart';
  13. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_input.dart';
  14. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_number_input.dart';
  15. import 'dart:math' as math;
  16. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_radio.dart';
  17. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_radio_score.dart';
  18. const double _width = 170;
  19. const double _height = 38;
  20. String tw = '00.0';
  21. class ConfigurableCard extends StatefulWidget {
  22. final String cardKey;
  23. final Function(String, String, dynamic) callBack;
  24. const ConfigurableCard({
  25. super.key,
  26. required this.cardKey,
  27. required this.callBack,
  28. });
  29. @override
  30. State<ConfigurableCard> createState() => _ConfigurableFormState();
  31. }
  32. class _ConfigurableFormState extends State<ConfigurableCard> {
  33. /// 当前最新的模板的键值对
  34. Map<String, dynamic> templateRelation = {};
  35. /// 当前模板数据
  36. List<FormObject> currentTemplate = [];
  37. /// 当前title的下标
  38. int currentTitleIndex = 0;
  39. Map<String, dynamic> formValue = {};
  40. var scaffoldKey = GlobalKey<ScaffoldState>();
  41. final _templateManager = Get.find<ITemplateManager>();
  42. final arrowHeight = math.tan(120 / 180) * (_height / 2);
  43. @override
  44. void initState() {
  45. super.initState();
  46. fetchTemplateIndex();
  47. }
  48. @override
  49. void dispose() {
  50. super.dispose();
  51. }
  52. Future<void> fetchTemplateIndex() async {
  53. try {
  54. /// 获取模板的键值对
  55. String? templates;
  56. templates = await _templateManager.readTemplate('templateRelation');
  57. templateRelation = jsonDecode(templates!);
  58. fetchTemplate(widget.cardKey);
  59. } catch (error) {
  60. print('发生错误: $error');
  61. }
  62. }
  63. Future<void> fetchTemplate(String key) async {
  64. try {
  65. if (templateRelation[key] == null) {
  66. currentTemplate = [];
  67. setState(() {});
  68. return;
  69. }
  70. var template =
  71. await _templateManager.readTemplate(templateRelation[key]!);
  72. String templateContent =
  73. TemplateDTO.fromJson(jsonDecode(template!)).templateContent!;
  74. List<Map<String, dynamic>> list =
  75. jsonDecode(templateContent).cast<Map<String, dynamic>>();
  76. for (var i in list) {
  77. if (i['children'] != null) {
  78. List<FormObject> currentChildren = [];
  79. for (var j in i['children']) {
  80. currentChildren.add(FormObject.fromJson(j));
  81. }
  82. i['children'] = currentChildren;
  83. }
  84. currentTemplate.add(FormObject.fromJson(i));
  85. }
  86. // var list = jsonDecode(templateContent).cast<Map<String, dynamic>>();
  87. // if (list == null) {
  88. // currentTemplate = [];
  89. // } else {
  90. // currentTemplate = updateChildren(list);
  91. // }
  92. // TextStorage t = TextStorage(
  93. // fileName: key,
  94. // directory: "template",
  95. // );
  96. // await t.save(jsonEncode(currentTemplate));
  97. print(currentTemplate);
  98. setState(() {});
  99. } catch (error) {
  100. print('发生错误: $error');
  101. }
  102. }
  103. @override
  104. Widget build(BuildContext context) {
  105. return Scaffold(
  106. key: scaffoldKey,
  107. endDrawer: VDynamicDrawerWrapper(scaffoldKey: scaffoldKey),
  108. resizeToAvoidBottomInset: false,
  109. body: Column(
  110. children: [
  111. Container(
  112. width: double.infinity,
  113. padding: const EdgeInsets.all(20),
  114. child: _buildTitleList(),
  115. ),
  116. Expanded(
  117. child: Stack(
  118. children: [
  119. Row(
  120. mainAxisAlignment: MainAxisAlignment.start,
  121. crossAxisAlignment: CrossAxisAlignment.start,
  122. children: [
  123. _buildDiagram(),
  124. _buildContent(),
  125. ],
  126. ),
  127. _buildPositionedButton(
  128. currentTitleIndex == currentTemplate.length - 1
  129. ? "保存"
  130. : "下一步",
  131. () async {
  132. if (currentTitleIndex == currentTemplate.length - 1) {
  133. widget
  134. .callBack(
  135. widget.cardKey,
  136. templateRelation[widget.cardKey]!,
  137. jsonEncode(formValue),
  138. )
  139. .call();
  140. return;
  141. }
  142. currentTitleIndex++;
  143. setState(() {});
  144. },
  145. right: 0,
  146. ),
  147. ],
  148. ),
  149. )
  150. ],
  151. ),
  152. );
  153. }
  154. Widget buildSingleItem(Widget item, int span) {
  155. return FractionallySizedBox(
  156. widthFactor: span == 24 ? 1 : 0.5,
  157. child: item,
  158. );
  159. }
  160. Widget buildWidget(FormObject? currentFormObject) {
  161. Map<String, Widget Function(FormObject)> widgetMap = {
  162. 'checkbox': _buildCheckBox,
  163. 'numberInput': _buildNumberInput,
  164. 'input': _buildInput,
  165. 'radio': _buildRadio,
  166. 'radioScore': _buildRadioScore,
  167. 'bloodPressure': _buildBloodPressure,
  168. 'bodyTemperature': _buildBodyTemperature,
  169. };
  170. Widget Function(FormObject) builder =
  171. widgetMap[currentFormObject?.type] ?? _buildInput;
  172. return builder(currentFormObject!);
  173. }
  174. Widget waterCardList() {
  175. int itemCount = 0;
  176. if (currentTemplate.isNotEmpty) {
  177. itemCount = currentTemplate[currentTitleIndex].children?.length ?? 0;
  178. }
  179. List<Widget> items = List.generate(itemCount, (index) {
  180. FormObject? currentFormObject =
  181. currentTemplate[currentTitleIndex].children?[index];
  182. int span = currentFormObject?.span ?? 12;
  183. return buildSingleItem(buildWidget(currentFormObject), span);
  184. });
  185. return Scrollbar(
  186. thumbVisibility: true,
  187. child: SingleChildScrollView(
  188. child: Container(
  189. alignment: Alignment.topCenter,
  190. padding: const EdgeInsets.all(15),
  191. child: Wrap(
  192. runSpacing: 20, // 纵向元素间距
  193. alignment: WrapAlignment.start,
  194. children: items,
  195. ),
  196. ),
  197. ),
  198. );
  199. }
  200. /// title标签
  201. Widget _buildTitleList() {
  202. return Wrap(
  203. runSpacing: 10, // 设置子小部件之间的间距
  204. alignment: WrapAlignment.start,
  205. children: currentTemplate.asMap().entries.map(
  206. (e) {
  207. /// TODO 这边需要改下
  208. MaterialColor currentColors = Colors.grey;
  209. e.value.children?.forEach((element) {
  210. if (formValue.containsKey(element.key)) {
  211. currentColors = Colors.green;
  212. }
  213. });
  214. return Transform.translate(
  215. offset: Offset((-arrowHeight + 2) * e.key, 0),
  216. child: TitleClipRect(
  217. title: e.value.label ?? '',
  218. color: currentTitleIndex == e.key ? null : currentColors,
  219. arrowHeight: arrowHeight,
  220. clickTitle: () {
  221. currentTitleIndex = e.key;
  222. setState(() {});
  223. },
  224. ),
  225. );
  226. },
  227. ).toList(),
  228. );
  229. }
  230. /// 示意图
  231. Widget _buildDiagram() {
  232. return Expanded(
  233. flex: 1,
  234. child: Stack(
  235. children: [
  236. Image.asset(
  237. 'assets/images/exam/test.png',
  238. height: double.infinity,
  239. fit: BoxFit.fitWidth, // 设置图像的适应方式
  240. ),
  241. _buildPositionedButton(
  242. currentTitleIndex == 0 ? "返回" : "上一步",
  243. () async {
  244. if (currentTitleIndex == 0) {
  245. Get.back();
  246. } else {
  247. currentTitleIndex--;
  248. setState(() {});
  249. }
  250. },
  251. ),
  252. ],
  253. ),
  254. );
  255. }
  256. /// 按钮
  257. Widget _buildPositionedButton(String title, Function onTap,
  258. {double? right, double? left}) {
  259. return Positioned(
  260. right: right,
  261. left: left,
  262. bottom: 0,
  263. child: Container(
  264. width: 150,
  265. padding: const EdgeInsets.symmetric(vertical: 30),
  266. child: Align(
  267. alignment: Alignment.bottomCenter,
  268. child: SizedBox(
  269. width: 100,
  270. height: 100,
  271. child: ElevatedButton(
  272. style: ButtonStyle(
  273. backgroundColor: MaterialStatePropertyAll(
  274. Colors.white.withOpacity(
  275. .8,
  276. ),
  277. ),
  278. shape: MaterialStatePropertyAll(
  279. RoundedRectangleBorder(
  280. borderRadius: BorderRadius.circular(50),
  281. ),
  282. ),
  283. ),
  284. onPressed: () => onTap.call(),
  285. child: Text(
  286. title,
  287. style:
  288. const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
  289. ),
  290. ),
  291. ),
  292. ),
  293. ),
  294. );
  295. }
  296. /// 主页面
  297. Widget _buildContent() {
  298. return Expanded(
  299. flex: 2,
  300. child: waterCardList(),
  301. );
  302. }
  303. /// 多选框组件
  304. Widget _buildCheckBox(FormObject currentFormObject) {
  305. List<Option> options = currentFormObject.options ?? [];
  306. List<String> currentSelectedCheckBox =
  307. formValue[currentFormObject.key!] ?? [];
  308. void selectCheckBoxChange(Option e) {
  309. if (currentSelectedCheckBox.contains(e.value)) {
  310. currentSelectedCheckBox.remove(e.value);
  311. } else {
  312. currentSelectedCheckBox.add(e.value ?? '');
  313. }
  314. formValue[currentFormObject.key!] = currentSelectedCheckBox;
  315. setState(() {});
  316. }
  317. return ExamCheckBox(
  318. options: options,
  319. currentSelectedCheckBox: currentSelectedCheckBox,
  320. currentFormObject: currentFormObject,
  321. selectCheckBoxChange: selectCheckBoxChange,
  322. );
  323. }
  324. /// 数字输入框组件
  325. Widget _buildNumberInput(FormObject currentFormObject) {
  326. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  327. Future<void> commonInput() async {
  328. String? result = await VDialogNumber(
  329. title: currentFormObject.label,
  330. initialValue: formValue[currentFormObject.key],
  331. ).show();
  332. if (result?.isNotEmpty ?? false) {
  333. formValue[currentFormObject.key!] = result;
  334. currentInputValue = formValue[currentFormObject.key!];
  335. setState(() {});
  336. }
  337. }
  338. void specialInput(String value) {
  339. formValue[currentFormObject.key!] = value;
  340. currentInputValue = formValue[currentFormObject.key!];
  341. setState(() {});
  342. }
  343. return ExamNumberInput(
  344. currentInputValue: currentInputValue,
  345. commonInput: commonInput,
  346. specialInput: specialInput,
  347. currentFormObject: currentFormObject,
  348. );
  349. }
  350. Widget _buildInput(FormObject currentFormObject) {
  351. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  352. Future<void> commonInput() async {
  353. String? result = await VDialogInput(
  354. title: currentFormObject.label,
  355. initialValue: formValue[currentFormObject.key],
  356. ).show();
  357. if (result?.isNotEmpty ?? false) {
  358. formValue[currentFormObject.key!] = result;
  359. currentInputValue = formValue[currentFormObject.key!];
  360. setState(() {});
  361. }
  362. }
  363. return ExamInput(
  364. currentInputValue: currentInputValue,
  365. commonInput: commonInput,
  366. currentFormObject: currentFormObject,
  367. );
  368. }
  369. /// 血压组件
  370. Widget _buildBloodPressure(FormObject currentFormObject) {
  371. String? highPressureKey = currentFormObject.childrenKey?.first;
  372. String? lowPressureKey = currentFormObject.childrenKey?.last;
  373. List<String> currentBloodPressureValue = [
  374. formValue[highPressureKey] ?? '',
  375. formValue[lowPressureKey] ?? ''
  376. ];
  377. Future<void> commonBloodPressure() async {
  378. String? result = await VDialogBloodPressure(
  379. title: currentFormObject.label,
  380. placeholder: '请输入',
  381. initialValue: currentBloodPressureValue,
  382. ).show();
  383. if (result?.isNotEmpty ?? false) {
  384. List resultList = jsonDecode(result!);
  385. formValue[highPressureKey!] = resultList.first;
  386. formValue[lowPressureKey!] = resultList.last;
  387. currentBloodPressureValue = [
  388. formValue[highPressureKey] ?? '',
  389. formValue[lowPressureKey] ?? ''
  390. ];
  391. formValue['Left'] = '111';
  392. setState(() {});
  393. }
  394. }
  395. return ExamBloodPressure(
  396. currentFormObject: currentFormObject,
  397. currentBloodPressureValue: currentBloodPressureValue,
  398. commonBloodPressure: commonBloodPressure,
  399. );
  400. }
  401. /// 单选框组件
  402. Widget _buildRadio(FormObject currentFormObject) {
  403. List<Option> options = currentFormObject.options ?? [];
  404. String currentSelected = formValue[currentFormObject.key!] ?? "";
  405. void selectRaidoChange(Option e) {
  406. currentSelected = e.value ?? '';
  407. formValue[currentFormObject.key!] = currentSelected;
  408. setState(() {});
  409. }
  410. return ExamRadio(
  411. options: options,
  412. currentFormObject: currentFormObject,
  413. selectRaidoChange: selectRaidoChange,
  414. currentSelected: currentSelected,
  415. );
  416. }
  417. Widget _buildRadioScore(FormObject currentFormObject) {
  418. print(currentFormObject.toJson());
  419. List<Option> options = currentFormObject.options ?? [];
  420. String currentSelected =
  421. formValue[currentFormObject.childrenKey!.first] ?? "";
  422. void selectRaidoChange(Option e) {
  423. currentSelected = e.value ?? '';
  424. formValue[currentFormObject.childrenKey!.first] = currentSelected;
  425. setState(() {});
  426. }
  427. return ExamRadioScore(
  428. options: options,
  429. currentFormObject: currentFormObject,
  430. selectRaidoChange: selectRaidoChange,
  431. currentSelected: currentSelected,
  432. );
  433. }
  434. /// 数字输入框组件
  435. Widget _buildBodyTemperature(FormObject currentFormObject) {
  436. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  437. Future<void> commonInput() async {
  438. String? result = await VDialogNumber(
  439. title: currentFormObject.label,
  440. initialValue: formValue[currentFormObject.key],
  441. ).show();
  442. if (result?.isNotEmpty ?? false) {
  443. formValue[currentFormObject.key!] = result;
  444. currentInputValue = formValue[currentFormObject.key!];
  445. setState(() {});
  446. }
  447. }
  448. void specialInput(String value) {
  449. formValue[currentFormObject.key!] = value;
  450. currentInputValue = formValue[currentFormObject.key!];
  451. setState(() {});
  452. }
  453. return ExamBodyTemperature(
  454. currentInputValue: currentInputValue,
  455. commonInput: commonInput,
  456. specialInput: specialInput,
  457. currentFormObject: currentFormObject,
  458. );
  459. }
  460. }
  461. class TitleClipRect extends StatelessWidget {
  462. const TitleClipRect({
  463. super.key,
  464. this.color = Colors.grey,
  465. required this.title,
  466. required this.arrowHeight,
  467. required this.clickTitle,
  468. });
  469. final Color? color;
  470. final String title;
  471. final double arrowHeight;
  472. final Function clickTitle;
  473. @override
  474. Widget build(BuildContext context) {
  475. return InkWell(
  476. onTap: () {
  477. clickTitle.call();
  478. },
  479. customBorder: HoleShapeBorder(arrowHeight),
  480. child: Card(
  481. margin: const EdgeInsets.all(0),
  482. shape: HoleShapeBorder(arrowHeight),
  483. color: color,
  484. elevation: color == null ? 5 : 0,
  485. shadowColor: Theme.of(context).primaryColor.withOpacity(0.8),
  486. child: ClipPath(
  487. clipper: _TitleClipPath(arrowHeight),
  488. child: Container(
  489. width: _width,
  490. height: _height,
  491. padding: const EdgeInsets.symmetric(
  492. horizontal: 15,
  493. ),
  494. decoration: BoxDecoration(
  495. boxShadow: [
  496. BoxShadow(
  497. color: Theme.of(context).primaryColor.withOpacity(1),
  498. ),
  499. ],
  500. color: color,
  501. ),
  502. alignment: Alignment.center,
  503. child: FittedBox(
  504. child: Text(
  505. title,
  506. style: const TextStyle(color: Colors.white, fontSize: 20),
  507. ),
  508. ),
  509. ),
  510. ),
  511. ),
  512. );
  513. }
  514. }
  515. class _TitleClipPath extends CustomClipper<Path> {
  516. final double arrowHeight;
  517. _TitleClipPath(this.arrowHeight);
  518. @override
  519. Path getClip(Size size) {
  520. final height = size.height;
  521. final arrowBase = height / 2;
  522. // final arrowPLine = math.tan(120 / 180) * arrowBase;
  523. final path = Path();
  524. path.moveTo(0, 0); // 左上角
  525. path.lineTo(size.width - arrowHeight, 0); // 右上角
  526. path.lineTo(size.width, arrowBase); // 右端点
  527. path.lineTo(size.width - arrowHeight, height); // 右下角
  528. path.lineTo(0, height); // 左下角
  529. path.lineTo(arrowHeight, arrowBase); // 左端点
  530. path.lineTo(0, 0); // 左上角
  531. return path;
  532. }
  533. @override
  534. bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
  535. return false;
  536. }
  537. }
  538. class HoleShapeBorder extends ShapeBorder {
  539. final Offset offset;
  540. final double size;
  541. final double arrowHeight;
  542. const HoleShapeBorder(this.arrowHeight,
  543. {this.offset = const Offset(0, 0), this.size = 0});
  544. @override
  545. EdgeInsetsGeometry get dimensions => throw UnimplementedError();
  546. @override
  547. void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
  548. @override
  549. Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
  550. var path = Path();
  551. final height = rect.size.height;
  552. final arrowBase = height / 2;
  553. path.moveTo(0, 0); // 左上角
  554. path.lineTo(rect.size.width - arrowHeight, 0); // 右上角
  555. path.lineTo(rect.size.width, arrowBase); // 右端点
  556. path.lineTo(rect.size.width - arrowHeight, height); // 右下角
  557. path.lineTo(0, height); // 左下角
  558. path.lineTo(arrowHeight, arrowBase); // 左端点
  559. path.lineTo(0, 0); // 左上角
  560. return path;
  561. }
  562. @override
  563. ShapeBorder scale(double t) {
  564. // TODO: implement scale
  565. throw UnimplementedError();
  566. }
  567. @override
  568. Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
  569. // TODO: implement getInnerPath
  570. throw UnimplementedError();
  571. }
  572. }
  573. // ClipPath(
  574. // clipper: TriangleClipper(),
  575. // child: Container(
  576. // width: 50,
  577. // height: 50,
  578. // padding:
  579. // const EdgeInsets.all(
  580. // 2),
  581. // alignment:
  582. // Alignment.topRight,
  583. // decoration:
  584. // const BoxDecoration(
  585. // color: Colors.blue,
  586. // borderRadius:
  587. // BorderRadius.only(
  588. // topRight:
  589. // Radius.circular(
  590. // 8,
  591. // ),
  592. // ),
  593. // ),
  594. // child: const Icon(
  595. // Icons.check,
  596. // color: Colors.white,
  597. // ),
  598. // ))
  599. // class TriangleClipper extends CustomClipper<Path> {
  600. // @override
  601. // Path getClip(Size size) {
  602. // final path = Path()
  603. // ..moveTo(size.width, 0)
  604. // ..lineTo(0, 0)
  605. // ..lineTo(size.width, size.height)
  606. // ..close();
  607. // return path;
  608. // }
  609. // @override
  610. // bool shouldReclip(CustomClipper<Path> oldClipper) => false;
  611. // }