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

@@ -21,6 +21,11 @@ const router = createRouter({
name: 'LoginPage',
component: () => import('@/views/login/LoginPage.vue')
},
{
path:'/reset',
name:'ResetPage',
component: () => import('@/views/login/ResetPage.vue')
},
{
path: '/signup',
name: 'SignupPage',

View File

@@ -52,7 +52,6 @@ interface NoticeItemProps {
const props = defineProps<NoticeItemProps>()
const isMyself = computed(()=>{
console.log('isMyself', props)
return props.value.userId === '1'
})

View File

@@ -1,13 +1,10 @@
<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>
@@ -15,14 +12,9 @@
</div>
<div class="login-container">
<form @submit.prevent="handleLogin" class="login-form" novalidate>
<div class="login-form">
<div class="input-group">
<input
type="email"
v-model="formData.email"
placeholder="Email"
class="input-field"
/>
<input type="email" v-model="formData.email" placeholder="Email" class="input-field" />
</div>
<div class="input-group pwd">
<input
@@ -33,17 +25,15 @@
/>
</div>
<!-- 登录按钮 -->
<button type="submit" class="login-button">Log in</button>
<div class="login-button" @click="handleLogin">Log in</div>
<div class="forgot-password" @click="handleForgotPassword">Forgot password?</div>
<!-- Google登录按钮 -->
<div type="button" class="google-button" @click="handleGoogleLogin">
<img :src="google" class="google-icon" />
Sign in with Google
</div>
<div class="sign-up-button" @click="handleSignup">Dont have an account? Sign Up</div>
</form>
</div>
</div>
</div>
@@ -139,10 +129,8 @@ const handleLogin = async () => {
// 处理忘记密码
const handleForgotPassword = () => {
showToast('忘记密码功能开发中...')
console.log('11111111111')
// 这里可以跳转到忘记密码页面
// router.push('/forgot-password')
router.push('/reset')
}
// 处理Google登录
@@ -291,6 +279,8 @@ const handleSignup = () => {
border-radius: 7rem;
font-size: 4rem;
margin-bottom: 1.67rem;
text-align: center;
line-height: 10rem;
}
.forgot-password {
font-family: 'satoshiRegular';

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

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>