|
|
@@ -0,0 +1,343 @@
|
|
|
+<template>
|
|
|
+ <div class="mobile-reset-container">
|
|
|
+ <div class="header">
|
|
|
+ <h2>重置密码</h2>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <el-steps :active="activeStep" finish-status="success" simple style="margin-bottom: 30px; background: transparent; padding: 10px 0;">
|
|
|
+ <el-step title="验证手机" />
|
|
|
+ <el-step title="设置新密码" />
|
|
|
+ </el-steps>
|
|
|
+
|
|
|
+ <!-- Step 1: Send SMS -->
|
|
|
+ <el-form v-if="activeStep === 0" :model="form" label-width="0" size="large">
|
|
|
+ <el-form-item>
|
|
|
+ <el-input v-model="form.mobile" placeholder="手机号" prefix-icon="Iphone" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <div style="display: flex; width: 100%; gap: 10px;">
|
|
|
+ <el-input v-model="form.captcha_code" placeholder="图形验证码" style="flex: 1" />
|
|
|
+ <div class="captcha-img" @click="fetchCaptcha" v-if="captchaImage" style="width: 100px;">
|
|
|
+ <img :src="captchaImage" alt="captcha" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" style="width: 100%" @click="handleSendSms" :loading="loading">
|
|
|
+ 发送验证码
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ <div class="links">
|
|
|
+ <router-link to="/mobile/login">返回登录</router-link>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- Step 2: Reset Password -->
|
|
|
+ <el-form v-if="activeStep === 1" :model="form" :rules="step2Rules" ref="step2FormRef" label-width="0" size="large">
|
|
|
+ <el-form-item>
|
|
|
+ <el-input v-model="form.mobile" disabled prefix-icon="Iphone" />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="sms_code">
|
|
|
+ <div style="display: flex; width: 100%; gap: 10px;">
|
|
|
+ <el-input
|
|
|
+ v-model="form.sms_code"
|
|
|
+ placeholder="短信验证码"
|
|
|
+ prefix-icon="Message"
|
|
|
+ style="flex: 1"
|
|
|
+ :name="dynamicFields.sms"
|
|
|
+ autocomplete="off"
|
|
|
+ />
|
|
|
+ <el-button type="primary" plain :disabled="countdown > 0" @click="openCaptchaDialog">
|
|
|
+ {{ countdown > 0 ? `${countdown}s` : '重新发送' }}
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="new_password">
|
|
|
+ <el-input
|
|
|
+ v-model="form.new_password"
|
|
|
+ type="password"
|
|
|
+ placeholder="新密码"
|
|
|
+ prefix-icon="Lock"
|
|
|
+ show-password
|
|
|
+ :name="dynamicFields.password"
|
|
|
+ autocomplete="new-password"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item prop="confirm_password">
|
|
|
+ <el-input
|
|
|
+ v-model="form.confirm_password"
|
|
|
+ type="password"
|
|
|
+ placeholder="确认新密码"
|
|
|
+ prefix-icon="Lock"
|
|
|
+ show-password
|
|
|
+ autocomplete="new-password"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item>
|
|
|
+ <el-button type="primary" style="width: 100%" @click="handleReset" :loading="loading">
|
|
|
+ 确认重置
|
|
|
+ </el-button>
|
|
|
+ </el-form-item>
|
|
|
+ <div class="links">
|
|
|
+ <el-button link @click="activeStep = 0">上一步</el-button>
|
|
|
+ </div>
|
|
|
+ </el-form>
|
|
|
+
|
|
|
+ <!-- Resend Captcha Dialog -->
|
|
|
+ <el-dialog v-model="captchaDialogVisible" title="安全验证" width="90%" append-to-body>
|
|
|
+ <el-form :model="captchaForm" label-width="0">
|
|
|
+ <el-form-item>
|
|
|
+ <div style="display: flex; width: 100%; gap: 10px;">
|
|
|
+ <el-input v-model="captchaForm.code" placeholder="图形验证码" style="flex: 1" />
|
|
|
+ <div class="captcha-img" @click="fetchResendCaptcha" v-if="resendCaptchaImage">
|
|
|
+ <img :src="resendCaptchaImage" alt="captcha" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <span class="dialog-footer">
|
|
|
+ <el-button @click="captchaDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="handleResendSmsConfirm" :loading="resendLoading">
|
|
|
+ 确认发送
|
|
|
+ </el-button>
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import { ref, reactive, onMounted, onUnmounted } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { getCaptcha, sendSms, resetPassword } from '../../api/public'
|
|
|
+import { ElMessage } from 'element-plus'
|
|
|
+import type { FormInstance, FormRules } from 'element-plus'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const activeStep = ref(0)
|
|
|
+const loading = ref(false)
|
|
|
+const captchaImage = ref('')
|
|
|
+const step2FormRef = ref<FormInstance>()
|
|
|
+
|
|
|
+// Resend Logic State
|
|
|
+const countdown = ref(0)
|
|
|
+let timer: any = null
|
|
|
+const captchaDialogVisible = ref(false)
|
|
|
+const resendCaptchaImage = ref('')
|
|
|
+const resendCaptchaId = ref('')
|
|
|
+const resendLoading = ref(false)
|
|
|
+const captchaForm = reactive({
|
|
|
+ code: ''
|
|
|
+})
|
|
|
+
|
|
|
+const dynamicFields = reactive({
|
|
|
+ sms: 'sms_code',
|
|
|
+ password: 'new_password'
|
|
|
+})
|
|
|
+
|
|
|
+const form = reactive({
|
|
|
+ mobile: '',
|
|
|
+ captcha_id: '',
|
|
|
+ captcha_code: '',
|
|
|
+ sms_code: '',
|
|
|
+ new_password: '',
|
|
|
+ confirm_password: ''
|
|
|
+})
|
|
|
+
|
|
|
+const validatePass2 = (rule: any, value: any, callback: any) => {
|
|
|
+ if (value === '') {
|
|
|
+ callback(new Error('请再次输入密码'))
|
|
|
+ } else if (value !== form.new_password) {
|
|
|
+ callback(new Error('两次输入密码不一致!'))
|
|
|
+ } else {
|
|
|
+ callback()
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const step2Rules = reactive<FormRules>({
|
|
|
+ sms_code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
|
|
|
+ new_password: [
|
|
|
+ { required: true, message: '请输入新密码', trigger: 'blur' },
|
|
|
+ { min: 6, message: '密码长度不能少于6位', trigger: 'blur' },
|
|
|
+ { pattern: /^(?=.*[a-zA-Z])(?=.*\d).+$/, message: '密码必须包含字母和数字', trigger: 'blur' }
|
|
|
+ ],
|
|
|
+ confirm_password: [{ validator: validatePass2, trigger: 'blur' }]
|
|
|
+})
|
|
|
+
|
|
|
+const fetchCaptcha = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getCaptcha()
|
|
|
+ captchaImage.value = res.data.image
|
|
|
+ form.captcha_id = res.data.captcha_id
|
|
|
+ form.captcha_code = ''
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const startCountdown = () => {
|
|
|
+ countdown.value = 60
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
+ timer = setInterval(() => {
|
|
|
+ countdown.value--
|
|
|
+ if (countdown.value <= 0) {
|
|
|
+ clearInterval(timer)
|
|
|
+ }
|
|
|
+ }, 1000)
|
|
|
+}
|
|
|
+
|
|
|
+const openCaptchaDialog = () => {
|
|
|
+ captchaForm.code = ''
|
|
|
+ fetchResendCaptcha()
|
|
|
+ captchaDialogVisible.value = true
|
|
|
+}
|
|
|
+
|
|
|
+const fetchResendCaptcha = async () => {
|
|
|
+ try {
|
|
|
+ const res = await getCaptcha()
|
|
|
+ resendCaptchaImage.value = res.data.image
|
|
|
+ resendCaptchaId.value = res.data.captcha_id
|
|
|
+ } catch (e) {
|
|
|
+ console.error(e)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleResendSmsConfirm = async () => {
|
|
|
+ if (!captchaForm.code) {
|
|
|
+ ElMessage.warning('请输入图形验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ resendLoading.value = true
|
|
|
+ try {
|
|
|
+ await sendSms({
|
|
|
+ mobile: form.mobile,
|
|
|
+ captcha_id: resendCaptchaId.value,
|
|
|
+ captcha_code: captchaForm.code
|
|
|
+ })
|
|
|
+ ElMessage.success('验证码已发送')
|
|
|
+ captchaDialogVisible.value = false
|
|
|
+ startCountdown()
|
|
|
+ } catch (e) {
|
|
|
+ fetchResendCaptcha()
|
|
|
+ } finally {
|
|
|
+ resendLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleSendSms = async () => {
|
|
|
+ if (!form.mobile || !form.captcha_code) {
|
|
|
+ ElMessage.warning('请输入手机号和图形验证码')
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ await sendSms({
|
|
|
+ mobile: form.mobile,
|
|
|
+ captcha_id: form.captcha_id,
|
|
|
+ captcha_code: form.captcha_code
|
|
|
+ })
|
|
|
+ ElMessage.success('验证码已发送')
|
|
|
+ activeStep.value = 1
|
|
|
+ startCountdown()
|
|
|
+ } catch (e) {
|
|
|
+ fetchCaptcha() // Refresh captcha on fail
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleReset = async () => {
|
|
|
+ if (!step2FormRef.value) return
|
|
|
+
|
|
|
+ await step2FormRef.value.validate(async (valid) => {
|
|
|
+ if (valid) {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ await resetPassword({
|
|
|
+ mobile: form.mobile,
|
|
|
+ sms_code: form.sms_code,
|
|
|
+ new_password: form.new_password
|
|
|
+ })
|
|
|
+ ElMessage.success('密码重置成功')
|
|
|
+ router.push('/mobile/login')
|
|
|
+ } catch (e) {
|
|
|
+ // handled
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+ }
|
|
|
+ })
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchCaptcha()
|
|
|
+ const suffix = Math.random().toString(36).slice(2, 8)
|
|
|
+ dynamicFields.sms = `sms_${suffix}`
|
|
|
+ dynamicFields.password = `pwd_${suffix}`
|
|
|
+})
|
|
|
+
|
|
|
+onUnmounted(() => {
|
|
|
+ if (timer) clearInterval(timer)
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.mobile-reset-container {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ justify-content: center;
|
|
|
+ min-height: 100vh;
|
|
|
+ background-color: #fff;
|
|
|
+ padding: 0 30px;
|
|
|
+}
|
|
|
+
|
|
|
+.header {
|
|
|
+ text-align: center;
|
|
|
+ margin-bottom: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+.captcha-img {
|
|
|
+ cursor: pointer;
|
|
|
+ height: 40px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+}
|
|
|
+.captcha-img img {
|
|
|
+ height: 100%;
|
|
|
+ width: 100%;
|
|
|
+ object-fit: cover;
|
|
|
+}
|
|
|
+
|
|
|
+.links {
|
|
|
+ text-align: center;
|
|
|
+ margin-top: 20px;
|
|
|
+}
|
|
|
+
|
|
|
+/* Custom Large Input Styles - Consistent with Login */
|
|
|
+:deep(.el-input--large .el-input__wrapper) {
|
|
|
+ padding: 8px 15px;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-input--large .el-input__inner) {
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: 500;
|
|
|
+ height: 40px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-button--large) {
|
|
|
+ height: 48px;
|
|
|
+ font-size: 18px;
|
|
|
+ border-radius: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.el-button--default) {
|
|
|
+ height: 48px;
|
|
|
+}
|
|
|
+</style>
|
|
|
+
|