new feature: capture
This commit is contained in:
parent
b380d37d8f
commit
48a613233f
|
@ -1,5 +1,9 @@
|
|||
<!-- App -->
|
||||
<script setup lang="ts">
|
||||
// 导入通知和提示组件
|
||||
import Alert from '@/components/KUNGalgameAlert/Alert.vue'
|
||||
import Info from '@/components/KUNGalgameAlert/Info.vue'
|
||||
|
||||
import { onBeforeMount } from 'vue'
|
||||
// 导入设置面板 store
|
||||
import { useKUNGalgameSettingsStore } from '@/store/modules/settings'
|
||||
|
@ -21,6 +25,11 @@ onBeforeMount(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 全局警告组件 -->
|
||||
<Alert />
|
||||
|
||||
<!-- 全局通知组件 -->
|
||||
<Info />
|
||||
<RouterView />
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,15 +2,12 @@
|
|||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
// Vue 中不允许在 template 中用 '<' 这个符号,换成 msg
|
||||
const msg = `< `
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 返回主页 -->
|
||||
<div class="return" @click="router.back()">
|
||||
<span>{{ msg + $t('back.back') }}</span>
|
||||
<span>{{ `< ${$t('back.back')}` }}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -23,7 +20,8 @@ const msg = `< `
|
|||
color: var(--kungalgame-font-color-0);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.return:hover {
|
||||
color: var(--kungalgame-blue-4)
|
||||
color: var(--kungalgame-blue-4);
|
||||
}
|
||||
</style>
|
||||
|
|
193
src/components/capture/Capture.vue
Normal file
193
src/components/capture/Capture.vue
Normal file
|
@ -0,0 +1,193 @@
|
|||
<template>
|
||||
<Teleport to="body" :disabled="props.isShowValidate">
|
||||
<Transition name="capture">
|
||||
<div class="mask" v-if="props.isShowValidate">
|
||||
<div class="validate">
|
||||
<div class="text">
|
||||
<div
|
||||
class="character"
|
||||
v-for="(char, index) in characters"
|
||||
:key="index"
|
||||
:style="char.style"
|
||||
@click="handleCharacterClick(char)"
|
||||
>
|
||||
<text>{{ char.value }}</text>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msg">
|
||||
<div v-if="isVerified" class="pass">验证通过</div>
|
||||
<div v-else-if="errorCount > 0" class="error">
|
||||
点击错误,请重新点击
|
||||
</div>
|
||||
<div class="hint" v-if="!isVerified">
|
||||
请按顺序点击以下字符:
|
||||
{{ characters.map((char) => char.value).join(' ') }}
|
||||
</div>
|
||||
<div class="refresh" @click="resetCharacters">点击刷新</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted } from 'vue'
|
||||
|
||||
const containerRef = ref<HTMLDivElement | null>(null)
|
||||
const characters = ref(generateRandomCharacters())
|
||||
const clickedCharacters = ref('')
|
||||
const isVerified = ref(false)
|
||||
const errorCount = ref(0)
|
||||
|
||||
const props = defineProps(['isShowValidate'])
|
||||
const emits = defineEmits(['validate'])
|
||||
|
||||
interface Character {
|
||||
value: string
|
||||
style: {
|
||||
top: string
|
||||
left: string
|
||||
}
|
||||
}
|
||||
|
||||
emits('validate', false)
|
||||
|
||||
// 获得随机 26 个字符
|
||||
function generateRandomCharacters(): Character[] {
|
||||
const chars = '小只可爱软萌鲲'
|
||||
const randomChars: Character[] = []
|
||||
|
||||
// 创建一个 Set 集合用于去重
|
||||
const charSet = new Set<string>()
|
||||
|
||||
while (charSet.size < 3) {
|
||||
const randomIndex = Math.floor(Math.random() * chars.length)
|
||||
const char = chars[randomIndex]
|
||||
|
||||
// 如果字符不在集合中,则添加到集合和结果数组中
|
||||
if (!charSet.has(char)) {
|
||||
charSet.add(char)
|
||||
const character: Character = {
|
||||
value: char,
|
||||
style: {
|
||||
top: `${getRandomCoordinate()}px`,
|
||||
left: `${getRandomCoordinate()}px`,
|
||||
},
|
||||
}
|
||||
randomChars.push(character)
|
||||
}
|
||||
}
|
||||
|
||||
return randomChars
|
||||
}
|
||||
|
||||
function getRandomCoordinate(): number {
|
||||
const minCoordinate = 50
|
||||
const maxCoordinate = 150
|
||||
return (
|
||||
Math.floor(Math.random() * (maxCoordinate - minCoordinate + 1)) +
|
||||
minCoordinate
|
||||
)
|
||||
}
|
||||
|
||||
const handleCharacterClick = (char: Character): Promise<boolean> => {
|
||||
return new Promise<boolean>((resolve, reject) => {
|
||||
clickedCharacters.value += char.value
|
||||
if (
|
||||
clickedCharacters.value.length === 3 &&
|
||||
clickedCharacters.value === characters.value.map((c) => c.value).join('')
|
||||
) {
|
||||
isVerified.value = true
|
||||
emits('validate', true)
|
||||
} else if (clickedCharacters.value.length >= 3) {
|
||||
errorCount.value += 1
|
||||
resetCharacters()
|
||||
emits('validate', false)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
function resetCharacters() {
|
||||
characters.value = generateRandomCharacters()
|
||||
clickedCharacters.value = ''
|
||||
isVerified.value = false
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
const container = containerRef.value
|
||||
if (container) {
|
||||
container.style.width = '300px'
|
||||
container.style.height = '300px'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.mask {
|
||||
position: fixed;
|
||||
z-index: 9999;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
transition: opacity 0.3s ease;
|
||||
color: var(--kungalgame-font-color-3);
|
||||
}
|
||||
|
||||
.validate {
|
||||
height: 100%;
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
margin: auto;
|
||||
padding: 20px 30px;
|
||||
background-color: var(--kungalgame-trans-white-2);
|
||||
border: 1px solid var(--kungalgame-blue-1);
|
||||
border-radius: 2px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33);
|
||||
transition: all 0.3s ease;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
font-size: 30px;
|
||||
transform: translateX(-30px);
|
||||
}
|
||||
|
||||
.character {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.refresh {
|
||||
width: 64px;
|
||||
color: var(--kungalgame-blue-4);
|
||||
margin-top: 20px;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid var(--kungalgame-trans-white-9);
|
||||
&:hover {
|
||||
border-bottom: 2px solid var(--kungalgame-blue-4);
|
||||
}
|
||||
}
|
||||
|
||||
.msg {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.capture-enter-from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.capture-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.capture-enter-from .validate,
|
||||
.capture-leave-to .validate {
|
||||
-webkit-transform: scale(1.1);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
</style>
|
|
@ -1,8 +1,5 @@
|
|||
<!-- 先放一个 Layout 在这里,后面应该用得到 -->
|
||||
<script setup lang="ts">
|
||||
// 导入通知和提示组件
|
||||
import Alert from '@/components/KUNGalgameAlert/Alert.vue'
|
||||
import Info from '@/components/KUNGalgameAlert/Info.vue'
|
||||
|
||||
// 导入动画
|
||||
import 'animate.css'
|
||||
|
||||
|
@ -12,12 +9,6 @@ import { currBackground } from '@/hooks/useBackgroundPicture'
|
|||
<template>
|
||||
<!-- #default 是 v-slot 的简写,route 就是路由,Component 是一个 v-node -->
|
||||
<div class="app" :style="{ backgroundImage: currBackground }">
|
||||
<!-- 全局警告组件 -->
|
||||
<Alert />
|
||||
|
||||
<!-- 全局通知组件 -->
|
||||
<Info />
|
||||
|
||||
<!-- <RouterView /> -->
|
||||
<RouterView #default="{ route, Component }">
|
||||
<transition
|
||||
|
|
|
@ -23,8 +23,6 @@ export const useKUNGalgamerStore = defineStore({
|
|||
this.token = token
|
||||
},
|
||||
login(loginData: LoginData): Promise<LoginResponseData> {
|
||||
console.log(1)
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
postLoginDataApi({
|
||||
username: loginData.username,
|
||||
|
|
30
src/utils/validate.ts
Normal file
30
src/utils/validate.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
// 正则表达式匹配合法 URL
|
||||
export const isValidURL = (url: string) => {
|
||||
const regex =
|
||||
/^(https?|http):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
|
||||
return regex.test(url)
|
||||
}
|
||||
|
||||
// 正则表达式匹配合法邮箱
|
||||
export const validateEmail = (email: string) => {
|
||||
const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
||||
return regex.test(email)
|
||||
}
|
||||
|
||||
// 匹配 1 到 17 位数 中文、英文、数字、下划线、~ 的用户名
|
||||
export const isValidName = (name: string) => {
|
||||
const regex = /^[\u4e00-\u9fa5a-zA-Z0-9_~]{1,17}$/
|
||||
return regex.test(name)
|
||||
}
|
||||
|
||||
// 正则表达式匹配 6 到 17 位数的密码,必须包含至少一个英文字符和一个数字,可以选择性的包含 \w!@#$%^&*()-+= 特殊字符
|
||||
export const isValidPassword = (pwd: string) => {
|
||||
const regex = /^(?=.*[a-zA-Z])(?=.*[0-9])[\w!@#$%^&*()-+=]{6,17}$/
|
||||
return regex.test(pwd)
|
||||
}
|
||||
|
||||
// 正则表达式匹配 7 位英文或数字,邮箱验证码
|
||||
export const isValidMailConfirmCode = (code: string) => {
|
||||
const regex = /^[a-zA-Z0-9]{7}$/
|
||||
return regex.test(code)
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, reactive } from 'vue'
|
||||
import { ref, reactive, onMounted, watch } from 'vue'
|
||||
|
||||
import KUNGalgameFooter from '@/components/KUNGalgameFooter.vue'
|
||||
|
||||
|
@ -144,7 +144,6 @@ const handleClickRegister = () => {
|
|||
top: 0;
|
||||
transition: transform 0.6s ease-in-out;
|
||||
width: 50%;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.overlay {
|
||||
|
|
|
@ -5,6 +5,9 @@ import { useRouter } from 'vue-router'
|
|||
// 使用全局通知
|
||||
import { useKUNGalgameMessageStore } from '@/store/modules/message'
|
||||
|
||||
// 导入人机验证组件
|
||||
import Capture from '@/components/capture/Capture.vue'
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const info = useKUNGalgameMessageStore()
|
||||
|
@ -24,11 +27,32 @@ const handleLogin = () => {
|
|||
}
|
||||
})
|
||||
}
|
||||
|
||||
// 定义标志位
|
||||
const isFirstLoad = ref(false)
|
||||
// 定义人机验证的标志
|
||||
const isShowValidate = ref(false)
|
||||
|
||||
const handleVerify = async (result: boolean) => {
|
||||
// 跳过首次加载页面
|
||||
if (isFirstLoad.value) {
|
||||
// 处理人机校验完成后的逻辑
|
||||
isShowValidate.value = false
|
||||
info.info('验证通过')
|
||||
} else {
|
||||
isFirstLoad.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const capture = () => {
|
||||
isShowValidate.value = true
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<!-- 登陆 -->
|
||||
<div class="login">
|
||||
<Capture @validate="handleVerify" :isShowValidate="isShowValidate" />
|
||||
<form class="form" @submit.prevent="handleLogin">
|
||||
<h2 class="title">登陆</h2>
|
||||
<input
|
||||
|
@ -44,6 +68,7 @@ const handleLogin = () => {
|
|||
class="input"
|
||||
/>
|
||||
<span class="forget">忘记密码? 点击发送重置邮件</span>
|
||||
<span @click="capture" class="capture">点击进行人机身份验证</span>
|
||||
<button class="btn" type="submit">登陆</button>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -103,6 +128,12 @@ const handleLogin = () => {
|
|||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.capture {
|
||||
color: var(--kungalgame-font-color-2);
|
||||
cursor: pointer;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
position: absolute;
|
||||
bottom: 7%;
|
||||
|
|
Loading…
Reference in a new issue