119 lines
3.6 KiB
Vue
119 lines
3.6 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* Виджет Yandex SmartCaptcha (M-2). v-model = строка-токен.
|
|
*
|
|
* sitekey задан (VITE_YANDEX_SMARTCAPTCHA_SITEKEY) → реальный виджет:
|
|
* подгружает captcha.js, рендерит через window.smartCaptcha.render, токен
|
|
* приходит в callback → emit. reset() — сброс после ошибки регистрации
|
|
* (одноразовый токен Yandex живёт 5 мин и используется однократно).
|
|
*
|
|
* sitekey пуст (dev/local/CI) → fallback на чекбокс «не робот» со стаб-токеном,
|
|
* чтобы без ключа окружение и существующие сценарии не падали.
|
|
*
|
|
* Интеграция сверена по докам Yandex SmartCaptcha (advanced method, render=onload).
|
|
*/
|
|
import { onMounted, onUnmounted, ref } from 'vue';
|
|
|
|
defineProps<{
|
|
modelValue: string;
|
|
errorMessages?: string[];
|
|
}>();
|
|
|
|
const emit = defineEmits<{
|
|
'update:modelValue': [token: string];
|
|
}>();
|
|
|
|
const sitekey = import.meta.env.VITE_YANDEX_SMARTCAPTCHA_SITEKEY ?? '';
|
|
const useReal = sitekey !== '';
|
|
|
|
const container = ref<HTMLElement | null>(null);
|
|
const widgetId = ref<number | null>(null);
|
|
const fallbackChecked = ref(false);
|
|
|
|
const SCRIPT_ID = 'yandex-smartcaptcha-js';
|
|
const SCRIPT_SRC = 'https://smartcaptcha.cloud.yandex.ru/captcha.js?render=onload&onload=__lidSmartCaptchaOnload';
|
|
|
|
function renderWidget(): void {
|
|
if (!window.smartCaptcha || container.value === null || widgetId.value !== null) {
|
|
return;
|
|
}
|
|
widgetId.value = window.smartCaptcha.render(container.value, {
|
|
sitekey,
|
|
hl: 'ru',
|
|
callback: (token: string) => emit('update:modelValue', token),
|
|
});
|
|
}
|
|
|
|
function loadAndRender(): void {
|
|
if (window.smartCaptcha) {
|
|
renderWidget();
|
|
return;
|
|
}
|
|
window.__lidSmartCaptchaOnload = renderWidget;
|
|
if (document.getElementById(SCRIPT_ID) === null) {
|
|
const script = document.createElement('script');
|
|
script.id = SCRIPT_ID;
|
|
script.src = SCRIPT_SRC;
|
|
script.defer = true;
|
|
document.head.appendChild(script);
|
|
}
|
|
}
|
|
|
|
function onFallbackChange(value: boolean | null): void {
|
|
fallbackChecked.value = value === true;
|
|
emit('update:modelValue', fallbackChecked.value ? 'dev-captcha-stub' : '');
|
|
}
|
|
|
|
/** Сброс капчи после ошибки регистрации — нужен свежий токен. */
|
|
function reset(): void {
|
|
if (useReal) {
|
|
if (window.smartCaptcha && widgetId.value !== null) {
|
|
window.smartCaptcha.reset(widgetId.value);
|
|
}
|
|
} else {
|
|
fallbackChecked.value = false;
|
|
}
|
|
emit('update:modelValue', '');
|
|
}
|
|
|
|
defineExpose({ reset });
|
|
|
|
onMounted(() => {
|
|
if (useReal) {
|
|
loadAndRender();
|
|
}
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
if (useReal && window.smartCaptcha?.destroy && widgetId.value !== null) {
|
|
window.smartCaptcha.destroy(widgetId.value);
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<div class="smart-captcha">
|
|
<div v-if="useReal" ref="container" class="smart-captcha__widget" />
|
|
|
|
<v-checkbox
|
|
v-else
|
|
:model-value="fallbackChecked"
|
|
density="compact"
|
|
hide-details
|
|
color="primary"
|
|
:error-messages="errorMessages"
|
|
@update:model-value="onFallbackChange"
|
|
>
|
|
<template #label>
|
|
<span class="text-body-2">Подтвердите, что вы не робот</span>
|
|
</template>
|
|
</v-checkbox>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.smart-captcha__widget {
|
|
min-height: 100px;
|
|
}
|
|
</style>
|