LoginStepFourPanel.vue 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. <template>
  2. <q-card-section class="no-padding">
  3. <div class="column items-center q-gutter-y-xl q-pt-md">
  4. <div class="column items-center q-gutter-y-sm">
  5. <div
  6. class="photo-circle"
  7. :class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
  8. @click="takeSelfie"
  9. >
  10. <q-icon
  11. color="white"
  12. name="photo_camera"
  13. size="52px"
  14. />
  15. </div>
  16. <template v-if="!selfieDone">
  17. <q-btn
  18. color="primary-button"
  19. no-caps
  20. padding="10px 28px"
  21. rounded
  22. :label="t('provider.login.steps.step_4.btn_selfie')"
  23. :loading="loadingSelfie"
  24. @click="takeSelfie"
  25. />
  26. <div class="text-text text-center font12">{{ t('provider.login.steps.step_4.selfie_hint') }}</div>
  27. </template>
  28. <template v-else>
  29. <div class="row items-center q-gutter-x-xs">
  30. <q-icon
  31. color="positive"
  32. name="check_circle"
  33. size="18px"
  34. />
  35. <span class="text-primary">
  36. {{ t('provider.login.steps.step_4.selfie_sent') }}
  37. </span>
  38. </div>
  39. </template>
  40. </div>
  41. <div class="column items-center q-gutter-y-sm">
  42. <div
  43. class="photo-circle"
  44. :class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
  45. @click="takeDocFront"
  46. >
  47. <q-icon
  48. color="white"
  49. name="mdi-card-account-details-outline"
  50. size="52px"
  51. />
  52. </div>
  53. <template v-if="!docDone">
  54. <q-btn
  55. color="primary-button"
  56. no-caps
  57. padding="10px 28px"
  58. rounded
  59. :label="t('provider.login.steps.step_4.btn_document')"
  60. :loading="loadingDoc"
  61. @click="takeDocFront"
  62. />
  63. <div class="text-text text-center font12">
  64. {{ t('provider.login.steps.step_4.document_hint') }}
  65. </div>
  66. </template>
  67. <template v-else>
  68. <div class="row items-center q-gutter-x-xs">
  69. <q-icon
  70. color="positive"
  71. name="check_circle"
  72. size="18px"
  73. />
  74. <span class="text-primary">
  75. {{ t('provider.login.steps.step_4.document_sent') }}
  76. </span>
  77. </div>
  78. </template>
  79. </div>
  80. </div>
  81. <Teleport to="body">
  82. <div v-if="showResult" class="fs-overlay">
  83. <div class="fs-result">
  84. <q-img
  85. class="q-mb-xl"
  86. style="max-width: 150px"
  87. :src="LogoDiaria"
  88. />
  89. <div
  90. class="result-circle"
  91. :class="resultError ? 'result-circle--error' : 'result-circle--done'"
  92. >
  93. <q-icon
  94. color="white"
  95. size="56px"
  96. :name="resultIcon"
  97. />
  98. </div>
  99. <template v-if="resultError">
  100. <q-icon
  101. class="q-mt-md"
  102. color="negative"
  103. name="cancel"
  104. size="28px"
  105. />
  106. <div class="text-primary text-center q-mt-xs q-px-lg font14">
  107. {{ t('provider.login.steps.step_4.error_message') }}
  108. </div>
  109. </template>
  110. <div v-else class="text-primary text-center q-mt-md q-px-lg font14">
  111. {{ resultMessage }}
  112. </div>
  113. <q-btn
  114. class="q-mt-xl"
  115. color="primary-button"
  116. no-caps
  117. padding="14px 48px"
  118. rounded
  119. style="min-width: 200px"
  120. :label="resultError ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
  121. @click="closeResult"
  122. />
  123. <div v-if="!resultError && showDocHint" class="text-grey-6 text-center q-mt-sm font12">
  124. {{ t('provider.login.steps.step_4.document_hint_result') }}
  125. </div>
  126. </div>
  127. </div>
  128. </Teleport>
  129. </q-card-section>
  130. </template>
  131. <script setup>
  132. import { Camera, CameraDirection, CameraResultType, CameraSource } from '@capacitor/camera';
  133. import { computed, ref } from 'vue';
  134. import { useI18n } from 'vue-i18n';
  135. import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
  136. const { t } = useI18n();
  137. const form = defineModel({ type: Object, required: true });
  138. const emit = defineEmits(['update:show-sub-step']);
  139. const loadingSelfie = ref(false);
  140. const loadingDoc = ref(false);
  141. const showResult = ref(false);
  142. const resultError = ref(false);
  143. const resultIcon = ref('photo_camera');
  144. const resultMessage = ref('');
  145. const showDocHint = ref(false);
  146. // foto de documento frente — aguarda verso antes de fechar
  147. const tempDocFrontFile = ref(null);
  148. const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
  149. const selfieDone = computed(() => !!form.value.selfie);
  150. const base64ToFile = (base64, filename, mimeType = 'image/jpeg') => {
  151. const byteString = atob(base64);
  152. const ab = new ArrayBuffer(byteString.length);
  153. const ia = new Uint8Array(ab);
  154. for (let i = 0; i < byteString.length; i++) {
  155. ia[i] = byteString.charCodeAt(i);
  156. }
  157. return new File([ab], filename, { type: mimeType });
  158. };
  159. const closeResult = () => {
  160. showResult.value = false;
  161. emit('update:show-sub-step', false);
  162. if (resultError.value) {
  163. resultError.value = false;
  164. }
  165. };
  166. const openCamera = async (direction) => {
  167. const photo = await Camera.getPhoto({
  168. resultType: CameraResultType.Base64,
  169. source: CameraSource.Camera,
  170. quality: 85,
  171. direction: direction === 'front' ? CameraDirection.Front : CameraDirection.Rear,
  172. allowEditing: false,
  173. saveToGallery: false,
  174. });
  175. return base64ToFile(photo.base64String.replace(/\s/g, ''), 'photo.jpg', `image/${photo.format}`);
  176. };
  177. const takeDocFront = async () => {
  178. if (loadingDoc.value) return;
  179. loadingDoc.value = true;
  180. try {
  181. const file = await openCamera('rear');
  182. tempDocFrontFile.value = file;
  183. await takeDocBack();
  184. } catch (err) {
  185. const msg = (err?.message || '').toLowerCase();
  186. if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
  187. resultIcon.value = 'mdi-card-account-details-outline';
  188. resultError.value = true;
  189. showDocHint.value = false;
  190. showResult.value = true;
  191. emit('update:show-sub-step', true);
  192. }
  193. } finally {
  194. loadingDoc.value = false;
  195. }
  196. };
  197. const takeDocBack = async () => {
  198. try {
  199. const file = await openCamera('rear');
  200. form.value.document_front = tempDocFrontFile.value;
  201. form.value.document_back = file;
  202. tempDocFrontFile.value = null;
  203. resultIcon.value = 'mdi-card-account-details-outline';
  204. resultMessage.value = t('provider.login.steps.step_4.document_success');
  205. resultError.value = false;
  206. showDocHint.value = true;
  207. showResult.value = true;
  208. emit('update:show-sub-step', true);
  209. } catch (err) {
  210. const msg = (err?.message || '').toLowerCase();
  211. if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
  212. resultIcon.value = 'mdi-card-account-details-outline';
  213. resultError.value = true;
  214. showDocHint.value = false;
  215. showResult.value = true;
  216. emit('update:show-sub-step', true);
  217. }
  218. tempDocFrontFile.value = null;
  219. }
  220. };
  221. const takeSelfie = async () => {
  222. if (loadingSelfie.value) return;
  223. loadingSelfie.value = true;
  224. try {
  225. const file = await openCamera('front');
  226. form.value.selfie = file;
  227. resultIcon.value = 'photo_camera';
  228. resultMessage.value = t('provider.login.steps.step_4.selfie_success');
  229. resultError.value = false;
  230. showDocHint.value = false;
  231. showResult.value = true;
  232. emit('update:show-sub-step', true);
  233. } catch (err) {
  234. const msg = (err?.message || '').toLowerCase();
  235. if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
  236. resultIcon.value = 'photo_camera';
  237. resultError.value = true;
  238. showDocHint.value = false;
  239. showResult.value = true;
  240. emit('update:show-sub-step', true);
  241. }
  242. } finally {
  243. loadingSelfie.value = false;
  244. }
  245. };
  246. </script>
  247. <style lang="scss" scoped>
  248. .photo-circle {
  249. width: 130px;
  250. height: 130px;
  251. border-radius: 50%;
  252. display: flex;
  253. align-items: center;
  254. justify-content: center;
  255. cursor: pointer;
  256. transition: transform 0.15s ease;
  257. &:active { transform: scale(0.95); }
  258. &--pending {
  259. background-color: rgba(139, 92, 246, 0.18);
  260. }
  261. &--done {
  262. background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
  263. }
  264. }
  265. .fs-overlay {
  266. position: fixed;
  267. inset: 0;
  268. z-index: 9999;
  269. background: white;
  270. }
  271. .fs-result {
  272. height: 100%;
  273. display: flex;
  274. flex-direction: column;
  275. align-items: center;
  276. justify-content: center;
  277. padding: 40px 28px;
  278. background: white;
  279. }
  280. .result-circle {
  281. width: 150px;
  282. height: 150px;
  283. border-radius: 50%;
  284. display: flex;
  285. align-items: center;
  286. justify-content: center;
  287. &--done {
  288. background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
  289. }
  290. &--error {
  291. background-color: rgba(139, 92, 246, 0.18);
  292. }
  293. }
  294. </style>