LoginStepFourPanel.vue 8.7 KB

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