controller.dart 20 KB

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