|
@@ -1,31 +1,43 @@
|
|
|
<template>
|
|
<template>
|
|
|
<q-card-section class="no-padding">
|
|
<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-xl q-pt-md">
|
|
|
-
|
|
|
|
|
<div class="column items-center q-gutter-y-sm">
|
|
<div class="column items-center q-gutter-y-sm">
|
|
|
<div
|
|
<div
|
|
|
class="photo-circle"
|
|
class="photo-circle"
|
|
|
:class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
|
|
:class="selfieDone ? 'photo-circle--done' : 'photo-circle--pending'"
|
|
|
- @click="subStep = 'selfie_camera'"
|
|
|
|
|
|
|
+ @click="takeSelfie"
|
|
|
>
|
|
>
|
|
|
- <q-icon name="photo_camera" size="52px" color="white" />
|
|
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="white"
|
|
|
|
|
+ name="photo_camera"
|
|
|
|
|
+ size="52px"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<template v-if="!selfieDone">
|
|
<template v-if="!selfieDone">
|
|
|
<q-btn
|
|
<q-btn
|
|
|
color="primary-button"
|
|
color="primary-button"
|
|
|
- :label="t('provider.login.steps.step_4.btn_selfie')"
|
|
|
|
|
- rounded
|
|
|
|
|
no-caps
|
|
no-caps
|
|
|
padding="10px 28px"
|
|
padding="10px 28px"
|
|
|
- @click="subStep = 'selfie_camera'"
|
|
|
|
|
|
|
+ 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>
|
|
<div class="text-text text-center font12">{{ t('provider.login.steps.step_4.selfie_hint') }}</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
<div class="row items-center q-gutter-x-xs">
|
|
<div class="row items-center q-gutter-x-xs">
|
|
|
- <q-icon name="check_circle" color="positive" size="18px" />
|
|
|
|
|
- <span class="text-primary">{{ t('provider.login.steps.step_4.selfie_sent') }}</span>
|
|
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ name="check_circle"
|
|
|
|
|
+ size="18px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="text-primary">
|
|
|
|
|
+ {{ t('provider.login.steps.step_4.selfie_sent') }}
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
@@ -34,140 +46,109 @@
|
|
|
<div
|
|
<div
|
|
|
class="photo-circle"
|
|
class="photo-circle"
|
|
|
:class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
|
|
:class="docDone ? 'photo-circle--done' : 'photo-circle--pending'"
|
|
|
- @click="subStep = 'doc_front_camera'"
|
|
|
|
|
|
|
+ @click="takeDocFront"
|
|
|
>
|
|
>
|
|
|
- <q-icon name="mdi-card-account-details-outline" size="52px" color="white" />
|
|
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="white"
|
|
|
|
|
+ name="mdi-card-account-details-outline"
|
|
|
|
|
+ size="52px"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<template v-if="!docDone">
|
|
<template v-if="!docDone">
|
|
|
<q-btn
|
|
<q-btn
|
|
|
color="primary-button"
|
|
color="primary-button"
|
|
|
- :label="t('provider.login.steps.step_4.btn_document')"
|
|
|
|
|
- rounded
|
|
|
|
|
no-caps
|
|
no-caps
|
|
|
padding="10px 28px"
|
|
padding="10px 28px"
|
|
|
- @click="subStep = 'doc_front_camera'"
|
|
|
|
|
|
|
+ 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>
|
|
|
|
|
|
|
+
|
|
|
|
|
+ <div class="text-text text-center font12">
|
|
|
|
|
+ {{ t('provider.login.steps.step_4.document_hint') }}
|
|
|
|
|
+ </div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
<template v-else>
|
|
<template v-else>
|
|
|
<div class="row items-center q-gutter-x-xs">
|
|
<div class="row items-center q-gutter-x-xs">
|
|
|
- <q-icon name="check_circle" color="positive" size="18px" />
|
|
|
|
|
- <span class="text-primary">{{ t('provider.login.steps.step_4.document_sent') }}</span>
|
|
|
|
|
|
|
+ <q-icon
|
|
|
|
|
+ color="positive"
|
|
|
|
|
+ name="check_circle"
|
|
|
|
|
+ size="18px"
|
|
|
|
|
+ />
|
|
|
|
|
+
|
|
|
|
|
+ <span class="text-primary">
|
|
|
|
|
+ {{ t('provider.login.steps.step_4.document_sent') }}
|
|
|
|
|
+ </span>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<Teleport to="body">
|
|
<Teleport to="body">
|
|
|
- <div v-if="subStep !== 'main'" class="fs-overlay">
|
|
|
|
|
- <div v-if="isCameraState" class="fs-camera">
|
|
|
|
|
- <div class="fs-header">
|
|
|
|
|
- <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
|
|
|
|
|
- <span class="font14">{{ t('common.actions.back') }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="fs-viewport">
|
|
|
|
|
- <video ref="videoRef" autoplay playsinline muted class="fs-video" />
|
|
|
|
|
- <div v-if="subStep === 'selfie_camera'" class="guide-oval" />
|
|
|
|
|
- <div v-else class="guide-doc" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="fs-footer">
|
|
|
|
|
- <q-btn
|
|
|
|
|
- color="primary-button"
|
|
|
|
|
- :label="captureLabel"
|
|
|
|
|
- rounded
|
|
|
|
|
- no-caps
|
|
|
|
|
- padding="14px 32px"
|
|
|
|
|
- class="full-width"
|
|
|
|
|
- @click="capturePhoto"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-else-if="isPreviewState" class="fs-camera">
|
|
|
|
|
- <div class="fs-header">
|
|
|
|
|
- <q-btn flat dense round icon="chevron_left" color="black" @click="goBack" />
|
|
|
|
|
- <span class="font14">{{ t('common.actions.back') }}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="fs-viewport">
|
|
|
|
|
- <img :src="currentPreview" class="fs-video fs-preview-img" />
|
|
|
|
|
- <div v-if="subStep === 'selfie_preview'" class="guide-oval" />
|
|
|
|
|
- <div v-else class="guide-doc" />
|
|
|
|
|
- </div>
|
|
|
|
|
- <div class="fs-footer column q-gutter-y-sm">
|
|
|
|
|
- <q-btn
|
|
|
|
|
- outline
|
|
|
|
|
- color="primary-button"
|
|
|
|
|
- :label="t('provider.login.steps.step_4.btn_retake')"
|
|
|
|
|
- rounded
|
|
|
|
|
- no-caps
|
|
|
|
|
- padding="12px 32px"
|
|
|
|
|
- class="full-width"
|
|
|
|
|
- @click="retakePhoto"
|
|
|
|
|
- />
|
|
|
|
|
- <q-btn
|
|
|
|
|
- color="primary-button"
|
|
|
|
|
- :label="subStep === 'selfie_preview' ? t('provider.login.steps.step_4.btn_confirm') : t('provider.login.steps.step_4.btn_use_photo')"
|
|
|
|
|
- rounded
|
|
|
|
|
- no-caps
|
|
|
|
|
- padding="12px 32px"
|
|
|
|
|
- class="full-width"
|
|
|
|
|
- @click="usePhoto"
|
|
|
|
|
- />
|
|
|
|
|
- </div>
|
|
|
|
|
- </div>
|
|
|
|
|
-
|
|
|
|
|
- <div v-else class="fs-result">
|
|
|
|
|
- <q-img :src="LogoDiaria" class="q-mb-xl" style="max-width: 150px" />
|
|
|
|
|
|
|
+ <div v-if="showResult" class="fs-overlay">
|
|
|
|
|
+ <div class="fs-result">
|
|
|
|
|
+ <q-img
|
|
|
|
|
+ class="q-mb-xl"
|
|
|
|
|
+ style="max-width: 150px"
|
|
|
|
|
+ :src="LogoDiaria"
|
|
|
|
|
+ />
|
|
|
|
|
|
|
|
<div
|
|
<div
|
|
|
class="result-circle"
|
|
class="result-circle"
|
|
|
- :class="docResult === 'error' ? 'result-circle--error' : 'result-circle--done'"
|
|
|
|
|
|
|
+ :class="resultError ? 'result-circle--error' : 'result-circle--done'"
|
|
|
>
|
|
>
|
|
|
<q-icon
|
|
<q-icon
|
|
|
- :name="subStep === 'selfie_result' ? 'photo_camera' : 'mdi-card-account-details-outline'"
|
|
|
|
|
- size="56px"
|
|
|
|
|
color="white"
|
|
color="white"
|
|
|
|
|
+ size="56px"
|
|
|
|
|
+ :name="resultIcon"
|
|
|
/>
|
|
/>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <template v-if="docResult === 'error'">
|
|
|
|
|
- <q-icon name="cancel" color="negative" size="28px" class="q-mt-md" />
|
|
|
|
|
|
|
+ <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">
|
|
<div class="text-primary text-center q-mt-xs q-px-lg font14">
|
|
|
{{ t('provider.login.steps.step_4.error_message') }}
|
|
{{ t('provider.login.steps.step_4.error_message') }}
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
+
|
|
|
<div v-else class="text-primary text-center q-mt-md q-px-lg font14">
|
|
<div v-else class="text-primary text-center q-mt-md q-px-lg font14">
|
|
|
- {{ subStep === 'selfie_result' ? t('provider.login.steps.step_4.selfie_success') : t('provider.login.steps.step_4.document_success') }}
|
|
|
|
|
|
|
+ {{ resultMessage }}
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<q-btn
|
|
<q-btn
|
|
|
|
|
+ class="q-mt-xl"
|
|
|
color="primary-button"
|
|
color="primary-button"
|
|
|
- :label="docResult === 'error' ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
|
|
|
|
|
- rounded
|
|
|
|
|
no-caps
|
|
no-caps
|
|
|
padding="14px 48px"
|
|
padding="14px 48px"
|
|
|
- class="q-mt-xl"
|
|
|
|
|
|
|
+ rounded
|
|
|
style="min-width: 200px"
|
|
style="min-width: 200px"
|
|
|
- @click="handleResult"
|
|
|
|
|
|
|
+ :label="resultError ? t('provider.login.steps.step_4.btn_retry') : t('provider.login.steps.step_4.btn_continue')"
|
|
|
|
|
+ @click="closeResult"
|
|
|
/>
|
|
/>
|
|
|
|
|
|
|
|
- <div v-if="subStep === 'doc_result' && docResult !== 'error'" class="text-grey-6 text-center q-mt-sm font12">
|
|
|
|
|
|
|
+ <div v-if="!resultError && showDocHint" class="text-grey-6 text-center q-mt-sm font12">
|
|
|
{{ t('provider.login.steps.step_4.document_hint_result') }}
|
|
{{ t('provider.login.steps.step_4.document_hint_result') }}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
-
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</Teleport>
|
|
</Teleport>
|
|
|
-
|
|
|
|
|
- <canvas ref="canvasRef" style="display: none" />
|
|
|
|
|
</q-card-section>
|
|
</q-card-section>
|
|
|
</template>
|
|
</template>
|
|
|
|
|
|
|
|
<script setup>
|
|
<script setup>
|
|
|
-import { ref, computed, watch, onUnmounted, nextTick } from 'vue';
|
|
|
|
|
|
|
+import { Camera, CameraDirection, CameraResultType, CameraSource } from '@capacitor/camera';
|
|
|
|
|
+import { computed, ref } from 'vue';
|
|
|
import { useI18n } from 'vue-i18n';
|
|
import { useI18n } from 'vue-i18n';
|
|
|
|
|
+
|
|
|
import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
|
|
import LogoDiaria from 'src/assets/logo_diaria_campos_login.svg';
|
|
|
|
|
|
|
|
const { t } = useI18n();
|
|
const { t } = useI18n();
|
|
@@ -175,168 +156,144 @@ const { t } = useI18n();
|
|
|
const form = defineModel({ type: Object, required: true });
|
|
const form = defineModel({ type: Object, required: true });
|
|
|
const emit = defineEmits(['update:show-sub-step']);
|
|
const emit = defineEmits(['update:show-sub-step']);
|
|
|
|
|
|
|
|
-const subStep = ref('main');
|
|
|
|
|
-const docResult = ref(null); // null | 'success' | 'error'
|
|
|
|
|
|
|
+const loadingSelfie = ref(false);
|
|
|
|
|
+const loadingDoc = ref(false);
|
|
|
|
|
|
|
|
-const tempDocFront = ref(null);
|
|
|
|
|
-const tempDocBack = ref(null);
|
|
|
|
|
-const tempSelfie = ref(null);
|
|
|
|
|
|
|
+const showResult = ref(false);
|
|
|
|
|
+const resultError = ref(false);
|
|
|
|
|
+const resultIcon = ref('photo_camera');
|
|
|
|
|
+const resultMessage = ref('');
|
|
|
|
|
+const showDocHint = ref(false);
|
|
|
|
|
|
|
|
-const tempSelfieFile = ref(null);
|
|
|
|
|
|
|
+// foto de documento frente — aguarda verso antes de fechar
|
|
|
const tempDocFrontFile = ref(null);
|
|
const tempDocFrontFile = ref(null);
|
|
|
-const tempDocBackFile = ref(null);
|
|
|
|
|
|
|
|
|
|
-const stream = ref(null);
|
|
|
|
|
-const videoRef = ref(null);
|
|
|
|
|
-const canvasRef = ref(null);
|
|
|
|
|
|
|
+const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
|
|
|
|
|
+const selfieDone = computed(() => !!form.value.selfie);
|
|
|
|
|
|
|
|
-const CAMERA_STATES = ['selfie_camera', 'doc_front_camera', 'doc_back_camera'];
|
|
|
|
|
-const PREVIEW_STATES = ['selfie_preview', 'doc_front_preview', 'doc_back_preview'];
|
|
|
|
|
|
|
+const base64ToFile = (base64, filename, mimeType = 'image/jpeg') => {
|
|
|
|
|
+ const byteString = atob(base64);
|
|
|
|
|
|
|
|
-const isCameraState = computed(() => CAMERA_STATES.includes(subStep.value));
|
|
|
|
|
-const isPreviewState = computed(() => PREVIEW_STATES.includes(subStep.value));
|
|
|
|
|
|
|
+ const ab = new ArrayBuffer(byteString.length);
|
|
|
|
|
|
|
|
-const selfieDone = computed(() => !!form.value.selfie);
|
|
|
|
|
-const docDone = computed(() => !!form.value.document_front && !!form.value.document_back);
|
|
|
|
|
|
|
+ const ia = new Uint8Array(ab);
|
|
|
|
|
|
|
|
-const currentPreview = computed(() => {
|
|
|
|
|
- if (subStep.value === 'selfie_preview') return tempSelfie.value;
|
|
|
|
|
- if (subStep.value === 'doc_front_preview') return tempDocFront.value;
|
|
|
|
|
- if (subStep.value === 'doc_back_preview') return tempDocBack.value;
|
|
|
|
|
- return null;
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const captureLabel = computed(() => {
|
|
|
|
|
- if (subStep.value === 'selfie_camera') return t('provider.login.steps.step_4.btn_capture_selfie');
|
|
|
|
|
- if (subStep.value === 'doc_front_camera') return t('provider.login.steps.step_4.btn_capture_front');
|
|
|
|
|
- return t('provider.login.steps.step_4.btn_capture_back');
|
|
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-const stopCamera = () => {
|
|
|
|
|
- if (stream.value) {
|
|
|
|
|
- stream.value.getTracks().forEach((t) => t.stop());
|
|
|
|
|
- stream.value = null;
|
|
|
|
|
- }
|
|
|
|
|
- if (videoRef.value) {
|
|
|
|
|
- videoRef.value.srcObject = null;
|
|
|
|
|
|
|
+ for (let i = 0; i < byteString.length; i++) {
|
|
|
|
|
+ ia[i] = byteString.charCodeAt(i);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ return new File([ab], filename, { type: mimeType });
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const startCameraStream = async (facingMode) => {
|
|
|
|
|
- try {
|
|
|
|
|
- stopCamera();
|
|
|
|
|
- stream.value = await navigator.mediaDevices.getUserMedia({
|
|
|
|
|
- video: { facingMode, width: { ideal: 1280 }, height: { ideal: 720 } },
|
|
|
|
|
- });
|
|
|
|
|
- await nextTick();
|
|
|
|
|
- if (videoRef.value) {
|
|
|
|
|
- videoRef.value.srcObject = stream.value;
|
|
|
|
|
- await videoRef.value.play().catch(() => {});
|
|
|
|
|
- }
|
|
|
|
|
- return true;
|
|
|
|
|
- } catch {
|
|
|
|
|
- stopCamera();
|
|
|
|
|
- return false;
|
|
|
|
|
|
|
+const closeResult = () => {
|
|
|
|
|
+ showResult.value = false;
|
|
|
|
|
+
|
|
|
|
|
+ emit('update:show-sub-step', false);
|
|
|
|
|
+
|
|
|
|
|
+ if (resultError.value) {
|
|
|
|
|
+ resultError.value = false;
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-watch(subStep, async (newStep) => {
|
|
|
|
|
- emit('update:show-sub-step', newStep !== 'main');
|
|
|
|
|
|
|
+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}`);
|
|
|
|
|
+};
|
|
|
|
|
|
|
|
- if (!CAMERA_STATES.includes(newStep)) {
|
|
|
|
|
- stopCamera();
|
|
|
|
|
- }
|
|
|
|
|
|
|
+const takeDocFront = async () => {
|
|
|
|
|
+ if (loadingDoc.value) return;
|
|
|
|
|
+
|
|
|
|
|
+ loadingDoc.value = true;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const file = await openCamera('rear');
|
|
|
|
|
|
|
|
- if (newStep === 'selfie_camera') {
|
|
|
|
|
- const ok = await startCameraStream('user');
|
|
|
|
|
- if (!ok) subStep.value = 'main';
|
|
|
|
|
- } else if (newStep === 'doc_front_camera' || newStep === 'doc_back_camera') {
|
|
|
|
|
- const ok = await startCameraStream('environment');
|
|
|
|
|
- if (!ok) {
|
|
|
|
|
- docResult.value = 'error';
|
|
|
|
|
- subStep.value = 'doc_result';
|
|
|
|
|
|
|
+ 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;
|
|
|
}
|
|
}
|
|
|
-});
|
|
|
|
|
-
|
|
|
|
|
-onUnmounted(stopCamera);
|
|
|
|
|
-
|
|
|
|
|
-const capturePhoto = () => {
|
|
|
|
|
- const video = videoRef.value;
|
|
|
|
|
- const canvas = canvasRef.value;
|
|
|
|
|
- if (!video || !canvas) return;
|
|
|
|
|
-
|
|
|
|
|
- canvas.width = video.videoWidth || 1280;
|
|
|
|
|
- canvas.height = video.videoHeight || 720;
|
|
|
|
|
- canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
|
|
|
|
|
-
|
|
|
|
|
- const capturing = subStep.value;
|
|
|
|
|
- canvas.toBlob((blob) => {
|
|
|
|
|
- const file = new File([blob], 'photo.jpg', { type: 'image/jpeg' });
|
|
|
|
|
- const preview = URL.createObjectURL(blob);
|
|
|
|
|
-
|
|
|
|
|
- if (capturing === 'selfie_camera') {
|
|
|
|
|
- if (tempSelfie.value) URL.revokeObjectURL(tempSelfie.value);
|
|
|
|
|
- tempSelfie.value = preview;
|
|
|
|
|
- tempSelfieFile.value = file;
|
|
|
|
|
- subStep.value = 'selfie_preview';
|
|
|
|
|
- } else if (capturing === 'doc_front_camera') {
|
|
|
|
|
- if (tempDocFront.value) URL.revokeObjectURL(tempDocFront.value);
|
|
|
|
|
- tempDocFront.value = preview;
|
|
|
|
|
- tempDocFrontFile.value = file;
|
|
|
|
|
- subStep.value = 'doc_front_preview';
|
|
|
|
|
- } else if (capturing === 'doc_back_camera') {
|
|
|
|
|
- if (tempDocBack.value) URL.revokeObjectURL(tempDocBack.value);
|
|
|
|
|
- tempDocBack.value = preview;
|
|
|
|
|
- tempDocBackFile.value = file;
|
|
|
|
|
- subStep.value = 'doc_back_preview';
|
|
|
|
|
- }
|
|
|
|
|
- }, 'image/jpeg', 0.85);
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
-const usePhoto = () => {
|
|
|
|
|
- if (subStep.value === 'selfie_preview') {
|
|
|
|
|
- form.value.selfie = tempSelfieFile.value;
|
|
|
|
|
- subStep.value = 'selfie_result';
|
|
|
|
|
- } else if (subStep.value === 'doc_front_preview') {
|
|
|
|
|
- subStep.value = 'doc_back_camera';
|
|
|
|
|
- } else if (subStep.value === 'doc_back_preview') {
|
|
|
|
|
|
|
+const takeDocBack = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const file = await openCamera('rear');
|
|
|
|
|
+
|
|
|
form.value.document_front = tempDocFrontFile.value;
|
|
form.value.document_front = tempDocFrontFile.value;
|
|
|
- form.value.document_back = tempDocBackFile.value;
|
|
|
|
|
- docResult.value = 'success';
|
|
|
|
|
- subStep.value = 'doc_result';
|
|
|
|
|
|
|
+ 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 retakePhoto = () => {
|
|
|
|
|
- const retakeMap = {
|
|
|
|
|
- selfie_preview: 'selfie_camera',
|
|
|
|
|
- doc_front_preview: 'doc_front_camera',
|
|
|
|
|
- doc_back_preview: 'doc_back_camera',
|
|
|
|
|
- };
|
|
|
|
|
- subStep.value = retakeMap[subStep.value] || 'main';
|
|
|
|
|
-};
|
|
|
|
|
|
|
+const takeSelfie = async () => {
|
|
|
|
|
+ if (loadingSelfie.value) return;
|
|
|
|
|
|
|
|
-const goBack = () => {
|
|
|
|
|
- const backMap = {
|
|
|
|
|
- selfie_camera: 'main',
|
|
|
|
|
- selfie_preview: 'selfie_camera',
|
|
|
|
|
- doc_front_camera: 'main',
|
|
|
|
|
- doc_front_preview: 'doc_front_camera',
|
|
|
|
|
- doc_back_camera: 'doc_front_preview',
|
|
|
|
|
- doc_back_preview: 'doc_back_camera',
|
|
|
|
|
- };
|
|
|
|
|
- subStep.value = backMap[subStep.value] || 'main';
|
|
|
|
|
-};
|
|
|
|
|
|
|
+ loadingSelfie.value = true;
|
|
|
|
|
|
|
|
-const handleResult = () => {
|
|
|
|
|
- if (docResult.value === 'error') {
|
|
|
|
|
- docResult.value = null;
|
|
|
|
|
- tempDocFront.value = null;
|
|
|
|
|
- tempDocBack.value = null;
|
|
|
|
|
- subStep.value = 'doc_front_camera';
|
|
|
|
|
- } else {
|
|
|
|
|
- subStep.value = 'main';
|
|
|
|
|
|
|
+ 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>
|
|
</script>
|
|
@@ -370,69 +327,6 @@ const handleResult = () => {
|
|
|
background: white;
|
|
background: white;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-.fs-camera {
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- display: flex;
|
|
|
|
|
- flex-direction: column;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.fs-header {
|
|
|
|
|
- display: flex;
|
|
|
|
|
- align-items: center;
|
|
|
|
|
- gap: 4px;
|
|
|
|
|
- padding: 8px 12px;
|
|
|
|
|
- background: white;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.fs-viewport {
|
|
|
|
|
- flex: 1;
|
|
|
|
|
- background: #000;
|
|
|
|
|
- position: relative;
|
|
|
|
|
- overflow: hidden;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.fs-video {
|
|
|
|
|
- width: 100%;
|
|
|
|
|
- height: 100%;
|
|
|
|
|
- object-fit: cover;
|
|
|
|
|
- display: block;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.fs-preview-img {
|
|
|
|
|
- object-fit: contain;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.fs-footer {
|
|
|
|
|
- padding: 16px 20px;
|
|
|
|
|
- background: white;
|
|
|
|
|
- flex-shrink: 0;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-oval {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- width: 62%;
|
|
|
|
|
- height: 70%;
|
|
|
|
|
- border: 3px solid #8b5cf6;
|
|
|
|
|
- border-radius: 50%;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
-.guide-doc {
|
|
|
|
|
- position: absolute;
|
|
|
|
|
- top: 50%;
|
|
|
|
|
- left: 50%;
|
|
|
|
|
- transform: translate(-50%, -50%);
|
|
|
|
|
- width: 86%;
|
|
|
|
|
- height: 52%;
|
|
|
|
|
- border: 2px solid rgba(255, 255, 255, 0.65);
|
|
|
|
|
- border-radius: 14px;
|
|
|
|
|
- pointer-events: none;
|
|
|
|
|
-}
|
|
|
|
|
-
|
|
|
|
|
.fs-result {
|
|
.fs-result {
|
|
|
height: 100%;
|
|
height: 100%;
|
|
|
display: flex;
|
|
display: flex;
|