feat: 忘记密码页面
This commit is contained in:
@@ -21,6 +21,11 @@ const router = createRouter({
|
|||||||
name: 'LoginPage',
|
name: 'LoginPage',
|
||||||
component: () => import('@/views/login/LoginPage.vue')
|
component: () => import('@/views/login/LoginPage.vue')
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path:'/reset',
|
||||||
|
name:'ResetPage',
|
||||||
|
component: () => import('@/views/login/ResetPage.vue')
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/signup',
|
path: '/signup',
|
||||||
name: 'SignupPage',
|
name: 'SignupPage',
|
||||||
|
|||||||
@@ -52,7 +52,6 @@ interface NoticeItemProps {
|
|||||||
const props = defineProps<NoticeItemProps>()
|
const props = defineProps<NoticeItemProps>()
|
||||||
|
|
||||||
const isMyself = computed(()=>{
|
const isMyself = computed(()=>{
|
||||||
console.log('isMyself', props)
|
|
||||||
return props.value.userId === '1'
|
return props.value.userId === '1'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,10 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="login-page">
|
<div class="login-page">
|
||||||
<!-- 主要内容区域 -->
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
<!-- 返回按钮 -->
|
|
||||||
<div class="back-button" @click="goBack">
|
<div class="back-button" @click="goBack">
|
||||||
<img src="@/assets/images/arrow_left.png" class="back-icon" />
|
<img src="@/assets/images/arrow_left.png" class="back-icon" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 标题区域 -->
|
|
||||||
<div class="header">
|
<div class="header">
|
||||||
<div class="title">Log in.</div>
|
<div class="title">Log in.</div>
|
||||||
<p class="subtitle">Redefine the styling experience with AI.</p>
|
<p class="subtitle">Redefine the styling experience with AI.</p>
|
||||||
@@ -15,14 +12,9 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="login-container">
|
<div class="login-container">
|
||||||
<form @submit.prevent="handleLogin" class="login-form" novalidate>
|
<div class="login-form">
|
||||||
<div class="input-group">
|
<div class="input-group">
|
||||||
<input
|
<input type="email" v-model="formData.email" placeholder="Email" class="input-field" />
|
||||||
type="email"
|
|
||||||
v-model="formData.email"
|
|
||||||
placeholder="Email"
|
|
||||||
class="input-field"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group pwd">
|
<div class="input-group pwd">
|
||||||
<input
|
<input
|
||||||
@@ -33,17 +25,15 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 登录按钮 -->
|
<div class="login-button" @click="handleLogin">Log in</div>
|
||||||
<button type="submit" class="login-button">Log in</button>
|
|
||||||
<div class="forgot-password" @click="handleForgotPassword">Forgot password?</div>
|
<div class="forgot-password" @click="handleForgotPassword">Forgot password?</div>
|
||||||
|
|
||||||
<!-- Google登录按钮 -->
|
|
||||||
<div type="button" class="google-button" @click="handleGoogleLogin">
|
<div type="button" class="google-button" @click="handleGoogleLogin">
|
||||||
<img :src="google" class="google-icon" />
|
<img :src="google" class="google-icon" />
|
||||||
Sign in with Google
|
Sign in with Google
|
||||||
</div>
|
</div>
|
||||||
<div class="sign-up-button" @click="handleSignup">Don’t have an account? Sign Up</div>
|
<div class="sign-up-button" @click="handleSignup">Don’t have an account? Sign Up</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -139,10 +129,8 @@ const handleLogin = async () => {
|
|||||||
|
|
||||||
// 处理忘记密码
|
// 处理忘记密码
|
||||||
const handleForgotPassword = () => {
|
const handleForgotPassword = () => {
|
||||||
showToast('忘记密码功能开发中...')
|
|
||||||
console.log('11111111111')
|
|
||||||
// 这里可以跳转到忘记密码页面
|
// 这里可以跳转到忘记密码页面
|
||||||
// router.push('/forgot-password')
|
router.push('/reset')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理Google登录
|
// 处理Google登录
|
||||||
@@ -291,6 +279,8 @@ const handleSignup = () => {
|
|||||||
border-radius: 7rem;
|
border-radius: 7rem;
|
||||||
font-size: 4rem;
|
font-size: 4rem;
|
||||||
margin-bottom: 1.67rem;
|
margin-bottom: 1.67rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 10rem;
|
||||||
}
|
}
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
font-family: 'satoshiRegular';
|
font-family: 'satoshiRegular';
|
||||||
|
|||||||
168
src/views/login/ResetPage.vue
Normal file
168
src/views/login/ResetPage.vue
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
<template>
|
||||||
|
<div class="login-page">
|
||||||
|
<!-- 主要内容区域 -->
|
||||||
|
<div class="content">
|
||||||
|
<!-- 返回按钮 -->
|
||||||
|
<div class="back-button" @click="goBack">
|
||||||
|
<img src="@/assets/images/arrow_left.png" class="back-icon" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 标题区域 -->
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Log in.</div>
|
||||||
|
<p class="subtitle">Redefine the styling experience with AI.</p>
|
||||||
|
<p class="subtitle">Use Styling Angel to speed up your fashion journey.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="login-container">
|
||||||
|
<div class="login-form">
|
||||||
|
<Mail v-if="step === 'mail'" @nextStep="mailData => handleStep('verify', mailData)" />
|
||||||
|
<Verify
|
||||||
|
:email="email"
|
||||||
|
v-else-if="step === 'verify'"
|
||||||
|
:ct="emailCode"
|
||||||
|
@nextStep="handleStep('password')"
|
||||||
|
/>
|
||||||
|
<Password v-else-if="step === 'password'" @sucess="handleSuccess"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>Powered by AiDLab for Lane Crawford</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, reactive, computed } from 'vue'
|
||||||
|
import { useRouter } from 'vue-router'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
import Mail from './components/Mail.vue'
|
||||||
|
import Verify from './components/Verify.vue'
|
||||||
|
import Password from './components/Password.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const step = ref<'mail' | 'verify' | 'password'>('mail')
|
||||||
|
const emailCode = ref(['', '', '', '', ''])
|
||||||
|
const email = ref('')
|
||||||
|
|
||||||
|
// 加载状态
|
||||||
|
const isLoading = ref(false)
|
||||||
|
|
||||||
|
// 返回上一页
|
||||||
|
const goBack = () => {
|
||||||
|
router.go(-1)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleStep = (type: 'mail' | 'verify' | 'password', data?: any) => {
|
||||||
|
if (step.value === 'mail') {
|
||||||
|
email.value = data.email
|
||||||
|
}
|
||||||
|
step.value = type
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSuccess = () => {
|
||||||
|
router.push('/login')
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.login-page {
|
||||||
|
position: relative;
|
||||||
|
color: #fff;
|
||||||
|
width: 100%;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-image: url('@/assets/images/login_bg.png');
|
||||||
|
background-size: cover;
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
padding-top: 12rem;
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
}
|
||||||
|
|
||||||
|
.back-button {
|
||||||
|
// position: absolute;
|
||||||
|
// top: 2rem;
|
||||||
|
// left: 2rem;
|
||||||
|
margin-top: 2.4rem;
|
||||||
|
margin-left: 6.1rem;
|
||||||
|
width: 2rem;
|
||||||
|
height: 3.4rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 3;
|
||||||
|
font-size: 3.4rem;
|
||||||
|
.back-icon {
|
||||||
|
width: 2.83rem;
|
||||||
|
height: 3.47rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
margin-top: 1.42rem;
|
||||||
|
padding-left: 15.5rem;
|
||||||
|
color: white;
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
.title {
|
||||||
|
font-size: 11rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
color: white;
|
||||||
|
font-family: 'satoshiBold';
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: white;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 141%;
|
||||||
|
letter-spacing: 0.08rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
position: relative;
|
||||||
|
// z-index: 2;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.login-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 7.2rem;
|
||||||
|
.login-form {
|
||||||
|
position: relative;
|
||||||
|
width: calc(100% - 28.4rem);
|
||||||
|
height: 107.8rem;
|
||||||
|
background: radial-gradient(
|
||||||
|
100% 100% at 0% 0%,
|
||||||
|
rgba(115, 115, 115, 0.4) 0%,
|
||||||
|
rgba(0, 0, 0, 0) 100%
|
||||||
|
);
|
||||||
|
backdrop-filter: blur(35px);
|
||||||
|
border: 2px solid rgba(255, 255, 255, 0.15);
|
||||||
|
border-radius: 4.79rem;
|
||||||
|
padding: 6.8rem 5.9rem 6.2rem 7.18rem;
|
||||||
|
box-shadow: 0 0.8rem 3.2rem rgba(0, 0, 0, 0.1);
|
||||||
|
overflow: hidden;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
font-size: 3.83rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: relative;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
font-size: 3rem;
|
||||||
|
margin-bottom: 15.5rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
61
src/views/login/components/Mail.vue
Normal file
61
src/views/login/components/Mail.vue
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<template>
|
||||||
|
<div class="mail-container">
|
||||||
|
<div class="label">Your Email</div>
|
||||||
|
<div class="input-group">
|
||||||
|
<input type="email" v-model="formData.email" placeholder="Email" class="input-field" />
|
||||||
|
</div>
|
||||||
|
<div class="btn" @click="handleNext">Next</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['nextStep'])
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
email: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
emit('nextStep', { email: formData.value.email })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.mail-container {
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
.label {
|
||||||
|
font-size: 4.8rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
margin-bottom: 6rem;
|
||||||
|
}
|
||||||
|
.input-field {
|
||||||
|
width: 100%;
|
||||||
|
height: 10rem;
|
||||||
|
padding: 2.4rem 5.5rem;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 7.1rem;
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
font-size: 3.83rem;
|
||||||
|
padding: 0 5.5rem;
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 3.83rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
margin-top: 7.6rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 10rem;
|
||||||
|
background: #000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1.67rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
72
src/views/login/components/Password.vue
Normal file
72
src/views/login/components/Password.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="pwd-container">
|
||||||
|
<div class="label">Reset Password</div>
|
||||||
|
<div class="input-group flex flex-align-center">
|
||||||
|
<input
|
||||||
|
:type="showPwd ? 'text' : 'password'"
|
||||||
|
v-model="formData.password"
|
||||||
|
placeholder="Enter your new password"
|
||||||
|
class="input-field flex-1"
|
||||||
|
/>
|
||||||
|
<van-icon v-if="showPwd" name="eye-o" @click="showPwd = !showPwd" />
|
||||||
|
<van-icon v-else name="closed-eye" @click="showPwd = !showPwd" />
|
||||||
|
</div>
|
||||||
|
<div class="btn" @click="handleNext">Submit</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const emit = defineEmits(['sucess'])
|
||||||
|
const showPwd = ref(false)
|
||||||
|
|
||||||
|
const formData = ref({
|
||||||
|
password: ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNext = () => {
|
||||||
|
emit('sucess')
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<style lang="less" scoped>
|
||||||
|
.pwd-container {
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
.label {
|
||||||
|
font-size: 4.8rem;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
margin-bottom: 6rem;
|
||||||
|
}
|
||||||
|
.input-group {
|
||||||
|
width: 100%;
|
||||||
|
height: 10rem;
|
||||||
|
padding: 0 2.2rem 0 5.5rem;
|
||||||
|
border: 2px solid #fff;
|
||||||
|
border-radius: 7.1rem;
|
||||||
|
}
|
||||||
|
.input-field {
|
||||||
|
background: transparent;
|
||||||
|
color: white;
|
||||||
|
outline: none;
|
||||||
|
border: none;
|
||||||
|
font-size: 3.83rem;
|
||||||
|
// padding: 0 5.5rem;
|
||||||
|
&::placeholder {
|
||||||
|
color: rgba(255, 255, 255, 0.6);
|
||||||
|
font-size: 3.83rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
margin-top: 10.6rem;
|
||||||
|
width: 100%;
|
||||||
|
height: 10rem;
|
||||||
|
background: #000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1.67rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 10rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
293
src/views/login/components/Verify.vue
Normal file
293
src/views/login/components/Verify.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<template>
|
||||||
|
<div class="verify-title">
|
||||||
|
Enter Verification Code
|
||||||
|
<div class="countdown" @click="handleResend">
|
||||||
|
{{ countdown <= 0 ? 'Resend' : countdown + 's' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="verify-subtitle">We’ve sent an code to your email {{ email }}</div>
|
||||||
|
<div class="captcha">
|
||||||
|
<input
|
||||||
|
v-for="(c, index) in getCtData"
|
||||||
|
:key="index"
|
||||||
|
type="text"
|
||||||
|
v-model="getCtData[index]"
|
||||||
|
:ref="(el) => setInputRef(el, index)"
|
||||||
|
inputmode="numeric"
|
||||||
|
pattern="[0-9]*"
|
||||||
|
@input="(e) => onInput(e.target.value, index)"
|
||||||
|
@keydown="(e) => onKeydown(e, index)"
|
||||||
|
@keypress="(e) => onKeypress(e)"
|
||||||
|
@focus="onFocus"
|
||||||
|
@pause="onPause"
|
||||||
|
:disabled="loading"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="policy">
|
||||||
|
<van-checkbox v-model="agreePolicy" shape="square">
|
||||||
|
I agree to all Terms, Privacy Policy, and Fees.
|
||||||
|
</van-checkbox>
|
||||||
|
</div>
|
||||||
|
<div class="btn" @click="handleConfirmCaptcha">Confirm</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed, watch, onMounted, nextTick } from 'vue'
|
||||||
|
import { showToast } from 'vant'
|
||||||
|
|
||||||
|
// Props
|
||||||
|
const props = defineProps({
|
||||||
|
ct: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
email: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const agreePolicy = ref(false)
|
||||||
|
// Emits
|
||||||
|
const emit = defineEmits(['nextStep'])
|
||||||
|
|
||||||
|
// Reactive data
|
||||||
|
const loading = ref(false)
|
||||||
|
const timeout = ref(null)
|
||||||
|
const inputRefs = ref([])
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
const getCtData = computed(() => props.ct)
|
||||||
|
const ctSize = computed(() => getCtData.value.length)
|
||||||
|
const cIndex = computed(() => {
|
||||||
|
let i = getCtData.value.findIndex((item) => item === '')
|
||||||
|
i = (i + ctSize.value) % ctSize.value
|
||||||
|
return i
|
||||||
|
})
|
||||||
|
const lastCode = computed(() => getCtData.value[ctSize.value - 1])
|
||||||
|
|
||||||
|
const countdown = ref(60)
|
||||||
|
const handleCountdown = () => {
|
||||||
|
const timer = setInterval(() => {
|
||||||
|
countdown.value--
|
||||||
|
if (countdown.value <= 0) {
|
||||||
|
clearInterval(timer)
|
||||||
|
}
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendVerifyCode = () => {
|
||||||
|
handleCountdown()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleResend = () => {
|
||||||
|
if (countdown.value > 0) return
|
||||||
|
countdown.value = 60
|
||||||
|
handleSendVerifyCode()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirmCaptcha = () => {
|
||||||
|
let password = getCtData.value.map((item) => item).join('')
|
||||||
|
// 验证验证码
|
||||||
|
if (!agreePolicy.value) {
|
||||||
|
showToast('please agree to all terms, privacy policy, and fees.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (password.length !== 5) {
|
||||||
|
showToast('please enter the correct verification code.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 验证验证码
|
||||||
|
new Promise((resolve, reject) => {
|
||||||
|
console.log('pwd', password)
|
||||||
|
resolve(true)
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
// showToast('verification code is correct.')
|
||||||
|
emit('nextStep', 'password')
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showToast('verification code is incorrect.')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
const setInputRef = (el, index) => {
|
||||||
|
if (el) {
|
||||||
|
inputRefs.value[index] = el
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onInput = (val, index) => {
|
||||||
|
clearTimeout(timeout.value)
|
||||||
|
timeout.value = setTimeout(() => {
|
||||||
|
// val = val.replace(/[^0-9]/g, '');
|
||||||
|
val = String(val).replace(/\D/g, '')
|
||||||
|
getCtData.value[index] = val
|
||||||
|
if (index == ctSize.value - 1) {
|
||||||
|
getCtData.value[ctSize.value - 1] = val[0] // 最后一个码,只允许输入一个字符。
|
||||||
|
} else if (val.length > 1) {
|
||||||
|
let i = index
|
||||||
|
for (i = index; i < ctSize.value && i - index < val.length; i++) {
|
||||||
|
getCtData.value[i] = val[i]
|
||||||
|
}
|
||||||
|
resetCaret()
|
||||||
|
} else if (!(val + '')) {
|
||||||
|
getCtData.value[index] = ''
|
||||||
|
}
|
||||||
|
}, 10)
|
||||||
|
}
|
||||||
|
|
||||||
|
const onPause = () => {}
|
||||||
|
|
||||||
|
// 重置光标位置。
|
||||||
|
const resetCaret = () => {
|
||||||
|
nextTick(() => {
|
||||||
|
inputRefs.value[ctSize.value - 1]?.focus()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const onFocus = () => {
|
||||||
|
// 监听 focus 事件,将光标重定位到"第一个空白符的位置"。
|
||||||
|
let index = getCtData.value.findIndex((item) => item === '')
|
||||||
|
index = (index + ctSize.value) % ctSize.value
|
||||||
|
inputRefs.value[index]?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeypress = (e) => {
|
||||||
|
// 只允许输入数字0-9
|
||||||
|
const char = String.fromCharCode(e.which)
|
||||||
|
if (!/[0-9]/.test(char)) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const onKeydown = (e, index) => {
|
||||||
|
// 处理删除键
|
||||||
|
if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||||
|
const val = e.target.value
|
||||||
|
if (val === '') {
|
||||||
|
// 删除上一个input里的值,并对其focus。
|
||||||
|
if (index > 0) {
|
||||||
|
getCtData.value[index - 1] = ''
|
||||||
|
inputRefs.value[index - 1]?.focus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 阻止其他非数字字符
|
||||||
|
else if (
|
||||||
|
e.key &&
|
||||||
|
!/[0-9]/.test(e.key) &&
|
||||||
|
![
|
||||||
|
'Backspace',
|
||||||
|
'Delete',
|
||||||
|
'Tab',
|
||||||
|
'Enter',
|
||||||
|
'ArrowLeft',
|
||||||
|
'ArrowRight',
|
||||||
|
'ArrowUp',
|
||||||
|
'ArrowDown'
|
||||||
|
].includes(e.key)
|
||||||
|
) {
|
||||||
|
e.preventDefault()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// const sendCaptcha = () => {
|
||||||
|
// let password = getCtData.value.map((item) => item).join('')
|
||||||
|
// emit('sendCaptcha', password)
|
||||||
|
// }
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
// 重置。一般是验证码错误时触发。
|
||||||
|
getCtData.value = getCtData.value.map((item) => '')
|
||||||
|
resetCaret()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Watchers
|
||||||
|
watch(cIndex, () => {
|
||||||
|
resetCaret()
|
||||||
|
})
|
||||||
|
|
||||||
|
// watch(lastCode, (newVal, oldVal) => {
|
||||||
|
// if (newVal && newVal != oldVal) {
|
||||||
|
// inputRefs.value[ctSize.value - 1]?.blur()
|
||||||
|
// sendCaptcha()
|
||||||
|
// }
|
||||||
|
// })
|
||||||
|
|
||||||
|
// Lifecycle
|
||||||
|
onMounted(() => {
|
||||||
|
resetCaret()
|
||||||
|
handleSendVerifyCode()
|
||||||
|
})
|
||||||
|
|
||||||
|
// Expose methods for parent component
|
||||||
|
defineExpose({
|
||||||
|
reset
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="less">
|
||||||
|
.captcha {
|
||||||
|
display: flex;
|
||||||
|
column-gap: 3rem;
|
||||||
|
}
|
||||||
|
input {
|
||||||
|
width: 11rem;
|
||||||
|
height: 12.6rem;
|
||||||
|
border: 0.2rem solid #fff;
|
||||||
|
border-radius: 2rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 4.8rem;
|
||||||
|
line-height: 12.6rem;
|
||||||
|
outline: none;
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.msg {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-title {
|
||||||
|
font-size: 4.8rem;
|
||||||
|
position: relative;
|
||||||
|
.countdown {
|
||||||
|
position: absolute;
|
||||||
|
right: 0;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(0, -50%);
|
||||||
|
font-size: 2.6rem;
|
||||||
|
color: #fff;
|
||||||
|
font-family: 'satoshiRegular';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.verify-subtitle {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
margin-bottom: 6.4rem;
|
||||||
|
}
|
||||||
|
.policy {
|
||||||
|
margin-top: 5.95rem;
|
||||||
|
:deep(.van-checkbox__icon) {
|
||||||
|
font-size: 3rem;
|
||||||
|
}
|
||||||
|
:deep(.van-checkbox__label) {
|
||||||
|
font-size: 2.4rem;
|
||||||
|
color: #fff;
|
||||||
|
margin-left: 1.68rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
height: 10rem;
|
||||||
|
background: #000;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 7rem;
|
||||||
|
font-size: 4rem;
|
||||||
|
margin-bottom: 1.67rem;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 10rem;
|
||||||
|
margin-top: 8.8rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user