فهرست منبع

feat(系统架构): 重构登录验证码功能

shizhongming 2 سال پیش
والد
کامیت
fa8bd09374

+ 8 - 0
src/App.vue

@@ -18,6 +18,8 @@
   import { computed } from 'vue';
   import { ExceptionModal } from '@/views/sys/exception';
 
+  import { useAppStore } from '@/store/modules/app';
+
   // support Multi-language
   const { getAntdLocale } = useLocale();
 
@@ -39,4 +41,10 @@
   );
   // Listening to page changes and dynamically changing site titles
   useTitle();
+
+  /**
+   * 初始化系统参数
+   */
+  const appStore = useAppStore();
+  appStore.initSystemProperties();
 </script>

+ 15 - 0
src/api/sys/app.ts

@@ -0,0 +1,15 @@
+import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
+
+enum Api {
+  getAuthProperties = 'public/auth/getAuthProperties',
+}
+
+/**
+ * 获取认证参数
+ */
+export const getAuthPropertiesApi = () => {
+  return defHttp.post({
+    service: ApiServiceEnum.SMART_AUTH,
+    url: Api.getAuthProperties,
+  });
+};

+ 1 - 1
src/api/sys/model/userModel.ts

@@ -4,7 +4,7 @@
 export interface LoginParams {
   username: string;
   password: string;
-  codeKey: string;
+  key: string;
   code: string;
 }
 

+ 3 - 0
src/components/Verify/index.ts

@@ -1,7 +1,10 @@
 import { withInstall } from '@/utils';
 import basicDragVerify from './src/DragVerify.vue';
 import rotateDragVerify from './src/ImgRotate.vue';
+import textCaptcha from './src/TextCaptcha.vue';
 
 export const BasicDragVerify = withInstall(basicDragVerify);
 export const RotateDragVerify = withInstall(rotateDragVerify);
 export * from './src/typing';
+
+export const TextCaptcha = withInstall(textCaptcha);

+ 55 - 0
src/components/Verify/src/TextCaptcha.vue

@@ -0,0 +1,55 @@
+<template>
+  <Tooltip>
+    <template #title>{{ t('component.verify.refresh') }}</template>
+    <img :height="height" :src="imageSrc" @click="refresh" />
+  </Tooltip>
+</template>
+
+<script setup lang="ts">
+  import { Tooltip } from 'ant-design-vue';
+  import { useI18n } from '@/hooks/web/useI18n';
+  import { propTypes } from '@/utils/propTypes';
+  import { computed, ref, unref } from 'vue';
+
+  const props = defineProps({
+    height: propTypes.string.def('px'),
+    api: {
+      type: Function as PropType<() => Promise<any>>,
+      required: true,
+    },
+  });
+
+  const emit = defineEmits(['after-refresh']);
+
+  const { t } = useI18n();
+
+  const captchaData = ref<Recordable>({});
+
+  const imageSrc = computed(() => {
+    return unref(captchaData).text?.image;
+  });
+
+  const refresh = async () => {
+    captchaData.value = await props.api();
+    emit('after-refresh', unref(captchaData));
+  };
+  refresh();
+
+  const createValidateParameter = (data) => {
+    const { key, type } = unref(captchaData);
+
+    return {
+      key,
+      type,
+      text: {
+        code: data,
+      },
+    };
+  };
+  defineExpose({
+    refresh,
+    createValidateParameter,
+  });
+</script>
+
+<style scoped lang="less"></style>

+ 8 - 0
src/components/registerGlobComp.ts

@@ -16,6 +16,10 @@ import {
 } from 'ant-design-vue';
 import VXETable from 'vxe-table';
 
+import ExcelJS from 'exceljs';
+import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx';
+import { VXETablePluginAntd } from '@/components/SmartTable/VXETablePluginAntd';
+
 import { i18n } from '@/locales/setupI18n';
 
 export function registerGlobComp(app: App) {
@@ -34,6 +38,10 @@ export function registerGlobComp(app: App) {
       return key;
     },
   });
+  VXETable.use(VXETablePluginAntd);
+  VXETable.use(VXETablePluginExportXLSX, {
+    ExcelJS,
+  });
   app
     .use(Input)
     .use(Button)

+ 2 - 1
src/locales/lang/zh-CN/component.json

@@ -123,6 +123,7 @@
     "time": "验证校验成功,耗时{time}秒!",
     "redoTip": "点击图片可刷新",
     "dragText": "请按住滑块拖动",
-    "successText": "验证通过"
+    "successText": "验证通过",
+    "refresh": "点击刷新验证码"
   }
 }

+ 17 - 1
src/store/modules/app.ts

@@ -6,7 +6,7 @@ import type {
   MultiTabsSetting,
   SizeConfig,
 } from '#/config';
-import type { BeforeMiniState, ApiAddress } from '#/store';
+import type { BeforeMiniState, ApiAddress, SystemProperties } from '#/store';
 
 import { defineStore } from 'pinia';
 import { store } from '@/store';
@@ -18,6 +18,8 @@ import { darkMode } from '@/settings/designSetting';
 import { resetRouter } from '@/router';
 import { deepMerge } from '@/utils';
 
+import { getAuthPropertiesApi } from '@/api/sys/app';
+
 interface AppState {
   darkMode?: ThemeEnum;
   // Page loading status
@@ -26,6 +28,7 @@ interface AppState {
   projectConfig: ProjectConfig | null;
   // When the window shrinks, remember some states, and restore these states when the window is restored
   beforeMiniInfo: BeforeMiniState;
+  systemProperties: SystemProperties;
 }
 let timeId: TimeoutHandle;
 export const useAppStore = defineStore({
@@ -35,6 +38,7 @@ export const useAppStore = defineStore({
     pageLoading: false,
     projectConfig: Persistent.getLocal(PROJ_CFG_KEY),
     beforeMiniInfo: {},
+    systemProperties: {},
   }),
   getters: {
     getPageLoading(state): boolean {
@@ -70,6 +74,9 @@ export const useAppStore = defineStore({
     getSizeSetting(): SizeConfig {
       return this.getProjectConfig.sizeConfig;
     },
+    getSystemProperties(state): SystemProperties {
+      return state.systemProperties;
+    },
   },
   actions: {
     setPageLoading(loading: boolean): void {
@@ -113,6 +120,15 @@ export const useAppStore = defineStore({
     setApiAddress(config: ApiAddress): void {
       localStorage.setItem(API_ADDRESS, JSON.stringify(config));
     },
+    /**
+     * 初始化系统参数
+     */
+    async initSystemProperties() {
+      const authProperties = await getAuthPropertiesApi();
+      this.systemProperties = {
+        ...authProperties,
+      };
+    },
   },
 });
 

+ 34 - 22
src/views/sys/login/LoginForm.vue

@@ -25,7 +25,8 @@
       />
     </FormItem>
 
-    <ARow :gutter="16">
+    <!--  文本验证码  -->
+    <ARow v-if="computedUseCaptcha == 'TEXT'" :gutter="16">
       <ACol :span="16">
         <FormItem name="captcha">
           <Input
@@ -37,10 +38,12 @@
         </FormItem>
       </ACol>
       <ACol :span="8">
-        <Tooltip>
-          <template #title>{{ t('system.login.captchaRefreshTooltip') }}</template>
-          <img style="height: 40px" :src="computedCaptchaUrl" @click="handleChangeCaptcha" />
-        </Tooltip>
+        <TextCaptcha
+          ref="captchaRef"
+          @after-refresh="({ key }) => (formData.key = key)"
+          height="40px"
+          :api="getCaptchaApi"
+        />
       </ACol>
     </ARow>
 
@@ -101,15 +104,15 @@
   </Form>
 </template>
 <script lang="ts" setup>
-  import { reactive, ref, unref, computed } from 'vue';
+  import { computed, reactive, ref, unref } from 'vue';
 
-  import { Checkbox, Form, Input, Row, Col, Button, Divider, Tooltip } from 'ant-design-vue';
+  import { Button, Checkbox, Col, Divider, Form, Input, Row } from 'ant-design-vue';
   import {
-    GithubFilled,
-    WechatFilled,
     AlipayCircleFilled,
+    GithubFilled,
     GoogleCircleFilled,
     TwitterCircleFilled,
+    WechatFilled,
   } from '@ant-design/icons-vue';
   import LoginFormTitle from './LoginFormTitle.vue';
 
@@ -117,13 +120,16 @@
   import { useMessage } from '@/hooks/web/useMessage';
 
   import { useUserStore } from '@/store/modules/user';
-  import { LoginStateEnum, useLoginState, useFormRules, useFormValid } from './useLogin';
+  import { LoginStateEnum, useFormRules, useFormValid, useLoginState } from './useLogin';
   import { useDesign } from '@/hooks/web/useDesign';
-  import { buildUUID } from '@/utils/uuid';
   import { ApiServiceEnum, defHttp } from '@/utils/http/axios';
   import { createPassword } from '@/utils/auth';
+  import { useAppStore } from '@/store/modules/app';
+  import { TextCaptcha } from '@/components/Verify';
   //import { onKeyStroke } from '@vueuse/core';
 
+  const captchaRef = ref();
+
   const ACol = Col;
   const ARow = Row;
   const FormItem = Form.Item;
@@ -132,6 +138,7 @@
   const { notification, createErrorModal } = useMessage();
   const { prefixCls } = useDesign('login');
   const userStore = useUserStore();
+  const appStore = useAppStore();
 
   const { setLoginState, getLoginState } = useLoginState();
   const { getFormRules } = useFormRules();
@@ -140,11 +147,18 @@
   const loading = ref(false);
   const rememberMe = ref(false);
 
+  /**
+   * 是否使用验证码
+   */
+  const computedUseCaptcha = computed(() => {
+    return appStore.systemProperties.captchaIdent;
+  });
+
   const formData = reactive({
     account: 'admin',
     password: '123456',
     captcha: '',
-    captchaKey: buildUUID(),
+    key: '',
   });
 
   const { validForm } = useFormValid(formRef);
@@ -162,8 +176,8 @@
         password: createPassword(data.account, data.password),
         username: data.account,
         mode: 'none', //不要默认的错误提示
-        codeKey: formData.captchaKey,
-        code: formData.captcha,
+        key: formData.key,
+        code: JSON.stringify(unref(captchaRef).createValidateParameter(formData.captcha)),
       });
       if (userInfo) {
         notification.success({
@@ -178,18 +192,16 @@
         content: (error as unknown as Error).message || t('sys.api.networkExceptionMsg'),
         getContainer: () => document.body.querySelector(`.${prefixCls}`) || document.body,
       });
-      handleChangeCaptcha();
+      unref(captchaRef).refresh();
     } finally {
       loading.value = false;
     }
   }
 
-  const computedCaptchaUrl = computed(() => {
-    return `${defHttp.getApiUrlByService(ApiServiceEnum.SMART_AUTH)}/auth/createCaptcha?codeKey=${
-      formData.captchaKey
-    }`;
-  });
-  const handleChangeCaptcha = () => {
-    formData.captchaKey = buildUUID();
+  const getCaptchaApi = () => {
+    return defHttp.post({
+      service: ApiServiceEnum.SMART_AUTH,
+      url: 'auth/createCaptcha',
+    });
   };
 </script>

+ 20 - 0
types/store.d.ts

@@ -56,3 +56,23 @@ export interface TableSetting {
   columns: Recordable<Nullable<Array<ColumnOptionsType>>>;
   showRowSelection: Recordable<Nullable<boolean>>;
 }
+
+/**
+ * 后台的系统参数
+ */
+export interface SystemProperties {
+  /**
+   * 是否启用验证码
+   */
+  captchaEnabled?: boolean;
+
+  /**
+   * 验证码类型
+   */
+  captchaType?: string;
+
+  /**
+   * 验证码标记
+   */
+  captchaIdent?: string;
+}