foreign-admin-ui/src/views/user/ForgotPassword.vue

357 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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