Przeglądaj źródła

Merge branch 'next' of https://gitee.com/zuohuaijun/Admin.NET into next

syxdevcode 3 lat temu
rodzic
commit
01629fe6a2
49 zmienionych plików z 3513 dodań i 431 usunięć
  1. 1 1
      Admin.NET/Admin.NET.Application/AppConfig.json
  2. 5 4
      Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj
  3. 232 52
      Admin.NET/Admin.NET.Core/Admin.NET.Core.xml
  4. 81 0
      Admin.NET/Admin.NET.Core/Captcha/LazyCaptchaSetup.cs
  5. 6 0
      Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs
  6. 174 1
      Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs
  7. 2 2
      Admin.NET/Admin.NET.Core/SeedData/SysRoleSeedData.cs
  8. 10 0
      Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs
  9. 23 1
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  10. 9 0
      Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs
  11. 2 2
      Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs
  12. 85 11
      Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs
  13. 1 1
      Admin.NET/Admin.NET.Core/Service/WeChat/WeChatPayService.cs
  14. 0 30
      Admin.NET/Admin.NET.Core/Util/CommonUtil.cs
  15. 309 0
      Admin.NET/Admin.NET.Core/Util/ComputerUtil.cs
  16. 136 0
      Admin.NET/Admin.NET.Core/Util/DateTimeUtil.cs
  17. 0 176
      Admin.NET/Admin.NET.Core/Util/ServerUtil.cs
  18. 0 55
      Admin.NET/Admin.NET.Core/Util/ShellUtil.cs
  19. 3 0
      Admin.NET/Admin.NET.Web.Core/Startup.cs
  20. 1 1
      vue-next-admin/.env.development
  21. 69 0
      vue-next-admin/src/api-services/apis/sys-auth-api.ts
  22. 88 20
      vue-next-admin/src/api-services/apis/sys-server-api.ts
  23. 3 2
      vue-next-admin/src/api-services/models/job-status-enum.ts
  24. 12 0
      vue-next-admin/src/api-services/models/login-input.ts
  25. 3 3
      vue-next-admin/src/api-services/models/login-type-enum.ts
  26. 19 1
      vue-next-admin/src/api-services/models/sys-log-ex.ts
  27. 19 1
      vue-next-admin/src/api-services/models/sys-log-op.ts
  28. 2 1
      vue-next-admin/src/api-services/models/user-type-enum.ts
  29. 300 0
      vue-next-admin/src/components/dragVerify/dragVerify.vue
  30. 466 0
      vue-next-admin/src/components/dragVerify/dragVerifyImg.vue
  31. 486 0
      vue-next-admin/src/components/dragVerify/dragVerifyImgChip.vue
  32. 449 0
      vue-next-admin/src/components/dragVerify/dragVerifyImgRotate.vue
  33. 5 1
      vue-next-admin/src/theme/app.scss
  34. 7 4
      vue-next-admin/src/theme/element.scss
  35. 6 1
      vue-next-admin/src/utils/authDirective.ts
  36. 91 16
      vue-next-admin/src/views/login/component/account.vue
  37. 1 1
      vue-next-admin/src/views/system/log/difflog/index.vue
  38. 1 1
      vue-next-admin/src/views/system/log/exlog/index.vue
  39. 46 21
      vue-next-admin/src/views/system/log/oplog/index.vue
  40. 1 1
      vue-next-admin/src/views/system/log/vislog/index.vue
  41. 1 1
      vue-next-admin/src/views/system/menu/index.vue
  42. 2 1
      vue-next-admin/src/views/system/org/component/orgTree.vue
  43. 2 2
      vue-next-admin/src/views/system/org/index.vue
  44. 1 1
      vue-next-admin/src/views/system/password/index.vue
  45. 1 1
      vue-next-admin/src/views/system/pos/index.vue
  46. 1 1
      vue-next-admin/src/views/system/role/component/editRole.vue
  47. 1 1
      vue-next-admin/src/views/system/role/index.vue
  48. 338 0
      vue-next-admin/src/views/system/server/index.vue
  49. 12 12
      vue-next-admin/src/views/system/user/index.vue

+ 1 - 1
Admin.NET/Admin.NET.Application/AppConfig.json

@@ -8,7 +8,7 @@
                 "ConfigId": "Dilon",
                 "DbType": "Sqlite", // MySql、SqlServer、Sqlite、Oracle、PostgreSQL、Dm、Kdbndp、Oscar、MySqlConnector、Access
                 "ConnectionString": "DataSource=./Admin.NET.db",
-                "EnableInitDb": true, // 启用库表初始化
+                "EnableInitDb": false, // 启用库表初始化
                 "EnableDiffLog": false // 启用库表差异日志
             },
             // 其他业务库

+ 5 - 4
Admin.NET/Admin.NET.Core/Admin.NET.Core.csproj

@@ -24,9 +24,10 @@
 
   <ItemGroup>
     <PackageReference Include="AspNetCoreRateLimit" Version="4.0.2" />
-    <PackageReference Include="Furion.Extras.Authentication.JwtBearer" Version="4.6.4" />
-    <PackageReference Include="Furion.Extras.ObjectMapper.Mapster" Version="4.6.4" />
-    <PackageReference Include="Furion.Pure" Version="4.6.4" />
+    <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" />

+ 232 - 52
Admin.NET/Admin.NET.Core/Admin.NET.Core.xml

@@ -30,6 +30,23 @@
             SqlSugar二级缓存(必须是内存缓存)
             </summary>
         </member>
+        <member name="M:Admin.NET.Core.LazyCaptchaSetup.AddLazyCaptcha(Microsoft.Extensions.DependencyInjection.IServiceCollection)">
+            <summary>
+            验证码初始化
+            </summary>
+            <param name="services"></param>
+        </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.CacheConst">
             <summary>
             缓存相关常量
@@ -1931,6 +1948,11 @@
             系统错误码
             </summary>
         </member>
+        <member name="F:Admin.NET.Core.ErrorCodeEnum.D0009">
+            <summary>
+            验证码错误
+            </summary>
+        </member>
         <member name="F:Admin.NET.Core.ErrorCodeEnum.D1000">
             <summary>
             用户名或密码不正确
@@ -2935,6 +2957,51 @@
             <param name="pi"></param>
             <returns></returns>
         </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToLong(System.Object)">
+            <summary>
+            将object转换为long,若失败则返回0
+            </summary>
+            <param name="obj"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToLong(System.String,System.Int64)">
+            <summary>
+            将object转换为long,若失败则返回指定值
+            </summary>
+            <param name="str"></param>
+            <param name="defaultValue"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToDouble(System.Object)">
+            <summary>
+            将object转换为double,若失败则返回0  
+            </summary>
+            <param name="obj"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToDouble(System.Object,System.Double)">
+            <summary>
+            将object转换为double,若失败则返回指定值 
+            </summary>
+            <param name="str"></param>
+            <param name="defaultValue"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToDateTime(System.String)">
+            <summary>
+            将string转换为DateTime,若失败则返回日期最小值 
+            </summary>
+            <param name="str"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.ObjectExtension.ParseToDateTime(System.String,System.Nullable{System.DateTime})">
+            <summary>
+            将string转换为DateTime,若失败则返回默认值  
+            </summary>
+            <param name="str"></param>
+            <param name="defaultValue"></param>
+            <returns></returns>
+        </member>
         <member name="T:Admin.NET.Core.RepositoryExtension">
             <summary>
             仓储拓展
@@ -3609,6 +3676,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 +3809,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登录检查
@@ -6309,21 +6392,27 @@
             系统服务器监控服务
             </summary>
         </member>
-        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerBaseInfo">
+        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerBase">
             <summary>
-            服务器基本配置
+            服务器配置信息
             </summary>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerUseInfo">
+        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerUsed">
             <summary>
-            服务器使用资源
+            服务器内存信息
             </summary>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerNetWorkInfo">
+        <member name="M:Admin.NET.Core.Service.SysServerService.GetServerDisk">
             <summary>
-            服务器网络信息
+            服务器磁盘信息
+            </summary>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.Service.SysServerService.GetAssembly">
+            <summary>
+            框架主要程序集
             </summary>
             <returns></returns>
         </member>
@@ -7478,120 +7567,211 @@
             </summary>
             <returns></returns>
         </member>
-        <member name="T:Admin.NET.Core.CommonUtil">
+        <member name="M:Admin.NET.Core.ComputerUtil.GetComputerInfo">
             <summary>
-            通用工具类
+            内存信息
             </summary>
+            <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.CommonUtil.GetTodayTimeList(System.DateTime)">
+        <member name="M:Admin.NET.Core.ComputerUtil.GetDiskInfos">
             <summary>
-            获取今天日期范围00:00:00 - 23:59:59
+            磁盘信息
             </summary>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.CommonUtil.GetRootPathFileText(System.String)">
+        <member name="M:Admin.NET.Core.ComputerUtil.GetIpFromPCOnline">
             <summary>
-            获取项目根目录文件内容
+            IP地址信息
             </summary>
-            <param name="path"></param>
             <returns></returns>
         </member>
-        <member name="T:Admin.NET.Core.LongJsonConverter">
+        <member name="M:Admin.NET.Core.ComputerUtil.GetRunTime">
             <summary>
-            序列化时long转string(防止js精度溢出)
+            获取系统运行时间
             </summary>
+            <returns></returns>
         </member>
-        <member name="T:Admin.NET.Core.ReflectionUtil">
+        <member name="T:Admin.NET.Core.MemoryMetrics">
             <summary>
-            反射工具类
+            内存信息
             </summary>
         </member>
-        <member name="M:Admin.NET.Core.ReflectionUtil.GetDescriptionValue``1(System.Reflection.FieldInfo)">
+        <member name="P:Admin.NET.Core.MemoryMetrics.UsedRam">
             <summary>
-            获取字段特性
+            已用内存
             </summary>
-            <param name="field"></param>
-            <typeparam name="T"></typeparam>
-            <returns></returns>
         </member>
-        <member name="T:Admin.NET.Core.ServerUtil">
+        <member name="P:Admin.NET.Core.MemoryMetrics.CpuRate">
             <summary>
-            服务器信息
+            CPU使用率%
             </summary>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetServerBaseInfo">
+        <member name="P:Admin.NET.Core.MemoryMetrics.TotalRam">
             <summary>
-            服务器基本配置
+            总内存 GB
             </summary>
-            <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetServerUseInfo">
+        <member name="P:Admin.NET.Core.MemoryMetrics.RamRate">
             <summary>
-            服务器使用资源
+            内存使用率 %
             </summary>
-            <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetServerNetWorkInfo">
+        <member name="P:Admin.NET.Core.MemoryMetrics.FreeRam">
             <summary>
-            服务器网络信息
+            空闲内存
+            </summary>
+        </member>
+        <member name="T:Admin.NET.Core.DiskInfo">
+            <summary>
+            磁盘信息
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.DiskName">
+            <summary>
+            磁盘名
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.TypeName">
+            <summary>
+            类型名
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.TotalFree">
+            <summary>
+            总剩余
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.TotalSize">
+            <summary>
+            总量
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.Used">
+            <summary>
+            已使用
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.AvailableFreeSpace">
+            <summary>
+            可使用
+            </summary>
+        </member>
+        <member name="P:Admin.NET.Core.DiskInfo.AvailablePercent">
+            <summary>
+            使用百分比
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.MemoryMetricsClient.GetWindowsMetrics">
+            <summary>
+            windows系统获取内存信息
             </summary>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetRamInfo">
+        <member name="M:Admin.NET.Core.MemoryMetricsClient.GetUnixMetrics">
             <summary>
-            内存信息
+            Unix系统获取
             </summary>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetCpuRate">
+        <member name="M:Admin.NET.Core.ShellUtil.Bash(System.String)">
             <summary>
-            CPU信息
+            linux 系统命令
             </summary>
+            <param name="command"></param>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetRunTime">
+        <member name="M:Admin.NET.Core.ShellUtil.Cmd(System.String,System.String)">
             <summary>
-            系统运行时间
+            windows系统命令
             </summary>
+            <param name="fileName"></param>
+            <param name="args"></param>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.IsUnix">
+        <member name="M:Admin.NET.Core.DateTimeUtil.GetBeginTime(System.Nullable{System.DateTime},System.Int32)">
             <summary>
-            是否Linux
+            获取开始时间
             </summary>
+            <param name="dateTime"></param>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.FormatTime(System.Int64)">
+        <member name="M:Admin.NET.Core.DateTimeUtil.ToLocalTimeDateBySeconds(System.Int64)">
+            <summary>
+             时间戳转本地时间-时间戳精确到秒
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.DateTimeUtil.ToUnixTimestampBySeconds(System.DateTime)">
+            <summary>
+             时间转时间戳Unix-时间戳精确到秒
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.DateTimeUtil.ToLocalTimeDateByMilliseconds(System.Int64)">
+            <summary>
+             时间戳转本地时间-时间戳精确到毫秒
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.DateTimeUtil.ToUnixTimestampByMilliseconds(System.DateTime)">
+            <summary>
+             时间转时间戳Unix-时间戳精确到毫秒
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.DateTimeUtil.FormatTime(System.Int64)">
             <summary>
             毫秒转天时分秒
             </summary>
             <param name="ms"></param>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ServerUtil.GetWanIpFromPCOnline">
+        <member name="M:Admin.NET.Core.DateTimeUtil.GetUnixTimeStamp(System.DateTime)">
             <summary>
-            获取外网IP和位置
+            获取unix时间戳
             </summary>
+            <param name="dt"></param>
             <returns></returns>
         </member>
-        <member name="T:Admin.NET.Core.ShellUtil">
+        <member name="M:Admin.NET.Core.DateTimeUtil.GetDayMinDate(System.DateTime)">
             <summary>
-            系统Shell命令
+            获取日期天的最小时间
             </summary>
+            <param name="dt"></param>
+            <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ShellUtil.Bash(System.String)">
+        <member name="M:Admin.NET.Core.DateTimeUtil.GetDayMaxDate(System.DateTime)">
             <summary>
-            Bash命令
+            获取日期天的最大时间
             </summary>
-            <param name="command"></param>
+            <param name="dt"></param>
             <returns></returns>
         </member>
-        <member name="M:Admin.NET.Core.ShellUtil.Cmd(System.String,System.String)">
+        <member name="M:Admin.NET.Core.DateTimeUtil.FormatDateTime(System.Nullable{System.DateTime})">
             <summary>
-            cmd命令
+            获取日期天的最大时间
             </summary>
-            <param name="fileName"></param>
-            <param name="args"></param>
+            <param name="dt"></param>
+            <returns></returns>
+        </member>
+        <member name="M:Admin.NET.Core.DateTimeUtil.GetTodayTimeList(System.DateTime)">
+            <summary>
+            获取今天日期范围00:00:00 - 23:59:59
+            </summary>
+            <returns></returns>
+        </member>
+        <member name="T:Admin.NET.Core.LongJsonConverter">
+            <summary>
+            序列化时long转string(防止js精度溢出)
+            </summary>
+        </member>
+        <member name="T:Admin.NET.Core.ReflectionUtil">
+            <summary>
+            反射工具类
+            </summary>
+        </member>
+        <member name="M:Admin.NET.Core.ReflectionUtil.GetDescriptionValue``1(System.Reflection.FieldInfo)">
+            <summary>
+            获取字段特性
+            </summary>
+            <param name="field"></param>
+            <typeparam name="T"></typeparam>
             <returns></returns>
         </member>
         <member name="T:Admin.NET.Core.ByteUtil">

+ 81 - 0
Admin.NET/Admin.NET.Core/Captcha/LazyCaptchaSetup.cs

@@ -0,0 +1,81 @@
+using Lazy.Captcha.Core;
+using Lazy.Captcha.Core.Generator;
+using Lazy.Captcha.Core.Storage;
+
+namespace Admin.NET.Core;
+
+public static class LazyCaptchaSetup
+{
+    /// <summary>
+    /// 验证码初始化
+    /// </summary>
+    /// <param name="services"></param>
+    public static void AddLazyCaptcha(this IServiceCollection services)
+    {
+        services.AddCaptcha(App.Configuration);
+        services.AddScoped<ICaptcha, RandomCaptcha>();
+    }
+}
+
+/// <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; // 字体
+    }
+}

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

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

+ 174 - 1
Admin.NET/Admin.NET.Core/Extension/ObjectExtension.cs

@@ -4,7 +4,7 @@ namespace Admin.NET.Core;
 /// 对象拓展
 /// </summary>
 [SuppressSniffer]
-public static class ObjectExtension
+public static partial class ObjectExtension
 {
     /// <summary>
     /// 判断类型是否实现某个泛型
@@ -125,4 +125,177 @@ public static class ObjectExtension
         var sc = pi.GetCustomAttributes<SugarColumn>(false).FirstOrDefault(u => u.IsIgnore == true);
         return sc != null;
     }
+
+    /// <summary>
+    /// 将object转换为long,若失败则返回0
+    /// </summary>
+    /// <param name="obj"></param>
+    /// <returns></returns>
+    public static long ParseToLong(this object obj)
+    {
+        try
+        {
+            return long.Parse(obj.ToString());
+        }
+        catch
+        {
+            return 0L;
+        }
+    }
+
+    /// <summary>
+    /// 将object转换为long,若失败则返回指定值
+    /// </summary>
+    /// <param name="str"></param>
+    /// <param name="defaultValue"></param>
+    /// <returns></returns>
+    public static long ParseToLong(this string str, long defaultValue)
+    {
+        try
+        {
+            return long.Parse(str);
+        }
+        catch
+        {
+            return defaultValue;
+        }
+    }
+
+    /// <summary>
+    /// 将object转换为double,若失败则返回0
+    /// </summary>
+    /// <param name="obj"></param>
+    /// <returns></returns>
+    public static double ParseToDouble(this object obj)
+    {
+        try
+        {
+            return double.Parse(obj.ToString());
+        }
+        catch
+        {
+            return 0;
+        }
+    }
+
+    /// <summary>
+    /// 将object转换为double,若失败则返回指定值
+    /// </summary>
+    /// <param name="str"></param>
+    /// <param name="defaultValue"></param>
+    /// <returns></returns>
+    public static double ParseToDouble(this object str, double defaultValue)
+    {
+        try
+        {
+            return double.Parse(str.ToString());
+        }
+        catch
+        {
+            return defaultValue;
+        }
+    }
+
+    /// <summary>
+    /// 将string转换为DateTime,若失败则返回日期最小值
+    /// </summary>
+    /// <param name="str"></param>
+    /// <returns></returns>
+    public static DateTime ParseToDateTime(this string str)
+    {
+        try
+        {
+            if (string.IsNullOrWhiteSpace(str))
+            {
+                return DateTime.MinValue;
+            }
+            if (str.Contains("-") || str.Contains("/"))
+            {
+                return DateTime.Parse(str);
+            }
+            else
+            {
+                int length = str.Length;
+                switch (length)
+                {
+                    case 4:
+                        return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 6:
+                        return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 8:
+                        return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 10:
+                        return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 12:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 14:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
+
+                    default:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
+                }
+            }
+        }
+        catch
+        {
+            return DateTime.MinValue;
+        }
+    }
+
+    /// <summary>
+    /// 将string转换为DateTime,若失败则返回默认值
+    /// </summary>
+    /// <param name="str"></param>
+    /// <param name="defaultValue"></param>
+    /// <returns></returns>
+    public static DateTime ParseToDateTime(this string str, DateTime? defaultValue)
+    {
+        try
+        {
+            if (string.IsNullOrWhiteSpace(str))
+            {
+                return defaultValue.GetValueOrDefault();
+            }
+            if (str.Contains("-") || str.Contains("/"))
+            {
+                return DateTime.Parse(str);
+            }
+            else
+            {
+                int length = str.Length;
+                switch (length)
+                {
+                    case 4:
+                        return DateTime.ParseExact(str, "yyyy", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 6:
+                        return DateTime.ParseExact(str, "yyyyMM", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 8:
+                        return DateTime.ParseExact(str, "yyyyMMdd", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 10:
+                        return DateTime.ParseExact(str, "yyyyMMddHH", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 12:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmm", System.Globalization.CultureInfo.CurrentCulture);
+
+                    case 14:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
+
+                    default:
+                        return DateTime.ParseExact(str, "yyyyMMddHHmmss", System.Globalization.CultureInfo.CurrentCulture);
+                }
+            }
+        }
+        catch
+        {
+            return defaultValue.GetValueOrDefault();
+        }
+    }
 }

+ 2 - 2
Admin.NET/Admin.NET.Core/SeedData/SysRoleSeedData.cs

@@ -13,11 +13,11 @@ public class SysRoleSeedData : ISqlSugarEntitySeedData<SysRole>
     {
         return new[]
         {
-            new SysRole{ Id=252885263003721, Name="管理员", DataScope=DataScopeEnum.Dept_with_child, Code="admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="管理员", TenantId=142307070918780 },
+            new SysRole{ Id=252885263003721, Name="管理员", DataScope=DataScopeEnum.All, Code="admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="管理员", TenantId=142307070918780 },
             new SysRole{ Id=252885263003722, Name="普通用户", DataScope=DataScopeEnum.Self, Code="user", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="普通用户", TenantId=142307070918780 },
             new SysRole{ Id=252885263003723, Name="游客", DataScope=DataScopeEnum.Define, Code="guest", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="游客", TenantId=142307070918780 },
 
-            new SysRole{ Id=252885263003724, Name="管理员", DataScope=DataScopeEnum.Dept_with_child, Code="admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="管理员", TenantId=142307070918781 },
+            new SysRole{ Id=252885263003724, Name="管理员", DataScope=DataScopeEnum.All, Code="admin", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="管理员", TenantId=142307070918781 },
             new SysRole{ Id=252885263003725, Name="普通用户", DataScope=DataScopeEnum.Self, Code="user", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="普通用户",  TenantId=142307070918781 },
             new SysRole{ Id=252885263003726, Name="游客", DataScope=DataScopeEnum.Define, Code="guest", CreateTime=DateTime.Parse("2022-02-10 00:00:00"), Remark="游客",  TenantId=142307070918781 },
         };

+ 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>

+ 9 - 0
Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs

@@ -42,6 +42,15 @@ public class SysMenuService : IDynamicApiController, ITransient
         else
         {
             var menuIdList = await GetMenuIdList();
+            //// 获取所有节点的父节点(当半选状态的父节点未存储时)
+            //var treeIdList = new List<long>();
+            //foreach (var cId in menuIdList)
+            //{
+            //    if (cId < 1)
+            //        continue;
+            //    var pIds = _sysMenuRep.AsQueryable().ToParentList(u => u.Pid, cId).Select(u => u.Id).ToList();
+            //    treeIdList = treeIdList.Union(pIds).ToList();
+            //}
             var menuList = await _sysMenuRep.AsQueryable()
                 .Where(u => u.Type != MenuTypeEnum.Btn)
                 .Where(u => menuIdList.Contains(u.Id))

+ 2 - 2
Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs

@@ -202,7 +202,7 @@ public class SysOrgService : IDynamicApiController, ITransient
             // 角色机构集合
             var orgList2 = await GetUserRoleOrgIdList(userId);
             // 并集机构集合
-            orgIdList = orgList1.Concat(orgList2).Distinct().ToList();
+            orgIdList = orgList1.Union(orgList2).ToList();
             _sysCacheService.SetOrgIdList(userId, orgIdList); // 存缓存
         }
         return orgIdList;
@@ -254,7 +254,7 @@ public class SysOrgService : IDynamicApiController, ITransient
         _sysCacheService.SetMaxDataScopeType(_userManager.UserId, strongerDataScopeType);
 
         // 并集机构集合
-        return orgIdList1.Concat(orgIdList2).Distinct().ToList();
+        return orgIdList1.Union(orgIdList2).ToList();
     }
 
     /// <summary>

+ 85 - 11
Admin.NET/Admin.NET.Core/Service/Server/SysServerService.cs

@@ -1,4 +1,13 @@
-namespace Admin.NET.Core.Service;
+using AspNetCoreRateLimit;
+using Lazy.Captcha.Core;
+using Magicodes.ExporterAndImporter.Excel;
+using Magicodes.ExporterAndImporter.Pdf;
+using Microsoft.Extensions.Hosting;
+using Nest;
+using NewLife.Caching;
+using OnceMi.AspNetCore.OSS;
+
+namespace Admin.NET.Core.Service;
 
 /// <summary>
 /// 系统服务器监控服务
@@ -11,32 +20,97 @@ public class SysServerService : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 服务器基本配置
+    /// 服务器配置信息
     /// </summary>
     /// <returns></returns>
     [HttpGet("/server/base")]
-    public async Task<dynamic> GetServerBaseInfo()
+    public dynamic GetServerBase()
     {
-        return await Task.FromResult(ServerUtil.GetServerBaseInfo());
+        return new
+        {
+            HostName = Environment.MachineName, // 主机名称
+            SystemOs = RuntimeInformation.OSDescription, // 操作系统
+            OsArchitecture = Environment.OSVersion.Platform.ToString() + " " + RuntimeInformation.OSArchitecture.ToString(), // 系统架构
+            ProcessorCount = Environment.ProcessorCount + " 核", // CPU核心数
+            SysRunTime = ComputerUtil.GetRunTime(), // 系统运行时间
+            RemoteIp = ComputerUtil.GetIpFromPCOnline(), // 外网地址
+            LocalIp = App.HttpContext?.Connection?.LocalIpAddress.ToString(), // 本地地址
+            FrameworkDescription = RuntimeInformation.FrameworkDescription, // NET框架
+            Environment = App.HostEnvironment.IsDevelopment() ? "Development" : "Production",
+        };
     }
 
     /// <summary>
-    /// 服务器使用资源
+    /// 服务器内存信息
     /// </summary>
     /// <returns></returns>
     [HttpGet("/server/use")]
-    public async Task<dynamic> GetServerUseInfo()
+    public dynamic GetServerUsed()
     {
-        return await Task.FromResult(ServerUtil.GetServerUseInfo());
+        var programStartTime = Process.GetCurrentProcess().StartTime;
+        var programRunTime = DateTimeUtil.FormatTime((DateTime.Now - programStartTime).TotalMilliseconds.ToString().Split('.')[0].ParseToLong());
+
+        var memoryMetrics = ComputerUtil.GetComputerInfo();
+        return new
+        {
+            memoryMetrics.FreeRam, // 空闲内存
+            memoryMetrics.UsedRam, // 已用内存
+            memoryMetrics.TotalRam, // 总内存
+            memoryMetrics.RamRate, // 内存使用率
+            memoryMetrics.CpuRate, // Cpu使用率
+            StartTime = programStartTime.ToString("yyyy-MM-dd HH:mm:ss"), // 服务启动时间
+            RunTime = programRunTime, // 服务运行时间
+        };
     }
 
     /// <summary>
-    /// 服务器网络信息
+    /// 服务器磁盘信息
     /// </summary>
     /// <returns></returns>
-    [HttpGet("/server/network")]
-    public async Task<dynamic> GetServerNetWorkInfo()
+    [HttpGet("/server/disk")]
+    public dynamic GetServerDisk()
     {
-        return await ServerUtil.GetServerNetWorkInfo();
+        return ComputerUtil.GetDiskInfos();
+    }
+
+    /// <summary>
+    /// 框架主要程序集
+    /// </summary>
+    /// <returns></returns>
+    [HttpGet("/server/assembly")]
+    public dynamic GetAssembly()
+    {
+        var furionAssembly = typeof(App).Assembly.GetName();
+        var sqlSugarAssembly = typeof(ISqlSugarClient).Assembly.GetName();
+        var yitIdAssembly = typeof(Yitter.IdGenerator.YitIdHelper).Assembly.GetName();
+        var redisAssembly = typeof(Redis).Assembly.GetName();
+        var jsonAssembly = typeof(NewtonsoftJsonMvcCoreBuilderExtensions).Assembly.GetName();
+        var excelAssembly = typeof(IExcelImporter).Assembly.GetName();
+        var pdfAssembly = typeof(IPdfExporter).Assembly.GetName();
+        var captchaAssembly = typeof(ICaptcha).Assembly.GetName();
+        var wechatApiAssembly = typeof(WechatApiClient).Assembly.GetName();
+        var wechatTenpayAssembly = typeof(WechatTenpayClient).Assembly.GetName();
+        var ossAssembly = typeof(IOSSServiceFactory).Assembly.GetName();
+        var parserAssembly = typeof(Parser).Assembly.GetName();
+        var nestAssembly = typeof(IElasticClient).Assembly.GetName();
+        var limitAssembly = typeof(IpRateLimitMiddleware).Assembly.GetName();
+
+        return new[]
+        {
+            new { furionAssembly.Name, furionAssembly.Version },
+            new { sqlSugarAssembly.Name, sqlSugarAssembly.Version },
+            new { yitIdAssembly.Name, yitIdAssembly.Version },
+            new { redisAssembly.Name, redisAssembly.Version },
+            new { jsonAssembly.Name, jsonAssembly.Version },
+            new { excelAssembly.Name, excelAssembly.Version },
+            new { pdfAssembly.Name, pdfAssembly.Version },
+            new { captchaAssembly.Name, captchaAssembly.Version },
+            new { wechatApiAssembly.Name, wechatApiAssembly.Version },
+            new { wechatTenpayAssembly.Name, wechatTenpayAssembly.Version },
+            new { ossAssembly.Name, ossAssembly.Version },
+            new { parserAssembly.Name, parserAssembly.Version },
+            new { nestAssembly.Name, nestAssembly.Version },
+            new { limitAssembly.Name, limitAssembly.Version },
+        };
     }
 }

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/WeChat/WeChatPayService.cs

@@ -33,7 +33,7 @@ public class WeChatPayService : IDynamicApiController, ITransient
             MerchantId = _weChatPayOptions.MerchantId,
             MerchantV3Secret = _weChatPayOptions.MerchantV3Secret,
             MerchantCertificateSerialNumber = _weChatPayOptions.MerchantCertificateSerialNumber,
-            MerchantCertificatePrivateKey = CommonUtil.GetRootPathFileText(_weChatPayOptions.MerchantCertificatePrivateKey),
+            MerchantCertificatePrivateKey = File.ReadAllText(App.WebHostEnvironment.ContentRootPath + _weChatPayOptions.MerchantCertificatePrivateKey),
             PlatformCertificateManager = new InMemoryCertificateManager()
         };
         return new WechatTenpayClient(tenpayClientOptions);

+ 0 - 30
Admin.NET/Admin.NET.Core/Util/CommonUtil.cs

@@ -1,30 +0,0 @@
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 通用工具类
-/// </summary>
-public static class CommonUtil
-{
-    /// <summary>
-    /// 获取今天日期范围00:00:00 - 23:59:59
-    /// </summary>
-    /// <returns></returns>
-    public static List<DateTime> GetTodayTimeList(DateTime time)
-    {
-        return new List<DateTime>
-        {
-            Convert.ToDateTime(time.ToString("D").ToString()),
-            Convert.ToDateTime(time.AddDays(1).ToString("D").ToString()).AddSeconds(-1)
-        };
-    }
-
-    /// <summary>
-    /// 获取项目根目录文件内容
-    /// </summary>
-    /// <param name="path"></param>
-    /// <returns></returns>
-    public static string GetRootPathFileText(string path)
-    {
-        return File.ReadAllText(App.WebHostEnvironment.ContentRootPath + path);
-    }
-}

+ 309 - 0
Admin.NET/Admin.NET.Core/Util/ComputerUtil.cs

@@ -0,0 +1,309 @@
+namespace Admin.NET.Core;
+
+public static class ComputerUtil
+{
+    /// <summary>
+    /// 内存信息
+    /// </summary>
+    /// <returns></returns>
+    public static MemoryMetrics GetComputerInfo()
+    {
+        MemoryMetricsClient client = new();
+        MemoryMetrics memoryMetrics = IsUnix() ? client.GetUnixMetrics() : client.GetWindowsMetrics();
+
+        memoryMetrics.FreeRam = Math.Round(memoryMetrics.Free / 1024, 2) + "GB";
+        memoryMetrics.UsedRam = Math.Round(memoryMetrics.Used / 1024, 2) + "GB";
+        memoryMetrics.TotalRam = Math.Round(memoryMetrics.Total / 1024, 2) + "GB";
+        memoryMetrics.RamRate = Math.Ceiling(100 * memoryMetrics.Used / memoryMetrics.Total).ToString() + "%";
+        memoryMetrics.CpuRate = Math.Ceiling(GetCPURate().ParseToDouble()) + "%";
+        return memoryMetrics;
+    }
+
+    /// <summary>
+    /// 磁盘信息
+    /// </summary>
+    /// <returns></returns>
+    public static List<DiskInfo> GetDiskInfos()
+    {
+        List<DiskInfo> diskInfos = new();
+
+        if (IsUnix())
+        {
+            string output = ShellUtil.Bash("df -m / | awk '{print $2,$3,$4,$5,$6}'");
+            var arr = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
+            if (arr.Length == 0) return diskInfos;
+
+            var rootDisk = arr[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
+            if (rootDisk == null || rootDisk.Length == 0)
+                return diskInfos;
+
+            DiskInfo diskInfo = new()
+            {
+                DiskName = "/",
+                TotalSize = long.Parse(rootDisk[0]) / 1024,
+                Used = long.Parse(rootDisk[1]) / 1024,
+                AvailableFreeSpace = long.Parse(rootDisk[2]) / 1024,
+                AvailablePercent = decimal.Parse(rootDisk[3].Replace("%", ""))
+            };
+            diskInfos.Add(diskInfo);
+        }
+        else
+        {
+            var driv = DriveInfo.GetDrives();
+            foreach (var item in driv)
+            {
+                var obj = new DiskInfo()
+                {
+                    DiskName = item.Name,
+                    TypeName = item.DriveType.ToString(),
+                    TotalSize = item.TotalSize / 1024 / 1024 / 1024,
+                    AvailableFreeSpace = item.AvailableFreeSpace / 1024 / 1024 / 1024,
+                };
+                obj.Used = obj.TotalSize - obj.AvailableFreeSpace;
+                obj.AvailablePercent = decimal.Ceiling(obj.Used / (decimal)obj.TotalSize * 100);
+                diskInfos.Add(obj);
+            }
+        }
+        return diskInfos;
+    }
+
+    /// <summary>
+    /// IP地址信息
+    /// </summary>
+    /// <returns></returns>
+    public static string GetIpFromPCOnline()
+    {
+        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
+
+        var url = "http://whois.pconline.com.cn/ipJson.jsp";
+        var stream = url.GetAsStreamAsync().GetAwaiter().GetResult();
+        var streamReader = new StreamReader(stream.Stream, stream.Encoding);
+        var html = streamReader.ReadToEnd();
+        var tmp = html[(html.IndexOf("({") + 2)..].Split(",");
+        var ipAddr = tmp[0].Split(":")[1] + "【" + tmp[7].Split(":")[1] + "】";
+        return ipAddr.Replace("\"", "");
+    }
+
+    public static bool IsUnix()
+    {
+        return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
+    }
+
+    public static string GetCPURate()
+    {
+        string cpuRate;
+        if (IsUnix())
+        {
+            string output = ShellUtil.Bash("top -b -n1 | grep \"Cpu(s)\" | awk '{print $2 + $4}'");
+            cpuRate = output.Trim();
+        }
+        else
+        {
+            string output = ShellUtil.Cmd("wmic", "cpu get LoadPercentage");
+            cpuRate = output.Replace("LoadPercentage", string.Empty).Trim();
+        }
+        return cpuRate;
+    }
+
+    /// <summary>
+    /// 获取系统运行时间
+    /// </summary>
+    /// <returns></returns>
+    public static string GetRunTime()
+    {
+        string runTime = string.Empty;
+        if (IsUnix())
+        {
+            string output = ShellUtil.Bash("uptime -s").Trim();
+            runTime = DateTimeUtil.FormatTime((DateTime.Now - output.ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong());
+        }
+        else
+        {
+            string output = ShellUtil.Cmd("wmic", "OS get LastBootUpTime/Value");
+            string[] outputArr = output.Split('=', (char)StringSplitOptions.RemoveEmptyEntries);
+            if (outputArr.Length == 2)
+                runTime = DateTimeUtil.FormatTime((DateTime.Now - outputArr[1].Split('.')[0].ParseToDateTime()).TotalMilliseconds.ToString().Split('.')[0].ParseToLong());
+        }
+        return runTime;
+    }
+}
+
+/// <summary>
+/// 内存信息
+/// </summary>
+public class MemoryMetrics
+{
+    [JsonIgnore]
+    public double Total { get; set; }
+
+    [JsonIgnore]
+    public double Used { get; set; }
+
+    [JsonIgnore]
+    public double Free { get; set; }
+
+    /// <summary>
+    /// 已用内存
+    /// </summary>
+    public string UsedRam { get; set; }
+
+    /// <summary>
+    /// CPU使用率%
+    /// </summary>
+    public string CpuRate { get; set; }
+
+    /// <summary>
+    /// 总内存 GB
+    /// </summary>
+    public string TotalRam { get; set; }
+
+    /// <summary>
+    /// 内存使用率 %
+    /// </summary>
+    public string RamRate { get; set; }
+
+    /// <summary>
+    /// 空闲内存
+    /// </summary>
+    public string FreeRam { get; set; }
+}
+
+/// <summary>
+/// 磁盘信息
+/// </summary>
+public class DiskInfo
+{
+    /// <summary>
+    /// 磁盘名
+    /// </summary>
+    public string DiskName { get; set; }
+
+    /// <summary>
+    /// 类型名
+    /// </summary>
+    public string TypeName { get; set; }
+
+    /// <summary>
+    /// 总剩余
+    /// </summary>
+    public long TotalFree { get; set; }
+
+    /// <summary>
+    /// 总量
+    /// </summary>
+    public long TotalSize { get; set; }
+
+    /// <summary>
+    /// 已使用
+    /// </summary>
+    public long Used { get; set; }
+
+    /// <summary>
+    /// 可使用
+    /// </summary>
+    public long AvailableFreeSpace { get; set; }
+
+    /// <summary>
+    /// 使用百分比
+    /// </summary>
+    public decimal AvailablePercent { get; set; }
+}
+
+public class MemoryMetricsClient
+{
+    /// <summary>
+    /// windows系统获取内存信息
+    /// </summary>
+    /// <returns></returns>
+    public MemoryMetrics GetWindowsMetrics()
+    {
+        string output = ShellUtil.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value");
+        var metrics = new MemoryMetrics();
+        var lines = output.Trim().Split('\n', (char)StringSplitOptions.RemoveEmptyEntries);
+        if (lines.Length <= 0) return metrics;
+
+        var freeMemoryParts = lines[0].Split('=', (char)StringSplitOptions.RemoveEmptyEntries);
+        var totalMemoryParts = lines[1].Split('=', (char)StringSplitOptions.RemoveEmptyEntries);
+
+        metrics.Total = Math.Round(double.Parse(totalMemoryParts[1]) / 1024, 0);
+        metrics.Free = Math.Round(double.Parse(freeMemoryParts[1]) / 1024, 0);//m
+        metrics.Used = metrics.Total - metrics.Free;
+
+        return metrics;
+    }
+
+    /// <summary>
+    /// Unix系统获取
+    /// </summary>
+    /// <returns></returns>
+    public MemoryMetrics GetUnixMetrics()
+    {
+        string output = ShellUtil.Bash("free -m | awk '{print $2,$3,$4,$5,$6}'");
+        var metrics = new MemoryMetrics();
+        var lines = output.Split('\n', (char)StringSplitOptions.RemoveEmptyEntries);
+        if (lines.Length <= 0) return metrics;
+
+        if (lines != null && lines.Length > 0)
+        {
+            var memory = lines[1].Split(' ', (char)StringSplitOptions.RemoveEmptyEntries);
+            if (memory.Length >= 3)
+            {
+                metrics.Total = double.Parse(memory[0]);
+                metrics.Used = double.Parse(memory[1]);
+                metrics.Free = double.Parse(memory[2]);//m
+            }
+        }
+        return metrics;
+    }
+}
+
+public class ShellUtil
+{
+    /// <summary>
+    /// linux 系统命令
+    /// </summary>
+    /// <param name="command"></param>
+    /// <returns></returns>
+    public static string Bash(string command)
+    {
+        var escapedArgs = command.Replace("\"", "\\\"");
+        var process = new Process()
+        {
+            StartInfo = new ProcessStartInfo
+            {
+                FileName = "/bin/bash",
+                Arguments = $"-c \"{escapedArgs}\"",
+                RedirectStandardOutput = true,
+                UseShellExecute = false,
+                CreateNoWindow = true,
+            }
+        };
+        process.Start();
+        string result = process.StandardOutput.ReadToEnd();
+        process.WaitForExit();
+        process.Dispose();
+        return result;
+    }
+
+    /// <summary>
+    /// windows系统命令
+    /// </summary>
+    /// <param name="fileName"></param>
+    /// <param name="args"></param>
+    /// <returns></returns>
+    public static string Cmd(string fileName, string args)
+    {
+        string output = string.Empty;
+
+        var info = new ProcessStartInfo();
+        info.FileName = fileName;
+        info.Arguments = args;
+        info.RedirectStandardOutput = true;
+
+        using (var process = Process.Start(info))
+        {
+            output = process.StandardOutput.ReadToEnd();
+        }
+        return output;
+    }
+}

+ 136 - 0
Admin.NET/Admin.NET.Core/Util/DateTimeUtil.cs

@@ -0,0 +1,136 @@
+namespace Admin.NET.Core;
+
+public class DateTimeUtil
+{
+    /// <summary>
+    /// 获取开始时间
+    /// </summary>
+    /// <param name="dateTime"></param>
+    /// <returns></returns>
+    public static DateTime GetBeginTime(DateTime? dateTime, int days = 0)
+    {
+        if (dateTime == DateTime.MinValue || dateTime == null)
+            return DateTime.Now.AddDays(days);
+
+        return dateTime ?? DateTime.Now;
+    }
+
+    /// <summary>
+    ///  时间戳转本地时间-时间戳精确到秒
+    /// </summary>
+    public static DateTime ToLocalTimeDateBySeconds(long unix)
+    {
+        return DateTimeOffset.FromUnixTimeSeconds(unix).ToLocalTime().DateTime;
+    }
+
+    /// <summary>
+    ///  时间转时间戳Unix-时间戳精确到秒
+    /// </summary>
+    public static long ToUnixTimestampBySeconds(DateTime dt)
+    {
+        return new DateTimeOffset(dt).ToUnixTimeSeconds();
+    }
+
+    /// <summary>
+    ///  时间戳转本地时间-时间戳精确到毫秒
+    /// </summary>
+    public static DateTime ToLocalTimeDateByMilliseconds(long unix)
+    {
+        return DateTimeOffset.FromUnixTimeMilliseconds(unix).ToLocalTime().DateTime;
+    }
+
+    /// <summary>
+    ///  时间转时间戳Unix-时间戳精确到毫秒
+    /// </summary>
+    public static long ToUnixTimestampByMilliseconds(DateTime dt)
+    {
+        return new DateTimeOffset(dt).ToUnixTimeMilliseconds();
+    }
+
+    /// <summary>
+    /// 毫秒转天时分秒
+    /// </summary>
+    /// <param name="ms"></param>
+    /// <returns></returns>
+    public static string FormatTime(long ms)
+    {
+        int ss = 1000;
+        int mi = ss * 60;
+        int hh = mi * 60;
+        int dd = hh * 24;
+
+        long day = ms / dd;
+        long hour = (ms - day * dd) / hh;
+        long minute = (ms - day * dd - hour * hh) / mi;
+        long second = (ms - day * dd - hour * hh - minute * mi) / ss;
+        long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss;
+
+        string sDay = day < 10 ? "0" + day : "" + day; //天
+        string sHour = hour < 10 ? "0" + hour : "" + hour;//小时
+        string sMinute = minute < 10 ? "0" + minute : "" + minute;//分钟
+        string sSecond = second < 10 ? "0" + second : "" + second;//秒
+        string sMilliSecond = milliSecond < 10 ? "0" + milliSecond : "" + milliSecond;//毫秒
+        sMilliSecond = milliSecond < 100 ? "0" + sMilliSecond : "" + sMilliSecond;
+
+        return string.Format("{0} 天 {1} 小时 {2} 分 {3} 秒", sDay, sHour, sMinute, sSecond);
+    }
+
+    /// <summary>
+    /// 获取unix时间戳
+    /// </summary>
+    /// <param name="dt"></param>
+    /// <returns></returns>
+    public static long GetUnixTimeStamp(DateTime dt)
+    {
+        return ((DateTimeOffset)dt).ToUnixTimeMilliseconds();
+    }
+
+    /// <summary>
+    /// 获取日期天的最小时间
+    /// </summary>
+    /// <param name="dt"></param>
+    /// <returns></returns>
+    public static DateTime GetDayMinDate(DateTime dt)
+    {
+        return new DateTime(dt.Year, dt.Month, dt.Day, 0, 0, 0);
+    }
+
+    /// <summary>
+    /// 获取日期天的最大时间
+    /// </summary>
+    /// <param name="dt"></param>
+    /// <returns></returns>
+
+    public static DateTime GetDayMaxDate(DateTime dt)
+    {
+        return new DateTime(dt.Year, dt.Month, dt.Day, 23, 59, 59);
+    }
+
+    /// <summary>
+    /// 获取日期天的最大时间
+    /// </summary>
+    /// <param name="dt"></param>
+    /// <returns></returns>
+    public static string FormatDateTime(DateTime? dt)
+    {
+        if (dt == null) return string.Empty;
+
+        if (dt.Value.Year == DateTime.Now.Year)
+            return dt.Value.ToString("MM-dd HH:mm");
+        else
+            return dt.Value.ToString("yyyy-MM-dd HH:mm");
+    }
+
+    /// <summary>
+    /// 获取今天日期范围00:00:00 - 23:59:59
+    /// </summary>
+    /// <returns></returns>
+    public static List<DateTime> GetTodayTimeList(DateTime time)
+    {
+        return new List<DateTime>
+        {
+            Convert.ToDateTime(time.ToString("D").ToString()),
+            Convert.ToDateTime(time.AddDays(1).ToString("D").ToString()).AddSeconds(-1)
+        };
+    }
+}

+ 0 - 176
Admin.NET/Admin.NET.Core/Util/ServerUtil.cs

@@ -1,176 +0,0 @@
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 服务器信息
-/// </summary>
-public class ServerUtil
-{
-    /// <summary>
-    /// 服务器基本配置
-    /// </summary>
-    /// <returns></returns>
-    public static dynamic GetServerBaseInfo()
-    {
-        var furionAssembly = typeof(Furion.App).Assembly.GetName();
-        var sqlSugarAssembly = typeof(ISqlSugarClient).Assembly.GetName();
-        return new
-        {
-            HostName = Environment.MachineName, // HostName
-            SystemOs = RuntimeInformation.OSDescription, // 系统名称
-            OsArchitecture = Environment.OSVersion.Platform.ToString() + " " + RuntimeInformation.OSArchitecture.ToString(), // 系统架构
-            ProcessorCount = Environment.ProcessorCount.ToString() + " 核", // CPU核心数
-            FrameworkDescription = RuntimeInformation.FrameworkDescription + " / " +
-            furionAssembly.Name.ToString() + " " + furionAssembly.Version.ToString() + " / " +
-            sqlSugarAssembly.Name.ToString() + " " + sqlSugarAssembly.Version.ToString(), // .NET、Furion、SqlSugar
-        };
-    }
-
-    /// <summary>
-    /// 服务器使用资源
-    /// </summary>
-    /// <returns></returns>
-    public static dynamic GetServerUseInfo()
-    {
-        var ramInfo = GetRamInfo();
-        return new
-        {
-            TotalRam = Math.Ceiling(ramInfo.Total / 1024).ToString() + " GB", // 总内存
-            RamRate = Math.Ceiling(100 * ramInfo.Used / ramInfo.Total) + " %", // 内存使用率
-            CpuRate = Math.Ceiling(double.Parse(GetCpuRate())) + " %", // Cpu使用率
-            RunTime = GetRunTime()
-        };
-    }
-
-    /// <summary>
-    /// 服务器网络信息
-    /// </summary>
-    /// <returns></returns>
-    public static async Task<dynamic> GetServerNetWorkInfo()
-    {
-        return new
-        {
-            WanIp = await GetWanIpFromPCOnline(), // 外网IP
-            LocalIp = App.HttpContext?.Connection?.LocalIpAddress.ToString(),
-            SendAndReceived = "", //"上行" + Math.Round(networkInfo.SendLength / 1024.0 / 1024 / 1024, 2) + "GB 下行" + Math.Round(networkInfo.ReceivedLength / 1024.0 / 1024 / 1024, 2) + "GB", // 上下行流量统计
-            NetworkSpeed = "" //"上行" + Send / 1024 + "kb/s 下行" + Received / 1024 + "kb/s" // 网络速度
-        };
-    }
-
-    /// <summary>
-    /// 内存信息
-    /// </summary>
-    /// <returns></returns>
-    private static dynamic GetRamInfo()
-    {
-        if (IsUnix())
-        {
-            var output = ShellUtil.Bash("cat /proc/meminfo");
-            var lines = output.Split("\n");
-            return new
-            {
-                Total = Math.Round(double.Parse(lines[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 2),
-                Used = Math.Round((double.Parse(lines[0].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1])
-                - double.Parse(lines[1].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1])) / 1024, 2),
-                Free = Math.Round(double.Parse(lines[1].Split(" ", StringSplitOptions.RemoveEmptyEntries)[1]) / 1024, 2),
-            };
-        }
-        else
-        {
-            var output = ShellUtil.Cmd("wmic", "OS get FreePhysicalMemory,TotalVisibleMemorySize /Value");
-            var lines = output.Trim().Split("\n");
-            var freeMemoryParts = lines[0].Split("=", StringSplitOptions.RemoveEmptyEntries);
-            var totalMemoryParts = lines[1].Split("=", StringSplitOptions.RemoveEmptyEntries);
-            var total = Math.Round(double.Parse(totalMemoryParts[1]) / 1024, 2);
-            var free = Math.Round(double.Parse(freeMemoryParts[1]) / 1024, 2);
-            return new
-            {
-                Total = total,
-                Free = free,
-                Used = total - free
-            };
-        }
-    }
-
-    /// <summary>
-    /// CPU信息
-    /// </summary>
-    /// <returns></returns>
-    private static string GetCpuRate()
-    {
-        string cpuRate;
-        if (IsUnix())
-        {
-            var output = ShellUtil.Bash("top -b -n1 | grep \"Cpu(s)\" | awk '{print $2 + $4}'");
-            cpuRate = output.Trim();
-        }
-        else
-        {
-            var output = ShellUtil.Cmd("wmic", "cpu get LoadPercentage");
-            cpuRate = output.Replace("LoadPercentage", string.Empty).Trim();
-        }
-        return cpuRate == "" ? "0" : cpuRate;
-    }
-
-    /// <summary>
-    /// 系统运行时间
-    /// </summary>
-    /// <returns></returns>
-    private static string GetRunTime()
-    {
-        return FormatTime((long)(DateTimeOffset.Now - Process.GetCurrentProcess().StartTime).TotalMilliseconds);
-        //return DateTimeUtil.FormatTime(Environment.TickCount);
-    }
-
-    /// <summary>
-    /// 是否Linux
-    /// </summary>
-    /// <returns></returns>
-    private static bool IsUnix()
-    {
-        return RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || RuntimeInformation.IsOSPlatform(OSPlatform.Linux);
-    }
-
-    /// <summary>
-    /// 毫秒转天时分秒
-    /// </summary>
-    /// <param name="ms"></param>
-    /// <returns></returns>
-    private static string FormatTime(long ms)
-    {
-        int ss = 1000;
-        int mi = ss * 60;
-        int hh = mi * 60;
-        int dd = hh * 24;
-
-        long day = ms / dd;
-        long hour = (ms - day * dd) / hh;
-        long minute = (ms - day * dd - hour * hh) / mi;
-        long second = (ms - day * dd - hour * hh - minute * mi) / ss;
-        //long milliSecond = ms - day * dd - hour * hh - minute * mi - second * ss;
-
-        string sDay = day < 10 ? "0" + day : "" + day; //天
-        string sHour = hour < 10 ? "0" + hour : "" + hour;//小时
-        string sMinute = minute < 10 ? "0" + minute : "" + minute;//分钟
-        string sSecond = second < 10 ? "0" + second : "" + second;//秒
-        //string sMilliSecond = milliSecond < 10 ? "0" + milliSecond : "" + milliSecond;//毫秒
-        //sMilliSecond = milliSecond < 100 ? "0" + sMilliSecond : "" + sMilliSecond;
-        return string.Format("{0} 天 {1} 小时 {2} 分 {3} 秒", sDay, sHour, sMinute, sSecond);
-    }
-
-    /// <summary>
-    /// 获取外网IP和位置
-    /// </summary>
-    /// <returns></returns>
-    private static async Task<string> GetWanIpFromPCOnline()
-    {
-        Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
-
-        var url = "http://whois.pconline.com.cn/ipJson.jsp";
-        var stream = await url.GetAsStreamAsync();
-        var streamReader = new StreamReader(stream.Stream, stream.Encoding);
-        var html = streamReader.ReadToEnd();
-        var tmp = html[(html.IndexOf("({") + 2)..].Split(",");
-        var ipAddr = tmp[0].Split(":")[1] + "【" + tmp[7].Split(":")[1] + "】";
-        return ipAddr.Replace("\"", "");
-    }
-}

+ 0 - 55
Admin.NET/Admin.NET.Core/Util/ShellUtil.cs

@@ -1,55 +0,0 @@
-namespace Admin.NET.Core;
-
-/// <summary>
-/// 系统Shell命令
-/// </summary>
-public class ShellUtil
-{
-    /// <summary>
-    /// Bash命令
-    /// </summary>
-    /// <param name="command"></param>
-    /// <returns></returns>
-    public static string Bash(string command)
-    {
-        var escapedArgs = command.Replace("\"", "\\\"");
-        var process = new Process()
-        {
-            StartInfo = new ProcessStartInfo
-            {
-                FileName = "/bin/bash",
-                Arguments = $"-c \"{escapedArgs}\"",
-                RedirectStandardOutput = true,
-                UseShellExecute = false,
-                CreateNoWindow = true,
-            }
-        };
-        process.Start();
-        string result = process.StandardOutput.ReadToEnd();
-        process.WaitForExit();
-        process.Dispose();
-        return result;
-    }
-
-    /// <summary>
-    /// cmd命令
-    /// </summary>
-    /// <param name="fileName"></param>
-    /// <param name="args"></param>
-    /// <returns></returns>
-    public static string Cmd(string fileName, string args)
-    {
-        string output = string.Empty;
-        var info = new ProcessStartInfo
-        {
-            FileName = fileName,
-            Arguments = args,
-            RedirectStandardOutput = true
-        };
-        using (var process = Process.Start(info))
-        {
-            output = process.StandardOutput.ReadToEnd();
-        }
-        return output;
-    }
-}

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

@@ -162,6 +162,9 @@ public class Startup : AppStartup
         {
             WorkerId = App.GetOptions<SnowIdOptions>().WorkerId
         });
+
+        // 验证码
+        services.AddLazyCaptcha();
     }
 
     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

+ 88 - 20
vue-next-admin/src/api-services/apis/sys-server-api.ts

@@ -25,7 +25,43 @@ export const SysServerApiAxiosParamCreator = function (configuration?: Configura
     return {
         /**
          * 
-         * @summary 服务器基本配置
+         * @summary 框架主要程序集
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        serverAssemblyGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/server/assembly`;
+            // 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 服务器配置信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -61,12 +97,12 @@ export const SysServerApiAxiosParamCreator = function (configuration?: Configura
         },
         /**
          * 
-         * @summary 服务器网络信息
+         * @summary 服务器磁盘信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        serverNetworkGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
-            const localVarPath = `/server/network`;
+        serverDiskGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/server/disk`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, 'https://example.com');
             let baseOptions;
@@ -97,7 +133,7 @@ export const SysServerApiAxiosParamCreator = function (configuration?: Configura
         },
         /**
          * 
-         * @summary 服务器使用资源
+         * @summary 服务器内存信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -142,7 +178,20 @@ export const SysServerApiFp = function(configuration?: Configuration) {
     return {
         /**
          * 
-         * @summary 服务器基本配置
+         * @summary 框架主要程序集
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async serverAssemblyGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultObject>>> {
+            const localVarAxiosArgs = await SysServerApiAxiosParamCreator(configuration).serverAssemblyGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 服务器配置信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -155,12 +204,12 @@ export const SysServerApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
-         * @summary 服务器网络信息
+         * @summary 服务器磁盘信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async serverNetworkGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultObject>>> {
-            const localVarAxiosArgs = await SysServerApiAxiosParamCreator(configuration).serverNetworkGet(options);
+        async serverDiskGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultObject>>> {
+            const localVarAxiosArgs = await SysServerApiAxiosParamCreator(configuration).serverDiskGet(options);
             return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
                 const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
                 return axios.request(axiosRequestArgs);
@@ -168,7 +217,7 @@ export const SysServerApiFp = function(configuration?: Configuration) {
         },
         /**
          * 
-         * @summary 服务器使用资源
+         * @summary 服务器内存信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -190,7 +239,16 @@ export const SysServerApiFactory = function (configuration?: Configuration, base
     return {
         /**
          * 
-         * @summary 服务器基本配置
+         * @summary 框架主要程序集
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async serverAssemblyGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultObject>> {
+            return SysServerApiFp(configuration).serverAssemblyGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 服务器配置信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -199,16 +257,16 @@ export const SysServerApiFactory = function (configuration?: Configuration, base
         },
         /**
          * 
-         * @summary 服务器网络信息
+         * @summary 服务器磁盘信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async serverNetworkGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultObject>> {
-            return SysServerApiFp(configuration).serverNetworkGet(options).then((request) => request(axios, basePath));
+        async serverDiskGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultObject>> {
+            return SysServerApiFp(configuration).serverDiskGet(options).then((request) => request(axios, basePath));
         },
         /**
          * 
-         * @summary 服务器使用资源
+         * @summary 服务器内存信息
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
@@ -227,7 +285,17 @@ export const SysServerApiFactory = function (configuration?: Configuration, base
 export class SysServerApi extends BaseAPI {
     /**
      * 
-     * @summary 服务器基本配置
+     * @summary 框架主要程序集
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysServerApi
+     */
+    public async serverAssemblyGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultObject>> {
+        return SysServerApiFp(this.configuration).serverAssemblyGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 服务器配置信息
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof SysServerApi
@@ -237,17 +305,17 @@ export class SysServerApi extends BaseAPI {
     }
     /**
      * 
-     * @summary 服务器网络信息
+     * @summary 服务器磁盘信息
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof SysServerApi
      */
-    public async serverNetworkGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultObject>> {
-        return SysServerApiFp(this.configuration).serverNetworkGet(options).then((request) => request(this.axios, this.basePath));
+    public async serverDiskGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultObject>> {
+        return SysServerApiFp(this.configuration).serverDiskGet(options).then((request) => request(this.axios, this.basePath));
     }
     /**
      * 
-     * @summary 服务器使用资源
+     * @summary 服务器内存信息
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof SysServerApi

+ 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>

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

@@ -22,6 +22,10 @@
 	--next-color-seting-main: #e9eef3;
 	--next-color-seting-aside: #d3dce6;
 	--next-color-seting-header: #b3c0d1;
+	// 重写全局样式
+	//--el-text-color-primary: rgba(0, 0, 0, .75) !important;
+    --el-text-color-regular: rgba(0, 0, 0, .75) !important;
+	//--el-text-color-secondary: var(--el-text-color-regular) !important;
 }
 
 html,
@@ -124,7 +128,7 @@ body,
 	}
 	.layout-scrollbar {
 		@extend .el-scrollbar;
-		padding: 5px; // 四周间隙
+		padding: 8px; // 四周间隙
 	}
 	.layout-mian-height-50 {
 		height: calc(100vh - 50px);

+ 7 - 4
vue-next-admin/src/theme/element.scss

@@ -263,10 +263,13 @@
 	.el-button.is-text {
 		padding: 0;
 	}
-	// .el-table__cell {
-	// 	// 	background:#FF0000;
-	// 	// 	font-weight: 300;
-	// }
+	// 标题背景色
+	--el-table-header-bg-color: #fafafa;
+	// 标题字体颜色
+	thead{
+		color: var(--el-text-color-regular);	
+		font-weight: normal;
+	}
 }
 // 分页组件靠右显示
 .el-pagination {

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

@@ -13,7 +13,12 @@ export function authDirective(app: App) {
 	app.directive('auth', {
 		mounted(el, binding) {
 			const stores = useUserInfo();
-			if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) el.parentNode.removeChild(el);
+			if (!stores.userInfos.authBtnList.some((v: string) => v === binding.value)) {
+				//el.parentNode.removeChild(el);
+				el.disabled = true;
+				el.classList.add('is-disabled');
+				el.setAttribute('aria-disabled', 'true');
+			}
 		},
 	});
 	// 多个权限验证,满足一个则显示(v-auths="[xxx,xxx]")

+ 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>

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

@@ -11,17 +11,13 @@
 						:shortcuts="shortcuts" />
 				</el-form-item>
 				<el-form-item>
-					<el-button icon="ele-Refresh" @click="resetQuery">
-						重置
-					</el-button>
-					<el-button type="primary" icon="ele-Search" @click="handleQuery">
-						查询
-					</el-button>
+					<el-button icon="ele-Refresh" @click="resetQuery"> 重置 </el-button>
+					<el-button type="primary" icon="ele-Search" @click="handleQuery"> 查询 </el-button>
 				</el-form-item>
 			</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>
@@ -34,12 +30,26 @@
 				<el-table-column prop="logDateTime" label="日志记录时间" align="center" show-overflow-tooltip>
 				</el-table-column>
 				<el-table-column prop="createTime" label="操作时间" align="center" show-overflow-tooltip></el-table-column>
+				<el-table-column label="操作" width="110" align="center" fixed="right" show-overflow-tooltip>
+					<template #default="scope">
+						<el-button icon="ele-Edit" size="small" text type="primary" @click="viewDetail(scope.row)">详情
+						</el-button>
+					</template>
+				</el-table-column>
 			</el-table>
 			<el-pagination v-model:currentPage="tableParams.page" v-model:page-size="tableParams.pageSize"
 				:total="tableParams.total" :page-sizes="[10, 20, 50, 100]" small background
 				@size-change="handleSizeChange" @current-change="handleCurrentChange"
 				layout="total, sizes, prev, pager, next, jumper" />
 		</el-card>
+		<el-dialog v-model="dialogVisible" title="日志详情" width="769px">
+			<pre>{{content}}</pre>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button type="primary" @click="dialogVisible = false">确认</el-button>
+				</span>
+			</template>
+		</el-dialog>
 	</div>
 </template>
 
@@ -66,19 +76,23 @@ export default defineComponent({
 				total: 0 as any,
 			},
 			logData: [] as any,
+			dialogVisible: false,
+			content: "",
 		});
 		onMounted(async () => {
 			handleQuery();
 		});
 		// 查询操作
 		const handleQuery = async () => {
-			if (state.queryParams.startTime == null)
-				state.queryParams.startTime = undefined;
-			if (state.queryParams.endTime == null)
-				state.queryParams.endTime = undefined;
+			if (state.queryParams.startTime == null) state.queryParams.startTime = undefined;
+			if (state.queryParams.endTime == null) state.queryParams.endTime = undefined;
 			state.loading = true;
-			var res = await getAPI(SysLogOpApi).sysLogOpPageGet(state.queryParams.startTime, state.queryParams.endTime,
-				state.tableParams.page, state.tableParams.pageSize);
+			var res = await getAPI(SysLogOpApi).sysLogOpPageGet(
+				state.queryParams.startTime,
+				state.queryParams.endTime,
+				state.tableParams.page,
+				state.tableParams.pageSize
+			);
 			state.logData = res.data.result?.items;
 			state.tableParams.total = res.data.result?.total;
 			state.loading = false;
@@ -108,6 +122,11 @@ export default defineComponent({
 			state.tableParams.page = val;
 			handleQuery();
 		};
+		// 查看详情
+		const viewDetail = (row: any) => {
+			state.content = row.message;
+			state.dialogVisible = true;
+		};
 		const shortcuts = [
 			{
 				text: '今天',
@@ -116,20 +135,20 @@ export default defineComponent({
 			{
 				text: '昨天',
 				value: () => {
-					const date = new Date()
-					date.setTime(date.getTime() - 3600 * 1000 * 24)
-					return date
+					const date = new Date();
+					date.setTime(date.getTime() - 3600 * 1000 * 24);
+					return date;
 				},
 			},
 			{
 				text: '上周',
 				value: () => {
-					const date = new Date()
-					date.setTime(date.getTime() - 3600 * 1000 * 24 * 7)
-					return date
+					const date = new Date();
+					date.setTime(date.getTime() - 3600 * 1000 * 24 * 7);
+					return date;
 				},
 			},
-		]
+		];
 		return {
 			handleQuery,
 			resetQuery,
@@ -137,6 +156,7 @@ export default defineComponent({
 			shortcuts,
 			handleSizeChange,
 			handleCurrentChange,
+			viewDetail,
 			...toRefs(state),
 		};
 	},
@@ -148,4 +168,9 @@ export default defineComponent({
 	//font-size: 14px;
 	max-width: 60%;
 }
-</style>
+
+pre {
+	white-space: break-spaces;
+	line-height: 20px;
+}
+</style>

+ 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 - 1
vue-next-admin/src/views/system/org/component/orgTree.vue

@@ -29,7 +29,8 @@
     <div style="margin-bottom: 45px" v-loading="state.loading">
       <el-tree ref='treeRef' class='filter-tree' :data='state.orgData' node-key="id"
         :props="{children: 'children', label: 'name'}" :filter-node-method='filterNode' @node-click="nodeClick"
-        :show-checkbox="state.isShowCheckbox" :default-checked-keys="state.ownOrgData" highlight-current />
+        :show-checkbox="state.isShowCheckbox" :default-checked-keys="state.ownOrgData" highlight-current
+        check-strictly />
     </div>
   </el-card>
 </template>

+ 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/component/editRole.vue

@@ -42,7 +42,7 @@
 							<el-tree ref="treeRef" :data="menuData" node-key="id" show-checkbox
 								:props="{ children: 'children', label: 'title', class: treeNodeClass }"
 								:default-checked-keys="ownMenuData" highlight-current class="menu-data-tree"
-								icon="ele-Menu" />
+								icon="ele-Menu" check-strictly />
 						</el-form-item>
 					</el-col>
 

+ 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>

+ 338 - 0
vue-next-admin/src/views/system/server/index.vue

@@ -0,0 +1,338 @@
+<template>
+  <div class="sys-server-container">
+    <el-row :gutter="8">
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" header="系统信息">
+          <table class="sysInfo_table">
+            <tr>
+              <td class="sysInfo_td">主机名称:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.hostName }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">操作系统:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.systemOs }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">系统架构:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.osArchitecture }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">CPU核数:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.processorCount }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">运行时长:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.sysRunTime }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">外网地址:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.remoteIp }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">内网地址:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.localIp }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">运行框架:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.frameworkDescription }}</td>
+            </tr>
+          </table>
+        </el-card>
+      </el-col>
+      <el-col :md="12" :sm="24">
+        <el-card shadow="hover" header="使用信息">
+          <table class="sysInfo_table">
+            <tr>
+              <td class="sysInfo_td">启动时间:</td>
+              <td class="sysInfo_td">{{ machineUseInfo.startTime }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">运行时长:</td>
+              <td class="sysInfo_td">{{ machineUseInfo.runTime }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">内存总量:</td>
+              <td class="sysInfo_td">{{ machineUseInfo.totalRam }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">使用内存:</td>
+              <td class="sysInfo_td">{{ machineUseInfo.usedRam }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">空闲内存:</td>
+              <td class="sysInfo_td">{{ machineUseInfo.freeRam }}</td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">内存使用率:</td>
+              <td class="sysInfo_td">
+                <el-tag round>{{ machineUseInfo.ramRate }}</el-tag>
+              </td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">CPU使用率:</td>
+              <td class="sysInfo_td">
+                <el-tag round>{{ machineUseInfo.cpuRate }}</el-tag>
+              </td>
+            </tr>
+            <tr>
+              <td class="sysInfo_td">其他信息:</td>
+              <td class="sysInfo_td">{{ machineBaseInfo.environment }}</td>
+            </tr>
+          </table>
+        </el-card>
+      </el-col>
+    </el-row>
+    <el-card shadow="hover" header="程序集信息" style="margin-top: 8px;">
+      <div v-for="d in assemblyInfo" :key="d.name" style="display:inline-flex; margin:4px;">
+        <el-tag round>
+          <div style="display:inline-flex;">
+            <div style="">{{ d.name }}</div>
+            <div style="font-size:9px; color:black; margin-left: 3px;">{{ d.version }}</div>
+          </div>
+        </el-tag>
+      </div>
+    </el-card>
+    <el-card shadow="hover" header="磁盘信息" style="margin-top: 8px">
+      <el-table :data="machineDiskInfo" style="width: 100%">
+        <el-table-column prop="diskName" label="盘符">
+          <template #default="scope">
+            <el-tag round> {{ scope.row.diskName }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="typeName" label="类型" />
+        <el-table-column prop="totalSize" label="磁盘总量">
+          <template #default="scope">{{ scope.row.totalSize }} GB</template>
+        </el-table-column>
+        <el-table-column prop="used" label="已使用">
+          <template #default="scope">{{ scope.row.used }} GB</template>
+        </el-table-column>
+        <el-table-column prop="availableFreeSpace" label="剩余量">
+          <template #default="scope">{{ scope.row.availableFreeSpace }} GB</template>
+        </el-table-column>
+        <el-table-column prop="availablePercent" label="使用率">
+          <template #default="scope">
+            <el-tag> {{ scope.row.availablePercent }}%</el-tag>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+  </div>
+</template>
+
+<script lang="ts">
+import { toRefs, reactive, defineComponent, onActivated, onDeactivated, onMounted } from 'vue';
+
+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,
+      machineDiskInfo: [] as any,
+      assemblyInfo: [] as any,
+      timer: null as any,
+    });
+    onMounted(async () => {
+      state.loading = true;
+      loadMachineBaseInfo();
+      loadMachineUseInfo();
+      loadMachineDiskInfo();
+      loadAssemblyInfo();
+      state.loading = false;
+    });
+    // 服务器配置信息
+    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 loadMachineDiskInfo = async () => {
+      var res = await getAPI(SysServerApi).serverDiskGet();
+      state.machineDiskInfo = res.data.result;
+    };
+    // 框架程序集信息
+    const loadAssemblyInfo = async () => {
+      var res = await getAPI(SysServerApi).serverAssemblyGet();
+      state.assemblyInfo = res.data.result;
+    };
+    // 实时刷新内存
+    const refreshData = () => {
+      loadMachineUseInfo();
+    };
+    onActivated(() => {
+      state.timer = setInterval(() => {
+        refreshData();
+      }, 5000);
+    });
+    onDeactivated(() => {
+      clearInterval(state.timer);
+    });
+
+    return {
+      loadMachineBaseInfo,
+      loadMachineUseInfo,
+      loadMachineDiskInfo,
+      loadAssemblyInfo,
+      refreshData,
+      ...toRefs(state),
+    };
+  },
+});
+</script>
+
+<style lang="scss">
+.sysInfo_table {
+  width: 100%;
+  min-height: 40px;
+  line-height: 40px;
+  text-align: center;
+}
+
+.sysInfo_td {
+  border-bottom: 1px solid #e8e8e8;
+}
+
+.waterfall-last {
+  display: grid;
+  grid-gap: 0.25em;
+  grid-auto-flow: row dense;
+  grid-auto-rows: minmax(188px, 20vmin);
+  grid-template-columns: 1fr;
+
+  .waterfall-last-item {
+    height: 100%;
+    background: var(--el-color-primary);
+    color: var(--el-color-white);
+    transition: all 0.3s ease;
+    border-radius: 3px;
+
+    &:hover {
+      box-shadow: 0 2px 12px 0 rgb(0 0 0 / 10%);
+      transition: all 0.3s ease;
+      cursor: pointer;
+    }
+  }
+}
+
+@media (min-width: 576px) {
+  .waterfall-last {
+    grid-template-columns: repeat(7, 1fr);
+
+    .waterfall-last-item {
+      &:nth-of-type(9n + 9) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(9n + 8) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(9n + 7) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(9n + 6) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(9n + 5) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(9n + 4) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(9n + 3) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(9n + 2) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(9n + 1) {
+        grid-column: auto / span 2;
+      }
+    }
+  }
+}
+
+@media (min-width: 576px) and (min-width: 1024px) {
+  .waterfall-last {
+    grid-template-columns: repeat(14, 1fr);
+
+    .waterfall-last-item {
+      &:nth-of-type(15n + 15) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 14) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 13) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(15n + 12) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 11) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 10) {
+        grid-column: auto / span 2;
+      }
+
+      &:nth-of-type(15n + 9) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 8) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 7) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 6) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 5) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 4) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 3) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 2) {
+        grid-column: auto / span 3;
+      }
+
+      &:nth-of-type(15n + 1) {
+        grid-column: auto / span 2;
+      }
+    }
+  }
+}
+</style>

+ 12 - 12
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>
@@ -20,17 +20,17 @@
 							<el-button icon="ele-Refresh" @click="resetQuery">
 								重置
 							</el-button>
-							<el-button type="primary" icon="ele-Search" @click="handleQuery">
+							<el-button type="primary" icon="ele-Search" @click="handleQuery" v-auth="'sysUser:page'">
 								查询
 							</el-button>
-							<el-button icon="ele-Plus" @click="openAddUser">
+							<el-button icon="ele-Plus" @click="openAddUser" v-auth="'sysUser:add'">
 								新增
 							</el-button>
 						</el-form-item>
 					</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>
@@ -60,7 +60,7 @@
 						<el-table-column label="状态" width="70" align="center" show-overflow-tooltip>
 							<template #default="scope">
 								<el-switch v-model="scope.row.status" :active-value="1" :inactive-value="2" size="small"
-									@change="changeStatus(scope.row)">
+									@change="changeStatus(scope.row)" v-auth="'sysUser:setStatus'">
 								</el-switch>
 							</template>
 						</el-table-column>
@@ -73,7 +73,7 @@
 							<template #default="scope">
 								<el-tooltip content="用户编辑">
 									<el-button icon="ele-Edit" size="small" text type="primary"
-										@click="openEditUser(scope.row)">编辑
+										@click="openEditUser(scope.row)" v-auth="'sysUser:update'">编辑
 									</el-button>
 								</el-tooltip>
 								<el-dropdown>
@@ -84,14 +84,14 @@
 									</span>
 									<template #dropdown>
 										<el-dropdown-menu>
-											<el-dropdown-item icon="ele-OfficeBuilding"
-												@click="openGrantOrg(scope.row)">
-												数据范围
+											<el-dropdown-item icon="ele-OfficeBuilding" @click="openGrantOrg(scope.row)"
+												v-auth="'sysUser:grantData'">数据范围
 											</el-dropdown-item>
-											<el-dropdown-item icon="ele-RefreshLeft" @click="resetUserPwd(scope.row)">
-												重置密码
+											<el-dropdown-item icon="ele-RefreshLeft" @click="resetUserPwd(scope.row)"
+												v-auth="'sysUser:resetPwd'">重置密码
 											</el-dropdown-item>
-											<el-dropdown-item icon="ele-Delete" @click="delUser(scope.row)">
+											<el-dropdown-item icon="ele-Delete" @click="delUser(scope.row)"
+												v-auth="'sysUser:delete'">
 												删除账号
 											</el-dropdown-item>
 										</el-dropdown-menu>