controller.dart 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622
  1. import 'dart:convert';
  2. import 'package:fis_jsonrpc/rpc.dart';
  3. import 'package:flutter/material.dart';
  4. import 'package:get/get.dart';
  5. import 'package:vitalapp/architecture/defines.dart';
  6. import 'package:vitalapp/architecture/utils/prompt_box.dart';
  7. import 'package:vitalapp/architecture/values/features.dart';
  8. import 'package:vitalapp/components/alert_dialog.dart';
  9. import 'package:vitalapp/global.dart';
  10. import 'package:vitalapp/helper/id_card_helper.dart';
  11. import 'package:vitalapp/managers/interfaces/device.dart';
  12. import 'package:vitalapp/managers/interfaces/models/device.dart';
  13. import 'package:vitalapp/managers/interfaces/patient.dart';
  14. import 'package:vitalapp/managers/interfaces/permission.dart';
  15. import 'package:vitalapp/pages/controllers/blue_location_mixin.dart';
  16. import 'package:vitalapp/pages/controllers/crowd_labels.dart';
  17. import 'package:vitalapp/pages/controllers/home_nav_mixin.dart';
  18. import 'package:vitalapp/pages/patient/bluetooth_card_reader/view.dart';
  19. import 'package:vitalapp/pages/patient/card_reader/index.dart';
  20. import 'package:vitalapp/pages/patient/create/state.dart';
  21. import 'package:vitalapp/pages/patient/create/widgets/face_result_dialog.dart';
  22. import 'package:vitalapp/pages/patient/list/controller.dart';
  23. import 'package:vitalapp/routes/routes.dart';
  24. import 'package:vitalapp/store/store.dart';
  25. import 'package:vnote_device_plugin/consts/types.dart';
  26. import 'package:fis_common/logger/logger.dart';
  27. import '../../facial_recognition/index.dart';
  28. import '../../id_card_scan/index.dart';
  29. class CreatePatientController extends FControllerBase
  30. with HomeNavMixin, BluetoothAndLocationMixin {
  31. final _patientManager = Get.find<IPatientManager>();
  32. final crowdLabelsController = Get.find<CrowdLabelsController>();
  33. final _deviceManager = Get.find<IDeviceManager>();
  34. final state = CreatePatientState();
  35. @override
  36. Future<void> onLoad() {
  37. final params = Get.parameters;
  38. if (params.containsKey("from")) {
  39. if (params["from"] == "list") {
  40. state.isCreateOnly = true;
  41. }
  42. }
  43. if (Routes.parameters.containsKey('patientInfo')) {
  44. if (Routes.parameters['patientInfo'] != null) {
  45. PatientBaseDTO patientInfo = PatientBaseDTO.fromJson(
  46. jsonDecode(
  47. Routes.parameters['patientInfo']!,
  48. ),
  49. );
  50. patientInfoParse(patientInfo);
  51. }
  52. }
  53. return super.onLoad();
  54. }
  55. /// 保存档案,调整签约
  56. void gotoSignContract() async {
  57. setBusy("正在保存...");
  58. String? code;
  59. try {
  60. final validateMsg = await _validateForm();
  61. if (validateMsg != null) {
  62. toast(validateMsg);
  63. return null;
  64. }
  65. code = await _submitForm();
  66. if (code == null) {
  67. busy = false;
  68. PromptBox.toast("保存失败");
  69. return;
  70. }
  71. // final code = "13B95A2B2790464BBFD9B30A71F15C95";
  72. busy = false;
  73. Future.delayed(
  74. const Duration(milliseconds: 800),
  75. () {
  76. state.reset(); // 重置状态
  77. },
  78. );
  79. } finally {
  80. if (code != null) {
  81. Get.toNamed(
  82. "/contract/package_list",
  83. parameters: {"patientCode": code},
  84. );
  85. }
  86. }
  87. // Get.find<HomeController>().switchNavByName("/patient/list");
  88. // Future.delayed(
  89. // const Duration(milliseconds: 800),
  90. // () {
  91. // // TODO:
  92. // Get.find<PatientListController>().gotoDetail(code);
  93. // busy = false;
  94. // },
  95. // );
  96. }
  97. /// 存为档案,调整到档案详情
  98. void saveAndBack() async {
  99. final validateMsg = await _validateForm();
  100. if (validateMsg != null) {
  101. toast(validateMsg);
  102. return null;
  103. }
  104. final code = await _submitForm();
  105. if (code == null) {
  106. busy = false;
  107. logger.e('CreatePatientController saveAndBack code is null');
  108. // PromptBox.toast("保存失败");
  109. return;
  110. } else {
  111. busy = false;
  112. // await queryIsNeedFaceInput(); //询问是否录入人脸
  113. if (Store.user.hasFeature(FeatureKeys.FaceRecognition) && kIsOnline) {
  114. var photoUrl = await getPatientPhoto(state.cardNo);
  115. if (photoUrl.isEmpty) {
  116. await onFaceEntryClicked(); //不询问直接录入
  117. }
  118. }
  119. Future.delayed(
  120. ///这个800ms不能移除,移除后会导致建档失败
  121. const Duration(milliseconds: 800),
  122. () {
  123. state.reset(); // 重置状态
  124. busy = false;
  125. PromptBox.toast("保存成功");
  126. Get.find<PatientListController>().reloadList();
  127. },
  128. );
  129. }
  130. ///不跳转到详情页
  131. // Get.find<HomeController>().switchNavByName("/patient/list");
  132. // Get.put(PatientListController());
  133. Future.delayed(
  134. ///这个800ms不能移除,移除后会导致建档失败
  135. const Duration(milliseconds: 800),
  136. () async {
  137. onIdcardInfoIsCreateRecord(code);
  138. await Get.find<PatientListController>().gotoDetail(
  139. code,
  140. );
  141. busy = false;
  142. },
  143. );
  144. }
  145. /// 询问是否需要人脸录入
  146. Future<void> queryIsNeedFaceInput() async {
  147. /// 拥有人脸权限并且不是身份证扫码建档
  148. if (Store.user.hasFeature(FeatureKeys.FaceRecognition) && kIsOnline) {
  149. var photoUrl = await getPatientPhoto(state.cardNo);
  150. if (photoUrl.isNotEmpty) {
  151. await Get.dialog(VAlertDialog(
  152. contentPadding: const EdgeInsets.fromLTRB(20, 1, 20, 1),
  153. title: '提示',
  154. content: Container(
  155. margin: const EdgeInsets.only(bottom: 20),
  156. child: Column(
  157. mainAxisSize: MainAxisSize.min,
  158. children: [
  159. Image.network(photoUrl,
  160. width: 300, height: 220, fit: BoxFit.cover,
  161. loadingBuilder: (context, child, progress) {
  162. if (progress == null ||
  163. progress.cumulativeBytesLoaded ==
  164. progress.expectedTotalBytes) {
  165. return Image.network(
  166. photoUrl,
  167. width: 300,
  168. height: 220,
  169. fit: BoxFit.cover,
  170. );
  171. }
  172. return const CircularProgressIndicator(
  173. color: Colors.blueAccent);
  174. }),
  175. const Text("该居民已采集过人脸,是否重新采集?",
  176. style: TextStyle(fontSize: 20),
  177. textAlign: TextAlign.center),
  178. ],
  179. ),
  180. ),
  181. confirmLabel: '重新采集',
  182. cancelLabel: '跳过',
  183. showCancel: true,
  184. onConfirm: () async {
  185. await onFaceEntryClicked();
  186. Get.back();
  187. },
  188. onCanceled: () {
  189. Get.back();
  190. },
  191. ));
  192. } else {
  193. await Get.dialog(VAlertDialog(
  194. contentPadding: const EdgeInsets.fromLTRB(20, 1, 20, 1),
  195. title: '提示',
  196. content: Container(
  197. margin: const EdgeInsets.only(bottom: 20),
  198. child: const Text(
  199. '请采集人像,完成后可以使用人脸识别功能',
  200. style: TextStyle(fontSize: 20),
  201. textAlign: TextAlign.center,
  202. ),
  203. ),
  204. confirmLabel: '采集',
  205. cancelLabel: '暂不采集',
  206. showCancel: true,
  207. onConfirm: () async {
  208. await onFaceEntryClicked();
  209. Get.back();
  210. },
  211. onCanceled: () {
  212. Get.back();
  213. },
  214. ));
  215. }
  216. }
  217. }
  218. Future<DeviceModel?> getDevice(String type) async {
  219. List<DeviceModel> devices = await _deviceManager.getDeviceList();
  220. return devices.firstWhereOrNull((element) => element.type == type);
  221. }
  222. /// 点击读卡事件
  223. void onReadCardClicked() async {
  224. final DeviceModel? device = await getDevice(DeviceTypes.IC_READER);
  225. CardReaderResult? result;
  226. if (device != null) {
  227. final envPassed = await checkDeviceConnectEnv();
  228. if (envPassed) {
  229. result = await Get.dialog<CardReaderResult>(
  230. const BluetoothCardReaderDialog(),
  231. barrierDismissible: false,
  232. );
  233. }
  234. } else {
  235. result = await Get.dialog<CardReaderResult>(
  236. const CardReaderDialog(),
  237. barrierDismissible: false,
  238. );
  239. }
  240. if (result != null && result.success) {
  241. PromptBox.toast("读取成功");
  242. state.cardNo = result.cardNo; // 回填身份证号
  243. state.name = result.name; // 回填姓名
  244. state.gender = result.gender; // 回填性别
  245. state.nation = result.nation; // 回填民族
  246. state.birthday = result.birthday; // 回填出生日期
  247. state.censusRegister = result.address; // 回填户籍地址
  248. if (state.isSyncAddresses) {
  249. state.address = result.address; // 回填现住地址
  250. }
  251. } else {
  252. print("读卡取消");
  253. }
  254. }
  255. /// 点击读卡事件 (若已建档跳详情页面 若未建档跳创建页面)
  256. void onReadCardClickedToDetail() async {
  257. final DeviceModel? device = await getDevice(DeviceTypes.IC_READER);
  258. CardReaderResult? result;
  259. if (device != null) {
  260. final envPassed = await checkDeviceConnectEnv();
  261. if (envPassed) {
  262. result = await Get.dialog<CardReaderResult>(
  263. const BluetoothCardReaderDialog(),
  264. barrierDismissible: false,
  265. );
  266. }
  267. } else {
  268. result = await Get.dialog<CardReaderResult>(
  269. const CardReaderDialog(),
  270. barrierDismissible: false,
  271. );
  272. }
  273. if (result != null && result.success) {
  274. PromptBox.toast("读取成功");
  275. PatientBaseDTO patientInfo = PatientBaseDTO();
  276. patientInfo.cardNo = result.cardNo; // 回填身份证号
  277. patientInfo.patientName = result.name; // 回填姓名
  278. patientInfo.patientGender = result.gender; // 回填性别
  279. patientInfo.nationality = result.nation; // 回填民族
  280. patientInfo.birthday = result.birthday; // 回填出生日期
  281. patientInfo.patientAddress = result.address; // 回填户籍地址
  282. onIdcardInfoIsCreateRecord(patientInfo.cardNo!, patientInfo);
  283. } else {
  284. print("读卡取消");
  285. }
  286. }
  287. /// 点击身份识别建档(拍摄扫描身份证)
  288. void onIdCardScanClicked() async {
  289. var verifyResult = await _verifyCameraPermissions();
  290. if (!verifyResult) {
  291. return;
  292. }
  293. final IdCardScanResult? result = await Get.to<IdCardScanResult>(
  294. () => const IdCardScanPage(),
  295. );
  296. if (result != null && result.success) {
  297. PromptBox.toast("身份证信息识别成功");
  298. PatientBaseDTO patientInfo = result.patientBaseDTO;
  299. patientInfoParse(patientInfo);
  300. } else {
  301. print("识别取消");
  302. }
  303. }
  304. /// 点击身份识别(拍摄扫描身份证)
  305. void onIdCardScanClickedToDetail() async {
  306. var verifyResult = await _verifyCameraPermissions();
  307. if (!verifyResult) {
  308. return;
  309. }
  310. final IdCardScanResult? result = await Get.to<IdCardScanResult>(
  311. () => const IdCardScanPage(),
  312. );
  313. if (result != null && result.success) {
  314. PromptBox.toast("身份证信息识别成功");
  315. PatientBaseDTO patientInfo = result.patientBaseDTO;
  316. onIdcardInfoIsCreateRecord(patientInfo.cardNo!, patientInfo);
  317. } else {
  318. print("识别取消");
  319. }
  320. }
  321. void patientInfoParse(PatientBaseDTO patientInfo) {
  322. state.cardNo = patientInfo.cardNo ?? ""; // 回填身份证号
  323. state.name = patientInfo.patientName ?? ""; // 回填姓名
  324. state.gender = patientInfo.patientGender; // 回填性别
  325. state.nation = patientInfo.nationality ?? ""; // 回填民族
  326. state.birthday = patientInfo.birthday; // 回填出生日期
  327. state.censusRegister = patientInfo.patientAddress ?? ""; // 回填户籍地址
  328. if (state.isSyncAddresses) {
  329. state.address = patientInfo.patientAddress ?? ""; // 回填现住地址
  330. }
  331. }
  332. Future<bool> _verifyCameraPermissions() async {
  333. IPermissionManager permissionManager = Get.find<IPermissionManager>();
  334. var isCameraPermissions =
  335. await permissionManager.requestCameraPermissions();
  336. if (!isCameraPermissions) {
  337. await Get.dialog(
  338. VAlertDialog(
  339. title: "提示",
  340. width: 420,
  341. content: Container(
  342. height: 32,
  343. padding: const EdgeInsets.symmetric(horizontal: 24),
  344. alignment: Alignment.center,
  345. child: const Text(
  346. "未授予相机权限,前去设置",
  347. style: TextStyle(fontSize: 20),
  348. ),
  349. ),
  350. showCancel: false,
  351. onConfirm: () async {
  352. Get.back();
  353. await permissionManager.openAppSettingsAsync();
  354. },
  355. ),
  356. barrierDismissible: false,
  357. barrierColor: Colors.black.withOpacity(.4),
  358. );
  359. }
  360. return isCameraPermissions;
  361. }
  362. /// 点击人脸识别
  363. void onFaceIdLoginClicked() async {
  364. var verifyResult = await _verifyCameraPermissions();
  365. if (!verifyResult) {
  366. return;
  367. }
  368. final FaceRecognitionResult? result = await Get.to<FaceRecognitionResult>(
  369. () => const FacialRecognitionPage(
  370. mode: FacialRecognitionMode.faceRecognition,
  371. ),
  372. );
  373. if (result != null && result.success) {
  374. final patient = result.patientInfo;
  375. final hasConfirmed = await FaceResultDialog.show(patient, true);
  376. if (hasConfirmed) {
  377. await _checkinPatient(patient);
  378. }
  379. } else {
  380. print("识别取消");
  381. }
  382. }
  383. void onIdcardInfoIsCreateRecord(
  384. String cardNo, [
  385. PatientBaseDTO? patientInfo,
  386. ]) async {
  387. PatientDTO? patientInfoDto = await _patientManager.getDetail(cardNo);
  388. if (patientInfoDto != null) {
  389. Store.user.currentSelectPatientInfo = patientInfoDto;
  390. }
  391. if (patientInfo != null) {
  392. final hasConfirmed =
  393. await FaceResultDialog.show(patientInfo, patientInfoDto != null);
  394. if (hasConfirmed) {
  395. if (patientInfoDto != null) {
  396. await Get.find<PatientListController>()
  397. .gotoDetail(patientInfo.cardNo!, patientInfoDto, patientInfo);
  398. } else {
  399. await Get.find<PatientListController>().gotoCreate(
  400. patientInfo.cardNo!,
  401. patientInfoDto,
  402. patientInfo,
  403. );
  404. }
  405. }
  406. }
  407. }
  408. /// 点击录入人脸
  409. Future<void> onFaceEntryClicked() async {
  410. final PatientDTO patientInfo = PatientDTO(
  411. patientName: state.name,
  412. phone: state.phoneNo,
  413. emergencyPhone: state.emergencyPhone,
  414. cardNo: state.cardNo,
  415. nationality: state.nation,
  416. birthday: state.birthday,
  417. cardType: state.cardType,
  418. patientGender: state.gender!,
  419. code: state.cardNo,
  420. );
  421. bool? result = await Get.to<bool>(
  422. () => FacialRecognitionPage(
  423. mode: FacialRecognitionMode.faceInput,
  424. patientInfo: patientInfo,
  425. ),
  426. );
  427. if (result != null && result) {
  428. PromptBox.toast('人脸数据存入成功');
  429. }
  430. }
  431. /// 处理 “同户籍地址” 勾选变更事件
  432. void onSyncAddressCheckChanged(bool isChecked) {
  433. state.isSyncAddresses = isChecked;
  434. if (isChecked) {
  435. // 同步户籍地址到现住地址
  436. state.address = state.censusRegister;
  437. } else {
  438. state.address = "";
  439. }
  440. }
  441. /// 处理户籍地址变更
  442. void onCensusRegisterChanged(String value) {
  443. state.censusRegister = value;
  444. if (state.isSyncAddresses) {
  445. state.address = value;
  446. }
  447. }
  448. Future<String> getPatientPhoto(String patientCode) async {
  449. final dto = await _patientManager.getDetail(patientCode);
  450. if (dto != null && dto.photos != null && dto.photos!.isNotEmpty) {
  451. return dto.photos![0];
  452. }
  453. return "";
  454. }
  455. Future<String?> _submitForm() async {
  456. setBusy("正在保存...");
  457. final crowdLabelCodes = crowdLabelsController.state.selectedCodes;
  458. final request = CreatePatientRequest2(
  459. patientName: state.name,
  460. phone: state.phoneNo,
  461. emergencyPhone: state.emergencyPhone,
  462. patientGender: state.gender!,
  463. nationality: state.nation,
  464. birthday: state.birthday?.toUtc(),
  465. cardType: state.cardType,
  466. cardNo: state.cardNo,
  467. patientAddress: state.address,
  468. permanentResidenceAddress: state.censusRegister,
  469. crowdLabels: crowdLabelCodes,
  470. );
  471. final result = await _patientManager.create(request);
  472. return result;
  473. }
  474. bool isNumeric(String str) {
  475. if (str.isEmpty) {
  476. return false;
  477. }
  478. return double.tryParse(str) != null;
  479. }
  480. bool isAlphaNumeric(String str) {
  481. final RegExp alphaNumericRegExp = RegExp(r'^[a-zA-Z0-9]+$');
  482. return alphaNumericRegExp.hasMatch(str);
  483. }
  484. bool isAlphaNumericChineseWithSpace(String str) {
  485. final RegExp alphaNumericChineseWithSpaceRegExp =
  486. RegExp(r'^[a-zA-Z0-9\u4e00-\u9fa5\s]+$');
  487. return alphaNumericChineseWithSpaceRegExp.hasMatch(str);
  488. }
  489. Future<String?> _validateForm() async {
  490. if (state.name.isEmpty) {
  491. return "请填写姓名";
  492. }
  493. // else {
  494. // if (!isAlphaNumericChineseWithSpace(state.name) ||
  495. // state.name.length >= 20) {
  496. // return "姓名只能由中文、字母或数字组成,并且小于20个字符";
  497. // }
  498. // }
  499. if (state.cardNo.isEmpty) {
  500. return "请填写证件号";
  501. }
  502. if (state.phoneNo.isNotEmpty) {
  503. if (state.phoneNo.length != 11 || !isNumeric(state.phoneNo)) {
  504. return "请填写正确的手机号";
  505. }
  506. }
  507. if (state.emergencyPhone.isNotEmpty) {
  508. if (state.emergencyPhone.length != 11 ||
  509. !isNumeric(state.emergencyPhone)) {
  510. return "请填写正确的紧急联系手机号";
  511. }
  512. }
  513. if (state.cardType == CardTypeEnum.Identity) {
  514. bool isNotIDCard = IdCardHelper.validateIDCard(state.cardNo);
  515. if (!isNotIDCard) {
  516. return "请填写正确的证件号";
  517. }
  518. }
  519. if (state.cardType == CardTypeEnum.SocialInsurance) {
  520. if (state.cardNo.length != 18 || !isNumeric(state.cardNo)) {
  521. return "请填写正确的社保号";
  522. }
  523. }
  524. if (state.cardType == CardTypeEnum.Passport) {
  525. if (state.cardNo.length != 9 || !isAlphaNumeric(state.cardNo)) {
  526. return "请填写正确的护照号";
  527. }
  528. }
  529. if (state.gender == null) {
  530. return "请选择性别";
  531. }
  532. /// TODO 需求变更暂时删除
  533. // final selectedNormalCodes = crowdLabelsController.state.selectedNormalCodes;
  534. // if (selectedNormalCodes.length > 1) {
  535. // return "人群分类:一般人群、儿童、孕妇、老年人,只可选择其一!";
  536. // }
  537. // final crowdLabelCodes = crowdLabelsController.state.selectedCodes;
  538. // if (crowdLabelCodes.isEmpty) {
  539. // return "请选择人群分类";
  540. // }
  541. // if (state.gender == GenderEnum.Male &&
  542. // crowdLabelCodes.contains('RQFL_YF')) {
  543. // return "当前居民性别为“男”,人群分类不可选择孕妇!";
  544. // }
  545. return null;
  546. }
  547. /// 切换当前登记居民
  548. Future<void> _checkinPatient(PatientBaseDTO patient) async {
  549. final patientDTO = PatientDTO(
  550. code: patient.cardNo,
  551. cardNo: patient.cardNo,
  552. patientName: patient.patientName,
  553. nationality: patient.nationality,
  554. patientGender: patient.patientGender,
  555. birthday: patient.birthday,
  556. patientAddress: patient.patientAddress,
  557. );
  558. Store.user.currentSelectPatientInfo = patientDTO;
  559. logger.i(
  560. 'create居民 当前居民是:${patientDTO.patientName} 居民code:${patientDTO.code}');
  561. onIdcardInfoIsCreateRecord(patientDTO.code!);
  562. await Get.find<PatientListController>().gotoDetail(
  563. patient.cardNo!,
  564. );
  565. }
  566. Future<void> setGender(String cardNo) async {
  567. if (cardNo.isNotEmpty && cardNo.length > 17) {
  568. var sexNum = cardNo.substring(16, 17);
  569. if (sexNum.isNotEmpty && RegExp(r'^\d+$').hasMatch(sexNum)) {
  570. if (int.parse(sexNum) % 2 == 1) {
  571. state.gender = GenderEnum.Male;
  572. } else {
  573. state.gender = GenderEnum.Female;
  574. }
  575. } else {
  576. state.gender = GenderEnum.Unknown;
  577. }
  578. }
  579. }
  580. }