296 lines
9.3 KiB
Vue
296 lines
9.3 KiB
Vue
<script setup lang="ts">
|
|
import { Label, PinInputInput, PinInputRoot } from 'radix-vue'
|
|
import { useFetchWrapped } from '~/composables/useFetchWrapped'
|
|
|
|
const toast = useToast()
|
|
const modal = useModal()
|
|
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: '使用已有账号和密码登录',
|
|
},
|
|
]
|
|
|
|
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()
|
|
modal.close()
|
|
toast.add({
|
|
title: '登录成功',
|
|
description: `${loginState.user.username}, 欢迎回来`,
|
|
color: 'primary',
|
|
icon: 'i-tabler-login-2',
|
|
})
|
|
}).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()
|
|
modal.close()
|
|
toast.add({
|
|
title: '登录成功',
|
|
description: `${loginState.user.username}, 欢迎回来`,
|
|
color: 'primary',
|
|
icon: 'i-tabler-login-2',
|
|
})
|
|
}).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)
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<UModal prevent-close>
|
|
<UCard>
|
|
<template #header>
|
|
<div class="flex items-center justify-between">
|
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
|
登录眩生花 AI 助手
|
|
</h3>
|
|
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1"
|
|
@click="modal.close()"/>
|
|
</div>
|
|
</template>
|
|
|
|
<UTabs :items="items" class="w-full">
|
|
<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">
|
|
<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"
|
|
:autofocus="index === 0"
|
|
/>
|
|
</PinInputRoot>
|
|
</div>
|
|
</Transition>
|
|
</div>
|
|
|
|
<template #footer v-if="item.key !== 'sms'">
|
|
<UButton type="submit" color="black" :loading="final_loading">
|
|
登录
|
|
</UButton>
|
|
</template>
|
|
</UCard>
|
|
</template>
|
|
</UTabs>
|
|
|
|
</UCard>
|
|
</UModal>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pin-root-enter-active,
|
|
.pin-root-leave-active {
|
|
@apply transition duration-500;
|
|
}
|
|
|
|
.pin-root-enter-from,
|
|
.pin-root-leave-to {
|
|
@apply opacity-0 -translate-y-2;
|
|
}
|
|
|
|
.pin-input {
|
|
@apply w-full md:w-16 aspect-square rounded text-center shadow caret-transparent;
|
|
@apply outline-0 ring-indigo-500 focus:ring font-bold;
|
|
}
|
|
|
|
.pin-label {
|
|
@apply block text-sm font-medium text-gray-700 dark:text-gray-200 after:content-['*'] after:ms-0.5 after:text-red-500 dark:after:text-red-400;
|
|
}
|
|
</style> |