You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

197 lines
5.5 KiB

<script setup lang="ts">
import type { FormError, FormSubmitEvent } from '@nuxt/ui'
import { normalizeSafeRedirect } from '../../utils/auth-routes'
definePageMeta({
title: '注册',
layout: 'blank',
middleware: [
async () => {
const { allowRegister, refresh } = useGlobalConfig()
await refresh()
if (!allowRegister.value) {
return navigateTo('/login')
}
},
],
})
type RegisterFormState = {
username: string
password: string
captchaAnswer: string
}
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
const state = reactive<RegisterFormState>({
username: '',
password: '',
captchaAnswer: '',
})
const captchaIdRef = ref('')
const captchaImageSrc = ref('')
async function refreshCaptcha() {
state.captchaAnswer = ''
captchaImageSrc.value = ''
const res = await fetchData<{ captchaId: string; imageSvg: string }>('/api/auth/captcha', {
method: 'GET',
notify: false,
})
captchaIdRef.value = res.captchaId
captchaImageSrc.value = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(res.imageSvg)}`
}
onMounted(() => {
void refreshCaptcha()
})
const loading = ref(false)
const resultType = ref<'error' | ''>('')
const resultMessage = ref('')
const route = useRoute()
const { fetchData, getApiErrorMessage } = useClientApi()
const validate = (formState: RegisterFormState): FormError[] => {
const errors: FormError[] = []
if (!formState.username) {
errors.push({ name: 'username', message: '请输入用户名' })
} else if (!USERNAME_REGEX.test(formState.username)) {
errors.push({ name: 'username', message: '用户名需为 3-20 位字母、数字或下划线' })
}
if (!formState.password) {
errors.push({ name: 'password', message: '请输入密码' })
} else if (formState.password.length < 6) {
errors.push({ name: 'password', message: '密码至少 6 位' })
}
if (!formState.captchaAnswer?.trim()) {
errors.push({ name: 'captchaAnswer', message: '请输入验证码' })
}
return errors
}
const onSubmit = async (_event: FormSubmitEvent<RegisterFormState>) => {
resultType.value = ''
resultMessage.value = ''
loading.value = true
try {
await fetchData<unknown>('/api/auth/register', {
method: 'POST',
body: {
username: state.username,
password: state.password,
captchaId: captchaIdRef.value,
captchaAnswer: state.captchaAnswer,
},
notify: false,
})
const redirectCandidate = Array.isArray(route.query.redirect)
? route.query.redirect[0]
: route.query.redirect
const redirect = normalizeSafeRedirect(redirectCandidate, '/')
await navigateTo({
path: '/login',
query: { redirect },
})
} catch (error: unknown) {
resultType.value = 'error'
resultMessage.value = getApiErrorMessage(error)
try {
await refreshCaptcha()
} catch {
/* ignore captcha refresh errors */
}
} finally {
loading.value = false
}
}
</script>
<template>
<div class="min-h-screen flex items-start justify-center px-4 pt-14 pb-8 md:pt-20">
<div class="w-full max-w-md space-y-4">
<div class="text-center space-y-1">
<NuxtLink to="/" class="inline-flex items-center gap-2 text-sm text-muted hover:text-primary transition-colors">
<UIcon name="i-lucide-arrow-left" class="size-4" />
返回首页
</NuxtLink>
</div>
<UCard class="shadow-lg">
<template #header>
<div class="space-y-1 text-center">
<h1 class="text-2xl font-semibold">创建账号</h1>
<p class="text-sm text-muted">用户名 3-20 密码至少 6 </p>
</div>
</template>
<UForm :state="state" :validate="validate" class="space-y-4" @submit="onSubmit">
<UFormField label="用户名" name="username" required>
<UInput
v-model="state.username"
placeholder="请输入用户名"
autocomplete="username"
class="w-full"
/>
</UFormField>
<UFormField label="密码" name="password" required>
<UInput
v-model="state.password"
type="password"
placeholder="请输入密码"
autocomplete="new-password"
class="w-full"
/>
</UFormField>
<UFormField label="验证码" name="captchaAnswer" required>
<div class="flex flex-wrap gap-2 items-center">
<img
v-if="captchaImageSrc"
:src="captchaImageSrc"
alt="验证码"
class="h-10 rounded border border-default bg-elevated shrink-0"
/>
<UInput
v-model="state.captchaAnswer"
placeholder="请输入图中字符"
class="flex-1 min-w-[8rem]"
autocomplete="off"
/>
<UButton type="button" color="neutral" variant="outline" class="shrink-0" @click="refreshCaptcha">
换一张
</UButton>
</div>
</UFormField>
<UButton type="submit" block :loading="loading">
立即注册
</UButton>
</UForm>
<UAlert
v-if="resultType"
color="error"
title="操作失败"
:description="resultMessage"
class="mt-4"
/>
<div class="mt-4 flex items-center justify-center text-sm">
<NuxtLink to="/login" class="text-primary hover:underline">
已有账号去登录
</NuxtLink>
</div>
</UCard>
</div>
</div>
</template>