@ -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 : 280 px ;
background : linear - gradient ( 180 deg , rgba ( 204 , 120 , 92 , 0.08 ) 0 % , transparent 100 % ) ;
}
. bg - gradient - bottom {
position : absolute ;
bottom : 0 ;
left : 0 ;
right : 0 ;
height : 200 px ;
background : linear - gradient ( 0 deg , rgba ( 204 , 120 , 92 , 0.05 ) 0 % , transparent 100 % ) ;
}
. bg - accent - block {
position : absolute ;
border - radius : 50 % ;
filter : blur ( 8 0px ) ;
filter : blur ( 150 px ) ;
}
. bg - accent - block - 1 {
width : 5 00px ;
height : 5 00px ;
width : 800 px ;
height : 800 px ;
background : var ( -- color - primary ) ;
top : - 15 0px ;
right : 10 % ;
top : - 300 px ;
right : - 200 px ;
opacity : 0.1 ;
}
. bg - accent - block - 2 {
width : 4 00px ;
height : 4 00px ;
width : 600 px ;
height : 600 px ;
background : var ( -- color - accent - amber ) ;
bottom : - 1 00px ;
left : 20 % ;
bottom : - 2 00px ;
left : - 150 px ;
opacity : 0.06 ;
}
. bg - accent - block - 3 {
width : 300 px ;
height : 300 px ;
background : var ( -- color - primary ) ;
bottom : 20 % ;
right : - 100 px ;
opacity : 0.08 ;
}
. bg - grid {
position : absolute ;
inset : 0 ;
background - image : radial - gradient ( circle , var ( -- color - hairline ) 1 px , transparent 1 px ) ;
background - size : 20 px 20 px ;
opacity : 0.4 ;
}
. bg - spike - spots {
position : absolute ;
inset : 0 ;
}
. spike {
position : absolute ;
width : 40 px ;
height : 40 px ;
opacity : 0.06 ;
background - image : linear - gradient ( var ( -- color - hairline ) 1 px , transparent 1 px ) ,
linear - gradient ( 90 deg , var ( -- color - hairline ) 1 px , transparent 1 px ) ;
background - size : 48 px 48 px ;
opacity : 0.2 ;
}
. spike : : before ,
. spike : : after {
content : '' ;
position : absolute ;
background : var ( -- color - ink ) ;
}
. spike : : before {
. auth - layout {
display : grid ;
grid - template - columns : 1.2 fr 1 fr ;
gap : 120 px ;
max - width : 1000 px ;
width : 100 % ;
height : 2 px ;
top : 50 % ;
left : 0 ;
transform : translateY ( - 50 % ) ;
padding : 64 px ;
position : relative ;
z - index : 1 ;
align - items : center ;
}
. spike : : after {
width : 2 px ;
height : 100 % ;
left : 50 % ;
top : 0 ;
transform : translateX ( - 50 % ) ;
. auth - brand {
display : flex ;
flex - direction : column ;
gap : 20 px ;
}
. spike - 1 {
top : 15 % ;
left : 10 % ;
transform : rotate ( 15 deg ) ;
. brand - mark {
margin - bottom : 12 px ;
}
. spike - 2 {
top : 25 % ;
right : 15 % ;
transform : rotate ( 45 deg ) ;
. brand - wordmark {
font - family : var ( -- font - display ) ;
font - size : 56 px ;
font - weight : 400 ;
color : var ( -- color - ink ) ;
letter - spacing : - 2 px ;
line - height : 1 ;
margin : 0 ;
}
. spike - 3 {
bottom : 20 % ;
left : 15 % ;
transform : rotate ( 30 deg ) ;
. brand - tagline {
font - family : var ( -- font - body ) ;
font - size : 18 px ;
color : var ( -- color - muted ) ;
margin : 0 ;
letter - spacing : 1 px ;
}
. spike - 4 {
bottom : 30 % ;
right : 10 % ;
transform : rotate ( 60 deg ) ;
. auth - form - wrap {
display : flex ;
flex - direction : column ;
}
. auth - card {
width : 100 % ;
max - width : 420 px ;
background : var ( -- color - canvas ) ;
border - radius : 12 px ;
box - shadow : 0 2 px 12 px rgba ( 20 , 20 , 19 , 0.06 ) ;
padding : 40 px ;
position : relative ;
z - index : 1 ;
margin : 24 px ;
. form - header {
margin - bottom : 48 px ;
}
. form - header {
margin - bottom : 32 px ;
. form - kicker {
font - family : var ( -- font - body ) ;
font - size : 10 px ;
font - weight : 500 ;
color : var ( -- color - primary ) ;
letter - spacing : 4 px ;
text - transform : uppercase ;
display : block ;
margin - bottom : 14 px ;
}
. form - title {
font - family : var ( -- font - display ) ;
font - size : 32 px ;
font - size : 4 2px ;
font - weight : 400 ;
color : var ( -- color - ink ) ;
letter - spacing : - 1 px ;
line - height : 1.1 ;
margin : 0 0 8 px ;
}
. form - subtitle {
font - family : var ( -- font - body ) ;
font - size : 15 px ;
color : var ( -- color - muted ) ;
line - height : 1 ;
margin : 0 ;
}
. auth - form {
display : flex ;
flex - direction : column ;
gap : 20 px ;
gap : 28 px ;
}
. form - field {
@ -353,15 +313,15 @@ onMounted(() => {
. form - field input {
width : 100 % ;
padding : 20 px 16 px 8 px ;
padding : 22 px 16 px 8 px ;
font - family : var ( -- font - body ) ;
font - size : 16 px ;
color : var ( -- color - ink ) ;
background : var ( -- color - canvas ) ;
border : 1.5 px s olid var ( -- color - hairli ne) ;
border - radius : 8 px ;
border : n one;
border - bottom : 2 px solid var ( -- color - hairline ) ;
outline : none ;
transition : border - color 0.2 s ease , box - shadow 0.2 s ease ;
transition : border - color 0.2 s ease ;
box - sizing : border - box ;
}
@ -382,13 +342,12 @@ onMounted(() => {
}
. form - field input : focus {
border - color : var ( -- color - primary ) ;
box - shadow : 0 0 0 4 px 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 : 12 px ;
top : 10 px ;
font - size : 11 px ;
color : var ( -- color - primary ) ;
letter - spacing : 0.5 px ;
@ -399,10 +358,11 @@ onMounted(() => {
right : 16 px ;
top : 50 % ;
transform : translateY ( - 50 % ) ;
font - size : 12 px ;
font - size : 11 px ;
color : var ( -- color - primary ) ;
text - decoration : none ;
font - weight : 500 ;
letter - spacing : 0.5 px ;
}
. field - link : hover {
@ -418,15 +378,14 @@ onMounted(() => {
right : 8 px ;
top : 50 % ;
transform : translateY ( - 50 % ) ;
height : 36 px ;
border - radius : 4 px ;
height : 40 px ;
cursor : pointer ;
transition : opacity 0.2 s ;
}
. captcha - img : deep ( svg ) {
display : block ;
height : 36 px ;
height : 40 px ;
}
. captcha - img . loading {
@ -442,7 +401,7 @@ onMounted(() => {
. checkbox - field {
display : flex ;
align - items : center ;
gap : 10 px ;
gap : 12 px ;
cursor : pointer ;
}
@ -457,7 +416,6 @@ onMounted(() => {
width : 18 px ;
height : 18 px ;
border : 1.5 px solid var ( -- color - hairline ) ;
border - radius : 4 px ;
background : var ( -- color - canvas ) ;
display : flex ;
align - items : center ;
@ -483,51 +441,38 @@ onMounted(() => {
. checkbox - field span : last - child {
font - size : 13 px ;
color : var ( -- color - muted ) ;
letter - spacing : 0.5 px ;
}
. submit - btn {
display : flex ;
align - items : center ;
justify - content : center ;
gap : 12 px ;
gap : 14 px ;
width : 100 % ;
padding : 18 px 24 px ;
padding : 20 px 3 2px ;
font - family : var ( -- font - body ) ;
font - size : 15 px ;
font - size : 13 px ;
font - weight : 500 ;
letter - spacing : 2 px ;
text - transform : uppercase ;
color : var ( -- color - on - primary ) ;
background : var ( -- color - primary ) ;
background : var ( -- color - ink ) ;
border : none ;
border - radius : 8 px ;
cursor : pointer ;
transition : background 0.2 s ease , transform 0.15 s ease ;
position : relative ;
overflow : hidden ;
}
. submit - btn : : before {
content : '' ;
position : absolute ;
inset : 0 ;
background : linear - gradient ( 90 deg , transparent , rgba ( 255 , 255 , 255 , 0.15 ) , transparent ) ;
transform : translateX ( - 100 % ) ;
transition : transform 0.5 s ease ;
}
. submit - btn : hover : : before {
transform : translateX ( 100 % ) ;
transition : background 0.2 s 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 ( 4 px ) ;
transform : translateX ( 5 px ) ;
}
. form - divider {
display : flex ;
align - items : center ;
gap : 16 px ;
margin : 2 4px 0 ;
gap : 20 px ;
margin : 40 px 0 ;
}
. divider - line {
@ -554,31 +499,31 @@ onMounted(() => {
}
. divider - text {
font - size : 12 px ;
font - size : 10 px ;
color : var ( -- color - muted - soft ) ;
text - transform : uppercase ;
letter - spacing : 1 px ;
letter - spacing : 3 px ;
}
. social - row {
display : flex ;
gap : 12 px ;
display : grid ;
grid - template - columns : 1 fr 1 fr ;
gap : 16 px ;
}
. social - btn {
flex : 1 ;
display : flex ;
align - items : center ;
justify - content : center ;
gap : 10 px ;
padding : 14 px 16 px ;
padding : 16 px 20 px ;
font - family : var ( -- font - body ) ;
font - size : 14 px ;
font - size : 12 px ;
font - weight : 500 ;
color : var ( -- color - muted ) ;
letter - spacing : 1 px ;
color : var ( -- color - body ) ;
background : var ( -- color - canvas ) ;
border : 1.5 px solid var ( -- color - hairline ) ;
border - radius : 10 px ;
border : 1 px solid var ( -- color - hairline ) ;
cursor : pointer ;
transition : all 0.2 s 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 : 2 4px ;
margin - top : 40 px ;
font - size : 14 px ;
color : var ( -- color - muted ) ;
}
@ -609,4 +554,21 @@ onMounted(() => {
. form - footer a : hover {
text - decoration : underline ;
}
@ media ( max - width : 768 px ) {
. auth - layout {
grid - template - columns : 1 fr ;
gap : 64 px ;
padding : 32 px ;
}
. auth - brand {
align - items : center ;
text - align : center ;
}
. brand - wordmark {
font - size : 42 px ;
}
}
< / style >