2025-11-24 16:06:44 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="w-full auth-fade-in">
|
|
|
|
|
|
<!-- 标题 -->
|
|
|
|
|
|
<div class="text-center mb-8">
|
|
|
|
|
|
<h2 class="auth-title">重置密码</h2>
|
|
|
|
|
|
<p class="auth-subtitle">请输入手机号和验证码重置密码</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<form class="space-y-4" @submit.prevent="onReset">
|
|
|
|
|
|
<!-- 手机号输入 -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="auth-label">手机号</label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.phone"
|
|
|
|
|
|
name="reset-phone"
|
|
|
|
|
|
placeholder="请输入手机号"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
clearable
|
|
|
|
|
|
maxlength="11"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
class="auth-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 验证码输入 -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="auth-label">验证码</label>
|
|
|
|
|
|
<div class="flex gap-3">
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.code"
|
|
|
|
|
|
name="reset-code"
|
|
|
|
|
|
placeholder="请输入验证码"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
maxlength="6"
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
class="auth-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path fill-rule="evenodd" d="M18 8A6 6 0 006 8c0 3.314-4.03 6-6 6s6 2.686 6 6a6 6 0 0012 0c0-3.314 4.03-6 6-6s-6-2.686-6-6z" clip-rule="evenodd"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
:disabled="!canSendCode || loading"
|
|
|
|
|
|
@click="sendCode"
|
|
|
|
|
|
:loading="sendingCode"
|
|
|
|
|
|
class="auth-button !px-6 !min-w-[120px]"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ countdown > 0 ? `${countdown}s` : '获取验证码' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 新密码输入 -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="auth-label">新密码</label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.newPassword"
|
|
|
|
|
|
name="reset-new-password"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="请输入新密码(至少6位)"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
show-password
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
class="auth-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 确认新密码输入 -->
|
|
|
|
|
|
<div>
|
|
|
|
|
|
<label class="auth-label">确认新密码</label>
|
|
|
|
|
|
<el-input
|
|
|
|
|
|
v-model="form.confirmNewPassword"
|
|
|
|
|
|
name="reset-confirm-new-password"
|
|
|
|
|
|
type="password"
|
|
|
|
|
|
placeholder="请再次输入新密码"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
show-password
|
|
|
|
|
|
:disabled="loading"
|
|
|
|
|
|
class="auth-input"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #prefix>
|
|
|
|
|
|
<svg class="w-4 h-4 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
|
|
|
|
|
|
<path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
|
|
|
|
|
|
</svg>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
</el-input>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 操作链接 -->
|
|
|
|
|
|
<div class="text-center py-2">
|
|
|
|
|
|
<router-link to="/auth/login" class="auth-link text-sm">
|
|
|
|
|
|
返回登录
|
|
|
|
|
|
</router-link>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 重置按钮 -->
|
|
|
|
|
|
<el-button
|
|
|
|
|
|
type="primary"
|
|
|
|
|
|
size="large"
|
|
|
|
|
|
class="auth-button w-full !h-12 !text-base !font-medium"
|
|
|
|
|
|
native-type="submit"
|
|
|
|
|
|
:loading="loading"
|
|
|
|
|
|
:disabled="!canSubmit"
|
|
|
|
|
|
>
|
|
|
|
|
|
{{ loading ? '重置中...' : '重置密码' }}
|
|
|
|
|
|
</el-button>
|
|
|
|
|
|
</form>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup name="UserResetPassword">
|
|
|
|
|
|
import { useUserStore } from '@/stores/user'
|
2026-02-27 14:49:21 +08:00
|
|
|
|
import { useAliyunCaptcha } from '@/composables/useAliyunCaptcha'
|
2025-11-24 16:06:44 +08:00
|
|
|
|
import { ElMessage } from 'element-plus'
|
|
|
|
|
|
|
|
|
|
|
|
const router = useRouter()
|
|
|
|
|
|
const userStore = useUserStore()
|
2026-02-27 14:49:21 +08:00
|
|
|
|
const { runWithCaptcha } = useAliyunCaptcha()
|
2025-11-24 16:06:44 +08:00
|
|
|
|
|
|
|
|
|
|
// 表单数据
|
|
|
|
|
|
const form = ref({
|
|
|
|
|
|
phone: '',
|
|
|
|
|
|
code: '',
|
|
|
|
|
|
newPassword: '',
|
|
|
|
|
|
confirmNewPassword: ''
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// 状态
|
|
|
|
|
|
const loading = ref(false)
|
|
|
|
|
|
const sendingCode = ref(false)
|
|
|
|
|
|
const countdown = ref(0)
|
|
|
|
|
|
let countdownTimer = null
|
|
|
|
|
|
|
|
|
|
|
|
// 计算属性
|
|
|
|
|
|
const canSendCode = computed(() => {
|
|
|
|
|
|
return form.value.phone && form.value.phone.length === 11 && countdown.value === 0
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
const canSubmit = computed(() => {
|
|
|
|
|
|
return form.value.phone && form.value.phone.length === 11 &&
|
|
|
|
|
|
form.value.code && form.value.code.length === 6 &&
|
|
|
|
|
|
form.value.newPassword && form.value.newPassword.length >= 6 &&
|
|
|
|
|
|
form.value.confirmNewPassword && form.value.confirmNewPassword === form.value.newPassword
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2026-02-27 14:49:21 +08:00
|
|
|
|
// 发送验证码(先通过滑块再请求后端发码)
|
2025-11-24 16:06:44 +08:00
|
|
|
|
const sendCode = async () => {
|
|
|
|
|
|
if (!canSendCode.value) return
|
|
|
|
|
|
|
|
|
|
|
|
sendingCode.value = true
|
|
|
|
|
|
try {
|
2026-02-27 14:49:21 +08:00
|
|
|
|
await runWithCaptcha(
|
|
|
|
|
|
async (captchaVerifyParam) => {
|
|
|
|
|
|
return await userStore.sendCode(form.value.phone, 'reset_password', captchaVerifyParam)
|
|
|
|
|
|
},
|
|
|
|
|
|
(res) => {
|
|
|
|
|
|
if (res.success) {
|
|
|
|
|
|
ElMessage.success('验证码发送成功')
|
|
|
|
|
|
startCountdown()
|
|
|
|
|
|
} else {
|
|
|
|
|
|
ElMessage.error(res?.error?.message || '验证码发送失败')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
)
|
2025-11-24 16:06:44 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('验证码发送失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
sendingCode.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始倒计时
|
|
|
|
|
|
const startCountdown = () => {
|
|
|
|
|
|
countdown.value = 60
|
|
|
|
|
|
countdownTimer = setInterval(() => {
|
|
|
|
|
|
countdown.value--
|
|
|
|
|
|
if (countdown.value <= 0) {
|
|
|
|
|
|
clearInterval(countdownTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 1000)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置密码
|
|
|
|
|
|
const onReset = async () => {
|
|
|
|
|
|
if (!canSubmit.value) return
|
|
|
|
|
|
|
|
|
|
|
|
if (form.value.newPassword !== form.value.confirmNewPassword) {
|
|
|
|
|
|
ElMessage.error('两次密码不一致')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
loading.value = true
|
|
|
|
|
|
try {
|
|
|
|
|
|
const resetData = {
|
|
|
|
|
|
phone: form.value.phone,
|
|
|
|
|
|
newPassword: form.value.newPassword,
|
|
|
|
|
|
confirmNewPassword: form.value.confirmNewPassword,
|
|
|
|
|
|
code: form.value.code
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const result = await userStore.resetPassword(resetData)
|
|
|
|
|
|
if (result.success) {
|
|
|
|
|
|
ElMessage.success('密码重置成功')
|
|
|
|
|
|
router.push('/auth/login')
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('密码重置失败:', error)
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
loading.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 组件卸载时清理定时器
|
|
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (countdownTimer) {
|
|
|
|
|
|
clearInterval(countdownTimer)
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
/* 输入框样式优化 */
|
|
|
|
|
|
:deep(.el-input__wrapper) {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
transition: all 0.3s ease !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper:hover) {
|
|
|
|
|
|
border-color: #3b82f6 !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-input__wrapper.is-focus) {
|
|
|
|
|
|
border-color: #3b82f6 !important;
|
|
|
|
|
|
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 按钮样式优化 */
|
|
|
|
|
|
:deep(.el-button--primary) {
|
|
|
|
|
|
border-radius: 8px !important;
|
|
|
|
|
|
font-weight: 500 !important;
|
|
|
|
|
|
transition: all 0.3s ease !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-button--primary:hover) {
|
|
|
|
|
|
transform: translateY(-1px) !important;
|
|
|
|
|
|
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.3) !important;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|