123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777 |
- import 'package:fis_jsonrpc/rpc.dart';
- 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/crowd_select_label.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<PatientListController> {
- 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<HomeController>().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<HomeController>().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<HomeController>().homeScaffoldKey,
- );
- },
- onCancel: () {
- // Get.back();
- VDynamicDrawerWrapper.hide(
- scaffoldKey: Get.find<HomeController>().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 ??
- DateTime.now())
- .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();
- }
- if (result != null) {
- 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 ?? DateTime.now())
- .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();
- }
- if (result != null) {
- controller.state.endTime.value = result;
- }
- },
- ),
- ),
- )
- ],
- ),
- const SizedBox(
- height: 20,
- ),
- const Text(
- '居民查询范围:',
- style: TextStyle(fontSize: 20),
- ),
- const SizedBox(
- height: 20,
- ),
- ScopeEnquiryResidents(
- selectRaidoChange: (int value) {
- controller.state.selectBoxFilterFounder = value;
- },
- selectIndex: controller.state.selectBoxFilterFounder,
- ),
- const SizedBox(
- height: 20,
- ),
- const Text(
- '人群分类:',
- style: TextStyle(fontSize: 20),
- ),
- const SizedBox(
- height: 20,
- ),
- CrowdSelectLabelView(
- controller: controller.crowdLabelsController,
- ),
- const SizedBox(
- height: 20,
- ),
- const Text(
- '签约状态:',
- style: TextStyle(fontSize: 20),
- ),
- const SizedBox(
- height: 20,
- ),
- ContractStateSelectLabelView(
- ContractStateEnums: [
- ContractStateEnum.Signed,
- ContractStateEnum.Cancelled,
- ContractStateEnum.Unsigned,
- ContractStateEnum.Expired,
- ContractStateEnum.Refused,
- ],
- selectContractState:
- controller.state.contractStateSelectedItem,
- selectRaidoChange: (String value) {
- if (value == "0") {
- controller.state.contractStateSelectedItem = null;
- return;
- }
- controller.state.contractStateSelectedItem =
- ContractStateEnum.values
- .where((element) => element.name == value)
- .first;
- },
- ),
- ],
- ),
- ),
- ),
- ),
- );
- }
- 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 = <Widget>[];
- 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<PatientListController> {
- 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<PatientListController>().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<PatientListController>().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<String> _getDeleteShowMessage() async {
- String message = "";
- var diagnosisManager = Get.find<IDiagnosisManager>();
- 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<IExamManager>();
- 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<PatientListController>();
- 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,
- ),
- ),
- ],
- ),
- ),
- );
- }
- }
|