import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:vitalapp/architecture/utils/advance_debounce.dart'; import 'package:vitalapp/architecture/utils/datetime.dart'; import 'package:vitalapp/architecture/utils/prompt_box.dart'; import 'package:vitalapp/architecture/utils/sensitive.dart'; import 'package:vitalapp/architecture/values/features.dart'; import 'package:vitalapp/components/button.dart'; import 'package:vitalapp/components/dialog_date.dart'; import 'package:vitalapp/components/dynamic_drawer.dart'; import 'package:vitalapp/components/input.dart'; import 'package:vitalapp/components/search_input.dart'; import 'package:vitalapp/consts/rpc_enum_labels.dart'; import 'package:vitalapp/consts/styles.dart'; import 'package:vitalapp/global.dart'; import 'package:vitalapp/managers/contract/index.dart'; import 'package:vitalapp/managers/interfaces/models/patient_model_dto.dart'; import 'package:vitalapp/pages/home/controller.dart'; import 'package:vitalapp/pages/patient/create/controller.dart'; import 'package:vitalapp/pages/patient/list/widgets/status.dart'; import 'package:vitalapp/store/store.dart'; import 'controller.dart'; class PatientListPage extends GetView { const PatientListPage({super.key}); @override Widget build(BuildContext context) { return GetBuilder( init: PatientListController(), id: "PatientListPage", builder: (_) { return Container( margin: const EdgeInsets.all(16), child: Column( children: [ _HeaderWidget( onFilterPressed: () { VDynamicDrawerWrapper.show( scaffoldKey: Get.find().homeScaffoldKey, builder: (_) => _filterdrawer(context), ); // scaffoldKey.currentState?.openEndDrawer(); }, ), const SizedBox(height: 20), Expanded(child: _buildListView()), ], ), ); }); } VDrawer _filterdrawer(BuildContext context) { final scrollController = ScrollController(); controller.crowdLabelsController.onReady(); return VDrawer( width: 600, title: "筛选", scaffoldKey: Get.find().homeScaffoldKey, onConfirm: () { var state = controller.state; var startTime = state.startTime.value; var endTime = state.endTime.value; if (startTime != null && endTime != null && endTime.difference(startTime).inSeconds < 0) { PromptBox.toast('起始时间不能晚于结束时间'); return; } controller.reloadList(isFilter: true); // Get.back(); VDynamicDrawerWrapper.hide( scaffoldKey: Get.find().homeScaffoldKey, ); }, onCancel: () { // Get.back(); VDynamicDrawerWrapper.hide( scaffoldKey: Get.find().homeScaffoldKey, ); }, child: Scrollbar( controller: scrollController, thumbVisibility: true, child: SingleChildScrollView( controller: scrollController, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 20), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text( '居民创建时间:', style: TextStyle(fontSize: 20), ), const SizedBox( height: 20, ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisSize: MainAxisSize.max, children: [ Expanded( child: Obx( () => VInput( readOnly: true, controller: TextEditingController( text: DateFormat('yyyy-MM-dd').format( controller.state.startTime.value!.toLocal(), ), ), radius: 4, onTap: () async { final result = await VDialogDate( maxValue: controller.state.endTime.value, title: '起始时间', initialValue: controller.state.startTime.value, ).show(); controller.state.startTime.value = result; }, ), ), ), Container( margin: const EdgeInsets.symmetric(horizontal: 16), child: const Text('一')), Expanded( child: Obx( () => VInput( readOnly: true, controller: TextEditingController( text: DateFormat('yyyy-MM-dd').format( controller.state.endTime.value!.toLocal(), ), ), radius: 4, onTap: () async { final result = await VDialogDate( title: '结束时间', initialValue: controller.state.endTime.value, ).show(); controller.state.endTime.value = result; }, ), ), ) ], ), // const SizedBox( // height: 20, // ), // const Text( // '人群分类:', // style: TextStyle(fontSize: 20), // ), // const SizedBox( // height: 20, // ), // CrowdSelectLabelView( // controller: controller.crowdLabelsController, // ), ], ), ), ), ), ); } Widget _buildListView() { final scrollController = ScrollController(); scrollController.addListener( () { // 如果滑动到底部 try { if (scrollController.position.atEdge) { if (scrollController.position.pixels != 0) { if (controller.state.hasNextPage) { controller.loadNextPageList(); } } } } catch (e) { // logger.e("listViewScrollController exception:", e); } }, ); return RefreshIndicator( onRefresh: () async { controller.reloadList(); }, child: Obx( () { final list = controller.state.dataList; final children = []; for (var i = 0; i < list.length; i++) { children.add(_PatientCard(dto: list[i])); } return children.isEmpty ? Container( margin: const EdgeInsets.only(top: 80), child: Column( children: [ Center( child: Image.asset( "assets/images/no_data.png", width: 300, height: 300, fit: BoxFit.cover, ), ), const Text( "暂无数据,先看看别的吧", style: TextStyle(fontSize: 18), ), ], ), ) : Scrollbar( trackVisibility: true, controller: scrollController, child: GridView( shrinkWrap: true, controller: scrollController, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 16, crossAxisSpacing: 20, childAspectRatio: 360 / 180, ), children: children, ), ); }, ), ); } } class _HeaderWidget extends GetView { final VoidCallback onFilterPressed; _HeaderWidget({ required this.onFilterPressed, }); final createPatientController = Get.find(); Widget _buildIconButton( IconData iconData, String textString, VoidCallback voidCallback, ) { return Material( child: InkWell( onTap: () => voidCallback.call(), child: Container( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 20), child: Column( children: [ Icon( iconData, size: 40, ), Text(textString), ], ), ), ), ); } // VSideNavMenuItem _buildCreateManualRecord() { // return VSideNavMenuItem( // title: "手动建档", // icon: Icon(Icons.edit_document, color: Colors.grey.shade700), // pageBuilder: (_) => _buildInterval( // PatientInfo(), // ), // ); // } // VSideNavMenuItem _buildIDReaderRecord() { // return VSideNavMenuItem( // title: "读卡建档", // icon: Icon(Icons.chrome_reader_mode, color: Colors.grey.shade700), // onTap: () { // controller.onReadCardClicked(); // }, // ); // } // VSideNavMenuItem _buildScanIdCardRecord() { // return VSideNavMenuItem( // title: "身份证识别建档", // icon: Icon(Icons.perm_contact_cal_rounded, color: Colors.grey.shade700), // onTap: () { // if (!kIsOnline) { // PromptBox.toast("当前为离线模式,不支持此功能"); // return; // } // controller.onIdCardScanClicked(); // }, // ); // } @override Widget build(BuildContext context) { return SizedBox( height: 76, child: Row( children: [ if (Store.user.hasFeature(FeatureKeys.FaceRecognition)) _buildIconButton(Icons.sensor_occupied, '人脸识别', () { if (!kIsOnline) { PromptBox.toast("当前为离线模式,不支持此功能"); return; } advanceDebounce( createPatientController.onFaceIdLoginClicked, "PatientList.OnFaceIdLoginClicked", 1500, ); }), if (Store.user.hasFeature(FeatureKeys.IdCardPhotoOCR)) _buildIconButton(Icons.perm_contact_cal_rounded, '拍照识别', () { if (!kIsOnline) { PromptBox.toast("当前为离线模式,不支持此功能"); return; } advanceDebounce( createPatientController.onIdCardScanClickedToDetail, "PatientList.OnIdCardScanClickedToDetail", 1500, ); }), if (Store.user.hasFeature(FeatureKeys.IDCardReader)) _buildIconButton(Icons.chrome_reader_mode, '读卡识别', () { advanceDebounce( createPatientController.onReadCardClickedToDetail, "PatientList.OnReadCardClickedToDetail", 1500, ); }), Expanded( child: SizedBox( height: 70, child: Obx( () => VSearchInput( placeholder: "身份证号码/姓名${controller.state.isOnline ? '/手机号' : ''}", clearable: true, onClear: () {}, onSearch: (value) { controller.state.searchString = value; controller.reloadList(); }, ), ), ), ), const SizedBox(width: 8), SizedBox( width: 180, height: 70, child: VButton( onTap: onFilterPressed, child: Row( mainAxisAlignment: MainAxisAlignment.center, children: const [ Icon(Icons.filter_alt, size: 24), Text("筛选", style: TextStyle(fontSize: 20)), ], ), ), ), ], ), ); } } class _PatientCard extends StatelessWidget { final PatientModelDTO dto; const _PatientCard({required this.dto}); @override Widget build(BuildContext context) { final body = Stack( children: [ Container( padding: const EdgeInsets.symmetric( horizontal: 16, vertical: 12, ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const SizedBox(height: 8), SizedBox( child: Text( dto.patientName!, overflow: TextOverflow.ellipsis, style: const TextStyle( color: Colors.black, fontSize: 26, fontWeight: FontWeight.bold, ), ), ), const SizedBox(height: 8), LayoutBuilder( builder: (context, c) { final width = c.maxWidth - 80; // 不和状态标签重叠,并保持一定距离 return SizedBox(width: width, child: _buildBaseInfoRow()); }, ), const SizedBox(height: 12), // Expanded(child: _buildClassTags()), // _buildClassTags(), _buildPhone(), const SizedBox(height: 6), _buildCardNo(), ], ), ), Positioned( top: 0, right: 0, child: _PatientSignStatusTag( dto: dto, ), ), ], ); return Material( borderRadius: GlobalStyles.borderRadius, child: Ink( decoration: BoxDecoration( color: Colors.white, borderRadius: GlobalStyles.borderRadius, border: Border.all(color: Colors.grey.shade400), ), child: InkWell( borderRadius: GlobalStyles.borderRadius, onTap: () { // Get.back(); Get.find().patientListGotoDetail(dto); }, child: body, ), ), ); } Widget _buildBaseInfoRow() { final birthday = dto.birthday!.toLocal(); final age = DataTimeUtils.calculateAge(birthday); return Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( child: Row( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, children: [ Expanded( child: Text( RpcEnumLabels.gender[dto.patientGender] ?? "未知", style: const TextStyle( color: Colors.grey, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), Expanded( child: Text( "$age岁", style: const TextStyle( color: Colors.grey, fontSize: 20, ), ), ), ], ), ), ], ); } Widget _buildClassTags() { return Column( children: [ ConstrainedBox( constraints: const BoxConstraints( minWidth: double.infinity, maxHeight: 50, ), child: Text( dto.labelNames?.join(' ') ?? '', style: const TextStyle(color: Colors.grey, fontSize: 18), maxLines: 2, overflow: TextOverflow.ellipsis, ), ), ], ); } Widget _buildPhone() { if (dto.phone != null && dto.phone!.isNotEmpty) { String phone = dto.phone!; if (Store.app.enableEncryptSensitiveInfo) { phone = SensitiveUtils.desensitizeMobilePhone(phone); } return Text( '手机号:$phone', style: const TextStyle(color: Colors.grey, fontSize: 18), ); } else { return const SizedBox(); } } Widget _buildCardNo() { if (dto.cardNo != null && dto.cardNo!.isNotEmpty) { String cardNo = dto.cardNo!; if (Store.app.enableEncryptSensitiveInfo) { cardNo = SensitiveUtils.desensitizeIdCard(cardNo); } return Text( '证件号码:$cardNo', style: const TextStyle(color: Colors.grey, fontSize: 18), ); } else { return const SizedBox(); } } } class _PatientSignStatusTag extends StatelessWidget { final PatientModelDTO dto; _PatientSignStatusTag({required this.dto}); final ContractUtils _contractUtils = ContractUtils(); @override Widget build(BuildContext context) { return Container( alignment: Alignment.centerRight, width: 120, padding: const EdgeInsets.only(top: 18), child: StatusLabel( title: _contractUtils.dataOfflineStatus(dto.isExistLocalData!), color: _contractUtils.dataOfflineColor(dto.isExistLocalData!), ), ); } }