import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:intl/intl.dart'; import 'package:vitalapp/architecture/app_parameters.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/alert_dialog.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/components/tag_widget.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/diagnosis.dart'; import 'package:vitalapp/managers/interfaces/exam.dart'; import 'package:vitalapp/managers/interfaces/models/crowd_labels.dart'; import 'package:vitalapp/managers/interfaces/models/patient_model_dto.dart'; import 'package:vitalapp/pages/home/controller.dart'; import 'package:vitalapp/pages/patient/list/widgets/status.dart'; import 'package:vitalapp/pages/widgets/icon_button.dart'; import 'package:vitalapp/store/store.dart'; import 'controller.dart'; class PatientListPage extends GetView { const PatientListPage({super.key}); @override Widget build(BuildContext context) { 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 { DateTime? result; bool _isLocalStation = AppParameters.data.isLocalStation; if (kIsWeb || _isLocalStation) { result = await showDatePicker( context: Get.context!, initialDate: controller.state.startTime.value ?? DateTime.now(), firstDate: DateTime(1900), lastDate: controller.state.endTime.value ?? DateTime(2100), ); } else { 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 { DateTime? result; bool _isLocalStation = AppParameters.data.isLocalStation; if (kIsWeb || _isLocalStation) { result = await showDatePicker( context: Get.context!, initialDate: controller.state.startTime.value ?? DateTime.now(), firstDate: DateTime(1900), lastDate: controller.state.endTime.value ?? DateTime(2100), ); } else { 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, // ), // const Text( // '创建者:', // style: TextStyle(fontSize: 20), // ), // const SizedBox( // height: 20, // ), // Obx( // () => Row( // children: [ // _tabRadio(title: "建档医生", value: 0), // _tabRadio(title: "团队", value: 1) // ], // ), // ), ], ), ), ), ), ); } Widget _tabRadio({ required String title, required dynamic value, }) { return InkWell( onTap: () { controller.changeFilterFounder(value); }, child: Container( margin: EdgeInsets.only(right: 15), child: Row( children: [ Radio( value: value, groupValue: controller.state.selectBoxFilterFounder, onChanged: (v) { controller.changeFilterFounder(value); }, ), Text( title, style: TextStyle( color: controller.state.selectBoxFilterFounder == value ? const Color(0xff2c77e5) : const Color(0xff4c4948), ), ), ], ), ), ); } 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 / 200, ), children: children, ), ); }, ), ); } } class _HeaderWidget extends GetView { final searchTextEditingController = TextEditingController(); final VoidCallback onFilterPressed; _HeaderWidget({ required this.onFilterPressed, }); @override Widget build(BuildContext context) { return SizedBox( height: 76, child: Row( children: [ _PatientStatisticWidget(), if (Store.user.hasFeature(FeatureKeys.FaceRecognition)) VIconButton( iconData: Icons.sensor_occupied, textString: '人脸识别', voidCallback: () { if (!kIsOnline) { PromptBox.toast("当前为离线模式,不支持此功能"); return; } Debouncer.run( controller.onFaceIdLoginClicked, ); }, ), if (Store.user.hasFeature(FeatureKeys.IdCardPhotoOCR)) VIconButton( iconData: Icons.perm_contact_cal_rounded, textString: '拍照识别', voidCallback: () { if (!Store.user .hasFeature(FeatureKeys.IdCardOfflineRecognition)) { if (!kIsOnline) { PromptBox.toast("当前为离线模式,不支持此功能"); return; } } Debouncer.run( controller.onIdCardScanClickedToDetail, ); }, ), if (Store.user.hasFeature(FeatureKeys.IDCardReader)) VIconButton( iconData: Icons.chrome_reader_mode, textString: '读卡识别', voidCallback: () { Debouncer.run( controller.onReadCardClickedToDetail, ); }, ), VIconButton( iconData: Icons.edit_document, textString: '手动录入', voidCallback: () { Debouncer.run( controller.onManualInputPatient, ); }, ), Expanded( child: SizedBox( height: 70, child: Obx( () => VSearchInput( textEditingController: searchTextEditingController, 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: 4), _buildPhone(), const SizedBox(height: 4), _buildCardNo(), if (Store.user.hasFeature(FeatureKeys.CrowdClassification) && (dto.labelNames?.isNotEmpty ?? false)) ...[ const Expanded(child: SizedBox()), _buildClassTags(), ], ], ), ), Positioned( top: 0, right: 0, child: _PatientSignStatusTag( dto: dto, ), ), if (kIsOnline || dto.isExistLocalData == true) Positioned( right: 0, bottom: 0, child: _PatientRemoveMarkButton(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( flex: 1, child: Text( RpcEnumLabels.gender[dto.patientGender] ?? "未知", style: const TextStyle( color: Colors.grey, fontSize: 20, fontWeight: FontWeight.bold, ), ), ), Expanded( flex: 1, child: Text( "$age岁", style: const TextStyle( color: Colors.grey, fontSize: 20, ), ), ), ], ), ), ], ); } Widget _buildClassTags() { return Column( children: [ ConstrainedBox( constraints: const BoxConstraints( minWidth: double.infinity, maxHeight: 28, ), child: ListView( controller: ScrollController(), scrollDirection: Axis.horizontal, children: [ Row( children: dto.labelNames! .map( (e) => TagWidget( height: 50, label: e, borderColor: (e == CrowdLabels.CHILDREN || e == CrowdLabels.ELDERLY) ? Colors.blue : Colors.orange, backgroundColor: Colors.transparent, textColor: Colors.black, padding: EdgeInsets.only( top: 2, bottom: 2, right: 8, left: 4, ), ), ) .toList(), ) ]), ), ], ); } 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!), ), ); } } class _PatientRemoveMarkButton extends StatelessWidget { final PatientModelDTO dto; const _PatientRemoveMarkButton({required this.dto}); @override Widget build(BuildContext context) { return GestureDetector( onTap: () async { var message = await _getDeleteShowMessage(); Get.dialog( VAlertDialog( title: "提示", width: 300, content: Container( height: 64, padding: const EdgeInsets.symmetric(horizontal: 24), alignment: Alignment.center, child: Text( " “${dto.patientName}”$message是否确定删除?", style: TextStyle(fontSize: 20), ), ), onConfirm: () async { Get.find().removePatient(dto.code!); Get.back(); }, ), barrierDismissible: false, barrierColor: Colors.black.withOpacity(.4), ); }, child: Container( padding: EdgeInsets.only(top: 10, left: 10, right: 2, bottom: 2), decoration: BoxDecoration( color: Colors.red, borderRadius: BorderRadius.only( topLeft: Radius.circular(32), bottomRight: Radius.circular(4), ), ), child: Icon( Icons.delete_forever, size: 26, color: Colors.white, ), ), ); } Future _getDeleteShowMessage() async { String message = ""; var diagnosisManager = Get.find(); var diagnosisList = await diagnosisManager.getDiagnosisAggregationPageAsync( dto.code!, 1, 10); var listRecords = await diagnosisManager.getListByPatientCode(dto.code!); if ((diagnosisList != null && diagnosisList.dataCount > 0) || (listRecords != null && listRecords.length > 0)) { message += "检测记录"; } var examManager = Get.find(); var examList = await examManager.getPatientExamByPageAsync(dto.code!, "HEITCMC"); if (examList != null && examList.length > 0) { if (message.length > 0) { message += "和中医体质记录"; } else { message += "中医体质记录"; } } if (message.length > 0) { message = "有" + message + ","; } return message; } } class _PatientStatisticWidget extends StatelessWidget { @override Widget build(BuildContext context) { final controller = Get.find(); final state = controller.state; return Container( width: 160, alignment: Alignment.centerLeft, child: Obx(() { return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildItem(context, "当日建档数量: ", state.statisticTodayCount), const SizedBox(height: 8), _buildItem(context, "总共建档数量: ", state.statisticTotalCount), const SizedBox(height: 8), _buildItem(context, "查询结果数量: ", state.queryResultTotalCount), ], ); }), ); } Widget _buildItem(BuildContext context, String label, int count) { return Expanded( child: RichText( text: TextSpan( style: TextStyle( fontSize: 16, color: Colors.black, fontFamily: "NotoSansSC", fontFamilyFallback: const ["NotoSansSC"], ), children: [ TextSpan(text: label), TextSpan(text: " "), TextSpan( text: "${count}", style: TextStyle( fontSize: 16, color: Theme.of(context).primaryColor, ), ), ], ), ), ); } }