| <template> |
| <div v-if="siteKey" class="turnstile-wrapper"> |
| <div ref="containerRef" class="turnstile-container"></div> |
| </div> |
| </template> |
|
|
| <script setup lang="ts"> |
| import { ref, onMounted, onUnmounted, watch } from 'vue' |
| |
| interface TurnstileRenderOptions { |
| sitekey: string |
| callback: (token: string) => void |
| 'expired-callback'?: () => void |
| 'error-callback'?: () => void |
| theme?: 'light' | 'dark' | 'auto' |
| size?: 'normal' | 'compact' | 'flexible' |
| } |
| |
| interface TurnstileAPI { |
| render: (container: HTMLElement, options: TurnstileRenderOptions) => string |
| reset: (widgetId?: string) => void |
| remove: (widgetId?: string) => void |
| } |
| |
| declare global { |
| interface Window { |
| turnstile?: TurnstileAPI |
| onTurnstileLoad?: () => void |
| } |
| } |
| |
| const props = withDefaults( |
| defineProps<{ |
| siteKey: string |
| theme?: 'light' | 'dark' | 'auto' |
| size?: 'normal' | 'compact' | 'flexible' |
| }>(), |
| { |
| theme: 'auto', |
| size: 'flexible' |
| } |
| ) |
| |
| const emit = defineEmits<{ |
| (e: 'verify', token: string): void |
| (e: 'expire'): void |
| (e: 'error'): void |
| }>() |
| |
| const containerRef = ref<HTMLElement | null>(null) |
| const widgetId = ref<string | null>(null) |
| const scriptLoaded = ref(false) |
| |
| const loadScript = (): Promise<void> => { |
| return new Promise((resolve, reject) => { |
| if (window.turnstile) { |
| scriptLoaded.value = true |
| resolve() |
| return |
| } |
| |
| |
| const existingScript = document.querySelector('script[src*="turnstile"]') |
| if (existingScript) { |
| window.onTurnstileLoad = () => { |
| scriptLoaded.value = true |
| resolve() |
| } |
| return |
| } |
| |
| const script = document.createElement('script') |
| script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?onload=onTurnstileLoad' |
| script.async = true |
| script.defer = true |
| |
| window.onTurnstileLoad = () => { |
| scriptLoaded.value = true |
| resolve() |
| } |
| |
| script.onerror = () => { |
| reject(new Error('Failed to load Turnstile script')) |
| } |
| |
| document.head.appendChild(script) |
| }) |
| } |
| |
| const renderWidget = () => { |
| if (!window.turnstile || !containerRef.value || !props.siteKey) { |
| return |
| } |
| |
| |
| if (widgetId.value) { |
| try { |
| window.turnstile.remove(widgetId.value) |
| } catch { |
| |
| } |
| widgetId.value = null |
| } |
| |
| |
| containerRef.value.innerHTML = '' |
| |
| widgetId.value = window.turnstile.render(containerRef.value, { |
| sitekey: props.siteKey, |
| callback: (token: string) => { |
| emit('verify', token) |
| }, |
| 'expired-callback': () => { |
| emit('expire') |
| }, |
| 'error-callback': () => { |
| emit('error') |
| }, |
| theme: props.theme, |
| size: props.size |
| }) |
| } |
| |
| const reset = () => { |
| if (window.turnstile && widgetId.value) { |
| window.turnstile.reset(widgetId.value) |
| } |
| } |
| |
| |
| defineExpose({ reset }) |
| |
| onMounted(async () => { |
| if (!props.siteKey) { |
| return |
| } |
| |
| try { |
| await loadScript() |
| renderWidget() |
| } catch (error) { |
| console.error('Failed to initialize Turnstile:', error) |
| emit('error') |
| } |
| }) |
| |
| onUnmounted(() => { |
| if (window.turnstile && widgetId.value) { |
| try { |
| window.turnstile.remove(widgetId.value) |
| } catch { |
| |
| } |
| } |
| }) |
| |
| |
| watch( |
| () => props.siteKey, |
| (newKey) => { |
| if (newKey && scriptLoaded.value) { |
| renderWidget() |
| } |
| } |
| ) |
| </script> |
|
|
| <style scoped> |
| .turnstile-wrapper { |
| width: 100%; |
| } |
| |
| .turnstile-container { |
| width: 100%; |
| min-height: 65px; |
| } |
| |
| |
| .turnstile-container :deep(iframe) { |
| width: 100% !important; |
| } |
| </style> |
|
|