Files
DinQuant/quantdinger_vue/src/components/Turnstile/index.vue
T
TIANHE 445e7ac290 feat: improve authentication and user management
- Fix token missing issue after email code registration/login
- Add last login time update for code-based login
- Support email login in addition to username login
- Fix password login for code-registered users (allow setting password)
- Fix referral code parameter passing from URL hash
- Add email column to user management table
- Improve profile page layout (align card heights, reorganize layout)
- Add i18n support for Register Bonus, Referral Bonus, Code Lock Minutes, Code Max Attempts
- Fix billing service add_credits to support reference_id parameter
- Update password handling documentation
2026-01-14 20:42:10 +08:00

200 lines
4.0 KiB
Vue

<template>
<div class="turnstile-container" v-if="enabled">
<div ref="turnstileRef" :id="containerId"></div>
<div v-if="error" class="turnstile-error">
{{ error }}
<a @click="reset">{{ $t('user.security.retry') || 'Retry' }}</a>
</div>
</div>
</template>
<script>
let turnstileScriptLoaded = false
let turnstileScriptLoading = false
const turnstileCallbacks = []
function loadTurnstileScript () {
return new Promise((resolve, reject) => {
if (turnstileScriptLoaded) {
resolve()
return
}
turnstileCallbacks.push({ resolve, reject })
if (turnstileScriptLoading) {
return
}
turnstileScriptLoading = true
const script = document.createElement('script')
script.src = 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'
script.async = true
script.defer = true
script.onload = () => {
turnstileScriptLoaded = true
turnstileCallbacks.forEach(cb => cb.resolve())
turnstileCallbacks.length = 0
}
script.onerror = () => {
turnstileScriptLoading = false
turnstileCallbacks.forEach(cb => cb.reject(new Error('Failed to load Turnstile script')))
turnstileCallbacks.length = 0
}
document.head.appendChild(script)
})
}
export default {
name: 'Turnstile',
props: {
siteKey: {
type: String,
default: ''
},
enabled: {
type: Boolean,
default: true
},
theme: {
type: String,
default: 'auto' // 'light', 'dark', 'auto'
},
size: {
type: String,
default: 'normal' // 'normal', 'compact'
}
},
data () {
return {
widgetId: null,
token: null,
error: null,
containerId: `turnstile-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
},
mounted () {
if (this.enabled && this.siteKey) {
this.initTurnstile()
}
},
beforeDestroy () {
this.cleanup()
},
watch: {
siteKey (newVal) {
if (newVal && this.enabled) {
this.initTurnstile()
}
},
enabled (newVal) {
if (newVal && this.siteKey) {
this.initTurnstile()
} else {
this.cleanup()
}
}
},
methods: {
async initTurnstile () {
try {
await loadTurnstileScript()
this.renderWidget()
} catch (e) {
this.error = 'Failed to load verification'
console.error('Turnstile init error:', e)
}
},
renderWidget () {
if (!window.turnstile || !this.$refs.turnstileRef) {
return
}
// Clean up existing widget
this.cleanup()
this.widgetId = window.turnstile.render(this.$refs.turnstileRef, {
sitekey: this.siteKey,
theme: this.theme,
size: this.size,
callback: (token) => {
this.token = token
this.error = null
this.$emit('success', token)
},
'error-callback': () => {
this.token = null
this.error = 'Verification failed'
this.$emit('error')
},
'expired-callback': () => {
this.token = null
this.$emit('expired')
}
})
},
reset () {
this.token = null
this.error = null
if (window.turnstile && this.widgetId !== null) {
window.turnstile.reset(this.widgetId)
} else {
this.renderWidget()
}
},
getToken () {
return this.token
},
cleanup () {
if (window.turnstile && this.widgetId !== null) {
try {
window.turnstile.remove(this.widgetId)
} catch (e) {
// Ignore cleanup errors
}
this.widgetId = null
}
}
}
}
</script>
<style lang="less" scoped>
.turnstile-container {
margin: 16px 0;
display: flex;
flex-direction: column;
align-items: center;
.turnstile-error {
margin-top: 8px;
color: #ff4d4f;
font-size: 13px;
a {
margin-left: 8px;
color: #1890ff;
cursor: pointer;
&:hover {
text-decoration: underline;
}
}
}
}
</style>