Browse Source

feat: enhance authentication UI with Google login option and improve layout

- Added Google login functionality to the registration page.
- Updated the login and registration forms with a new layout and branding elements.
- Improved user experience with loading states and form validation.
- Removed unused background elements for a cleaner design.
as
npmrun 2 weeks ago
parent
commit
5c49335c86
  1. 414
      app/pages/auth/login.vue
  2. 384
      app/pages/auth/register.vue
  3. 8
      packages/bolt-ui/components/ConfigProvider/src/ConfigProvider.vue
  4. 2
      packages/bolt-ui/components/ConfigProvider/src/config.ts
  5. 5
      packages/bolt-ui/components/Container/src/Container.vue
  6. BIN
      packages/drizzle-pkg/db.sqlite

414
app/pages/auth/login.vue

@ -25,11 +25,11 @@ const { refresh } = useAuthSession()
const router = useRouter()
async function loginWithGithub() {
window.location.href = '/api/auth/oauth/github/authorize';
window.location.href = '/api/auth/oauth/github/authorize'
}
async function loginWithGitea() {
window.location.href = '/api/auth/oauth/gitea/authorize';
window.location.href = '/api/auth/oauth/gitea/authorize'
}
async function fetchCaptcha() {
@ -90,84 +90,92 @@ onMounted(() => {
<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="handleLogin">
<div class="form-field">
<input id="login-username" v-model="loginForm.username" type="text" placeholder=" " required />
<label for="login-username">用户名或邮箱</label>
<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="form-field">
<input id="login-password" v-model="loginForm.password" type="password" placeholder=" " required />
<label for="login-password">密码</label>
<a href="#" class="field-link">忘记密码</a>
<div class="auth-form-wrap">
<div class="form-header">
<span class="form-kicker">SIGN IN</span>
<h1 class="form-title">欢迎回来</h1>
</div>
<div class="form-field captcha-field">
<input id="login-captcha" v-model="captcha.answer" type="text" placeholder=" " required />
<label for="login-captcha">验证码</label>
<div class="captcha-img" :class="{ loading: captcha.loading }" @click="fetchCaptcha" v-html="captcha.svg"></div>
<form class="auth-form" @submit.prevent="handleLogin">
<div class="form-field">
<input id="login-username" v-model="loginForm.username" type="text" placeholder=" " required />
<label for="login-username">用户名或邮箱</label>
</div>
<div class="form-field">
<input id="login-password" v-model="loginForm.password" type="password" placeholder=" " required />
<label for="login-password">密码</label>
<a href="#" class="field-link">忘记密码</a>
</div>
<div class="form-field captcha-field">
<input id="login-captcha" v-model="captcha.answer" type="text" placeholder=" " required />
<label for="login-captcha">验证码</label>
<div class="captcha-img" :class="{ loading: captcha.loading }" @click="fetchCaptcha" v-html="captcha.svg"></div>
</div>
<div class="form-options">
<label class="checkbox-field">
<input type="checkbox" v-model="loginForm.rememberMe" />
<span class="checkbox-custom"></span>
<span>记住我</span>
</label>
</div>
<button type="submit" class="submit-btn" :class="{ loading: loginLoading }" :disabled="loginLoading">
<span>{{ loginLoading ? '登录中...' : '登录' }}</span>
<span v-if="!loginLoading" 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>
<div class="form-options">
<label class="checkbox-field">
<input type="checkbox" v-model="loginForm.rememberMe" />
<span class="checkbox-custom"></span>
<span>记住我</span>
</label>
<div class="social-row">
<button class="social-btn social-btn--github" @click="loginWithGithub">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>GitHub</span>
</button>
<button class="social-btn social-btn--gitea" @click="loginWithGitea">
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.5 10.5a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0V11a.5.5 0 0 1 .5-.5Zm18.5.5v1.5a.5.5 0 0 0 1 0V11a.5.5 0 0 0-1 0ZM12 15.5a1.5 1.5 0 0 1 1.5 1.5v1a.5.5 0 0 0 1 0v-1a2.5 2.5 0 0 0-5 0v1a.5.5 0 0 0 1 0v-1A1.5 1.5 0 0 1 12 15.5Z"/>
<path d="M20.5 12A8.5 8.5 0 0 0 12 3.5 8.5 8.5 0 0 0 3.5 12v5.25c0 .69.56 1.25 1.25 1.25h2.5c.69 0 1.25-.56 1.25-1.25V13.5c0-.69-.56-1.25-1.25-1.25h-2.5c-.09 0-.17.01-.25.02v-.27a8.5 8.5 0 0 1 17 0v.27a1.2 1.2 0 0 0-.25-.02h-2.5c-.69 0-1.25.56-1.25 1.25v3.75c0 .69.56 1.25 1.25 1.25H19.25c.69 0 1.25-.56 1.25-1.25V12Z"/>
</svg>
<span>Gitea</span>
</button>
</div>
<button type="submit" class="submit-btn" :class="{ loading: loginLoading }" :disabled="loginLoading">
<span class="btn-text">{{ loginLoading ? '登录中...' : '登录' }}</span>
<span v-if="!loginLoading" 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>
<p class="form-footer">
还没有账户<NuxtLink to="/auth/register">立即注册</NuxtLink>
</p>
</div>
<div class="social-row">
<button class="social-btn" @click="loginWithGithub">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z"/>
</svg>
<span>GitHub</span>
</button>
<button class="social-btn" @click="loginWithGitea">
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M2.5 10.5a.5.5 0 0 1 .5.5v1.5a.5.5 0 0 1-1 0V11a.5.5 0 0 1 .5-.5Zm18.5.5v1.5a.5.5 0 0 0 1 0V11a.5.5 0 0 0-1 0ZM12 15.5a1.5 1.5 0 0 1 1.5 1.5v1a.5.5 0 0 0 1 0v-1a2.5 2.5 0 0 0-5 0v1a.5.5 0 0 0 1 0v-1A1.5 1.5 0 0 1 12 15.5Z"/>
<path d="M20.5 12A8.5 8.5 0 0 0 12 3.5 8.5 8.5 0 0 0 3.5 12v5.25c0 .69.56 1.25 1.25 1.25h2.5c.69 0 1.25-.56 1.25-1.25V13.5c0-.69-.56-1.25-1.25-1.25h-2.5c-.09 0-.17.01-.25.02v-.27a8.5 8.5 0 0 1 17 0v.27a1.2 1.2 0 0 0-.25-.02h-2.5c-.69 0-1.25.56-1.25 1.25v3.75c0 .69.56 1.25 1.25 1.25H19.25c.69 0 1.25-.56 1.25-1.25V12Z"/>
</svg>
<span>Gitea</span>
</button>
</div>
<p class="form-footer">
还没有账户<NuxtLink to="/auth/register">立即注册</NuxtLink>
</p>
</div>
</div>
</template>
@ -190,161 +198,113 @@ onMounted(() => {
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);
filter: blur(150px);
}
.bg-accent-block-1 {
width: 500px;
height: 500px;
width: 800px;
height: 800px;
background: var(--color-primary);
top: -150px;
right: 10%;
top: -300px;
right: -200px;
opacity: 0.1;
}
.bg-accent-block-2 {
width: 400px;
height: 400px;
width: 600px;
height: 600px;
background: var(--color-accent-amber);
bottom: -100px;
left: 20%;
bottom: -200px;
left: -150px;
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;
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;
}
.spike::before,
.spike::after {
content: '';
position: absolute;
background: var(--color-ink);
}
.spike::before {
.auth-layout {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 120px;
max-width: 1000px;
width: 100%;
height: 2px;
top: 50%;
left: 0;
transform: translateY(-50%);
padding: 64px;
position: relative;
z-index: 1;
align-items: center;
}
.spike::after {
width: 2px;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
.auth-brand {
display: flex;
flex-direction: column;
gap: 20px;
}
.spike-1 {
top: 15%;
left: 10%;
transform: rotate(15deg);
.brand-mark {
margin-bottom: 12px;
}
.spike-2 {
top: 25%;
right: 15%;
transform: rotate(45deg);
.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;
}
.spike-3 {
bottom: 20%;
left: 15%;
transform: rotate(30deg);
.brand-tagline {
font-family: var(--font-body);
font-size: 18px;
color: var(--color-muted);
margin: 0;
letter-spacing: 1px;
}
.spike-4 {
bottom: 30%;
right: 10%;
transform: rotate(60deg);
.auth-form-wrap {
display: flex;
flex-direction: column;
}
.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: 48px;
}
.form-header {
margin-bottom: 32px;
.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: 32px;
font-size: 42px;
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);
line-height: 1;
margin: 0;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 20px;
gap: 28px;
}
.form-field {
@ -353,15 +313,15 @@ onMounted(() => {
.form-field input {
width: 100%;
padding: 20px 16px 8px;
padding: 22px 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;
border: none;
border-bottom: 2px solid var(--color-hairline);
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
@ -382,13 +342,12 @@ onMounted(() => {
}
.form-field input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px rgba(204, 120, 92, 0.1);
border-bottom-color: var(--color-primary);
}
.form-field input:focus + label,
.form-field input:not(:placeholder-shown) + label {
top: 12px;
top: 10px;
font-size: 11px;
color: var(--color-primary);
letter-spacing: 0.5px;
@ -399,10 +358,11 @@ onMounted(() => {
right: 16px;
top: 50%;
transform: translateY(-50%);
font-size: 12px;
font-size: 11px;
color: var(--color-primary);
text-decoration: none;
font-weight: 500;
letter-spacing: 0.5px;
}
.field-link:hover {
@ -418,15 +378,14 @@ onMounted(() => {
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 36px;
border-radius: 4px;
height: 40px;
cursor: pointer;
transition: opacity 0.2s;
}
.captcha-img :deep(svg) {
display: block;
height: 36px;
height: 40px;
}
.captcha-img.loading {
@ -442,7 +401,7 @@ onMounted(() => {
.checkbox-field {
display: flex;
align-items: center;
gap: 10px;
gap: 12px;
cursor: pointer;
}
@ -457,7 +416,6 @@ onMounted(() => {
width: 18px;
height: 18px;
border: 1.5px solid var(--color-hairline);
border-radius: 4px;
background: var(--color-canvas);
display: flex;
align-items: center;
@ -483,51 +441,38 @@ onMounted(() => {
.checkbox-field span:last-child {
font-size: 13px;
color: var(--color-muted);
letter-spacing: 0.5px;
}
.submit-btn {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
gap: 14px;
width: 100%;
padding: 18px 24px;
padding: 20px 32px;
font-family: var(--font-body);
font-size: 15px;
font-size: 13px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--color-on-primary);
background: var(--color-primary);
background: var(--color-ink);
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%);
transition: background 0.2s ease;
}
.submit-btn:hover {
background: var(--color-primary-active);
background: var(--color-primary);
}
.submit-btn:active {
transform: scale(0.98);
transform: scale(0.99);
}
.submit-btn.loading {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
@ -537,14 +482,14 @@ onMounted(() => {
}
.submit-btn:hover .btn-arrow {
transform: translateX(4px);
transform: translateX(5px);
}
.form-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 24px 0;
gap: 20px;
margin: 40px 0;
}
.divider-line {
@ -554,31 +499,31 @@ onMounted(() => {
}
.divider-text {
font-size: 12px;
font-size: 10px;
color: var(--color-muted-soft);
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: 3px;
}
.social-row {
display: flex;
gap: 12px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.social-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 14px 16px;
padding: 16px 20px;
font-family: var(--font-body);
font-size: 14px;
font-size: 12px;
font-weight: 500;
color: var(--color-muted);
letter-spacing: 1px;
color: var(--color-body);
background: var(--color-canvas);
border: 1.5px solid var(--color-hairline);
border-radius: 10px;
border: 1px solid var(--color-hairline);
cursor: pointer;
transition: all 0.2s ease;
}
@ -586,16 +531,16 @@ onMounted(() => {
.social-btn:hover {
color: var(--color-ink);
background: var(--color-surface-soft);
border-color: var(--color-hairline-soft);
border-color: var(--color-ink);
}
.social-btn:active {
transform: scale(0.97);
transform: scale(0.98);
}
.form-footer {
text-align: center;
margin-top: 24px;
margin-top: 40px;
font-size: 14px;
color: var(--color-muted);
}
@ -609,4 +554,21 @@ onMounted(() => {
.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>

384
app/pages/auth/register.vue

@ -20,6 +20,10 @@ const captcha = reactive({
const registerLoading = ref(false)
async function loginWithGoogle() {
window.location.href = '/api/auth/oauth/google/authorize'
}
async function fetchCaptcha() {
captcha.loading = true
try {
@ -66,73 +70,81 @@ onMounted(fetchCaptcha)
<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 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="form-field">
<input id="register-confirm" v-model="registerForm.confirmPassword" type="password" placeholder=" " required />
<label for="register-confirm">确认密码</label>
<div class="auth-form-wrap">
<div class="form-header">
<span class="form-kicker">CREATE ACCOUNT</span>
<h1 class="form-title">创建账户</h1>
</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>
<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 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 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>
</form>
<div class="form-divider">
<span class="divider-line"></span>
<span class="divider-text">其他方式</span>
<span class="divider-line"></span>
<p class="form-footer">
已有账户<NuxtLink to="/auth/login">立即登录</NuxtLink>
</p>
</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>
@ -155,161 +167,113 @@ onMounted(fetchCaptcha)
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);
filter: blur(150px);
}
.bg-accent-block-1 {
width: 500px;
height: 500px;
width: 800px;
height: 800px;
background: var(--color-primary);
top: -150px;
right: 10%;
top: -300px;
right: -200px;
opacity: 0.1;
}
.bg-accent-block-2 {
width: 400px;
height: 400px;
width: 600px;
height: 600px;
background: var(--color-accent-amber);
bottom: -100px;
left: 20%;
bottom: -200px;
left: -150px;
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;
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;
}
.spike::before,
.spike::after {
content: '';
position: absolute;
background: var(--color-ink);
}
.spike::before {
.auth-layout {
display: grid;
grid-template-columns: 1.2fr 1fr;
gap: 120px;
max-width: 1000px;
width: 100%;
height: 2px;
top: 50%;
left: 0;
transform: translateY(-50%);
padding: 64px;
position: relative;
z-index: 1;
align-items: center;
}
.spike::after {
width: 2px;
height: 100%;
left: 50%;
top: 0;
transform: translateX(-50%);
.auth-brand {
display: flex;
flex-direction: column;
gap: 20px;
}
.spike-1 {
top: 15%;
left: 10%;
transform: rotate(15deg);
.brand-mark {
margin-bottom: 12px;
}
.spike-2 {
top: 25%;
right: 15%;
transform: rotate(45deg);
.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;
}
.spike-3 {
bottom: 20%;
left: 15%;
transform: rotate(30deg);
.brand-tagline {
font-family: var(--font-body);
font-size: 18px;
color: var(--color-muted);
margin: 0;
letter-spacing: 1px;
}
.spike-4 {
bottom: 30%;
right: 10%;
transform: rotate(60deg);
.auth-form-wrap {
display: flex;
flex-direction: column;
}
.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: 48px;
}
.form-header {
margin-bottom: 32px;
.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: 32px;
font-size: 42px;
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);
line-height: 1;
margin: 0;
}
.auth-form {
display: flex;
flex-direction: column;
gap: 20px;
gap: 28px;
}
.form-field {
@ -318,15 +282,15 @@ onMounted(fetchCaptcha)
.form-field input {
width: 100%;
padding: 20px 16px 8px;
padding: 22px 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;
border: none;
border-bottom: 2px solid var(--color-hairline);
outline: none;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
transition: border-color 0.2s ease;
box-sizing: border-box;
}
@ -347,13 +311,12 @@ onMounted(fetchCaptcha)
}
.form-field input:focus {
border-color: var(--color-primary);
box-shadow: 0 0 0 4px rgba(204, 120, 92, 0.1);
border-bottom-color: var(--color-primary);
}
.form-field input:focus + label,
.form-field input:not(:placeholder-shown) + label {
top: 12px;
top: 10px;
font-size: 11px;
color: var(--color-primary);
letter-spacing: 0.5px;
@ -368,15 +331,14 @@ onMounted(fetchCaptcha)
right: 8px;
top: 50%;
transform: translateY(-50%);
height: 36px;
border-radius: 4px;
height: 40px;
cursor: pointer;
transition: opacity 0.2s;
}
.captcha-img :deep(svg) {
display: block;
height: 36px;
height: 40px;
}
.captcha-img.loading {
@ -387,45 +349,31 @@ onMounted(fetchCaptcha)
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
gap: 14px;
width: 100%;
padding: 18px 24px;
padding: 20px 32px;
font-family: var(--font-body);
font-size: 15px;
font-size: 13px;
font-weight: 500;
letter-spacing: 2px;
text-transform: uppercase;
color: var(--color-on-primary);
background: var(--color-primary);
background: var(--color-ink);
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%);
transition: background 0.2s ease;
}
.submit-btn:hover {
background: var(--color-primary-active);
background: var(--color-primary);
}
.submit-btn:active {
transform: scale(0.98);
transform: scale(0.99);
}
.submit-btn.loading {
opacity: 0.7;
opacity: 0.6;
cursor: not-allowed;
}
@ -435,14 +383,14 @@ onMounted(fetchCaptcha)
}
.submit-btn:hover .btn-arrow {
transform: translateX(4px);
transform: translateX(5px);
}
.form-divider {
display: flex;
align-items: center;
gap: 16px;
margin: 24px 0;
gap: 20px;
margin: 40px 0;
}
.divider-line {
@ -452,10 +400,10 @@ onMounted(fetchCaptcha)
}
.divider-text {
font-size: 12px;
font-size: 10px;
color: var(--color-muted-soft);
text-transform: uppercase;
letter-spacing: 1px;
letter-spacing: 3px;
}
.social-btn {
@ -464,26 +412,31 @@ onMounted(fetchCaptcha)
justify-content: center;
gap: 12px;
width: 100%;
padding: 16px 20px;
padding: 18px 24px;
font-family: var(--font-body);
font-size: 14px;
font-size: 12px;
font-weight: 500;
color: var(--color-ink);
letter-spacing: 1px;
color: var(--color-body);
background: var(--color-canvas);
border: 1.5px solid var(--color-hairline);
border-radius: 8px;
border: 1px solid var(--color-hairline);
cursor: pointer;
transition: background 0.2s ease, border-color 0.2s ease;
transition: all 0.2s ease;
}
.social-btn:hover {
color: var(--color-ink);
background: var(--color-surface-soft);
border-color: var(--color-hairline-soft);
border-color: var(--color-ink);
}
.social-btn:active {
transform: scale(0.98);
}
.form-footer {
text-align: center;
margin-top: 24px;
margin-top: 40px;
font-size: 14px;
color: var(--color-muted);
}
@ -497,4 +450,21 @@ onMounted(fetchCaptcha)
.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>

8
packages/bolt-ui/components/ConfigProvider/src/ConfigProvider.vue

@ -3,10 +3,10 @@
</template>
<script setup lang="ts">
import { provide, computed, watch } from 'vue'
import { ConfigToken } from './Token'
import { computed, watch } from 'vue'
import { provideData } from './Token'
import { useLocale } from 'bolt-ui/locales'
import { IConfig } from './config';
import type { IConfig } from './config';
const props = withDefaults(
defineProps<IConfig>(),
@ -32,5 +32,5 @@ const state = computed(() => {
locale: props.locale ?? 'zh'
}
})
provide(ConfigToken, state)
provideData(state)
</script>

2
packages/bolt-ui/components/ConfigProvider/src/config.ts

@ -1,5 +1,5 @@
import type { LanguagesType } from 'bolt-ui/locales'
export interface IConfig { locale: LanguagesType }
export interface IConfig { locale?: LanguagesType }
export const defaultConfig: IConfig = { locale: 'zh' }

5
packages/bolt-ui/components/Container/src/Container.vue

@ -6,8 +6,7 @@
<script setup lang="ts">
import { useNamespace } from 'bolt-ui/utils/hooks/use-namespace'
import { inject } from 'vue';
import { ConfigToken } from 'bolt-ui/components/ConfigProvider/src/Token';
// import { useConfigData } from 'bolt-ui/components/ConfigProvider/src/Token';
withDefaults(
defineProps<{
@ -23,6 +22,6 @@ defineOptions({
name: 'BoContainer'
})
const aa = inject(ConfigToken)
// const config = useConfigData()
</script>

BIN
packages/drizzle-pkg/db.sqlite

Binary file not shown.
Loading…
Cancel
Save