Browse Source

feat: add sliding verification to the login form (#4461)

Vben 6 months ago
parent
commit
dac80703d9

+ 9 - 2
apps/web-antd/src/views/_core/authentication/login.vue

@@ -2,9 +2,9 @@
 import type { VbenFormSchema } from '@vben/common-ui';
 import type { BasicOption } from '@vben/types';
 
-import { computed } from 'vue';
+import { computed, markRaw } from 'vue';
 
-import { AuthenticationLogin, z } from '@vben/common-ui';
+import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import { useAuthStore } from '#/store';
@@ -78,6 +78,13 @@ const formSchema = computed((): VbenFormSchema[] => {
       label: $t('authentication.password'),
       rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
     },
+    {
+      component: markRaw(SliderCaptcha),
+      fieldName: 'captcha',
+      rules: z.boolean().refine((value) => value, {
+        message: $t('authentication.verifyRequiredTip'),
+      }),
+    },
   ];
 });
 </script>

+ 9 - 2
apps/web-ele/src/views/_core/authentication/login.vue

@@ -2,9 +2,9 @@
 import type { VbenFormSchema } from '@vben/common-ui';
 import type { BasicOption } from '@vben/types';
 
-import { computed } from 'vue';
+import { computed, markRaw } from 'vue';
 
-import { AuthenticationLogin, z } from '@vben/common-ui';
+import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import { useAuthStore } from '#/store';
@@ -78,6 +78,13 @@ const formSchema = computed((): VbenFormSchema[] => {
       label: $t('authentication.password'),
       rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
     },
+    {
+      component: markRaw(SliderCaptcha),
+      fieldName: 'captcha',
+      rules: z.boolean().refine((value) => value, {
+        message: $t('authentication.verifyRequiredTip'),
+      }),
+    },
   ];
 });
 </script>

+ 1 - 0
apps/web-naive/src/adapter/index.ts

@@ -1 +1,2 @@
 export * from './form';
+export * from './naive';

+ 0 - 0
apps/web-naive/src/naive.ts → apps/web-naive/src/adapter/naive.ts


+ 1 - 1
apps/web-naive/src/api/request.ts

@@ -10,7 +10,7 @@ import {
 } from '@vben/request';
 import { useAccessStore } from '@vben/stores';
 
-import { message } from '#/naive';
+import { message } from '#/adapter';
 import { useAuthStore } from '#/store';
 
 import { refreshTokenApi } from './core';

+ 1 - 1
apps/web-naive/src/router/access.ts

@@ -6,10 +6,10 @@ import type {
 import { generateAccessible } from '@vben/access';
 import { preferences } from '@vben/preferences';
 
+import { message } from '#/adapter';
 import { getAllMenusApi } from '#/api';
 import { BasicLayout, IFrameView } from '#/layouts';
 import { $t } from '#/locales';
-import { message } from '#/naive';
 
 const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
 

+ 1 - 1
apps/web-naive/src/store/auth.ts

@@ -9,9 +9,9 @@ import { resetAllStores, useAccessStore, useUserStore } from '@vben/stores';
 
 import { defineStore } from 'pinia';
 
+import { notification } from '#/adapter';
 import { getAccessCodesApi, getUserInfoApi, loginApi, logoutApi } from '#/api';
 import { $t } from '#/locales';
-import { notification } from '#/naive';
 
 export const useAuthStore = defineStore('auth', () => {
   const accessStore = useAccessStore();

+ 9 - 2
apps/web-naive/src/views/_core/authentication/login.vue

@@ -2,9 +2,9 @@
 import type { VbenFormSchema } from '@vben/common-ui';
 import type { BasicOption } from '@vben/types';
 
-import { computed } from 'vue';
+import { computed, markRaw } from 'vue';
 
-import { AuthenticationLogin, z } from '@vben/common-ui';
+import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import { useAuthStore } from '#/store';
@@ -78,6 +78,13 @@ const formSchema = computed((): VbenFormSchema[] => {
       label: $t('authentication.password'),
       rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
     },
+    {
+      component: markRaw(SliderCaptcha),
+      fieldName: 'captcha',
+      rules: z.boolean().refine((value) => value, {
+        message: $t('authentication.verifyRequiredTip'),
+      }),
+    },
   ];
 });
 </script>

+ 1 - 1
packages/@core/ui-kit/shadcn-ui/src/components/ui/select/SelectTrigger.vue

@@ -27,7 +27,7 @@ const forwardedProps = useForwardProps(delegatedProps);
     v-bind="forwardedProps"
     :class="
       cn(
-        'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
+        'border-input ring-offset-background placeholder:text-muted-foreground focus:ring-ring flex h-10 w-full items-center justify-between whitespace-nowrap rounded-md border bg-transparent px-3 py-2 text-sm shadow-sm focus:outline-none focus:ring-1 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1',
         props.class,
       )
     "

+ 4 - 2
packages/effects/common-ui/src/components/captcha/slider-captcha/index.vue

@@ -70,9 +70,9 @@ watchEffect(() => {
 });
 
 function getEventPageX(e: MouseEvent | TouchEvent): number {
-  if (e instanceof MouseEvent) {
+  if ('pageX' in e) {
     return e.pageX;
-  } else if (e instanceof TouchEvent && e.touches[0]) {
+  } else if ('touches' in e && e.touches[0]) {
     return e.touches[0].pageX;
   }
   return 0;
@@ -183,6 +183,8 @@ function resume() {
   const barEl = unref(barRef);
   const contentEl = unref(contentRef);
   if (!actionEl || !barEl || !contentEl) return;
+
+  contentEl.getEl().style.width = '100%';
   state.toLeft = true;
   useTimeoutFn(() => {
     state.toLeft = false;

+ 7 - 2
packages/effects/common-ui/src/components/captcha/slider-rotate-captcha/index.vue

@@ -66,6 +66,10 @@ const getImgWrapStyleRef = computed(() => {
 
 const getFactorRef = computed(() => {
   const { maxDegree, minDegree } = props;
+  if (minDegree > maxDegree) {
+    console.warn('minDegree should not be greater than maxDegree');
+  }
+
   if (minDegree === maxDegree) {
     return Math.floor(1 + Math.random() * 1) / 10 + 1;
   }
@@ -116,6 +120,7 @@ function handleDragEnd() {
     checkPass();
   }
   state.showTip = true;
+  state.dragging = false;
 }
 
 function setImgRotate(deg: number) {
@@ -162,7 +167,7 @@ defineExpose({
   <div class="relative flex flex-col items-center">
     <div
       :style="getImgWrapStyleRef"
-      class="border-border relative overflow-hidden rounded-full border shadow-md"
+      class="border-border relative cursor-pointer overflow-hidden rounded-full border shadow-md"
     >
       <img
         :class="imgCls"
@@ -185,7 +190,7 @@ defineExpose({
         >
           {{ verifyTip }}
         </div>
-        <div v-if="!state.showTip && !state.dragging" class="bg-black/30">
+        <div v-if="!state.dragging" class="bg-black/30">
           {{ defaultTip || $t('ui.captcha.sliderRotateDefaultTip') }}
         </div>
       </div>

+ 1 - 0
packages/locales/src/langs/en-US.json

@@ -103,6 +103,7 @@
     "usernameTip": "Please enter username",
     "passwordErrorTip": "Password is incorrect",
     "passwordTip": "Please enter password",
+    "verifyRequiredTip": "Please complete the verification first",
     "rememberMe": "Remember Me",
     "createAnAccount": "Create an Account",
     "createAccount": "Create Account",

+ 1 - 0
packages/locales/src/langs/zh-CN.json

@@ -102,6 +102,7 @@
     "password": "密码",
     "usernameTip": "请输入用户名",
     "passwordTip": "请输入密码",
+    "verifyRequiredTip": "请先完成验证",
     "passwordErrorTip": "密码错误",
     "rememberMe": "记住账号",
     "createAnAccount": "创建一个账号",

+ 9 - 2
playground/src/views/_core/authentication/login.vue

@@ -2,9 +2,9 @@
 import type { VbenFormSchema } from '@vben/common-ui';
 import type { BasicOption } from '@vben/types';
 
-import { computed } from 'vue';
+import { computed, markRaw } from 'vue';
 
-import { AuthenticationLogin, z } from '@vben/common-ui';
+import { AuthenticationLogin, SliderCaptcha, z } from '@vben/common-ui';
 import { $t } from '@vben/locales';
 
 import { useAuthStore } from '#/store';
@@ -95,6 +95,13 @@ const formSchema = computed((): VbenFormSchema[] => {
       label: $t('authentication.password'),
       rules: z.string().min(1, { message: $t('authentication.passwordTip') }),
     },
+    {
+      component: markRaw(SliderCaptcha),
+      fieldName: 'captcha',
+      rules: z.boolean().refine((value) => value, {
+        message: $t('authentication.verifyRequiredTip'),
+      }),
+    },
   ];
 });
 </script>

File diff suppressed because it is too large
+ 135 - 368
pnpm-lock.yaml


+ 1 - 1
pnpm-workspace.yaml

@@ -134,7 +134,7 @@ catalog:
   radix-vue: ^1.9.6
   resolve.exports: ^2.0.2
   rimraf: ^6.0.1
-  rollup: ^4.22.2
+  rollup: ^4.22.4
   rollup-plugin-visualizer: ^5.12.0
   sass: ^1.79.3
   sortablejs: ^1.15.3

Some files were not shown because too many files changed in this diff