Files
MrleeAdmin/app/components/login-dialog/forget-password.vue
T
Mr.April d84d972e73 feat: 适配5.x后端,重构前端架构
主要变更:
1. 重构API目录结构,采用模块化设计,适配5.x后端接口规范
2. 优化布局组件,提升用户界面体验
3. 增强国际化支持,完善多语言配置
4. 新增权限控制功能,支持前端路由权限管理
5. 优化认证流程,支持JWT认证机制
6. 新增中间件和插件,提升系统扩展性
7. 优化状态管理,采用更清晰的模块化设计
8. 新增搜索框、广告等组件,丰富页面功能
9. 更改包管理工具从npm到pnpm,提升依赖管理效率
10. 完善配置系统,支持多环境配置

适配5.x后端特性:
- 采用新的API接口规范
- 支持JWT认证机制
- 适配后端权限控制体系
- 支持后端国际化接口
- 优化数据交互方式
2026-04-07 11:01:11 +08:00

566 lines
14 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="forget-password-container" :class="{'dark-mode': isDarkMode}">
<div class="forget-password-form-container">
<!-- 第一步:邮箱验证 -->
<div v-if="currentStep === 1">
<div class="forget-password-title">{{ t('auth.forget.password.title') }}</div>
<el-form
:model="step1Data"
ref="step1FormRef"
:rules="step1Rules"
:validate-on-rule-change="false"
class="forget-password-form"
>
<el-form-item prop="email" class="form-item">
<div class="input-container">
<el-input
v-model="step1Data.email"
:placeholder="t('auth.forget.password.email_placeholder')"
clearable
class="forget-password-input"
>
<template #prefix>
<Icon
icon="ant-design:mail-outlined"
:color="isDarkMode ? '#ffffff' : '#000000'"
class="input-prefix-icon"
/>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="captcha_code" class="form-item">
<div class="input-container">
<el-input
v-model="step1Data.captcha_code"
:placeholder="t('auth.forget.password.captcha_placeholder')"
class="forget-password-input"
>
<template #prefix>
<Icon
icon="ant-design:safety-outlined"
:color="isDarkMode ? '#ffffff' : '#000000'"
class="input-prefix-icon"
/>
</template>
<template #suffix>
<div class="captcha-image" @click="captcha.refresh()">
<img v-if="captcha.image.value" :src="captcha.image.value" :alt="t('auth.forget.password.captcha_alt')" />
<Icon v-else icon="ant-design:loading-outlined" :size="24" />
</div>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item class="form-item">
<el-button
type="primary"
class="next-step-button"
@click="handleNextStep"
>
{{ t('auth.forget.password.next_step') }}
</el-button>
</el-form-item>
<div class="login-link">
{{ t('auth.forget.password.back_to_login') }}
<span class="login-button" @click="switchToLogin">
{{ t('auth.forget.password.login') }}
</span>
</div>
</el-form>
</div>
<!-- 第二步:设置新密码 -->
<div v-else>
<div class="forget-password-title">{{ t('auth.forget.password.reset_title') }}</div>
<!-- 验证码发送提示 -->
<div class="verification-tip" v-if="verificationSent">
{{ t('auth.forget.password.verification_sent', { email: step1Data.email }) }}<br>
{{ t('auth.forget.password.verification_instruction') }}
</div>
<el-form
:model="step2Data"
ref="step2FormRef"
:rules="step2Rules"
:validate-on-rule-change="false"
class="forget-password-form"
>
<el-form-item prop="password" class="form-item">
<div class="input-container">
<el-input
v-model="step2Data.password"
:placeholder="t('auth.forget.password.password_placeholder')"
type="password"
show-password
clearable
class="forget-password-input"
>
<template #prefix>
<Icon
icon="ant-design:lock-outlined"
:color="isDarkMode ? '#ffffff' : '#000000'"
class="input-prefix-icon"
/>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="confirm_password" class="form-item">
<div class="input-container">
<el-input
v-model="step2Data.confirm_password"
:placeholder="t('auth.forget.password.confirm_password_placeholder')"
type="password"
show-password
clearable
class="forget-password-input"
>
<template #prefix>
<Icon
icon="ant-design:lock-outlined"
:color="isDarkMode ? '#ffffff' : '#000000'"
class="input-prefix-icon"
/>
</template>
</el-input>
</div>
</el-form-item>
<el-form-item prop="code" class="form-item">
<div class="email-code-container">
<div class="input-container">
<el-input
v-model="step2Data.code"
:placeholder="t('auth.forget.password.verification_code_placeholder')"
class="forget-password-input"
>
<template #prefix>
<Icon
icon="ant-design:key-outlined"
:color="isDarkMode ? '#ffffff' : '#000000'"
class="input-prefix-icon"
/>
</template>
</el-input>
</div>
<el-button
type="primary"
class="send-email-code-button"
:disabled="emailCodeCountdown > 0"
@click="handleSendEmailCode"
>
{{ emailCodeCountdown > 0 ? `${emailCodeCountdown}s` : t('auth.forget.password.resend_code') }}
</el-button>
</div>
</el-form-item>
<el-form-item class="form-item">
<el-button
type="primary"
class="reset-password-button"
@click="handleResetPassword"
>
{{ t('auth.forget.password.reset_password') }}
</el-button>
</el-form-item>
<div class="prev-step-container">
<span class="prev-step" @click="handlePrevStep">
<Icon icon="ant-design:arrow-left-outlined" />
{{ t('auth.forget.password.prev_step') }}
</span>
</div>
</el-form>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, reactive, computed, onMounted } from 'vue'
import { forgetPassword, verifyEmail, sendEmailCode } from '@/api/auth'
import type { FormInstance } from 'element-plus'
import { Icon } from '~/components/icon'
import { t } from '~/composables/lang'
import { useCaptcha } from '~/composables/captcha'
import validate from '~/utils/validate'
import { ElMessage } from 'element-plus'
// 暗黑模式状态
const isDarkMode = computed(() => {
const root = document.querySelector('html')
return root?.classList.contains('dark') || false
})
// 当前步骤
const currentStep = ref(1)
const isLoading = ref(false)
const emailCodeCountdown = ref(0)
const verificationSent = ref(false)
let emailCodeTimer: any = null
// 第一步数据:邮箱 + 图片验证码
const step1Data = reactive({
email: '',
captcha_code: '',
captcha_key: ''
})
// 第二步数据:新密码 + 确认密码 + 验证码
const step2Data = reactive({
password: '',
confirm_password: '',
code: ''
})
// 存储邮箱,用于第二步
const savedEmail = ref('')
// 表单引用
const step1FormRef = ref<FormInstance>()
const step2FormRef = ref<FormInstance>()
// 使用验证码组合式函数
const captcha = useCaptcha(step1Data)
// 第一步验证规则
const step1Rules = computed(() => ({
email: [
{
type: 'string',
required: true,
message: t('auth.forget.password.email_required'),
trigger: ['blur', 'change']
},
{
validator: (_rule: any, value: string) => {
return validate.email(value)
},
message: t('auth.forget.password.email_invalid'),
trigger: ['blur']
}
],
captcha_code: {
type: 'string',
required: true,
message: t('auth.forget.password.captcha_required'),
trigger: ['blur', 'change']
}
}) as any)
// 第二步验证规则
const step2Rules = computed(() => ({
password: {
type: 'string',
required: true,
message: t('auth.forget.password.password_required'),
trigger: ['blur', 'change']
},
confirm_password: [
{
type: 'string',
required: true,
message: t('auth.forget.password.confirm_password_required'),
trigger: ['blur', 'change']
},
{
validator: (_rule: any, value: string) => {
return value === step2Data.password
},
message: t('auth.forget.password.password_mismatch'),
trigger: ['blur', 'change']
}
],
code: {
type: 'string',
required: true,
message: t('auth.forget.password.verification_code_required'),
trigger: ['blur', 'change']
}
}) as any)
// 发送邮箱验证码
const handleSendEmailCode = async () => {
if (!step1Data.email || !validate.email(step1Data.email)) {
ElMessage.error(t('auth.forget.password.email_invalid'))
return
}
if (emailCodeCountdown.value > 0) return
try {
await sendEmailCode({
email: step1Data.email,
captcha_key: step1Data.captcha_key,
captcha_code: step1Data.captcha_code
})
ElMessage.success(t('auth.forget.password.code_sent'))
emailCodeCountdown.value = 60
emailCodeTimer = setInterval(() => {
emailCodeCountdown.value--
if (emailCodeCountdown.value <= 0) {
clearInterval(emailCodeTimer)
}
}, 1000)
} catch (error) {
captcha.refresh()
}
}
// 组件挂载时加载验证码
onMounted(() => {
captcha.refresh()
})
// 下一步:验证邮箱
const handleNextStep = async () => {
await step1FormRef.value?.validate(async (valid) => {
if (!valid) return
if (isLoading.value) return
isLoading.value = true
try {
// 发送验证码
await sendEmailCode({
email: step1Data.email,
captcha_key: step1Data.captcha_key,
captcha_code: step1Data.captcha_code
})
savedEmail.value = step1Data.email
verificationSent.value = true
currentStep.value = 2
// 开始倒计时
emailCodeCountdown.value = 60
emailCodeTimer = setInterval(() => {
emailCodeCountdown.value--
if (emailCodeCountdown.value <= 0) {
clearInterval(emailCodeTimer)
}
}, 1000)
ElMessage.success(t('auth.forget.password.code_sent_check_email'))
} catch (error) {
captcha.refresh()
} finally {
isLoading.value = false
}
})
}
// 上一步
const handlePrevStep = () => {
currentStep.value = 1
verificationSent.value = false
if (emailCodeTimer) {
clearInterval(emailCodeTimer)
emailCodeCountdown.value = 0
}
}
// 重置密码
const handleResetPassword = async () => {
await step2FormRef.value?.validate(async (valid) => {
if (!valid) return
if (isLoading.value) return
isLoading.value = true
try {
await forgetPassword({
email: savedEmail.value,
code: step2Data.code,
new_password: step2Data.password,
confirm_password: step2Data.confirm_password
})
ElMessage.success(t('auth.forget.password.reset_success'))
emit('typeChange', 'login')
} catch (error) {
captcha.refresh()
} finally {
isLoading.value = false
}
})
}
// 切换到登录
const emit = defineEmits(['typeChange'])
const switchToLogin = () => {
emit('typeChange', 'login')
}
</script>
<style lang="scss" scoped>
.forget-password-container {
width: 100%;
&.dark-mode {
background: var(--el-bg-color);
color: var(--el-text-color-primary);
}
}
.forget-password-form-container {
background: var(--el-bg-color);
width: 100%;
padding: 60px 30px;
border-radius: var(--rounded-big);
}
.forget-password-title {
font-size: 24px;
color: var(--el-text-color-primary);
font-weight: 600;
text-align: center;
margin-bottom: 30px;
font-family: 'OppoSans-R';
}
.forget-password-tip {
font-size: 14px;
color: var(--el-text-color-secondary);
text-align: center;
margin-bottom: 40px;
line-height: 20px;
}
.verification-tip {
background-color: #E8F5E8;
color: #2E7D32;
padding: 15px;
border-radius: 8px;
margin-bottom: 30px;
text-align: center;
font-size: 14px;
line-height: 1.4;
}
.forget-password-form {
width: 100%;
}
.form-item {
margin-bottom: 20px;
}
.captcha-image {
height: 30px;
cursor: pointer;
img {
height: 100%;
object-fit: cover;
}
.iconify {
font-size: 24px;
color: var(--el-text-color-secondary);
}
}
.input-container {
flex: 1;
height: 50px;
border: 1px solid var(--el-border-color);
border-radius: 8px;
display: flex;
align-items: center;
background: var(--el-bg-color);
}
.forget-password-input {
width: 100%;
border: none;
:deep(.el-input__wrapper) {
box-shadow: none;
background: transparent;
.el-input__inner {
color: var(--el-text-color-primary);
&::placeholder {
color: var(--el-text-color-placeholder);
}
}
}
:deep(.el-input__suffix) {
padding: 0;
line-height: 1;
}
}
.input-prefix-icon {
margin-right: 14px;
}
.email-code-container {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
}
.send-email-code-button {
flex-shrink: 0;
height: 50px;
white-space: nowrap;
border-radius: 8px;
}
.login-link {
text-align: center;
margin-top: 20px;
font-size: 14px;
color: var(--el-text-color-secondary);
.login-button {
color: var(--el-color-primary);
cursor: pointer;
margin-left: 5px;
&:hover {
text-decoration: underline;
}
}
}
.next-step-button,
.reset-password-button {
width: 100%;
height: 50px;
border-radius: 8px;
font-size: 16px;
font-weight: 500;
}
.prev-step-container {
text-align: center;
margin-top: 20px;
.prev-step {
color: var(--el-color-primary);
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 5px;
&:hover {
text-decoration: underline;
}
}
}
</style>