Files
sub2api/frontend/src/views/auth/RegisterView.vue
IanShaw027 4251a5a451 refactor(frontend): 完成所有组件的内联SVG统一替换为Icon组件
- 扩展 Icon.vue 组件,新增 60+ 图标路径
  - 导航类: arrowRight, arrowLeft, arrowUp, arrowDown, chevronUp, externalLink
  - 状态类: checkCircle, xCircle, exclamationCircle, exclamationTriangle, infoCircle
  - 用户类: user, userCircle, userPlus, users
  - 文档类: document, clipboard, copy, inbox
  - 操作类: download, upload, filter, sort
  - 安全类: key, lock, shield
  - UI类: menu, calendar, home, terminal, gift, creditCard, mail
  - 数据类: chartBar, trendingUp, database, cube
  - 其他: bolt, sparkles, cloud, server, sun, moon, book 等

- 重构 56 个 Vue 组件,用 Icon 组件替换内联 SVG
  - 净减少约 2200 行代码
  - 提升代码可维护性和一致性
  - 统一图标样式和尺寸管理
2026-01-05 20:22:48 +08:00

383 lines
11 KiB
Vue

<template>
<AuthLayout>
<div class="space-y-6">
<!-- Title -->
<div class="text-center">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white">
{{ t('auth.createAccount') }}
</h2>
<p class="mt-2 text-sm text-gray-500 dark:text-dark-400">
{{ t('auth.signUpToStart', { siteName }) }}
</p>
</div>
<!-- Registration Disabled Message -->
<div
v-if="!registrationEnabled && settingsLoaded"
class="rounded-xl border border-amber-200 bg-amber-50 p-4 dark:border-amber-800/50 dark:bg-amber-900/20"
>
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<Icon name="exclamationCircle" size="md" class="text-amber-500" />
</div>
<p class="text-sm text-amber-700 dark:text-amber-400">
{{ t('auth.registrationDisabled') }}
</p>
</div>
</div>
<!-- Registration Form -->
<form v-else @submit.prevent="handleRegister" class="space-y-5">
<!-- Email Input -->
<div>
<label for="email" class="input-label">
{{ t('auth.emailLabel') }}
</label>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
<Icon name="mail" size="md" class="text-gray-400 dark:text-dark-500" />
</div>
<input
id="email"
v-model="formData.email"
type="email"
required
autofocus
autocomplete="email"
:disabled="isLoading"
class="input pl-11"
:class="{ 'input-error': errors.email }"
:placeholder="t('auth.emailPlaceholder')"
/>
</div>
<p v-if="errors.email" class="input-error-text">
{{ errors.email }}
</p>
</div>
<!-- Password Input -->
<div>
<label for="password" class="input-label">
{{ t('auth.passwordLabel') }}
</label>
<div class="relative">
<div class="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3.5">
<Icon name="lock" size="md" class="text-gray-400 dark:text-dark-500" />
</div>
<input
id="password"
v-model="formData.password"
:type="showPassword ? 'text' : 'password'"
required
autocomplete="new-password"
:disabled="isLoading"
class="input pl-11 pr-11"
:class="{ 'input-error': errors.password }"
:placeholder="t('auth.createPasswordPlaceholder')"
/>
<button
type="button"
@click="showPassword = !showPassword"
class="absolute inset-y-0 right-0 flex items-center pr-3.5 text-gray-400 transition-colors hover:text-gray-600 dark:hover:text-dark-300"
>
<Icon v-if="showPassword" name="eyeOff" size="md" />
<Icon v-else name="eye" size="md" />
</button>
</div>
<p v-if="errors.password" class="input-error-text">
{{ errors.password }}
</p>
<p v-else class="input-hint">
{{ t('auth.passwordHint') }}
</p>
</div>
<!-- Turnstile Widget -->
<div v-if="turnstileEnabled && turnstileSiteKey">
<TurnstileWidget
ref="turnstileRef"
:site-key="turnstileSiteKey"
@verify="onTurnstileVerify"
@expire="onTurnstileExpire"
@error="onTurnstileError"
/>
<p v-if="errors.turnstile" class="input-error-text mt-2 text-center">
{{ errors.turnstile }}
</p>
</div>
<!-- Error Message -->
<transition name="fade">
<div
v-if="errorMessage"
class="rounded-xl border border-red-200 bg-red-50 p-4 dark:border-red-800/50 dark:bg-red-900/20"
>
<div class="flex items-start gap-3">
<div class="flex-shrink-0">
<Icon name="exclamationCircle" size="md" class="text-red-500" />
</div>
<p class="text-sm text-red-700 dark:text-red-400">
{{ errorMessage }}
</p>
</div>
</div>
</transition>
<!-- Submit Button -->
<button
type="submit"
:disabled="isLoading || (turnstileEnabled && !turnstileToken)"
class="btn btn-primary w-full"
>
<svg
v-if="isLoading"
class="-ml-1 mr-2 h-4 w-4 animate-spin text-white"
fill="none"
viewBox="0 0 24 24"
>
<circle
class="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
stroke-width="4"
></circle>
<path
class="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
></path>
</svg>
<Icon v-else name="userPlus" size="md" class="mr-2" />
{{
isLoading
? t('auth.processing')
: emailVerifyEnabled
? t('auth.continue')
: t('auth.createAccount')
}}
</button>
</form>
</div>
<!-- Footer -->
<template #footer>
<p class="text-gray-500 dark:text-dark-400">
{{ t('auth.alreadyHaveAccount') }}
<router-link
to="/login"
class="font-medium text-primary-600 transition-colors hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
>
{{ t('auth.signIn') }}
</router-link>
</p>
</template>
</AuthLayout>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useI18n } from 'vue-i18n'
import { AuthLayout } from '@/components/layout'
import Icon from '@/components/icons/Icon.vue'
import TurnstileWidget from '@/components/TurnstileWidget.vue'
import { useAuthStore, useAppStore } from '@/stores'
import { getPublicSettings } from '@/api/auth'
const { t } = useI18n()
// ==================== Router & Stores ====================
const router = useRouter()
const authStore = useAuthStore()
const appStore = useAppStore()
// ==================== State ====================
const isLoading = ref<boolean>(false)
const settingsLoaded = ref<boolean>(false)
const errorMessage = ref<string>('')
const showPassword = ref<boolean>(false)
// Public settings
const registrationEnabled = ref<boolean>(true)
const emailVerifyEnabled = ref<boolean>(false)
const turnstileEnabled = ref<boolean>(false)
const turnstileSiteKey = ref<string>('')
const siteName = ref<string>('Sub2API')
// Turnstile
const turnstileRef = ref<InstanceType<typeof TurnstileWidget> | null>(null)
const turnstileToken = ref<string>('')
const formData = reactive({
email: '',
password: ''
})
const errors = reactive({
email: '',
password: '',
turnstile: ''
})
// ==================== Lifecycle ====================
onMounted(async () => {
try {
const settings = await getPublicSettings()
registrationEnabled.value = settings.registration_enabled
emailVerifyEnabled.value = settings.email_verify_enabled
turnstileEnabled.value = settings.turnstile_enabled
turnstileSiteKey.value = settings.turnstile_site_key || ''
siteName.value = settings.site_name || 'Sub2API'
} catch (error) {
console.error('Failed to load public settings:', error)
} finally {
settingsLoaded.value = true
}
})
// ==================== Turnstile Handlers ====================
function onTurnstileVerify(token: string): void {
turnstileToken.value = token
errors.turnstile = ''
}
function onTurnstileExpire(): void {
turnstileToken.value = ''
errors.turnstile = t('auth.turnstileExpired')
}
function onTurnstileError(): void {
turnstileToken.value = ''
errors.turnstile = t('auth.turnstileFailed')
}
// ==================== Validation ====================
function validateEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email)
}
function validateForm(): boolean {
// Reset errors
errors.email = ''
errors.password = ''
errors.turnstile = ''
let isValid = true
// Email validation
if (!formData.email.trim()) {
errors.email = t('auth.emailRequired')
isValid = false
} else if (!validateEmail(formData.email)) {
errors.email = t('auth.invalidEmail')
isValid = false
}
// Password validation
if (!formData.password) {
errors.password = t('auth.passwordRequired')
isValid = false
} else if (formData.password.length < 6) {
errors.password = t('auth.passwordMinLength')
isValid = false
}
// Turnstile validation
if (turnstileEnabled.value && !turnstileToken.value) {
errors.turnstile = t('auth.completeVerification')
isValid = false
}
return isValid
}
// ==================== Form Handlers ====================
async function handleRegister(): Promise<void> {
// Clear previous error
errorMessage.value = ''
// Validate form
if (!validateForm()) {
return
}
isLoading.value = true
try {
// If email verification is enabled, redirect to verification page
if (emailVerifyEnabled.value) {
// Store registration data in sessionStorage
sessionStorage.setItem(
'register_data',
JSON.stringify({
email: formData.email,
password: formData.password,
turnstile_token: turnstileToken.value
})
)
// Navigate to email verification page
await router.push('/email-verify')
return
}
// Otherwise, directly register
await authStore.register({
email: formData.email,
password: formData.password,
turnstile_token: turnstileEnabled.value ? turnstileToken.value : undefined
})
// Show success toast
appStore.showSuccess(t('auth.accountCreatedSuccess', { siteName: siteName.value }))
// Redirect to dashboard
await router.push('/dashboard')
} catch (error: unknown) {
// Reset Turnstile on error
if (turnstileRef.value) {
turnstileRef.value.reset()
turnstileToken.value = ''
}
// Handle registration error
const err = error as { message?: string; response?: { data?: { detail?: string } } }
if (err.response?.data?.detail) {
errorMessage.value = err.response.data.detail
} else if (err.message) {
errorMessage.value = err.message
} else {
errorMessage.value = t('auth.registrationFailed')
}
// Also show error toast
appStore.showError(errorMessage.value)
} finally {
isLoading.value = false
}
}
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
transform: translateY(-8px);
}
</style>