feat: login
This commit is contained in:
26
components/LoginNeededContent.vue
Normal file
26
components/LoginNeededContent.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<script setup lang="ts">
|
||||
const loginState = useLoginState()
|
||||
|
||||
defineProps({
|
||||
contentClass: {
|
||||
type: String,
|
||||
default: ''
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="!loginState.is_logged_in"
|
||||
class="w-full flex flex-col justify-center items-center gap-2 bg-neutral-100 dark:bg-neutral-900">
|
||||
<Icon name="i-tabler-user-circle" class="text-7xl text-neutral-300 dark:text-neutral-700"/>
|
||||
<p class="text-sm text-neutral-500 dark:text-neutral-400">请登录后使用</p>
|
||||
<UButton class="mt-2 font-bold" to="/login" color="black" variant="solid" size="xs">登录</UButton>
|
||||
</div>
|
||||
<div :class="contentClass" v-else>
|
||||
<slot/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
@@ -1,14 +1,15 @@
|
||||
<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 handleComplete = (e: string[]) => toast.add({title: `提交验证码 ${e.join('')}`, color: 'indigo', icon: 'i-tabler-circle-check'})
|
||||
|
||||
const items = [
|
||||
{
|
||||
@@ -25,20 +26,85 @@ const items = [
|
||||
},
|
||||
]
|
||||
|
||||
const accountForm = reactive({username: '', password: ''})
|
||||
const accountForm = reactive<req.user.Login>({username: '', password: ''})
|
||||
const smsForm = reactive({mobile: '', sms_code: []})
|
||||
|
||||
function onSubmit(form: any) {
|
||||
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
|
||||
setTimeout(() => {
|
||||
|
||||
useFetchWrapped<req.user.SmsLogin, BaseResponse<resp.user.SmsLogin>>('App.User_User.MobileLogin', {
|
||||
mobile: smsForm.mobile
|
||||
}).then(res => {
|
||||
sms_triggered.value = true
|
||||
sms_sending.value = false
|
||||
sms_counting_down.value = 15 // TODO: modify this to change the countdown time
|
||||
sms_counting_down.value = 60 // TODO: modify this to change the countdown time
|
||||
toast.add({title: '短信验证码已发送', color: 'indigo', icon: 'i-tabler-circle-check'})
|
||||
const interval = setInterval(() => {
|
||||
sms_counting_down.value--
|
||||
@@ -46,7 +112,64 @@ const obtainSmsCode = () => {
|
||||
clearInterval(interval)
|
||||
}
|
||||
}, 1000)
|
||||
}, 2000)
|
||||
}).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>
|
||||
|
||||
@@ -58,7 +181,8 @@ const obtainSmsCode = () => {
|
||||
<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()"/>
|
||||
<UButton color="gray" variant="ghost" icon="i-heroicons-x-mark-20-solid" class="-my-1"
|
||||
@click="modal.close()"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -71,7 +195,7 @@ const obtainSmsCode = () => {
|
||||
</div>
|
||||
</template>
|
||||
<template #item="{ item }">
|
||||
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : smsForm)">
|
||||
<UCard @submit.prevent="() => onSubmit(accountForm)">
|
||||
<template #header>
|
||||
<p class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||
{{ item.label }}
|
||||
@@ -93,14 +217,14 @@ const obtainSmsCode = () => {
|
||||
<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" type="sms" class="w-full" required>
|
||||
<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"
|
||||
:loading="sms_sending" :disabled="!!sms_counting_down || final_loading"
|
||||
class="text-xs font-bold" color="gray"/>
|
||||
</UButtonGroup>
|
||||
</UFormGroup>
|
||||
@@ -110,17 +234,18 @@ const obtainSmsCode = () => {
|
||||
<PinInputRoot
|
||||
id="sms-input"
|
||||
v-model="smsForm.sms_code"
|
||||
:disabled="sms_sending"
|
||||
:disabled="sms_sending || final_loading"
|
||||
placeholder="○"
|
||||
class="w-full flex gap-2 justify-between items-center mt-1"
|
||||
@complete="handleComplete"
|
||||
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 6"
|
||||
v-for="(id, index) in 4"
|
||||
:key="id"
|
||||
:index="index"
|
||||
class="pin-input"
|
||||
:autofocus="index === 0"
|
||||
/>
|
||||
</PinInputRoot>
|
||||
</div>
|
||||
@@ -128,7 +253,7 @@ const obtainSmsCode = () => {
|
||||
</div>
|
||||
|
||||
<template #footer v-if="item.key !== 'sms'">
|
||||
<UButton type="submit" color="black" :disabled="final_loading">
|
||||
<UButton type="submit" color="black" :loading="final_loading">
|
||||
登录
|
||||
</UButton>
|
||||
</template>
|
||||
@@ -152,7 +277,7 @@ const obtainSmsCode = () => {
|
||||
}
|
||||
|
||||
.pin-input {
|
||||
@apply w-full aspect-square rounded text-center shadow caret-transparent;
|
||||
@apply w-full md:w-16 aspect-square rounded text-center shadow caret-transparent;
|
||||
@apply outline-0 ring-indigo-500 focus:ring font-bold;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user