view.dart 16 KB

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