view.dart 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  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 VoidCallback onFilterPressed;
  240. _HeaderWidget({
  241. required this.onFilterPressed,
  242. });
  243. final createPatientController = Get.find<CreatePatientController>();
  244. Widget _buildIconButton(
  245. IconData iconData,
  246. String textString,
  247. VoidCallback voidCallback,
  248. ) {
  249. return Material(
  250. child: InkWell(
  251. onTap: () => voidCallback.call(),
  252. child: Container(
  253. margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 20),
  254. child: Column(
  255. children: [
  256. Icon(
  257. iconData,
  258. size: 40,
  259. ),
  260. Text(textString),
  261. ],
  262. ),
  263. ),
  264. ),
  265. );
  266. }
  267. // VSideNavMenuItem _buildCreateManualRecord() {
  268. // return VSideNavMenuItem(
  269. // title: "手动建档",
  270. // icon: Icon(Icons.edit_document, color: Colors.grey.shade700),
  271. // pageBuilder: (_) => _buildInterval(
  272. // PatientInfo(),
  273. // ),
  274. // );
  275. // }
  276. // VSideNavMenuItem _buildIDReaderRecord() {
  277. // return VSideNavMenuItem(
  278. // title: "读卡建档",
  279. // icon: Icon(Icons.chrome_reader_mode, color: Colors.grey.shade700),
  280. // onTap: () {
  281. // controller.onReadCardClicked();
  282. // },
  283. // );
  284. // }
  285. // VSideNavMenuItem _buildScanIdCardRecord() {
  286. // return VSideNavMenuItem(
  287. // title: "身份证识别建档",
  288. // icon: Icon(Icons.perm_contact_cal_rounded, color: Colors.grey.shade700),
  289. // onTap: () {
  290. // if (!kIsOnline) {
  291. // PromptBox.toast("当前为离线模式,不支持此功能");
  292. // return;
  293. // }
  294. // controller.onIdCardScanClicked();
  295. // },
  296. // );
  297. // }
  298. @override
  299. Widget build(BuildContext context) {
  300. return SizedBox(
  301. height: 76,
  302. child: Row(
  303. children: [
  304. if (Store.user.hasFeature(FeatureKeys.FaceRecognition))
  305. _buildIconButton(Icons.sensor_occupied, '人脸识别', () {
  306. if (!kIsOnline) {
  307. PromptBox.toast("当前为离线模式,不支持此功能");
  308. return;
  309. }
  310. advanceDebounce(
  311. createPatientController.onFaceIdLoginClicked,
  312. "PatientList.OnFaceIdLoginClicked",
  313. 1500,
  314. );
  315. }),
  316. if (Store.user.hasFeature(FeatureKeys.IdCardPhotoOCR))
  317. _buildIconButton(Icons.perm_contact_cal_rounded, '拍照识别', () {
  318. if (!kIsOnline) {
  319. PromptBox.toast("当前为离线模式,不支持此功能");
  320. return;
  321. }
  322. advanceDebounce(
  323. createPatientController.onIdCardScanClickedToDetail,
  324. "PatientList.OnIdCardScanClickedToDetail",
  325. 1500,
  326. );
  327. }),
  328. if (Store.user.hasFeature(FeatureKeys.IDCardReader))
  329. _buildIconButton(Icons.chrome_reader_mode, '读卡识别', () {
  330. advanceDebounce(
  331. createPatientController.onReadCardClickedToDetail,
  332. "PatientList.OnReadCardClickedToDetail",
  333. 1500,
  334. );
  335. }),
  336. Expanded(
  337. child: SizedBox(
  338. height: 70,
  339. child: Obx(
  340. () => VSearchInput(
  341. placeholder:
  342. "身份证号码/姓名${controller.state.isOnline ? '/手机号' : ''}",
  343. clearable: true,
  344. onClear: () {},
  345. onSearch: (value) {
  346. controller.state.searchString = value;
  347. controller.reloadList();
  348. },
  349. ),
  350. ),
  351. ),
  352. ),
  353. const SizedBox(width: 8),
  354. SizedBox(
  355. width: 180,
  356. height: 70,
  357. child: VButton(
  358. onTap: onFilterPressed,
  359. child: Row(
  360. mainAxisAlignment: MainAxisAlignment.center,
  361. children: const [
  362. Icon(Icons.filter_alt, size: 24),
  363. Text("筛选", style: TextStyle(fontSize: 20)),
  364. ],
  365. ),
  366. ),
  367. ),
  368. ],
  369. ),
  370. );
  371. }
  372. }
  373. class _PatientCard extends StatelessWidget {
  374. final PatientModelDTO dto;
  375. const _PatientCard({required this.dto});
  376. @override
  377. Widget build(BuildContext context) {
  378. final body = Stack(
  379. children: [
  380. Container(
  381. padding: const EdgeInsets.symmetric(
  382. horizontal: 16,
  383. vertical: 12,
  384. ),
  385. child: Column(
  386. crossAxisAlignment: CrossAxisAlignment.start,
  387. children: [
  388. const SizedBox(height: 8),
  389. SizedBox(
  390. child: Text(
  391. dto.patientName!,
  392. overflow: TextOverflow.ellipsis,
  393. style: const TextStyle(
  394. color: Colors.black,
  395. fontSize: 26,
  396. fontWeight: FontWeight.bold,
  397. ),
  398. ),
  399. ),
  400. const SizedBox(height: 8),
  401. LayoutBuilder(
  402. builder: (context, c) {
  403. final width = c.maxWidth - 80;
  404. // 不和状态标签重叠,并保持一定距离
  405. return SizedBox(width: width, child: _buildBaseInfoRow());
  406. },
  407. ),
  408. const SizedBox(height: 12),
  409. // Expanded(child: _buildClassTags()),
  410. // _buildClassTags(),
  411. _buildPhone(),
  412. const SizedBox(height: 6),
  413. _buildCardNo(),
  414. ],
  415. ),
  416. ),
  417. Positioned(
  418. top: 0,
  419. right: 0,
  420. child: _PatientSignStatusTag(
  421. dto: dto,
  422. ),
  423. ),
  424. ],
  425. );
  426. return Material(
  427. borderRadius: GlobalStyles.borderRadius,
  428. child: Ink(
  429. decoration: BoxDecoration(
  430. color: Colors.white,
  431. borderRadius: GlobalStyles.borderRadius,
  432. border: Border.all(color: Colors.grey.shade400),
  433. ),
  434. child: InkWell(
  435. borderRadius: GlobalStyles.borderRadius,
  436. onTap: () {
  437. // Get.back();
  438. Get.find<PatientListController>().patientListGotoDetail(dto);
  439. },
  440. child: body,
  441. ),
  442. ),
  443. );
  444. }
  445. Widget _buildBaseInfoRow() {
  446. final birthday = dto.birthday!.toLocal();
  447. final age = DataTimeUtils.calculateAge(birthday);
  448. return Row(
  449. mainAxisSize: MainAxisSize.max,
  450. mainAxisAlignment: MainAxisAlignment.spaceBetween,
  451. children: [
  452. Expanded(
  453. child: Row(
  454. mainAxisSize: MainAxisSize.max,
  455. mainAxisAlignment: MainAxisAlignment.start,
  456. children: [
  457. Expanded(
  458. child: Text(
  459. RpcEnumLabels.gender[dto.patientGender] ?? "未知",
  460. style: const TextStyle(
  461. color: Colors.grey,
  462. fontSize: 20,
  463. fontWeight: FontWeight.bold,
  464. ),
  465. ),
  466. ),
  467. Expanded(
  468. child: Text(
  469. "$age岁",
  470. style: const TextStyle(
  471. color: Colors.grey,
  472. fontSize: 20,
  473. ),
  474. ),
  475. ),
  476. ],
  477. ),
  478. ),
  479. ],
  480. );
  481. }
  482. Widget _buildClassTags() {
  483. return Column(
  484. children: [
  485. ConstrainedBox(
  486. constraints: const BoxConstraints(
  487. minWidth: double.infinity,
  488. maxHeight: 50,
  489. ),
  490. child: Text(
  491. dto.labelNames?.join(' ') ?? '',
  492. style: const TextStyle(color: Colors.grey, fontSize: 18),
  493. maxLines: 2,
  494. overflow: TextOverflow.ellipsis,
  495. ),
  496. ),
  497. ],
  498. );
  499. }
  500. Widget _buildPhone() {
  501. if (dto.phone != null && dto.phone!.isNotEmpty) {
  502. String phone = dto.phone!;
  503. if (Store.app.enableEncryptSensitiveInfo) {
  504. phone = SensitiveUtils.desensitizeMobilePhone(phone);
  505. }
  506. return Text(
  507. '手机号:$phone',
  508. style: const TextStyle(color: Colors.grey, fontSize: 18),
  509. );
  510. } else {
  511. return const SizedBox();
  512. }
  513. }
  514. Widget _buildCardNo() {
  515. if (dto.cardNo != null && dto.cardNo!.isNotEmpty) {
  516. String cardNo = dto.cardNo!;
  517. if (Store.app.enableEncryptSensitiveInfo) {
  518. cardNo = SensitiveUtils.desensitizeIdCard(cardNo);
  519. }
  520. return Text(
  521. '证件号码:$cardNo',
  522. style: const TextStyle(color: Colors.grey, fontSize: 18),
  523. );
  524. } else {
  525. return const SizedBox();
  526. }
  527. }
  528. }
  529. class _PatientSignStatusTag extends StatelessWidget {
  530. final PatientModelDTO dto;
  531. _PatientSignStatusTag({required this.dto});
  532. final ContractUtils _contractUtils = ContractUtils();
  533. @override
  534. Widget build(BuildContext context) {
  535. return Container(
  536. alignment: Alignment.centerRight,
  537. width: 120,
  538. padding: const EdgeInsets.only(top: 18),
  539. child: StatusLabel(
  540. title: _contractUtils.dataOfflineStatus(dto.isExistLocalData!),
  541. color: _contractUtils.dataOfflineColor(dto.isExistLocalData!),
  542. ),
  543. );
  544. }
  545. }