357 lines
12 KiB
Vue
357 lines
12 KiB
Vue
<template>
|
||
<div class="user-card">
|
||
<UserHeader />
|
||
<UserText :user-text="userText" @back="handleBack" />
|
||
<UserTabs v-model="activeTab" :hide-tab="hideTab" :tab-list="tabList" />
|
||
|
||
<el-form class="user-form" ref="forgotPasswordFormRef" :model="forgotPasswordForm" :rules="forgotPasswordRules">
|
||
<Transition name="form-switch" mode="out-in">
|
||
<el-form-item v-if="activeTab === 'email'" key="form-email-item" prop="email">
|
||
<el-input v-model="forgotPasswordForm.email" placeholder="请输入邮箱" clearable size="large" />
|
||
</el-form-item>
|
||
|
||
<el-form-item class="phone-item" prop="phone" v-else-if="activeTab === 'phone'" key="form-phone-item">
|
||
<el-select v-model="forgotPasswordForm.countryCode" :teleported="false" value-key="code" placeholder="国家"
|
||
size="large">
|
||
<el-option v-for="item in countryOptions" :key="item.code" :label="item.code" :value="item.code">
|
||
<div class="country-option">
|
||
<span class="country-name">{{ item.name }}</span>
|
||
<span class="country-code">{{ item.code }}</span>
|
||
</div>
|
||
</el-option>
|
||
</el-select>
|
||
<el-input v-model="forgotPasswordForm.phone" placeholder="请输入手机号" clearable size="large" />
|
||
</el-form-item>
|
||
<UserCaptcha v-model="forgotPasswordForm.code" v-else-if="activeTab === 'code'" key="form-code-item"
|
||
@sendCode="handleSendCode" :show-send-button="showSendButton" :send-state="sendState" />
|
||
<div v-else-if="activeTab === 'password'" key="form-password-group">
|
||
<el-form-item prop="password">
|
||
<el-input v-model="forgotPasswordForm.password" placeholder="请输入新密码" type="password" show-password clearable
|
||
size="large" />
|
||
</el-form-item>
|
||
<el-form-item prop="confirmPassword">
|
||
<el-input v-model="forgotPasswordForm.confirmPassword" placeholder="请确认新密码" type="password" show-password
|
||
clearable size="large" />
|
||
</el-form-item>
|
||
</div>
|
||
</Transition>
|
||
<div class="forgot-next">
|
||
<div :class="{ active: activeTab === 'email' || activeTab === 'phone' }"></div>
|
||
<div :class="{ active: activeTab === 'code' }"></div>
|
||
<div :class="{ active: activeTab === 'password' }"></div>
|
||
</div>
|
||
<el-form-item class="form-submit">
|
||
<el-button type="primary" @click="handleForgotPassword">{{ activeTab === 'password' ? '去登录' : '继续'
|
||
}}</el-button>
|
||
</el-form-item>
|
||
</el-form>
|
||
</div>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
// type类型
|
||
import type { FormInstance, FormRules } from 'element-plus'
|
||
// 组件
|
||
import UserHeader from './components/Header.vue'
|
||
import UserText from './components/Text.vue'
|
||
import UserTabs from './components/Tabs.vue'
|
||
import UserCaptcha from './components/Captcha.vue'
|
||
|
||
//公共方法/store/配置文件
|
||
import { useUserStore } from '@/stores/modules/user'
|
||
import { setStorage, getStorage, removeStorage } from '@/utils/storage'
|
||
import { md5Encrypt } from '@/utils/crypto'
|
||
import { sendCaptcha, initCaptchaState, clearCaptchaTimer } from '@/utils/sendCaptcha'
|
||
|
||
//用户校验规则
|
||
import { countryOptions } from '@/views/user/config/mock'
|
||
import { passwordRules, emailRules, phoneRules, codeRules } from './config/rules'
|
||
|
||
//用户store和router实例
|
||
const userStore = useUserStore()
|
||
const router = useRouter()
|
||
|
||
//userText 实例
|
||
const userText = ref({
|
||
backText: '返回',
|
||
title: '恢复密码',
|
||
text: '请输入您注册时使用的邮箱或手机号',
|
||
account: '',
|
||
|
||
})
|
||
|
||
//tabs实例
|
||
const tabList = ref([
|
||
{ label: '邮箱', name: 'email' },
|
||
{ label: '手机', name: 'phone' },
|
||
])
|
||
const activeTab = ref<string>('email')
|
||
const hideTab = ref<boolean>(false)
|
||
|
||
// 表单实例
|
||
const forgotPasswordFormRef = ref<FormInstance>()
|
||
const forgotPasswordForm = ref({
|
||
email: '',
|
||
countryCode: '+86',
|
||
phone: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
code: ''
|
||
})
|
||
const forgotPasswordRules = ref<FormRules>({
|
||
email: emailRules().email,
|
||
phone: phoneRules(forgotPasswordForm.value).phone,
|
||
password: passwordRules(forgotPasswordForm.value).password,
|
||
confirmPassword: passwordRules(forgotPasswordForm.value).confirmPassword,
|
||
code: codeRules().code
|
||
})
|
||
|
||
// 验证码实例
|
||
const showSendButton = ref<boolean>(true)
|
||
const sendState = ref<object>({
|
||
isCounting: false,
|
||
countdown: 0,
|
||
})
|
||
//初始化验证码倒计时状态
|
||
const emailCaptchaState = initCaptchaState('email'); // 邮箱专属
|
||
const phoneCaptchaState = initCaptchaState('phone'); // 手机专属
|
||
// 邮箱验证码状态
|
||
const { isCounting: emailIsCounting, countdown: emailCountdown } = emailCaptchaState
|
||
// 手机验证码状态
|
||
const { isCounting: phoneIsCounting, countdown: phoneCountdown } = phoneCaptchaState
|
||
|
||
//方法
|
||
|
||
|
||
// 统一更新sendState的方法
|
||
const updateSendState = (type: string) => {
|
||
if (type === 'email') {
|
||
sendState.value = {
|
||
isCounting: emailIsCounting,
|
||
countdown: emailCountdown
|
||
}
|
||
} else {
|
||
sendState.value = {
|
||
isCounting: phoneIsCounting,
|
||
countdown: phoneCountdown
|
||
}
|
||
}
|
||
}
|
||
|
||
// 监听activeTab变化,同步sendState
|
||
watch(activeTab, (newTab) => {
|
||
if (newTab === 'email' || newTab === 'phone') {
|
||
// 切换回邮箱/手机页时,重置sendState
|
||
sendState.value = { isCounting: false, countdown: 0 }
|
||
} else if (newTab === 'code') {
|
||
// 切换到验证码页时,根据当前存储的类型更新状态
|
||
const currentType: string = getStorage('forgotType', 'session') || 'email'
|
||
updateSendState(currentType)
|
||
}
|
||
})
|
||
|
||
// 定义发送验证码接口
|
||
const sendCodeApi = async () => {
|
||
// 发送验证码参数
|
||
const sendParams = {
|
||
email: forgotPasswordForm.value.email,
|
||
mobile: forgotPasswordForm.value.phone,
|
||
reg_type: activeTab.value === 'email' ? 1 : 2,
|
||
type:'transfer_code'
|
||
}
|
||
return userStore.sendCode(sendParams)
|
||
}
|
||
|
||
//发送验证码
|
||
const handleSendCode = async () => {
|
||
// 第二步:确定类型、接收方、对应的倒计时状态和发送函数
|
||
const type: string = getStorage('forgotType', 'session') || 'email'
|
||
const receiver = type === 'phone' ? forgotPasswordForm.value.phone : forgotPasswordForm.value.email;
|
||
const currentCaptchaState = type === 'phone' ? phoneCaptchaState : emailCaptchaState;
|
||
const sendFunc = type === 'phone' ? "sendPhoneCaptcha" : "sendEmailCaptcha";
|
||
|
||
// 第三步:调用封装的发送函数(传正确的函数,而非字符串)
|
||
await sendCaptcha(
|
||
type,
|
||
receiver,
|
||
currentCaptchaState, // 只更新当前类型的倒计时状态
|
||
() => sendCodeApi() // 传入返回Promise的函数
|
||
);
|
||
|
||
//把状态同步给子组件
|
||
updateSendState(type)
|
||
}
|
||
|
||
// 返回上一页
|
||
const handleBack = () => {
|
||
// 从密码页返回 → 验证码页
|
||
if (activeTab.value === 'password') {
|
||
activeTab.value = 'code'
|
||
userText.value = {
|
||
...userText.value,
|
||
title: '双重验证',
|
||
text: '请输入发送的验证码',
|
||
account: getStorage('forgot', 'session') || ''
|
||
}
|
||
// 清空密码字段
|
||
forgotPasswordForm.value.password = ''
|
||
forgotPasswordForm.value.confirmPassword = ''
|
||
forgotPasswordFormRef.value?.clearValidate(['password', 'confirmPassword'])
|
||
}
|
||
|
||
// 从验证码页返回 → 邮箱/手机页
|
||
else if (activeTab.value === 'code') {
|
||
const getForgotType: string = getStorage('forgotType', 'session') || 'email'
|
||
activeTab.value = getForgotType
|
||
hideTab.value = false
|
||
userText.value = {
|
||
...userText.value,
|
||
title: '恢复密码',
|
||
text: '请输入您注册时使用的邮箱或手机号',
|
||
account: ''
|
||
}
|
||
// 清空验证码字段
|
||
forgotPasswordForm.value.code = ''
|
||
forgotPasswordFormRef.value?.clearValidate(['code'])
|
||
}
|
||
|
||
// 从邮箱/手机页返回 → 重置为初始状态
|
||
else {
|
||
activeTab.value = 'email'
|
||
forgotPasswordForm.value.email = ''
|
||
forgotPasswordForm.value.phone = ''
|
||
forgotPasswordFormRef.value?.clearValidate(['email', 'phone'])
|
||
router.push('/user/login')
|
||
}
|
||
}
|
||
|
||
//重置密码
|
||
const handleForgotPassword = async () => {
|
||
if (!forgotPasswordFormRef.value) return
|
||
|
||
try {
|
||
const currentTab = activeTab.value;
|
||
const getForgotType: string = getStorage('forgotType', 'session') || 'email'
|
||
// 步骤1:校验邮箱/手机号 → 切换到验证码页
|
||
if (currentTab === 'email' || currentTab === 'phone') {
|
||
// 校验当前字段
|
||
await forgotPasswordFormRef.value.validateField(currentTab)
|
||
// 存储账号类型和值
|
||
setStorage('forgotType', currentTab, 'session')
|
||
setStorage('forgot', currentTab === 'email' ? forgotPasswordForm.value.email : forgotPasswordForm.value.phone, 'session')
|
||
// 更新状态
|
||
hideTab.value = true
|
||
activeTab.value = 'code'
|
||
userText.value = {
|
||
...userText.value,
|
||
title: '双重验证',
|
||
text: '请输入发送的验证码',
|
||
account: currentTab === 'email' ? forgotPasswordForm.value.email : forgotPasswordForm.value.phone
|
||
}
|
||
updateSendState(getForgotType)
|
||
}
|
||
|
||
// 步骤2:校验验证码 → 切换到密码页
|
||
else if (currentTab === 'code') {
|
||
// 校验验证码
|
||
await forgotPasswordFormRef.value.validateField(currentTab)
|
||
// 更新状态
|
||
activeTab.value = 'password'
|
||
userText.value = {
|
||
...userText.value,
|
||
title: '设置新密码',
|
||
text: '请设置您的新登录密码',
|
||
account: getStorage('forgot', 'session') || ''
|
||
}
|
||
}
|
||
|
||
// 步骤3:校验密码 → 重置密码
|
||
else if (currentTab === 'password') {
|
||
// 校验密码+确认密码
|
||
await forgotPasswordFormRef.value.validateField(['password', 'confirmPassword'])
|
||
|
||
// 重置密码参数
|
||
const getForgotType = getStorage('forgotType', 'session') || 'email'
|
||
const forgotParams = {
|
||
mobile: forgotPasswordForm.value.phone,
|
||
email: forgotPasswordForm.value.email,
|
||
reg_type: getForgotType === 'email' ? 1 : 2,
|
||
code: forgotPasswordForm.value.code,
|
||
login_password1: md5Encrypt(forgotPasswordForm.value.password),
|
||
login_password2: md5Encrypt(forgotPasswordForm.value.confirmPassword),
|
||
}
|
||
|
||
// 调用重置密码接口
|
||
await userStore.forgotPassword(forgotParams)
|
||
|
||
// 清空存储和表单
|
||
removeStorage('forgotType', 'session')
|
||
removeStorage('forgot', 'session')
|
||
forgotPasswordForm.value = {
|
||
email: '',
|
||
countryCode: '+86',
|
||
phone: '',
|
||
password: '',
|
||
confirmPassword: '',
|
||
code: ''
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('表单提交失败:', error)
|
||
}
|
||
}
|
||
// 4. 组件卸载时清除定时器(关键:防止内存泄漏)
|
||
onUnmounted(() => {
|
||
clearCaptchaTimer(emailCaptchaState);
|
||
clearCaptchaTimer(phoneCaptchaState);
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.user-form {
|
||
.phone-item {
|
||
:deep(.el-form-item__content) {
|
||
flex-wrap: nowrap;
|
||
}
|
||
}
|
||
|
||
.forgot-next {
|
||
display: flex;
|
||
justify-content: center;
|
||
width: 100%;
|
||
margin-bottom: 20px;
|
||
margin-top: 60px;
|
||
|
||
div {
|
||
position: relative;
|
||
height: 4px;
|
||
width: 40px;
|
||
background: rgb(209 213 219 / var(--tw-bg-opacity, 1));
|
||
border-radius: 999px;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
height: 100%;
|
||
width: 0;
|
||
background-color: var(--el-color-primary);
|
||
transition: all 0.3s ease-out;
|
||
border-radius: 999px;
|
||
}
|
||
|
||
&:nth-child(2) {
|
||
margin: 0 10px;
|
||
}
|
||
}
|
||
|
||
.active {
|
||
&::before {
|
||
width: 100%;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
</style>
|