feat: 独立登录页面

This commit is contained in:
2025-02-17 18:54:57 +08:00
parent 2cb9dc10ca
commit 8209a314f2
5 changed files with 658 additions and 35 deletions

View File

@@ -26,6 +26,8 @@ onMounted(() => {
icon: 'i-tabler-alert-triangle', icon: 'i-tabler-alert-triangle',
}) })
modal.open(ModalAuthentication) modal.open(ModalAuthentication)
} else if (!res && !loginState.token) {
router.replace('/user/authenticate')
} }
}) })
}) })

View File

@@ -39,6 +39,8 @@ export const useVideoSubtitleEmbedding = async (
}; };
} }
console.log(`video clip: ${videoUrl}`)
const videoClip = new MP4Clip((await fetch(videoUrl)).body!) const videoClip = new MP4Clip((await fetch(videoUrl)).body!)
const videoSprite = new OffscreenSprite(videoClip) const videoSprite = new OffscreenSprite(videoClip)
videoSprite.time = { duration: videoClip.meta.duration, offset: 0 } videoSprite.time = { duration: videoClip.meta.duration, offset: 0 }

9
layouts/authenticate.vue Normal file
View File

@@ -0,0 +1,9 @@
<script lang="ts" setup></script>
<template>
<div>
<slot />
</div>
</template>
<style scoped></style>

View File

@@ -37,28 +37,40 @@ const links = [
label: 'AI 工具导航', label: 'AI 工具导航',
icon: 'tabler:planet', icon: 'tabler:planet',
to: '/aigc/navigation', to: '/aigc/navigation',
} },
] ]
const items = [ const items = [
[{ [
label: 'support@fenshenzhike.com', {
slot: 'account', label: 'support@fenshenzhike.com',
disabled: true, slot: 'account',
}], [{ disabled: true,
label: '账号管理', },
icon: 'i-tabler-user-circle', ],
to: '/profile' [
}], [{ {
label: '注销登录', label: '账号管理',
icon: 'i-tabler-logout', icon: 'i-tabler-user-circle',
click: () => loginState.logout().then(() => toast.add({ to: '/profile',
title: '退出登录', },
description: `您已成功退出登录账号`, ],
color: 'indigo', [
icon: 'i-tabler-logout-2', {
})), label: '注销登录',
}], icon: 'i-tabler-logout',
click: () =>
loginState.logout().then(() => {
toast.add({
title: '退出登录',
description: `您已成功退出登录账号`,
color: 'indigo',
icon: 'i-tabler-logout-2',
})
router.push({ path: '/user/authenticate' })
}),
},
],
] ]
const open_login_modal = () => { const open_login_modal = () => {
@@ -70,23 +82,38 @@ const open_login_modal = () => {
<div class="relative grid w-full min-h-screen"> <div class="relative grid w-full min-h-screen">
<header> <header>
<h1 class="inline-flex flex-col"> <h1 class="inline-flex flex-col">
<span class="text-lg text-neutral-600 dark:text-neutral-300 font-bold">AIGC 微课视频研创平台</span> <span class="text-lg text-neutral-600 dark:text-neutral-300 font-bold">
AIGC 微课视频研创平台
</span>
<!-- <span class="text-xs text-neutral-600 dark:text-neutral-300">眩生花科技</span> --> <!-- <span class="text-xs text-neutral-600 dark:text-neutral-300">眩生花科技</span> -->
</h1> </h1>
<div class="hidden md:block"> <div class="hidden md:block">
<UHorizontalNavigation :links="links" class="select-none"/> <UHorizontalNavigation
:links="links"
class="select-none"
/>
</div> </div>
<div class="flex flex-row items-center gap-4"> <div class="flex flex-row items-center gap-4">
<ClientOnly> <ClientOnly>
<UButton <UButton
:icon="isDark ? 'i-line-md-sunny-outline-to-moon-alt-loop-transition' : 'i-line-md-moon-alt-to-sunny-outline-loop-transition'" :icon="
isDark
? 'i-line-md-sunny-outline-to-moon-alt-loop-transition'
: 'i-line-md-moon-alt-to-sunny-outline-loop-transition'
"
color="gray" color="gray"
variant="ghost" variant="ghost"
aria-label="Theme" aria-label="Theme"
@click="isDark = !isDark" @click="isDark = !isDark"
/> />
<UButton v-if="!loginState.is_logged_in" label="登录或注册" size="xs" class="font-bold" color="indigo" <UButton
@click="open_login_modal"/> v-if="!loginState.is_logged_in"
label="登录或注册"
size="xs"
class="font-bold"
color="indigo"
@click="open_login_modal"
/>
<UDropdown <UDropdown
v-if="loginState.is_logged_in" v-if="loginState.is_logged_in"
:items="items" :items="items"
@@ -105,18 +132,28 @@ const open_login_modal = () => {
<div class="text-left"> <div class="text-left">
<p class="flex items-center gap-1"> <p class="flex items-center gap-1">
已登录为 已登录为
<UBadge v-if="loginState.user.auth_code === 2" color="amber" size="xs" variant="subtle"> <UBadge
v-if="loginState.user.auth_code === 2"
color="amber"
size="xs"
variant="subtle"
>
OP OP
</UBadge> </UBadge>
</p> </p>
<p class="truncate whitespace-nowrap max-w-40 font-medium text-gray-900 dark:text-white"> <p
class="truncate whitespace-nowrap max-w-40 font-medium text-gray-900 dark:text-white"
>
{{ loginState.user?.username }} {{ loginState.user?.username }}
</p> </p>
</div> </div>
</template> </template>
<template #item="{ item }"> <template #item="{ item }">
<span class="truncate">{{ item.label }}</span> <span class="truncate">{{ item.label }}</span>
<UIcon :name="item.icon" class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto"/> <UIcon
:name="item.icon"
class="flex-shrink-0 h-4 w-4 text-gray-400 dark:text-gray-500 ms-auto"
/>
</template> </template>
</UDropdown> </UDropdown>
</ClientOnly> </ClientOnly>
@@ -124,11 +161,13 @@ const open_login_modal = () => {
</header> </header>
<main> <main>
<slot/> <slot />
</main> </main>
<footer> <footer>
<p class="text-sm text-neutral-500">© {{ dayjs().year() }} 重庆眩生花科技有限公司</p> <p class="text-sm text-neutral-500">
© {{ dayjs().year() }} 重庆眩生花科技有限公司
</p>
</footer> </footer>
</div> </div>
</template> </template>
@@ -136,25 +175,25 @@ const open_login_modal = () => {
<style> <style>
body { body {
@apply bg-neutral-50 dark:bg-neutral-950 bg-fixed; @apply bg-neutral-50 dark:bg-neutral-950 bg-fixed;
@apply bg-[url('~/assets/background-pattern.svg')] dark:bg-[url('~/assets/background-pattern-dark.svg')]; /* @apply bg-[url('~/assets/background-pattern.svg')] dark:bg-[url('~/assets/background-pattern-dark.svg')]; */
} }
::-webkit-scrollbar { ::-webkit-scrollbar {
--bar-width: 5px; --bar-width: 5px;
width: var(--bar-width); width: var(--bar-width);
height: var(--bar-width) height: var(--bar-width);
} }
::-webkit-scrollbar-track { ::-webkit-scrollbar-track {
background-color: transparent background-color: transparent;
} }
::-webkit-scrollbar-thumb { ::-webkit-scrollbar-thumb {
--bar-color: rgba(0, 0, 0, .1); --bar-color: rgba(0, 0, 0, 0.1);
background-color: var(--bar-color); background-color: var(--bar-color);
border-radius: 20px; border-radius: 20px;
background-clip: content-box; background-clip: content-box;
border: 1px solid transparent border: 1px solid transparent;
} }
/* /*
@@ -191,4 +230,4 @@ footer {
@apply dark:bg-neutral-900 dark:border-neutral-800; @apply dark:bg-neutral-900 dark:border-neutral-800;
@apply flex flex-row items-center justify-between px-4; @apply flex flex-row items-center justify-between px-4;
} }
</style> </style>

571
pages/user/authenticate.vue Normal file
View File

@@ -0,0 +1,571 @@
<script lang="ts" setup>
import type { FormSubmitEvent } from '#ui/types'
import { object, string, type InferType } from 'yup'
definePageMeta({
layout: 'authenticate',
})
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',
})
})
}
</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>
</template>
<style scoped></style>