浏览代码

!617 feat: 支持通过短信重置后台密码
* feat: 支持通过短信重置后台密码

杨宇庆 7 月之前
父节点
当前提交
65924bb921

+ 5 - 0
src/api/login/index.ts

@@ -85,3 +85,8 @@ export const getCode = (data) => {
 export const reqCheck = (data) => {
   return request.postOriginal({ url: 'system/captcha/check', data })
 }
+
+// 通过短信重置密码
+export const smsResetPassword = (data) => {
+  return request.post({ url: '/system/auth/sms-reset-password', data })
+}

+ 4 - 1
src/locales/en.ts

@@ -140,7 +140,10 @@ export default {
     btnQRCode: 'QR code sign in',
     qrcode: 'Scan the QR code to log in',
     btnRegister: 'Sign up',
-    SmsSendMsg: 'code has been sent'
+    SmsSendMsg: 'code has been sent',
+    resetPassword: "Reset Password",
+    resetPasswordSuccess: "Reset Password Success",
+    invalidTenantName:"Invalid Tenant Name"
   },
   captcha: {
     verification: 'Please complete security verification',

+ 4 - 1
src/locales/zh-CN.ts

@@ -141,7 +141,10 @@ export default {
     btnQRCode: '二维码登录',
     qrcode: '扫描二维码登录',
     btnRegister: '注册',
-    SmsSendMsg: '验证码已发送'
+    SmsSendMsg: '验证码已发送',
+    resetPassword: "重置密码",
+    resetPasswordSuccess: "重置密码成功",
+    invalidTenantName: "无效的租户名称"
   },
   captcha: {
     verification: '请完成安全验证',

+ 3 - 1
src/views/Login/Login.vue

@@ -59,6 +59,8 @@
             <RegisterForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
             <!-- 三方登录 -->
             <SSOLoginVue class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
+            <!-- 忘记密码 -->
+            <ForgetPasswordForm class="m-auto h-auto p-20px lt-xl:(rounded-3xl light:bg-white)" />
           </div>
         </Transition>
       </div>
@@ -73,7 +75,7 @@ import { useAppStore } from '@/store/modules/app'
 import { ThemeSwitch } from '@/layout/components/ThemeSwitch'
 import { LocaleDropdown } from '@/layout/components/LocaleDropdown'
 
-import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue } from './components'
+import { LoginForm, MobileForm, QrCodeForm, RegisterForm, SSOLoginVue, ForgetPasswordForm } from './components'
 
 defineOptions({ name: 'Login' })
 

+ 278 - 0
src/views/Login/components/ForgetPasswordForm.vue

@@ -0,0 +1,278 @@
+<template>
+  <el-form
+    v-show="getShow"
+    ref="formSmsResetPassword"
+    :model="resetPasswordData"
+    :rules="rules"
+    class="login-form"
+    label-position="top"
+    label-width="120px"
+    size="large"
+  >
+    <el-row style="margin-right: -10px; margin-left: -10px">
+      <!-- 租户名 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <LoginFormTitle style="width: 100%" />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item v-if="resetPasswordData.tenantEnable === 'true'" prop="tenantName">
+          <el-input
+            v-model="resetPasswordData.tenantName"
+            :placeholder="t('login.tenantNamePlaceholder')"
+            :prefix-icon="iconHouse"
+            type="primary"
+            link
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 手机号 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="mobile">
+          <el-input
+            v-model="resetPasswordData.mobile"
+            :placeholder="t('login.mobileNumberPlaceholder')"
+            :prefix-icon="iconCellphone"
+          />
+        </el-form-item>
+      </el-col>
+      <Verify
+        ref="verify"
+        :captchaType="captchaType"
+        :imgSize="{ width: '400px', height: '200px' }"
+        mode="pop"
+        @success="getSmsCode"
+      />
+      <!-- 验证码 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="code">
+          <el-row :gutter="5" justify="space-between" style="width: 100%">
+            <el-col :span="24">
+              <el-input
+                v-model="resetPasswordData.code"
+                :placeholder="t('login.codePlaceholder')"
+                :prefix-icon="iconCircleCheck"
+              >
+                <template #append>
+                  <span
+                    v-if="mobileCodeTimer <= 0"
+                    class="getMobileCode"
+                    style="cursor: pointer"
+                    @click="getCode"
+                  >
+                    {{ t('login.getSmsCode') }}
+                  </span>
+                  <span v-if="mobileCodeTimer > 0" class="getMobileCode" style="cursor: pointer">
+                    {{ mobileCodeTimer }}秒后可重新获取
+                  </span>
+                </template>
+              </el-input>
+              <!-- </el-button> -->
+            </el-col>
+          </el-row>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="password">
+          <InputPassword
+            v-model="resetPasswordData.password"
+            :placeholder="t('login.passwordPlaceholder')"
+            style="width: 100%"
+            strength="true"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item prop="check_password">
+          <InputPassword
+            v-model="resetPasswordData.check_password"
+            :placeholder="t('login.checkPassword')"
+            style="width: 100%"
+            strength="true"
+          />
+        </el-form-item>
+      </el-col>
+      <!-- 登录按钮 / 返回按钮 -->
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.resetPassword')"
+            class="w-[100%]"
+            type="primary"
+            @click="resetPassword()"
+          />
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" style="padding-right: 10px; padding-left: 10px">
+        <el-form-item>
+          <XButton
+            :loading="loginLoading"
+            :title="t('login.backLogin')"
+            class="w-[100%]"
+            @click="handleBackLogin()"
+          />
+        </el-form-item>
+      </el-col>
+    </el-row>
+  </el-form>
+</template>
+<script lang="ts" setup>
+import type { RouteLocationNormalizedLoaded } from 'vue-router'
+
+import { useIcon } from '@/hooks/web/useIcon'
+
+import { sendSmsCode, smsResetPassword } from '@/api/login'
+import LoginFormTitle from './LoginFormTitle.vue'
+import { LoginStateEnum, useFormValid, useLoginState } from './useLogin'
+import { ElLoading } from 'element-plus'
+import * as authUtil from '@/utils/auth'
+import * as LoginApi from '@/api/login'
+defineOptions({ name: 'ForgetPasswordForm' })
+const verify = ref()
+
+const { t } = useI18n()
+const message = useMessage()
+const { currentRoute, push } = useRouter()
+const formSmsResetPassword = ref()
+const loginLoading = ref(false)
+const iconHouse = useIcon({ icon: 'ep:house' })
+const iconCellphone = useIcon({ icon: 'ep:cellphone' })
+const iconCircleCheck = useIcon({ icon: 'ep:circle-check' })
+const { validForm } = useFormValid(formSmsResetPassword)
+const { handleBackLogin, getLoginState, setLoginState } = useLoginState()
+const getShow = computed(() => unref(getLoginState) === LoginStateEnum.RESET_PASSWORD)
+const captchaType = ref('blockPuzzle') // blockPuzzle 滑块 clickWord 点击文字
+
+const validatePass2 = (rule, value, callback) => {
+  if (value === '') {
+    callback(new Error('请再次输入密码'))
+  } else if (value !== resetPasswordData.password) {
+    callback(new Error('两次输入密码不一致!'))
+  } else {
+    callback()
+  }
+}
+
+const rules = {
+  tenantName: [{ required: true, min: 2, max: 20, trigger: 'blur', message: '长度为4到16位' }],
+  mobile: [{ required: true, min: 11, max: 11, trigger: 'blur', message: '手机号长度为11位' }],
+  password: [
+    {
+      required: true,
+      min: 4,
+      max: 16,
+      validator: validatePass2,
+      trigger: 'blur',
+      message: '密码长度为4到16位'
+    }
+  ],
+  check_password: [{ required: true, validator: validatePass2, trigger: 'blur' }],
+  code: [required]
+}
+
+const resetPasswordData = reactive({
+  captchaEnable: import.meta.env.VITE_APP_CAPTCHA_ENABLE,
+  tenantEnable: import.meta.env.VITE_APP_TENANT_ENABLE,
+  tenantName: '',
+  username: '',
+  password: '',
+  check_password: '',
+  mobile: '',
+  code: ''
+})
+
+const smsVO = reactive({
+  tenantName: '',
+  mobile: '',
+  captchaVerification: '',
+  scene: 23
+})
+const mobileCodeTimer = ref(0)
+const redirect = ref<string>('')
+
+// 获取验证码
+const getCode = async () => {
+  // 情况一,未开启:则直接发送验证码
+  if (resetPasswordData.captchaEnable === 'false') {
+    await getSmsCode({})
+  } else {
+    // 情况二,已开启:则展示验证码;只有完成验证码的情况,才进行发送验证码
+    // 弹出验证码
+    verify.value.show()
+  }
+}
+
+const getSmsCode = async (params) => {
+  if (resetPasswordData.tenantEnable === 'true') {
+    await getTenantId()
+  }
+  smsVO.captchaVerification = params.captchaVerification
+  smsVO.mobile = resetPasswordData.mobile
+  await sendSmsCode(smsVO).then(async () => {
+    message.success(t('login.SmsSendMsg'))
+    // 设置倒计时
+    mobileCodeTimer.value = 60
+    let msgTimer = setInterval(() => {
+      mobileCodeTimer.value = mobileCodeTimer.value - 1
+      if (mobileCodeTimer.value <= 0) {
+        clearInterval(msgTimer)
+      }
+    }, 1000)
+  })
+}
+watch(
+  () => currentRoute.value,
+  (route: RouteLocationNormalizedLoaded) => {
+    redirect.value = route?.query?.redirect as string
+  },
+  {
+    immediate: true
+  }
+)
+
+const getTenantId = async () => {
+  if (resetPasswordData.tenantEnable === 'true') {
+    const res = await LoginApi.getTenantIdByName(resetPasswordData.tenantName)
+    if (res == null) {
+      message.error(t('login.invalidTenantName'))
+      throw t('login.invalidTenantName')
+    }
+    authUtil.setTenantId(res)
+  }
+}
+
+// 重置密码
+const resetPassword = async () => {
+  const data = await validForm()
+  if (!data) return
+  await getTenantId()
+  loginLoading.value = true
+  await smsResetPassword(resetPasswordData)
+    .then(async () => {
+      message.success(t('login.resetPasswordSuccess'))
+      setLoginState(LoginStateEnum.LOGIN)
+    })
+    .catch(() => {})
+    .finally(() => {
+      loginLoading.value = false
+      setTimeout(() => {
+        const loadingInstance = ElLoading.service()
+        loadingInstance.close()
+      }, 400)
+    })
+}
+</script>
+
+<style lang="scss" scoped>
+:deep(.anticon) {
+  &:hover {
+    color: var(--el-color-primary) !important;
+  }
+}
+
+.smsbtn {
+  margin-top: 33px;
+}
+</style>

+ 2 - 1
src/views/Login/components/LoginForm.vue

@@ -59,7 +59,8 @@
               </el-checkbox>
             </el-col>
             <el-col :offset="6" :span="12">
-              <el-link style="float: right" type="primary">{{ t('login.forgetPassword') }}</el-link>
+              <el-link style="float: right" type="primary" @click="setLoginState(LoginStateEnum.RESET_PASSWORD)">{{
+                t('login.forgetPassword') }}</el-link>
             </el-col>
           </el-row>
         </el-form-item>

+ 2 - 1
src/views/Login/components/index.ts

@@ -4,5 +4,6 @@ import LoginFormTitle from './LoginFormTitle.vue'
 import RegisterForm from './RegisterForm.vue'
 import QrCodeForm from './QrCodeForm.vue'
 import SSOLoginVue from './SSOLogin.vue'
+import ForgetPasswordForm from './ForgetPasswordForm.vue'
 
-export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue }
+export { LoginForm, MobileForm, LoginFormTitle, RegisterForm, QrCodeForm, SSOLoginVue, ForgetPasswordForm }