configurable_card.dart 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833
  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/button.dart';
  6. import 'package:vnoteapp/components/dialog_input.dart';
  7. import 'package:vnoteapp/components/dialog_number.dart';
  8. import 'package:vnoteapp/components/dynamic_drawer.dart';
  9. import 'package:vnoteapp/managers/interfaces/cachedRecord.dart';
  10. import 'package:vnoteapp/managers/interfaces/template.dart';
  11. import 'package:vnoteapp/pages/check/models/form.dart';
  12. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_blood_pressure.dart';
  13. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_blood_sugar.dart';
  14. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_body_temperature.dart';
  15. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_body_weight.dart';
  16. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_boold_oxygen.dart';
  17. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_check_box.dart';
  18. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_input.dart';
  19. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_number_input.dart';
  20. import 'dart:math' as math;
  21. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_radio.dart';
  22. import 'package:vnoteapp/pages/check/widgets/exam_configurable/exam_radio_score.dart';
  23. const double _width = 170;
  24. const double _height = 38;
  25. String tw = '00.0';
  26. class ConfigurableCard extends StatefulWidget {
  27. final String cardKey;
  28. final Function(String, String, dynamic) callBack;
  29. final Widget? followUpWidget;
  30. final String? patientCode;
  31. final String? examData;
  32. const ConfigurableCard({
  33. super.key,
  34. required this.cardKey,
  35. required this.callBack,
  36. this.followUpWidget,
  37. this.patientCode,
  38. this.examData,
  39. });
  40. @override
  41. State<ConfigurableCard> createState() => _ConfigurableFormState();
  42. }
  43. class _ConfigurableFormState extends State<ConfigurableCard> {
  44. /// 当前最新的模板的键值对
  45. Map<String, dynamic> templateRelation = {};
  46. /// 当前模板数据
  47. List<FormObject> currentTemplate = [];
  48. /// 当前title的下标
  49. int currentTitleIndex = 0;
  50. Map<String, dynamic> formValue = {};
  51. var scaffoldKey = GlobalKey<ScaffoldState>();
  52. final _templateManager = Get.find<ITemplateManager>();
  53. final _cachedRecordManager = Get.find<ICachedRecordManager>();
  54. final arrowHeight = math.tan(120 / 180) * (_height / 2);
  55. List<String> deviceList = ['Temp', 'GLU', 'NIBP', 'SpO2', 'BMI'];
  56. Map<String, dynamic> deviceCached = {};
  57. @override
  58. void initState() {
  59. super.initState();
  60. initTemplate();
  61. }
  62. @override
  63. void dispose() {
  64. super.dispose();
  65. }
  66. Future<void> initTemplate() async {
  67. await fetchTemplateIndex();
  68. await fetchTemplate(widget.cardKey);
  69. if (!['GXY', 'TNB'].contains(widget.cardKey)) {
  70. fetchTemplateData();
  71. }
  72. }
  73. /// 读取诊疗展示的缓存
  74. Future<String?> readCachedRecord(String currentDevice) async {
  75. String? value = await _cachedRecordManager.readCachedRecord(
  76. currentDevice,
  77. widget.patientCode!,
  78. 'ZLZS',
  79. );
  80. return value;
  81. }
  82. /// 读取体检的缓存
  83. Future<String?> readCachedCheck() async {
  84. String? value = await _cachedRecordManager.readCachedRecord(
  85. widget.cardKey,
  86. widget.patientCode!,
  87. 'EXAM',
  88. );
  89. return value;
  90. }
  91. Future<void> readCached() async {
  92. for (var element in deviceList) {
  93. String? value = await readCachedRecord(element);
  94. if (value?.isNotEmpty ?? false) {
  95. deviceCached.addAll(jsonDecode(value!));
  96. }
  97. }
  98. }
  99. Future<void> fetchTemplateIndex() async {
  100. try {
  101. /// 获取模板的键值对
  102. String? templates;
  103. templates = await _templateManager.readTemplate('templateRelation');
  104. templateRelation = jsonDecode(templates!);
  105. setState(() {});
  106. } catch (error) {
  107. print('发生错误: $error');
  108. }
  109. }
  110. Future<void> fetchTemplateData() async {
  111. String? value = await readCachedCheck();
  112. await readCached();
  113. if (deviceCached.isNotEmpty) {
  114. formValue = deviceCached;
  115. setState(() {});
  116. return;
  117. }
  118. if (value?.isNotEmpty ?? false) {
  119. formValue = jsonDecode(value!);
  120. }
  121. /// 这逻辑需要优化
  122. // if (widget.examData?.isNotEmpty ?? false) {
  123. // formValue = jsonDecode(widget.examData!);
  124. // }
  125. formValue.forEach(
  126. (key, value) {
  127. if (value is List) {
  128. formValue[key] = List<String>.from(formValue[key]);
  129. }
  130. },
  131. );
  132. setState(() {});
  133. }
  134. Future<void> fetchTemplate(String key) async {
  135. try {
  136. if (templateRelation[key] == null) {
  137. currentTemplate = [];
  138. setState(() {});
  139. return;
  140. }
  141. var template =
  142. await _templateManager.readTemplate(templateRelation[key]!);
  143. String templateContent =
  144. TemplateDTO.fromJson(jsonDecode(template!)).templateContent!;
  145. List<Map<String, dynamic>> list =
  146. jsonDecode(templateContent).cast<Map<String, dynamic>>();
  147. for (var i in list) {
  148. if (i['children'] != null) {
  149. List<FormObject> currentChildren = [];
  150. for (var j in i['children']) {
  151. currentChildren.add(FormObject.fromJson(j));
  152. }
  153. i['children'] = currentChildren;
  154. }
  155. currentTemplate.add(FormObject.fromJson(i));
  156. }
  157. // var list = jsonDecode(templateContent).cast<Map<String, dynamic>>();
  158. // if (list == null) {
  159. // currentTemplate = [];
  160. // } else {
  161. // currentTemplate = updateChildren(list);
  162. // }
  163. // TextStorage t = TextStorage(
  164. // fileName: key,
  165. // directory: "template",
  166. // );
  167. // await t.save(jsonEncode(currentTemplate));
  168. setState(() {});
  169. } catch (error) {
  170. print('发生错误: $error');
  171. }
  172. }
  173. @override
  174. Widget build(BuildContext context) {
  175. return Scaffold(
  176. key: scaffoldKey,
  177. endDrawer: VDynamicDrawerWrapper(scaffoldKey: scaffoldKey),
  178. resizeToAvoidBottomInset: false,
  179. body: Column(
  180. children: [
  181. Row(
  182. crossAxisAlignment: CrossAxisAlignment.start,
  183. children: [
  184. const SizedBox(
  185. width: 16,
  186. ),
  187. Container(
  188. margin: const EdgeInsets.only(top: 8),
  189. width: 130,
  190. height: 54,
  191. child: VButton(
  192. onTap: () => {Get.back()},
  193. child: const Row(
  194. mainAxisAlignment: MainAxisAlignment.center,
  195. children: [
  196. Icon(Icons.arrow_back_ios_new, size: 24),
  197. SizedBox(
  198. width: 8,
  199. ),
  200. Text("返回", style: TextStyle(fontSize: 20)),
  201. ],
  202. ),
  203. ),
  204. ),
  205. Expanded(
  206. child: Container(
  207. padding:
  208. const EdgeInsets.symmetric(vertical: 16, horizontal: 10),
  209. child: _buildTitleList(),
  210. ),
  211. ),
  212. Container(
  213. margin: const EdgeInsets.only(top: 8, right: 16),
  214. width: 130,
  215. height: 54,
  216. child: VButton(
  217. onTap: () {
  218. widget
  219. .callBack(
  220. widget.cardKey,
  221. templateRelation[widget.cardKey]!,
  222. jsonEncode(formValue),
  223. )
  224. .call();
  225. Get.back();
  226. },
  227. child: const Row(
  228. mainAxisAlignment: MainAxisAlignment.center,
  229. children: [
  230. Icon(Icons.save, size: 24),
  231. SizedBox(
  232. width: 8,
  233. ),
  234. Text("保存", style: TextStyle(fontSize: 20)),
  235. ],
  236. ),
  237. ),
  238. ),
  239. ],
  240. ),
  241. Expanded(
  242. child: Stack(
  243. children: [
  244. Row(
  245. mainAxisAlignment: MainAxisAlignment.start,
  246. crossAxisAlignment: CrossAxisAlignment.start,
  247. children: [
  248. _buildDiagram(),
  249. _buildContent(),
  250. ],
  251. ),
  252. if (currentTitleIndex != currentTemplate.length - 1)
  253. _buildPositionedButton(
  254. () async {
  255. currentTitleIndex++;
  256. setState(() {});
  257. },
  258. right: -30,
  259. ),
  260. ],
  261. ),
  262. )
  263. ],
  264. ),
  265. );
  266. }
  267. Widget buildSingleItem(Widget item, int span) {
  268. return FractionallySizedBox(
  269. widthFactor: span == 24 ? 1 : 0.5,
  270. child: item,
  271. );
  272. }
  273. Widget buildWidget(FormObject? currentFormObject) {
  274. Map<String, Widget Function(FormObject)> widgetMap = {
  275. 'checkbox': _buildCheckBox,
  276. 'numberInput': _buildNumberInput,
  277. 'input': _buildInput,
  278. 'radio': _buildRadio,
  279. 'radioScore': _buildRadioScore,
  280. 'bloodPressure': _buildBloodPressure,
  281. 'bodyTemperature': _buildBodyTemperature,
  282. 'weight': _buildBodyWeight,
  283. 'sugar': _buildBodySugar,
  284. 'bloodOxygen': _buildBloodOxygen,
  285. };
  286. Widget Function(FormObject) builder =
  287. widgetMap[currentFormObject?.type] ?? _buildInput;
  288. return builder(currentFormObject!);
  289. }
  290. Widget waterCardList() {
  291. int itemCount = 0;
  292. if (currentTemplate.isNotEmpty) {
  293. itemCount = currentTemplate[currentTitleIndex].children?.length ?? 0;
  294. }
  295. bool currentTemplateOptionsIsNotEmpty =
  296. currentTemplate[currentTitleIndex].options?.isNotEmpty ?? false;
  297. List<Widget> items = List.generate(itemCount, (index) {
  298. FormObject? currentFormObject =
  299. currentTemplate[currentTitleIndex].children?[index];
  300. int span = currentFormObject?.span ?? 12;
  301. //父结构的options不等于空或null
  302. if (true) {
  303. //子结构的options若是无值则取父类的options值
  304. if (currentTemplateOptionsIsNotEmpty) {
  305. currentFormObject!.options =
  306. currentTemplate[currentTitleIndex].options;
  307. }
  308. }
  309. return buildSingleItem(buildWidget(currentFormObject), span);
  310. });
  311. return Scrollbar(
  312. thumbVisibility: true,
  313. child: SingleChildScrollView(
  314. child: Container(
  315. alignment: Alignment.topCenter,
  316. padding: const EdgeInsets.all(15),
  317. child: Wrap(
  318. runSpacing: 20, // 纵向元素间距
  319. alignment: WrapAlignment.start,
  320. children: items,
  321. ),
  322. ),
  323. ),
  324. );
  325. }
  326. /// title标签
  327. Widget _buildTitleList() {
  328. return Wrap(
  329. runSpacing: 10, // 设置子小部件之间的间距
  330. spacing: -12,
  331. alignment: WrapAlignment.start,
  332. children: currentTemplate.asMap().entries.map(
  333. (e) {
  334. /// TODO 这边需要改下
  335. MaterialColor currentColors = Colors.grey;
  336. e.value.children?.forEach((element) {
  337. if (formValue.containsKey(element.key)) {
  338. currentColors = Colors.green;
  339. }
  340. });
  341. return TitleClipRect(
  342. title: e.value.label ?? '',
  343. color: currentTitleIndex == e.key ? null : currentColors,
  344. arrowHeight: arrowHeight,
  345. clickTitle: () {
  346. currentTitleIndex = e.key;
  347. setState(() {});
  348. },
  349. );
  350. },
  351. ).toList(),
  352. );
  353. }
  354. /// 示意图
  355. Widget _buildDiagram() {
  356. return Expanded(
  357. flex: 1,
  358. child: Stack(
  359. children: [
  360. /// TODO BAKA-优化
  361. ['GXY', 'TNB'].contains(widget.cardKey)
  362. ? widget.followUpWidget!
  363. : Container(
  364. alignment: Alignment.topCenter,
  365. margin: const EdgeInsets.all(16).copyWith(top: 0),
  366. child: Image.asset(
  367. 'assets/images/exam/normalMeasurementChart.png',
  368. height: double.infinity,
  369. fit: BoxFit.fitWidth, // 设置图像的适应方式
  370. ),
  371. ),
  372. if (currentTitleIndex != 0)
  373. _buildPositionedButton(
  374. () async {
  375. if (currentTitleIndex == 0) {
  376. Get.back();
  377. } else {
  378. currentTitleIndex--;
  379. setState(() {});
  380. }
  381. },
  382. left: -30,
  383. ),
  384. ],
  385. ),
  386. );
  387. }
  388. /// 按钮
  389. Widget _buildPositionedButton(Function onTap, {double? right, double? left}) {
  390. return Positioned(
  391. right: right,
  392. left: left,
  393. bottom: 0,
  394. top: 0,
  395. child: Container(
  396. width: 100,
  397. height: 100,
  398. alignment: Alignment.centerLeft,
  399. child: InkWell(
  400. onTap: () => onTap.call(),
  401. child: Container(
  402. width: 100,
  403. height: 100,
  404. padding: const EdgeInsets.all(20),
  405. alignment:
  406. right.isNull ? Alignment.centerRight : Alignment.centerLeft,
  407. decoration: BoxDecoration(
  408. borderRadius: BorderRadius.circular(50),
  409. color: Theme.of(context).primaryColor.withOpacity(
  410. .8,
  411. ),
  412. ),
  413. child: Image.asset(
  414. right.isNull
  415. ? "assets/images/exam/left-arrow.png"
  416. : "assets/images/exam/right-arrow.png",
  417. width: 40,
  418. height: 40,
  419. color: Colors.white,
  420. fit: BoxFit.contain,
  421. ),
  422. ),
  423. ),
  424. ),
  425. );
  426. }
  427. /// 主页面
  428. Widget _buildContent() {
  429. return Expanded(
  430. flex: 2,
  431. child: waterCardList(),
  432. );
  433. }
  434. /// 多选框组件
  435. Widget _buildCheckBox(FormObject currentFormObject) {
  436. List<Option> options = currentFormObject.options ?? [];
  437. List<String> currentSelectedCheckBox =
  438. formValue[currentFormObject.key!] ?? [];
  439. void selectCheckBoxChange(Option e) {
  440. if (currentSelectedCheckBox.contains(e.value)) {
  441. currentSelectedCheckBox.remove(e.value);
  442. } else {
  443. currentSelectedCheckBox.add(e.value ?? '');
  444. }
  445. formValue[currentFormObject.key!] = currentSelectedCheckBox;
  446. setState(() {});
  447. }
  448. return ExamCheckBox(
  449. options: options,
  450. currentSelectedCheckBox: currentSelectedCheckBox,
  451. currentFormObject: currentFormObject,
  452. selectCheckBoxChange: selectCheckBoxChange,
  453. );
  454. }
  455. /// 数字输入框组件
  456. Widget _buildNumberInput(FormObject currentFormObject) {
  457. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  458. if ((formValue['Height']?.isNotEmpty ?? false) &&
  459. (formValue['Weight']?.isNotEmpty ?? false)) {
  460. formValue['Bmi'] = (double.parse(formValue['Weight']) /
  461. ((double.parse(formValue['Height']) / 100) *
  462. (double.parse(formValue['Height']) / 100)))
  463. .toStringAsFixed(2);
  464. }
  465. Future<void> commonInput() async {
  466. String? result = await VDialogNumber(
  467. title: currentFormObject.label,
  468. initialValue: formValue[currentFormObject.key],
  469. ).show();
  470. if (result?.isNotEmpty ?? false) {
  471. formValue[currentFormObject.key!] = result;
  472. currentInputValue = formValue[currentFormObject.key!];
  473. setState(() {});
  474. }
  475. }
  476. void specialInput(String value) {
  477. formValue[currentFormObject.key!] = value;
  478. currentInputValue = formValue[currentFormObject.key!];
  479. setState(() {});
  480. }
  481. return ExamNumberInput(
  482. currentInputValue: currentInputValue,
  483. commonInput: commonInput,
  484. specialInput: specialInput,
  485. currentFormObject: currentFormObject,
  486. );
  487. }
  488. Widget _buildInput(FormObject currentFormObject) {
  489. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  490. Future<void> commonInput() async {
  491. String? result = await VDialogInput(
  492. title: currentFormObject.label,
  493. initialValue: formValue[currentFormObject.key],
  494. ).show();
  495. if (result?.isNotEmpty ?? false) {
  496. formValue[currentFormObject.key!] = result;
  497. currentInputValue = formValue[currentFormObject.key!];
  498. setState(() {});
  499. }
  500. }
  501. return ExamInput(
  502. currentInputValue: currentInputValue,
  503. commonInput: commonInput,
  504. currentFormObject: currentFormObject,
  505. );
  506. }
  507. /// 血压组件
  508. Widget _buildBloodPressure(FormObject currentFormObject) {
  509. return const ExamBloodPressure();
  510. }
  511. /// 单选框组件
  512. Widget _buildRadio(FormObject currentFormObject) {
  513. List<Option> options = currentFormObject.options ?? [];
  514. String currentSelected = formValue[currentFormObject.key!] ?? "";
  515. void selectRaidoChange(Option e) {
  516. currentSelected = e.value ?? '';
  517. formValue[currentFormObject.key!] = currentSelected;
  518. setState(() {});
  519. }
  520. return ExamRadio(
  521. options: options,
  522. currentFormObject: currentFormObject,
  523. selectRaidoChange: selectRaidoChange,
  524. currentSelected: currentSelected,
  525. );
  526. }
  527. Widget _buildRadioScore(FormObject currentFormObject) {
  528. print(currentFormObject.toJson());
  529. List<Option> options = currentFormObject.options ?? [];
  530. String currentSelected =
  531. formValue[currentFormObject.childrenKey!.first] ?? "";
  532. void selectRaidoChange(Option e) {
  533. currentSelected = e.value ?? '';
  534. formValue[currentFormObject.childrenKey!.first] = currentSelected;
  535. setState(() {});
  536. }
  537. return ExamRadioScore(
  538. options: options,
  539. currentFormObject: currentFormObject,
  540. selectRaidoChange: selectRaidoChange,
  541. currentSelected: currentSelected,
  542. );
  543. }
  544. /// 体温组件
  545. Widget _buildBodyTemperature(FormObject currentFormObject) {
  546. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  547. void bodyTemperatureInput(String value) {
  548. formValue[currentFormObject.key!] = value;
  549. currentInputValue = formValue[currentFormObject.key!];
  550. setState(() {});
  551. }
  552. return ExamBodyTemperature(
  553. currentInputValue: currentInputValue,
  554. bodyTemperatureInput: bodyTemperatureInput,
  555. currentFormObject: currentFormObject,
  556. );
  557. }
  558. /// 血氧
  559. Widget _buildBloodOxygen(FormObject currentFormObject) {
  560. Map<String, dynamic> currentValue = formValue;
  561. void bloodOxygenInput(Map<String, dynamic> bloodOxygen) {
  562. formValue['Pulse_Frequency'] = bloodOxygen['Pulse_Frequency'];
  563. formValue['Spo2'] = bloodOxygen['Spo2'];
  564. currentValue = bloodOxygen;
  565. setState(() {});
  566. }
  567. return ExamBloodOxygen(
  568. currentValue: currentValue,
  569. bloodOxygenInput: bloodOxygenInput,
  570. );
  571. }
  572. /// 体重
  573. Widget _buildBodyWeight(FormObject currentFormObject) {
  574. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  575. void bodyWeightInput(String value) {
  576. formValue[currentFormObject.key!] = value;
  577. currentInputValue = formValue[currentFormObject.key!];
  578. setState(() {});
  579. }
  580. return ExamBodyWeight(
  581. currentInputValue: currentInputValue,
  582. bodyWeightInput: bodyWeightInput,
  583. currentFormObject: currentFormObject,
  584. );
  585. }
  586. /// 血糖
  587. Widget _buildBodySugar(FormObject currentFormObject) {
  588. String currentInputValue = formValue[currentFormObject.key!] ?? '';
  589. void bloodSugarInput(String value) {
  590. formValue[currentFormObject.key!] = value;
  591. currentInputValue = formValue[currentFormObject.key!];
  592. setState(() {});
  593. }
  594. return ExamBloodSugar(
  595. currentInputValue: currentInputValue,
  596. bloodSugarInput: bloodSugarInput,
  597. currentFormObject: currentFormObject,
  598. );
  599. }
  600. }
  601. class TitleClipRect extends StatelessWidget {
  602. const TitleClipRect({
  603. super.key,
  604. this.color = Colors.grey,
  605. required this.title,
  606. required this.arrowHeight,
  607. required this.clickTitle,
  608. });
  609. final Color? color;
  610. final String title;
  611. final double arrowHeight;
  612. final Function clickTitle;
  613. @override
  614. Widget build(BuildContext context) {
  615. return InkWell(
  616. onTap: () {
  617. clickTitle.call();
  618. },
  619. customBorder: HoleShapeBorder(arrowHeight),
  620. child: Card(
  621. margin: const EdgeInsets.all(0),
  622. shape: HoleShapeBorder(arrowHeight),
  623. color: color,
  624. elevation: color == null ? 5 : 0,
  625. shadowColor: Theme.of(context).primaryColor.withOpacity(0.8),
  626. child: ClipPath(
  627. clipper: _TitleClipPath(arrowHeight),
  628. child: Container(
  629. width: _width,
  630. height: _height,
  631. padding: const EdgeInsets.symmetric(
  632. horizontal: 15,
  633. ),
  634. decoration: BoxDecoration(
  635. boxShadow: [
  636. BoxShadow(
  637. color: Theme.of(context).primaryColor.withOpacity(1),
  638. ),
  639. ],
  640. color: color,
  641. ),
  642. alignment: Alignment.center,
  643. child: FittedBox(
  644. child: Text(
  645. title,
  646. style: const TextStyle(color: Colors.white, fontSize: 20),
  647. ),
  648. ),
  649. ),
  650. ),
  651. ),
  652. );
  653. }
  654. }
  655. class _TitleClipPath extends CustomClipper<Path> {
  656. final double arrowHeight;
  657. _TitleClipPath(this.arrowHeight);
  658. @override
  659. Path getClip(Size size) {
  660. final height = size.height;
  661. final arrowBase = height / 2;
  662. // final arrowPLine = math.tan(120 / 180) * arrowBase;
  663. final path = Path();
  664. path.moveTo(0, 0); // 左上角
  665. path.lineTo(size.width - arrowHeight, 0); // 右上角
  666. path.lineTo(size.width, arrowBase); // 右端点
  667. path.lineTo(size.width - arrowHeight, height); // 右下角
  668. path.lineTo(0, height); // 左下角
  669. path.lineTo(arrowHeight, arrowBase); // 左端点
  670. path.lineTo(0, 0); // 左上角
  671. return path;
  672. }
  673. @override
  674. bool shouldReclip(covariant CustomClipper<Path> oldClipper) {
  675. return false;
  676. }
  677. }
  678. class HoleShapeBorder extends ShapeBorder {
  679. final Offset offset;
  680. final double size;
  681. final double arrowHeight;
  682. const HoleShapeBorder(this.arrowHeight,
  683. {this.offset = const Offset(0, 0), this.size = 0});
  684. @override
  685. EdgeInsetsGeometry get dimensions => throw UnimplementedError();
  686. @override
  687. void paint(Canvas canvas, Rect rect, {TextDirection? textDirection}) {}
  688. @override
  689. Path getOuterPath(Rect rect, {TextDirection? textDirection}) {
  690. var path = Path();
  691. final height = rect.size.height;
  692. final arrowBase = height / 2;
  693. path.moveTo(0, 0); // 左上角
  694. path.lineTo(rect.size.width - arrowHeight, 0); // 右上角
  695. path.lineTo(rect.size.width, arrowBase); // 右端点
  696. path.lineTo(rect.size.width - arrowHeight, height); // 右下角
  697. path.lineTo(0, height); // 左下角
  698. path.lineTo(arrowHeight, arrowBase); // 左端点
  699. path.lineTo(0, 0); // 左上角
  700. return path;
  701. }
  702. @override
  703. ShapeBorder scale(double t) {
  704. // TODO: implement scale
  705. throw UnimplementedError();
  706. }
  707. @override
  708. Path getInnerPath(Rect rect, {TextDirection? textDirection}) {
  709. // TODO: implement getInnerPath
  710. throw UnimplementedError();
  711. }
  712. }
  713. // ClipPath(
  714. // clipper: TriangleClipper(),
  715. // child: Container(
  716. // width: 50,
  717. // height: 50,
  718. // padding:
  719. // const EdgeInsets.all(
  720. // 2),
  721. // alignment:
  722. // Alignment.topRight,
  723. // decoration:
  724. // const BoxDecoration(
  725. // color: Colors.blue,
  726. // borderRadius:
  727. // BorderRadius.only(
  728. // topRight:
  729. // Radius.circular(
  730. // 8,
  731. // ),
  732. // ),
  733. // ),
  734. // child: const Icon(
  735. // Icons.check,
  736. // color: Colors.white,
  737. // ),
  738. // ))
  739. // class TriangleClipper extends CustomClipper<Path> {
  740. // @override
  741. // Path getClip(Size size) {
  742. // final path = Path()
  743. // ..moveTo(size.width, 0)
  744. // ..lineTo(0, 0)
  745. // ..lineTo(size.width, size.height)
  746. // ..close();
  747. // return path;
  748. // }
  749. // @override
  750. // bool shouldReclip(CustomClipper<Path> oldClipper) => false;
  751. // }