controller.dart 18 KB

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