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.
 
 
 
 

500 lines
12 KiB

<script setup lang="ts">
definePageMeta({
layout: false,
})
const { $toast } = useNuxtApp()
const registerForm = reactive({
username: '',
password: '',
confirmPassword: '',
})
const captcha = reactive({
id: '',
svg: '',
answer: '',
loading: false,
})
const registerLoading = ref(false)
async function fetchCaptcha() {
captcha.loading = true
try {
const res = await $fetch<{ code: number; data: { captchaId: string; imageSvg: string } }>('/api/auth/captcha')
captcha.id = res.data.captchaId
captcha.svg = res.data.imageSvg
captcha.answer = ''
} catch (e: any) {
console.error('获取验证码失败', e)
} finally {
captcha.loading = false
}
}
async function handleRegister() {
if (registerForm.password !== registerForm.confirmPassword) {
$toast.error('两次密码输入不一致')
return
}
registerLoading.value = true
try {
await $fetch('/api/auth/register', {
method: 'POST',
body: {
username: registerForm.username,
password: registerForm.password,
captchaId: captcha.id,
captchaAnswer: captcha.answer,
},
})
$toast.success('注册成功!正在跳转...')
setTimeout(() => navigateTo('/auth/login?tab=login'), 1500)
} catch (e: any) {
$toast.error(e?.data?.statusMessage || e?.message || '注册失败')
await fetchCaptcha()
} finally {
registerLoading.value = false
}
}
onMounted(fetchCaptcha)
</script>
<template>
<div class="auth-page">
<div class="auth-bg-deco" aria-hidden="true">
<div class="bg-gradient-top"></div>
<div class="bg-gradient-bottom"></div>
<div class="bg-accent-block bg-accent-block-1"></div>
<div class="bg-accent-block bg-accent-block-2"></div>
<div class="bg-accent-block bg-accent-block-3"></div>
<div class="bg-grid"></div>
<div class="bg-spike-spots">
<div class="spike spike-1"></div>
<div class="spike spike-2"></div>
<div class="spike spike-3"></div>
<div class="spike spike-4"></div>
</div>
</div>
<div class="auth-card">
<div class="form-header">
<h1 class="form-title">创建账户</h1>
<p class="form-subtitle">加入我们开始策展</p>
</div>
<form class="auth-form" @submit.prevent="handleRegister">
<div class="form-field">
<input id="register-username" v-model="registerForm.username" type="text" placeholder=" " required />
<label for="register-username">用户名</label>
</div>
<div class="form-field">
<input id="register-password" v-model="registerForm.password" type="password" placeholder=" " required />
<label for="register-password">密码</label>
</div>
<div class="form-field">
<input id="register-confirm" v-model="registerForm.confirmPassword" type="password" placeholder=" " required />
<label for="register-confirm">确认密码</label>
</div>
<div class="form-field captcha-field">
<input id="register-captcha" v-model="captcha.answer" type="text" placeholder=" " required />
<label for="register-captcha">验证码</label>
<div class="captcha-img" :class="{ loading: captcha.loading }" @click="fetchCaptcha" v-html="captcha.svg"></div>
</div>
<button type="submit" class="submit-btn" :class="{ loading: registerLoading }" :disabled="registerLoading">
<span class="btn-text">{{ registerLoading ? '注册中...' : '创建账户' }}</span>
<span v-if="!registerLoading" class="btn-arrow">→</span>
</button>
</form>
<div class="form-divider">
<span class="divider-line"></span>
<span class="divider-text">其他方式</span>
<span class="divider-line"></span>
</div>
<button class="social-btn">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z" />
<path d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z" />
<path d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z" />
<path d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z" />
</svg>
使用 Google 注册
</button>
<p class="form-footer">
已有账户?<NuxtLink to="/auth/login">立即登录</NuxtLink>
</p>
</div>
</div>
</template>
<style scoped>
.auth-page {
min-height: 100vh;
background: var(--color-canvas);
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
}
.auth-bg-deco {
position: absolute;
inset: 0;
pointer-events: none;
overflow: hidden;
}
.bg-gradient-top {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 280px;
background: linear-gradient(180deg, rgba(204, 120, 92, 0.08) 0%, transparent 100%);
}
.bg-gradient-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 200px;
background: linear-gradient(0deg, rgba(204, 120, 92, 0.05) 0%, transparent 100%);
}
.bg-accent-block {
position: absolute;
border-radius: 50%;
filter: blur(80px);
}
.bg-accent-block-1 {
width: 500px;
height: 500px;
background: var(--color-primary);
top: -150px;
right: 10%;
opacity: 0.1;
}
.bg-accent-block-2 {
width: 400px;
height: 400px;
background: var(--color-accent-amber);
bottom: -100px;
left: 20%;
opacity: 0.06;
}
.bg-accent-block-3 {
width: 300px;
height: 300px;
background: var(--color-primary);
bottom: 20%;
right: -100px;
opacity: 0.08;
}
.bg-grid {
position: absolute;
inset: 0;
background-image: radial-gradient(circle, var(--color-hairline) 1px, transparent 1px);
background-size: 20px 20px;
opacity: 0.4;
}
.bg-spike-spots {
position: absolute;
inset: 0;
}
.spike {
position: absolute;
width: 40px;
height: 40px;
opacity: 0.06;
}
.spike::before,
.spike::after {
content: '';
position: absolute;
background: var(--color-ink);
}
.spike::before {
width: 100%;
height: 2px;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.spike::after {
width: 2px;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
}
.spike-1 {
top: 15%;
left: 10%;
transform: rotate(15deg);
}
.spike-2 {
top: 25%;
right: 15%;
transform: rotate(45deg);
}
.spike-3 {
bottom: 20%;
left: 15%;
transform: rotate(30deg);
}
.spike-4 {
bottom: 30%;
right: 10%;
transform: rotate(60deg);
}
.auth-card {
width: 100%;
max-width: 420px;
background: var(--color-canvas);
border-radius: 12px;
box-shadow: 0 2px 12px rgba(20, 20, 19, 0.06);
padding: 40px;
position: relative;
z-index: 1;
margin: 24px;
}
.form-header {
margin-bottom: 32px;
}
.form-title {
font-family: var(--font-display);
font-size: 32px;
font-weight: 400;
color: var(--color-ink);
letter-spacing: -1px;
line-height: 1.1;
margin: 0 0 8px;
}
.form-subtitle {
font-family: var(--font-body);
font-size: 15px;
color: var(--color-muted);
margin: 0;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 20px;
}
.form-field {
position: relative;
}
.form-field input {
width: 100%;
padding: 20px 16px 8px;
font-family: var(--font-body);
font-size: 16px;
color: var(--color-ink);
background: var(--color-canvas);
border: 1.5px solid var(--color-hairline);
border-radius: 8px;
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
box-sizing: border-box;
}
.form-field input::placeholder {
color: transparent;
}
.form-field label {
position: absolute;
left: 16px;
top: 50%;
transform: translateY(-50%);
font-family: var(--font-body);
font-size: 15px;
color: var(--color-muted);
pointer-events: none;
transition: all 0.2s ease;
}
.form-field input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px rgba(204, 120, 92, 0.1);
}
.form-field input:focus + label,
.form-field input:not(:placeholder-shown) + label {
top: 12px;
font-size: 11px;
color: var(--color-primary);
letter-spacing: 0.5px;
}
.captcha-field input {
padding-right: 100px;
}
.captcha-img {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 36px;
border-radius: 4px;
cursor: pointer;
transition: opacity 0.2s;
}
.captcha-img :deep(svg) {
display: block;
height: 36px;
}
.captcha-img.loading {
opacity: 0.5;
}
.submit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 18px 24px;
font-family: var(--font-body);
font-size: 15px;
font-weight: 500;
color: var(--color-on-primary);
background: var(--color-primary);
border: none;
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, transform 0.15s ease;
position: relative;
overflow: hidden;
}
.submit-btn::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.15), transparent);
transform: translateX(-100%);
transition: transform 0.5s ease;
}
.submit-btn:hover::before {
transform: translateX(100%);
}
.submit-btn:hover {
background: var(--color-primary-active);
}
.submit-btn:active {
transform: scale(0.98);
}
.submit-btn.loading {
opacity: 0.7;
cursor: not-allowed;
}
.btn-arrow {
font-size: 18px;
transition: transform 0.2s ease;
}
.submit-btn:hover .btn-arrow {
transform: translateX(4px);
}
.form-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 24px 0;
}
.divider-line {
flex: 1;
height: 1px;
background: var(--color-hairline);
}
.divider-text {
font-size: 12px;
color: var(--color-muted-soft);
text-transform: uppercase;
letter-spacing: 1px;
}
.social-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
width: 100%;
padding: 16px 20px;
font-family: var(--font-body);
font-size: 14px;
font-weight: 500;
color: var(--color-ink);
background: var(--color-canvas);
border: 1.5px solid var(--color-hairline);
border-radius: 8px;
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
}
.social-btn:hover {
background: var(--color-surface-soft);
border-color: var(--color-hairline-soft);
}
.form-footer {
text-align: center;
margin-top: 24px;
font-size: 14px;
color: var(--color-muted);
}
.form-footer a {
color: var(--color-primary);
text-decoration: none;
font-weight: 500;
}
.form-footer a:hover {
text-decoration: underline;
}
</style>