Skip to content

Commit

Permalink
feat: 完善登录 & 注册验证机制
Browse files Browse the repository at this point in the history
  • Loading branch information
nonhana committed Nov 25, 2024
1 parent f0c6867 commit 191c591
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 239 deletions.
108 changes: 56 additions & 52 deletions components/hana/Dialog/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ watch(visible, (newV) => {
requestAnimationFrame(() => {
overlayRef.value!.style.opacity = String(props.overlayOpacity)
})
document.body.style.overflow = 'hidden'
}
else {
overlayRef.value!.style.opacity = '0'
document.body.style.overflow = ''
}
}
})
Expand All @@ -69,57 +71,59 @@ defineExpose({
</script>

<template>
<transition v-bind="transitionClasses" @after-leave="handleAfterLeave">
<div
v-if="visible"
class="fixed left-1/2 top-1/2 z-50 max-w-[90%] -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white p-5 shadow"
:style="{ width }"
>
<div class="mb-5">
<slot
name="header"
:close="handleClose"
>
<div v-if="!hideHeader" class="flex items-center">
<span v-if="title" class="text-2xl font-bold">{{ title }}</span>
<HanaButton v-if="!programmatic" icon="lucide:x" class="relative -right-2 -top-2 ml-auto" icon-button @click="handleClose" />
</div>
</slot>
</div>
<div v-if="programmatic">
<span>{{ content }}</span>
</div>
<div v-else :style="{ height }">
<slot />
<teleport to="body">
<transition v-bind="transitionClasses" @after-leave="handleAfterLeave">
<div
v-if="visible"
class="fixed left-1/2 top-1/2 z-50 max-w-[90%] -translate-x-1/2 -translate-y-1/2 rounded-2xl bg-white p-5 shadow"
:style="{ width }"
>
<div class="mb-5">
<slot
name="header"
:close="handleClose"
>
<div v-if="!hideHeader" class="flex items-center">
<span v-if="title" class="text-2xl font-bold">{{ title }}</span>
<HanaButton v-if="!programmatic" icon="lucide:x" class="relative -right-2 -top-2 ml-auto" icon-button @click="handleClose" />
</div>
</slot>
</div>
<div v-if="programmatic">
<span>{{ content }}</span>
</div>
<div v-else :style="{ height }">
<slot />
</div>
<div v-if="programmatic" class="mt-5 flex justify-end gap-4">
<HanaButton
v-if="showCancelButton || false"
shape="square"
class="w-20"
@click="emits('cancel')"
>
{{ cancelText || '取消' }}
</HanaButton>
<HanaButton
v-if="showOkButton || true"
dark-mode
shape="square"
class="w-20"
@click="emits('ok')"
>
{{ okText || '确定' }}
</HanaButton>
</div>
<div v-else-if="$slots.footer" class="mt-5">
<slot name="footer" />
</div>
</div>
<div v-if="programmatic" class="mt-5 flex justify-end gap-4">
<HanaButton
v-if="showCancelButton || false"
shape="square"
class="w-20"
@click="emits('cancel')"
>
{{ cancelText || '取消' }}
</HanaButton>
<HanaButton
v-if="showOkButton || true"
dark-mode
shape="square"
class="w-20"
@click="emits('ok')"
>
{{ okText || '确定' }}
</HanaButton>
</div>
<div v-else-if="$slots.footer" class="mt-5">
<slot name="footer" />
</div>
</div>
</transition>
<div
ref="overlayRef"
class="fixed inset-0 z-40 bg-black opacity-0 transition-opacity duration-300"
:class="{ 'pointer-events-none': !visible }"
@click="handleClose"
/>
</transition>
<div
ref="overlayRef"
class="fixed inset-0 z-40 bg-black opacity-0 transition-opacity duration-300"
:class="{ 'pointer-events-none': !visible }"
@click="handleClose"
/>
</teleport>
</template>
28 changes: 24 additions & 4 deletions components/hana/Input.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
withDefaults(defineProps<{
const props = withDefaults(defineProps<{
name?: string
type?: 'text' | 'textarea' | 'password'
placeholder?: string
Expand Down Expand Up @@ -35,6 +35,19 @@ function handleMouseLeave() {
function handleKeyDown(event: KeyboardEvent) {
emits('keydown', event)
}
const showPassword = ref(false)
function toggleShowPassword() {
showPassword.value = !showPassword.value
}
const curType = computed(() => {
if (props.type === 'textarea') {
return props.type
}
return showPassword.value ? 'text' : props.type
})
</script>

<template>
Expand All @@ -58,19 +71,26 @@ function handleKeyDown(event: KeyboardEvent) {
<input
v-model="value"
:name="name"
:type="type"
:type="curType"
class="w-full border-none bg-hana-blue-50 py-2 pl-10 pr-3 text-sm focus:ring-2 focus:ring-hana-blue-400"
:class="shape === 'rounded' ? 'rounded-full' : 'rounded-lg'"
:placeholder="placeholder"
@keydown="handleKeyDown"
>
<!-- suffix slot or Icon -->
<span
v-if="suffixIcon || $slots.suffix"
v-if="suffixIcon || $slots.suffix || type === 'password'"
class="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500"
>
<slot name="suffix" />
<Icon v-if="suffixIcon" :name="suffixIcon" size="20" />
<Icon v-if="suffixIcon && type !== 'password'" :name="suffixIcon" size="20" />
<Icon
v-else-if="type === 'password'"
:name="showPassword ? 'lucide:eye' : 'lucide:eye-off'"
size="20"
class="cursor-pointer"
@click="toggleShowPassword"
/>
</span>
</div>
</template>
Expand Down
212 changes: 212 additions & 0 deletions components/main/Header/User.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
<script setup lang="ts">
import useDialog from '~/components/hana/Dialog/useDialog'
import useMessage from '~/components/hana/Message/useMessage'
import { useStore } from '~/store'
const { t } = useI18n()
const { userStore } = useStore()
const { callHanaMessage } = useMessage()
const { callHanaDialog } = useDialog()
const notLoggedInMap = [{
text: t('header.user.notLoggedIn.login'),
icon: 'lucide:log-in',
}, {
text: t('header.user.notLoggedIn.register'),
icon: 'lucide:user-plus',
}]
const loggedInMap = [{
text: t('header.user.loggedIn.profile'),
icon: 'lucide:user',
}, {
text: t('header.user.loggedIn.messages'),
icon: 'lucide:mail',
}, {
text: t('header.user.loggedIn.comments'),
icon: 'lucide:message-square-more',
}, {
text: t('header.user.loggedIn.logout'),
icon: 'lucide:log-out',
}]
const loginWindowVisible = ref(false)
const registerWindowVisible = ref(false)
function toggleLoginRegisterWindow() {
loginWindowVisible.value = !loginWindowVisible.value
registerWindowVisible.value = !registerWindowVisible.value
}
function handleUserCommand(command: string | number | object) {
switch (command) {
case t('header.user.notLoggedIn.login'):
loginWindowVisible.value = true
break
case t('header.user.notLoggedIn.register'):
registerWindowVisible.value = true
break
case t('header.user.loggedIn.logout'):
callHanaDialog({
title: '提示',
content: '确定要退出登录吗?',
showCancelButton: true,
onOk: () => {
localStorage.removeItem('token')
userStore.logout()
callHanaMessage({
message: '已退出登录。',
type: 'success',
})
},
})
break
default:
callHanaMessage({
message: '功能开发中...',
type: 'error',
})
break
}
}
async function handleLogin(e: Event) {
if (e.target) {
const formData = new FormData(e.target as HTMLFormElement)
const objData = Object.fromEntries(formData)
if (!objData.account || !objData.password) {
callHanaMessage({
message: '请输入用户名和密码。',
type: 'error',
})
return
}
const { data } = await useFetch('/api/auth/login', { method: 'POST', body: JSON.stringify(objData) })
if (data.value?.success) {
localStorage.setItem('token', data.value.payload!.token)
userStore.setUserInfo(data.value.payload!.userInfo)
loginWindowVisible.value = false
callHanaMessage({
message: `欢迎回来,${userStore.userInfo?.username}。`,
type: 'success',
})
}
else {
callHanaMessage({
message: data.value?.statusMessage || '登录失败。',
type: 'error',
})
}
}
}
async function handleRegister(e: Event) {
if (e.target) {
const formData = new FormData(e.target as HTMLFormElement)
const objData = Object.fromEntries(formData)
if (!objData.username || !objData.email || !objData.password) {
callHanaMessage({
message: '请填写用户名、邮箱和密码。',
type: 'error',
})
return
}
const { data } = await useFetch('/api/auth/register', { method: 'POST', body: JSON.stringify(objData) })
if (data.value?.success) {
loginWindowVisible.value = true
}
else {
const errorList = data.value?.payload?.map(item => item.message).join(', ')
callHanaMessage({
message: errorList || '注册失败。',
type: 'error',
})
}
}
}
async function handleSubmit(type: 'login' | 'register', e: Event) {
switch (type) {
case 'login':
handleLogin(e)
break
case 'register':
handleRegister(e)
break
}
}
</script>

<template>
<HanaDropdown v-if="!userStore.loggedIn" animation="slide" offset="end" :show-arrow="false" @command="handleUserCommand">
<HanaButton
icon-button
icon="lucide:user-round"
class="ml-auto"
/>
<template #dropdown>
<HanaDropdownMenu>
<HanaDropdownItem
v-for="item in notLoggedInMap"
:key="item.text"
:icon="item.icon"
:command="item.text"
>
{{ item.text }}
</HanaDropdownItem>
</HanaDropdownMenu>
</template>
</HanaDropdown>
<HanaDropdown v-if="userStore.loggedIn" animation="slide" offset="end" :show-arrow="false" @command="handleUserCommand">
<NuxtImg v-if="userStore.userInfo!.avatar" :src="userStore.userInfo!.avatar" class="size-8 cursor-pointer rounded-full" />
<div v-else class="flex size-8 cursor-pointer items-center justify-center rounded-full bg-hana-blue text-xl text-white">
<span>{{ userStore.userInfo!.username[0] }}</span>
</div>
<template #dropdown>
<HanaDropdownMenu>
<HanaDropdownItem
v-for="item in loggedInMap"
:key="item.text"
:icon="item.icon"
:command="item.text"
>
{{ item.text }}
</HanaDropdownItem>
</HanaDropdownMenu>
</template>
</HanaDropdown>
<HanaDialog v-model="loginWindowVisible" title="欢迎来到...花园。">
<form @submit.prevent="(e) => handleSubmit('login', e)">
<div class="flex flex-col gap-4">
<HanaInput name="account" prefix-icon="lucide:user-round" shape="rounded" placeholder="用户名 / 邮箱" />
<HanaInput name="password" prefix-icon="lucide:key-round" shape="rounded" type="password" placeholder="密码" />
</div>
<div class="mt-8 flex flex-col gap-4">
<HanaButton class="w-full" dark-mode type="submit">
<span>登录</span>
</HanaButton>
<HanaButton class="w-full" @click="toggleLoginRegisterWindow">
<span class="text-hana-blue">创建账户</span>
</HanaButton>
</div>
</form>
</HanaDialog>
<HanaDialog v-model="registerWindowVisible" title="这里有你想找的花吗?">
<form @submit.prevent="(e) => handleSubmit('register', e)">
<div class="flex flex-col gap-4">
<HanaInput name="username" prefix-icon="lucide:user-round" shape="rounded" placeholder="用户名" />
<HanaInput name="email" prefix-icon="lucide:mail" shape="rounded" placeholder="邮箱" />
<HanaInput name="site" prefix-icon="lucide:globe" shape="rounded" placeholder="站点(无可不填)" />
<HanaInput name="password" prefix-icon="lucide:key-round" shape="rounded" type="password" placeholder="密码" />
</div>
<div class="mt-8 flex flex-col gap-4">
<HanaButton class="w-full" dark-mode type="submit">
<span>注册</span>
</HanaButton>
<HanaButton class="w-full" @click="toggleLoginRegisterWindow">
<span class="text-hana-blue">已有帐号</span>
</HanaButton>
</div>
</form>
</HanaDialog>
</template>
Loading

0 comments on commit 191c591

Please sign in to comment.