| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356 |
- <template>
- <q-card-section class="no-padding">
- <div class="column items-center q-gutter-y-xl q-pt-md">
- <div class="column items-center q-gutter-y-sm">
- <div
- class="photo-circle"
- :class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
- @click="takeSelfie"
- >
- <q-icon
- color="white"
- name="photo_camera"
- size="52px"
- />
- </div>
- <template v-if="!selfieDone">
- <q-btn
- color="primary-button"
- no-caps
- padding="10px 28px"
- rounded
- :label="t('provider.login.steps.step_4.btn_selfie')"
- :loading="loadingSelfie"
- @click="takeSelfie"
- />
- <div class="text-text text-center font12">{{ t('provider.login.steps.step_4.selfie_hint') }}</div>
- </template>
- <template v-else>
- <div class="row items-center q-gutter-x-xs">
- <q-icon
- color="positive"
- name="check_circle"
- size="18px"
- />
- <span class="text-primary">
- {{ t('provider.login.steps.step_4.selfie_sent') }}
- </span>
- </div>
- </template>
- </div>
- <div class="column items-center q-gutter-y-sm">
- <div
- class="photo-circle"
- :class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
- @click="takeDocFront"
- >
- <q-icon
- color="white"
- name="mdi-card-account-details-outline"
- size="52px"
- />
- </div>
- <template v-if="!docDone">
- <q-btn
- color="primary-button"
- no-caps
- padding="10px 28px"
- rounded
- :label="t('provider.login.steps.step_4.btn_document')"
- :loading="loadingDoc"
- @click="takeDocFront"
- />
- <div class="text-text text-center font12">
- {{ t('provider.login.steps.step_4.document_hint') }}
- </div>
- </template>
- <template v-else>
- <div class="row items-center q-gutter-x-xs">
- <q-icon
- color="positive"
- name="check_circle"
- size="18px"
- />
- <span class="text-primary">
- {{ t('provider.login.steps.step_4.document_sent') }}
- </span>
- </div>
- </template>
- </div>
- </div>
- <Teleport to="body">
- <div v-if="showResult" class="fs-overlay">
- <div class="fs-result">
- <q-img
- class="q-mb-xl"
- style="max-width: 150px"
- :src="LogoDiaria"
- />
- <div
- class="result-circle"
- :class="resultError ? 'result-circle--error' : 'result-circle--done'"
- >
- <q-icon
- color="white"
- size="56px"
- :name="resultIcon"
- />
- </div>
- <template v-if="resultError">
- <q-icon
- class="q-mt-md"
- color="negative"
- name="cancel"
- size="28px"
- />
- <div class="text-primary text-center q-mt-xs q-px-lg font14">
- {{ t('provider.login.steps.step_4.error_message') }}
- </div>
- </template>
- <div v-else class="text-primary text-center q-mt-md q-px-lg font14">
- {{ resultMessage }}
- </div>
- <q-btn
- class="q-mt-xl"
- color="primary-button"
- no-caps
- padding="14px 48px"
- rounded
- style="min-width: 200px"
- :label="resultError ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
- @click="closeResult"
- />
- <div v-if="!resultError && showDocHint" class="text-grey-6 text-center q-mt-sm font12">
- {{ t('provider.login.steps.step_4.document_hint_result') }}
- </div>
- </div>
- </div>
- </Teleport>
- </q-card-section>
- </template>
- <script setup>
- import { Camera, CameraDirection, CameraResultType, CameraSource } from '@capacitor/camera';
- import { computed, ref } from 'vue';
- import { useI18n } from 'vue-i18n';
- import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
- const { t } = useI18n();
- const form = defineModel({ type: Object, required: true });
- const emit = defineEmits(['update:show-sub-step']);
- const loadingSelfie = ref(false);
- const loadingDoc = ref(false);
- const showResult = ref(false);
- const resultError = ref(false);
- const resultIcon = ref('photo_camera');
- const resultMessage = ref('');
- const showDocHint = ref(false);
- // foto de documento frente — aguarda verso antes de fechar
- const tempDocFrontFile = ref(null);
- const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
- const selfieDone = computed(() => !!form.value.selfie);
- const base64ToFile = (base64, filename, mimeType = 'image/jpeg') => {
- const byteString = atob(base64);
- const ab = new ArrayBuffer(byteString.length);
- const ia = new Uint8Array(ab);
- for (let i = 0; i < byteString.length; i++) {
- ia[i] = byteString.charCodeAt(i);
- }
- return new File([ab], filename, { type: mimeType });
- };
- const closeResult = () => {
- showResult.value = false;
- emit('update:show-sub-step', false);
- if (resultError.value) {
- resultError.value = false;
- }
- };
- const openCamera = async (direction) => {
- const photo = await Camera.getPhoto({
- resultType: CameraResultType.Base64,
- source: CameraSource.Camera,
- quality: 85,
- direction: direction === 'front' ? CameraDirection.Front : CameraDirection.Rear,
- allowEditing: false,
- saveToGallery: false,
- });
- return base64ToFile(photo.base64String.replace(/\s/g, ''), 'photo.jpg', `image/${photo.format}`);
- };
- const takeDocFront = async () => {
- if (loadingDoc.value) return;
- loadingDoc.value = true;
- try {
- const file = await openCamera('rear');
- tempDocFrontFile.value = file;
- await takeDocBack();
- } catch (err) {
- const msg = (err?.message || '').toLowerCase();
- if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
- resultIcon.value = 'mdi-card-account-details-outline';
- resultError.value = true;
- showDocHint.value = false;
- showResult.value = true;
- emit('update:show-sub-step', true);
- }
- } finally {
- loadingDoc.value = false;
- }
- };
- const takeDocBack = async () => {
- try {
- const file = await openCamera('rear');
- form.value.document_front = tempDocFrontFile.value;
- form.value.document_back = file;
- tempDocFrontFile.value = null;
- resultIcon.value = 'mdi-card-account-details-outline';
- resultMessage.value = t('provider.login.steps.step_4.document_success');
- resultError.value = false;
- showDocHint.value = true;
- showResult.value = true;
- emit('update:show-sub-step', true);
- } catch (err) {
- const msg = (err?.message || '').toLowerCase();
- if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
- resultIcon.value = 'mdi-card-account-details-outline';
- resultError.value = true;
- showDocHint.value = false;
- showResult.value = true;
- emit('update:show-sub-step', true);
- }
- tempDocFrontFile.value = null;
- }
- };
- const takeSelfie = async () => {
- if (loadingSelfie.value) return;
- loadingSelfie.value = true;
- try {
- const file = await openCamera('front');
- form.value.selfie = file;
- resultIcon.value = 'photo_camera';
- resultMessage.value = t('provider.login.steps.step_4.selfie_success');
- resultError.value = false;
- showDocHint.value = false;
- showResult.value = true;
- emit('update:show-sub-step', true);
- } catch (err) {
- const msg = (err?.message || '').toLowerCase();
- if (!msg.includes('cancel') && !msg.includes('dismiss') && !msg.includes('no image')) {
- resultIcon.value = 'photo_camera';
- resultError.value = true;
- showDocHint.value = false;
- showResult.value = true;
- emit('update:show-sub-step', true);
- }
- } finally {
- loadingSelfie.value = false;
- }
- };
- </script>
- <style lang="scss" scoped>
- .photo-circle {
- width: 130px;
- height: 130px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- cursor: pointer;
- transition: transform 0.15s ease;
- &:active { transform: scale(0.95); }
- &--pending {
- background-color: rgba(139, 92, 246, 0.18);
- }
- &--done {
- background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
- }
- }
- .fs-overlay {
- position: fixed;
- inset: 0;
- z-index: 9999;
- background: white;
- }
- .fs-result {
- height: 100%;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- padding: 40px 28px;
- background: white;
- }
- .result-circle {
- width: 150px;
- height: 150px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- &--done {
- background: linear-gradient(-90deg, #ec48d1 5%, #6b11cb 65%, #2574fc 100%);
- }
- &--error {
- background-color: rgba(139, 92, 246, 0.18);
- }
- }
- </style>
|