feat: 忘记密码页面
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -52,7 +52,6 @@ interface NoticeItemProps {
|
||||
const props = defineProps<NoticeItemProps>()
|
||||
|
||||
const isMyself = computed(()=>{
|
||||
console.log('isMyself', props)
|
||||
return props.value.userId === '1'
|
||||
})
|
||||
|
||||
|
||||
@@ -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">Don’t 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';
|
||||
|
||||
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