view.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570
  1. import 'package:flutter/material.dart';
  2. import 'package:get/get.dart';
  3. import 'package:intl/intl.dart';
  4. import 'package:vitalapp/architecture/utils/advance_debounce.dart';
  5. import 'package:vitalapp/architecture/utils/datetime.dart';
  6. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  7. import 'package:vitalapp/architecture/utils/sensitive.dart';
  8. import 'package:vitalapp/architecture/values/features.dart';
  9. import 'package:vitalapp/components/button.dart';
  10. import 'package:vitalapp/components/dialog_date.dart';
  11. import 'package:vitalapp/components/dynamic_drawer.dart';
  12. import 'package:vitalapp/components/input.dart';
  13. import 'package:vitalapp/components/search_input.dart';
  14. import 'package:vitalapp/consts/rpc_enum_labels.dart';
  15. import 'package:vitalapp/consts/styles.dart';
  16. import 'package:vitalapp/global.dart';
  17. import 'package:vitalapp/managers/contract/index.dart';
  18. import 'package:vitalapp/managers/interfaces/models/patient_model_dto.dart';
  19. import 'package:vitalapp/pages/home/controller.dart';
  20. import 'package:vitalapp/pages/patient/create/controller.dart';
  21. import 'package:vitalapp/pages/patient/list/widgets/status.dart';
  22. import 'package:vitalapp/store/store.dart';
  23. import 'controller.dart';
  24. class PatientListPage extends GetView<PatientListController> {
  25. const PatientListPage({super.key});
  26. @override
  27. Widget build(BuildContext context) {
  28. return GetBuilder(
  29. init: PatientListController(),
  30. id: "PatientListPage",
  31. builder: (_) {
  32. return Container(
  33. margin: const EdgeInsets.all(16),
  34. child: Column(
  35. children: [
  36. _HeaderWidget(
  37. onFilterPressed: () {
  38. VDynamicDrawerWrapper.show(
  39. scaffoldKey: Get.find<HomeController>().homeScaffoldKey,
  40. builder: (_) => _filterdrawer(context),
  41. );
  42. // scaffoldKey.currentState?.openEndDrawer();
  43. },
  44. ),
  45. const SizedBox(height: 20),
  46. Expanded(child: _buildListView()),
  47. ],
  48. ),
  49. );
  50. });
  51. }
  52. VDrawer _filterdrawer(BuildContext context) {
  53. final scrollController = ScrollController();
  54. controller.crowdLabelsController.onReady();
  55. return VDrawer(
  56. width: 600,
  57. title: "筛选",
  58. scaffoldKey: Get.find<HomeController>().homeScaffoldKey,
  59. onConfirm: () {
  60. var state = controller.state;
  61. var startTime = state.startTime.value;
  62. var endTime = state.endTime.value;
  63. if (startTime != null &&
  64. endTime != null &&
  65. endTime.difference(startTime).inSeconds < 0) {
  66. PromptBox.toast('起始时间不能晚于结束时间');
  67. return;
  68. }
  69. controller.reloadList(isFilter: true);
  70. // Get.back();
  71. VDynamicDrawerWrapper.hide(
  72. scaffoldKey: Get.find<HomeController>().homeScaffoldKey,
  73. );
  74. },
  75. onCancel: () {
  76. // Get.back();
  77. VDynamicDrawerWrapper.hide(
  78. scaffoldKey: Get.find<HomeController>().homeScaffoldKey,
  79. );
  80. },
  81. child: Scrollbar(
  82. controller: scrollController,
  83. thumbVisibility: true,
  84. child: SingleChildScrollView(
  85. controller: scrollController,
  86. child: Padding(
  87. padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20),
  88. child: Column(
  89. crossAxisAlignment: CrossAxisAlignment.start,
  90. children: [
  91. const Text(
  92. '居民创建时间:',
  93. style: TextStyle(fontSize: 20),
  94. ),
  95. const SizedBox(
  96. height: 20,
  97. ),
  98. Row(
  99. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  100. mainAxisSize: MainAxisSize.max,
  101. children: [
  102. Expanded(
  103. child: Obx(
  104. () => VInput(
  105. readOnly: true,
  106. controller: TextEditingController(
  107. text: DateFormat('yyyy-MM-dd').format(
  108. controller.state.startTime.value!.toLocal(),
  109. ),
  110. ),
  111. radius: 4,
  112. onTap: () async {
  113. final result = await VDialogDate(
  114. maxValue: controller.state.endTime.value,
  115. title: '起始时间',
  116. initialValue: controller.state.startTime.value,
  117. ).show();
  118. controller.state.startTime.value = result;
  119. },
  120. ),
  121. ),
  122. ),
  123. Container(
  124. margin: const EdgeInsets.symmetric(horizontal: 16),
  125. child: const Text('一')),
  126. Expanded(
  127. child: Obx(
  128. () => VInput(
  129. readOnly: true,
  130. controller: TextEditingController(
  131. text: DateFormat('yyyy-MM-dd').format(
  132. controller.state.endTime.value!.toLocal(),
  133. ),
  134. ),
  135. radius: 4,
  136. onTap: () async {
  137. final result = await VDialogDate(
  138. title: '结束时间',
  139. initialValue: controller.state.endTime.value,
  140. ).show();
  141. controller.state.endTime.value = result;
  142. },
  143. ),
  144. ),
  145. )
  146. ],
  147. ),
  148. // const SizedBox(
  149. // height: 20,
  150. // ),
  151. // const Text(
  152. // '人群分类:',
  153. // style: TextStyle(fontSize: 20),
  154. // ),
  155. // const SizedBox(
  156. // height: 20,
  157. // ),
  158. // CrowdSelectLabelView(
  159. // controller: controller.crowdLabelsController,
  160. // ),
  161. ],
  162. ),
  163. ),
  164. ),
  165. ),
  166. );
  167. }
  168. Widget _buildListView() {
  169. final scrollController = ScrollController();
  170. scrollController.addListener(
  171. () {
  172. // 如果滑动到底部
  173. try {
  174. if (scrollController.position.atEdge) {
  175. if (scrollController.position.pixels != 0) {
  176. if (controller.state.hasNextPage) {
  177. controller.loadNextPageList();
  178. }
  179. }
  180. }
  181. } catch (e) {
  182. // logger.e("listViewScrollController exception:", e);
  183. }
  184. },
  185. );
  186. return RefreshIndicator(
  187. onRefresh: () async {
  188. controller.reloadList();
  189. },
  190. child: Obx(
  191. () {
  192. final list = controller.state.dataList;
  193. final children = <Widget>[];
  194. for (var i = 0; i < list.length; i++) {
  195. children.add(_PatientCard(dto: list[i]));
  196. }
  197. return children.isEmpty
  198. ? Container(
  199. margin: const EdgeInsets.only(top: 80),
  200. child: Column(
  201. children: [
  202. Center(
  203. child: Image.asset(
  204. "assets/images/no_data.png",
  205. width: 300,
  206. height: 300,
  207. fit: BoxFit.cover,
  208. ),
  209. ),
  210. const Text(
  211. "暂无数据,先看看别的吧",
  212. style: TextStyle(fontSize: 18),
  213. ),
  214. ],
  215. ),
  216. )
  217. : Scrollbar(
  218. trackVisibility: true,
  219. controller: scrollController,
  220. child: GridView(
  221. shrinkWrap: true,
  222. controller: scrollController,
  223. gridDelegate:
  224. const SliverGridDelegateWithFixedCrossAxisCount(
  225. crossAxisCount: 3,
  226. mainAxisSpacing: 16,
  227. crossAxisSpacing: 20,
  228. childAspectRatio: 360 / 180,
  229. ),
  230. children: children,
  231. ),
  232. );
  233. },
  234. ),
  235. );
  236. }
  237. }
  238. class _HeaderWidget extends GetView<PatientListController> {
  239. final searchTextEditingController = TextEditingController();
  240. final VoidCallback onFilterPressed;
  241. _HeaderWidget({
  242. required this.onFilterPressed,
  243. });
  244. final createPatientController = Get.find<CreatePatientController>();
  245. Widget _buildIconButton(
  246. IconData iconData,
  247. String textString,
  248. VoidCallback voidCallback,
  249. ) {
  250. return Material(
  251. child: InkWell(
  252. onTap: () => voidCallback.call(),
  253. child: Container(
  254. margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
  255. child: Column(
  256. children: [
  257. Icon(
  258. iconData,
  259. size: 38,
  260. ),
  261. Text(textString),
  262. ],
  263. ),
  264. ),
  265. ),
  266. );
  267. }
  268. // VSideNavMenuItem _buildCreateManualRecord() {
  269. // return VSideNavMenuItem(
  270. // title: "手动建档",
  271. // icon: Icon(Icons.edit_document, color: Colors.grey.shade700),
  272. // pageBuilder: (_) => _buildInterval(
  273. // PatientInfo(),
  274. // ),
  275. // );
  276. // }
  277. // VSideNavMenuItem _buildIDReaderRecord() {
  278. // return VSideNavMenuItem(
  279. // title: "读卡建档",
  280. // icon: Icon(Icons.chrome_reader_mode, color: Colors.grey.shade700),
  281. // onTap: () {
  282. // controller.onReadCardClicked();
  283. // },
  284. // );
  285. // }
  286. // VSideNavMenuItem _buildScanIdCardRecord() {
  287. // return VSideNavMenuItem(
  288. // title: "身份证识别建档",
  289. // icon: Icon(Icons.perm_contact_cal_rounded, color: Colors.grey.shade700),
  290. // onTap: () {
  291. // if (!kIsOnline) {
  292. // PromptBox.toast("当前为离线模式,不支持此功能");
  293. // return;
  294. // }
  295. // controller.onIdCardScanClicked();
  296. // },
  297. // );
  298. // }
  299. @override
  300. Widget build(BuildContext context) {
  301. return SizedBox(
  302. height: 76,
  303. child: Row(
  304. children: [
  305. if (Store.user.hasFeature(FeatureKeys.FaceRecognition))
  306. _buildIconButton(Icons.sensor_occupied, '人脸识别', () {
  307. if (!kIsOnline) {
  308. PromptBox.toast("当前为离线模式,不支持此功能");
  309. return;
  310. }
  311. advanceDebounce(
  312. createPatientController.onFaceIdLoginClicked,
  313. "PatientList.OnFaceIdLoginClicked",
  314. 1500,
  315. );
  316. }),
  317. if (Store.user.hasFeature(FeatureKeys.IdCardPhotoOCR))
  318. _buildIconButton(Icons.perm_contact_cal_rounded, '拍照识别', () {
  319. if (!Store.user
  320. .hasFeature(FeatureKeys.IdCardOfflineRecognition)) {
  321. if (!kIsOnline) {
  322. PromptBox.toast("当前为离线模式,不支持此功能");
  323. return;
  324. }
  325. }
  326. advanceDebounce(
  327. createPatientController.onIdCardScanClickedToDetail,
  328. "PatientList.OnIdCardScanClickedToDetail",
  329. 1500,
  330. );
  331. }),
  332. if (Store.user.hasFeature(FeatureKeys.IDCardReader))
  333. _buildIconButton(Icons.chrome_reader_mode, '读卡识别', () {
  334. advanceDebounce(
  335. createPatientController.onReadCardClickedToDetail,
  336. "PatientList.OnReadCardClickedToDetail",
  337. 1500,
  338. );
  339. }),
  340. Expanded(
  341. child: SizedBox(
  342. height: 70,
  343. child: Obx(
  344. () => VSearchInput(
  345. textEditingController: searchTextEditingController,
  346. placeholder:
  347. "身份证号码/姓名${controller.state.isOnline ? '/手机号' : ''}",
  348. clearable: true,
  349. onClear: () {},
  350. onSearch: (value) {
  351. controller.state.searchString = value;
  352. controller.reloadList();
  353. },
  354. ),
  355. ),
  356. ),
  357. ),
  358. const SizedBox(width: 8),
  359. SizedBox(
  360. width: 180,
  361. height: 70,
  362. child: VButton(
  363. onTap: onFilterPressed,
  364. child: Row(
  365. mainAxisAlignment: MainAxisAlignment.center,
  366. children: const [
  367. Icon(Icons.filter_alt, size: 24),
  368. Text("筛选", style: TextStyle(fontSize: 20)),
  369. ],
  370. ),
  371. ),
  372. ),
  373. ],
  374. ),
  375. );
  376. }
  377. }
  378. class _PatientCard extends StatelessWidget {
  379. final PatientModelDTO dto;
  380. const _PatientCard({required this.dto});
  381. @override
  382. Widget build(BuildContext context) {
  383. final body = Stack(
  384. children: [
  385. Container(
  386. padding: const EdgeInsets.symmetric(
  387. horizontal: 16,
  388. vertical: 12,
  389. ),
  390. child: Column(
  391. crossAxisAlignment: CrossAxisAlignment.start,
  392. children: [
  393. const SizedBox(height: 8),
  394. SizedBox(
  395. child: Text(
  396. dto.patientName!,
  397. overflow: TextOverflow.ellipsis,
  398. style: const TextStyle(
  399. color: Colors.black,
  400. fontSize: 26,
  401. fontWeight: FontWeight.bold,
  402. ),
  403. ),
  404. ),
  405. const SizedBox(height: 8),
  406. LayoutBuilder(
  407. builder: (context, c) {
  408. final width = c.maxWidth - 80;
  409. // 不和状态标签重叠,并保持一定距离
  410. return SizedBox(width: width, child: _buildBaseInfoRow());
  411. },
  412. ),
  413. const SizedBox(height: 8),
  414. // Expanded(child: _buildClassTags()),
  415. // _buildClassTags(),
  416. _buildPhone(),
  417. const SizedBox(height: 4),
  418. _buildCardNo(),
  419. ],
  420. ),
  421. ),
  422. Positioned(
  423. top: 0,
  424. right: 0,
  425. child: _PatientSignStatusTag(
  426. dto: dto,
  427. ),
  428. ),
  429. ],
  430. );
  431. return Material(
  432. borderRadius: GlobalStyles.borderRadius,
  433. child: Ink(
  434. decoration: BoxDecoration(
  435. color: Colors.white,
  436. borderRadius: GlobalStyles.borderRadius,
  437. border: Border.all(color: Colors.grey.shade400),
  438. ),
  439. child: InkWell(
  440. borderRadius: GlobalStyles.borderRadius,
  441. onTap: () {
  442. // Get.back();
  443. Get.find<PatientListController>().patientListGotoDetail(dto);
  444. },
  445. child: body,
  446. ),
  447. ),
  448. );
  449. }
  450. Widget _buildBaseInfoRow() {
  451. final birthday = dto.birthday!.toLocal();
  452. final age = DataTimeUtils.calculateAge(birthday);
  453. return Row(
  454. mainAxisSize: MainAxisSize.max,
  455. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  456. children: [
  457. Expanded(
  458. child: Row(
  459. mainAxisSize: MainAxisSize.max,
  460. mainAxisAlignment: MainAxisAlignment.start,
  461. children: [
  462. Expanded(
  463. child: Text(
  464. RpcEnumLabels.gender[dto.patientGender] ?? "未知",
  465. style: const TextStyle(
  466. color: Colors.grey,
  467. fontSize: 20,
  468. fontWeight: FontWeight.bold,
  469. ),
  470. ),
  471. ),
  472. Expanded(
  473. child: Text(
  474. "$age岁",
  475. style: const TextStyle(
  476. color: Colors.grey,
  477. fontSize: 20,
  478. ),
  479. ),
  480. ),
  481. ],
  482. ),
  483. ),
  484. ],
  485. );
  486. }
  487. Widget _buildClassTags() {
  488. return Column(
  489. children: [
  490. ConstrainedBox(
  491. constraints: const BoxConstraints(
  492. minWidth: double.infinity,
  493. maxHeight: 50,
  494. ),
  495. child: Text(
  496. dto.labelNames?.join(' ') ?? '',
  497. style: const TextStyle(color: Colors.grey, fontSize: 18),
  498. maxLines: 2,
  499. overflow: TextOverflow.ellipsis,
  500. ),
  501. ),
  502. ],
  503. );
  504. }
  505. Widget _buildPhone() {
  506. if (dto.phone != null && dto.phone!.isNotEmpty) {
  507. String phone = dto.phone!;
  508. if (Store.app.enableEncryptSensitiveInfo) {
  509. phone = SensitiveUtils.desensitizeMobilePhone(phone);
  510. }
  511. return Text(
  512. '手机号:$phone',
  513. style: const TextStyle(color: Colors.grey, fontSize: 18),
  514. );
  515. } else {
  516. return const SizedBox();
  517. }
  518. }
  519. Widget _buildCardNo() {
  520. if (dto.cardNo != null && dto.cardNo!.isNotEmpty) {
  521. String cardNo = dto.cardNo!;
  522. if (Store.app.enableEncryptSensitiveInfo) {
  523. cardNo = SensitiveUtils.desensitizeIdCard(cardNo);
  524. }
  525. return Text(
  526. '证件号码:$cardNo',
  527. style: const TextStyle(color: Colors.grey, fontSize: 18),
  528. );
  529. } else {
  530. return const SizedBox();
  531. }
  532. }
  533. }
  534. class _PatientSignStatusTag extends StatelessWidget {
  535. final PatientModelDTO dto;
  536. _PatientSignStatusTag({required this.dto});
  537. final ContractUtils _contractUtils = ContractUtils();
  538. @override
  539. Widget build(BuildContext context) {
  540. return Container(
  541. alignment: Alignment.centerRight,
  542. width: 120,
  543. padding: const EdgeInsets.only(top: 18),
  544. child: StatusLabel(
  545. title: _contractUtils.dataOfflineStatus(dto.isExistLocalData!),
  546. color: _contractUtils.dataOfflineColor(dto.isExistLocalData!),
  547. ),
  548. );
  549. }
  550. }