refactor(deps): migrate to nuxt v4
This commit is contained in:
624
app/pages/user/authenticate.vue
Normal file
624
app/pages/user/authenticate.vue
Normal file
@@ -0,0 +1,624 @@
|
||||
<script lang="ts" setup>
|
||||
import type { FormSubmitEvent } from '#ui/types'
|
||||
import { object, string, type InferType } from 'yup'
|
||||
|
||||
definePageMeta({
|
||||
layout: 'authenticate',
|
||||
preventLoginCheck: true,
|
||||
})
|
||||
|
||||
useSeoMeta({
|
||||
title: '登录',
|
||||
})
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const toast = useToast()
|
||||
const loginState = useLoginState()
|
||||
|
||||
const sms_triggered = ref(false)
|
||||
const sms_sending = ref(false)
|
||||
const sms_counting_down = ref(0)
|
||||
const final_loading = ref(false)
|
||||
|
||||
const items = [
|
||||
{
|
||||
key: 'sms',
|
||||
label: '短信登录',
|
||||
icon: 'i-tabler-message-2',
|
||||
description: '使用短信验证码登录',
|
||||
},
|
||||
{
|
||||
key: 'account',
|
||||
label: '密码登录',
|
||||
icon: 'i-tabler-key',
|
||||
description: '使用已有账号和密码登录',
|
||||
},
|
||||
{
|
||||
key: 'recovery',
|
||||
label: '找回密码',
|
||||
icon: 'i-tabler-lock',
|
||||
description: '忘记密码时,可以通过手机号和验证码重置密码',
|
||||
},
|
||||
]
|
||||
|
||||
const currentTab = ref(0)
|
||||
|
||||
const accountForm = reactive<req.user.Login>({ username: '', password: '' })
|
||||
const smsForm = reactive({ mobile: '', sms_code: [] })
|
||||
|
||||
function onSubmit(form: req.user.Login) {
|
||||
console.log('Submitted form:', form)
|
||||
final_loading.value = true
|
||||
useFetchWrapped<req.user.Login, BaseResponse<resp.user.Login>>(
|
||||
'App.User_User.Login',
|
||||
{
|
||||
username: form.username,
|
||||
password: form.password,
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
final_loading.value = false
|
||||
if (res.ret !== 200) {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!res.data.is_login) {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: res.msg || '账号或密码错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!res.data.token || !res.data.user_id) {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: res.msg || '无法获取登录状态',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
loginState.token = res.data.token
|
||||
loginState.user.id = res.data.user_id
|
||||
loginState
|
||||
.updateProfile()
|
||||
.then(() => {
|
||||
loginState.checkSession()
|
||||
toast.add({
|
||||
title: '登录成功',
|
||||
description: `${loginState.user.username}, 欢迎回来`,
|
||||
color: 'primary',
|
||||
icon: 'i-tabler-login-2',
|
||||
})
|
||||
router.replace('/')
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: err.msg || '网络错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
final_loading.value = false
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: err.msg || '网络错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const obtainSmsCode = () => {
|
||||
smsForm.sms_code = []
|
||||
sms_sending.value = true
|
||||
|
||||
useFetchWrapped<req.user.SmsLogin, BaseResponse<resp.user.SmsLogin>>(
|
||||
'App.User_User.MobileLogin',
|
||||
{
|
||||
mobile: smsForm.mobile,
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.ret !== 200) {
|
||||
sms_sending.value = false
|
||||
toast.add({
|
||||
title: '验证码发送失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
sms_triggered.value = true
|
||||
sms_sending.value = false
|
||||
sms_counting_down.value = 60 // TODO: save timestamp to localstorage
|
||||
toast.add({
|
||||
title: '短信验证码已发送',
|
||||
color: 'indigo',
|
||||
icon: 'i-tabler-circle-check',
|
||||
})
|
||||
const interval = setInterval(() => {
|
||||
sms_counting_down.value--
|
||||
if (sms_counting_down.value <= 0) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '验证码发送失败',
|
||||
description: err.msg || '网络错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handle_sms_verify = (e: string[]) => {
|
||||
final_loading.value = true
|
||||
useFetchWrapped<
|
||||
req.user.SmsLoginVerify,
|
||||
BaseResponse<resp.user.SmsLoginVerify>
|
||||
>('App.User_User.MobileLoginVerify', {
|
||||
mobile: smsForm.mobile,
|
||||
sms_code: e.join(''),
|
||||
})
|
||||
.then((res) => {
|
||||
if (res.ret !== 200) {
|
||||
smsForm.sms_code = []
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
if (!res.data.token || !res.data.person_id) {
|
||||
smsForm.sms_code = []
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: res.msg || '无法获取登录状态',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
loginState.token = res.data.token
|
||||
loginState.user.id = res.data.person_id
|
||||
loginState
|
||||
.updateProfile()
|
||||
.then(() => {
|
||||
loginState.checkSession()
|
||||
toast.add({
|
||||
title: '登录成功',
|
||||
description: `${loginState.user.username}, 欢迎回来`,
|
||||
color: 'primary',
|
||||
icon: 'i-tabler-login-2',
|
||||
})
|
||||
router.replace('/')
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '登录失败',
|
||||
description: err.msg || '网络错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
.finally(() => {
|
||||
final_loading.value = false
|
||||
})
|
||||
})
|
||||
.finally(() => (final_loading.value = false))
|
||||
}
|
||||
|
||||
const forgetPasswordState = reactive({
|
||||
mobile: '',
|
||||
sms_code: '',
|
||||
password: '',
|
||||
})
|
||||
|
||||
const forgetPasswordSchema = object({
|
||||
mobile: string()
|
||||
.required('请输入手机号')
|
||||
.matches(/^1[3-9]\d{9}$/, '手机号格式不正确'),
|
||||
sms_code: string().required('请输入验证码').length(4, '验证码长度为4位'),
|
||||
password: string().required('请输入新密码').min(6, '密码长度至少为6位'),
|
||||
})
|
||||
|
||||
type ForgetPasswordSchema = InferType<typeof forgetPasswordSchema>
|
||||
|
||||
const obtainForgetSmsCode = () => {
|
||||
forgetPasswordState.sms_code = ''
|
||||
sms_sending.value = true
|
||||
|
||||
useFetchWrapped<req.user.SmsChangePasswordVerify, BaseResponse<{}>>(
|
||||
'App.User_User.ForgotPasswordSms',
|
||||
{
|
||||
mobile: forgetPasswordState.mobile,
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
if (res.ret !== 200) {
|
||||
sms_sending.value = false
|
||||
toast.add({
|
||||
title: '验证码发送失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
sms_triggered.value = true
|
||||
sms_sending.value = false
|
||||
sms_counting_down.value = 60 // TODO: save timestamp to localstorage
|
||||
toast.add({
|
||||
title: '短信验证码已发送',
|
||||
color: 'indigo',
|
||||
icon: 'i-tabler-circle-check',
|
||||
})
|
||||
const interval = setInterval(() => {
|
||||
sms_counting_down.value--
|
||||
if (sms_counting_down.value <= 0) {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '验证码发送失败',
|
||||
description: err.msg || '网络错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const onForgetPasswordSubmit = (
|
||||
event: FormSubmitEvent<ForgetPasswordSchema>
|
||||
) => {
|
||||
final_loading.value = true
|
||||
useFetchWrapped<req.user.SmsChangePassword, BaseResponse<{}>>(
|
||||
'App.User_User.ForgotPassword',
|
||||
{
|
||||
mobile: event.data.mobile,
|
||||
sms_code: event.data.sms_code,
|
||||
new_password: event.data.password,
|
||||
}
|
||||
)
|
||||
.then((res) => {
|
||||
final_loading.value = false
|
||||
if (res.ret !== 200) {
|
||||
toast.add({
|
||||
title: '重置密码失败',
|
||||
description: res.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
return
|
||||
}
|
||||
toast.add({
|
||||
title: '重置密码成功',
|
||||
description: '请您继续登录',
|
||||
color: 'green',
|
||||
icon: 'i-tabler-circle-check',
|
||||
})
|
||||
currentTab.value = 1
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '重置密码失败',
|
||||
description: err.msg || '未知错误',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const { token, user_id } = route.query
|
||||
if (!token || !user_id) {
|
||||
return
|
||||
}
|
||||
loginState.token = token.toString()
|
||||
loginState.user.id = user_id as unknown as number
|
||||
loginState
|
||||
.updateProfile()
|
||||
.then(() => {
|
||||
loginState.checkSession()
|
||||
// toast.add({
|
||||
// title: '登录成功',
|
||||
// description: `合作渠道认证成功`,
|
||||
// color: 'primary',
|
||||
// icon: 'i-tabler-login-2',
|
||||
// })
|
||||
router.replace('/')
|
||||
})
|
||||
.catch((err) => {
|
||||
toast.add({
|
||||
title: '认证失败',
|
||||
description: err.msg || 'Token 或 UserID 无效',
|
||||
color: 'red',
|
||||
icon: 'i-tabler-circle-x',
|
||||
})
|
||||
router.replace('/')
|
||||
})
|
||||
.finally(() => {
|
||||
final_loading.value = false
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="w-full min-h-screen flex flex-col justify-center items-center">
|
||||
<div class="flex flex-col items-center py-12 gap-2">
|
||||
<h1 class="text-3xl font-bold text-gray-900 dark:text-white">
|
||||
AIGC 微课视频研创平台
|
||||
</h1>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
请使用以下方式登录
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex flex-col items-center">
|
||||
<UTabs
|
||||
:items="items"
|
||||
class="w-full sm:w-[400px]"
|
||||
v-model="currentTab"
|
||||
>
|
||||
<template #default="{ item, index, selected }">
|
||||
<div class="flex items-center gap-2 relative truncate">
|
||||
<span class="truncate">{{ item.label }}</span>
|
||||
<span
|
||||
v-if="selected"
|
||||
class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<UCard @submit.prevent="() => onSubmit(accountForm)">
|
||||
<template #header>
|
||||
<p
|
||||
class="text-base font-semibold leading-6 text-gray-900 dark:text-white"
|
||||
>
|
||||
{{ item.label }}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||
{{ item.description }}
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<div
|
||||
v-if="item.key === 'account'"
|
||||
class="space-y-3"
|
||||
>
|
||||
<UFormGroup
|
||||
label="用户名"
|
||||
name="username"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="accountForm.username"
|
||||
:disabled="final_loading"
|
||||
required
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="密码"
|
||||
name="password"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="accountForm.password"
|
||||
:disabled="final_loading"
|
||||
type="password"
|
||||
required
|
||||
/>
|
||||
</UFormGroup>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-else-if="item.key === 'sms'"
|
||||
class="space-y-3"
|
||||
>
|
||||
<UFormGroup
|
||||
label="手机号"
|
||||
name="mobile"
|
||||
required
|
||||
>
|
||||
<UButtonGroup class="w-full">
|
||||
<UInput
|
||||
v-model="smsForm.mobile"
|
||||
:disabled="final_loading"
|
||||
type="sms"
|
||||
class="w-full"
|
||||
required
|
||||
>
|
||||
<template #leading>
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">
|
||||
+86
|
||||
</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UButton
|
||||
:label="
|
||||
sms_counting_down
|
||||
? `${sms_counting_down}秒后重发`
|
||||
: '获取验证码'
|
||||
"
|
||||
@click="obtainSmsCode"
|
||||
:loading="sms_sending"
|
||||
:disabled="!!sms_counting_down || final_loading"
|
||||
class="text-xs font-bold"
|
||||
color="gray"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
</UFormGroup>
|
||||
<Transition name="pin-root">
|
||||
<div
|
||||
v-if="sms_triggered"
|
||||
class="w-full overflow-hidden"
|
||||
>
|
||||
<Label
|
||||
for="pin-input"
|
||||
class="pin-label"
|
||||
>
|
||||
验证码
|
||||
</Label>
|
||||
<PinInputRoot
|
||||
id="sms-input"
|
||||
v-model="smsForm.sms_code"
|
||||
:disabled="sms_sending || final_loading"
|
||||
placeholder="○"
|
||||
class="w-full flex gap-2 justify-between md:justify-start items-center mt-1"
|
||||
@complete="handle_sms_verify"
|
||||
type="number"
|
||||
otp
|
||||
required
|
||||
>
|
||||
<PinInputInput
|
||||
v-for="(id, index) in 4"
|
||||
:key="id"
|
||||
:index="index"
|
||||
class="pin-input w-12 h-12 text-center rounded-lg"
|
||||
:autofocus="index === 0"
|
||||
/>
|
||||
</PinInputRoot>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="item.key === 'recovery'"
|
||||
class="space-y-3"
|
||||
>
|
||||
<UForm
|
||||
class="space-y-3"
|
||||
:schema="forgetPasswordSchema"
|
||||
:state="forgetPasswordState"
|
||||
@submit="onForgetPasswordSubmit"
|
||||
>
|
||||
<UFormGroup
|
||||
label="手机号"
|
||||
name="mobile"
|
||||
required
|
||||
>
|
||||
<UButtonGroup class="w-full">
|
||||
<UInput
|
||||
v-model="forgetPasswordState.mobile"
|
||||
:disabled="final_loading"
|
||||
type="tel"
|
||||
class="w-full"
|
||||
>
|
||||
<template #leading>
|
||||
<span class="text-gray-500 dark:text-gray-400 text-xs">
|
||||
+86
|
||||
</span>
|
||||
</template>
|
||||
</UInput>
|
||||
<UButton
|
||||
:label="
|
||||
sms_counting_down
|
||||
? `${sms_counting_down}秒后重发`
|
||||
: '获取验证码'
|
||||
"
|
||||
@click="obtainForgetSmsCode"
|
||||
:loading="sms_sending"
|
||||
:disabled="!!sms_counting_down"
|
||||
class="text-xs font-bold"
|
||||
color="gray"
|
||||
/>
|
||||
</UButtonGroup>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="验证码"
|
||||
name="sms_code"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="forgetPasswordState.sms_code"
|
||||
type="sms"
|
||||
class="w-full"
|
||||
:disabled="final_loading"
|
||||
/>
|
||||
</UFormGroup>
|
||||
<UFormGroup
|
||||
label="新密码"
|
||||
name="password"
|
||||
required
|
||||
>
|
||||
<UInput
|
||||
v-model="forgetPasswordState.password"
|
||||
type="password"
|
||||
:disabled="final_loading"
|
||||
/>
|
||||
</UFormGroup>
|
||||
|
||||
<div>
|
||||
<UButton
|
||||
type="submit"
|
||||
:loading="final_loading"
|
||||
>
|
||||
重置密码
|
||||
</UButton>
|
||||
</div>
|
||||
</UForm>
|
||||
</div>
|
||||
|
||||
<template
|
||||
#footer
|
||||
v-if="item.key === 'account'"
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<UButton
|
||||
type="submit"
|
||||
color="black"
|
||||
:loading="final_loading"
|
||||
>
|
||||
登录
|
||||
</UButton>
|
||||
<UButton
|
||||
variant="link"
|
||||
color="gray"
|
||||
@click="currentTab = 2"
|
||||
>
|
||||
忘记密码
|
||||
</UButton>
|
||||
</div>
|
||||
</template>
|
||||
</UCard>
|
||||
</template>
|
||||
</UTabs>
|
||||
</div>
|
||||
<div class="pt-4">
|
||||
<UButton
|
||||
color="gray"
|
||||
variant="ghost"
|
||||
class="!text-gray-500"
|
||||
@click="
|
||||
() => {
|
||||
router.push('/user/register')
|
||||
}
|
||||
"
|
||||
>
|
||||
注册新账号
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
Reference in New Issue
Block a user