1 changed files with 137 additions and 0 deletions
@ -0,0 +1,137 @@ |
|||
<script setup lang="ts"> |
|||
import { z } from 'zod' |
|||
|
|||
const formSchema = z |
|||
.object({ |
|||
username: z.string().trim().min(3, '用户名至少需要3个字符').max(30), |
|||
password: z.string().min(8, '密码至少需要8个字符'), |
|||
confirmPassword: z.string(), |
|||
captchaText: z.string().min(1, '请输入验证码'), |
|||
}) |
|||
.refine((data) => data.password === data.confirmPassword, { |
|||
message: '两次输入的密码不一致', |
|||
path: ['confirmPassword'], |
|||
}) |
|||
|
|||
const state = reactive({ |
|||
username: '', |
|||
password: '', |
|||
confirmPassword: '', |
|||
captchaText: '', |
|||
}) |
|||
|
|||
const captchaToken = ref('') |
|||
const captchaSvg = ref('') |
|||
const loading = ref(false) |
|||
const toast = useToast() |
|||
|
|||
async function fetchCaptcha() { |
|||
const res = await $fetch<{ code: number; data: { token: string; svg: string } }>( |
|||
'/api/auth/captcha' |
|||
) |
|||
captchaToken.value = res.data.token |
|||
captchaSvg.value = res.data.svg |
|||
} |
|||
|
|||
async function onSubmit() { |
|||
loading.value = true |
|||
try { |
|||
const res = await $fetch<{ code: number; message: string; data: unknown }>( |
|||
'/api/auth/register', |
|||
{ |
|||
method: 'POST', |
|||
body: { |
|||
username: state.username, |
|||
password: state.password, |
|||
confirmPassword: state.confirmPassword, |
|||
captchaToken: captchaToken.value, |
|||
captchaText: state.captchaText, |
|||
}, |
|||
} |
|||
) |
|||
|
|||
if (res.code !== 0) { |
|||
toast.add({ title: res.message, color: 'error' }) |
|||
if (res.message.includes('验证码')) { |
|||
fetchCaptcha() |
|||
state.captchaText = '' |
|||
} |
|||
return |
|||
} |
|||
|
|||
navigateTo('/login?registered=1') |
|||
} catch { |
|||
toast.add({ title: '注册失败,请稍后重试', color: 'error' }) |
|||
} finally { |
|||
loading.value = false |
|||
} |
|||
} |
|||
|
|||
onMounted(fetchCaptcha) |
|||
</script> |
|||
|
|||
<template> |
|||
<div class="flex items-center justify-center py-8"> |
|||
<UCard class="w-full max-w-[380px]"> |
|||
<template #header> |
|||
<h1 class="text-xl font-semibold">Create Account</h1> |
|||
<p class="text-sm text-muted mt-1">Fill in the details to get started</p> |
|||
</template> |
|||
|
|||
<UForm :state="state" :schema="formSchema" @submit="onSubmit"> |
|||
<UFormField name="username" label="Username" required class="mb-4"> |
|||
<UInput v-model="state.username" placeholder="Enter your username" /> |
|||
</UFormField> |
|||
|
|||
<UFormField name="password" label="Password" required class="mb-4"> |
|||
<UInput |
|||
v-model="state.password" |
|||
type="password" |
|||
placeholder="At least 8 characters" |
|||
/> |
|||
</UFormField> |
|||
|
|||
<UFormField name="confirmPassword" label="Confirm Password" required class="mb-4"> |
|||
<UInput |
|||
v-model="state.confirmPassword" |
|||
type="password" |
|||
placeholder="Re-enter your password" |
|||
/> |
|||
</UFormField> |
|||
|
|||
<UFormField name="captchaText" label="Captcha" required class="mb-4"> |
|||
<div class="flex gap-2 items-start"> |
|||
<!-- eslint-disable-next-line vue/no-v-html --> |
|||
<div |
|||
class="flex-1 h-10 border rounded-md overflow-hidden bg-[#f8f9fa]" |
|||
v-html="captchaSvg" |
|||
/> |
|||
<UButton |
|||
variant="ghost" |
|||
color="neutral" |
|||
icon="i-lucide-refresh-cw" |
|||
square |
|||
@click="fetchCaptcha" |
|||
/> |
|||
</div> |
|||
<UInput |
|||
v-model="state.captchaText" |
|||
placeholder="Enter the code above" |
|||
class="mt-2" |
|||
/> |
|||
</UFormField> |
|||
|
|||
<UButton type="submit" block :loading="loading" class="mt-6"> |
|||
Create Account |
|||
</UButton> |
|||
</UForm> |
|||
|
|||
<template #footer> |
|||
<p class="text-sm text-center text-muted"> |
|||
Already have an account? |
|||
<NuxtLink to="/login" class="text-primary hover:underline">Log in</NuxtLink> |
|||
</p> |
|||
</template> |
|||
</UCard> |
|||
</div> |
|||
</template> |
|||
Loading…
Reference in new issue