feat: 忘记密码页面
This commit is contained in:
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