Browse Source

1、增加登录验证码 2、调整组件间距调整默认8px

zuohuaijun 3 years ago
parent
commit
1836f77a34
33 changed files with 2144 additions and 111 deletions
  1. 2 1
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  2. 32 0
      Admin.NET/Admin.NET.Core/Admin.NET.Core.xml
  3. 6 0
      Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs
  4. 10 0
      Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs
  5. 23 1
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  6. 68 0
      Admin.NET/Admin.NET.Core/Util/RandomCaptcha.cs
  7. 7 0
      Admin.NET/Admin.NET.Web.Core/Startup.cs
  8. 1 1
      vue-next-admin/.env.development
  9. 69 0
      vue-next-admin/src/api-services/apis/sys-auth-api.ts
  10. 3 2
      vue-next-admin/src/api-services/models/job-status-enum.ts
  11. 12 0
      vue-next-admin/src/api-services/models/login-input.ts
  12. 3 3
      vue-next-admin/src/api-services/models/login-type-enum.ts
  13. 19 1
      vue-next-admin/src/api-services/models/sys-log-ex.ts
  14. 19 1
      vue-next-admin/src/api-services/models/sys-log-op.ts
  15. 2 1
      vue-next-admin/src/api-services/models/user-type-enum.ts
  16. 300 0
      vue-next-admin/src/components/dragVerify/dragVerify.vue
  17. 466 0
      vue-next-admin/src/components/dragVerify/dragVerifyImg.vue
  18. 486 0
      vue-next-admin/src/components/dragVerify/dragVerifyImgChip.vue
  19. 449 0
      vue-next-admin/src/components/dragVerify/dragVerifyImgRotate.vue
  20. 1 1
      vue-next-admin/src/theme/app.scss
  21. 1 2
      vue-next-admin/src/utils/authDirective.ts
  22. 91 16
      vue-next-admin/src/views/login/component/account.vue
  23. 1 1
      vue-next-admin/src/views/system/log/difflog/index.vue
  24. 1 1
      vue-next-admin/src/views/system/log/exlog/index.vue
  25. 1 1
      vue-next-admin/src/views/system/log/oplog/index.vue
  26. 1 1
      vue-next-admin/src/views/system/log/vislog/index.vue
  27. 1 1
      vue-next-admin/src/views/system/menu/index.vue
  28. 2 2
      vue-next-admin/src/views/system/org/index.vue
  29. 1 1
      vue-next-admin/src/views/system/password/index.vue
  30. 1 1
      vue-next-admin/src/views/system/pos/index.vue
  31. 1 1
      vue-next-admin/src/views/system/role/index.vue
  32. 62 69
      vue-next-admin/src/views/system/server/index.vue
  33. 2 2
      vue-next-admin/src/views/system/user/index.vue

+ 2 - 1
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -27,6 +27,7 @@
     <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.6.5" />
     <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.6.5" />
     <PackageReference Include="Furion.Pure" Version="4.6.5" />
+    <PackageReference Include="Lazy.Captcha.Core" Version="1.1.6" />
     <PackageReference Include="Magicodes.IE.Excel" Version="2.6.7" />
     <PackageReference Include="Magicodes.IE.Pdf" Version="2.6.7" />
     <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="6.0.10" />
@@ -36,7 +37,7 @@
     <PackageReference Include="OnceMi.AspNetCore.OSS" Version="1.1.8" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.Api" Version="2.18.0" />
     <PackageReference Include="SKIT.FlurlHttpClient.Wechat.TenpayV3" Version="2.13.1" />
-    <PackageReference Include="SqlSugarCore" Version="5.1.3.25" />
+    <PackageReference Include="SqlSugarCore" Version="5.1.3.27" />
     <PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.20" />
     <PackageReference Include="UAParser" Version="3.1.47" />
     <PackageReference Include="Yitter.IdGenerator" Version="1.0.14" />

+ 32 - 0
Admin.NET/Admin.NET.Core/Admin.NET.Core.xml

@@ -1931,6 +1931,11 @@
             系统错误码
             </summary>
         </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D0009">
+            <summary>
+            验证码错误
+            </summary>
+        </member>
         <member name="F:Admin.NET.Core.ErrorCodeEnum.D1000">
             <summary>
             用户名或密码不正确
@@ -3609,6 +3614,16 @@
             </summary>
             <example>123456</example>
         </member>
+        <member name="P:Admin.NET.Core.Service.LoginInput.CodeId">
+            <summary>
+            验证码Id
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.Service.LoginInput.Code">
+            <summary>
+            验证码
+            </summary>
+        </member>
         <member name="T:Admin.NET.Core.Service.LoginOutput">
             <summary>
             用户登录结果
@@ -3732,6 +3747,12 @@
             退出系统
             </summary>
         </member>
+        <member name="M:Admin.NET.Core.Service.SysAuthService.GetCaptcha">
+            <summary>
+            生成图片验证码
+            </summary>
+            <returns></returns>
+        </member>
         <member name="M:Admin.NET.Core.Service.SysAuthService.SwaggerCheckUrl">
             <summary>
             Swagger登录检查
@@ -7501,6 +7522,17 @@
             序列化时long转string(防止js精度溢出)
             </summary>
         </member>
+        <member name="T:Admin.NET.Core.RandomCaptcha">
+            <summary>
+            随机验证码
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.RandomCaptcha.ChangeOptions(Lazy.Captcha.Core.CaptchaOptions)">
+            <summary>
+            更新选项
+            </summary>
+            <param name="options"></param>
+        </member>
         <member name="T:Admin.NET.Core.ReflectionUtil">
             <summary>
             反射工具类

+ 6 - 0
Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs

@@ -6,6 +6,12 @@
 [ErrorCodeType]
 public enum ErrorCodeEnum
 {
+    /// <summary>
+    /// 验证码错误
+    /// </summary>
+    [ErrorCodeItemMetadata("验证码错误")]
+    D0009,
+
     /// <summary>
     /// 用户名或密码不正确
     /// </summary>

+ 10 - 0
Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs

@@ -18,4 +18,14 @@ public class LoginInput
     /// <example>123456</example>
     [Required(ErrorMessage = "密码不能为空"), MinLength(3, ErrorMessage = "密码不能少于3个字符")]
     public string Password { get; set; }
+
+    /// <summary>
+    /// 验证码Id
+    /// </summary>
+    public long CodeId { get; set; }
+
+    /// <summary>
+    /// 验证码
+    /// </summary>
+    public string Code { get; set; }
 }

+ 23 - 1
Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs

@@ -1,4 +1,5 @@
 using Furion.SpecificationDocument;
+using Lazy.Captcha.Core;
 using Microsoft.Extensions.Caching.Memory;
 
 namespace Admin.NET.Core.Service;
@@ -19,6 +20,7 @@ public class SysAuthService : IDynamicApiController, ITransient
     private readonly SysMenuService _sysMenuService;
     private readonly ISysOnlineUserService _sysOnlineUserService;
     private readonly IMemoryCache _cache;
+    private readonly ICaptcha _captcha;
 
     public SysAuthService(SqlSugarRepository<SysUser> sysUserRep,
         IOptions<RefreshTokenOptions> refreshTokenOptions,
@@ -29,7 +31,8 @@ public class SysAuthService : IDynamicApiController, ITransient
         SysUserRoleService sysUserRoleService,
         SysMenuService sysMenuService,
         ISysOnlineUserService sysOnlineUserService,
-        IMemoryCache cache)
+        IMemoryCache cache,
+        ICaptcha captcha)
     {
         _sysUserRep = sysUserRep;
         _httpContextAccessor = httpContextAccessor;
@@ -41,6 +44,7 @@ public class SysAuthService : IDynamicApiController, ITransient
         _sysMenuService = sysMenuService;
         _sysOnlineUserService = sysOnlineUserService;
         _cache = cache;
+        _captcha = captcha;
     }
 
     /// <summary>
@@ -54,6 +58,10 @@ public class SysAuthService : IDynamicApiController, ITransient
     [SuppressMonitor]
     public async Task<LoginOutput> Login([Required] LoginInput input)
     {
+        // 判断验证码
+        if (!_captcha.Validate(input.CodeId.ToString(), input.Code))
+            throw Oops.Oh(ErrorCodeEnum.D0009);
+
         var encryptPasswod = MD5Encryption.Encrypt(input.Password); // 加密密码
 
         // 判断用户名密码
@@ -187,6 +195,20 @@ public class SysAuthService : IDynamicApiController, ITransient
         });
     }
 
+    /// <summary>
+    /// 生成图片验证码
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("/captcha")]
+    [AllowAnonymous]
+    [SuppressMonitor]
+    public dynamic GetCaptcha()
+    {
+        var codeId = Yitter.IdGenerator.YitIdHelper.NextId();
+        var captcha = _captcha.Generate(codeId.ToString());
+        return new { Id = codeId, Img = captcha.Base64 };
+    }
+
     /// <summary>
     /// Swagger登录检查
     /// </summary>

+ 68 - 0
Admin.NET/Admin.NET.Core/Util/RandomCaptcha.cs

@@ -0,0 +1,68 @@
+using Lazy.Captcha.Core;
+using Lazy.Captcha.Core.Generator;
+using Lazy.Captcha.Core.Storage;
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 随机验证码
+/// </summary>
+public class RandomCaptcha : DefaultCaptcha
+{
+    private static readonly Random random = new();
+    private static readonly CaptchaType[] captchaTypes = Enum.GetValues<CaptchaType>();
+
+    public RandomCaptcha(IOptionsSnapshot<CaptchaOptions> options, IStorage storage) : base(options, storage)
+    {
+    }
+
+    /// <summary>
+    /// 更新选项
+    /// </summary>
+    /// <param name="options"></param>
+    protected override void ChangeOptions(CaptchaOptions options)
+    {
+        // 随机验证码类型
+        options.CaptchaType = captchaTypes[random.Next(0, captchaTypes.Length)];
+
+        // 当是算数运算时,CodeLength是指运算数个数
+        if (options.CaptchaType.IsArithmetic())
+        {
+            options.CodeLength = 2;
+        }
+        else
+        {
+            options.CodeLength = 4;
+        }
+
+        // 如果包含中文时,使用kaiti字体,否则文字乱码
+        if (options.CaptchaType.ContainsChinese())
+        {
+            options.ImageOption.FontFamily = DefaultFontFamilys.Instance.Kaiti;
+            options.ImageOption.FontSize = 24;
+        }
+        else
+        {
+            options.ImageOption.FontFamily = DefaultFontFamilys.Instance.Actionj;
+        }
+
+        options.IgnoreCase = true; // 忽略大小写
+
+        options.ImageOption.Animation = random.Next(2) == 0; // 动静
+
+        options.ImageOption.InterferenceLineCount = random.Next(1, 5); // 干扰线数量
+
+        options.ImageOption.BubbleCount = random.Next(1, 5); // 气泡数量
+                                                             //options.ImageOption.BubbleMinRadius = 5; // 气泡最小半径
+                                                             //options.ImageOption.BubbleMaxRadius = 15; // 气泡最大半径
+                                                             //options.ImageOption.BubbleThickness = 1; // 气泡边沿厚度
+
+        options.ImageOption.BackgroundColor = SixLabors.ImageSharp.Color.White; // 背景色
+
+        options.ImageOption.Width = 150; // 验证码宽度
+        options.ImageOption.Height = 50; // 验证码高度
+
+        options.ImageOption.FontSize = 36; // 字体大小
+        //options.ImageOption.FontFamily = DefaultFontFamilys.Instance.Actionj; // 字体
+    }
+}

+ 7 - 0
Admin.NET/Admin.NET.Web.Core/Startup.cs

@@ -3,6 +3,7 @@ using AspNetCoreRateLimit;
 using Furion;
 using Furion.SpecificationDocument;
 using IGeekFan.AspNetCore.Knife4jUI;
+using Lazy.Captcha.Core;
 using Microsoft.AspNetCore.Builder;
 using Microsoft.AspNetCore.Hosting;
 using Microsoft.AspNetCore.HttpOverrides;
@@ -162,6 +163,12 @@ public class Startup : AppStartup
         {
             WorkerId = App.GetOptions<SnowIdOptions>().WorkerId
         });
+
+        // 验证码
+        services.AddCaptcha(App.Configuration);
+        //services.AddScoped<ICaptcha, DefaultCaptcha>();
+        services.AddScoped<ICaptcha, RandomCaptcha>();
+        //services.AddScoped<IStorage, DefaultStorage>();        
     }
 
     public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

+ 1 - 1
vue-next-admin/.env.development

@@ -1,5 +1,5 @@
 # 本地环境
 ENV = 'development'
 
-# 本地环境接口地址
+# 本地环境接口地址 // 'https://localhost:5005'
 VITE_API_URL = 'https://localhost:44326'

+ 69 - 0
vue-next-admin/src/api-services/apis/sys-auth-api.ts

@@ -18,6 +18,7 @@ import { Configuration } from '../configuration';
 import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
 import { AdminResultLoginOutput } from '../models';
 import { AdminResultLoginUserInfoOutput } from '../models';
+import { AdminResultObject } from '../models';
 import { AdminResultString } from '../models';
 import { LoginInput } from '../models';
 /**
@@ -26,6 +27,42 @@ import { LoginInput } from '../models';
  */
 export const SysAuthApiAxiosParamCreator = function (configuration?: Configuration) {
     return {
+        /**
+         * 
+         * @summary 生成图片验证码
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        captchaGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/captcha`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
         /**
          * 
          * @summary 获取刷新Token
@@ -283,6 +320,19 @@ export const SysAuthApiAxiosParamCreator = function (configuration?: Configurati
  */
 export const SysAuthApiFp = function(configuration?: Configuration) {
     return {
+        /**
+         * 
+         * @summary 生成图片验证码
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async captchaGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultObject>>> {
+            const localVarAxiosArgs = await SysAuthApiAxiosParamCreator(configuration).captchaGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
         /**
          * 
          * @summary 获取刷新Token
@@ -374,6 +424,15 @@ export const SysAuthApiFp = function(configuration?: Configuration) {
  */
 export const SysAuthApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
     return {
+        /**
+         * 
+         * @summary 生成图片验证码
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async captchaGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultObject>> {
+            return SysAuthApiFp(configuration).captchaGet(options).then((request) => request(axios, basePath));
+        },
         /**
          * 
          * @summary 获取刷新Token
@@ -442,6 +501,16 @@ export const SysAuthApiFactory = function (configuration?: Configuration, basePa
  * @extends {BaseAPI}
  */
 export class SysAuthApi extends BaseAPI {
+    /**
+     * 
+     * @summary 生成图片验证码
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAuthApi
+     */
+    public async captchaGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultObject>> {
+        return SysAuthApiFp(this.configuration).captchaGet(options).then((request) => request(this.axios, this.basePath));
+    }
     /**
      * 
      * @summary 获取刷新Token

+ 3 - 2
vue-next-admin/src/api-services/models/job-status-enum.ts

@@ -12,13 +12,14 @@
  * Do not edit the class manually.
  */
 /**
- * 岗位状态枚举<br />&nbsp;在职 On = 1<br />&nbsp;离职 Off = 2<br />&nbsp;请假 Leave = 3<br />
+ * 岗位状态枚举<br />&nbsp;在职 On = 1<br />&nbsp;离职 Off = 2<br />&nbsp;请假 Leave = 3<br />&nbsp;其他 Other = 4<br />
  * @export
  * @enum {string}
  */
 export enum JobStatusEnum {
     NUMBER_1 = 1,
     NUMBER_2 = 2,
-    NUMBER_3 = 3
+    NUMBER_3 = 3,
+    NUMBER_4 = 4
 }
 

+ 12 - 0
vue-next-admin/src/api-services/models/login-input.ts

@@ -29,4 +29,16 @@ export interface LoginInput {
      * @memberof LoginInput
      */
     password: string;
+    /**
+     * 验证码Id
+     * @type {number}
+     * @memberof LoginInput
+     */
+    codeId?: number;
+    /**
+     * 验证码
+     * @type {string}
+     * @memberof LoginInput
+     */
+    code?: string | null;
 }

+ 3 - 3
vue-next-admin/src/api-services/models/login-type-enum.ts

@@ -12,15 +12,15 @@
  * Do not edit the class manually.
  */
 /**
- * 登录类型枚举<br />&nbsp;登录 Login = 0<br />&nbsp;退出 Logout = 1<br />&nbsp;注册 Register = 2<br />&nbsp;改密 Change_password = 3<br />&nbsp;授权登陆 Authorized_login = 4<br />
+ * 登录类型枚举<br />&nbsp;登录 Login = 1<br />&nbsp;退出 Logout = 2<br />&nbsp;注册 Register = 3<br />&nbsp;改密 Change_password = 4<br />&nbsp;授权登陆 Authorized_login = 5<br />
  * @export
  * @enum {string}
  */
 export enum LoginTypeEnum {
-    NUMBER_0 = 0,
     NUMBER_1 = 1,
     NUMBER_2 = 2,
     NUMBER_3 = 3,
-    NUMBER_4 = 4
+    NUMBER_4 = 4,
+    NUMBER_5 = 5
 }
 

+ 19 - 1
vue-next-admin/src/api-services/models/sys-log-ex.ts

@@ -54,7 +54,7 @@ export interface SysLogEx {
      */
     isDelete?: boolean;
     /**
-     * 日志名称
+     * 记录器类别名称
      * @type {string}
      * @memberof SysLogEx
      */
@@ -83,4 +83,22 @@ export interface SysLogEx {
      * @memberof SysLogEx
      */
     exception?: string | null;
+    /**
+     * 当前状态值
+     * @type {string}
+     * @memberof SysLogEx
+     */
+    state?: string | null;
+    /**
+     * 日志记录时间
+     * @type {Date}
+     * @memberof SysLogEx
+     */
+    logDateTime?: Date;
+    /**
+     * 线程Id
+     * @type {number}
+     * @memberof SysLogEx
+     */
+    threadId?: number;
 }

+ 19 - 1
vue-next-admin/src/api-services/models/sys-log-op.ts

@@ -54,7 +54,7 @@ export interface SysLogOp {
      */
     isDelete?: boolean;
     /**
-     * 日志名称
+     * 记录器类别名称
      * @type {string}
      * @memberof SysLogOp
      */
@@ -83,4 +83,22 @@ export interface SysLogOp {
      * @memberof SysLogOp
      */
     exception?: string | null;
+    /**
+     * 当前状态值
+     * @type {string}
+     * @memberof SysLogOp
+     */
+    state?: string | null;
+    /**
+     * 日志记录时间
+     * @type {Date}
+     * @memberof SysLogOp
+     */
+    logDateTime?: Date;
+    /**
+     * 线程Id
+     * @type {number}
+     * @memberof SysLogOp
+     */
+    threadId?: number;
 }

+ 2 - 1
vue-next-admin/src/api-services/models/user-type-enum.ts

@@ -12,13 +12,14 @@
  * Do not edit the class manually.
  */
 /**
- * 账号类型枚举<br />&nbsp;管理员 Admin = 1<br />&nbsp;普通账号 None = 2<br />&nbsp;超级管理员 SuperAdmin = 999<br />
+ * 账号类型枚举<br />&nbsp;管理员 Admin = 1<br />&nbsp;普通账号 User = 2<br />&nbsp;游客 None = 3<br />&nbsp;超级管理员 SuperAdmin = 999<br />
  * @export
  * @enum {string}
  */
 export enum UserTypeEnum {
     NUMBER_1 = 1,
     NUMBER_2 = 2,
+    NUMBER_3 = 3,
     NUMBER_999 = 999
 }
 

+ 300 - 0
vue-next-admin/src/components/dragVerify/dragVerify.vue

@@ -0,0 +1,300 @@
+<template>
+	<div ref="dragVerify" class="drag_verify" :style="dragVerifyStyle" @mousemove="dragMoving" @mouseup="dragFinish"
+		@mouseleave="dragFinish" @touchmove="dragMoving" @touchend="dragFinish">
+
+		<div class="dv_progress_bar" :class="{goFirst2:isOk}" ref="progressBar" :style="progressBarStyle">
+
+		</div>
+		<div class="dv_text" :style="textStyle" ref="message">
+			<slot name="textBefore" v-if="$slots.textBefore"></slot>
+			{{message}}
+			<slot name="textAfter" v-if="$slots.textAfter"></slot>
+		</div>
+
+		<div class="dv_handler dv_handler_bg" :class="{goFirst:isOk}" @mousedown="dragStart" @touchstart="dragStart"
+			ref="handler" :style="handlerStyle">
+			<i :class="handlerIcon"></i>
+		</div>
+
+	</div>
+</template>
+
+<script lang="ts">
+export default {
+	name: "dragVerify",
+	props: {
+		isPassing: {
+			type: Boolean,
+			default: false
+		},
+		width: {
+			type: Number,
+			default: 250
+		},
+		height: {
+			type: Number,
+			default: 40
+		},
+		text: {
+			type: String,
+			default: "swiping to the right side"
+		},
+		successText: {
+			type: String,
+			default: "success"
+		},
+		background: {
+			type: String,
+			default: "#eee"
+		},
+		progressBarBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		completedBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		circle: {
+			type: Boolean,
+			default: false
+		},
+		radius: {
+			type: String,
+			default: "4px"
+		},
+		handlerIcon: {
+			type: String
+		},
+		successIcon: {
+			type: String
+		},
+		handlerBg: {
+			type: String,
+			default: "#fff"
+		},
+		textSize: {
+			type: String,
+			default: "14px"
+		},
+		textColor: {
+			type: String,
+			default: "#333"
+		}
+	},
+	mounted: function () {
+		const dragEl = this.$refs.dragVerify;
+		dragEl.style.setProperty("--textColor", this.textColor);
+		dragEl.style.setProperty("--width", Math.floor(this.width / 2) + "px");
+		dragEl.style.setProperty("--pwidth", -Math.floor(this.width / 2) + "px");
+		console.log(this.$slots);
+	},
+	computed: {
+		handlerStyle: function () {
+			return {
+				width: this.height + "px",
+				height: this.height + "px",
+				background: this.handlerBg
+			};
+		},
+		message: function () {
+			return this.isPassing ? this.successText : this.text;
+		},
+		dragVerifyStyle: function () {
+			return {
+				width: this.width + "px",
+				height: this.height + "px",
+				lineHeight: this.height + "px",
+				background: this.background,
+				borderRadius: this.circle ? this.height / 2 + "px" : this.radius
+			};
+		},
+		progressBarStyle: function () {
+			return {
+				background: this.progressBarBg,
+				height: this.height + "px",
+				borderRadius: this.circle
+					? this.height / 2 + "px 0 0 " + this.height / 2 + "px"
+					: this.radius
+			};
+		},
+		textStyle: function () {
+			return {
+				height: this.height + "px",
+				width: this.width + "px",
+				fontSize: this.textSize
+			};
+		}
+	},
+	data() {
+		return {
+			isMoving: false,
+			x: 0,
+			isOk: false
+		};
+	},
+	methods: {
+		dragStart: function (e) {
+			if (!this.isPassing) {
+				this.isMoving = true;
+				this.x = (e.pageX || e.touches[0].pageX)
+			}
+			this.$emit("handlerMove");
+		},
+		dragMoving: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				var _x = (e.pageX || e.touches[0].pageX) - this.x;
+				var handler = this.$refs.handler;
+				if (_x > 0 && _x <= this.width - this.height) {
+					handler.style.left = _x + "px";
+					this.$refs.progressBar.style.width = _x + this.height / 2 + "px";
+				} else if (_x > this.width - this.height) {
+					handler.style.left = this.width - this.height + "px";
+					this.$refs.progressBar.style.width =
+						this.width - this.height / 2 + "px";
+					this.passVerify();
+				}
+			}
+		},
+		dragFinish: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				var _x = (e.pageX || e.changedTouches[0].pageX) - this.x;
+				if (_x < this.width - this.height) {
+					this.isOk = true;
+					var that = this;
+					setTimeout(function () {
+						that.$refs.handler.style.left = "0";
+						that.$refs.progressBar.style.width = "0";
+						that.isOk = false;
+					}, 500);
+					this.$emit("passfail");
+				} else {
+					var handler = this.$refs.handler;
+					handler.style.left = this.width - this.height + "px";
+					this.$refs.progressBar.style.width =
+						this.width - this.height / 2 + "px";
+					this.passVerify();
+				}
+				this.isMoving = false;
+			}
+		},
+		passVerify: function () {
+			this.$emit("update:isPassing", true);
+			this.isMoving = false;
+			var handler = this.$refs.handler;
+			handler.children[0].className = this.successIcon;
+			this.$refs.progressBar.style.background = this.completedBg;
+			this.$refs.message.style["-webkit-text-fill-color"] = "unset";
+			this.$refs.message.style.animation = "slidetounlock2 3s infinite";
+			this.$refs.message.style.color = "#fff";
+			this.$emit("passcallback");
+		},
+		reset: function () {
+			const oriData = this.$options.data();
+			for (const key in oriData) {
+				if (Object.prototype.hasOwnProperty.call(oriData, key)) {
+					this[key] = oriData[key]
+				}
+			}
+			var handler = this.$refs.handler;
+			var message = this.$refs.message;
+			handler.style.left = "0";
+			this.$refs.progressBar.style.width = "0";
+			handler.children[0].className = this.handlerIcon;
+			message.style["-webkit-text-fill-color"] = "transparent";
+			message.style.animation = "slidetounlock 3s infinite";
+			message.style.color = this.background;
+		}
+	}
+};
+</script>
+<style scoped>
+.drag_verify {
+	position: relative;
+	background-color: #e8e8e8;
+	text-align: center;
+	overflow: hidden;
+}
+
+.drag_verify .dv_handler {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	cursor: move;
+}
+
+.drag_verify .dv_handler i {
+	color: #666;
+	padding-left: 0;
+	font-size: 16px;
+}
+
+.drag_verify .dv_handler .el-icon-circle-check {
+	color: #6c6;
+	margin-top: 9px;
+}
+
+.drag_verify .dv_progress_bar {
+	position: absolute;
+	height: 34px;
+	width: 0px;
+}
+
+.drag_verify .dv_text {
+	position: absolute;
+	top: 0px;
+	color: transparent;
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	user-select: none;
+	-o-user-select: none;
+	-ms-user-select: none;
+	background: -webkit-gradient(linear,
+			left top,
+			right top,
+			color-stop(0, var(--textColor)),
+			color-stop(0.4, var(--textColor)),
+			color-stop(0.5, #fff),
+			color-stop(0.6, var(--textColor)),
+			color-stop(1, var(--textColor)));
+	-webkit-background-clip: text;
+	-webkit-text-fill-color: transparent;
+	-webkit-text-size-adjust: none;
+	animation: slidetounlock 3s infinite;
+}
+
+.drag_verify .dv_text * {
+	-webkit-text-fill-color: var(--textColor);
+}
+
+.goFirst {
+	left: 0px !important;
+	transition: left 0.5s;
+}
+
+.goFirst2 {
+	width: 0px !important;
+	transition: width 0.5s;
+}
+</style>
+<style>
+@-webkit-keyframes slidetounlock {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--width) 0;
+	}
+}
+
+@-webkit-keyframes slidetounlock2 {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--pwidth) 0;
+	}
+}
+</style>

+ 466 - 0
vue-next-admin/src/components/dragVerify/dragVerifyImg.vue

@@ -0,0 +1,466 @@
+<template>
+	<div class="drag-verify-container">
+		<div :style="dragVerifyImgStyle">
+			<img ref="checkImg" :src="imgsrc" @load="checkimgLoaded" style="width:100%" alt="">
+			<div class="move-bar" :class="{goFirst:isOk, goKeep:isKeep}" :style="movebarStyle" ref="moveBar"
+				v-show="showBar"></div>
+			<div class="clip-bar" :style="clipbarStyle" ref="clipBar"></div>
+			<div class="refresh" v-if="showRefresh && !isPassing">
+				<i :class="refreshIcon" @click="refreshimg"></i>
+			</div>
+			<div class="tips success" v-if="showTips && isPassing">{{successTip}}</div>
+			<div class="tips danger" v-if="showTips && !isPassing && showErrorTip">{{failTip}}</div>
+		</div>
+		<div ref="dragVerify" class="drag_verify" :style="dragVerifyStyle" @mousemove="dragMoving" @mouseup="dragFinish"
+			@mouseleave="dragFinish" @touchmove="dragMoving" @touchend="dragFinish">
+
+			<div class="dv_progress_bar" :class="{goFirst2:isOk}" ref="progressBar" :style="progressBarStyle">
+				{{successMessage}}
+			</div>
+			<div class="dv_text" :style="textStyle" ref="message">
+				{{message}}
+			</div>
+
+			<div class="dv_handler dv_handler_bg" :class="{goFirst:isOk}" @mousedown="dragStart" @touchstart="dragStart"
+				ref="handler" :style="handlerStyle">
+				<i :class="handlerIcon"></i>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script  lang="ts">
+export default {
+	name: "dragVerify",
+	props: {
+		isPassing: {
+			type: Boolean,
+			default: false
+		},
+		width: {
+			type: Number,
+			default: 250
+		},
+		height: {
+			type: Number,
+			default: 40
+		},
+		text: {
+			type: String,
+			default: "swiping to the right side"
+		},
+		successText: {
+			type: String,
+			default: "success"
+		},
+		background: {
+			type: String,
+			default: "#eee"
+		},
+		progressBarBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		completedBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		circle: {
+			type: Boolean,
+			default: false
+		},
+		radius: {
+			type: String,
+			default: "4px"
+		},
+		handlerIcon: {
+			type: String
+		},
+		successIcon: {
+			type: String
+		},
+		handlerBg: {
+			type: String,
+			default: "#fff"
+		},
+		textSize: {
+			type: String,
+			default: "14px"
+		},
+		textColor: {
+			type: String,
+			default: "#333"
+		},
+		imgsrc: {
+			type: String
+		},
+		barWidth: {
+			type: Number,
+			default: 70
+		},
+		barHeight: {
+			type: Number,
+			default: 40
+		},
+		barRadius: {
+			type: Number,
+			default: 2
+		},
+		showRefresh: {
+			type: Boolean,
+			default: false
+		},
+		refreshIcon: {
+			type: String
+		},
+		showTips: {
+			type: Boolean,
+			default: true
+		},
+		successTip: {
+			type: String,
+			default: "验证通过,超过80%用户"
+		},
+		failTip: {
+			type: String,
+			default: "验证未通过,拖动滑块将悬浮图像正确合并"
+		},
+		diffWidth: {
+			type: Number,
+			default: 20
+		}
+	},
+	mounted: function () {
+		const dragEl = this.$refs.dragVerify;
+		dragEl.style.setProperty("--textColor", this.textColor);
+		dragEl.style.setProperty("--width", Math.floor(this.width / 2) + "px");
+		dragEl.style.setProperty("--pwidth", -Math.floor(this.width / 2) + "px");
+	},
+	computed: {
+		handlerStyle: function () {
+			return {
+				width: this.height + "px",
+				height: this.height + "px",
+				background: this.handlerBg
+			};
+		},
+		message: function () {
+			return this.isPassing ? "" : this.text;
+		},
+		successMessage: function () {
+			return this.isPassing ? this.successText : "";
+		},
+		dragVerifyStyle: function () {
+			console.log(this.width, "width");
+			return {
+				width: this.width + "px",
+				height: this.height + "px",
+				lineHeight: this.height + "px",
+				background: this.background,
+				borderRadius: this.circle ? this.height / 2 + "px" : this.radius
+			};
+		},
+		dragVerifyImgStyle: function () {
+			return {
+				width: this.width + "px",
+				position: "relative",
+				overflow: "hidden"
+			};
+		},
+		progressBarStyle: function () {
+			return {
+				background: this.progressBarBg,
+				height: this.height + "px",
+				borderRadius: this.circle
+					? this.height / 2 + "px 0 0 " + this.height / 2 + "px"
+					: this.radius
+			};
+		},
+		textStyle: function () {
+			return {
+				height: this.height + "px",
+				width: this.width + "px",
+				fontSize: this.textSize
+			};
+		}
+	},
+	data() {
+		return {
+			isMoving: false,
+			x: 0,
+			isOk: false,
+			isKeep: false,
+			movebarStyle: {},
+			clipbarStyle: {},
+			showBar: false,
+			clipBarx: 0,
+			showErrorTip: false
+		};
+	},
+	methods: {
+		checkimgLoaded: function () {
+			//生成图片缺失位置
+			var barWidth = this.barWidth;
+			var barHeight = this.barHeight;
+			var imgHeight = this.$refs.checkImg.height;
+			var halfWidth = Math.floor(this.width / 2);
+			var refreshHeigth = 25;
+			var tipHeight = 20;
+			var x = halfWidth + Math.ceil(Math.random() * (halfWidth - barWidth));
+			var y =
+				refreshHeigth +
+				Math.floor(
+					Math.random() * (imgHeight - barHeight - refreshHeigth - tipHeight)
+				);
+			this.clipbarStyle = {
+				width: barWidth + "px",
+				height: barHeight + "px",
+				top: y + "px",
+				left: x + "px",
+				"border-radius": this.barRadius + "px"
+			};
+			this.clipBarx = x;
+			var imgsrc = this.imgsrc;
+			var width = this.width;
+			this.movebarStyle = {
+				background: `url(${imgsrc})`,
+				"background-position": `-${x}px -${y}px`,
+				"background-size": `${width}px`,
+				width: barWidth + "px",
+				height: barHeight + "px",
+				top: y + "px",
+				"border-radius": this.barRadius + "px"
+			};
+		},
+		dragStart: function (e) {
+			if (!this.isPassing) {
+				this.isMoving = true;
+				this.x =
+					(e.pageX || e.touches[0].pageX)
+			}
+			this.showBar = true;
+			this.showErrorTip = false;
+			this.$emit("handlerMove");
+		},
+		dragMoving: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				var _x = (e.pageX || e.touches[0].pageX) - this.x;
+				var handler = this.$refs.handler;
+				handler.style.left = _x + "px";
+				this.$refs.progressBar.style.width = _x + this.height / 2 + "px";
+				this.$refs.moveBar.style.left = _x + "px";
+			}
+		},
+		dragFinish: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				var _x = (e.pageX || e.changedTouches[0].pageX) - this.x;
+				if (Math.abs(_x - this.clipBarx) > this.diffWidth) {
+					this.isOk = true;
+					var that = this;
+					setTimeout(function () {
+						that.$refs.handler.style.left = "0";
+						that.$refs.progressBar.style.width = "0";
+						that.$refs.moveBar.style.left = "0";
+						that.isOk = false;
+					}, 500);
+					this.showErrorTip = true;
+					this.$emit("passfail");
+				} else {
+					this.passVerify();
+				}
+				this.isMoving = false;
+			}
+		},
+		passVerify: function () {
+			this.$emit("update:isPassing", true);
+			this.isMoving = false;
+			var handler = this.$refs.handler;
+			handler.children[0].className = this.successIcon;
+			this.$refs.progressBar.style.background = this.completedBg;
+			this.$refs.message.style["-webkit-text-fill-color"] = "unset";
+			this.$refs.message.style.animation = "slidetounlock2 3s infinite";
+			this.$refs.progressBar.style.color = "#fff";
+			this.$refs.progressBar.style.fontSize = this.textSize;
+			this.isKeep = true;
+			setTimeout(() => {
+				this.$refs.moveBar.style.left = this.clipBarx + "px";
+				setTimeout(() => {
+					this.isKeep = false;
+				}, 200);
+			}, 100);
+			this.$emit("passcallback");
+		},
+		reset: function () {
+			this.reImg();
+			this.checkimgLoaded();
+		},
+		reImg: function () {
+			this.$emit("update:isPassing", false);
+			const oriData = this.$options.data();
+			for (const key in oriData) {
+				if (Object.prototype.hasOwnProperty.call(oriData, key)) {
+					this[key] = oriData[key]
+				}
+			}
+			var handler = this.$refs.handler;
+			var message = this.$refs.message;
+			handler.style.left = "0";
+			this.$refs.progressBar.style.width = "0";
+			handler.children[0].className = this.handlerIcon;
+			message.style["-webkit-text-fill-color"] = "transparent";
+			message.style.animation = "slidetounlock 3s infinite";
+			message.style.color = this.background;
+		},
+		refreshimg: function () {
+			this.$emit("refresh");
+		}
+	},
+	watch: {
+		imgsrc: {
+			immediate: false,
+			handler: function () {
+				this.reImg();
+			}
+		}
+	}
+};
+</script>
+<style scoped>
+.drag_verify {
+	position: relative;
+	background-color: #e8e8e8;
+	text-align: center;
+	overflow: hidden;
+}
+
+.drag_verify .dv_handler {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	cursor: move;
+}
+
+.drag_verify .dv_handler i {
+	color: #666;
+	padding-left: 0;
+	font-size: 16px;
+}
+
+.drag_verify .dv_handler .el-icon-circle-check {
+	color: #6c6;
+	margin-top: 9px;
+}
+
+.drag_verify .dv_progress_bar {
+	position: absolute;
+	height: 34px;
+	width: 0px;
+}
+
+.drag_verify .dv_text {
+	position: absolute;
+	top: 0px;
+	color: transparent;
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	user-select: none;
+	-o-user-select: none;
+	-ms-user-select: none;
+	background: -webkit-gradient(linear,
+			left top,
+			right top,
+			color-stop(0, var(--textColor)),
+			color-stop(0.4, var(--textColor)),
+			color-stop(0.5, #fff),
+			color-stop(0.6, var(--textColor)),
+			color-stop(1, var(--textColor)));
+	-webkit-background-clip: text;
+	-webkit-text-fill-color: transparent;
+	-webkit-text-size-adjust: none;
+	animation: slidetounlock 3s infinite;
+}
+
+.drag_verify .dv_text * {
+	-webkit-text-fill-color: var(--textColor);
+}
+
+.goFirst {
+	left: 0px !important;
+	transition: left 0.5s;
+}
+
+.goKeep {
+	transition: left 0.2s;
+}
+
+.goFirst2 {
+	width: 0px !important;
+	transition: width 0.5s;
+}
+
+.drag-verify-container {
+	position: relative;
+	line-height: 0;
+}
+
+.move-bar {
+	position: absolute;
+	z-index: 100;
+}
+
+.clip-bar {
+	position: absolute;
+	background: rgba(255, 255, 255, 0.8);
+}
+
+.refresh {
+	position: absolute;
+	right: 5px;
+	top: 5px;
+	cursor: pointer;
+	font-size: 20px;
+	z-index: 200;
+}
+
+.tips {
+	position: absolute;
+	bottom: 0;
+	height: 20px;
+	line-height: 20px;
+	text-align: center;
+	width: 100%;
+	font-size: 12px;
+	z-index: 200;
+}
+
+.tips.success {
+	background: rgba(255, 255, 255, 0.6);
+	color: green;
+}
+
+.tips.danger {
+	background: rgba(0, 0, 0, 0.6);
+	color: yellow;
+}
+</style>
+<style>
+@-webkit-keyframes slidetounlock {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--width) 0;
+	}
+}
+
+@-webkit-keyframes slidetounlock2 {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--pwidth) 0;
+	}
+}
+</style>

+ 486 - 0
vue-next-admin/src/components/dragVerify/dragVerifyImgChip.vue

@@ -0,0 +1,486 @@
+<template>
+  <div class="drag-verify-container">
+    <div :style="dragVerifyImgStyle">
+      <img ref="checkImg" crossOrigin="anonymous" :src="imgsrc" @load="checkimgLoaded" style="width:100%" alt="">
+      <canvas ref="maincanvas" class="main-canvas"></canvas>
+      <canvas ref="movecanvas" :class="{goFirst:isOk, goKeep:isKeep}" class="move-canvas"></canvas>
+      <div class="refresh" v-if="showRefresh && !isPassing">
+        <i :class="refreshIcon" @click="refreshimg"></i>
+      </div>
+      <div class="tips success" v-if="showTips && isPassing">{{successTip}}</div>
+      <div class="tips danger" v-if="showTips && !isPassing && showErrorTip">{{failTip}}</div>
+    </div>
+    <div ref="dragVerify" class="drag_verify" :style="dragVerifyStyle" @mousemove="dragMoving" @mouseup="dragFinish"
+      @mouseleave="dragFinish" @touchmove="dragMoving" @touchend="dragFinish">
+
+      <div class="dv_progress_bar" :class="{goFirst2:isOk}" ref="progressBar" :style="progressBarStyle">
+        {{successMessage}}
+      </div>
+      <div class="dv_text" :style="textStyle" ref="message">
+        {{message}}
+      </div>
+
+      <div class="dv_handler dv_handler_bg" :class="{goFirst:isOk}" @mousedown="dragStart" @touchstart="dragStart"
+        ref="handler" :style="handlerStyle">
+        <i :class="handlerIcon"></i>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script  lang="ts">
+export default {
+  name: "dragVerifyImgChip",
+  props: {
+    isPassing: {
+      type: Boolean,
+      default: false
+    },
+    width: {
+      type: Number,
+      default: 250
+    },
+    height: {
+      type: Number,
+      default: 40
+    },
+    text: {
+      type: String,
+      default: "swiping to the right side"
+    },
+    successText: {
+      type: String,
+      default: "success"
+    },
+    background: {
+      type: String,
+      default: "#eee"
+    },
+    progressBarBg: {
+      type: String,
+      default: "#76c61d"
+    },
+    completedBg: {
+      type: String,
+      default: "#76c61d"
+    },
+    circle: {
+      type: Boolean,
+      default: false
+    },
+    radius: {
+      type: String,
+      default: "4px"
+    },
+    handlerIcon: {
+      type: String
+    },
+    successIcon: {
+      type: String
+    },
+    handlerBg: {
+      type: String,
+      default: "#fff"
+    },
+    textSize: {
+      type: String,
+      default: "14px"
+    },
+    textColor: {
+      type: String,
+      default: "#333"
+    },
+    imgsrc: {
+      type: String
+    },
+    barWidth: {
+      type: Number,
+      default: 40
+    },
+    barRadius: {
+      type: Number,
+      default: 8
+    },
+    showRefresh: {
+      type: Boolean,
+      default: false
+    },
+    refreshIcon: {
+      type: String
+    },
+    showTips: {
+      type: Boolean,
+      default: true
+    },
+    successTip: {
+      type: String,
+      default: "验证通过,超过80%用户"
+    },
+    failTip: {
+      type: String,
+      default: "验证未通过,拖动滑块将悬浮图像正确合并"
+    },
+    diffWidth: {
+      type: Number,
+      default: 20
+    }
+  },
+  mounted: function () {
+    const dragEl = this.$refs.dragVerify;
+    dragEl.style.setProperty("--textColor", this.textColor);
+    dragEl.style.setProperty("--width", Math.floor(this.width / 2) + "px");
+    dragEl.style.setProperty("--pwidth", -Math.floor(this.width / 2) + "px");
+  },
+  computed: {
+    handlerStyle: function () {
+      return {
+        width: this.height + "px",
+        height: this.height + "px",
+        background: this.handlerBg
+      };
+    },
+    message: function () {
+      return this.isPassing ? "" : this.text;
+    },
+    successMessage: function () {
+      return this.isPassing ? this.successText : "";
+    },
+    dragVerifyStyle: function () {
+      return {
+        width: this.width + "px",
+        height: this.height + "px",
+        lineHeight: this.height + "px",
+        background: this.background,
+        borderRadius: this.circle ? this.height / 2 + "px" : this.radius
+      };
+    },
+    dragVerifyImgStyle: function () {
+      return {
+        width: this.width + "px",
+        position: "relative",
+        overflow: "hidden"
+      };
+    },
+    progressBarStyle: function () {
+      return {
+        background: this.progressBarBg,
+        height: this.height + "px",
+        borderRadius: this.circle
+          ? this.height / 2 + "px 0 0 " + this.height / 2 + "px"
+          : this.radius
+      };
+    },
+    textStyle: function () {
+      return {
+        height: this.height + "px",
+        width: this.width + "px",
+        fontSize: this.textSize
+      };
+    }
+  },
+  data() {
+    return {
+      isMoving: false,
+      x: 0,
+      isOk: false,
+      isKeep: false,
+      clipBarx: 0,
+      showErrorTip: false
+    };
+  },
+  methods: {
+    draw: function (ctx, x, y, operation) {
+      var l = this.barWidth;
+      var r = this.barRadius;
+      const PI = Math.PI;
+      ctx.beginPath();
+      ctx.moveTo(x, y);
+      ctx.arc(x + l / 2, y - r + 2, r, 0.72 * PI, 2.26 * PI);
+      ctx.lineTo(x + l, y);
+      ctx.arc(x + l + r - 2, y + l / 2, r, 1.21 * PI, 2.78 * PI);
+      ctx.lineTo(x + l, y + l);
+      ctx.lineTo(x, y + l);
+      ctx.arc(x + r - 2, y + l / 2, r + 0.4, 2.76 * PI, 1.24 * PI, true);
+      ctx.lineTo(x, y);
+      ctx.lineWidth = 2;
+      ctx.fillStyle = "rgba(255, 255, 255, 0.8)";
+      ctx.strokeStyle = "rgba(255, 255, 255, 0.8)";
+      ctx.stroke();
+      ctx[operation]();
+      ctx.globalCompositeOperation = "destination-over";
+    },
+    checkimgLoaded: function () {
+      // 生成图片缺失位置
+      var barWidth = this.barWidth;
+      var imgHeight = this.$refs.checkImg.height;
+      var imgWidth = this.$refs.checkImg.width;
+      var halfWidth = Math.floor(this.width / 2);
+      var refreshHeigth = 25;
+      var tipHeight = 20;
+      var x = halfWidth + Math.ceil(Math.random() * (halfWidth - barWidth - this.barRadius - 5));
+      var y =
+        refreshHeigth +
+        Math.floor(
+          Math.random() * (imgHeight - barWidth - refreshHeigth - tipHeight)
+        );
+      this.$refs.maincanvas.setAttribute("width", imgWidth);
+      this.$refs.maincanvas.setAttribute("height", imgHeight);
+      this.$refs.maincanvas.style.display = "block";
+      var canvasCtx = this.$refs.maincanvas.getContext("2d");
+      this.draw(canvasCtx, x, y, "fill");
+      this.clipBarx = x;
+
+      var moveCanvas = this.$refs.movecanvas;
+      moveCanvas.setAttribute("width", imgWidth);
+      moveCanvas.setAttribute("height", imgHeight);
+      this.$refs.movecanvas.style.display = "block";
+      const L = barWidth + this.barRadius * 2 + 3; //实际宽度
+      var moveCtx = this.$refs.movecanvas.getContext("2d");
+      moveCtx.clearRect(0, 0, imgWidth, imgHeight);
+      this.draw(moveCtx, x, y, "clip");
+      moveCtx.drawImage(this.$refs.checkImg, 0, 0, imgWidth, imgHeight);
+      var y = y - this.barRadius * 2 - 1;
+      const ImageData = moveCtx.getImageData(x, y, L, L);
+      moveCanvas.setAttribute("width", L);
+      moveCanvas.setAttribute("height", imgHeight);
+      moveCtx.putImageData(ImageData, 0, y);
+    },
+    dragStart: function (e) {
+      if (!this.isPassing) {
+        this.isMoving = true;
+        this.x =
+          (e.pageX || e.touches[0].pageX)
+      }
+      this.showBar = true;
+      this.showErrorTip = false;
+      this.$emit("handlerMove");
+    },
+    dragMoving: function (e) {
+      if (this.isMoving && !this.isPassing) {
+        var _x = (e.pageX || e.touches[0].pageX) - this.x;
+        var handler = this.$refs.handler;
+        handler.style.left = _x + "px";
+        this.$refs.progressBar.style.width = _x + this.height / 2 + "px";
+        this.$refs.movecanvas.style.left = _x + "px";
+      }
+    },
+    dragFinish: function (e) {
+      if (this.isMoving && !this.isPassing) {
+        var _x = (e.pageX || e.changedTouches[0].pageX) - this.x;
+        if (Math.abs(_x - this.clipBarx) > this.diffWidth) {
+          this.isOk = true;
+          var that = this;
+          setTimeout(function () {
+            that.$refs.handler.style.left = "0";
+            that.$refs.progressBar.style.width = "0";
+            that.$refs.movecanvas.style.left = "0";
+            that.isOk = false;
+          }, 500);
+          this.$emit("passfail");
+          this.showErrorTip = true;
+        } else {
+          this.passVerify();
+        }
+        this.isMoving = false;
+      }
+    },
+    passVerify: function () {
+      this.$emit("update:isPassing", true);
+      this.isMoving = false;
+      var handler = this.$refs.handler;
+      handler.children[0].className = this.successIcon;
+      this.$refs.progressBar.style.background = this.completedBg;
+      this.$refs.message.style["-webkit-text-fill-color"] = "unset";
+      this.$refs.message.style.animation = "slidetounlock2 3s infinite";
+      this.$refs.progressBar.style.color = "#fff";
+      this.$refs.progressBar.style.fontSize = this.textSize;
+      this.isKeep = true;
+      setTimeout(() => {
+        this.$refs.movecanvas.style.left = this.clipBarx + "px";
+        setTimeout(() => {
+          this.isKeep = false;
+          this.$refs.maincanvas.style.display = "none";
+          this.$refs.movecanvas.style.display = "none";
+        }, 200);
+      }, 100);
+      this.$emit("passcallback");
+    },
+    reset: function () {
+      this.reImg();
+      this.checkimgLoaded();
+    },
+    reImg: function () {
+      this.$emit("update:isPassing", false);
+      const oriData = this.$options.data();
+      for (const key in oriData) {
+        if (Object.prototype.hasOwnProperty.call(oriData, key)) {
+          this[key] = oriData[key]
+        }
+      }
+      var handler = this.$refs.handler;
+      var message = this.$refs.message;
+      handler.style.left = "0";
+      this.$refs.progressBar.style.width = "0";
+      handler.children[0].className = this.handlerIcon;
+      message.style["-webkit-text-fill-color"] = "transparent";
+      message.style.animation = "slidetounlock 3s infinite";
+      message.style.color = this.background;
+      this.$refs.movecanvas.style.left = "0px";
+    },
+    refreshimg: function () {
+      this.$emit("refresh");
+    }
+  },
+  watch: {
+    imgsrc: {
+      immediate: false,
+      handler: function () {
+        this.reImg();
+      }
+    }
+  }
+};
+</script>
+<style scoped>
+.drag_verify {
+  position: relative;
+  background-color: #e8e8e8;
+  text-align: center;
+  overflow: hidden;
+}
+
+.drag_verify .dv_handler {
+  position: absolute;
+  top: 0px;
+  left: 0px;
+  cursor: move;
+}
+
+.drag_verify .dv_handler i {
+  color: #666;
+  padding-left: 0;
+  font-size: 16px;
+}
+
+.drag_verify .dv_handler .el-icon-circle-check {
+  color: #6c6;
+  margin-top: 9px;
+}
+
+.drag_verify .dv_progress_bar {
+  position: absolute;
+  height: 34px;
+  width: 0px;
+}
+
+.drag_verify .dv_text {
+  position: absolute;
+  top: 0px;
+  color: transparent;
+  -moz-user-select: none;
+  -webkit-user-select: none;
+  user-select: none;
+  -o-user-select: none;
+  -ms-user-select: none;
+  background: -webkit-gradient(linear,
+      left top,
+      right top,
+      color-stop(0, var(--textColor)),
+      color-stop(0.4, var(--textColor)),
+      color-stop(0.5, #fff),
+      color-stop(0.6, var(--textColor)),
+      color-stop(1, var(--textColor)));
+  -webkit-background-clip: text;
+  -webkit-text-fill-color: transparent;
+  -webkit-text-size-adjust: none;
+  animation: slidetounlock 3s infinite;
+}
+
+.drag_verify .dv_text * {
+  -webkit-text-fill-color: var(--textColor);
+}
+
+.goFirst {
+  left: 0px !important;
+  transition: left 0.5s;
+}
+
+.goKeep {
+  transition: left 0.2s;
+}
+
+.goFirst2 {
+  width: 0px !important;
+  transition: width 0.5s;
+}
+
+.drag-verify-container {
+  position: relative;
+  line-height: 0;
+}
+
+.refresh {
+  position: absolute;
+  right: 5px;
+  top: 5px;
+  cursor: pointer;
+  font-size: 20px;
+  z-index: 200;
+}
+
+.tips {
+  position: absolute;
+  bottom: 0;
+  height: 20px;
+  line-height: 20px;
+  text-align: center;
+  width: 100%;
+  font-size: 12px;
+  z-index: 200;
+}
+
+.tips.success {
+  background: rgba(255, 255, 255, 0.6);
+  color: green;
+}
+
+.tips.danger {
+  background: rgba(0, 0, 0, 0.6);
+  color: yellow;
+}
+
+.main-canvas {
+  width: 100%;
+  height: 100%;
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+
+.move-canvas {
+  position: absolute;
+  top: 0;
+  left: 0;
+}
+</style>
+<style>
+@-webkit-keyframes slidetounlock {
+  0% {
+    background-position: var(--pwidth) 0;
+  }
+
+  100% {
+    background-position: var(--width) 0;
+  }
+}
+
+@-webkit-keyframes slidetounlock2 {
+  0% {
+    background-position: var(--pwidth) 0;
+  }
+
+  100% {
+    background-position: var(--pwidth) 0;
+  }
+}
+</style>

+ 449 - 0
vue-next-admin/src/components/dragVerify/dragVerifyImgRotate.vue

@@ -0,0 +1,449 @@
+<template>
+	<div class="drag-verify-container">
+		<div :style="dragVerifyImgStyle" style="background-color: var(--el-color-primary)">
+			<img ref="checkImg" :src="imgsrc" class="check-img" :class="{goOrigin:isOk}" @load="checkimgLoaded"
+				:style="imgStyle" alt="">
+			<div class="tips success" v-if="showTips && isPassing">{{successTip}}</div>
+			<div class="tips danger" v-if="showTips && !isPassing && showErrorTip">{{failTip}}</div>
+		</div>
+		<div ref="dragVerify" class="drag_verify" :style="dragVerifyStyle" @mousemove="dragMoving" @mouseup="dragFinish"
+			@mouseleave="dragFinish" @touchmove="dragMoving" @touchend="dragFinish">
+			<div class="dv_progress_bar" :class="{goFirst2:isOk}" ref="progressBar" :style="progressBarStyle">
+				{{successMessage}}
+			</div>
+			<div class="dv_text" :style="textStyle" ref="message">
+				{{message}}
+			</div>
+
+			<div class="dv_handler dv_handler_bg" :class="{goFirst:isOk}" @mousedown="dragStart" @touchstart="dragStart"
+				ref="handler" :style="handlerStyle" style="background-color: var(--el-color-primary)">
+				<i :class="handlerIcon"></i>
+			</div>
+		</div>
+	</div>
+</template>
+
+<script  lang="ts">
+export default {
+	name: "dragVerify",
+	props: {
+		isPassing: {
+			type: Boolean,
+			default: false
+		},
+		width: {
+			type: Number,
+			default: 250
+		},
+		height: {
+			type: Number,
+			default: 40
+		},
+		text: {
+			type: String,
+			default: "swiping to the right side"
+		},
+		successText: {
+			type: String,
+			default: "success"
+		},
+		background: {
+			type: String,
+			default: "#eee"
+		},
+		progressBarBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		completedBg: {
+			type: String,
+			default: "#76c61d"
+		},
+		circle: {
+			type: Boolean,
+			default: false
+		},
+		radius: {
+			type: String,
+			default: "4px"
+		},
+		handlerIcon: {
+			type: String
+		},
+		successIcon: {
+			type: String
+		},
+		handlerBg: {
+			type: String,
+			default: "#fff"
+		},
+		textSize: {
+			type: String,
+			default: "14px"
+		},
+		textColor: {
+			type: String,
+			default: "#333"
+		},
+		imgsrc: {
+			type: String
+		},
+		showTips: {
+			type: Boolean,
+			default: true
+		},
+		successTip: {
+			type: String,
+			default: "验证通过"
+		},
+		failTip: {
+			type: String,
+			default: "验证失败"
+		},
+		diffDegree: {
+			type: Number,
+			default: 10
+		},
+		minDegree: {
+			type: Number,
+			default: 90
+		},
+		maxDegree: {
+			type: Number,
+			default: 270
+		}
+	},
+	mounted: function () {
+		const dragEl = this.$refs.dragVerify;
+		dragEl.style.setProperty("--textColor", this.textColor);
+		dragEl.style.setProperty("--width", Math.floor(this.width / 2) + "px");
+		dragEl.style.setProperty("--pwidth", -Math.floor(this.width / 2) + "px");
+	},
+	computed: {
+		handlerStyle: function () {
+			return {
+				width: this.height + "px",
+				height: this.height + "px",
+				background: this.handlerBg
+			};
+		},
+		message: function () {
+			return this.isPassing ? "" : this.text;
+		},
+		successMessage: function () {
+			return this.isPassing ? this.successText : "";
+		},
+		dragVerifyStyle: function () {
+			return {
+				width: this.width + "px",
+				height: this.height + "px",
+				lineHeight: this.height + "px",
+				marginTop: '20px',
+				background: this.background,
+				borderRadius: this.circle ? this.height / 2 + "px" : this.radius
+			};
+		},
+		dragVerifyImgStyle: function () {
+			return {
+				width: this.width + "px",
+				height: this.width + "px",
+				position: "relative",
+				overflow: "hidden",
+				"border-radius": "50%"
+			};
+		},
+		progressBarStyle: function () {
+			return {
+				background: this.progressBarBg,
+				height: this.height + "px",
+				borderRadius: this.circle
+					? this.height / 2 + "px 0 0 " + this.height / 2 + "px"
+					: this.radius
+			};
+		},
+		textStyle: function () {
+			return {
+				height: this.height + "px",
+				width: this.width + "px",
+				fontSize: this.textSize
+			};
+		},
+		factor: function () {
+			//避免指定旋转角度时一直拖动到最右侧才验证通过
+			if (this.minDegree == this.maxDegree) {
+				return Math.floor(1 + Math.random() * 6) / 10 + 1;
+			}
+			return 1;
+		}
+	},
+	data() {
+		return {
+			isMoving: false,
+			x: 0,
+			isOk: false,
+			showBar: false,
+			showErrorTip: false,
+			ranRotate: 0,
+			cRotate: 0,
+			imgStyle: {}
+		};
+	},
+	methods: {
+		checkimgLoaded: function () {
+			//生成旋转角度
+			var minDegree = this.minDegree;
+			var maxDegree = this.maxDegree;
+			var ranRotate = Math.floor(
+				minDegree + Math.random() * (maxDegree - minDegree)
+			); //生成随机角度
+			this.ranRotate = ranRotate;
+			//console.log("旋转" + ranRotate);
+			this.imgStyle = {
+				transform: `rotateZ(${ranRotate}deg)`
+			};
+		},
+		dragStart: function (e) {
+			if (!this.isPassing) {
+				this.isMoving = true;
+				this.x =
+					(e.pageX || e.touches[0].pageX)
+			}
+			this.showBar = true;
+			this.showErrorTip = false;
+			this.$emit("handlerMove");
+		},
+		dragMoving: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				var _x = (e.pageX || e.touches[0].pageX) - this.x;
+				//console.log(_x, "_x");
+				var handler = this.$refs.handler;
+				handler.style.left = _x + "px";
+				this.$refs.progressBar.style.width = _x + this.height / 2 + "px";
+				var cRotate = Math.ceil(
+					(_x / (this.width - this.height)) * this.maxDegree * this.factor
+				);
+				//console.log(cRotate, "cRotate");
+				this.cRotate = cRotate;
+				var rotate = this.ranRotate - cRotate;
+				this.imgStyle = {
+					transform: `rotateZ(${rotate}deg)`
+				};
+			}
+		},
+		dragFinish: function (e) {
+			if (this.isMoving && !this.isPassing) {
+				if (Math.abs(this.ranRotate - this.cRotate) > this.diffDegree) {
+					this.isOk = true;
+					this.imgStyle = {
+						transform: `rotateZ(${this.ranRotate}deg)`
+					};
+					var that = this;
+					setTimeout(function () {
+						that.$refs.handler.style.left = "0";
+						that.$refs.progressBar.style.width = "0";
+						that.isOk = false;
+					}, 500);
+					this.showErrorTip = true;
+					this.$emit("passfail");
+				} else {
+					this.passVerify();
+				}
+				this.isMoving = false;
+			}
+		},
+		passVerify: function () {
+			this.$emit("update:isPassing", true);
+			this.isMoving = false;
+			var handler = this.$refs.handler;
+			handler.children[0].className = this.successIcon;
+			this.$refs.progressBar.style.background = this.completedBg;
+			this.$refs.message.style["-webkit-text-fill-color"] = "unset";
+			this.$refs.message.style.animation = "slidetounlock2 3s infinite";
+			this.$refs.progressBar.style.color = "#fff";
+			this.$refs.progressBar.style.fontSize = this.textSize;
+			this.$emit("passcallback");
+		},
+		reset: function () {
+			this.reImg();
+			this.checkimgLoaded();
+		},
+		reImg: function () {
+			this.$emit("update:isPassing", false);
+			const oriData = this.$options.data();
+			for (const key in oriData) {
+				if (Object.prototype.hasOwnProperty.call(oriData, key)) {
+					this[key] = oriData[key]
+				}
+			}
+			var handler = this.$refs.handler;
+			var message = this.$refs.message;
+			handler.style.left = "0";
+			this.$refs.progressBar.style.width = "0";
+			handler.children[0].className = this.handlerIcon;
+			message.style["-webkit-text-fill-color"] = "transparent";
+			message.style.animation = "slidetounlock 3s infinite";
+			message.style.color = this.background;
+		},
+		refreshimg: function () {
+			this.$emit("refresh");
+		}
+	},
+	watch: {
+		imgsrc: {
+			immediate: false,
+			handler: function () {
+				this.reImg();
+			}
+		}
+	}
+};
+</script>
+<style scoped>
+.drag_verify {
+	position: relative;
+	background-color: #e8e8e8;
+	text-align: center;
+	overflow: hidden;
+}
+
+.drag_verify .dv_handler {
+	position: absolute;
+	top: 0px;
+	left: 0px;
+	cursor: move;
+}
+
+.drag_verify .dv_handler i {
+	color: #666;
+	padding-left: 0;
+	font-size: 16px;
+}
+
+.drag_verify .dv_handler .el-icon-circle-check {
+	color: #6c6;
+	margin-top: 9px;
+}
+
+.drag_verify .dv_progress_bar {
+	position: absolute;
+	height: 34px;
+	width: 0px;
+}
+
+.drag_verify .dv_text {
+	position: absolute;
+	top: 0px;
+	color: transparent;
+	-moz-user-select: none;
+	-webkit-user-select: none;
+	user-select: none;
+	-o-user-select: none;
+	-ms-user-select: none;
+	background: -webkit-gradient(linear,
+			left top,
+			right top,
+			color-stop(0, var(--textColor)),
+			color-stop(0.4, var(--textColor)),
+			color-stop(0.5, #fff),
+			color-stop(0.6, var(--textColor)),
+			color-stop(1, var(--textColor)));
+	-webkit-background-clip: text;
+	-webkit-text-fill-color: transparent;
+	-webkit-text-size-adjust: none;
+	animation: slidetounlock 3s infinite;
+}
+
+.drag_verify .dv_text * {
+	-webkit-text-fill-color: var(--textColor);
+}
+
+.goFirst {
+	left: 0px !important;
+	transition: left 0.5s;
+}
+
+.goOrigin {
+	transition: transform 0.5s;
+}
+
+.goKeep {
+	transition: left 0.2s;
+}
+
+.goFirst2 {
+	width: 0px !important;
+	transition: width 0.5s;
+}
+
+.drag-verify-container {
+	position: relative;
+	line-height: 0;
+	border-radius: 50%;
+}
+
+.move-bar {
+	position: absolute;
+	z-index: 100;
+}
+
+.clip-bar {
+	position: absolute;
+	background: rgba(255, 255, 255, 0.8);
+}
+
+.refresh {
+	position: absolute;
+	right: 5px;
+	top: 5px;
+	cursor: pointer;
+	font-size: 20px;
+	z-index: 200;
+}
+
+.tips {
+	position: absolute;
+	bottom: 25px;
+	height: 20px;
+	line-height: 20px;
+	text-align: center;
+	width: 100%;
+	font-size: 12px;
+	z-index: 200;
+}
+
+.tips.success {
+	background: rgba(255, 255, 255, 0.6);
+	color: green;
+}
+
+.tips.danger {
+	background: rgba(0, 0, 0, 0.6);
+	color: yellow;
+}
+
+.check-img {
+	width: 100%;
+	border-radius: 50%;
+}
+</style>
+<style>
+@-webkit-keyframes slidetounlock {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--width) 0;
+	}
+}
+
+@-webkit-keyframes slidetounlock2 {
+	0% {
+		background-position: var(--pwidth) 0;
+	}
+
+	100% {
+		background-position: var(--pwidth) 0;
+	}
+}
+</style>

+ 1 - 1
vue-next-admin/src/theme/app.scss

@@ -128,7 +128,7 @@ body,
 	}
 	.layout-scrollbar {
 		@extend .el-scrollbar;
-		padding: 5px; // 四周间隙
+		padding: 8px; // 四周间隙
 	}
 	.layout-mian-height-50 {
 		height: calc(100vh - 50px);

+ 1 - 2
vue-next-admin/src/utils/authDirective.ts

@@ -13,10 +13,9 @@ export function authDirective(app: App) {
 	app.directive('auth', {
 		mounted(el, binding) {
 			const stores = useUserInfo();
-			console.log(binding.value)
 			if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) {
 				//el.parentNode.removeChild(el);
-				//el.disabled = true;
+				el.disabled = true;
 				el.classList.add('is-disabled');
 				el.setAttribute('aria-disabled', 'true');
 			}

+ 91 - 16
vue-next-admin/src/views/login/component/account.vue

@@ -1,8 +1,7 @@
 <template>
-	<el-form size="large" class="login-content-form">
-		<el-form-item class="login-animation1">
-			<el-input type="text" :placeholder="$t('message.account.accountPlaceholder1')" v-model="ruleForm.userName"
-				clearable autocomplete="off">
+	<el-form ref="ruleFormRef" :model="ruleForm" size="large" :rules="rules" class="login-content-form">
+		<el-form-item class="login-animation1" prop="userName">
+			<el-input type="text" placeholder="请输入账号" v-model="ruleForm.userName" clearable autocomplete="off">
 				<template #prefix>
 					<el-icon class="el-input__icon">
 						<ele-User />
@@ -10,9 +9,9 @@
 				</template>
 			</el-input>
 		</el-form-item>
-		<el-form-item class="login-animation2">
-			<el-input :type="isShowPassword ? 'text' : 'password'"
-				:placeholder="$t('message.account.accountPlaceholder2')" v-model="ruleForm.password" autocomplete="off">
+		<el-form-item class="login-animation2" prop="password">
+			<el-input :type="isShowPassword ? 'text' : 'password'" placeholder="请输入密码" v-model="ruleForm.password"
+				autocomplete="off">
 				<template #prefix>
 					<el-icon class="el-input__icon">
 						<ele-Unlock />
@@ -26,7 +25,7 @@
 				</template>
 			</el-input>
 		</el-form-item>
-		<el-form-item class="login-animation3">
+		<el-form-item class="login-animation3" prop="captcha">
 			<el-col :span="15">
 				<el-input type="text" maxlength="4" :placeholder="$t('message.account.accountPlaceholder3')"
 					v-model="ruleForm.code" clearable autocomplete="off">
@@ -39,20 +38,29 @@
 			</el-col>
 			<el-col :span="1"></el-col>
 			<el-col :span="8">
-				<el-button class="login-content-code">1234</el-button>
+				<div class="login-content-code">
+					<img class="login-content-code-img" @click="getCaptcha" width="130px" height="38px"
+						:src="captchaImage" style="cursor: pointer" />
+				</div>
 			</el-col>
 		</el-form-item>
 		<el-form-item class="login-animation4">
-			<el-button type="primary" class="login-content-submit" round @click="onSignIn" :loading="loading.signIn">
+			<el-button type="primary" class="login-content-submit" round @click="openVerify" :loading="loading.signIn">
 				<span>{{ $t('message.account.accountBtnText') }}</span>
 			</el-button>
 		</el-form-item>
 		<div class="font12 mt30 login-animation4 login-msg">{{ $t('message.mobile.msgText') }}</div>
 	</el-form>
+
+	<el-dialog v-model="verifyVisible" title="" width="300px" center>
+		<DragVerifyImgRotate ref="dragRef" :imgsrc="verifyImg" v-model:isPassing="isPass" text="请按住滑块拖动"
+			successText="验证通过" handlerIcon="fa fa-angle-double-right" successIcon="fa fa-hand-peace-o"
+			@passcallback="passVerify" />
+	</el-dialog>
 </template>
 
 <script lang="ts">
-import { toRefs, reactive, defineComponent, computed } from 'vue';
+import { toRefs, reactive, defineComponent, computed, ref, onMounted } from 'vue';
 import { useRoute, useRouter } from 'vue-router';
 import { ElMessage } from 'element-plus';
 import { useI18n } from 'vue-i18n';
@@ -64,28 +72,57 @@ import { initBackEndControlRoutes } from '/@/router/backEnd';
 import { Session } from '/@/utils/storage';
 import { formatAxis } from '/@/utils/formatTime';
 import { NextLoading } from '/@/utils/loading';
+
 import { getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi } from '/@/api-services/apis/sys-auth-api';
 
+// 旋转图片滑块组件
+import DragVerifyImgRotate from "/@/components/dragVerify/dragVerifyImgRotate.vue";
+import verifyImg from '/@/assets/logo-mini.svg'
+
 export default defineComponent({
 	name: 'loginAccount',
+	components: { DragVerifyImgRotate },
 	setup() {
 		const { t } = useI18n();
 		const storesThemeConfig = useThemeConfig();
 		const { themeConfig } = storeToRefs(storesThemeConfig);
 		const route = useRoute();
 		const router = useRouter();
+
+		const ruleFormRef = ref();
+		const dragRef: any = ref(null);
 		const state = reactive({
 			isShowPassword: false,
 			ruleForm: {
 				userName: 'superadmin',
 				password: '123456',
 				code: '1234',
+				codeId: 0,
+			},
+			rules: {
+				userName: [{ required: true, message: "请输入用户名", trigger: "blur" }],
+				password: [{ required: true, message: "请输入密码", trigger: "blur" }],
+				code: [{ required: true, message: "请输入验证码", trigger: "blur" }],
 			},
 			loading: {
 				signIn: false,
 			},
+			verifyVisible: false,
+			isPass: false,
+			verifyImg: verifyImg,
+			captchaImage: "",
+		});
+		onMounted(() => {
+			getCaptcha();
 		});
+		// 获取验证码
+		const getCaptcha = async () => {
+			state.ruleForm.code = "";
+			var res = await getAPI(SysAuthApi).captchaGet();
+			state.captchaImage = "data:text/html;base64," + res.data.result?.img;
+			state.ruleForm.codeId = res.data.result?.id;
+		};
 		// 时间获取
 		const currentTime = computed(() => {
 			return formatAxis(new Date());
@@ -94,6 +131,8 @@ export default defineComponent({
 		const onSignIn = async () => {
 			var res = await getAPI(SysAuthApi).loginPost(state.ruleForm);
 			if (res.data.result?.token == null) {
+				getCaptcha(); // 重新获取验证码
+
 				ElMessage.error("登录失败,请检查账号!");
 				return;
 			}
@@ -137,8 +176,28 @@ export default defineComponent({
 			// 添加 loading,防止第一次进入界面时出现短暂空白
 			NextLoading.start();
 		};
+		// 打开旋转验证
+		const openVerify = () => {
+			ruleFormRef.value.validate((valid: boolean) => {
+				if (!valid) return false;
+
+				state.verifyVisible = true;
+				state.isPass = false;
+				dragRef.value.reset();
+			});
+		};
+		// 通过旋转验证
+		const passVerify = () => {
+			state.verifyVisible = false;
+			state.isPass = false;
+			onSignIn();
+		};
 		return {
-			onSignIn,
+			ruleFormRef,
+			dragRef,
+			openVerify,
+			passVerify,
+			getCaptcha,
 			...toRefs(state),
 		};
 	},
@@ -170,10 +229,26 @@ export default defineComponent({
 	}
 
 	.login-content-code {
-		width: 100%;
-		padding: 0;
-		font-weight: bold;
-		letter-spacing: 5px;
+		display: flex;
+		align-items: center;
+		justify-content: space-around;
+
+		.login-content-code-img {
+			width: 100%;
+			height: 40px;
+			line-height: 40px;
+			background-color: #ffffff;
+			border: 1px solid rgb(220, 223, 230);
+			cursor: pointer;
+			transition: all ease 0.2s;
+			border-radius: 4px;
+			user-select: none;
+
+			&:hover {
+				border-color: #c0c4cc;
+				transition: all ease 0.2s;
+			}
+		}
 	}
 
 	.login-content-submit {

+ 1 - 1
vue-next-admin/src/views/system/log/difflog/index.vue

@@ -21,7 +21,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px;">
+		<el-card shadow="hover" style="margin-top: 8px;">
 			<el-table :data="logData" style="width: 100%" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="diffType" label="差异操作" show-overflow-tooltip></el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/log/exlog/index.vue

@@ -21,7 +21,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px;">
+		<el-card shadow="hover" style="margin-top: 8px;">
 			<el-table :data="logData" style="width: 100%" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="logName" label="记录器类别名称" show-overflow-tooltip></el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/log/oplog/index.vue

@@ -17,7 +17,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px">
+		<el-card shadow="hover" style="margin-top: 8px">
 			<el-table :data="logData" style="width: 100%" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="logName" label="记录器类别名称" show-overflow-tooltip></el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/log/vislog/index.vue

@@ -21,7 +21,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px;">
+		<el-card shadow="hover" style="margin-top: 8px;">
 			<el-table :data="logData" style="width: 100%" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="userName" label="账号名称" show-overflow-tooltip></el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/menu/index.vue

@@ -24,7 +24,7 @@
       </el-form>
     </el-card>
 
-    <el-card shadow="hover" style="margin-top: 5px;">
+    <el-card shadow="hover" style="margin-top: 8px;">
       <el-table :data="menuData" v-loading="loading" row-key="id"
         :tree-props="{ children: 'children', hasChildren: 'hasChildren' }" border>
         <el-table-column label="菜单名称" show-overflow-tooltip>

+ 2 - 2
vue-next-admin/src/views/system/org/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="sys-org-container">
-		<el-row :gutter="5">
+		<el-row :gutter="8" style="width:100%">
 			<el-col :span="4" :xs="24">
 				<OrgTree @node-click='nodeClick' />
 			</el-col>
@@ -30,7 +30,7 @@
 					</el-form>
 				</el-card>
 
-				<el-card shadow="hover" style="margin-top: 5px;">
+				<el-card shadow="hover" style="margin-top: 8px;">
 					<el-table :data="orgData" style="width: 100%" v-loading="loading" row-key="id" default-expand-all
 						:tree-props="{ children: 'children', hasChildren: 'hasChildren' }" border>
 						<el-table-column prop="name" label="机构名称" show-overflow-tooltip> </el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/password/index.vue

@@ -2,7 +2,7 @@
 	<div class="sys-password-container">
 		<NoticeBar text="账号密码修改,请慎重操作!" leftIcon="iconfont icon-tongzhi2" background="var(--el-color-primary-light-9)"
 			color="var(--el-color-primary)" />
-		<el-card shadow="hover" header="修改当前账号密码" class="mt15">
+		<el-card shadow="hover" header="修改当前账号密码" class="mt8">
 			<el-form ref="ruleFormRef" :model="ruleForm" status-icon :rules="ruleRules" label-width="80px">
 				<el-form-item label="当前密码" prop="passwordOld">
 					<el-input v-model="ruleForm.passwordOld" type="password" autocomplete="off" />

+ 1 - 1
vue-next-admin/src/views/system/pos/index.vue

@@ -22,7 +22,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px;">
+		<el-card shadow="hover" style="margin-top: 8px;">
 			<el-table :data="posData" style="width: 100%" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" />
 				<el-table-column prop="name" label="职位名称" show-overflow-tooltip></el-table-column>

+ 1 - 1
vue-next-admin/src/views/system/role/index.vue

@@ -22,7 +22,7 @@
 			</el-form>
 		</el-card>
 
-		<el-card shadow="hover" style="margin-top: 5px;">
+		<el-card shadow="hover" style="margin-top: 8px;">
 			<el-table :data="roleData" style="width: 100%;" v-loading="loading" border>
 				<el-table-column type="index" label="序号" width="55" align="center" fixed />
 				<el-table-column prop="name" label="角色名称" show-overflow-tooltip>

+ 62 - 69
vue-next-admin/src/views/system/server/index.vue

@@ -1,11 +1,8 @@
 <template>
   <div class="sys-server-container">
-    <el-row :gutter="24">
-      <el-col :md="12"
-              :sm="24">
-        <el-card shadow="hover"
-                 header="系统信息"
-                 style="margin-bottom: 20px">
+    <el-row :gutter="8">
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" header="系统信息" style="margin-bottom;: 8px">
           <table class="sysInfo_table">
             <tr>
               <td class="sysInfo_td">主机名称:</td>
@@ -30,11 +27,8 @@
           </table>
         </el-card>
       </el-col>
-      <el-col :md="12"
-              :sm="24">
-        <el-card shadow="hover"
-                 header="网络信息"
-                 style="margin-bottom: 20px">
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" header="网络信息" style="margin-bottom;: 8px">
           <table class="sysInfo_table">
             <tr>
               <td class="sysInfo_td">外网信息:</td>
@@ -60,8 +54,7 @@
         </el-card>
       </el-col>
     </el-row>
-    <el-card shadow="hover"
-             header="其他信息">
+    <el-card shadow="hover" header="其他信息" style="margin-top: 8px">
       <table class="sysInfo_table">
         <tr>
           <td class="sysInfo_td">运行时间:</td>
@@ -77,7 +70,6 @@
         </tr>
       </table>
     </el-card>
-
   </div>
 </template>
 
@@ -88,68 +80,69 @@ import { getAPI } from '/@/utils/axios-utils';
 import { SysServerApi } from '/@/api-services';
 
 export default defineComponent({
-	name: 'sysServer',
-	components: {},
-	setup() {
-		const state = reactive({
-			loading: true,
-			machineBaseInfo: [] as any,
-			machineUseInfo: [] as any,
-			machineNetworkInfo: [] as any,
-			timer: null,
-		});
+  name: 'sysServer',
+  components: {},
+  setup() {
+    const state = reactive({
+      loading: true,
+      machineBaseInfo: [] as any,
+      machineUseInfo: [] as any,
+      machineNetworkInfo: [] as any,
+      timer: null as any,
+    });
 
-		// 服务器基本配置
-		const loadMachineBaseInfo = async () => {
-			var res = await getAPI(SysServerApi).serverBaseGet();
-			state.machineBaseInfo = res.data.result;
-		};
-		// 服务器使用资源
-		const loadMachineUseInfo = async () => {
-			var res = await getAPI(SysServerApi).serverUseGet();
-			state.machineUseInfo = res.data.result;
-		};
-		// 服务器网络信息
-		const loadMachineNetworkInfo = async () => {
-			var res = await getAPI(SysServerApi).serverNetworkGet();
-			state.machineNetworkInfo = res.data.result;
-		};
-		const refreshData = () => {
-			loadMachineUseInfo();
-			loadMachineNetworkInfo();
-		};
-		onActivated(() => {
-			state.timer = setInterval(() => {
-				refreshData();
-			}, 3000);
-		});
-		onDeactivated(() => {
-			clearInterval(state.timer);
-			// state.timer = null;
-		});
+    // 服务器基本配置
+    const loadMachineBaseInfo = async () => {
+      var res = await getAPI(SysServerApi).serverBaseGet();
+      state.machineBaseInfo = res.data.result;
+    };
+    // 服务器使用资源
+    const loadMachineUseInfo = async () => {
+      var res = await getAPI(SysServerApi).serverUseGet();
+      state.machineUseInfo = res.data.result;
+    };
+    // 服务器网络信息
+    const loadMachineNetworkInfo = async () => {
+      var res = await getAPI(SysServerApi).serverNetworkGet();
+      state.machineNetworkInfo = res.data.result;
+    };
+    const refreshData = () => {
+      loadMachineUseInfo();
+      loadMachineNetworkInfo();
+    };
+    onActivated(() => {
+      state.timer = setInterval(() => {
+        refreshData();
+      }, 3000);
+    });
+    onDeactivated(() => {
+      clearInterval(state.timer);
+      // state.timer = null;
+    });
 
-		loadMachineBaseInfo();
-		loadMachineUseInfo();
-		loadMachineNetworkInfo();
-		return {
-			loadMachineBaseInfo,
-			loadMachineUseInfo,
-			loadMachineNetworkInfo,
-			refreshData,
-			...toRefs(state),
-		};
-	},
+    loadMachineBaseInfo();
+    loadMachineUseInfo();
+    loadMachineNetworkInfo();
+    return {
+      loadMachineBaseInfo,
+      loadMachineUseInfo,
+      loadMachineNetworkInfo,
+      refreshData,
+      ...toRefs(state),
+    };
+  },
 });
 </script>
+
 <style lang="scss">
 .sysInfo_table {
-	width: 100%;
-	min-height: 45px;
-	line-height: 45px;
-	text-align: center;
+  width: 100%;
+  min-height: 45px;
+  line-height: 45px;
+  text-align: center;
 }
 
 .sysInfo_td {
-	border-bottom: 1px solid #e8e8e8;
+  border-bottom: 1px solid #e8e8e8;
 }
-</style>
+</style>

+ 2 - 2
vue-next-admin/src/views/system/user/index.vue

@@ -1,6 +1,6 @@
 <template>
 	<div class="sys-user-container">
-		<el-row :gutter="5">
+		<el-row :gutter="8" style="width:100%">
 			<el-col :span="4" :xs="24">
 				<OrgTree ref="orgTreeRef" @node-click='nodeClick' />
 			</el-col>
@@ -30,7 +30,7 @@
 					</el-form>
 				</el-card>
 
-				<el-card shadow="hover" style="margin-top: 5px;">
+				<el-card shadow="hover" style="margin-top: 8px;">
 					<el-table :data="userData" style="width: 100%;" v-loading="loading" border>
 						<el-table-column type="index" label="序号" width="55" align="center" fixed />
 						<el-table-column prop="userName" label="账号名称" width="120" fixed show-overflow-tooltip>