This commit is contained in:
parent
be35ecb62a
commit
595d1d0932
@ -1,15 +1,40 @@
|
|||||||
<template>
|
<template>
|
||||||
<form @submit.prevent="handleLogin" class="flex flex-col gap-6 text-base text-black md:text-2xl">
|
<form @submit.prevent="handleLogin" class="flex flex-col gap-6 text-base text-black md:text-2xl">
|
||||||
<div class="overflow-hidden rounded-[20px] bg-[#78788029] px-4">
|
<div class="rounded-[20px] bg-[#78788029] px-4">
|
||||||
<div class="relative">
|
<div class="relative">
|
||||||
<Input
|
<Input
|
||||||
v-model="email"
|
v-model="email"
|
||||||
type="email"
|
type="text"
|
||||||
name="email"
|
name="user_email_identity"
|
||||||
autocomplete="email"
|
autocomplete="new-password"
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
class="h-[50px] border-none bg-transparent text-base focus-visible:ring-0 md:text-2xl"
|
class="h-[50px] border-none bg-transparent text-base focus-visible:ring-0 md:text-2xl"
|
||||||
|
@focus="isFocused = true"
|
||||||
|
@blur="onBlur"
|
||||||
|
@keydown="handleKeyDown"
|
||||||
/>
|
/>
|
||||||
|
<transition name="fade">
|
||||||
|
<div
|
||||||
|
v-if="isFocused && suggestList.length > 0"
|
||||||
|
class="absolute top-[55px] left-0 z-[100] w-full rounded-xl border border-white/20 bg-white/95 p-1 shadow-2xl backdrop-blur-xl dark:bg-black/90"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="(item, index) in suggestList"
|
||||||
|
:key="item.full"
|
||||||
|
@mousedown="selectSuggest(item.full)"
|
||||||
|
:class="[
|
||||||
|
'flex cursor-pointer items-center rounded-lg px-3 py-2 text-sm transition-colors md:text-xl',
|
||||||
|
activeIndex === index ? 'bg-[#A8FF53]/10' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="font-medium text-black/80">{{ prefix }}</span>
|
||||||
|
|
||||||
|
<span class="font-medium text-black">{{ item.userInputPart }}</span>
|
||||||
|
|
||||||
|
<span class="text-black opacity-30">{{ item.suggestPart }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
<div class="absolute bottom-0 left-0 h-[1px] w-full bg-gray-400/30"></div>
|
<div class="absolute bottom-0 left-0 h-[1px] w-full bg-gray-400/30"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -56,7 +81,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, watch } from 'vue'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
@ -68,21 +93,90 @@ const CodeSentTipRef = ref<InstanceType<typeof CodeSentTip> | null>(null)
|
|||||||
const email = ref('')
|
const email = ref('')
|
||||||
const code = ref('')
|
const code = ref('')
|
||||||
const countdown = ref(0)
|
const countdown = ref(0)
|
||||||
|
const isFocused = ref(false)
|
||||||
|
const commonSuffixes = ['@gmail.com', '@outlook.com', '@qq.com', '@163.com']
|
||||||
|
const activeIndex = ref(-1) // 记录当前键盘选中的索引
|
||||||
|
|
||||||
|
// 1. 逻辑:提取 @ 前的字符
|
||||||
|
const prefix = computed(() => {
|
||||||
|
const atIndex = email.value.indexOf('@')
|
||||||
|
return atIndex > -1 ? email.value.slice(0, atIndex) : email.value
|
||||||
|
})
|
||||||
|
|
||||||
|
// 输入变化时重置索引
|
||||||
|
watch(email, () => {
|
||||||
|
activeIndex.value = -1
|
||||||
|
})
|
||||||
|
// 键盘控制逻辑
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
if (!suggestList.value.length) return
|
||||||
|
|
||||||
|
if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
activeIndex.value = (activeIndex.value + 1) % suggestList.value.length
|
||||||
|
} else if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
activeIndex.value =
|
||||||
|
(activeIndex.value - 1 + suggestList.value.length) % suggestList.value.length
|
||||||
|
} else if (e.key === 'Enter' && activeIndex.value !== -1) {
|
||||||
|
e.preventDefault()
|
||||||
|
selectSuggest(suggestList.value[activeIndex.value])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 逻辑:生成建议列表
|
||||||
|
const suggestList = computed(() => {
|
||||||
|
const val = email.value.trim()
|
||||||
|
if (!val || val.length < 1) return []
|
||||||
|
|
||||||
|
const atIndex = val.indexOf('@')
|
||||||
|
// 用户目前输入的后缀部分(例如输入了 "abc@gm",则 domainPart 为 "@gm")
|
||||||
|
const domainPart = atIndex > -1 ? val.slice(atIndex) : ''
|
||||||
|
|
||||||
|
// 过滤匹配的后缀
|
||||||
|
const matches = commonSuffixes.filter((s) => s.startsWith(domainPart) && s !== domainPart)
|
||||||
|
|
||||||
|
return matches.map((full) => {
|
||||||
|
return {
|
||||||
|
full: full, // 完整后缀,用于点击填入
|
||||||
|
userInputPart: domainPart, // 用户已经输入的后缀部分(标色)
|
||||||
|
suggestPart: full.replace(domainPart, ''), // 还没输入的推荐部分(置灰)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
// 4. 处理选中
|
||||||
|
const selectSuggest = (fullSuffix: string) => {
|
||||||
|
email.value = prefix.value + fullSuffix
|
||||||
|
isFocused.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const onBlur = () => {
|
||||||
|
// 必须延迟,否则点击列表时会先触发 blur 导致列表消失无法选中
|
||||||
|
setTimeout(() => (isFocused.value = false), 200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 校验逻辑
|
||||||
|
const validateEmail = (str: string) => {
|
||||||
|
return /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(str)
|
||||||
|
}
|
||||||
const emit = defineEmits(['close'])
|
const emit = defineEmits(['close'])
|
||||||
const handleGetCode = () => {
|
const handleGetCode = () => {
|
||||||
if (!email.value) {
|
if (!email.value) {
|
||||||
toast('请输入邮箱')
|
toast('请输入邮箱')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
CodeSentTipRef.value?.show()
|
|
||||||
|
// 发送前强制校验格式
|
||||||
|
if (!validateEmail(email.value)) {
|
||||||
|
return toast.error('邮箱格式无效,请检查')
|
||||||
|
}
|
||||||
|
|
||||||
request
|
request
|
||||||
.get<{ exist: boolean }>('/api/v1/auth/check', {
|
.get<{ exist: boolean }>('/api/v1/auth/check', {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
})
|
})
|
||||||
.then(({ exist }) => {
|
.then(({ exist }) => {
|
||||||
console.log(exist)
|
|
||||||
request
|
request
|
||||||
.post('/api/v1/common/send_code', {
|
.post('/api/v1/common/send_code', {
|
||||||
// 1=登录, 2=注册,
|
// 1=登录, 2=注册,
|
||||||
@ -90,6 +184,7 @@ const handleGetCode = () => {
|
|||||||
type: exist ? 2 : 1,
|
type: exist ? 2 : 1,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
CodeSentTipRef.value?.show()
|
||||||
countdown.value = 60
|
countdown.value = 60
|
||||||
const timer = setInterval(() => {
|
const timer = setInterval(() => {
|
||||||
countdown.value--
|
countdown.value--
|
||||||
@ -100,6 +195,13 @@ const handleGetCode = () => {
|
|||||||
}
|
}
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
|
if (!code.value) {
|
||||||
|
toast('请输入验证码')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!validateEmail(email.value)) {
|
||||||
|
return toast.error('邮箱格式无效,请检查')
|
||||||
|
}
|
||||||
request
|
request
|
||||||
.post<any, { token: string }>('/api/v1/auth/login/email', {
|
.post<any, { token: string }>('/api/v1/auth/login/email', {
|
||||||
email: email.value,
|
email: email.value,
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
<button
|
<button
|
||||||
v-else
|
v-else
|
||||||
@click="openLoginModal"
|
@click="openLoginModal"
|
||||||
class="flex h-[30px] cursor-pointer items-center justify-center rounded-full bg-[#78788029] px-6 text-sm font-bold backdrop-blur-md transition hover:brightness-110 md:h-[60px] md:w-[220px] md:text-2xl"
|
class="flex h-[30px] cursor-pointer items-center justify-center rounded-full bg-[#78788029] px-6 text-sm font-bold backdrop-blur-md transition hover:brightness-110 md:h-[40px] md:w-[220px] md:text-xl"
|
||||||
>
|
>
|
||||||
登录 / 注册
|
登录 / 注册
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user