Procházet zdrojové kódy

feat: 新增用户注册功能

喵你个旺呀 před 1 rokem
rodič
revize
096516a8e5
44 změnil soubory, kde provedl 2376 přidání a 34 odebrání
  1. 12 0
      Admin.NET/Admin.NET.Core/Entity/SysTenant.cs
  2. 59 0
      Admin.NET/Admin.NET.Core/Entity/SysUserRegWay.cs
  3. 56 2
      Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs
  4. 7 1
      Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs
  5. 6 0
      Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs
  6. 54 0
      Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginInput.cs
  7. 36 0
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  8. 10 0
      Admin.NET/Admin.NET.Core/Service/Config/Dto/InfoInput.cs
  9. 6 2
      Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs
  10. 5 2
      Admin.NET/Admin.NET.Core/Service/Org/SysOrgService.cs
  11. 4 0
      Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs
  12. 4 0
      Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs
  13. 4 7
      Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs
  14. 73 0
      Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs
  15. 28 0
      Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs
  16. 118 0
      Admin.NET/Admin.NET.Core/Service/User/SysUserRegWayService.cs
  17. 35 1
      Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs
  18. 5 0
      Admin.NET/Admin.NET.Core/Service/User/UserManager.cs
  19. 2 0
      Web/src/App.vue
  20. 1 0
      Web/src/api-services/api.ts
  21. 84 0
      Web/src/api-services/apis/sys-auth-api.ts
  22. 391 0
      Web/src/api-services/apis/sys-user-reg-way-api.ts
  23. 14 1
      Web/src/api-services/models/add-tenant-input.ts
  24. 117 0
      Web/src/api-services/models/add-user-reg-way-input.ts
  25. 57 0
      Web/src/api-services/models/admin-result-list-user-reg-way-output.ts
  26. 6 0
      Web/src/api-services/models/index.ts
  27. 12 0
      Web/src/api-services/models/info-save-input.ts
  28. 82 0
      Web/src/api-services/models/page-user-reg-way-input.ts
  29. 13 0
      Web/src/api-services/models/tenant-output.ts
  30. 13 0
      Web/src/api-services/models/update-tenant-input.ts
  31. 117 0
      Web/src/api-services/models/update-user-reg-way-input.ts
  32. 135 0
      Web/src/api-services/models/user-reg-way-output.ts
  33. 68 0
      Web/src/api-services/models/user-registration-input.ts
  34. 10 0
      Web/src/i18n/pages/login/zh-cn.ts
  35. 1 0
      Web/src/types/pinia.d.ts
  36. 2 0
      Web/src/views/login/component/account.vue
  37. 2 2
      Web/src/views/login/component/mobile.vue
  38. 352 0
      Web/src/views/login/component/register.vue
  39. 34 4
      Web/src/views/login/index.vue
  40. 34 7
      Web/src/views/system/infoSetting/index.vue
  41. 28 5
      Web/src/views/system/tenant/component/editTenant.vue
  42. 6 0
      Web/src/views/system/tenant/index.vue
  43. 146 0
      Web/src/views/system/userRegWay/component/editRegWay.vue
  44. 127 0
      Web/src/views/system/userRegWay/index.vue

+ 12 - 0
Admin.NET/Admin.NET.Core/Entity/SysTenant.cs

@@ -72,6 +72,18 @@ public partial class SysTenant : EntityBase
     /// </summary>
     [SugarColumn(ColumnDescription = "从库连接/读写分离", ColumnDataType = StaticConfig.CodeFirst_BigString)]
     public virtual string? SlaveConnections { get; set; }
+    
+    /// <summary>
+    /// 启用注册功能
+    /// </summary>
+    [SugarColumn(ColumnDescription = "启用注册功能")]
+    public virtual YesNoEnum? EnableReg { get; set; } = YesNoEnum.N;
+    
+    /// <summary>
+    /// 默认注册方案Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "默认注册方案")]
+    public virtual long? RegWayId { get; set; }
 
     /// <summary>
     /// 图标

+ 59 - 0
Admin.NET/Admin.NET.Core/Entity/SysUserRegWay.cs

@@ -0,0 +1,59 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统用户注册方案表
+/// </summary>
+[SugarTable(null, "系统用户注册方案表")]
+[SysTable]
+public partial class SysUserRegWay : EntityTenant
+{
+    /// <summary>
+    /// 方案名称
+    /// </summary>
+    [MaxLength(32)]
+    [SugarColumn(ColumnDescription = "方案名称", Length = 32)]
+    public virtual string Name { get; set; }
+    
+    /// <summary>
+    /// 账号类型
+    /// </summary>
+    [SugarColumn(ColumnDescription = "账号类型")]
+    public virtual AccountTypeEnum AccountType { get; set; } = AccountTypeEnum.NormalUser;
+    
+    /// <summary>
+    /// 注册用户默认角色
+    /// </summary>
+    [SugarColumn(ColumnDescription = "角色")]
+    public virtual long RoleId { get; set; }
+    
+    /// <summary>
+    /// 注册用户默认机构
+    /// </summary>
+    [SugarColumn(ColumnDescription = "机构")]
+    public virtual long OrgId { get; set; }
+    
+    /// <summary>
+    /// 注册用户默认职位
+    /// </summary>
+    [SugarColumn(ColumnDescription = "职位")]
+    public virtual long PosId { get; set; }
+    
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [SugarColumn(ColumnDescription = "排序")]
+    public int OrderNo { get; set; } = 100;
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [MaxLength(128)]
+    [SugarColumn(ColumnDescription = "备注", Length = 128)]
+    public string? Remark { get; set; }
+}

+ 56 - 2
Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs

@@ -204,7 +204,37 @@ public enum ErrorCodeEnum
     /// </summary>
     [ErrorCodeItemMetadata("手机号已存在")]
     D1032,
-
+    
+    /// <summary>
+    /// 此角色下存在注册方案禁止删除
+    /// </summary>
+    [ErrorCodeItemMetadata("此角色下存在注册方案禁止删除")]
+    D1033,
+    
+    /// <summary>
+    /// 注册功能未开启禁止注册
+    /// </summary>
+    [ErrorCodeItemMetadata("注册功能未开启禁止注册")]
+    D1034,
+    
+    /// <summary>
+    /// 注册方案不存在
+    /// </summary>
+    [ErrorCodeItemMetadata("注册方案不存在")]
+    D1035,
+    
+    /// <summary>
+    /// 角色不存在
+    /// </summary>
+    [ErrorCodeItemMetadata("角色不存在")]
+    D1036,
+    
+    /// <summary>
+    /// 禁止注册超级管理员和系统管理员
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止注册超级管理员和系统管理员")]
+    D1037,
+    
     /// <summary>
     /// 父机构不存在
     /// </summary>
@@ -264,6 +294,18 @@ public enum ErrorCodeEnum
     /// </summary>
     [ErrorCodeItemMetadata("禁止增加根节点机构")]
     D2009,
+    
+    /// <summary>
+    /// 此机构下存在注册方案禁止删除
+    /// </summary>
+    [ErrorCodeItemMetadata("此机构下存在注册方案禁止删除")]
+    D2010,
+    
+    /// <summary>
+    /// 机构不存在
+    /// </summary>
+    [ErrorCodeItemMetadata("机构不存在")]
+    D2011,
 
     /// <summary>
     /// 字典类型不存在
@@ -432,6 +474,12 @@ public enum ErrorCodeEnum
     /// </summary>
     [ErrorCodeItemMetadata("职位不存在")]
     D6003,
+    
+    /// <summary>
+    /// 此职位下存在注册方案禁止删除
+    /// </summary>
+    [ErrorCodeItemMetadata("此职位下存在注册方案禁止删除")]
+    D6004,
 
     /// <summary>
     /// 通知公告状态错误
@@ -642,6 +690,12 @@ public enum ErrorCodeEnum
     /// </summary>
     [ErrorCodeItemMetadata("已存在同名功能或同名程序及插件")]
     D1900,
+    
+    /// <summary>
+    /// 注册方案名称已存在
+    /// </summary>
+    [ErrorCodeItemMetadata("注册方案名称已存在")]
+    D2101,
 
     /// <summary>
     /// 禁止删除存在关联租户的应用
@@ -767,5 +821,5 @@ public enum ErrorCodeEnum
     /// 身份标识已存在
     /// </summary>
     [ErrorCodeItemMetadata("身份标识已存在")]
-    O1000,
+    O1000
 }

+ 7 - 1
Admin.NET/Admin.NET.Core/Enum/SysUserEventTypeEnum.cs

@@ -59,5 +59,11 @@ public enum SysUserEventTypeEnum
     /// 解除登录锁定
     /// </summary>
     [Description("解除登录锁定")]
-    UnlockLogin = 888
+    UnlockLogin = 888,
+    
+    /// <summary>
+    /// 注册用户
+    /// </summary>
+    [Description("注册用户")]
+    Register = 999,
 }

+ 6 - 0
Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs

@@ -176,6 +176,12 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1310000000445, Pid=1310000000441, Title="获取支付订单详情(微信接口)", Permission="sysWechatPay:payInfoFromWechat", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000446, Pid=1310000000441, Title="退款申请", Permission="sysWechatPay:refundDomestic", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
+            new SysMenu{ Id=1310000000451, Pid=1310000000301, Title="注册方案", Path="/platform/regWay", Name="sysUserRegWay", Component="/system/userRegWay/index", Icon="ele-Menu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=105 },
+            new SysMenu{ Id=1310000000452, Pid=1310000000451, Title="查询", Permission="sysUserRegWay:list", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000000453, Pid=1310000000451, Title="编辑", Permission="sysUserRegWay:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000000454, Pid=1310000000451, Title="增加", Permission="sysUserRegWay:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000000455, Pid=1310000000451, Title="删除", Permission="sysUserRegWay:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+
             new SysMenu{ Id=1310000000501, Pid=0, Title="日志管理", Path="/log", Name="log", Component="Layout", Icon="ele-DocumentCopy", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=12000 },
             new SysMenu{ Id=1310000000511, Pid=1310000000501, Title="访问日志", Path="/log/vislog", Name="sysVisLog", Component="/system/log/vislog/index", Icon="ele-Document", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000512, Pid=1310000000511, Title="查询", Permission="sysVislog:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

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

@@ -64,4 +64,58 @@ public class LoginPhoneInput
     /// </summary>
     [Required(ErrorMessage = "租户不能为空")]
     public long TenantId { get; set; }
+}
+
+/// <summary>
+/// 用户注册输入参数
+/// </summary>
+public class UserRegistrationInput
+{
+    /// <summary>
+    /// 真实姓名
+    /// </summary>
+    [Required(ErrorMessage = "真实姓名不能为空"), MinLength(2, ErrorMessage = "真实姓名不能少于2个字符")]
+    public string RealName { get; set; }
+    
+    /// <summary>
+    /// 账号
+    /// </summary>
+    [Required(ErrorMessage = "账号不能为空"), MinLength(6, ErrorMessage = "账号不能少于6个字符")]
+    public string Account { get; set; }
+    
+    /// <summary>
+    /// 手机号码
+    /// </summary>
+    /// <example>admin</example>
+    [Required(ErrorMessage = "手机号码不能为空")]
+    [DataValidation(ValidationTypes.PhoneNumber, ErrorMessage = "手机号码不正确")]
+    public string Phone { get; set; }
+    
+    /// <summary>
+    /// 验证码
+    /// </summary>
+    /// <example>123456</example>
+    [Required(ErrorMessage = "验证码不能为空")]
+    public string Code { get; set; }
+    
+    /// <summary>
+    /// 验证码Id
+    /// </summary>
+    public long CodeId { get; set; }
+    
+    /// <summary>
+    /// 租户
+    /// </summary>
+    [Required(ErrorMessage = "租户不能为空")]
+    public long TenantId { get; set; }
+    
+    /// <summary>
+    /// 密码
+    /// </summary>
+    public string Password { get; set; }
+    
+    /// <summary>
+    /// 注册方案
+    /// </summary>
+    public long WayId { get; set; }
 }

+ 36 - 0
Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs

@@ -6,6 +6,7 @@
 
 using Furion.SpecificationDocument;
 using Lazy.Captcha.Core;
+using NewLife.Reflection;
 
 namespace Admin.NET.Core.Service;
 
@@ -343,6 +344,41 @@ public class SysAuthService : IDynamicApiController, ITransient
         return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
     }
 
+    /// <summary>
+    /// 用户注册 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [AllowAnonymous]
+    [HttpPost, ApiDescriptionSettings(Description = "用户注册", DisableInherite = true)]
+    public async Task UserRegistration(UserRegistrationInput input)
+    {
+        // 校验验证码
+        if (!_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008);
+        _captcha.Generate(input.CodeId.ToString());
+        
+        // 判断租户是否有效且启用注册功能
+        var tenant = await _sysUserRep.Context.Queryable<SysTenant>().FirstAsync(u => u.Id == input.TenantId && u.Status == StatusEnum.Enable);
+        if (tenant?.EnableReg != YesNoEnum.Y) throw Oops.Oh(ErrorCodeEnum.D1034);
+        
+        // 查找注册方案
+        var wayId = input.WayId <= 0 ? tenant.RegWayId : input.WayId;
+        var regWay = await _sysUserRep.Context.Queryable<SysUserRegWay>().FirstAsync(u => u.Id == wayId) ?? throw Oops.Oh(ErrorCodeEnum.D1035);
+
+        var addUserInput = new AddUserInput
+        {
+            AccountType = regWay.AccountType,
+            NickName = "注册用户-" + input.Account,
+            OrgId = regWay.OrgId,
+            PosId = regWay.PosId,
+            TenantId = input.TenantId,
+            RoleIdList = new List<long> { regWay.RoleId },
+        };
+        addUserInput.Copy(input);
+        await App.GetService<SysUserService>().RegisterUser(addUserInput);
+    }
+
     /// <summary>
     /// Swagger登录检查 🔖
     /// </summary>

+ 10 - 0
Admin.NET/Admin.NET.Core/Service/Config/Dto/InfoInput.cs

@@ -62,6 +62,11 @@ public class InfoSaveInput
     /// </summary>
     [Required(ErrorMessage = "ICP地址不能为空")]
     public string SysIcpUrl { get; set; }
+    
+    /// <summary>
+    /// 启用注册功能
+    /// </summary>
+    public bool SysRegistration { get; set; }
 
     /// <summary>
     /// 登录二次验证
@@ -72,4 +77,9 @@ public class InfoSaveInput
     /// 图形验证码
     /// </summary>
     public bool SysCaptcha { get; set; }
+    
+    /// <summary>
+    /// 默认注册方案Id
+    /// </summary>
+    public virtual long SysRegWayId { get; set; }
 }

+ 6 - 2
Admin.NET/Admin.NET.Core/Service/Config/SysConfigService.cs

@@ -256,8 +256,10 @@ public class SysConfigService : IDynamicApiController, ITransient
             SysCopyright = tenant.Copyright,
             SysIcp = tenant.Icp,
             SysIcpUrl = tenant.IcpUrl,
+            SysRegWayId = tenant.RegWayId,
+            SysRegistration = tenant.EnableReg == YesNoEnum.Y,
             SysSecondVer = sysSecondVer,
-            SysCaptcha = sysCaptcha
+            SysCaptcha = sysCaptcha,
         };
     }
 
@@ -279,10 +281,12 @@ public class SysConfigService : IDynamicApiController, ITransient
         tenant.Copyright = input.SysCopyright;
         tenant.IcpUrl = input.SysIcpUrl;
         tenant.Icp = input.SysIcp;
+        tenant.RegWayId = input.SysRegistration ? input.SysRegWayId : null;
+        tenant.EnableReg = input.SysRegistration ? YesNoEnum.Y : YesNoEnum.N;
 
         await _sysConfigRep.Context.Updateable(tenant).ExecuteCommandAsync();
         await UpdateConfigValue(ConfigConst.SysSecondVer, input.SysSecondVer.ToString());
-        await UpdateConfigValue(ConfigConst.SysCaptcha, input.SysSecondVer.ToString());
+        await UpdateConfigValue(ConfigConst.SysCaptcha, input.SysCaptcha.ToString());
     }
 
     private void Remove(SysConfig config)

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

@@ -238,8 +238,11 @@ public class SysOrgService : IDynamicApiController, ITransient
         // 若子机构有用户则禁止删除
         var cOrgHasEmp = await _sysOrgRep.ChangeRepository<SqlSugarRepository<SysUser>>()
             .IsAnyAsync(u => childOrgIdList.Contains(u.OrgId));
-        if (cOrgHasEmp)
-            throw Oops.Oh(ErrorCodeEnum.D2007);
+        if (cOrgHasEmp) throw Oops.Oh(ErrorCodeEnum.D2007);
+        
+        // 若有绑定注册方案则禁止删除
+        var hasUserRegWay = await _sysOrgRep.Context.Queryable<SysUserRegWay>().AnyAsync(u => u.OrgId == input.Id);
+        if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D2010);
 
         // 删除与此机构、父机构有关的用户机构缓存
         DeleteAllUserOrgCache(sysOrg.Id, sysOrg.Pid);

+ 4 - 0
Admin.NET/Admin.NET.Core/Service/Pos/SysPosService.cs

@@ -100,6 +100,10 @@ public class SysPosService : IDynamicApiController, ITransient
         // 若附属职位有用户则禁止删除
         var hasExtPosEmp = await _sysUserExtOrgService.HasUserPos(input.Id);
         if (hasExtPosEmp) throw Oops.Oh(ErrorCodeEnum.D6001);
+        
+        // 若有绑定注册方案则禁止删除
+        var hasUserRegWay = await _sysPosRep.Context.Queryable<SysUserRegWay>().AnyAsync(u => u.PosId == input.Id);
+        if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D6004);
 
         await _sysPosRep.DeleteAsync(u => u.Id == input.Id);
     }

+ 4 - 0
Admin.NET/Admin.NET.Core/Service/Role/SysRoleService.cs

@@ -138,6 +138,10 @@ public class SysRoleService : IDynamicApiController, ITransient
         // 若角色有用户则禁止删除
         var userIds = await _sysUserRoleService.GetUserIdList(input.Id);
         if (userIds != null && userIds.Count > 0) throw Oops.Oh(ErrorCodeEnum.D1025);
+        
+        // 若有绑定注册方案则禁止删除
+        var hasUserRegWay = await _sysRoleRep.Context.Queryable<SysUserRegWay>().AnyAsync(u => u.RoleId == input.Id);
+        if (hasUserRegWay) throw Oops.Oh(ErrorCodeEnum.D1033);
 
         var sysRole = await _sysRoleRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
         await _sysRoleRep.DeleteAsync(sysRole);

+ 4 - 7
Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs

@@ -24,7 +24,6 @@ public class SysTenantService : IDynamicApiController, ITransient
     private readonly SysConfigService _sysConfigService;
     private readonly SysCacheService _sysCacheService;
     private readonly UploadOptions _uploadOptions;
-    private readonly UserManager _userManager;
 
     public SysTenantService(SqlSugarRepository<SysTenant> sysTenantRep,
         SqlSugarRepository<SysOrg> sysOrgRep,
@@ -37,8 +36,7 @@ public class SysTenantService : IDynamicApiController, ITransient
         SqlSugarRepository<SysUserRole> userRoleRep,
         IOptions<UploadOptions> uploadOptions,
         SysConfigService sysConfigService,
-        SysCacheService sysCacheService,
-        UserManager userManager)
+        SysCacheService sysCacheService)
     {
         _sysTenantRep = sysTenantRep;
         _sysOrgRep = sysOrgRep;
@@ -52,7 +50,6 @@ public class SysTenantService : IDynamicApiController, ITransient
         _uploadOptions = uploadOptions.Value;
         _sysConfigService = sysConfigService;
         _sysCacheService = sysCacheService;
-        _userManager = userManager;
     }
 
     /// <summary>
@@ -152,7 +149,7 @@ public class SysTenantService : IDynamicApiController, ITransient
         var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300);
 
-        input.Host = input.Host.ToLower();
+        input.Host = input.Host?.ToLower();
         isExist = await _sysTenantRep.IsAnyAsync(u => u.Host == input.Host);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303);
 
@@ -179,7 +176,7 @@ public class SysTenantService : IDynamicApiController, ITransient
             default:
                 throw Oops.Oh(ErrorCodeEnum.D3004);
         }
-
+        if (input.EnableReg == YesNoEnum.N) input.RegWayId = null;
         var tenant = input.Adapt<TenantOutput>();
         
         // 设置logo
@@ -360,7 +357,7 @@ public class SysTenantService : IDynamicApiController, ITransient
         var isExist = await _sysOrgRep.IsAnyAsync(u => u.Name == input.Name && u.Id != input.OrgId);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D1300);
 
-        input.Host = input.Host.ToLower();
+        input.Host = input.Host?.ToLower();
         isExist = await _sysTenantRep.IsAnyAsync(u => u.Host == input.Host && u.Id != input.Id);
         if (isExist) throw Oops.Oh(ErrorCodeEnum.D1303);
 

+ 73 - 0
Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayInput.cs

@@ -0,0 +1,73 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 注册方案分页查询输入参数
+/// </summary>
+public class PageUserRegWayInput : BasePageInput
+{
+    /// <summary>
+    /// 方案名称
+    /// </summary>
+    public string? Name { get; set; }
+    
+    /// <summary>
+    /// 租户Id
+    /// </summary>
+    public long TenantId { get; set; }
+}
+
+/// <summary>
+/// 注册方案增加输入参数
+/// </summary>
+public class AddUserRegWayInput : SysUserRegWay
+{
+    /// <summary>
+    /// 方案名称
+    /// </summary>
+    [Required(ErrorMessage = "方案名称不能为空")]
+    [MaxLength(32, ErrorMessage = "方案名称字符长度不能超过32")]
+    public override string Name { get; set; }
+    
+    /// <summary>
+    /// 账号类型
+    /// </summary>
+    [Dict(nameof(AccountTypeEnum), AllowNullValue=true)]
+    [Required(ErrorMessage = "账号类型不能为空")]
+    public override AccountTypeEnum AccountType { get; set; }
+    
+    /// <summary>
+    /// 角色
+    /// </summary>
+    [Required(ErrorMessage = "角色不能为空")]
+    public override long RoleId { get; set; }
+    
+    /// <summary>
+    /// 机构
+    /// </summary>
+    [Required(ErrorMessage = "机构不能为空")]
+    public override long OrgId { get; set; }
+    
+    /// <summary>
+    /// 职位
+    /// </summary>
+    [Required(ErrorMessage = "职位不能为空")]
+    public override long PosId { get; set; }
+}
+
+/// <summary>
+/// 注册方案更新输入参数
+/// </summary>
+public class UpdateUserRegWayInput : AddUserRegWayInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>    
+    [Required(ErrorMessage = "主键Id不能为空")]
+    public override long Id { get; set; }
+}

+ 28 - 0
Admin.NET/Admin.NET.Core/Service/User/Dto/UserRegWayOutput.cs

@@ -0,0 +1,28 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 注册方案输出参数
+/// </summary>
+public class UserRegWayOutput : SysUserRegWay
+{
+    /// <summary>
+    /// 角色名称
+    /// </summary>
+    public string RoleName { get; set; }   
+    
+    /// <summary>
+    /// 机构名称
+    /// </summary>
+    public string OrgName { get; set; }
+    
+    /// <summary>
+    /// 职位名称
+    /// </summary>
+    public string PosName { get; set; }
+}

+ 118 - 0
Admin.NET/Admin.NET.Core/Service/User/SysUserRegWayService.cs

@@ -0,0 +1,118 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统用户服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 490)]
+public class SysUserRegWayService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<SysUserRegWay> _sysUserRegWayRep;
+    private readonly UserManager _userManager;
+
+    public SysUserRegWayService(SqlSugarRepository<SysUserRegWay> sysUserRegWayRep, UserManager userManager)
+    {
+        _sysUserRegWayRep = sysUserRegWayRep;
+        _userManager = userManager;
+    }
+    
+    /// <summary>
+    /// 查询注册方案列表 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("查询注册方案列表")]
+    [ApiDescriptionSettings(Name = "List"), HttpPost]
+    public async Task<List<UserRegWayOutput>> List(PageUserRegWayInput input)
+    {
+        input.Keyword = input.Keyword?.Trim();
+        var query = _sysUserRegWayRep.AsQueryable()
+            .WhereIF(_userManager.SuperAdmin && input.TenantId > 0, u => u.TenantId == input.TenantId)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Name), u => u.Name.Contains(input.Name.Trim()))
+            .LeftJoin<SysRole>((u, a) => u.RoleId == a.Id)
+            .LeftJoin<SysOrg>((u, a, b) => u.OrgId == b.Id)
+            .LeftJoin<SysPos>((u, a, b, c) => u.PosId == c.Id)
+            .Select((u, a, b, c) => new UserRegWayOutput
+            {
+                RoleName = a.Name,
+                OrgName = b.Name,
+                PosName = c.Name,
+            }, true);
+        return await query.OrderBuilder(input).ToListAsync();
+    }
+
+    /// <summary>
+    /// 增加注册方案 ➕
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("增加注册方案")]
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    public async Task<long> Add(AddUserRegWayInput input)
+    {
+        var entity = input.Adapt<SysUserRegWay>();
+        if (await _sysUserRegWayRep.IsAnyAsync(u => u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101);
+        
+        await CheckData(input);
+        return await _sysUserRegWayRep.InsertAsync(entity) ? entity.Id : 0;
+    }
+
+    /// <summary>
+    /// 更新注册方案 ✏️
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("更新注册方案")]
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    public async Task Update(UpdateUserRegWayInput input)
+    {
+        if (await _sysUserRegWayRep.IsAnyAsync(u => u.Id != input.Id && u.Name == input.Name)) throw Oops.Oh(ErrorCodeEnum.D2101);
+
+        await CheckData(input);
+        await _sysUserRegWayRep.AsUpdateable(input).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 检查数据
+    /// </summary>
+    /// <param name="input"></param>
+    [NonAction]
+    public async Task CheckData(AddUserRegWayInput input)
+    {
+        // 检查外键数据是否存在
+        if (!await _sysUserRegWayRep.Context.Queryable<SysRole>().AnyAsync(u => u.Id == input.RoleId)) throw Oops.Oh(ErrorCodeEnum.D1036);
+        if (!await _sysUserRegWayRep.Context.Queryable<SysOrg>().AnyAsync(u => u.Id == input.OrgId)) throw Oops.Oh(ErrorCodeEnum.D2011);
+        if (!await _sysUserRegWayRep.Context.Queryable<SysPos>().AnyAsync(u => u.Id == input.PosId)) throw Oops.Oh(ErrorCodeEnum.D6003);
+        
+        // 禁止注册超级管理员和系统管理员
+        if (input.AccountType is AccountTypeEnum.SysAdmin or AccountTypeEnum.SuperAdmin) throw Oops.Oh(ErrorCodeEnum.D1037);
+    }
+
+    /// <summary>
+    /// 删除注册方案 ❌
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("删除注册方案")]
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    public async Task Delete(BaseIdInput input)
+    {
+        var entity = await _sysUserRegWayRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        
+        // 关闭相关租户注册功能
+        await _sysUserRegWayRep.Context.Updateable(new SysTenant{ EnableReg = YesNoEnum.N, RegWayId = null })
+            .UpdateColumns(u => new { u.EnableReg, u.RegWayId })
+            .Where(u => u.RegWayId == input.Id)
+            .ExecuteCommandAsync();
+        
+        // 删除方案
+        await _sysUserRegWayRep.DeleteAsync(entity);
+    }
+}

+ 35 - 1
Admin.NET/Admin.NET.Core/Service/User/SysUserService.cs

@@ -102,7 +102,6 @@ public class SysUserService : IDynamicApiController, ITransient
     [DisplayName("增加用户")]
     public virtual async Task<long> AddUser(AddUserInput input)
     {
-        // 是否租户隔离登录验证
         var query = _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == _userManager.TenantId || u.AccountType == AccountTypeEnum.SuperAdmin);
 
         if (await query.AnyAsync(u => u.Account == input.Account)) throw Oops.Oh(ErrorCodeEnum.D1003);
@@ -126,6 +125,41 @@ public class SysUserService : IDynamicApiController, ITransient
 
         return newUser.Id;
     }
+    
+    /// <summary>
+    /// 增加用户 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [NonAction]
+    public virtual async Task<long> RegisterUser(AddUserInput input)
+    {
+        var query = _sysUserRep.AsQueryable().ClearFilter().Where(u => u.TenantId == input.TenantId || u.AccountType == AccountTypeEnum.SuperAdmin);
+
+        if (await query.AnyAsync(u => u.Account == input.Account)) throw Oops.Oh(ErrorCodeEnum.D1003);
+        if (!string.IsNullOrWhiteSpace(input.Phone) && await query.AnyAsync(u => u.Phone == input.Phone)) throw Oops.Oh(ErrorCodeEnum.D1032);
+
+        if (string.IsNullOrWhiteSpace(input.Password))
+        {
+            var password = await _sysConfigService.GetConfigValue<string>(ConfigConst.SysPassword);
+            input.Password = CryptogramUtil.Encrypt(password);
+        }
+
+        var user = input.Adapt<SysUser>();
+        var newUser = await _sysUserRep.AsInsertable(user).ExecuteReturnEntityAsync();
+
+        input.Id = newUser.Id;
+        await UpdateRoleAndExtOrg(input);
+
+        // 增加域账号
+        if (!string.IsNullOrWhiteSpace(input.DomainAccount))
+            await _sysUserLdapService.AddUserLdap(newUser.TenantId!.Value, newUser.Id, newUser.Account, input.DomainAccount);
+
+        // 执行订阅事件
+        _sysUserEventHandler.OnEvent(this, SysUserEventTypeEnum.Register, input);
+
+        return newUser.Id;
+    }
 
     /// <summary>
     /// 更新用户 🔖

+ 5 - 0
Admin.NET/Admin.NET.Core/Service/User/UserManager.cs

@@ -32,6 +32,11 @@ public class UserManager : IScoped
     /// 真实姓名
     /// </summary>
     public string RealName => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.RealName)?.Value;
+    
+    /// <summary>
+    /// 是否超级管理员
+    /// </summary>
+    public AccountTypeEnum? AccountType => int.TryParse(_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AccountType)?.Value, out var val) ? (AccountTypeEnum?)val : null;
 
     /// <summary>
     /// 是否超级管理员

+ 2 - 0
Web/src/App.vue

@@ -136,6 +136,8 @@ const loadSysInfo = () => {
 			// 登录验证
 			themeConfig.value.secondVer = data.sysSecondVer;
 			themeConfig.value.captcha = data.sysCaptcha;
+			// 注册功能
+			themeConfig.value.registration = data.sysRegistration;
 			// 更新配置加载状态
 			themeConfig.value.isLoaded = true;
 

+ 1 - 0
Web/src/api-services/api.ts

@@ -49,6 +49,7 @@ export * from './apis/sys-sms-api';
 export * from './apis/sys-tenant-api';
 export * from './apis/sys-user-api';
 export * from './apis/sys-user-menu-api';
+export * from './apis/sys-user-reg-way-api';
 export * from './apis/sys-wechat-api';
 export * from './apis/sys-wechat-pay-api';
 export * from './apis/sys-wechat-user-api';

+ 84 - 0
Web/src/api-services/apis/sys-auth-api.ts

@@ -23,6 +23,7 @@ import { AdminResultObject } from '../models';
 import { AdminResultString } from '../models';
 import { LoginInput } from '../models';
 import { LoginPhoneInput } from '../models';
+import { UserRegistrationInput } from '../models';
 /**
  * SysAuthApi - axios parameter creator
  * @export
@@ -457,6 +458,54 @@ export const SysAuthApiAxiosParamCreator = function (configuration?: Configurati
             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 {UserRegistrationInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAuthUserRegistrationPost: async (body?: UserRegistrationInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysAuth/userRegistration`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            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};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
             return {
                 url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
                 options: localVarRequestOptions,
@@ -594,6 +643,20 @@ export const SysAuthApiFp = function(configuration?: Configuration) {
                 return axios.request(axiosRequestArgs);
             };
         },
+        /**
+         * 用户注册
+         * @summary 用户注册 🔖
+         * @param {UserRegistrationInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAuthUserRegistrationPost(body?: UserRegistrationInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysAuthApiAxiosParamCreator(configuration).apiSysAuthUserRegistrationPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
     }
 };
 
@@ -690,6 +753,16 @@ export const SysAuthApiFactory = function (configuration?: Configuration, basePa
         async apiSysAuthUserInfoGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultLoginUserOutput>> {
             return SysAuthApiFp(configuration).apiSysAuthUserInfoGet(options).then((request) => request(axios, basePath));
         },
+        /**
+         * 用户注册
+         * @summary 用户注册 🔖
+         * @param {UserRegistrationInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAuthUserRegistrationPost(body?: UserRegistrationInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysAuthApiFp(configuration).apiSysAuthUserRegistrationPost(body, options).then((request) => request(axios, basePath));
+        },
     };
 };
 
@@ -796,4 +869,15 @@ export class SysAuthApi extends BaseAPI {
     public async apiSysAuthUserInfoGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultLoginUserOutput>> {
         return SysAuthApiFp(this.configuration).apiSysAuthUserInfoGet(options).then((request) => request(this.axios, this.basePath));
     }
+    /**
+     * 用户注册
+     * @summary 用户注册 🔖
+     * @param {UserRegistrationInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAuthApi
+     */
+    public async apiSysAuthUserRegistrationPost(body?: UserRegistrationInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysAuthApiFp(this.configuration).apiSysAuthUserRegistrationPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
 }

+ 391 - 0
Web/src/api-services/apis/sys-user-reg-way-api.ts

@@ -0,0 +1,391 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import globalAxios, { AxiosResponse, AxiosInstance, AxiosRequestConfig } from 'axios';
+import { Configuration } from '../configuration';
+// Some imports not used depending on template conditions
+// @ts-ignore
+import { BASE_PATH, COLLECTION_FORMATS, RequestArgs, BaseAPI, RequiredError } from '../base';
+import { AddUserRegWayInput } from '../models';
+import { AdminResultInt64 } from '../models';
+import { AdminResultListUserRegWayOutput } from '../models';
+import { BaseIdInput } from '../models';
+import { PageUserRegWayInput } from '../models';
+import { UpdateUserRegWayInput } from '../models';
+/**
+ * SysUserRegWayApi - axios parameter creator
+ * @export
+ */
+export const SysUserRegWayApiAxiosParamCreator = function (configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加注册方案 ➕
+         * @param {AddUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUserRegWayAddPost: async (body?: AddUserRegWayInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUserRegWay/add`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            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};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 删除注册方案 ❌
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUserRegWayDeletePost: async (body?: BaseIdInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUserRegWay/delete`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            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};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 查询注册方案列表 🔖
+         * @param {PageUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUserRegWayListPost: async (body?: PageUserRegWayInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUserRegWay/list`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            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};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 更新注册方案 ✏️
+         * @param {UpdateUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysUserRegWayUpdatePost: async (body?: UpdateUserRegWayInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysUserRegWay/update`;
+            // 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: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'application/json-patch+json';
+
+            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};
+            const needsSerialization = (typeof body !== "string") || localVarRequestOptions.headers['Content-Type'] === 'application/json';
+            localVarRequestOptions.data =  needsSerialization ? JSON.stringify(body !== undefined ? body : {}) : (body || "");
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+    }
+};
+
+/**
+ * SysUserRegWayApi - functional programming interface
+ * @export
+ */
+export const SysUserRegWayApiFp = function(configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加注册方案 ➕
+         * @param {AddUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayAddPost(body?: AddUserRegWayInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultInt64>>> {
+            const localVarAxiosArgs = await SysUserRegWayApiAxiosParamCreator(configuration).apiSysUserRegWayAddPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 删除注册方案 ❌
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUserRegWayApiAxiosParamCreator(configuration).apiSysUserRegWayDeletePost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 查询注册方案列表 🔖
+         * @param {PageUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayListPost(body?: PageUserRegWayInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListUserRegWayOutput>>> {
+            const localVarAxiosArgs = await SysUserRegWayApiAxiosParamCreator(configuration).apiSysUserRegWayListPost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 更新注册方案 ✏️
+         * @param {UpdateUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayUpdatePost(body?: UpdateUserRegWayInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysUserRegWayApiAxiosParamCreator(configuration).apiSysUserRegWayUpdatePost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+    }
+};
+
+/**
+ * SysUserRegWayApi - factory interface
+ * @export
+ */
+export const SysUserRegWayApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    return {
+        /**
+         * 
+         * @summary 增加注册方案 ➕
+         * @param {AddUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayAddPost(body?: AddUserRegWayInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultInt64>> {
+            return SysUserRegWayApiFp(configuration).apiSysUserRegWayAddPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 删除注册方案 ❌
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUserRegWayApiFp(configuration).apiSysUserRegWayDeletePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 查询注册方案列表 🔖
+         * @param {PageUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayListPost(body?: PageUserRegWayInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListUserRegWayOutput>> {
+            return SysUserRegWayApiFp(configuration).apiSysUserRegWayListPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 更新注册方案 ✏️
+         * @param {UpdateUserRegWayInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysUserRegWayUpdatePost(body?: UpdateUserRegWayInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysUserRegWayApiFp(configuration).apiSysUserRegWayUpdatePost(body, options).then((request) => request(axios, basePath));
+        },
+    };
+};
+
+/**
+ * SysUserRegWayApi - object-oriented interface
+ * @export
+ * @class SysUserRegWayApi
+ * @extends {BaseAPI}
+ */
+export class SysUserRegWayApi extends BaseAPI {
+    /**
+     * 
+     * @summary 增加注册方案 ➕
+     * @param {AddUserRegWayInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUserRegWayApi
+     */
+    public async apiSysUserRegWayAddPost(body?: AddUserRegWayInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultInt64>> {
+        return SysUserRegWayApiFp(this.configuration).apiSysUserRegWayAddPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 删除注册方案 ❌
+     * @param {BaseIdInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUserRegWayApi
+     */
+    public async apiSysUserRegWayDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUserRegWayApiFp(this.configuration).apiSysUserRegWayDeletePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 查询注册方案列表 🔖
+     * @param {PageUserRegWayInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUserRegWayApi
+     */
+    public async apiSysUserRegWayListPost(body?: PageUserRegWayInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListUserRegWayOutput>> {
+        return SysUserRegWayApiFp(this.configuration).apiSysUserRegWayListPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 更新注册方案 ✏️
+     * @param {UpdateUserRegWayInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysUserRegWayApi
+     */
+    public async apiSysUserRegWayUpdatePost(body?: UpdateUserRegWayInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysUserRegWayApiFp(this.configuration).apiSysUserRegWayUpdatePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+}

+ 14 - 1
Web/src/api-services/models/add-tenant-input.ts

@@ -14,6 +14,7 @@
 import { DbType } from './db-type';
 import { StatusEnum } from './status-enum';
 import { TenantTypeEnum } from './tenant-type-enum';
+import { YesNoEnum } from './yes-no-enum';
 /**
  * 
  * @export
@@ -116,6 +117,18 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     slaveConnections?: string | null;
+    /**
+     * 
+     * @type {YesNoEnum}
+     * @memberof AddTenantInput
+     */
+    enableReg?: YesNoEnum;
+    /**
+     * 默认注册方案Id
+     * @type {number}
+     * @memberof AddTenantInput
+     */
+    regWayId?: number | null;
     /**
      * 图标
      * @type {string}
@@ -217,7 +230,7 @@ export interface AddTenantInput {
      * @type {string}
      * @memberof AddTenantInput
      */
-    logoBase64: string;
+    logoBase64?: string | null;
     /**
      * Logo文件名
      * @type {string}

+ 117 - 0
Web/src/api-services/models/add-user-reg-way-input.ts

@@ -0,0 +1,117 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { AccountTypeEnum } from './account-type-enum';
+/**
+ * 注册方案增加输入参数
+ * @export
+ * @interface AddUserRegWayInput
+ */
+export interface AddUserRegWayInput {
+    /**
+     * 雪花Id
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    id?: number;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof AddUserRegWayInput
+     */
+    createTime?: Date;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof AddUserRegWayInput
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof AddUserRegWayInput
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof AddUserRegWayInput
+     */
+    updateUserName?: string | null;
+    /**
+     * 软删除
+     * @type {boolean}
+     * @memberof AddUserRegWayInput
+     */
+    isDelete?: boolean;
+    /**
+     * 租户Id
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    tenantId?: number | null;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    orderNo?: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof AddUserRegWayInput
+     */
+    remark?: string | null;
+    /**
+     * 方案名称
+     * @type {string}
+     * @memberof AddUserRegWayInput
+     */
+    name: string;
+    /**
+     * 
+     * @type {AccountTypeEnum}
+     * @memberof AddUserRegWayInput
+     */
+    accountType: AccountTypeEnum;
+    /**
+     * 角色
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    roleId: number;
+    /**
+     * 机构
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    orgId: number;
+    /**
+     * 职位
+     * @type {number}
+     * @memberof AddUserRegWayInput
+     */
+    posId: number;
+}

+ 57 - 0
Web/src/api-services/models/admin-result-list-user-reg-way-output.ts

@@ -0,0 +1,57 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { UserRegWayOutput } from './user-reg-way-output';
+/**
+ * 全局返回结果
+ * @export
+ * @interface AdminResultListUserRegWayOutput
+ */
+export interface AdminResultListUserRegWayOutput {
+    /**
+     * 状态码
+     * @type {number}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    code?: number;
+    /**
+     * 类型success、warning、error
+     * @type {string}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    type?: string | null;
+    /**
+     * 错误信息
+     * @type {string}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    message?: string | null;
+    /**
+     * 数据
+     * @type {Array<UserRegWayOutput>}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    result?: Array<UserRegWayOutput> | null;
+    /**
+     * 附加数据
+     * @type {any}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    extras?: any | null;
+    /**
+     * 时间
+     * @type {Date}
+     * @memberof AdminResultListUserRegWayOutput
+     */
+    time?: Date;
+}

+ 6 - 0
Web/src/api-services/models/index.ts

@@ -19,6 +19,7 @@ export * from './add-subscribe-message-template-input';
 export * from './add-sys-ldap-input';
 export * from './add-tenant-input';
 export * from './add-user-input';
+export * from './add-user-reg-way-input';
 export * from './admin-result-boolean';
 export * from './admin-result-data-set';
 export * from './admin-result-data-table';
@@ -61,6 +62,7 @@ export * from './admin-result-list-sys-user-ext-org';
 export * from './admin-result-list-sys-user-ldap';
 export * from './admin-result-list-sys-wechat-refund';
 export * from './admin-result-list-table-output';
+export * from './admin-result-list-user-reg-way-output';
 export * from './admin-result-login-output';
 export * from './admin-result-login-user-output';
 export * from './admin-result-object';
@@ -259,6 +261,7 @@ export * from './page-region-input';
 export * from './page-role-input';
 export * from './page-tenant-input';
 export * from './page-user-input';
+export * from './page-user-reg-way-input';
 export * from './page-vis-log-input';
 export * from './parameter-attributes';
 export * from './parameter-info';
@@ -387,10 +390,13 @@ export * from './update-schedule-input';
 export * from './update-sys-ldap-input';
 export * from './update-tenant-input';
 export * from './update-user-input';
+export * from './update-user-reg-way-input';
 export * from './upload-file-from-base64-input';
 export * from './user-input';
 export * from './user-menu-input';
 export * from './user-output';
+export * from './user-reg-way-output';
+export * from './user-registration-input';
 export * from './user-role-input';
 export * from './visual-column';
 export * from './visual-db-table';

+ 12 - 0
Web/src/api-services/models/info-save-input.ts

@@ -71,6 +71,12 @@ export interface InfoSaveInput {
      * @memberof InfoSaveInput
      */
     sysIcpUrl: string;
+    /**
+     * 启用注册功能
+     * @type {boolean}
+     * @memberof InfoSaveInput
+     */
+    sysRegistration?: boolean;
     /**
      * 登录二次验证
      * @type {boolean}
@@ -83,4 +89,10 @@ export interface InfoSaveInput {
      * @memberof InfoSaveInput
      */
     sysCaptcha?: boolean;
+    /**
+     * 默认注册方案Id
+     * @type {number}
+     * @memberof InfoSaveInput
+     */
+    sysRegWayId?: number;
 }

+ 82 - 0
Web/src/api-services/models/page-user-reg-way-input.ts

@@ -0,0 +1,82 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { Filter } from './filter';
+import { Search } from './search';
+/**
+ * 注册方案分页查询输入参数
+ * @export
+ * @interface PageUserRegWayInput
+ */
+export interface PageUserRegWayInput {
+    /**
+     * 
+     * @type {Search}
+     * @memberof PageUserRegWayInput
+     */
+    search?: Search;
+    /**
+     * 模糊查询关键字
+     * @type {string}
+     * @memberof PageUserRegWayInput
+     */
+    keyword?: string | null;
+    /**
+     * 
+     * @type {Filter}
+     * @memberof PageUserRegWayInput
+     */
+    filter?: Filter;
+    /**
+     * 当前页码
+     * @type {number}
+     * @memberof PageUserRegWayInput
+     */
+    page?: number;
+    /**
+     * 页码容量
+     * @type {number}
+     * @memberof PageUserRegWayInput
+     */
+    pageSize?: number;
+    /**
+     * 排序字段
+     * @type {string}
+     * @memberof PageUserRegWayInput
+     */
+    field?: string | null;
+    /**
+     * 排序方向
+     * @type {string}
+     * @memberof PageUserRegWayInput
+     */
+    order?: string | null;
+    /**
+     * 降序排序
+     * @type {string}
+     * @memberof PageUserRegWayInput
+     */
+    descStr?: string | null;
+    /**
+     * 方案名称
+     * @type {string}
+     * @memberof PageUserRegWayInput
+     */
+    name?: string | null;
+    /**
+     * 租户Id
+     * @type {number}
+     * @memberof PageUserRegWayInput
+     */
+    tenantId?: number;
+}

+ 13 - 0
Web/src/api-services/models/tenant-output.ts

@@ -14,6 +14,7 @@
 import { DbType } from './db-type';
 import { StatusEnum } from './status-enum';
 import { TenantTypeEnum } from './tenant-type-enum';
+import { YesNoEnum } from './yes-no-enum';
 /**
  * 
  * @export
@@ -122,6 +123,18 @@ export interface TenantOutput {
      * @memberof TenantOutput
      */
     slaveConnections?: string | null;
+    /**
+     * 
+     * @type {YesNoEnum}
+     * @memberof TenantOutput
+     */
+    enableReg?: YesNoEnum;
+    /**
+     * 默认注册方案Id
+     * @type {number}
+     * @memberof TenantOutput
+     */
+    regWayId?: number | null;
     /**
      * 图标
      * @type {string}

+ 13 - 0
Web/src/api-services/models/update-tenant-input.ts

@@ -14,6 +14,7 @@
 import { DbType } from './db-type';
 import { StatusEnum } from './status-enum';
 import { TenantTypeEnum } from './tenant-type-enum';
+import { YesNoEnum } from './yes-no-enum';
 /**
  * 
  * @export
@@ -116,6 +117,18 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     slaveConnections?: string | null;
+    /**
+     * 
+     * @type {YesNoEnum}
+     * @memberof UpdateTenantInput
+     */
+    enableReg?: YesNoEnum;
+    /**
+     * 默认注册方案Id
+     * @type {number}
+     * @memberof UpdateTenantInput
+     */
+    regWayId?: number | null;
     /**
      * 图标
      * @type {string}

+ 117 - 0
Web/src/api-services/models/update-user-reg-way-input.ts

@@ -0,0 +1,117 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { AccountTypeEnum } from './account-type-enum';
+/**
+ * 注册方案更新输入参数
+ * @export
+ * @interface UpdateUserRegWayInput
+ */
+export interface UpdateUserRegWayInput {
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof UpdateUserRegWayInput
+     */
+    createTime?: Date;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof UpdateUserRegWayInput
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof UpdateUserRegWayInput
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof UpdateUserRegWayInput
+     */
+    updateUserName?: string | null;
+    /**
+     * 软删除
+     * @type {boolean}
+     * @memberof UpdateUserRegWayInput
+     */
+    isDelete?: boolean;
+    /**
+     * 租户Id
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    tenantId?: number | null;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    orderNo?: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof UpdateUserRegWayInput
+     */
+    remark?: string | null;
+    /**
+     * 方案名称
+     * @type {string}
+     * @memberof UpdateUserRegWayInput
+     */
+    name: string;
+    /**
+     * 
+     * @type {AccountTypeEnum}
+     * @memberof UpdateUserRegWayInput
+     */
+    accountType: AccountTypeEnum;
+    /**
+     * 角色
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    roleId: number;
+    /**
+     * 机构
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    orgId: number;
+    /**
+     * 职位
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    posId: number;
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof UpdateUserRegWayInput
+     */
+    id: number;
+}

+ 135 - 0
Web/src/api-services/models/user-reg-way-output.ts

@@ -0,0 +1,135 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+import { AccountTypeEnum } from './account-type-enum';
+/**
+ * 注册方案输出参数
+ * @export
+ * @interface UserRegWayOutput
+ */
+export interface UserRegWayOutput {
+    /**
+     * 雪花Id
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    id?: number;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof UserRegWayOutput
+     */
+    createTime?: Date;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof UserRegWayOutput
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    updateUserName?: string | null;
+    /**
+     * 软删除
+     * @type {boolean}
+     * @memberof UserRegWayOutput
+     */
+    isDelete?: boolean;
+    /**
+     * 租户Id
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    tenantId?: number | null;
+    /**
+     * 方案名称
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    name?: string | null;
+    /**
+     * 
+     * @type {AccountTypeEnum}
+     * @memberof UserRegWayOutput
+     */
+    accountType?: AccountTypeEnum;
+    /**
+     * 注册用户默认角色
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    roleId?: number;
+    /**
+     * 注册用户默认机构
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    orgId?: number;
+    /**
+     * 注册用户默认职位
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    posId?: number;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof UserRegWayOutput
+     */
+    orderNo?: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    remark?: string | null;
+    /**
+     * 角色名称
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    roleName?: string | null;
+    /**
+     * 机构名称
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    orgName?: string | null;
+    /**
+     * 职位名称
+     * @type {string}
+     * @memberof UserRegWayOutput
+     */
+    posName?: string | null;
+}

+ 68 - 0
Web/src/api-services/models/user-registration-input.ts

@@ -0,0 +1,68 @@
+/* tslint:disable */
+/* eslint-disable */
+/**
+ * Admin.NET 通用权限开发平台
+ * 让 .NET 开发更简单、更通用、更流行。整合最新技术,模块插件式开发,前后端分离,开箱即用。<br/><u><b><font color='FF0000'> 👮不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!</font></b></u>
+ *
+ * OpenAPI spec version: 1.0.0
+ * 
+ *
+ * NOTE: This class is auto generated by the swagger code generator program.
+ * https://github.com/swagger-api/swagger-codegen.git
+ * Do not edit the class manually.
+ */
+/**
+ * 用户注册输入参数
+ * @export
+ * @interface UserRegistrationInput
+ */
+export interface UserRegistrationInput {
+    /**
+     * 真实姓名
+     * @type {string}
+     * @memberof UserRegistrationInput
+     */
+    realName: string;
+    /**
+     * 账号
+     * @type {string}
+     * @memberof UserRegistrationInput
+     */
+    account: string;
+    /**
+     * 手机号码
+     * @type {string}
+     * @memberof UserRegistrationInput
+     */
+    phone: string;
+    /**
+     * 验证码
+     * @type {string}
+     * @memberof UserRegistrationInput
+     */
+    code: string;
+    /**
+     * 验证码Id
+     * @type {number}
+     * @memberof UserRegistrationInput
+     */
+    codeId?: number;
+    /**
+     * 租户
+     * @type {number}
+     * @memberof UserRegistrationInput
+     */
+    tenantId: number;
+    /**
+     * 密码
+     * @type {string}
+     * @memberof UserRegistrationInput
+     */
+    password?: string | null;
+    /**
+     * 注册方案
+     * @type {number}
+     * @memberof UserRegistrationInput
+     */
+    wayId?: number;
+}

+ 10 - 0
Web/src/i18n/pages/login/zh-cn.ts

@@ -3,11 +3,20 @@ export default {
 	label: {
 		one1: '用户名登录',
 		two2: '手机号登录',
+		two3: '用户注册',
 	},
 	link: {
 		one3: '第三方登录',
 		two4: '友情链接',
 	},
+	register: {
+		placeholder1: '请选择租户',
+		placeholder2: '请输入手机号',
+		placeholder3: '请输入登录账号',
+		placeholder4: '请输入您的姓名',
+		placeholder5: '请输入验证码',
+		btnText: '注 册',
+	},
 	account: {
 		accountPlaceholder1: '用户名 admin 或不输均为 common',
 		accountPlaceholder2: '密码:123456',
@@ -28,4 +37,5 @@ export default {
 		text: '打开手机扫一扫,快速登录/注册',
 	},
 	signInText: '欢迎回来!',
+	registerText: '欢迎加入, 请使用默认密码登录!',
 };

+ 1 - 0
Web/src/types/pinia.d.ts

@@ -98,6 +98,7 @@ declare interface ThemeConfigState {
 		icp: string; // Icp备案号
 		icpUrl: string; // Icp地址
 		secondVer: boolean; // 是否开启二级验证
+		registration: boolean; // 是否开启注册功能
 		captcha: boolean; // 是否开启验证码
 		isLoaded: boolean; // 是否加载完成
 	};

+ 2 - 0
Web/src/views/login/component/account.vue

@@ -97,6 +97,7 @@ import { storeToRefs } from 'pinia';
 
 import { accessTokenKey, clearTokens, feature, getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi } from '/@/api-services/api';
+import {useUserInfo} from "/@/stores/userInfo";
 
 const props = defineProps({
 	tenantInfo: {
@@ -224,6 +225,7 @@ const onSignIn = async () => {
 
 			// SM2加密密码
 			// const keys = SM2.generateKeyPair();
+			state.ruleForm.tenantId ??= props.tenantInfo.id;
 			const publicKey = window.__env__.VITE_SM_PUBLIC_KEY;
 			const password = sm2.doEncrypt(state.ruleForm.password, publicKey, 1);
 			const [err, res] = await feature(getAPI(SysAuthApi).apiSysAuthLoginPost({ ...state.ruleForm, password: password } as any));

+ 2 - 2
Web/src/views/login/component/mobile.vue

@@ -98,8 +98,8 @@ const getSmsCode = async () => {
 
 // 登录
 const onSignIn = async () => {
-  const host = route.query.host ?? location.host;
-	var res = await getAPI(SysAuthApi).apiSysAuthLoginPhonePost({...state.ruleForm, host: host});
+	state.ruleForm.tenantId ??= props.tenantInfo.id;
+	const res = await getAPI(SysAuthApi).apiSysAuthLoginPhonePost({...state.ruleForm, host: host});
 	if (res.data.result?.accessToken == undefined) {
 		ElMessage.error('登录失败,请检查账号!');
 		return;

+ 352 - 0
Web/src/views/login/component/register.vue

@@ -0,0 +1,352 @@
+<template>
+	<el-tooltip :visible="state.capsLockVisible" effect="light" content="大写锁定已打开" placement="top">
+		<el-form ref="ruleFormRef" :model="state.ruleForm" size="large" :rules="state.rules" class="login-content-form">
+			<el-form-item class="login-animation2" prop="tenantId" clearable v-if="!tenantInfo.list.some((e: any) => e.host === tenantInfo.host)">
+				<el-select v-model="state.ruleForm.tenantId" :placeholder="$t('message.register.placeholder1')" style="width: 100%">
+					<template #prefix>
+						<i class="iconfont icon-shuxingtu el-input__icon"></i>
+					</template>
+					<el-option :value="item.value" :label="`${item.label} (${item.host})`" v-for="(item, index) in tenantInfo.list" :key="index" />
+				</el-select>
+			</el-form-item>
+			<el-form-item class="login-animation1" prop="phone" clearable>
+				<el-input text :placeholder="$t('message.register.placeholder2')" v-model="state.ruleForm.phone" clearable autocomplete="off">
+					<template #prefix>
+						<i class="iconfont icon-dianhua el-input__icon"></i>
+					</template>
+				</el-input>
+			</el-form-item>
+			<el-form-item class="login-animation1" prop="account" clearable>
+				<el-input ref="accountRef" text :placeholder="$t('message.register.placeholder3')" v-model="state.ruleForm.account" clearable autocomplete="off" @keyup.enter.native="handleRegister">
+					<template #prefix>
+						<el-icon>
+							<ele-User />
+						</el-icon>
+					</template>
+				</el-input>
+			</el-form-item>
+			<el-form-item class="login-animation1" prop="realName" clearable>
+				<el-input ref="accountRef" text :placeholder="$t('message.register.placeholder4')" v-model="state.ruleForm.realName" clearable autocomplete="off" @keyup.enter.native="handleRegister">
+					<template #prefix>
+						<el-icon>
+							<ele-User />
+						</el-icon>
+					</template>
+				</el-input>
+			</el-form-item>
+			<el-form-item class="login-animation3" prop="code">
+				<el-col :span="15">
+					<el-input
+						ref="codeRef"
+						text
+						maxlength="4"
+						:placeholder="$t('message.register.placeholder5')"
+						v-model="state.ruleForm.code"
+						clearable
+						autocomplete="off"
+						@keyup.enter.native="handleRegister"
+					>
+						<template #prefix>
+							<el-icon>
+								<ele-Position />
+							</el-icon>
+						</template>
+					</el-input>
+				</el-col>
+				<el-col :span="1"></el-col>
+				<el-col :span="8">
+					<div :class="[state.expirySeconds > 0 ? 'login-content-code' : 'login-content-code-expired']" @click="getCaptcha">
+						<img class="login-content-code-img" width="130px" height="38px" :src="state.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 v-waves @click="handleRegister" :loading="state.loading.register">
+					<span>{{ $t('message.register.btnText') }}</span>
+				</el-button>
+			</el-form-item>
+			<div class="font12 mt30 login-animation4 login-msg">{{ $t('message.mobile.msgText') }}</div>
+		</el-form>
+	</el-tooltip>
+	<div class="dialog-header">
+		<el-dialog v-model="state.rotateVerifyVisible" :show-close="false">
+			<DragVerifyImgRotate
+				ref="dragRef"
+				:imgsrc="state.rotateVerifyImg"
+				v-model:isPassing="state.isPassRotate"
+				text="请按住滑块拖动"
+				successText="验证通过"
+				handlerIcon="fa fa-angle-double-right"
+				successIcon="fa fa-hand-peace-o"
+				@passcallback="passRotateVerify"
+			/>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts" setup name="loginAccount">
+import {reactive, ref, onMounted, defineAsyncComponent, onUnmounted, watch } from 'vue';
+import { useRoute, useRouter } from 'vue-router';
+import { ElMessage, InputInstance } from 'element-plus';
+import { useI18n } from 'vue-i18n';
+import { storeToRefs } from 'pinia';
+import { feature, getAPI } from '/@/utils/axios-utils';
+import { SysAuthApi } from '/@/api-services/api';
+import { useThemeConfig } from '/@/stores/themeConfig';
+import {sm2} from "sm-crypto-v2";
+
+const props = defineProps({
+	tenantInfo: {
+		required: true,
+		type: Object,
+	},
+});
+
+// 旋转图片滑块组件
+// import verifyImg from '/@/assets/logo-mini.svg';
+const DragVerifyImgRotate = defineAsyncComponent(() => import('/@/components/dragVerify/dragVerifyImgRotate.vue'));
+
+const storesThemeConfig = useThemeConfig();
+const { themeConfig } = storeToRefs(storesThemeConfig);
+
+const { t } = useI18n();
+const route = useRoute();
+const router = useRouter();
+
+const ruleFormRef = ref();
+const accountRef = ref<InputInstance>();
+const codeRef = ref<InputInstance>();
+
+const emits = defineEmits(['reload']);
+const dragRef: any = ref(null);
+const state = reactive({
+	ruleForm: {
+		tenantId: props.tenantInfo.id,
+		account: undefined,
+		password: undefined,
+		realName: undefined,
+		phone: undefined,
+		wayId: undefined,
+		code: '',
+		codeId: 0,
+	},
+	rules: {
+		account: [{ required: true, message: '请输入用户名', trigger: 'blur' }],
+		realName: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
+		phone: [{ required: true, message: '请输入手机号', trigger: 'blur' }],
+		code: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
+	},
+	loading: {
+		register: false,
+	},
+	captchaImage: '',
+	rotateVerifyVisible: false,
+	rotateVerifyImg: themeConfig.value.logoUrl,
+	secondVerEnabled: false,
+	isPassRotate: false,
+	capsLockVisible: false,
+	expirySeconds: 60, // 验证码过期时间
+});
+
+// 验证码过期计时器
+let timer: any = null;
+
+// 页面初始化
+onMounted(async () => {
+	// 默认尝试从地址栏获取wayid注册方案id
+	if (route.query.wayid) state.ruleForm.wayId = route.query.wayid;
+	watch(
+		() => themeConfig.value.isLoaded,
+		(isLoaded) => {
+			if (isLoaded) {
+				// 获取登录配置
+				state.secondVerEnabled = themeConfig.value.secondVer ?? true;
+
+				// 获取验证码
+				getCaptcha();
+
+				// 注册验证码过期计时器
+				timer = setInterval(() => {
+					if (state.expirySeconds > 0) state.expirySeconds -= 1;
+				}, 1000);
+			}
+		},
+		{ immediate: true }
+	);
+
+	// 检测大小写按键/CapsLK
+	document.addEventListener('keyup', handleKeyPress);
+});
+
+// 页面卸载
+onUnmounted(() => {
+	// 销毁验证码过期计时器
+	clearInterval(timer);
+	timer = null;
+
+	document.removeEventListener('keyup', handleKeyPress);
+});
+
+// 检测大小写按键
+const handleKeyPress = (e: KeyboardEvent) => {
+	state.capsLockVisible = e.getModifierState('CapsLock');
+};
+
+// 获取验证码
+const getCaptcha = async () => {
+	state.ruleForm.code = '';
+	const res = await getAPI(SysAuthApi).apiSysAuthCaptchaGet().then(res => res.data.result);
+	state.captchaImage = 'data:text/html;base64,' + res?.img;
+	state.expirySeconds = res?.expirySeconds;
+  state.ruleForm.codeId = res?.id;
+};
+
+// 注册
+const onRegister = async () => {
+	ruleFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return false;
+
+		try {
+			state.loading.register = true;
+
+			state.ruleForm.tenantId ??= props.tenantInfo.id;
+			const publicKey = window.__env__.VITE_SM_PUBLIC_KEY;
+			const password = state.ruleForm.password ? sm2.doEncrypt(state.ruleForm.password, publicKey, 1) : undefined;
+			const [err, res] = await feature(getAPI(SysAuthApi).apiSysAuthUserRegistrationPost({...state.ruleForm, password: password } as any));
+
+			if (res?.data?.code === 200) {
+				const registerText = t('message.registerText');
+				ElMessage.success(registerText);
+				emits('goLogin');
+				return;
+			}
+
+			if (err) {
+				getCaptcha(); // 重新获取验证码
+			} else if (res.data.type != 'success') {
+				getCaptcha(); // 重新获取验证码
+				ElMessage.error('注册失败!');
+			}
+		} finally {
+			state.loading.register = false;
+		}
+	});
+};
+
+// 打开旋转验证
+const openRotateVerify = () => {
+	state.rotateVerifyVisible = true;
+	state.isPassRotate = false;
+	dragRef.value?.reset();
+};
+
+// 通过旋转验证
+const passRotateVerify = () => {
+	state.rotateVerifyVisible = false;
+	state.isPassRotate = true;
+	onSignIn();
+};
+
+// 注册处理
+const handleRegister = () => {
+	ruleFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return false;
+		state.secondVerEnabled ? openRotateVerify() : onRegister();
+	});
+};
+</script>
+
+<style lang="scss" scoped>
+.dialog-header {
+	:deep(.el-dialog) {
+		width: unset !important;
+
+		.el-dialog__header {
+			display: none;
+		}
+
+		.el-dialog__wrapper {
+			position: absolute !important;
+		}
+
+		.v-modal {
+			position: absolute !important;
+		}
+	}
+}
+
+.login-content-form {
+	margin-top: 20px;
+
+	@for $i from 0 through 4 {
+		.login-animation#{$i} {
+			opacity: 0;
+			animation-name: error-num;
+			animation-duration: 0.5s;
+			animation-fill-mode: forwards;
+			animation-delay: calc($i/10) + s;
+		}
+	}
+
+	.login-content-password {
+		display: inline-block;
+		width: 20px;
+		cursor: pointer;
+
+		&:hover {
+			color: #909399;
+		}
+	}
+
+	.login-content-code {
+		display: flex;
+		align-items: center;
+		justify-content: space-around;
+		position: relative;
+
+		.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-code-expired {
+		@extend .login-content-code;
+		&::before {
+			content: '验证码已过期';
+			position: absolute;
+			top: 0;
+			left: 0;
+			right: 0;
+			bottom: 0;
+			border-radius: 4px;
+			background-color: rgba(0, 0, 0, 0.5);
+			color: #ffffff;
+			text-align: center;
+		}
+	}
+
+	.login-content-submit {
+		width: 100%;
+		letter-spacing: 2px;
+		font-weight: 300;
+		margin-top: 15px;
+	}
+
+	.login-msg {
+		color: var(--el-text-color-placeholder);
+	}
+}
+</style>

+ 34 - 4
Web/src/views/login/index.vue

@@ -29,12 +29,15 @@
 					<div class="login-right-warp-main-form">
 						<div v-if="!state.isScan">
 							<el-tabs v-model="state.tabsActiveName">
-								<el-tab-pane :label="$t('message.label.one1')" name="account">
+								<el-tab-pane :label="$t('message.label.one1')" name="account" v-if="state.tabsActiveName != 'register'">
 									<Account :tenant-info="state.tenantInfo" />
 								</el-tab-pane>
-								<el-tab-pane :label="$t('message.label.two2')" name="mobile">
+								<el-tab-pane :label="$t('message.label.two2')" name="mobile" v-if="state.tabsActiveName != 'register'">
 									<Mobile :tenant-info="state.tenantInfo" />
 								</el-tab-pane>
+								<el-tab-pane :label="$t('message.label.two3')" name="register" v-if="state.tabsActiveName == 'register'">
+									<Register :tenant-info="state.tenantInfo" @goLogin="() => state.tabsActiveName = 'account'" />
+								</el-tab-pane>
 							</el-tabs>
 						</div>
 						<Scan v-if="state.isScan" :tenant-info="state.tenantInfo" />
@@ -42,19 +45,27 @@
 							<i class="iconfont" :class="state.isScan ? 'icon-diannao1' : 'icon-barcode-qr'"></i>
 							<div class="login-content-main-scan-delta"></div>
 						</div>
+						<div class="login-content-main-left" v-if="getThemeConfig.registration">
+							<template v-if="state.tabsActiveName != 'register'">
+								没有账号? 去<el-link class="login-content-main-left-register" @click="() => state.tabsActiveName = 'register'">注册账号</el-link>
+							</template>
+							<template v-else>
+								已有账户? 去<el-link class="login-content-main-left-register" @click="() => state.tabsActiveName = 'account'">登录账号</el-link>
+							</template>
+						</div>
 					</div>
 				</div>
 			</div>
 		</div>
 		<div class="copyright" :class="[getThemeConfig.icp ? 'mb25' : 'mt5']">{{ getThemeConfig.copyright }}</div>
-		<div v-if="getThemeConfig.icp" class="icp mt5">
+		<div v-if="getThemeConfig.icp" class="icp mt5" onselect="false">
 			<el-link :href="getThemeConfig.icpUrl" target="_blank">{{ getThemeConfig.icp }}</el-link>
 		</div>
 	</div>
 </template>
 
 <script setup lang="ts" name="loginIndex">
-import { defineAsyncComponent, onMounted, reactive, computed } from 'vue';
+import {defineAsyncComponent, onMounted, reactive, computed} from 'vue';
 import { storeToRefs } from 'pinia';
 import { useThemeConfig } from '/@/stores/themeConfig';
 import { NextLoading } from '/@/utils/loading';
@@ -66,6 +77,7 @@ import {SysTenantApi} from "/@/api-services";
 import {useRoute} from "vue-router";
 
 // 引入组件
+const Register = defineAsyncComponent(() => import('/@/views/login/component/register.vue'));
 const Account = defineAsyncComponent(() => import('/@/views/login/component/account.vue'));
 const Mobile = defineAsyncComponent(() => import('/@/views/login/component/mobile.vue'));
 const Scan = defineAsyncComponent(() => import('/@/views/login/component/scan.vue'));
@@ -88,6 +100,8 @@ const getThemeConfig = computed(() => {
 });
 // 页面加载时
 onMounted(async () => {
+	// 地址栏存在wayid参数时,默认切换到注册界面
+	if (route.query.wayid != undefined) state.tabsActiveName = 'register';
 	await getTenantInfo();
 	NextLoading.done();
 });
@@ -239,6 +253,7 @@ const getTenantInfo = async () => {
 					animation: logoAnimation 0.3s ease;
 					animation-delay: 0.3s;
 					color: var(--el-color-primary);
+					user-select: none;
 				}
 				.login-right-warp-main-form {
 					flex: 1;
@@ -278,6 +293,21 @@ const getTenantInfo = async () => {
 							top: 0px;
 						}
 					}
+					.login-content-main-left {
+						position: absolute;
+						top: 10px;
+						left: 10px;
+						width: 150px;
+						height: 50px;
+						overflow: hidden;
+						cursor: pointer;
+						transition: all ease 0.3s;
+						user-select: none;
+						.login-content-main-left-register {
+							top: -1.5px;
+							color: var(--el-color-primary);
+						}
+					}
 				}
 			}
 		}

+ 34 - 7
Web/src/views/system/infoSetting/index.vue

@@ -39,17 +39,27 @@
 				</el-descriptions-item>
 				<el-descriptions-item label="登录二次验证">
 					<el-radio-group v-model="state.formData.sysSecondVer">
-						<el-radio :value="1">启用</el-radio>
-						<el-radio :value="2">禁用</el-radio>
+						<el-radio :value="true">启用</el-radio>
+						<el-radio :value="false">禁用</el-radio>
 					</el-radio-group>
 				</el-descriptions-item>
 				<el-descriptions-item label="图形验证码">
 					<el-radio-group v-model="state.formData.sysCaptcha">
-						<el-radio :value="1">启用</el-radio>
-						<el-radio :value="2">禁用</el-radio>
+						<el-radio :value="true">启用</el-radio>
+						<el-radio :value="false">禁用</el-radio>
 					</el-radio-group>
 				</el-descriptions-item>
-
+				<el-descriptions-item label="用户注册">
+					<el-radio-group v-model="state.formData.sysRegistration">
+						<el-radio :value="true">启用</el-radio>
+						<el-radio :value="false">禁用</el-radio>
+					</el-radio-group>
+				</el-descriptions-item>
+				<el-descriptions-item label="注册方案" v-if="state.formData.sysRegistration">
+					<el-select v-model="state.formData.regWayId" placeholder="注册方案" clearable class="w100">
+						<el-option :label="item.name" :value="item.id" v-for="(item, index) in state.regWayData" :key="index" />
+					</el-select>
+				</el-descriptions-item>
 				<template #extra>
 					<el-button type="primary" icon="ele-SuccessFilled" @click="onSave">保存</el-button>
 				</template>
@@ -59,17 +69,18 @@
 </template>
 
 <script setup lang="ts" name="sysInfoSetting">
-import { nextTick, reactive, ref } from 'vue';
+import {nextTick, onMounted, reactive, ref} from 'vue';
 import { ElMessage, UploadInstance } from 'element-plus';
 import { fileToBase64 } from '/@/utils/base64Conver';
 
 import { getAPI } from '/@/utils/axios-utils';
-import { SysConfigApi } from '/@/api-services';
+import {SysConfigApi, SysUserRegWayApi} from '/@/api-services';
 
 const uploadRef = ref<UploadInstance>();
 const state = reactive({
 	isLoading: false,
 	file: undefined as any,
+	regWayData: [] as Array<any>,
 	formData: {
 		sysLogoBlob: undefined,
 		sysLogo: '',
@@ -81,11 +92,17 @@ const state = reactive({
 		sysCopyright: '',
 		sysIcp: '',
 		sysIcpUrl: '',
+		regWayId: undefined,
+		sysRegistration: undefined,
 		sysSecondVer: undefined,
 		sysCaptcha: undefined,
 	},
 });
 
+onMounted(async () => {
+	state.regWayData = await getAPI(SysUserRegWayApi).apiSysUserRegWayListPost({}).then((res) => res.data.result ?? []);
+});
+
 // 通过onChange方法获得文件列表
 const handleUploadChange = (file: any) => {
 	uploadRef.value!.clearFiles();
@@ -106,6 +123,12 @@ const onSave = async () => {
 
 	try {
 		state.isLoading = true;
+		if (!state.formData.sysRegistration) {
+			state.formData.regWayId = undefined;
+		} else if (!state.formData.regWayId) {
+			ElMessage.error('注册方案不能为空');
+			return;
+		}
 		const res = await getAPI(SysConfigApi).apiSysConfigSaveSysInfoPost({
 			sysLogoBase64: sysLogoBase64,
 			sysLogoFileName: sysLogoFileName,
@@ -115,9 +138,11 @@ const onSave = async () => {
 			sysWatermark: state.formData.sysWatermark,
 			sysCopyright: state.formData.sysCopyright,
 			sysIcp: state.formData.sysIcp,
+			sysRegWayId: state.formData.regWayId,
 			sysIcpUrl: state.formData.sysIcpUrl,
 			sysSecondVer: state.formData.sysSecondVer,
 			sysCaptcha: state.formData.sysCaptcha,
+			sysRegistration: state.formData.sysRegistration,
 		});
 		if (res.data!.type !== 'success') return;
 
@@ -153,6 +178,8 @@ const loadData = async () => {
 			sysIcpUrl: result.sysIcpUrl,
 			sysSecondVer: result.sysSecondVer,
 			sysCaptcha: result.sysCaptcha,
+			regWayId: result.sysRegWayId,
+			sysRegistration: result.sysRegistration,
 		};
 	} finally {
 		nextTick(() => {

+ 28 - 5
Web/src/views/system/tenant/component/editTenant.vue

@@ -145,6 +145,25 @@
 									<el-input v-model="state.ruleForm.watermark" placeholder="应用水印" maxlength="32" clearable />
 								</el-form-item>
 							</el-col>
+							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+								<el-form-item label="启用注册" prop="watermark">
+									<el-switch
+											v-model="state.ruleForm.enableReg"
+											inline-prompt
+											:active-value="1"
+											:inactive-value="2"
+											active-text="开启"
+											inactive-text="关闭"
+									/>
+								</el-form-item>
+							</el-col>
+							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" v-if="state.ruleForm.enableReg == 1">
+								<el-form-item label="注册方案" prop="regWayId" :rules="[{ required: true, message: '职位不能为空', trigger: 'blur' }]">
+									<el-select v-model="state.ruleForm.regWayId" placeholder="注册方案" clearable class="w100">
+										<el-option :label="item.name" :value="item.id" v-for="(item, index) in state.regWayData" :key="index" />
+									</el-select>
+								</el-form-item>
+							</el-col>
 						</el-row>
 					</el-tab-pane>
 				</el-tabs>
@@ -160,12 +179,13 @@
 </template>
 
 <script lang="ts" setup name="sysEditTenant">
-import { reactive, ref } from 'vue';
+import {onMounted, reactive, ref} from 'vue';
 import { getAPI } from '/@/utils/axios-utils';
-import { SysTenantApi } from '/@/api-services/api';
+import {SysTenantApi, SysUserRegWayApi} from '/@/api-services/api';
 import { UpdateTenantInput } from '/@/api-services/models';
 import {UploadInstance} from "element-plus";
 import {fileToBase64} from "/@/utils/base64Conver";
+import GSysDict from "/@/components/sysDict/sysDict.vue";
 
 const props = defineProps({
 	title: String,
@@ -178,13 +198,17 @@ const state = reactive({
 	selectedTabName: '0',
 	isShowDialog: false,
 	file: undefined as any,
+	regWayData: [] as Array<any>,
 	ruleForm: {} as UpdateTenantInput,
 });
 
+onMounted(async () => {
+	state.regWayData = await getAPI(SysUserRegWayApi).apiSysUserRegWayListPost({}).then((res) => res.data.result ?? []);
+});
+
 // 通过onChange方法获得文件列表
 const handleUploadChange = (file: any) => {
 	uploadRef.value!.clearFiles();
-
 	state.file = file;
 	state.ruleForm.logo = URL.createObjectURL(state.file.raw); // 显示预览logo
 };
@@ -214,14 +238,13 @@ const cancel = () => {
 // 提交
 const submit = async () => {
 	// 如果有选择图标,则转换为 base64
-	let sysLogoBase64 = '';
-	let sysLogoFileName = '';
 	if (state.file) {
 		state.ruleForm.logoBase64 = (await fileToBase64(state.file.raw)) as string;
 		state.ruleForm.logoFileName = state.file.raw.name;
 	}
 	ruleFormRef.value.validate(async (valid: boolean) => {
 		if (!valid) return;
+		if (state.ruleForm.enableReg != 1) state.ruleForm.regWayId = undefined;
 		if (state.ruleForm.id != undefined && state.ruleForm.id > 0) {
 			await getAPI(SysTenantApi).apiSysTenantUpdatePost(state.ruleForm);
 		} else {

+ 6 - 0
Web/src/views/system/tenant/index.vue

@@ -87,6 +87,11 @@
 				<el-table-column prop="copyright" label="版权信息" width="350" show-overflow-tooltip />
 				<el-table-column prop="icp" label="备案号" width="130" show-overflow-tooltip />
 				<el-table-column prop="icpUrl" label="icp地址" width="280" show-overflow-tooltip />
+				<el-table-column prop="enableReg" label="启用注册" width="280" show-overflow-tooltip>
+					<template #default="scope">
+						<g-sys-dict v-model="scope.row.enableReg" code="YesNoEnum" />
+					</template>
+				</el-table-column>
 				<el-table-column prop="orderNo" label="排序" width="70" show-overflow-tooltip />
 				<el-table-column label="修改记录" width="100" align="center" show-overflow-tooltip>
 					<template #default="scope">
@@ -144,6 +149,7 @@ import { SysTenantApi } from '/@/api-services/api';
 import { TenantOutput } from '/@/api-services/models';
 import {Local} from "/@/utils/storage";
 import {accessTokenKey, refreshAccessTokenKey} from "/@/utils/request";
+import GSysDict from "/@/components/sysDict/sysDict.vue";
 
 const editTenantRef = ref<InstanceType<typeof EditTenant>>();
 const grantMenuRef = ref<InstanceType<typeof GrantMenu>>();

+ 146 - 0
Web/src/views/system/userRegWay/component/editRegWay.vue

@@ -0,0 +1,146 @@
+<template>
+	<div class="sys-tenant-container">
+		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false" width="700px">
+			<template #header>
+				<div style="color: #fff">
+					<el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Edit /> </el-icon>
+					<span> {{ props.title }} </span>
+				</div>
+			</template>
+			<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto">
+				<el-row :gutter="35">
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="方案名称" prop="name" :rules="[{ required: true, message: '方案名称不能为空', trigger: 'blur' }]">
+							<el-input v-model="state.ruleForm.name" placeholder="方案名称" clearable />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="账户类型" prop="posId" :rules="[{ required: true, message: '账户类型不能为空', trigger: 'blur' }]">
+							<g-sys-dict
+									v-model="state.ruleForm.accountType"
+									:on-item-filter="(data: any) => !['SuperAdmin','SysAdmin'].includes(data.name)"
+									code="AccountTypeEnum"
+									render-as="select"
+							/>
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="绑定角色" prop="roleId" :rules="[{ required: true, message: '角色不能为空', trigger: 'blur' }]">
+							<el-select v-model="state.ruleForm.roleId" placeholder="绑定角色" clearable class="w100">
+								<el-option :label="item.name" :value="item.id" v-for="(item, index) in state.roleData" :key="index" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="绑定机构" prop="orgId" :rules="[{ required: true, message: '机构不能为空', trigger: 'blur' }]">
+							<el-cascader :options="state.orgData" :props="cascaderConfig" v-model="state.ruleForm.orgId" placeholder="绑定机构" clearable filterable class="w100" >
+								<template #default="{ node, data }">
+									<span>{{ data.name }}</span>
+									<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+								</template>
+							</el-cascader>
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="绑定职位" prop="posId" :rules="[{ required: true, message: '职位不能为空', trigger: 'blur' }]">
+							<el-select v-model="state.ruleForm.posId" placeholder="绑定职位" clearable class="w100">
+								<el-option :label="item.name" :value="item.id" v-for="(item, index) in state.posData" :key="index" />
+							</el-select>
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+						<el-form-item label="排序">
+							<el-input-number v-model="state.ruleForm.orderNo" placeholder="排序" class="w100" />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						<el-form-item label="备注">
+							<el-input v-model="state.ruleForm.remark" placeholder="请输入备注内容" clearable type="textarea" />
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="() => state.isShowDialog = false">取 消</el-button>
+					<el-button type="primary" @click="submit">确 定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts" setup name="sysEditTenant">
+import { onMounted, reactive, ref } from 'vue';
+import { getAPI } from '/@/utils/axios-utils';
+import {SysOrgApi, SysPosApi, SysRoleApi, SysUserRegWayApi} from '/@/api-services/api';
+import {AccountTypeEnum, RoleOutput, SysOrg, SysPos, UpdateUserRegWayInput} from '/@/api-services/models';
+
+const props = defineProps({
+	title: String,
+});
+const emits = defineEmits(['handleQuery']);
+const ruleFormRef = ref();
+const state = reactive({
+	loading: false,
+	selectedTabName: '0',
+	isShowDialog: false,
+	file: undefined as any,
+	ruleForm: {} as UpdateUserRegWayInput,
+	orgData: [] as Array<SysOrg>,
+	posData: [] as Array<SysPos>, // 职位数据
+	roleData: [] as Array<RoleOutput>, // 角色数据
+});
+
+onMounted(async () => {
+	state.loading = true;
+	state.posData = await getAPI(SysPosApi).apiSysPosListGet().then(res => res.data.result ?? []);
+	state.roleData = await getAPI(SysRoleApi).apiSysRoleListGet().then(res => res.data.result ?? []);
+	state.orgData = await getAPI(SysOrgApi).apiSysOrgListGet(0).then(res => res.data.result ?? []);
+	state.loading = false;
+});
+
+// 级联选择器配置选项
+const cascaderConfig = {
+	checkStrictly: true,
+	emitPath: false,
+	value: 'id',
+	label: 'name',
+	expandTrigger: 'hover'
+};
+
+// 打开弹窗
+const openDialog = async (row: any) => {
+	state.ruleForm = JSON.parse(JSON.stringify(row));
+	if (!state.ruleForm?.id) {
+		state.ruleForm.accountType = AccountTypeEnum.NUMBER_666;
+		state.ruleForm.roleId = <number>state.roleData[0].id;
+		state.ruleForm.orgId = <number>state.orgData[0].id;
+		state.ruleForm.posId = <number>state.posData[0].id;
+	}
+	state.isShowDialog = true;
+	ruleFormRef.value?.resetFields();
+};
+
+// 关闭弹窗
+const closeDialog = () => {
+	emits('handleQuery');
+	state.isShowDialog = false;
+};
+
+// 提交
+const submit = async () => {
+	ruleFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return;
+		if (state.ruleForm.id) {
+			await getAPI(SysUserRegWayApi).apiSysUserRegWayUpdatePost(state.ruleForm);
+		} else {
+			await getAPI(SysUserRegWayApi).apiSysUserRegWayAddPost(state.ruleForm);
+		}
+		closeDialog();
+	});
+};
+
+// 导出对象
+defineExpose({ openDialog });
+</script>

+ 127 - 0
Web/src/views/system/userRegWay/index.vue

@@ -0,0 +1,127 @@
+<template>
+	<div class="sys-user-reg-way-container">
+		<el-card shadow="hover" :body-style="{ paddingBottom: '0' }" v-auth="'sysUserRegWay:list'">
+			<el-form :model="state.queryParams" ref="queryForm" :inline="true">
+				<el-form-item label="租户" v-if="userStore.userInfos.accountType == 999">
+					<el-select v-model="state.queryParams.tenantId" placeholder="租户" style="width: 100%">
+						<el-option :value="item.value" :label="`${item.label} (${item.host})`" v-for="(item, index) in state.tenantList" :key="index" />
+					</el-select>
+				</el-form-item>
+				<el-form-item label="关键字">
+					<el-input v-model="state.queryParams.keyword" placeholder="关键字" clearable />
+				</el-form-item>
+				<el-form-item label="名称">
+					<el-input v-model="state.queryParams.name" placeholder="名称" clearable />
+				</el-form-item>
+				<el-form-item>
+					<el-button-group>
+						<el-button type="primary" icon="ele-Search" @click="handleQuery"> 查询
+						</el-button>
+						<el-button icon="ele-Refresh" @click="resetQuery"> 重置 </el-button>
+					</el-button-group>
+				</el-form-item>
+				<el-form-item>
+					<el-button type="primary" icon="ele-Plus" @click="openAddRegWay" v-auth="'sysUserRegWay:add'"> 新增
+					</el-button>
+				</el-form-item>
+			</el-form>
+		</el-card>
+
+		<el-card class="full-table" shadow="hover" style="margin-top: 5px">
+			<el-table :data="state.regWayData" style="width: 100%" v-loading="state.loading" border>
+				<el-table-column type="index" label="序号" width="55" align="center" fixed />
+				<el-table-column prop="name" label="名称" align="center" show-overflow-tooltip />
+				<el-table-column prop="orgName" label="机构" align="center" show-overflow-tooltip />
+				<el-table-column prop="roleName" label="角色" align="center" show-overflow-tooltip />
+				<el-table-column prop="posName" label="职位" align="center" show-overflow-tooltip />
+				<el-table-column prop="orderNo" label="排序" width="70" show-overflow-tooltip />
+				<el-table-column label="修改记录" width="100" align="center" show-overflow-tooltip>
+					<template #default="scope">
+						<ModifyRecord :data="scope.row" />
+					</template>
+				</el-table-column>
+				<el-table-column label="操作" width="200" fixed="right" align="center" show-overflow-tooltip v-if="auths(['sysUserRegWay:update', 'sysUserRegWay:delete'])">
+					<template #default="scope">
+						<el-button icon="ele-Edit" size="small" text type="primary" @click="openEditRegWay(scope.row)" v-auth="'sysUserRegWay:update'"> 编辑 </el-button>
+						<el-button icon="ele-Delete" size="small" text type="danger" @click="delRegWay(scope.row)" v-auth="'sysUserRegWay:delete'"> 删除 </el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+		</el-card>
+		<EditRegWay ref="editRegWayRef" :title="state.editRegWayTitle" @handleQuery="handleQuery" />
+	</div>
+</template>
+
+<script lang="ts" setup name="sysUserRegWay">
+import { onMounted, reactive, ref } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import { getAPI } from '/@/utils/axios-utils';
+import { UserRegWayOutput } from '/@/api-services/models';
+import { SysTenantApi, SysUserRegWayApi} from '/@/api-services/api';
+import { auths } from "/@/utils/authFunction";
+import { useUserInfo } from "/@/stores/userInfo";
+import EditRegWay from './component/editRegWay.vue';
+import ModifyRecord from '/@/components/table/modifyRecord.vue';
+
+const userStore = useUserInfo();
+const editRegWayRef = ref<InstanceType<typeof EditRegWay>>();
+const state = reactive({
+	loading: false,
+	tenantList: [] as Array<any>,
+	regWayData: [] as Array<UserRegWayOutput>,
+	queryParams: {
+		name: undefined,
+		keyword: undefined,
+		tenantId: undefined,
+	},
+	editRegWayTitle: '',
+});
+
+onMounted(async () => {
+	if (userStore.userInfos.accountType == 999) {
+		state.tenantList = await getAPI(SysTenantApi).apiSysTenantListGet().then(res => res.data.result ?? []);
+		state.queryParams.tenantId = state.tenantList[0].value;
+	}
+	handleQuery();
+});
+
+// 查询操作
+const handleQuery = async () => {
+	state.loading = true;
+	state.regWayData = await getAPI(SysUserRegWayApi).apiSysUserRegWayListPost(state.queryParams).then(res => res.data.result ?? []);
+	state.loading = false;
+};
+
+// 重置操作
+const resetQuery = () => {
+	state.queryParams.name = undefined;
+	state.queryParams.keyword = undefined;
+	state.queryParams.tenantId = undefined;
+	handleQuery();
+};
+
+// 打开新增页面
+const openAddRegWay = () => {
+	state.editRegWayTitle = '添加注册方案';
+	editRegWayRef.value?.openDialog({ tenantId: state.queryParams.tenantId, orderNo: 100 });
+};
+
+// 打开编辑页面
+const openEditRegWay = (row: any) => {
+	state.editRegWayTitle = '编辑注册方案';
+	editRegWayRef.value?.openDialog(row);
+};
+
+// 删除
+const delRegWay = (row: any) => {
+	ElMessageBox.confirm(`确定删除方案:【${row.name}】?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	}).then(async () => {
+			await getAPI(SysUserRegWayApi).apiSysUserRegWayDeletePost({ id: row.id });
+			handleQuery();
+			ElMessage.success('删除成功');
+	}).catch(() => { });
+};
+</script>