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.
151 lines
4.4 KiB
151 lines
4.4 KiB
<script setup lang="ts">
|
|
import type { FormError, FormSubmitEvent } from '@nuxt/ui'
|
|
import { request, unwrapApiBody, type ApiResponse } from '../../utils/http/factory'
|
|
import { DEFAULT_AUTHENTICATED_LANDING_PATH, normalizeSafeRedirect } from '../../utils/auth-routes'
|
|
import { useAuthSession } from '../../composables/useAuthSession'
|
|
|
|
definePageMeta({
|
|
title: '登录',
|
|
layout: 'blank',
|
|
})
|
|
|
|
type LoginFormState = {
|
|
username: string
|
|
password: string
|
|
}
|
|
|
|
type LoginResult = {
|
|
user: {
|
|
id: number
|
|
username: string
|
|
}
|
|
}
|
|
|
|
const USERNAME_REGEX = /^[a-zA-Z0-9_]{3,20}$/
|
|
|
|
const state = reactive<LoginFormState>({
|
|
username: '',
|
|
password: '',
|
|
})
|
|
|
|
const loading = ref(false)
|
|
const resultType = ref<'success' | 'error' | ''>('')
|
|
const resultMessage = ref('')
|
|
const route = useRoute()
|
|
const { refresh } = useAuthSession()
|
|
const { allowRegister } = useGlobalConfig()
|
|
|
|
const validate = (formState: LoginFormState): 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 位' })
|
|
}
|
|
|
|
return errors
|
|
}
|
|
|
|
const onSubmit = async (_event: FormSubmitEvent<LoginFormState>) => {
|
|
resultType.value = ''
|
|
resultMessage.value = ''
|
|
loading.value = true
|
|
|
|
try {
|
|
const res = unwrapApiBody(await request<ApiResponse<LoginResult>>('/api/auth/login', {
|
|
method: 'POST',
|
|
body: {
|
|
username: state.username,
|
|
password: state.password,
|
|
},
|
|
}))
|
|
|
|
await refresh(true)
|
|
|
|
resultType.value = 'success'
|
|
resultMessage.value = `登录成功,欢迎 ${res.user.username}`
|
|
const redirectCandidate = Array.isArray(route.query.redirect)
|
|
? route.query.redirect[0]
|
|
: route.query.redirect
|
|
const redirectTarget = normalizeSafeRedirect(redirectCandidate, DEFAULT_AUTHENTICATED_LANDING_PATH)
|
|
await navigateTo(redirectTarget)
|
|
} catch (error: unknown) {
|
|
const message = typeof error === 'object' && error !== null && 'statusMessage' in error
|
|
? String(error.statusMessage)
|
|
: '登录失败,请稍后重试'
|
|
|
|
resultType.value = 'error'
|
|
resultMessage.value = message
|
|
} 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">请输入用户名和密码继续使用</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="current-password"
|
|
class="w-full"
|
|
/>
|
|
</UFormField>
|
|
|
|
<UButton type="submit" block :loading="loading">
|
|
立即登录
|
|
</UButton>
|
|
</UForm>
|
|
|
|
<UAlert
|
|
v-if="resultType"
|
|
:color="resultType === 'success' ? 'success' : 'error'"
|
|
:title="resultType === 'success' ? '操作成功' : '操作失败'"
|
|
:description="resultMessage"
|
|
class="mt-4"
|
|
/>
|
|
|
|
<div v-if="allowRegister" class="mt-4 flex items-center justify-center text-sm">
|
|
<NuxtLink to="/register" class="text-primary hover:underline">
|
|
没有账号?去注册
|
|
</NuxtLink>
|
|
</div>
|
|
</UCard>
|
|
</div>
|
|
</div>
|
|
</template>
|