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.
470 lines
12 KiB
470 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 loginWithGoogle() {
|
|
window.location.href = '/api/auth/oauth/google/authorize'
|
|
}
|
|
|
|
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-accent-block bg-accent-block-1"></div>
|
|
<div class="bg-accent-block bg-accent-block-2"></div>
|
|
<div class="bg-grid"></div>
|
|
</div>
|
|
|
|
<div class="auth-layout">
|
|
<div class="auth-brand">
|
|
<div class="brand-mark">
|
|
<svg width="48" height="48" viewBox="0 0 48 48" fill="none">
|
|
<circle cx="24" cy="24" r="22" stroke="var(--color-primary)" stroke-width="1.5"/>
|
|
<circle cx="24" cy="24" r="5" fill="var(--color-primary)"/>
|
|
<line x1="24" y1="2" x2="24" y2="12" stroke="var(--color-primary)" stroke-width="1.5" stroke-linecap="round"/>
|
|
<line x1="24" y1="36" x2="24" y2="46" stroke="var(--color-primary)" stroke-width="1.5" stroke-linecap="round"/>
|
|
<line x1="2" y1="24" x2="12" y2="24" stroke="var(--color-primary)" stroke-width="1.5" stroke-linecap="round"/>
|
|
<line x1="36" y1="24" x2="46" y2="24" stroke="var(--color-primary)" stroke-width="1.5" stroke-linecap="round"/>
|
|
</svg>
|
|
</div>
|
|
<h1 class="brand-wordmark">NuxtArtisan</h1>
|
|
<p class="brand-tagline">策展你的灵感世界</p>
|
|
</div>
|
|
|
|
<div class="auth-form-wrap">
|
|
<div class="form-header">
|
|
<span class="form-kicker">CREATE ACCOUNT</span>
|
|
<h1 class="form-title">创建账户</h1>
|
|
</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>{{ 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 social-btn--google" @click="loginWithGoogle">
|
|
<svg width="20" height="20" 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>
|
|
<span>使用 Google 注册</span>
|
|
</button>
|
|
|
|
<p class="form-footer">
|
|
已有账户?<NuxtLink to="/auth/login">立即登录</NuxtLink>
|
|
</p>
|
|
</div>
|
|
</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-accent-block {
|
|
position: absolute;
|
|
border-radius: 50%;
|
|
filter: blur(150px);
|
|
}
|
|
|
|
.bg-accent-block-1 {
|
|
width: 800px;
|
|
height: 800px;
|
|
background: var(--color-primary);
|
|
top: -300px;
|
|
right: -200px;
|
|
opacity: 0.1;
|
|
}
|
|
|
|
.bg-accent-block-2 {
|
|
width: 600px;
|
|
height: 600px;
|
|
background: var(--color-accent-amber);
|
|
bottom: -200px;
|
|
left: -150px;
|
|
opacity: 0.06;
|
|
}
|
|
|
|
.bg-grid {
|
|
position: absolute;
|
|
inset: 0;
|
|
background-image: linear-gradient(var(--color-hairline) 1px, transparent 1px),
|
|
linear-gradient(90deg, var(--color-hairline) 1px, transparent 1px);
|
|
background-size: 48px 48px;
|
|
opacity: 0.2;
|
|
}
|
|
|
|
.auth-layout {
|
|
display: grid;
|
|
grid-template-columns: 1.2fr 1fr;
|
|
gap: 120px;
|
|
max-width: 1000px;
|
|
width: 100%;
|
|
padding: 64px;
|
|
position: relative;
|
|
z-index: 1;
|
|
align-items: center;
|
|
}
|
|
|
|
.auth-brand {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 20px;
|
|
}
|
|
|
|
.brand-mark {
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.brand-wordmark {
|
|
font-family: var(--font-display);
|
|
font-size: 56px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
letter-spacing: -2px;
|
|
line-height: 1;
|
|
margin: 0;
|
|
}
|
|
|
|
.brand-tagline {
|
|
font-family: var(--font-body);
|
|
font-size: 18px;
|
|
color: var(--color-muted);
|
|
margin: 0;
|
|
letter-spacing: 1px;
|
|
}
|
|
|
|
.auth-form-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.form-header {
|
|
margin-bottom: 48px;
|
|
}
|
|
|
|
.form-kicker {
|
|
font-family: var(--font-body);
|
|
font-size: 10px;
|
|
font-weight: 500;
|
|
color: var(--color-primary);
|
|
letter-spacing: 4px;
|
|
text-transform: uppercase;
|
|
display: block;
|
|
margin-bottom: 14px;
|
|
}
|
|
|
|
.form-title {
|
|
font-family: var(--font-display);
|
|
font-size: 42px;
|
|
font-weight: 400;
|
|
color: var(--color-ink);
|
|
letter-spacing: -1px;
|
|
line-height: 1;
|
|
margin: 0;
|
|
}
|
|
|
|
.auth-form {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 28px;
|
|
}
|
|
|
|
.form-field {
|
|
position: relative;
|
|
}
|
|
|
|
.form-field input {
|
|
width: 100%;
|
|
padding: 22px 16px 8px;
|
|
font-family: var(--font-body);
|
|
font-size: 16px;
|
|
color: var(--color-ink);
|
|
background: var(--color-canvas);
|
|
border: none;
|
|
border-bottom: 2px solid var(--color-hairline);
|
|
outline: none;
|
|
transition: border-color 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-bottom-color: var(--color-primary);
|
|
}
|
|
|
|
.form-field input:focus + label,
|
|
.form-field input:not(:placeholder-shown) + label {
|
|
top: 10px;
|
|
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: 40px;
|
|
cursor: pointer;
|
|
transition: opacity 0.2s;
|
|
}
|
|
|
|
.captcha-img :deep(svg) {
|
|
display: block;
|
|
height: 40px;
|
|
}
|
|
|
|
.captcha-img.loading {
|
|
opacity: 0.5;
|
|
}
|
|
|
|
.submit-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 14px;
|
|
width: 100%;
|
|
padding: 20px 32px;
|
|
font-family: var(--font-body);
|
|
font-size: 13px;
|
|
font-weight: 500;
|
|
letter-spacing: 2px;
|
|
text-transform: uppercase;
|
|
color: var(--color-on-primary);
|
|
background: var(--color-ink);
|
|
border: none;
|
|
cursor: pointer;
|
|
transition: background 0.2s ease;
|
|
}
|
|
|
|
.submit-btn:hover {
|
|
background: var(--color-primary);
|
|
}
|
|
|
|
.submit-btn:active {
|
|
transform: scale(0.99);
|
|
}
|
|
|
|
.submit-btn.loading {
|
|
opacity: 0.6;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.btn-arrow {
|
|
font-size: 18px;
|
|
transition: transform 0.2s ease;
|
|
}
|
|
|
|
.submit-btn:hover .btn-arrow {
|
|
transform: translateX(5px);
|
|
}
|
|
|
|
.form-divider {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 20px;
|
|
margin: 40px 0;
|
|
}
|
|
|
|
.divider-line {
|
|
flex: 1;
|
|
height: 1px;
|
|
background: var(--color-hairline);
|
|
}
|
|
|
|
.divider-text {
|
|
font-size: 10px;
|
|
color: var(--color-muted-soft);
|
|
text-transform: uppercase;
|
|
letter-spacing: 3px;
|
|
}
|
|
|
|
.social-btn {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 12px;
|
|
width: 100%;
|
|
padding: 18px 24px;
|
|
font-family: var(--font-body);
|
|
font-size: 12px;
|
|
font-weight: 500;
|
|
letter-spacing: 1px;
|
|
color: var(--color-body);
|
|
background: var(--color-canvas);
|
|
border: 1px solid var(--color-hairline);
|
|
cursor: pointer;
|
|
transition: all 0.2s ease;
|
|
}
|
|
|
|
.social-btn:hover {
|
|
color: var(--color-ink);
|
|
background: var(--color-surface-soft);
|
|
border-color: var(--color-ink);
|
|
}
|
|
|
|
.social-btn:active {
|
|
transform: scale(0.98);
|
|
}
|
|
|
|
.form-footer {
|
|
text-align: center;
|
|
margin-top: 40px;
|
|
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;
|
|
}
|
|
|
|
@media (max-width: 768px) {
|
|
.auth-layout {
|
|
grid-template-columns: 1fr;
|
|
gap: 64px;
|
|
padding: 32px;
|
|
}
|
|
|
|
.auth-brand {
|
|
align-items: center;
|
|
text-align: center;
|
|
}
|
|
|
|
.brand-wordmark {
|
|
font-size: 42px;
|
|
}
|
|
}
|
|
</style>
|