feat: 忘记密码页面

This commit is contained in:
zhangyh
2025-10-22 14:03:57 +08:00
parent 2e2ea5dceb
commit e995295f98
7 changed files with 606 additions and 18 deletions

View 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>

View 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>

View 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">Weve 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>