Преглед изворни кода

新增用户名密码登录支持滑块拼图验证

zhontai пре 2 година
родитељ
комит
c7c785c2a2

+ 7 - 37
src/api/admin/Auth.ts

@@ -9,14 +9,13 @@
  * ---------------------------------------------------------------
  */
 
-import { AxiosResponse } from 'axios'
 import {
   AuthLoginInput,
   ResultOutputAuthGetPasswordEncryptKeyOutput,
   ResultOutputAuthGetUserInfoOutput,
   ResultOutputAuthGetUserPermissionsOutput,
   ResultOutputAuthUserProfileDto,
-  ResultOutputCaptchaOutput,
+  ResultOutputBoolean,
   ResultOutputListAuthUserMenuDto,
   ResultOutputObject,
 } from './data-contracts'
@@ -155,46 +154,17 @@ export class AuthApi<SecurityDataType = unknown> extends HttpClient<SecurityData
    * No description
    *
    * @tags auth
-   * @name GetCaptcha
-   * @summary 获取验证数据
-   * @request GET:/api/admin/auth/get-captcha
+   * @name IsCaptcha
+   * @summary 是否开启验证码
+   * @request GET:/api/admin/auth/is-captcha
    * @secure
    */
-  getCaptcha = (params: RequestParams = {}) =>
-    this.request<ResultOutputCaptchaOutput, any>({
-      path: `/api/admin/auth/get-captcha`,
+  isCaptcha = (params: RequestParams = {}) =>
+    this.request<ResultOutputBoolean, any>({
+      path: `/api/admin/auth/is-captcha`,
       method: 'GET',
       secure: true,
       format: 'json',
       ...params,
     })
-  /**
-   * No description
-   *
-   * @tags auth
-   * @name CheckCaptcha
-   * @summary 检查验证数据
-   * @request GET:/api/admin/auth/check-captcha
-   * @secure
-   */
-  checkCaptcha = (
-    query?: {
-      /** 校验唯一标识 */
-      Token?: string
-      /** 缓存键 */
-      CaptchaKey?: string
-      /** 删除缓存 */
-      DeleteCache?: boolean
-      /** 数据 */
-      Data?: string
-    },
-    params: RequestParams = {}
-  ) =>
-    this.request<AxiosResponse, any>({
-      path: `/api/admin/auth/check-captcha`,
-      method: 'GET',
-      query: query,
-      secure: true,
-      ...params,
-    })
 }

+ 3 - 1
src/api/admin/Captcha.ts

@@ -24,6 +24,7 @@ export class CaptchaApi<SecurityDataType = unknown> extends HttpClient<SecurityD
    */
   generate = (
     query?: {
+      /** 验证码id */
       captchaId?: string
     },
     params: RequestParams = {}
@@ -48,7 +49,8 @@ export class CaptchaApi<SecurityDataType = unknown> extends HttpClient<SecurityD
   check = (
     data: SlideTrack,
     query?: {
-      id?: string
+      /** 验证码id */
+      captchaId?: string
     },
     params: RequestParams = {}
   ) =>

+ 9 - 25
src/api/admin/data-contracts.ts

@@ -244,7 +244,10 @@ export interface AuthLoginInput {
   password: string
   /** 密码键 */
   passwordKey?: string | null
-  captcha?: CaptchaInput
+  /** 验证码Id */
+  captchaId?: string | null
+  /** 验证码数据 */
+  captchaData?: string | null
 }
 
 export interface AuthUserMenuDto {
@@ -311,25 +314,6 @@ export interface CaptchaData {
   sliderImage?: string | null
 }
 
-export interface CaptchaInput {
-  /** 校验唯一标识 */
-  token?: string | null
-  /** 缓存键 */
-  captchaKey?: string | null
-  /** 删除缓存 */
-  deleteCache?: boolean
-  /** 数据 */
-  data?: string | null
-}
-
-/** 验证数据 */
-export interface CaptchaOutput {
-  /** 校验唯一标识 */
-  token?: string | null
-  /** 数据 */
-  data?: any
-}
-
 /**
  * 数据范围:All=1,DeptWithChild=2,Dept=3,Self=4,Custom=5
  * @format int32
@@ -2112,26 +2096,26 @@ export interface ResultOutputAuthUserProfileDto {
 }
 
 /** 结果输出 */
-export interface ResultOutputCaptchaData {
+export interface ResultOutputBoolean {
   /** 是否成功标记 */
   success?: boolean
   /** 编码 */
   code?: string | null
   /** 消息 */
   msg?: string | null
-  data?: CaptchaData
+  /** 数据 */
+  data?: boolean
 }
 
 /** 结果输出 */
-export interface ResultOutputCaptchaOutput {
+export interface ResultOutputCaptchaData {
   /** 是否成功标记 */
   success?: boolean
   /** 编码 */
   code?: string | null
   /** 消息 */
   msg?: string | null
-  /** 验证数据 */
-  data?: CaptchaOutput
+  data?: CaptchaData
 }
 
 /** 结果输出 */

+ 23 - 0
src/components/my-captcha/dialog.vue

@@ -0,0 +1,23 @@
+<template>
+  <el-dialog class="my-captcha" title="请完成安全验证" draggable append-to-body width="380px" v-bind="$attrs">
+    <MyCaptcha ref="myCaptchaRef" v-bind="$attrs" />
+  </el-dialog>
+</template>
+
+<script lang="ts" setup name="my-captcha-dialog">
+import { defineAsyncComponent, ref } from 'vue'
+
+const MyCaptcha = defineAsyncComponent(() => import('./index.vue'))
+
+const myCaptchaRef = ref()
+
+const refresh = () => {
+  myCaptchaRef.value?.refresh()
+}
+
+defineExpose({
+  refresh,
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 75 - 0
src/components/my-captcha/index.vue

@@ -0,0 +1,75 @@
+<template>
+  <SlideCaptcha
+    ref="slideCaptchaRef"
+    :fail-tip="state.failTip"
+    :success-tip="state.successTip"
+    width="100%"
+    height="auto"
+    @refresh="onGenerate"
+    @finish="onFinish"
+    v-bind="$attrs"
+  />
+</template>
+
+<script lang="ts" setup name="my-captcha">
+import { defineAsyncComponent, ref, reactive } from 'vue'
+import { CaptchaApi } from '/@/api/admin/Captcha'
+
+const emits = defineEmits(['ok'])
+
+const SlideCaptcha = defineAsyncComponent(() => import('./slide-captcha.vue'))
+
+const slideCaptchaRef = ref()
+
+const state = reactive({
+  requestId: '',
+  failTip: '',
+  successTip: '',
+})
+
+//生成滑块验证码
+const onGenerate = async () => {
+  slideCaptchaRef.value.startRequestGenerate()
+  const res = await new CaptchaApi().generate({ captchaId: state.requestId }).catch(() => {
+    slideCaptchaRef.value.endRequestGenerate(null, null)
+  })
+  if (res?.success && res.data) {
+    state.requestId = res.data.id || ''
+    slideCaptchaRef.value.endRequestGenerate(res.data.backgroundImage, res.data.sliderImage)
+  }
+}
+
+//验证滑块验证码
+const onFinish = async (data: any) => {
+  slideCaptchaRef.value.startRequestVerify()
+  const res = await new CaptchaApi().check(data, { captchaId: state.requestId }).catch(() => {
+    state.failTip = '服务异常,请稍后重试'
+    slideCaptchaRef.value.endRequestVerify(false)
+  })
+  if (res?.success && res.data) {
+    let success = res.data.result === 0
+    state.failTip = res.data.result == 1 ? '验证未通过,拖动滑块将悬浮图像正确合并' : '验证超时, 请重新操作'
+    state.successTip = '验证通过'
+    slideCaptchaRef.value.endRequestVerify(success)
+    if (success) {
+      //验证成功
+      emits('ok', { captchaId: state.requestId, captchaData: data })
+    } else {
+      setTimeout(() => {
+        onGenerate()
+      }, 1000)
+    }
+  }
+}
+
+//刷新滑块验证码
+const refresh = () => {
+  slideCaptchaRef.value?.handleRefresh()
+}
+
+defineExpose({
+  refresh,
+})
+</script>
+
+<style scoped lang="scss"></style>

+ 0 - 0
src/components/my-slide-captcha/index.vue → src/components/my-captcha/slide-captcha.vue


+ 11 - 54
src/components/my-input-code/index.vue

@@ -22,27 +22,16 @@
         />
       </template>
     </el-input>
-    <el-dialog ref="dialogRef" v-model="state.showDialog" class="my-captcha" title="请完成安全验证" draggable append-to-body width="380px">
-      <MySlideCaptcha
-        ref="slideCaptchaRef"
-        :fail-tip="state.failTip"
-        :success-tip="state.successTip"
-        width="100%"
-        height="auto"
-        @refresh="onRefresh"
-        @finish="onFinish"
-      />
-    </el-dialog>
+    <MyCaptchaDialog ref="myCaptchaDialogRef" v-model="state.showDialog" @ok="onOk" />
   </div>
 </template>
 
 <script lang="ts" setup name="my-input-code">
-import { ref, reactive, defineAsyncComponent } from 'vue'
-import { CaptchaApi } from '/@/api/admin/Captcha'
+import { reactive, defineAsyncComponent, ref } from 'vue'
 import { isMobile } from '/@/utils/test'
 import { ElMessage } from 'element-plus'
 
-const MySlideCaptcha = defineAsyncComponent(() => import('/@/components/my-slide-captcha/index.vue'))
+const MyCaptchaDialog = defineAsyncComponent(() => import('/@/components/my-captcha/dialog.vue'))
 
 const props = defineProps({
   seconds: {
@@ -71,8 +60,7 @@ const props = defineProps({
   },
 })
 
-const slideCaptchaRef = ref()
-const dialogRef = ref()
+const myCaptchaDialogRef = ref()
 
 const state = reactive({
   status: 'ready',
@@ -83,8 +71,6 @@ const state = reactive({
 
   showDialog: false,
   requestId: '',
-  failTip: '',
-  successTip: '',
 })
 
 const startCountdown = () => {
@@ -106,40 +92,11 @@ const onChange = (value: number) => {
   if (value < 1000) state.status = 'finish'
 }
 
-//刷新滑块验证码
-const onRefresh = async () => {
-  slideCaptchaRef.value.startRequestGenerate()
-  const res = await new CaptchaApi().generate({ captchaId: state.requestId }).catch(() => {
-    slideCaptchaRef.value.endRequestGenerate(null, null)
-  })
-  if (res?.success && res.data) {
-    state.requestId = res.data.id || ''
-    slideCaptchaRef.value.endRequestGenerate(res.data.backgroundImage, res.data.sliderImage)
-  }
-}
-
-//验证滑块验证码
-const onFinish = async (data: any) => {
-  slideCaptchaRef.value.startRequestVerify()
-  const res = await new CaptchaApi().check(data, { id: state.requestId }).catch(() => {
-    state.failTip = '服务异常,请稍后重试'
-    slideCaptchaRef.value.endRequestVerify(false)
-  })
-  if (res?.success && res.data) {
-    let success = res.data.result === 0
-    state.failTip = res.data.result == 1 ? '验证未通过,拖动滑块将悬浮图像正确合并' : '验证超时, 请重新操作'
-    state.successTip = '验证通过'
-    slideCaptchaRef.value.endRequestVerify(success)
-    if (success) {
-      state.showDialog = false
-      startCountdown()
-      //发送短信验证码
-    } else {
-      setTimeout(() => {
-        onRefresh()
-      }, 1000)
-    }
-  }
+//验证通过 data: any
+const onOk = () => {
+  state.showDialog = false
+  startCountdown()
+  //发送短信验证码
 }
 
 //获得验证码
@@ -151,8 +108,8 @@ const onGetCode = () => {
   }
 
   state.showDialog = true
-  //刷新验证码
-  slideCaptchaRef.value?.handleRefresh()
+  //刷新滑块拼图
+  myCaptchaDialogRef.value?.refresh()
 }
 </script>
 

+ 106 - 60
src/views/admin/login/component/account.vue

@@ -1,41 +1,42 @@
 <template>
-  <el-form ref="formRef" :model="state.ruleForm" size="large" class="login-content-form">
-    <el-form-item class="login-animation1" prop="userName" :rules="[{ required: true, message: '请输入用户名', trigger: ['blur', 'change'] }]">
-      <el-input
-        text
-        :placeholder="$t('message.account.accountPlaceholder1')"
-        v-model="state.ruleForm.userName"
-        clearable
-        autocomplete="off"
-        @keyup.enter="onSignIn"
-      >
-        <template #prefix>
-          <el-icon class="el-input__icon"><ele-User /></el-icon>
-        </template>
-      </el-input>
-    </el-form-item>
-    <el-form-item class="login-animation2" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: ['blur', 'change'] }]">
-      <el-input
-        :type="state.isShowPassword ? 'text' : 'password'"
-        :placeholder="$t('message.account.accountPlaceholder2')"
-        v-model="state.ruleForm.password"
-        autocomplete="off"
-        @keyup.enter="onSignIn"
-      >
-        <template #prefix>
-          <el-icon class="el-input__icon"><ele-Unlock /></el-icon>
-        </template>
-        <template #suffix>
-          <i
-            class="iconfont el-input__icon login-content-password"
-            :class="state.isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
-            @click="state.isShowPassword = !state.isShowPassword"
-          >
-          </i>
-        </template>
-      </el-input>
-    </el-form-item>
-    <!-- <el-form-item class="login-animation3">
+  <div>
+    <el-form ref="formRef" :model="state.ruleForm" size="large" class="login-content-form">
+      <el-form-item class="login-animation1" prop="userName" :rules="[{ required: true, message: '请输入用户名', trigger: ['blur', 'change'] }]">
+        <el-input
+          text
+          :placeholder="$t('message.account.accountPlaceholder1')"
+          v-model="state.ruleForm.userName"
+          clearable
+          autocomplete="off"
+          @keyup.enter="onSignIn"
+        >
+          <template #prefix>
+            <el-icon class="el-input__icon"><ele-User /></el-icon>
+          </template>
+        </el-input>
+      </el-form-item>
+      <el-form-item class="login-animation2" prop="password" :rules="[{ required: true, message: '请输入密码', trigger: ['blur', 'change'] }]">
+        <el-input
+          :type="state.isShowPassword ? 'text' : 'password'"
+          :placeholder="$t('message.account.accountPlaceholder2')"
+          v-model="state.ruleForm.password"
+          autocomplete="off"
+          @keyup.enter="onSignIn"
+        >
+          <template #prefix>
+            <el-icon class="el-input__icon"><ele-Unlock /></el-icon>
+          </template>
+          <template #suffix>
+            <i
+              class="iconfont el-input__icon login-content-password"
+              :class="state.isShowPassword ? 'icon-yincangmima' : 'icon-xianshimima'"
+              @click="state.isShowPassword = !state.isShowPassword"
+            >
+            </i>
+          </template>
+        </el-input>
+      </el-form-item>
+      <!-- <el-form-item class="login-animation3">
       <el-col :span="15">
         <el-input
           text
@@ -55,16 +56,26 @@
         <el-button class="login-content-code" v-waves>1234</el-button>
       </el-col>
     </el-form-item> -->
-    <el-form-item class="login-animation4">
-      <el-button type="primary" class="login-content-submit" round v-waves @click="onSignIn" :loading="state.loading.signIn">
-        <span>{{ $t('message.account.accountBtnText') }}</span>
-      </el-button>
-    </el-form-item>
-  </el-form>
+      <el-form-item class="login-animation4">
+        <el-button
+          type="primary"
+          class="login-content-submit"
+          round
+          v-waves
+          @click="onSignIn"
+          :disabled="state.disabled.signIn"
+          :loading="state.loading.signIn"
+        >
+          <span>{{ $t('message.account.accountBtnText') }}</span>
+        </el-button>
+      </el-form-item>
+    </el-form>
+    <MyCaptchaDialog ref="myCaptchaDialogRef" v-model="state.showDialog" @ok="onOk" />
+  </div>
 </template>
 
 <script setup lang="ts" name="loginAccount">
-import { reactive, computed, ref } from 'vue'
+import { reactive, computed, ref, defineAsyncComponent } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
 import { useI18n } from 'vue-i18n'
@@ -80,6 +91,8 @@ import { AuthApi } from '/@/api/admin/Auth'
 import { AuthLoginInput } from '/@/api/admin/data-contracts'
 import { useUserInfo } from '/@/stores/userInfo'
 
+const MyCaptchaDialog = defineAsyncComponent(() => import('/@/components/my-captcha/dialog.vue'))
+
 // 定义变量内容
 const { t } = useI18n()
 // const storesThemeConfig = useThemeConfig()
@@ -88,41 +101,74 @@ const route = useRoute()
 const router = useRouter()
 const formRef = ref()
 const state = reactive({
+  showDialog: false,
   isShowPassword: false,
   ruleForm: {
     userName: 'admin',
     password: '111111',
-    //code: '1234',
+    captchaId: '',
+    captchaData: '',
   } as AuthLoginInput,
   loading: {
     signIn: false,
   },
+  disabled: {
+    signIn: false,
+  },
 })
 
 // 时间获取
 const currentTime = computed(() => {
   return formatAxis(new Date())
 })
-// 登录
+
+//验证通过
+const onOk = (data: any) => {
+  state.showDialog = false
+  //开始登录
+  state.ruleForm.captchaId = data.captchaId
+  state.ruleForm.captchaData = JSON.stringify(data.captchaData)
+  login()
+}
+
+//登录
+const login = async () => {
+  state.loading.signIn = true
+  const res = await new AuthApi().login(state.ruleForm).catch(() => {
+    state.loading.signIn = false
+  })
+  if (!res?.success) {
+    state.loading.signIn = false
+    return
+  }
+
+  const token = res.data?.token
+  useUserInfo().setToken(token)
+  // 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
+  const isNoPower = await initBackEndControlRoutes()
+  // 执行完 initBackEndControlRoutes,再执行 signInSuccess
+  signInSuccess(isNoPower)
+}
+
+// 点击登录
 const onSignIn = async () => {
   formRef.value.validate(async (valid: boolean) => {
     if (!valid) return
 
-    state.loading.signIn = true
-    const res = await new AuthApi().login(state.ruleForm).catch(() => {
-      state.loading.signIn = false
-    })
-    if (!res?.success) {
-      state.loading.signIn = false
-      return
-    }
+    //检查是否开启验证码登录
+    state.disabled.signIn = true
+    const res = await new AuthApi()
+      .isCaptcha()
+      .catch(() => {})
+      .finally(() => {
+        state.disabled.signIn = false
+      })
 
-    const token = res.data?.token
-    useUserInfo().setToken(token)
-    // 添加完动态路由,再进行 router 跳转,否则可能报错 No match found for location with path "/"
-    const isNoPower = await initBackEndControlRoutes()
-    // 执行完 initBackEndControlRoutes,再执行 signInSuccess
-    signInSuccess(isNoPower)
+    if (res?.success && res.data) {
+      state.showDialog = true
+    } else {
+      login()
+    }
   })
 }
 // 登录成功后的跳转

+ 0 - 1
src/views/admin/login/component/mobile.vue

@@ -32,7 +32,6 @@
           <span>{{ $t('message.mobile.btnText') }}</span>
         </el-button>
       </el-form-item>
-
       <div class="font12 mt30 login-animation4 login-msg">{{ $t('message.mobile.msgText') }}</div>
     </el-form>
   </div>