Explorar el Código

chore: 新增多应用管理

喵你个旺呀 hace 1 año
padre
commit
f1179ac4d2
Se han modificado 51 ficheros con 3000 adiciones y 462 borrados
  1. 25 0
      Admin.NET/Admin.NET.Application/SeedData/SysAppSeedData.cs
  2. 5 0
      Admin.NET/Admin.NET.Core/Const/ClaimConst.cs
  3. 5 0
      Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs
  4. 82 0
      Admin.NET/Admin.NET.Core/Entity/SysApp.cs
  5. 41 0
      Admin.NET/Admin.NET.Core/Entity/SysAppMenu.cs
  6. 1 1
      Admin.NET/Admin.NET.Core/Entity/SysMenu.cs
  7. 14 0
      Admin.NET/Admin.NET.Core/Entity/SysTenant.cs
  8. 18 0
      Admin.NET/Admin.NET.Core/Enum/ErrorCodeEnum.cs
  9. 23 0
      Admin.NET/Admin.NET.Core/SeedData/SysAppMenuSeedData.cs
  10. 25 0
      Admin.NET/Admin.NET.Core/SeedData/SysAppSeedData.cs
  11. 8 0
      Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs
  12. 1 1
      Admin.NET/Admin.NET.Core/SeedData/SysTenantSeedData.cs
  13. 173 0
      Admin.NET/Admin.NET.Core/Service/Appliication/Dto/SysAppInput.cs
  14. 103 0
      Admin.NET/Admin.NET.Core/Service/Appliication/Dto/SysAppOutput.cs
  15. 165 0
      Admin.NET/Admin.NET.Core/Service/Appliication/SysAppService.cs
  16. 411 409
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  17. 10 0
      Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs
  18. 30 20
      Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs
  19. 1 1
      Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs
  20. 6 0
      Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs
  21. 5 0
      Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs
  22. 5 1
      Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs
  23. 5 0
      Admin.NET/Admin.NET.Core/Service/User/UserManager.cs
  24. 1 0
      Web/src/api-services/api.ts
  25. 720 0
      Web/src/api-services/apis/sys-app-api.ts
  26. 23 7
      Web/src/api-services/apis/sys-menu-api.ts
  27. 80 0
      Web/src/api-services/models/add-sys-app-input.ts
  28. 12 0
      Web/src/api-services/models/add-tenant-input.ts
  29. 57 0
      Web/src/api-services/models/admin-result-sql-sugar-paged-list-sys-app-output.ts
  30. 26 0
      Web/src/api-services/models/base-id-input.ts
  31. 70 0
      Web/src/api-services/models/base-page-input.ts
  32. 32 0
      Web/src/api-services/models/change-app-input.ts
  33. 9 0
      Web/src/api-services/models/index.ts
  34. 6 6
      Web/src/api-services/models/login-input.ts
  35. 1 1
      Web/src/api-services/models/login-phone-input.ts
  36. 63 0
      Web/src/api-services/models/sql-sugar-paged-list-sys-app-output.ts
  37. 128 0
      Web/src/api-services/models/sys-app-output.ts
  38. 13 1
      Web/src/api-services/models/tenant-output.ts
  39. 32 0
      Web/src/api-services/models/update-app-menu-input.ts
  40. 86 0
      Web/src/api-services/models/update-sys-app-input.ts
  41. 12 0
      Web/src/api-services/models/update-tenant-input.ts
  42. 1 0
      Web/src/i18n/lang/zh-cn.ts
  43. 1 0
      Web/src/i18n/lang/zh-tw.ts
  44. 86 0
      Web/src/layout/navBars/topBar/changeApp.vue
  45. 8 2
      Web/src/layout/navBars/topBar/user.vue
  46. 2 2
      Web/src/utils/request.ts
  47. 207 0
      Web/src/views/system/app/component/editApp.vue
  48. 147 0
      Web/src/views/system/app/index.vue
  49. 11 2
      Web/src/views/system/tenant/component/editTenant.vue
  50. 3 8
      Web/src/views/system/tenant/component/grantMenu.vue
  51. 1 0
      Web/src/views/system/tenant/index.vue

+ 25 - 0
Admin.NET/Admin.NET.Application/SeedData/SysAppSeedData.cs

@@ -0,0 +1,25 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Application;
+
+/// <summary>
+/// 系统应用表种子数据
+/// </summary>
+public class SysAppSeedData : ISqlSugarEntitySeedData<SysApp>
+{
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<SysApp> HasData()
+    {
+        return new[]
+        {
+            new SysApp{ Id=1310000000001, Name=ApplicationConst.GroupName, Logo="/upload/logo.png", Title="Admin.App", ViceTitle="Admin.App", ViceDesc="站在巨人肩膀上的 .NET 通用权限开发框架", Watermark="Admin.App", Copyright="Copyright \u00a9 2021-present Admin.NET All rights reserved.", Icp="省ICP备12345678号", Remark=ApplicationConst.GroupName, CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
+        };
+    }
+}

+ 5 - 0
Admin.NET/Admin.NET.Core/Const/ClaimConst.cs

@@ -11,6 +11,11 @@ namespace Admin.NET.Core;
 /// </summary>
 public class ClaimConst
 {
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    public const string AppId = "AppId";
+    
     /// <summary>
     /// 用户Id
     /// </summary>

+ 5 - 0
Admin.NET/Admin.NET.Core/Const/SqlSugarConst.cs

@@ -25,6 +25,11 @@ public class SqlSugarConst
     /// 默认表主键
     /// </summary>
     public const string PrimaryKey = "Id";
+    
+    /// <summary>
+    /// 默认租户Id
+    /// </summary>
+    public const long DefaultAppId = 1300000000001;
 
     /// <summary>
     /// 默认租户Id

+ 82 - 0
Admin.NET/Admin.NET.Core/Entity/SysApp.cs

@@ -0,0 +1,82 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统应用表
+/// </summary>
+[SysTable]
+[SugarTable(null, "系统应用表")]
+public partial class SysApp : EntityBase
+{
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [SugarColumn(ColumnDescription = "名称", Length = 32), Required, MaxLength(32)]
+    public virtual string Name { get; set; }
+    
+    /// <summary>
+    /// 图标
+    /// </summary>
+    [SugarColumn(ColumnDescription = "图标", Length = 256), Required, MaxLength(256)]
+    public virtual string? Logo { get; set; }
+    
+    /// <summary>
+    /// 标题
+    /// </summary>
+    [SugarColumn(ColumnDescription = "标题", Length = 32), MaxLength(32)]
+    public string Title { get; set; }
+    
+    /// <summary>
+    /// 副标题
+    /// </summary>
+    [SugarColumn(ColumnDescription = "副标题", Length = 32), MaxLength(32)]
+    public string ViceTitle { get; set; }
+    
+    /// <summary>
+    /// 副描述
+    /// </summary>
+    [SugarColumn(ColumnDescription = "副描述", Length = 64), MaxLength(64)]
+    public string? ViceDesc { get; set; }
+   
+    /// <summary>
+    /// 水印
+    /// </summary>
+    [SugarColumn(ColumnDescription = "水印", Length = 32), MaxLength(32)]
+    public string? Watermark { get; set; }
+    
+    /// <summary>
+    /// 版权信息
+    /// </summary>
+    [SugarColumn(ColumnDescription = "版权信息", Length = 64), MaxLength(64)]
+    public string? Copyright { get; set; }
+    
+    /// <summary>
+    /// ICP备案号
+    /// </summary>
+    [SugarColumn(ColumnDescription = "ICP备案号", Length = 32), MaxLength(32)]
+    public string? Icp { get; set; }
+    
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [IgnoreUpdateSeedColumn]
+    [SugarColumn(ColumnDescription = "排序")]
+    public int OrderNo { get; set; } = 100;
+
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [SugarColumn(ColumnDescription = "备注", Length = 256), MaxLength(256)]
+    public string? Remark { get; set; }
+    
+    /// <summary>
+    /// 应用租户
+    /// </summary>
+    [Navigate(NavigateType.OneToMany, nameof(SysTenant.AppId))]
+    public List<SysTenant> TenantList { get; set; }
+}

+ 41 - 0
Admin.NET/Admin.NET.Core/Entity/SysAppMenu.cs

@@ -0,0 +1,41 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统应用菜单表
+/// </summary>
+[SugarTable(null, "系统应用菜单表")]
+[SysTable]
+public partial class SysAppMenu : EntityBaseId
+{
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "应用Id")]
+    public long AppId { get; set; }
+
+    /// <summary>
+    /// 应用
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    [Navigate(NavigateType.OneToOne, nameof(AppId))]
+    public SysApp SysApp { get; set; }
+
+    /// <summary>
+    /// 菜单Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "菜单Id")]
+    public long MenuId { get; set; }
+
+    /// <summary>
+    /// 菜单
+    /// </summary>
+    [Navigate(NavigateType.OneToOne, nameof(MenuId))]
+    public SysMenu SysMenu { get; set; }
+}

+ 1 - 1
Admin.NET/Admin.NET.Core/Entity/SysMenu.cs

@@ -130,5 +130,5 @@ public partial class SysMenu : EntityBase
     /// 菜单子项
     /// </summary>
     [SugarColumn(IsIgnore = true)]
-    public List<SysMenu> Children { get; set; } = new List<SysMenu>();
+    public List<SysMenu> Children { get; set; } = new ();
 }

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

@@ -13,6 +13,12 @@ namespace Admin.NET.Core;
 [SysTable]
 public partial class SysTenant : EntityBase
 {
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    [SugarColumn(ColumnDescription = "应用Id")]
+    public long AppId { get; set; }
+    
     /// <summary>
     /// 用户Id
     /// </summary>
@@ -82,4 +88,12 @@ public partial class SysTenant : EntityBase
     /// </summary>
     [SugarColumn(ColumnDescription = "状态")]
     public StatusEnum Status { get; set; } = StatusEnum.Enable;
+    
+    /// <summary>
+    /// 应用
+    /// </summary>
+    [Newtonsoft.Json.JsonIgnore]
+    [System.Text.Json.Serialization.JsonIgnore]
+    [Navigate(NavigateType.OneToOne, nameof(AppId))]
+    public SysApp SysApp { get; set; }
 }

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

@@ -654,6 +654,24 @@ public enum ErrorCodeEnum
     /// </summary>
     [ErrorCodeItemMetadata("已存在同名功能或同名程序及插件")]
     D1900,
+    
+    /// <summary>
+    /// 禁止删除存在关联租户的应用
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止删除存在关联租户的应用")]
+    A1001,
+    
+    /// <summary>
+    /// 禁止删除存在关联菜单的应用
+    /// </summary>
+    [ErrorCodeItemMetadata("禁止删除存在关联菜单的应用")]
+    A1002,
+    
+    /// <summary>
+    /// 找不到系统应用
+    /// </summary>
+    [ErrorCodeItemMetadata("找不到系统应用")]
+    A1000,
 
     /// <summary>
     /// 已存在同名或同编码项目

+ 23 - 0
Admin.NET/Admin.NET.Core/SeedData/SysAppMenuSeedData.cs

@@ -0,0 +1,23 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统应用菜单表种子数据
+/// </summary>
+public class SysAppMenuSeedData : ISqlSugarEntitySeedData<SysAppMenu>
+{
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<SysAppMenu> HasData()
+    {
+        long id = 1300000000001;
+        return new SysMenuSeedData().HasData().Select(m => new SysAppMenu { Id=id++, AppId=SqlSugarConst.DefaultAppId, MenuId=m.Id });
+    }
+}

+ 25 - 0
Admin.NET/Admin.NET.Core/SeedData/SysAppSeedData.cs

@@ -0,0 +1,25 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 系统应用表种子数据
+/// </summary>
+public class SysAppSeedData : ISqlSugarEntitySeedData<SysApp>
+{
+    /// <summary>
+    /// 种子数据
+    /// </summary>
+    /// <returns></returns>
+    public IEnumerable<SysApp> HasData()
+    {
+        return new[]
+        {
+            new SysApp{ Id=SqlSugarConst.DefaultAppId, Name="默认应用", Logo="/upload/logo.png", Title="Admin.NET", ViceTitle="Admin.NET", ViceDesc="站在巨人肩膀上的 .NET 通用权限开发框架", Watermark="Admin.NET", Copyright="Copyright \u00a9 2021-present Admin.NET All rights reserved.", Icp="省ICP备12345678号", Remark="系统默认应用", CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
+        };
+    }
+}

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

@@ -91,6 +91,14 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
 
             new SysMenu{ Id=1310000000301, Pid=0, Title="平台管理", Path="/platform", Name="platform", Component="Layout", Icon="ele-Menu", Type=MenuTypeEnum.Dir, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=11000 },
 
+            new SysMenu{ Id=1310000001311, Pid=1310000000301, Title="应用管理", Path="/platform/app", Name="sysApp", Component="/system/app/index", Icon="ele-School", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=90 },
+            new SysMenu{ Id=1310000001312, Pid=1310000001311, Title="查询", Permission="sysApp:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001313, Pid=1310000001311, Title="编辑", Permission="sysApp:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001314, Pid=1310000001311, Title="增加", Permission="sysApp:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001315, Pid=1310000001311, Title="删除", Permission="sysApp:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001316, Pid=1310000001311, Title="授权菜单", Permission="sysApp:grantMenu", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1310000001317, Pid=1310000001311, Title="切换应用", Permission="sysApp:changeApp", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            
             new SysMenu{ Id=1310000000311, Pid=1310000000301, Title="租户管理", Path="/platform/tenant", Name="sysTenant", Component="/system/tenant/index", Icon="ele-School", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000312, Pid=1310000000311, Title="查询", Permission="sysTenant:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1310000000313, Pid=1310000000311, Title="编辑", Permission="sysTenant:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },

+ 1 - 1
Admin.NET/Admin.NET.Core/SeedData/SysTenantSeedData.cs

@@ -21,7 +21,7 @@ public class SysTenantSeedData : ISqlSugarEntitySeedData<SysTenant>
 
         return new[]
         {
-            new SysTenant{ Id=SqlSugarConst.DefaultTenantId, OrgId=SqlSugarConst.DefaultTenantId, UserId=1300000000111, Host=SqlSugarConst.DefaultTenantHost, TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
+            new SysTenant{ Id=SqlSugarConst.DefaultTenantId, AppId=SqlSugarConst.DefaultAppId, OrgId=SqlSugarConst.DefaultTenantId, UserId=1300000000111, Host=SqlSugarConst.DefaultTenantHost, TenantType=TenantTypeEnum.Id, DbType=defaultDbConfig.DbType, Connection=defaultDbConfig.ConnectionString, ConfigId=SqlSugarConst.MainConfigId, Remark="系统默认", CreateTime=DateTime.Parse("2022-02-10 00:00:00") },
         };
     }
 }

+ 173 - 0
Admin.NET/Admin.NET.Core/Service/Appliication/Dto/SysAppInput.cs

@@ -0,0 +1,173 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 应用基础输入参数
+/// </summary>
+public class SysAppBaseInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    public virtual long? Id { get; set; }
+    
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [Required(ErrorMessage = "名称不能为空")]
+    public virtual string Name { get; set; }
+    
+    /// <summary>
+    /// 标题
+    /// </summary>
+    [Required(ErrorMessage = "标题不能为空")]
+    public virtual string Title { get; set; }
+    
+    /// <summary>
+    /// 副标题
+    /// </summary>
+    [Required(ErrorMessage = "副标题不能为空")]
+    public virtual string ViceTitle { get; set; }
+    
+    /// <summary>
+    /// 副描述
+    /// </summary>
+    public virtual string? ViceDesc { get; set; }
+    
+    /// <summary>
+    /// 水印
+    /// </summary>
+    public virtual string? Watermark { get; set; }
+    
+    /// <summary>
+    /// 版权信息
+    /// </summary>
+    public virtual string? Copyright { get; set; }
+    
+    /// <summary>
+    /// ICP备案号
+    /// </summary>
+    public virtual string? Icp { get; set; }
+    
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [Required(ErrorMessage = "排序不能为空")]
+    public virtual int? OrderNo { get; set; }
+    
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public virtual string? Remark { get; set; }
+    
+    /// <summary>
+    /// 图标
+    /// </summary>
+    public virtual string? Logo { get; set; }
+}
+
+/// <summary>
+/// 应用增加输入参数
+/// </summary>
+public class AddSysAppInput
+{
+    /// <summary>
+    /// 图标
+    /// </summary>
+    [Required(ErrorMessage = "Logo不能为空")]
+    public string Logo { get; set; }
+    
+    /// <summary>
+    /// 名称
+    /// </summary>
+    [Required(ErrorMessage = "名称不能为空")]
+    public string Name { get; set; }
+    
+    /// <summary>
+    /// 标题
+    /// </summary>
+    [Required(ErrorMessage = "标题不能为空")]
+    public string Title { get; set; }
+    
+    /// <summary>
+    /// 副标题
+    /// </summary>
+    [Required(ErrorMessage = "副标题不能为空")]
+    public string ViceTitle { get; set; }
+    
+    /// <summary>
+    /// 副描述
+    /// </summary>
+    [Required(ErrorMessage = "副描述不能为空")]
+    public string ViceDesc { get; set; }
+    
+    /// <summary>
+    /// 水印
+    /// </summary>
+    [Required(ErrorMessage = "水印不能为空")]
+    public string Watermark { get; set; }
+    
+    /// <summary>
+    /// 版权信息
+    /// </summary>
+    [Required(ErrorMessage = "版权信息不能为空")]
+    public string Copyright { get; set; }
+    
+    /// <summary>
+    /// ICP备案号
+    /// </summary>
+    [Required(ErrorMessage = "备案号不能为空")]
+    public string Icp { get; set; }
+
+    /// <summary>
+    /// 排序
+    /// </summary>
+    [Required(ErrorMessage = "排序不能为空")]
+    public int OrderNo { get; set; } = 100;
+    
+    /// <summary>
+    /// 备注
+    /// </summary>
+    [MaxLength(256, ErrorMessage = "备注字符长度不能超过256")]
+    public string Remark { get; set; }
+}
+
+/// <summary>
+/// 应用更新输入参数
+/// </summary>
+public class UpdateSysAppInput : AddSysAppInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>    
+    [Required(ErrorMessage = "主键Id不能为空")]
+    public long Id { get; set; }
+}
+
+/// <summary>
+/// 授权应用菜单
+/// </summary>
+public class UpdateAppMenuInput : BaseIdInput
+{
+    /// <summary>
+    /// 菜单Id集合
+    /// </summary>
+    public List<long> MenuIdList { get; set; }
+}
+
+/// <summary>
+/// 租户iId
+/// </summary>
+public class ChangeAppInput : BaseIdInput
+{
+    /// <summary>
+    /// 租户Id
+    /// </summary>
+    [Required(ErrorMessage = "租户不能为空")]
+    public long TenantId { get; set; }
+}

+ 103 - 0
Admin.NET/Admin.NET.Core/Service/Appliication/Dto/SysAppOutput.cs

@@ -0,0 +1,103 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 应用输出参数
+/// </summary>
+public class SysAppOutput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    public long Id { get; set; }    
+    
+    /// <summary>
+    /// 名称
+    /// </summary>
+    public string Name { get; set; }
+    
+    /// <summary>
+    /// 图标
+    /// </summary>
+    public string? Logo { get; set; }
+    
+    /// <summary>
+    /// 标题
+    /// </summary>
+    public string Title { get; set; }
+    
+    /// <summary>
+    /// 副标题
+    /// </summary>
+    public string ViceTitle { get; set; }    
+    
+    /// <summary>
+    /// 副描述
+    /// </summary>
+    public string? ViceDesc { get; set; }    
+    
+    /// <summary>
+    /// 水印
+    /// </summary>
+    public string? Watermark { get; set; }    
+    
+    /// <summary>
+    /// 版权信息
+    /// </summary>
+    public string? Copyright { get; set; }    
+    
+    /// <summary>
+    /// ICP备案号
+    /// </summary>
+    public string? Icp { get; set; }    
+    
+    /// <summary>
+    /// 排序
+    /// </summary>
+    public int OrderNo { get; set; }    
+    
+    /// <summary>
+    /// 备注
+    /// </summary>
+    public string? Remark { get; set; }    
+    
+    /// <summary>
+    /// 创建时间
+    /// </summary>
+    public DateTime? CreateTime { get; set; }    
+    
+    /// <summary>
+    /// 更新时间
+    /// </summary>
+    public DateTime? UpdateTime { get; set; }    
+    
+    /// <summary>
+    /// 创建者Id
+    /// </summary>
+    public long? CreateUserId { get; set; }    
+    
+    /// <summary>
+    /// 创建者姓名
+    /// </summary>
+    public string? CreateUserName { get; set; }    
+    
+    /// <summary>
+    /// 修改者Id
+    /// </summary>
+    public long? UpdateUserId { get; set; }    
+    
+    /// <summary>
+    /// 修改者姓名
+    /// </summary>
+    public string? UpdateUserName { get; set; }    
+    
+    /// <summary>
+    /// 软删除
+    /// </summary>
+    public bool IsDelete { get; set; }
+}

+ 165 - 0
Admin.NET/Admin.NET.Core/Service/Appliication/SysAppService.cs

@@ -0,0 +1,165 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+// 
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+// 
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统应用服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Name = "SysApp", Order = 495)]
+public class SysAppService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<SysApp> _sysAppRep;
+    private readonly UserManager _userManager;
+
+    public SysAppService(SqlSugarRepository<SysApp> sysAppRep, UserManager userManager)
+    {
+        _sysAppRep = sysAppRep;
+        _userManager = userManager;
+    }
+
+    /// <summary>
+    /// 分页查询应用 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("分页查询应用")]
+    [ApiDescriptionSettings(Name = "Page"), HttpPost]
+    public async Task<SqlSugarPagedList<SysAppOutput>> Page(BasePageInput input)
+    {
+        input.Keyword = input.Keyword?.Trim();
+        var query = _sysAppRep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.Name.Contains(input.Keyword) || 
+               u.Title.Contains(input.Keyword) || u.ViceTitle.Contains(input.Keyword) || 
+               u.ViceDesc.Contains(input.Keyword) || u.Remark.Contains(input.Keyword))
+            .OrderBy(u => new { u.OrderNo, u.Id })
+            .Select<SysAppOutput>();
+		return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
+    }
+
+    /// <summary>
+    /// 增加应用 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("增加应用")]
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    public async Task<long> Add(AddSysAppInput input)
+    {
+        var entity = input.Adapt<SysApp>();
+        return await _sysAppRep.InsertAsync(entity) ? entity.Id : 0;
+    }
+
+    /// <summary>
+    /// 更新应用 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("更新应用")]
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    public async Task Update(UpdateSysAppInput input)
+    {
+        _ = await _sysAppRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        
+        var entity = input.Adapt<SysApp>();
+        await _sysAppRep.AsUpdateable(entity).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 删除应用 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("删除应用")]
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    public async Task Delete(BaseIdInput input)
+    {
+        var entity = await _sysAppRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        
+        // 禁止删除存在关联租户的应用
+        if (await _sysAppRep.Context.Queryable<SysTenant>().AnyAsync(u => u.AppId == input.Id)) throw Oops.Oh(ErrorCodeEnum.A1001);
+        
+        // 禁止删除存在关联菜单的应用
+        if (await _sysAppRep.Context.Queryable<SysAppMenu>().AnyAsync(u => u.AppId == input.Id)) throw Oops.Oh(ErrorCodeEnum.A1002);
+        
+        await _sysAppRep.DeleteAsync(entity);
+    }
+    
+    /// <summary>
+    /// 获取授权菜单 🔖
+    /// </summary>
+    /// <param name="id"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("获取授权菜单")]
+    [ApiDescriptionSettings(Name = "GrantMenu"), HttpGet]
+    public async Task<List<long>> GrantMenu([FromQuery]long id)
+    {
+         return await _sysAppRep.ChangeRepository<SqlSugarRepository<SysAppMenu>>().AsQueryable().Where(u => u.AppId == id).Select(u => u.MenuId).ToListAsync();
+    }
+    
+    /// <summary>
+    /// 授权菜单 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("授权菜单")]
+    [ApiDescriptionSettings(Name = "GrantMenu"), HttpPost]
+    public async Task GrantMenu(UpdateAppMenuInput input)
+    {
+        input.MenuIdList ??= new();
+        var rep = _sysAppRep.ChangeRepository<SqlSugarRepository<SysAppMenu>>();
+        
+        await rep.DeleteAsync(u => u.AppId == input.Id);
+        
+        var list = input.MenuIdList.Select(id => new SysAppMenu { AppId = input.Id, MenuId = id }).ToList();
+        await rep.InsertRangeAsync(list);
+    }
+    
+    /// <summary>
+    /// 获取切换应用数据 🔖
+    /// </summary>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("切换应用")]
+    [ApiDescriptionSettings(Name = "ChangeApp"), HttpGet]
+    public async Task<dynamic> GetChangeAppData()
+    {
+        var list = await _sysAppRep.AsQueryable().Includes(u => u.TenantList).ToListAsync();
+        return list.Where(u => u.TenantList.Count > 0).Select(u => new
+        {
+            u.Id,
+            Value = u.Id,
+            Label = u.Name,
+            Children = u.TenantList.Select(t => new
+            {
+                t.Id,
+                Value = t.Id,
+                Label = t.Host ?? (t.Id + ""),
+            })
+        });
+    }
+    
+    /// <summary>
+    /// 切换应用 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [UnitOfWork]
+    [DisplayName("切换应用")]
+    [ApiDescriptionSettings(Name = "ChangeApp"), HttpPost]
+    public async Task<LoginOutput> ChangeApp(ChangeAppInput input)
+    {
+        _ = await _sysAppRep.Context.Queryable<SysTenant>().FirstAsync(u => u.Id == input.TenantId) ?? throw Oops.Oh(ErrorCodeEnum.Z1003);
+        _ = await _sysAppRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        
+        var user = await _sysAppRep.Context.Queryable<SysUser>().FirstAsync(u => u.Id == _userManager.UserId);
+        user.TenantId = input.TenantId;
+        
+        return await App.GetRequiredService<SysAuthService>().CreateToken(user, input.Id);
+    }
+}

+ 411 - 409
Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs

@@ -1,410 +1,412 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using Furion.SpecificationDocument;
-using Lazy.Captcha.Core;
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 系统登录授权服务 🧩
-/// </summary>
-[ApiDescriptionSettings(Order = 500)]
-public class SysAuthService : IDynamicApiController, ITransient
-{
-    private readonly UserManager _userManager;
-    private readonly SqlSugarRepository<SysUser> _sysUserRep;
-    private readonly IHttpContextAccessor _httpContextAccessor;
-    private readonly SysMenuService _sysMenuService;
-    private readonly SysOnlineUserService _sysOnlineUserService;
-    private readonly SysConfigService _sysConfigService;
-    private readonly ICaptcha _captcha;
-    private readonly SysCacheService _sysCacheService;
-
-    public SysAuthService(UserManager userManager,
-        SqlSugarRepository<SysUser> sysUserRep,
-        IHttpContextAccessor httpContextAccessor,
-        SysMenuService sysMenuService,
-        SysOnlineUserService sysOnlineUserService,
-        SysConfigService sysConfigService,
-        ICaptcha captcha,
-        SysCacheService sysCacheService)
-    {
-        _userManager = userManager;
-        _sysUserRep = sysUserRep;
-        _httpContextAccessor = httpContextAccessor;
-        _sysMenuService = sysMenuService;
-        _sysOnlineUserService = sysOnlineUserService;
-        _sysConfigService = sysConfigService;
-        _captcha = captcha;
-        _sysCacheService = sysCacheService;
-    }
-
-    /// <summary>
-    /// 账号密码登录 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <remarks>用户名/密码:superadmin/123456</remarks>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [DisplayName("账号密码登录")]
-    public virtual async Task<LoginOutput> Login([Required] LoginInput input)
-    {
-        //// 可以根据域名获取具体租户
-        //var host = _httpContextAccessor.HttpContext.Request.Host;
-
-        // 判断密码错误次数(缓存30分钟)
-        var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}";
-        var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
-        var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue<int>(ConfigConst.SysPasswordMaxErrorTimes);
-        // 若未配置或误配置为0、负数, 则默认密码错误次数最大为10次
-        if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 10;
-        if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027);
-
-        // 判断是否开启验证码,其校验验证码
-        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysCaptcha) && !_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008);
-        
-        // 获取登录租户和用户
-        var (tenant, user) = await GetLoginUserAndTenant(input.Host, account: input.Account);
-
-        // 账号是否被冻结
-        if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017);
-
-        // 是否开启域登录验证
-        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
-        {
-            var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id);
-            if (userLdap == null)
-            {
-                VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
-            }
-            else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(tenant.Id, userLdap.Account, CryptogramUtil.Decrypt(input.Password)))
-            {
-                _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
-                throw Oops.Oh(ErrorCodeEnum.D1000);
-            }
-        }
-        else
-            VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
-
-        // 登录成功则清空密码错误次数
-        _sysCacheService.Remove(keyPasswordErrorTimes);
-
-        return await CreateToken(user);
-    }
-    
-    /// <summary>
-    /// 获取登录租户和用户
-    /// </summary>
-    /// <param name="host"></param>
-    /// <param name="account"></param>
-    /// <param name="phone"></param>
-    /// <returns></returns>
-    [NonAction]
-    public async Task<(SysTenant tenant, SysUser user)> GetLoginUserAndTenant(string host, string account=null, string phone=null)
-    {
-        // 是否租户隔离登录验证
-        var isTenantHostLogin = await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysTenantHostLogin);
-        
-        SysUser user;
-        SysTenant tenant;
-        
-        // 租户隔离登录
-        if (isTenantHostLogin)
-        {
-            // 若租户域名为空或为本地域名,则取默认租户域名
-            if (string.IsNullOrWhiteSpace(host) || host.StartsWith("localhost")) host = SqlSugarConst.DefaultTenantHost;
-            
-            // 租户是否存在或已禁用
-            tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Host == host.ToLower());
-            if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
-
-            // 根据入参类型、租户查询登录用户
-            user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).ClearFilter()
-                .Where(u => u.TenantId == tenant.Id || u.AccountType == AccountTypeEnum.SuperAdmin)
-                .WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
-                .WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
-                .FirstAsync();
-            _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
-        }
-        else
-        {
-            // 根据入参类型查询登录用户
-            user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).ClearFilter()
-                .WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
-                .WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
-                .FirstAsync();
-            _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
-            
-            // 租户是否存在或已禁用
-            tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Id == user.TenantId);
-            if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
-        }
-        
-        // 若登录的是超级管理员,则引用当前绑定的租户,这样登陆后操作的租户数据才会与该租户关联
-        if (user.AccountType == AccountTypeEnum.SuperAdmin) user.TenantId = tenant.Id;
-
-        return (tenant, user);
-    }
-
-    /// <summary>
-    /// 验证用户密码
-    /// </summary>
-    /// <param name="password"></param>
-    /// <param name="keyPasswordErrorTimes"></param>
-    /// <param name="passwordErrorTimes"></param>
-    /// <param name="user"></param>
-    private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user)
-    {
-        if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
-        {
-            if (user.Password.Equals(MD5Encryption.Encrypt(password))) return;
-            
-            _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
-            throw Oops.Oh(ErrorCodeEnum.D1000);
-        }
-
-        // 国密SM2解密(前端密码传输SM2加密后的)
-        password = CryptogramUtil.SM2Decrypt(password);
-        if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return;
-
-        _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
-        throw Oops.Oh(ErrorCodeEnum.D1000);
-    }
-
-    /// <summary>
-    /// 验证锁屏密码 🔖
-    /// </summary>
-    /// <param name="password"></param>
-    /// <returns></returns>
-    [DisplayName("验证锁屏密码")]
-    public virtual async Task<bool> UnLockScreen([Required, FromQuery] string password)
-    {
-        // 账号是否存在
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
-        _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
-
-        var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
-        var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
-
-        // 是否开启域登录验证
-        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
-        {
-            var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
-            if (userLdap == null)
-            {
-                VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
-            }
-            else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId.Value, userLdap.Account, CryptogramUtil.Decrypt(password)))
-            {
-                _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
-                throw Oops.Oh(ErrorCodeEnum.D1000);
-            }
-        }
-        else
-            VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
-
-        return true;
-    }
-
-    /// <summary>
-    /// 手机号登录 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [DisplayName("手机号登录")]
-    public virtual async Task<LoginOutput> LoginPhone([Required] LoginPhoneInput input)
-    {
-        // 校验短信验证码
-        App.GetRequiredService<SysSmsService>().VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code });
-        
-        // 获取登录租户和用户
-        var (_, user) = await GetLoginUserAndTenant(input.Host, phone: input.Phone);
-
-        return await CreateToken(user);
-    }
-
-    /// <summary>
-    /// 生成Token令牌 🔖
-    /// </summary>
-    /// <param name="user"></param>
-    /// <returns></returns>
-    [NonAction]
-    internal virtual async Task<LoginOutput> CreateToken(SysUser user)
-    {
-        // 单用户登录
-        await _sysOnlineUserService.SingleLogin(user.Id);
-
-        // 生成Token令牌
-        var tokenExpire = await _sysConfigService.GetTokenExpire();
-        var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
-        {
-            { ClaimConst.UserId, user.Id },
-            { ClaimConst.TenantId, user.TenantId },
-            { ClaimConst.Account, user.Account },
-            { ClaimConst.RealName, user.RealName },
-            { ClaimConst.AccountType, user.AccountType },
-            { ClaimConst.OrgId, user.OrgId },
-            { ClaimConst.OrgName, user.SysOrg?.Name },
-            { ClaimConst.OrgType, user.SysOrg?.Type },
-        }, tokenExpire);
-
-        // 生成刷新Token令牌
-        var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
-        var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
-
-        // 设置响应报文头
-        _httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
-
-        // Swagger Knife4UI-AfterScript登录脚本
-        // ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
-
-        // 更新用户登录信息
-        user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true);
-        (user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp);
-        user.LastLoginTime = DateTime.Now;
-        user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent);
-        await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new
-        {
-            u.LastLoginIp,
-            u.LastLoginAddress,
-            u.LastLoginTime,
-            u.LastLoginDevice,
-        }).ExecuteCommandAsync();
-
-        return new LoginOutput
-        {
-            AccessToken = accessToken,
-            RefreshToken = refreshToken
-        };
-    }
-
-    /// <summary>
-    /// 获取登录账号 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取登录账号")]
-    public virtual async Task<LoginUserOutput> GetUserInfo()
-    {
-        var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
-        // 获取机构
-        var org = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysOrg>>().GetFirstAsync(u => u.Id == user.OrgId);
-        // 获取职位
-        var pos = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysPos>>().GetFirstAsync(u => u.Id == user.PosId);
-        // 获取按钮集合
-        var buttons = await _sysMenuService.GetOwnBtnPermList();
-        // 获取角色集合
-        var roleIds = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserRole>>().AsQueryable()
-            .Where(u => u.UserId == user.Id).Select(u => u.RoleId).ToListAsync();
-        // 获取水印文字(若系统水印为空则全局为空)
-        var watermarkText = await _sysConfigService.GetConfigValue<string>(ConfigConst.SysWebWatermark);
-        if (!string.IsNullOrWhiteSpace(watermarkText))
-            watermarkText += $"-{user.RealName}"; // $"-{user.RealName}-{_httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true)}-{DateTime.Now}";
-        return new LoginUserOutput
-        {
-            Id = user.Id,
-            Account = user.Account,
-            RealName = user.RealName,
-            Phone = user.Phone,
-            IdCardNum = user.IdCardNum,
-            Email = user.Email,
-            AccountType = user.AccountType,
-            Avatar = user.Avatar,
-            Address = user.Address,
-            Signature = user.Signature,
-            OrgId = user.OrgId,
-            OrgName = org?.Name,
-            OrgType = org?.Type,
-            PosName = pos?.Name,
-            Buttons = buttons,
-            RoleIds = roleIds,
-            WatermarkText = watermarkText
-        };
-    }
-
-    /// <summary>
-    /// 获取刷新Token 🔖
-    /// </summary>
-    /// <param name="accessToken"></param>
-    /// <returns></returns>
-    [DisplayName("获取刷新Token")]
-    public virtual string GetRefreshToken([FromQuery] string accessToken)
-    {
-        var refreshTokenExpire = _sysConfigService.GetRefreshTokenExpire().GetAwaiter().GetResult();
-        return JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
-    }
-
-    /// <summary>
-    /// 退出系统 🔖
-    /// </summary>
-    [DisplayName("退出系统")]
-    public void Logout()
-    {
-        if (string.IsNullOrWhiteSpace(_userManager.Account))
-            throw Oops.Oh(ErrorCodeEnum.D1011);
-
-        _httpContextAccessor.HttpContext.SignoutToSwagger();
-    }
-
-    /// <summary>
-    /// 获取验证码 🔖
-    /// </summary>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [SuppressMonitor]
-    [DisplayName("获取验证码")]
-    public dynamic GetCaptcha()
-    {
-        var codeId = YitIdHelper.NextId().ToString();
-        var captcha = _captcha.Generate(codeId);
-        var expirySeconds = App.GetOptions<CaptchaOptions>()?.ExpirySeconds ?? 60;
-        return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
-    }
-
-    /// <summary>
-    /// Swagger登录检查 🔖
-    /// </summary>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [HttpPost("/api/swagger/checkUrl"), NonUnify]
-    [ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
-    public int SwaggerCheckUrl()
-    {
-        return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
-    }
-
-    /// <summary>
-    /// Swagger登录提交 🔖
-    /// </summary>
-    /// <param name="auth"></param>
-    /// <returns></returns>
-    [AllowAnonymous]
-    [HttpPost("/api/swagger/submitUrl"), NonUnify]
-    [ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
-    public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
-    {
-        try
-        {
-            _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}", false);
-
-            await Login(new LoginInput
-            {
-                Account = auth.UserName,
-                Password = CryptogramUtil.SM2Encrypt(auth.Password),
-                Host = SqlSugarConst.DefaultTenantHost
-            });
-
-            _sysCacheService.Remove($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}");
-
-            return 200;
-        }
-        catch (Exception)
-        {
-            return 401;
-        }
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using Furion.SpecificationDocument;
+using Lazy.Captcha.Core;
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统登录授权服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 500)]
+public class SysAuthService : IDynamicApiController, ITransient
+{
+    private readonly UserManager _userManager;
+    private readonly SqlSugarRepository<SysUser> _sysUserRep;
+    private readonly IHttpContextAccessor _httpContextAccessor;
+    private readonly SysMenuService _sysMenuService;
+    private readonly SysOnlineUserService _sysOnlineUserService;
+    private readonly SysConfigService _sysConfigService;
+    private readonly ICaptcha _captcha;
+    private readonly SysCacheService _sysCacheService;
+
+    public SysAuthService(UserManager userManager,
+        SqlSugarRepository<SysUser> sysUserRep,
+        IHttpContextAccessor httpContextAccessor,
+        SysMenuService sysMenuService,
+        SysOnlineUserService sysOnlineUserService,
+        SysConfigService sysConfigService,
+        ICaptcha captcha,
+        SysCacheService sysCacheService)
+    {
+        _userManager = userManager;
+        _sysUserRep = sysUserRep;
+        _httpContextAccessor = httpContextAccessor;
+        _sysMenuService = sysMenuService;
+        _sysOnlineUserService = sysOnlineUserService;
+        _sysConfigService = sysConfigService;
+        _captcha = captcha;
+        _sysCacheService = sysCacheService;
+    }
+
+    /// <summary>
+    /// 账号密码登录 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <remarks>用户名/密码:superadmin/123456</remarks>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("账号密码登录")]
+    public virtual async Task<LoginOutput> Login([Required] LoginInput input)
+    {
+        //// 可以根据域名获取具体租户
+        //var host = _httpContextAccessor.HttpContext.Request.Host;
+
+        // 判断密码错误次数(缓存30分钟)
+        var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{input.Account}";
+        var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
+        var passwordMaxErrorTimes = await _sysConfigService.GetConfigValue<int>(ConfigConst.SysPasswordMaxErrorTimes);
+        // 若未配置或误配置为0、负数, 则默认密码错误次数最大为10次
+        if (passwordMaxErrorTimes < 1) passwordMaxErrorTimes = 10;
+        if (passwordErrorTimes > passwordMaxErrorTimes) throw Oops.Oh(ErrorCodeEnum.D1027);
+
+        // 判断是否开启验证码,其校验验证码
+        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysCaptcha) && !_captcha.Validate(input.CodeId.ToString(), input.Code)) throw Oops.Oh(ErrorCodeEnum.D0008);
+        
+        // 获取登录租户和用户
+        var (tenant, user) = await GetLoginUserAndTenant(input.Host, account: input.Account);
+
+        // 账号是否被冻结
+        if (user.Status == StatusEnum.Disable) throw Oops.Oh(ErrorCodeEnum.D1017);
+
+        // 是否开启域登录验证
+        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
+        {
+            var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == tenant.Id);
+            if (userLdap == null)
+            {
+                VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
+            }
+            else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(tenant.Id, userLdap.Account, CryptogramUtil.Decrypt(input.Password)))
+            {
+                _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
+                throw Oops.Oh(ErrorCodeEnum.D1000);
+            }
+        }
+        else
+            VerifyPassword(input.Password, keyPasswordErrorTimes, passwordErrorTimes, user);
+
+        // 登录成功则清空密码错误次数
+        _sysCacheService.Remove(keyPasswordErrorTimes);
+
+        return await CreateToken(user, tenant.AppId);
+    }
+    
+    /// <summary>
+    /// 获取登录租户和用户
+    /// </summary>
+    /// <param name="host"></param>
+    /// <param name="account"></param>
+    /// <param name="phone"></param>
+    /// <returns></returns>
+    [NonAction]
+    public async Task<(SysTenant tenant, SysUser user)> GetLoginUserAndTenant(string host, string account=null, string phone=null)
+    {
+        // 是否租户隔离登录验证
+        var isTenantHostLogin = await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysTenantHostLogin);
+        
+        SysUser user;
+        SysTenant tenant;
+        
+        // 租户隔离登录
+        if (isTenantHostLogin)
+        {
+            // 若租户域名为空或为本地域名,则取默认租户域名
+            if (string.IsNullOrWhiteSpace(host) || host.StartsWith("localhost")) host = SqlSugarConst.DefaultTenantHost;
+            
+            // 租户是否存在或已禁用
+            tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Host == host.ToLower());
+            if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
+
+            // 根据入参类型、租户查询登录用户
+            user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).ClearFilter()
+                .Where(u => u.TenantId == tenant.Id || u.AccountType == AccountTypeEnum.SuperAdmin)
+                .WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
+                .WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
+                .FirstAsync();
+            _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+            
+            // 若登录的是超级管理员,则引用当前绑定的租户,这样登陆后操作的租户数据会与该租户关联
+            if (user.AccountType == AccountTypeEnum.SuperAdmin) user.TenantId = tenant.Id;
+        }
+        else
+        {
+            // 根据入参类型查询登录用户
+            user = await _sysUserRep.AsQueryable().Includes(t => t.SysOrg).ClearFilter()
+                .WhereIF(!string.IsNullOrWhiteSpace(account), u => u.Account.Equals(account))
+                .WhereIF(!string.IsNullOrWhiteSpace(phone), u => u.Phone.Equals(phone))
+                .FirstAsync();
+            _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+            
+            // 租户是否存在或已禁用
+            tenant = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysTenant>>().GetFirstAsync(u => u.Id == user.TenantId);
+            if (tenant?.Status != StatusEnum.Enable) throw Oops.Oh(ErrorCodeEnum.Z1003);
+        }
+
+        return (tenant, user);
+    }
+
+    /// <summary>
+    /// 验证用户密码
+    /// </summary>
+    /// <param name="password"></param>
+    /// <param name="keyPasswordErrorTimes"></param>
+    /// <param name="passwordErrorTimes"></param>
+    /// <param name="user"></param>
+    private void VerifyPassword(string password, string keyPasswordErrorTimes, int passwordErrorTimes, SysUser user)
+    {
+        if (CryptogramUtil.CryptoType == CryptogramEnum.MD5.ToString())
+        {
+            if (user.Password.Equals(MD5Encryption.Encrypt(password))) return;
+            
+            _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
+            throw Oops.Oh(ErrorCodeEnum.D1000);
+        }
+
+        // 国密SM2解密(前端密码传输SM2加密后的)
+        password = CryptogramUtil.SM2Decrypt(password);
+        if (CryptogramUtil.Decrypt(user.Password).Equals(password)) return;
+
+        _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
+        throw Oops.Oh(ErrorCodeEnum.D1000);
+    }
+
+    /// <summary>
+    /// 验证锁屏密码 🔖
+    /// </summary>
+    /// <param name="password"></param>
+    /// <returns></returns>
+    [DisplayName("验证锁屏密码")]
+    public virtual async Task<bool> UnLockScreen([Required, FromQuery] string password)
+    {
+        // 账号是否存在
+        var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId);
+        _ = user ?? throw Oops.Oh(ErrorCodeEnum.D0009);
+
+        var keyPasswordErrorTimes = $"{CacheConst.KeyPasswordErrorTimes}{user.Account}";
+        var passwordErrorTimes = _sysCacheService.Get<int>(keyPasswordErrorTimes);
+
+        // 是否开启域登录验证
+        if (await _sysConfigService.GetConfigValue<bool>(ConfigConst.SysDomainLogin))
+        {
+            var userLdap = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserLdap>>().GetFirstAsync(u => u.UserId == user.Id && u.TenantId == user.TenantId);
+            if (userLdap == null)
+            {
+                VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
+            }
+            else if (!await App.GetRequiredService<SysLdapService>().AuthAccount(user.TenantId.Value, userLdap.Account, CryptogramUtil.Decrypt(password)))
+            {
+                _sysCacheService.Set(keyPasswordErrorTimes, ++passwordErrorTimes, TimeSpan.FromMinutes(30));
+                throw Oops.Oh(ErrorCodeEnum.D1000);
+            }
+        }
+        else
+            VerifyPassword(password, keyPasswordErrorTimes, passwordErrorTimes, user);
+
+        return true;
+    }
+
+    /// <summary>
+    /// 手机号登录 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [DisplayName("手机号登录")]
+    public virtual async Task<LoginOutput> LoginPhone([Required] LoginPhoneInput input)
+    {
+        // 校验短信验证码
+        App.GetRequiredService<SysSmsService>().VerifyCode(new SmsVerifyCodeInput { Phone = input.Phone, Code = input.Code });
+        
+        // 获取登录租户和用户
+        var (tenant, user) = await GetLoginUserAndTenant(input.Host, phone: input.Phone);
+
+        return await CreateToken(user, tenant.AppId);
+    }
+
+    /// <summary>
+    /// 生成Token令牌 🔖
+    /// </summary>
+    /// <param name="user"></param>
+    /// <param name="appId"></param>
+    /// <returns></returns>
+    [NonAction]
+    internal virtual async Task<LoginOutput> CreateToken(SysUser user, long appId)
+    {
+        // 单用户登录
+        await _sysOnlineUserService.SingleLogin(user.Id);
+
+        // 生成Token令牌
+        var tokenExpire = await _sysConfigService.GetTokenExpire();
+        var accessToken = JWTEncryption.Encrypt(new Dictionary<string, object>
+        {
+            { ClaimConst.AppId, appId },
+            { ClaimConst.UserId, user.Id },
+            { ClaimConst.TenantId, user.TenantId },
+            { ClaimConst.Account, user.Account },
+            { ClaimConst.RealName, user.RealName },
+            { ClaimConst.AccountType, user.AccountType },
+            { ClaimConst.OrgId, user.OrgId },
+            { ClaimConst.OrgName, user.SysOrg?.Name },
+            { ClaimConst.OrgType, user.SysOrg?.Type },
+        }, tokenExpire);
+
+        // 生成刷新Token令牌
+        var refreshTokenExpire = await _sysConfigService.GetRefreshTokenExpire();
+        var refreshToken = JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
+
+        // 设置响应报文头
+        _httpContextAccessor.HttpContext.SetTokensOfResponseHeaders(accessToken, refreshToken);
+
+        // Swagger Knife4UI-AfterScript登录脚本
+        // ke.global.setAllHeader('Authorization', 'Bearer ' + ke.response.headers['access-token']);
+
+        // 更新用户登录信息
+        user.LastLoginIp = _httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true);
+        (user.LastLoginAddress, double? longitude, double? latitude) = CommonUtil.GetIpAddress(user.LastLoginIp);
+        user.LastLoginTime = DateTime.Now;
+        user.LastLoginDevice = CommonUtil.GetClientDeviceInfo(_httpContextAccessor.HttpContext?.Request?.Headers?.UserAgent);
+        await _sysUserRep.AsUpdateable(user).UpdateColumns(u => new
+        {
+            u.LastLoginIp,
+            u.LastLoginAddress,
+            u.LastLoginTime,
+            u.LastLoginDevice,
+        }).ExecuteCommandAsync();
+
+        return new LoginOutput
+        {
+            AccessToken = accessToken,
+            RefreshToken = refreshToken
+        };
+    }
+
+    /// <summary>
+    /// 获取登录账号 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取登录账号")]
+    public virtual async Task<LoginUserOutput> GetUserInfo()
+    {
+        var user = await _sysUserRep.GetFirstAsync(u => u.Id == _userManager.UserId) ?? throw Oops.Oh(ErrorCodeEnum.D1011).StatusCode(401);
+        // 获取机构
+        var org = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysOrg>>().GetFirstAsync(u => u.Id == user.OrgId);
+        // 获取职位
+        var pos = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysPos>>().GetFirstAsync(u => u.Id == user.PosId);
+        // 获取按钮集合
+        var buttons = await _sysMenuService.GetOwnBtnPermList();
+        // 获取角色集合
+        var roleIds = await _sysUserRep.ChangeRepository<SqlSugarRepository<SysUserRole>>().AsQueryable()
+            .Where(u => u.UserId == user.Id).Select(u => u.RoleId).ToListAsync();
+        // 获取水印文字(若系统水印为空则全局为空)
+        var watermarkText = await _sysConfigService.GetConfigValue<string>(ConfigConst.SysWebWatermark);
+        if (!string.IsNullOrWhiteSpace(watermarkText))
+            watermarkText += $"-{user.RealName}"; // $"-{user.RealName}-{_httpContextAccessor.HttpContext.GetRemoteIpAddressToIPv4(true)}-{DateTime.Now}";
+        return new LoginUserOutput
+        {
+            Id = user.Id,
+            Account = user.Account,
+            RealName = user.RealName,
+            Phone = user.Phone,
+            IdCardNum = user.IdCardNum,
+            Email = user.Email,
+            AccountType = user.AccountType,
+            Avatar = user.Avatar,
+            Address = user.Address,
+            Signature = user.Signature,
+            OrgId = user.OrgId,
+            OrgName = org?.Name,
+            OrgType = org?.Type,
+            PosName = pos?.Name,
+            Buttons = buttons,
+            RoleIds = roleIds,
+            WatermarkText = watermarkText
+        };
+    }
+
+    /// <summary>
+    /// 获取刷新Token 🔖
+    /// </summary>
+    /// <param name="accessToken"></param>
+    /// <returns></returns>
+    [DisplayName("获取刷新Token")]
+    public virtual string GetRefreshToken([FromQuery] string accessToken)
+    {
+        var refreshTokenExpire = _sysConfigService.GetRefreshTokenExpire().GetAwaiter().GetResult();
+        return JWTEncryption.GenerateRefreshToken(accessToken, refreshTokenExpire);
+    }
+
+    /// <summary>
+    /// 退出系统 🔖
+    /// </summary>
+    [DisplayName("退出系统")]
+    public void Logout()
+    {
+        if (string.IsNullOrWhiteSpace(_userManager.Account))
+            throw Oops.Oh(ErrorCodeEnum.D1011);
+
+        _httpContextAccessor.HttpContext.SignoutToSwagger();
+    }
+
+    /// <summary>
+    /// 获取验证码 🔖
+    /// </summary>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [SuppressMonitor]
+    [DisplayName("获取验证码")]
+    public dynamic GetCaptcha()
+    {
+        var codeId = YitIdHelper.NextId().ToString();
+        var captcha = _captcha.Generate(codeId);
+        var expirySeconds = App.GetOptions<CaptchaOptions>()?.ExpirySeconds ?? 60;
+        return new { Id = codeId, Img = captcha.Base64, ExpirySeconds = expirySeconds };
+    }
+
+    /// <summary>
+    /// Swagger登录检查 🔖
+    /// </summary>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost("/api/swagger/checkUrl"), NonUnify]
+    [ApiDescriptionSettings(Description = "Swagger登录检查", DisableInherite = true)]
+    public int SwaggerCheckUrl()
+    {
+        return _httpContextAccessor.HttpContext.User.Identity.IsAuthenticated ? 200 : 401;
+    }
+
+    /// <summary>
+    /// Swagger登录提交 🔖
+    /// </summary>
+    /// <param name="auth"></param>
+    /// <returns></returns>
+    [AllowAnonymous]
+    [HttpPost("/api/swagger/submitUrl"), NonUnify]
+    [ApiDescriptionSettings(Description = "Swagger登录提交", DisableInherite = true)]
+    public async Task<int> SwaggerSubmitUrl([FromForm] SpecificationAuth auth)
+    {
+        try
+        {
+            _sysCacheService.Set($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}", false);
+
+            await Login(new LoginInput
+            {
+                Account = auth.UserName,
+                Password = CryptogramUtil.SM2Encrypt(auth.Password),
+                Host = SqlSugarConst.DefaultTenantHost
+            });
+
+            _sysCacheService.Remove($"{CacheConst.KeyConfig}{ConfigConst.SysCaptcha}");
+
+            return 200;
+        }
+        catch (Exception)
+        {
+            return 401;
+        }
+    }
 }

+ 10 - 0
Admin.NET/Admin.NET.Core/Service/Menu/Dto/MenuInput.cs

@@ -17,6 +17,16 @@ public class MenuInput
     /// 菜单类型(1目录 2菜单 3按钮)
     /// </summary>
     public MenuTypeEnum? Type { get; set; }
+    
+    /// <summary>
+    /// 获取所有菜单
+    /// </summary>
+    public bool All { get; set; }
+    
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    public long AppId { get; set; }
 }
 
 public class AddMenuInput : SysMenu

+ 30 - 20
Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs

@@ -14,12 +14,14 @@ public class SysMenuService : IDynamicApiController, ITransient
 {
     private readonly UserManager _userManager;
     private readonly SqlSugarRepository<SysMenu> _sysMenuRep;
+    private readonly SqlSugarRepository<SysAppMenu> _sysAppMenuRep;
     private readonly SysRoleMenuService _sysRoleMenuService;
     private readonly SysUserRoleService _sysUserRoleService;
     private readonly SysUserMenuService _sysUserMenuService;
     private readonly SysCacheService _sysCacheService;
 
     public SysMenuService(UserManager userManager,
+        SqlSugarRepository<SysAppMenu> sysAppMenuRep,
         SqlSugarRepository<SysMenu> sysMenuRep,
         SysRoleMenuService sysRoleMenuService,
         SysUserRoleService sysUserRoleService,
@@ -28,6 +30,7 @@ public class SysMenuService : IDynamicApiController, ITransient
     {
         _userManager = userManager;
         _sysMenuRep = sysMenuRep;
+        _sysAppMenuRep = sysAppMenuRep;
         _sysRoleMenuService = sysRoleMenuService;
         _sysUserRoleService = sysUserRoleService;
         _sysUserMenuService = sysUserMenuService;
@@ -44,19 +47,18 @@ public class SysMenuService : IDynamicApiController, ITransient
         if (_userManager.SuperAdmin)
         {
             var menuList = await _sysMenuRep.AsQueryable()
+                .InnerJoin<SysAppMenu>((u, am) => am.AppId == _userManager.AppId && u.Id == am.MenuId)
                 .Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable)
                 .OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0);
             return menuList.Adapt<List<MenuOutput>>();
         }
-        else
-        {
-            var menuIdList = await GetMenuIdList();
-            var menuTree = await _sysMenuRep.AsQueryable()
-                .Where(u => u.Status == StatusEnum.Enable)
-                .OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray());
-            DeleteBtnFromMenuTree(menuTree);
-            return menuTree.Adapt<List<MenuOutput>>();
-        }
+        var menuIdList = await GetMenuIdList();
+        var menuTree = await _sysMenuRep.AsQueryable()
+            .InnerJoin<SysAppMenu>((u, am) => am.AppId == _userManager.AppId && u.Id == am.MenuId)
+            .Where(u => u.Status == StatusEnum.Enable)
+            .OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray());
+        DeleteBtnFromMenuTree(menuTree);
+        return menuTree.Adapt<List<MenuOutput>>();
     }
 
     /// <summary>
@@ -84,20 +86,25 @@ public class SysMenuService : IDynamicApiController, ITransient
     {
         var menuIdList = _userManager.SuperAdmin ? new List<long>() : await GetMenuIdList();
 
+        // 仅超级管理员可以获取全部菜单
+        var joinApp = _userManager.SuperAdmin == false || (_userManager.SuperAdmin && !input.All);
+        // 超级管理员可根据传参获取指定应用的菜单
+        var appId = _userManager.SuperAdmin && input.AppId > 0 ? input.AppId : _userManager.AppId;
+        var query = _sysMenuRep.AsQueryable()
+            .InnerJoinIF<SysAppMenu>(joinApp, (u, am) => am.AppId == appId && u.Id == am.MenuId);
+
         // 有筛选条件时返回list列表(防止构造不出树)
         if (!string.IsNullOrWhiteSpace(input.Title) || input.Type is > 0)
         {
-            return await _sysMenuRep.AsQueryable()
-                .WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title))
+            return await query.WhereIF(!string.IsNullOrWhiteSpace(input.Title), u => u.Title.Contains(input.Title))
                 .WhereIF(input.Type is > 0, u => u.Type == input.Type)
                 .WhereIF(menuIdList.Count > 1, u => menuIdList.Contains(u.Id))
                 .OrderBy(u => new { u.OrderNo, u.Id }).ToListAsync();
         }
 
         return _userManager.SuperAdmin ?
-            await _sysMenuRep.AsQueryable().OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0) :
-            await _sysMenuRep.AsQueryable()
-                .OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray()); // 角色菜单授权时
+            await query.OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0) :
+            await query.OrderBy(u => new { u.OrderNo, u.Id }).ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray()); // 角色菜单授权时
     }
 
     /// <summary>
@@ -112,8 +119,7 @@ public class SysMenuService : IDynamicApiController, ITransient
         var isExist = input.Type != MenuTypeEnum.Btn
             ? await _sysMenuRep.IsAnyAsync(u => u.Title == input.Title && u.Pid == input.Pid)
             : await _sysMenuRep.IsAnyAsync(u => u.Permission == input.Permission && u.Pid == input.Pid);
-        if (isExist)
-            throw Oops.Oh(ErrorCodeEnum.D4000);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D4000);
 
         if (!string.IsNullOrWhiteSpace(input.Name))
         {
@@ -133,6 +139,9 @@ public class SysMenuService : IDynamicApiController, ITransient
 
         await _sysMenuRep.InsertAsync(sysMenu);
 
+        // 保存应用菜单关联
+        await _sysAppMenuRep.InsertAsync(new SysAppMenu { AppId = _userManager.AppId, MenuId = sysMenu.Id });
+
         // 清除缓存
         DeleteMenuCache();
     }
@@ -197,6 +206,9 @@ public class SysMenuService : IDynamicApiController, ITransient
         
         // 级联删除用户收藏菜单
         await _sysUserMenuService.DeleteMenuList(menuIdList);
+        
+        // 删除应用菜单关联
+        await _sysAppMenuRep.AsDeleteable().Where(u => u.MenuId == input.Id).ExecuteCommandAsync();
 
         // 清除缓存
         DeleteMenuCache();
@@ -222,10 +234,8 @@ public class SysMenuService : IDynamicApiController, ITransient
             menu.IsAffix = false;
             menu.IsIframe = false;
 
-            if (string.IsNullOrEmpty(permission))
-                throw Oops.Oh(ErrorCodeEnum.D4003);
-            if (!permission.Contains(':'))
-                throw Oops.Oh(ErrorCodeEnum.D4004);
+            if (string.IsNullOrEmpty(permission)) throw Oops.Oh(ErrorCodeEnum.D4003);
+            if (!permission.Contains(':')) throw Oops.Oh(ErrorCodeEnum.D4004);
         }
         else
         {

+ 1 - 1
Admin.NET/Admin.NET.Core/Service/OAuth/SysOAuthService.cs

@@ -113,7 +113,7 @@ public class SysOAuthService : IDynamicApiController, ITransient
         }
 
         // 构建Token令牌
-        var token = await App.GetRequiredService<SysAuthService>().CreateToken(wechatUser.SysUser);
+        var token = await App.GetRequiredService<SysAuthService>().CreateToken(wechatUser.SysUser, SqlSugarConst.DefaultAppId);
 
         return new RedirectResult($"{redirectUrl}/#/login?token={token.AccessToken}");
     }

+ 6 - 0
Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantInput.cs

@@ -29,6 +29,12 @@ public class PageTenantInput : BasePageInput
 
 public class AddTenantInput : TenantOutput
 {
+    /// <summary>
+    /// 应用Id
+    /// </summary>
+    [Required(ErrorMessage = "应用不能为空")]
+    public new long? AppId { get; set; }
+    
     /// <summary>
     /// 租户名称
     /// </summary>

+ 5 - 0
Admin.NET/Admin.NET.Core/Service/Tenant/Dto/TenantOutput.cs

@@ -12,6 +12,11 @@ public class TenantOutput : SysTenant
     /// 租户名称
     /// </summary>
     public virtual string Name { get; set; }
+    
+    /// <summary>
+    /// 关联应用名称
+    /// </summary>
+    public virtual string AppName { get; set; }
 
     /// <summary>
     /// 管理员账号

+ 5 - 1
Admin.NET/Admin.NET.Core/Service/Tenant/SysTenantService.cs

@@ -63,16 +63,19 @@ public class SysTenantService : IDynamicApiController, ITransient
         return await _sysTenantRep.AsQueryable()
             .LeftJoin<SysUser>((u, a) => u.UserId == a.Id)
             .LeftJoin<SysOrg>((u, a, b) => u.OrgId == b.Id)
+            .LeftJoin<SysApp>((u, a, b, c) => u.AppId == c.Id)
             .WhereIF(!string.IsNullOrWhiteSpace(input.Phone), (u, a) => a.Phone.Contains(input.Phone.Trim()))
             .WhereIF(!string.IsNullOrWhiteSpace(input.Name), (u, a, b) => b.Name.Contains(input.Name.Trim()))
             .Where(u => u.Id.ToString() != SqlSugarConst.MainConfigId) // 排除默认主库/主租户
             .OrderBy(u => new { u.OrderNo, u.Id })
-            .Select((u, a, b) => new TenantOutput
+            .Select((u, a, b, c) => new TenantOutput
             {
                 Id = u.Id,
                 OrgId = b.Id,
                 Name = b.Name,
                 UserId = a.Id,
+                AppId = u.AppId,
+                AppName = c.Name,
                 AdminAccount = a.Account,
                 Phone = a.Phone,
                 Host = u.Host,
@@ -306,6 +309,7 @@ public class SysTenantService : IDynamicApiController, ITransient
         if (!string.IsNullOrWhiteSpace(input.SlaveConnections) && !JSON.IsValid(input.SlaveConnections))
             throw Oops.Oh(ErrorCodeEnum.D1302);
 
+        input.AppId = null;
         await _sysTenantRep.AsUpdateable(input.Adapt<TenantOutput>()).IgnoreColumns(true).ExecuteCommandAsync();
 
         // 更新系统机构

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

@@ -12,6 +12,11 @@ namespace Admin.NET.Core;
 public class UserManager : IScoped
 {
     private readonly IHttpContextAccessor _httpContextAccessor;
+    
+    /// <summary>
+    /// 应用ID
+    /// </summary>
+    public long AppId => (_httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.AppId)?.Value).ToLong();
 
     /// <summary>
     /// 用户ID

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

@@ -11,6 +11,7 @@
  * https://github.com/swagger-api/swagger-codegen.git
  * Do not edit the class manually.
  */export * from './apis/apijsonapi';
+export * from './apis/sys-app-api';
 export * from './apis/sys-auth-api';
 export * from './apis/sys-cache-api';
 export * from './apis/sys-code-gen-api';

+ 720 - 0
Web/src/api-services/apis/sys-app-api.ts

@@ -0,0 +1,720 @@
+/* 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 { AddSysAppInput } from '../models';
+import { AdminResultInt64 } from '../models';
+import { AdminResultListInt64 } from '../models';
+import { AdminResultLoginOutput } from '../models';
+import { AdminResultObject } from '../models';
+import { AdminResultSqlSugarPagedListSysAppOutput } from '../models';
+import { BaseIdInput } from '../models';
+import { BasePageInput } from '../models';
+import { ChangeAppInput } from '../models';
+import { UpdateAppMenuInput } from '../models';
+import { UpdateSysAppInput } from '../models';
+/**
+ * SysAppApi - axios parameter creator
+ * @export
+ */
+export const SysAppApiAxiosParamCreator = function (configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加应用 🔖
+         * @param {AddSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppAddPost: async (body?: AddSysAppInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/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 {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppChangeAppGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/changeApp`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 切换应用 🔖
+         * @param {ChangeAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppChangeAppPost: async (body?: ChangeAppInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/changeApp`;
+            // 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}
+         */
+        apiSysAppDeletePost: async (body?: BaseIdInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/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 {number} [id] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppGrantMenuGet: async (id?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/grantMenu`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+            // http bearer authentication required
+            if (configuration && configuration.accessToken) {
+                const accessToken = typeof configuration.accessToken === 'function'
+                    ? await configuration.accessToken()
+                    : await configuration.accessToken;
+                localVarHeaderParameter["Authorization"] = "Bearer " + accessToken;
+            }
+
+            if (id !== undefined) {
+                localVarQueryParameter['id'] = id;
+            }
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 授权菜单 🔖
+         * @param {UpdateAppMenuInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppGrantMenuPost: async (body?: UpdateAppMenuInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/grantMenu`;
+            // 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 {BasePageInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppPagePost: async (body?: BasePageInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/page`;
+            // 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 {UpdateSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysAppUpdatePost: async (body?: UpdateSysAppInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysApp/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,
+            };
+        },
+    }
+};
+
+/**
+ * SysAppApi - functional programming interface
+ * @export
+ */
+export const SysAppApiFp = function(configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加应用 🔖
+         * @param {AddSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppAddPost(body?: AddSysAppInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultInt64>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppAddPost(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 {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppChangeAppGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultObject>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppChangeAppGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 切换应用 🔖
+         * @param {ChangeAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppChangeAppPost(body?: ChangeAppInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultLoginOutput>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppChangeAppPost(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 apiSysAppDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppDeletePost(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 {number} [id] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppGrantMenuGet(id?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListInt64>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppGrantMenuGet(id, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 授权菜单 🔖
+         * @param {UpdateAppMenuInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppGrantMenuPost(body?: UpdateAppMenuInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppGrantMenuPost(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 {BasePageInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppPagePost(body?: BasePageInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultSqlSugarPagedListSysAppOutput>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppPagePost(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 {UpdateSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppUpdatePost(body?: UpdateSysAppInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysAppApiAxiosParamCreator(configuration).apiSysAppUpdatePost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+    }
+};
+
+/**
+ * SysAppApi - factory interface
+ * @export
+ */
+export const SysAppApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    return {
+        /**
+         * 
+         * @summary 增加应用 🔖
+         * @param {AddSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppAddPost(body?: AddSysAppInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultInt64>> {
+            return SysAppApiFp(configuration).apiSysAppAddPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取切换应用数据 🔖
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppChangeAppGet(options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultObject>> {
+            return SysAppApiFp(configuration).apiSysAppChangeAppGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 切换应用 🔖
+         * @param {ChangeAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppChangeAppPost(body?: ChangeAppInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultLoginOutput>> {
+            return SysAppApiFp(configuration).apiSysAppChangeAppPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 删除应用 🔖
+         * @param {BaseIdInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysAppApiFp(configuration).apiSysAppDeletePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取授权菜单 🔖
+         * @param {number} [id] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppGrantMenuGet(id?: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListInt64>> {
+            return SysAppApiFp(configuration).apiSysAppGrantMenuGet(id, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 授权菜单 🔖
+         * @param {UpdateAppMenuInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppGrantMenuPost(body?: UpdateAppMenuInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysAppApiFp(configuration).apiSysAppGrantMenuPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 分页查询应用 🔖
+         * @param {BasePageInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppPagePost(body?: BasePageInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultSqlSugarPagedListSysAppOutput>> {
+            return SysAppApiFp(configuration).apiSysAppPagePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 更新应用 🔖
+         * @param {UpdateSysAppInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysAppUpdatePost(body?: UpdateSysAppInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysAppApiFp(configuration).apiSysAppUpdatePost(body, options).then((request) => request(axios, basePath));
+        },
+    };
+};
+
+/**
+ * SysAppApi - object-oriented interface
+ * @export
+ * @class SysAppApi
+ * @extends {BaseAPI}
+ */
+export class SysAppApi extends BaseAPI {
+    /**
+     * 
+     * @summary 增加应用 🔖
+     * @param {AddSysAppInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppAddPost(body?: AddSysAppInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultInt64>> {
+        return SysAppApiFp(this.configuration).apiSysAppAddPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取切换应用数据 🔖
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppChangeAppGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultObject>> {
+        return SysAppApiFp(this.configuration).apiSysAppChangeAppGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 切换应用 🔖
+     * @param {ChangeAppInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppChangeAppPost(body?: ChangeAppInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultLoginOutput>> {
+        return SysAppApiFp(this.configuration).apiSysAppChangeAppPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 删除应用 🔖
+     * @param {BaseIdInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppDeletePost(body?: BaseIdInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysAppApiFp(this.configuration).apiSysAppDeletePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取授权菜单 🔖
+     * @param {number} [id] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppGrantMenuGet(id?: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListInt64>> {
+        return SysAppApiFp(this.configuration).apiSysAppGrantMenuGet(id, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 授权菜单 🔖
+     * @param {UpdateAppMenuInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppGrantMenuPost(body?: UpdateAppMenuInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysAppApiFp(this.configuration).apiSysAppGrantMenuPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 分页查询应用 🔖
+     * @param {BasePageInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppPagePost(body?: BasePageInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultSqlSugarPagedListSysAppOutput>> {
+        return SysAppApiFp(this.configuration).apiSysAppPagePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 更新应用 🔖
+     * @param {UpdateSysAppInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysAppApi
+     */
+    public async apiSysAppUpdatePost(body?: UpdateSysAppInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysAppApiFp(this.configuration).apiSysAppUpdatePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+}

+ 23 - 7
Web/src/api-services/apis/sys-menu-api.ts

@@ -130,10 +130,12 @@ export const SysMenuApiAxiosParamCreator = function (configuration?: Configurati
          * @summary 获取菜单列表 🔖
          * @param {string} [title] 标题
          * @param {MenuTypeEnum} [type] 菜单类型(1目录 2菜单 3按钮)
+         * @param {boolean} [all] 获取所有菜单
+         * @param {number} [appId] 应用Id
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        apiSysMenuListGet: async (title?: string, type?: MenuTypeEnum, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+        apiSysMenuListGet: async (title?: string, type?: MenuTypeEnum, all?: boolean, appId?: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
             const localVarPath = `/api/sysMenu/list`;
             // use dummy base URL string because the URL constructor only accepts absolute URLs.
             const localVarUrlObj = new URL(localVarPath, 'https://example.com');
@@ -162,6 +164,14 @@ export const SysMenuApiAxiosParamCreator = function (configuration?: Configurati
                 localVarQueryParameter['Type'] = type;
             }
 
+            if (all !== undefined) {
+                localVarQueryParameter['All'] = all;
+            }
+
+            if (appId !== undefined) {
+                localVarQueryParameter['AppId'] = appId;
+            }
+
             const query = new URLSearchParams(localVarUrlObj.search);
             for (const key in localVarQueryParameter) {
                 query.set(key, localVarQueryParameter[key]);
@@ -354,11 +364,13 @@ export const SysMenuApiFp = function(configuration?: Configuration) {
          * @summary 获取菜单列表 🔖
          * @param {string} [title] 标题
          * @param {MenuTypeEnum} [type] 菜单类型(1目录 2菜单 3按钮)
+         * @param {boolean} [all] 获取所有菜单
+         * @param {number} [appId] 应用Id
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListSysMenu>>> {
-            const localVarAxiosArgs = await SysMenuApiAxiosParamCreator(configuration).apiSysMenuListGet(title, type, options);
+        async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, all?: boolean, appId?: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListSysMenu>>> {
+            const localVarAxiosArgs = await SysMenuApiAxiosParamCreator(configuration).apiSysMenuListGet(title, type, all, appId, options);
             return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
                 const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
                 return axios.request(axiosRequestArgs);
@@ -438,11 +450,13 @@ export const SysMenuApiFactory = function (configuration?: Configuration, basePa
          * @summary 获取菜单列表 🔖
          * @param {string} [title] 标题
          * @param {MenuTypeEnum} [type] 菜单类型(1目录 2菜单 3按钮)
+         * @param {boolean} [all] 获取所有菜单
+         * @param {number} [appId] 应用Id
          * @param {*} [options] Override http request option.
          * @throws {RequiredError}
          */
-        async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListSysMenu>> {
-            return SysMenuApiFp(configuration).apiSysMenuListGet(title, type, options).then((request) => request(axios, basePath));
+        async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, all?: boolean, appId?: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListSysMenu>> {
+            return SysMenuApiFp(configuration).apiSysMenuListGet(title, type, all, appId, options).then((request) => request(axios, basePath));
         },
         /**
          * 
@@ -509,12 +523,14 @@ export class SysMenuApi extends BaseAPI {
      * @summary 获取菜单列表 🔖
      * @param {string} [title] 标题
      * @param {MenuTypeEnum} [type] 菜单类型(1目录 2菜单 3按钮)
+     * @param {boolean} [all] 获取所有菜单
+     * @param {number} [appId] 应用Id
      * @param {*} [options] Override http request option.
      * @throws {RequiredError}
      * @memberof SysMenuApi
      */
-    public async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListSysMenu>> {
-        return SysMenuApiFp(this.configuration).apiSysMenuListGet(title, type, options).then((request) => request(this.axios, this.basePath));
+    public async apiSysMenuListGet(title?: string, type?: MenuTypeEnum, all?: boolean, appId?: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListSysMenu>> {
+        return SysMenuApiFp(this.configuration).apiSysMenuListGet(title, type, all, appId, options).then((request) => request(this.axios, this.basePath));
     }
     /**
      * 

+ 80 - 0
Web/src/api-services/models/add-sys-app-input.ts

@@ -0,0 +1,80 @@
+/* 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 AddSysAppInput
+ */
+export interface AddSysAppInput {
+    /**
+     * 图标
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    logo: string;
+    /**
+     * 名称
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    name: string;
+    /**
+     * 标题
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    title: string;
+    /**
+     * 副标题
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    viceTitle: string;
+    /**
+     * 副描述
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    viceDesc: string;
+    /**
+     * 水印
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    watermark: string;
+    /**
+     * 版权信息
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    copyright: string;
+    /**
+     * ICP备案号
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    icp: string;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof AddSysAppInput
+     */
+    orderNo: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof AddSysAppInput
+     */
+    remark?: string | null;
+}

+ 12 - 0
Web/src/api-services/models/add-tenant-input.ts

@@ -68,6 +68,12 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     isDelete?: boolean;
+    /**
+     * 应用Id
+     * @type {number}
+     * @memberof AddTenantInput
+     */
+    appId: number;
     /**
      * 用户Id
      * @type {number}
@@ -128,6 +134,12 @@ export interface AddTenantInput {
      * @memberof AddTenantInput
      */
     status?: StatusEnum;
+    /**
+     * 关联应用名称
+     * @type {string}
+     * @memberof AddTenantInput
+     */
+    appName?: string | null;
     /**
      * 电子邮箱
      * @type {string}

+ 57 - 0
Web/src/api-services/models/admin-result-sql-sugar-paged-list-sys-app-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 { SqlSugarPagedListSysAppOutput } from './sql-sugar-paged-list-sys-app-output';
+/**
+ * 全局返回结果
+ * @export
+ * @interface AdminResultSqlSugarPagedListSysAppOutput
+ */
+export interface AdminResultSqlSugarPagedListSysAppOutput {
+    /**
+     * 状态码
+     * @type {number}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    code?: number;
+    /**
+     * 类型success、warning、error
+     * @type {string}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    type?: string | null;
+    /**
+     * 错误信息
+     * @type {string}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    message?: string | null;
+    /**
+     * 
+     * @type {SqlSugarPagedListSysAppOutput}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    result?: SqlSugarPagedListSysAppOutput;
+    /**
+     * 附加数据
+     * @type {any}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    extras?: any | null;
+    /**
+     * 时间
+     * @type {Date}
+     * @memberof AdminResultSqlSugarPagedListSysAppOutput
+     */
+    time?: Date;
+}

+ 26 - 0
Web/src/api-services/models/base-id-input.ts

@@ -0,0 +1,26 @@
+/* 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.
+ */
+/**
+ * 主键Id输入参数
+ * @export
+ * @interface BaseIdInput
+ */
+export interface BaseIdInput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof BaseIdInput
+     */
+    id: number;
+}

+ 70 - 0
Web/src/api-services/models/base-page-input.ts

@@ -0,0 +1,70 @@
+/* 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 BasePageInput
+ */
+export interface BasePageInput {
+    /**
+     * 
+     * @type {Search}
+     * @memberof BasePageInput
+     */
+    search?: Search;
+    /**
+     * 模糊查询关键字
+     * @type {string}
+     * @memberof BasePageInput
+     */
+    keyword?: string | null;
+    /**
+     * 
+     * @type {Filter}
+     * @memberof BasePageInput
+     */
+    filter?: Filter;
+    /**
+     * 当前页码
+     * @type {number}
+     * @memberof BasePageInput
+     */
+    page?: number;
+    /**
+     * 页码容量
+     * @type {number}
+     * @memberof BasePageInput
+     */
+    pageSize?: number;
+    /**
+     * 排序字段
+     * @type {string}
+     * @memberof BasePageInput
+     */
+    field?: string | null;
+    /**
+     * 排序方向
+     * @type {string}
+     * @memberof BasePageInput
+     */
+    order?: string | null;
+    /**
+     * 降序排序
+     * @type {string}
+     * @memberof BasePageInput
+     */
+    descStr?: string | null;
+}

+ 32 - 0
Web/src/api-services/models/change-app-input.ts

@@ -0,0 +1,32 @@
+/* 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.
+ */
+/**
+ * 租户iId
+ * @export
+ * @interface ChangeAppInput
+ */
+export interface ChangeAppInput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof ChangeAppInput
+     */
+    id: number;
+    /**
+     * 租户Id
+     * @type {number}
+     * @memberof ChangeAppInput
+     */
+    tenantId: number;
+}

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

@@ -16,6 +16,7 @@ export * from './add-region-input';
 export * from './add-role-input';
 export * from './add-schedule-input';
 export * from './add-subscribe-message-template-input';
+export * from './add-sys-app-input';
 export * from './add-sys-ldap-input';
 export * from './add-tenant-input';
 export * from './add-user-input';
@@ -67,6 +68,7 @@ export * from './admin-result-object';
 export * from './admin-result-sm-key-pair-output';
 export * from './admin-result-sql-sugar-paged-list-job-detail-output';
 export * from './admin-result-sql-sugar-paged-list-open-access-output';
+export * from './admin-result-sql-sugar-paged-list-sys-app-output';
 export * from './admin-result-sql-sugar-paged-list-sys-code-gen';
 export * from './admin-result-sql-sugar-paged-list-sys-config';
 export * from './admin-result-sql-sugar-paged-list-sys-dict-data';
@@ -113,6 +115,8 @@ export * from './admin-result-wx-open-id-output';
 export * from './admin-result-wx-phone-output';
 export * from './api-output';
 export * from './assembly';
+export * from './base-id-input';
+export * from './base-page-input';
 export * from './base-proc-input';
 export * from './batch-config-input';
 export * from './calendar';
@@ -120,6 +124,7 @@ export * from './calendar-algorithm-type';
 export * from './calendar-week-rule';
 export * from './calling-conventions';
 export * from './card-type-enum';
+export * from './change-app-input';
 export * from './change-pwd-input';
 export * from './cluster-status';
 export * from './code-gen-config';
@@ -286,6 +291,7 @@ export * from './sms-verify-code-input';
 export * from './sort-version';
 export * from './sql-sugar-paged-list-job-detail-output';
 export * from './sql-sugar-paged-list-open-access-output';
+export * from './sql-sugar-paged-list-sys-app-output';
 export * from './sql-sugar-paged-list-sys-code-gen';
 export * from './sql-sugar-paged-list-sys-config';
 export * from './sql-sugar-paged-list-sys-dict-data';
@@ -312,6 +318,7 @@ export * from './status-enum';
 export * from './struct-layout-attribute';
 export * from './swagger-submit-url-body';
 export * from './sync-sys-ldap-input';
+export * from './sys-app-output';
 export * from './sys-code-gen';
 export * from './sys-code-gen-config';
 export * from './sys-config';
@@ -364,6 +371,7 @@ export * from './type';
 export * from './type-attributes';
 export * from './type-info';
 export * from './unlock-login-input';
+export * from './update-app-menu-input';
 export * from './update-code-gen-input';
 export * from './update-config-input';
 export * from './update-db-column-input';
@@ -382,6 +390,7 @@ export * from './update-print-input';
 export * from './update-region-input';
 export * from './update-role-input';
 export * from './update-schedule-input';
+export * from './update-sys-app-input';
 export * from './update-sys-ldap-input';
 export * from './update-tenant-input';
 export * from './update-user-input';

+ 6 - 6
Web/src/api-services/models/login-input.ts

@@ -29,6 +29,12 @@ export interface LoginInput {
      * @memberof LoginInput
      */
     password: string;
+    /**
+     * 租户域名
+     * @type {string}
+     * @memberof LoginInput
+     */
+    host: string;
     /**
      * 验证码Id
      * @type {number}
@@ -41,10 +47,4 @@ export interface LoginInput {
      * @memberof LoginInput
      */
     code?: string | null;
-    /**
-     * 租户域名
-     * @type {string}
-     * @memberof LoginInput
-     */
-    host?: string | null;
 }

+ 1 - 1
Web/src/api-services/models/login-phone-input.ts

@@ -34,5 +34,5 @@ export interface LoginPhoneInput {
      * @type {string}
      * @memberof LoginPhoneInput
      */
-    host?: string | null;
+    host: string;
 }

+ 63 - 0
Web/src/api-services/models/sql-sugar-paged-list-sys-app-output.ts

@@ -0,0 +1,63 @@
+/* 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 { SysAppOutput } from './sys-app-output';
+/**
+ * 分页泛型集合
+ * @export
+ * @interface SqlSugarPagedListSysAppOutput
+ */
+export interface SqlSugarPagedListSysAppOutput {
+    /**
+     * 页码
+     * @type {number}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    page?: number;
+    /**
+     * 页容量
+     * @type {number}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    pageSize?: number;
+    /**
+     * 总条数
+     * @type {number}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    total?: number;
+    /**
+     * 总页数
+     * @type {number}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    totalPages?: number;
+    /**
+     * 当前页集合
+     * @type {Array<SysAppOutput>}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    items?: Array<SysAppOutput> | null;
+    /**
+     * 是否有上一页
+     * @type {boolean}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    hasPrevPage?: boolean;
+    /**
+     * 是否有下一页
+     * @type {boolean}
+     * @memberof SqlSugarPagedListSysAppOutput
+     */
+    hasNextPage?: boolean;
+}

+ 128 - 0
Web/src/api-services/models/sys-app-output.ts

@@ -0,0 +1,128 @@
+/* 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 SysAppOutput
+ */
+export interface SysAppOutput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof SysAppOutput
+     */
+    id?: number;
+    /**
+     * 名称
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    name?: string | null;
+    /**
+     * 图标
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    logo?: string | null;
+    /**
+     * 标题
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    title?: string | null;
+    /**
+     * 副标题
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    viceTitle?: string | null;
+    /**
+     * 副描述
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    viceDesc?: string | null;
+    /**
+     * 水印
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    watermark?: string | null;
+    /**
+     * 版权信息
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    copyright?: string | null;
+    /**
+     * ICP备案号
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    icp?: string | null;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof SysAppOutput
+     */
+    orderNo?: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    remark?: string | null;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof SysAppOutput
+     */
+    createTime?: Date | null;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof SysAppOutput
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof SysAppOutput
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof SysAppOutput
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof SysAppOutput
+     */
+    updateUserName?: string | null;
+    /**
+     * 软删除
+     * @type {boolean}
+     * @memberof SysAppOutput
+     */
+    isDelete?: boolean;
+}

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

@@ -68,6 +68,12 @@ export interface TenantOutput {
      * @memberof TenantOutput
      */
     isDelete?: boolean;
+    /**
+     * 应用Id
+     * @type {number}
+     * @memberof TenantOutput
+     */
+    appId?: number;
     /**
      * 用户Id
      * @type {number}
@@ -81,7 +87,7 @@ export interface TenantOutput {
      */
     orgId?: number;
     /**
-     * 主机
+     * 域名
      * @type {string}
      * @memberof TenantOutput
      */
@@ -140,6 +146,12 @@ export interface TenantOutput {
      * @memberof TenantOutput
      */
     name?: string | null;
+    /**
+     * 关联应用名称
+     * @type {string}
+     * @memberof TenantOutput
+     */
+    appName?: string | null;
     /**
      * 管理员账号
      * @type {string}

+ 32 - 0
Web/src/api-services/models/update-app-menu-input.ts

@@ -0,0 +1,32 @@
+/* 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 UpdateAppMenuInput
+ */
+export interface UpdateAppMenuInput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof UpdateAppMenuInput
+     */
+    id: number;
+    /**
+     * 菜单Id集合
+     * @type {Array<number>}
+     * @memberof UpdateAppMenuInput
+     */
+    menuIdList?: Array<number> | null;
+}

+ 86 - 0
Web/src/api-services/models/update-sys-app-input.ts

@@ -0,0 +1,86 @@
+/* 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 UpdateSysAppInput
+ */
+export interface UpdateSysAppInput {
+    /**
+     * 图标
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    logo: string;
+    /**
+     * 名称
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    name: string;
+    /**
+     * 标题
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    title: string;
+    /**
+     * 副标题
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    viceTitle: string;
+    /**
+     * 副描述
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    viceDesc: string;
+    /**
+     * 水印
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    watermark: string;
+    /**
+     * 版权信息
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    copyright: string;
+    /**
+     * ICP备案号
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    icp: string;
+    /**
+     * 排序
+     * @type {number}
+     * @memberof UpdateSysAppInput
+     */
+    orderNo: number;
+    /**
+     * 备注
+     * @type {string}
+     * @memberof UpdateSysAppInput
+     */
+    remark?: string | null;
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof UpdateSysAppInput
+     */
+    id: number;
+}

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

@@ -68,6 +68,12 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     isDelete?: boolean;
+    /**
+     * 应用Id
+     * @type {number}
+     * @memberof UpdateTenantInput
+     */
+    appId: number;
     /**
      * 用户Id
      * @type {number}
@@ -128,6 +134,12 @@ export interface UpdateTenantInput {
      * @memberof UpdateTenantInput
      */
     status?: StatusEnum;
+    /**
+     * 关联应用名称
+     * @type {string}
+     * @memberof UpdateTenantInput
+     */
+    appName?: string | null;
     /**
      * 电子邮箱
      * @type {string}

+ 1 - 0
Web/src/i18n/lang/zh-cn.ts

@@ -152,6 +152,7 @@ export default {
 		threeIsFixedHeader: '固定 Header',
 		threeIsClassicSplitMenu: '经典布局分割菜单',
 		threeIsLockScreen: '开启锁屏',
+		changeApp: '切换应用',
 		threeLockScreenTime: '自动锁屏(s/秒)',
 		fourTitle: '界面显示',
 		fourIsShowLogo: '侧边栏 Logo',

+ 1 - 0
Web/src/i18n/lang/zh-tw.ts

@@ -152,6 +152,7 @@ export default {
 		threeIsFixedHeader: '固定 Header',
 		threeIsClassicSplitMenu: '經典佈局分割選單',
 		threeIsLockScreen: '開啟鎖屏',
+		changeApp: '切换应用',
 		threeLockScreenTime: '自動鎖屏(s/秒)',
 		fourTitle: '介面顯示',
 		fourIsShowLogo: '側邊欄 Logo',

+ 86 - 0
Web/src/layout/navBars/topBar/changeApp.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="sys-app-container">
+    <el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false">
+      <template #header>
+        <div style="color: #fff">
+          <el-icon size="16" style="margin-right: 3px; display: inline; vertical-align: middle"> <ele-Switch /> </el-icon>
+          <span>切换应用</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="id" :rules="[{ required: true, message: '应用不能为空', trigger: 'blur' }]">
+              <el-select v-model="state.ruleForm.id" @change="changeApp" value-key="id" placeholder="应用" class="w100" clearable>
+                <el-option v-for="item in state.appList" :key="item.id" :label="item.label" :value="item.id" />
+              </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="tenantId" :rules="[{ required: true, message: '租户不能为空', trigger: 'blur' }]">
+              <el-select v-model="state.ruleForm.tenantId" value-key="id" placeholder="租户" class="w100" clearable>
+                <el-option v-for="item in state.tenantList" :key="item.id" :label="item.label" :value="item.id" />
+              </el-select>
+            </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="sysEditApp">
+import { reactive, ref } from 'vue';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysAppApi } from '/@/api-services/api';
+import {Local} from "/@/utils/storage";
+import {accessTokenKey, refreshAccessTokenKey} from "/@/utils/request";
+
+const ruleFormRef = ref();
+const state = reactive({
+  loading: false,
+  isShowDialog: false,
+  ruleForm: {} as any,
+  appList: [] as Array<any>,
+  tenantList: [] as Array<any>
+});
+
+// 打开弹窗
+const openDialog = async () => {
+  if (state.appList.length == 0) state.appList = await getAPI(SysAppApi).apiSysAppChangeAppGet().then(res => res.data.result ?? []);
+  ruleFormRef.value?.resetFields();
+  state.isShowDialog = true;
+  state.loading = false;
+};
+
+// 应用id改变事件
+const changeApp = (val: any) => {
+  state.tenantList = state.appList.find(u => u.id === val)?.children ?? [];
+  state.ruleForm.tenantId = undefined;
+}
+
+// 提交
+const submit = () => {
+  ruleFormRef.value.validate(async (valid: boolean) => {
+    if (!valid) return;
+    state.loading = true;
+    const newToken = await getAPI(SysAppApi).apiSysAppChangeAppPost(state.ruleForm).then(res => res.data.result);
+    if (newToken) {
+      Local.set(accessTokenKey, newToken.accessToken);
+      Local.set(refreshAccessTokenKey, newToken.refreshToken);
+      location.reload();
+    }
+    state.loading = false;
+    state.isShowDialog = false;
+  });
+};
+
+// 导出对象
+defineExpose({ openDialog });
+</script>

+ 8 - 2
Web/src/layout/navBars/topBar/user.vue

@@ -75,12 +75,14 @@
 					<!-- <el-dropdown-item command="/dashboard/home">{{ $t('message.user.dropdown1') }}</el-dropdown-item> -->
 					<el-dropdown-item :icon="Avatar" command="/system/userCenter">{{ $t('message.user.dropdown2') }}</el-dropdown-item>
 					<el-dropdown-item :icon="Loading" command="clearCache">{{ $t('message.user.dropdown3') }}</el-dropdown-item>
+					<el-dropdown-item :icon="Switch" divided command="changeApp" v-auth="'sysApp:changeApp'">{{ $t('message.layout.changeApp') }}</el-dropdown-item>
 					<el-dropdown-item :icon="Lock" divided command="lockScreen">{{ $t('message.layout.threeIsLockScreen') }}</el-dropdown-item>
 					<el-dropdown-item :icon="CircleCloseFilled" divided command="logOut">{{ $t('message.user.dropdown5') }}</el-dropdown-item>
 				</el-dropdown-menu>
 			</template>
 		</el-dropdown>
 		<Search ref="searchRef" />
+    <ChangeApp ref="changeAppRef" />
 		<OnlineUser ref="onlineUserRef" />
 	</div>
 </template>
@@ -99,7 +101,7 @@ import mittBus from '/@/utils/mitt';
 import { Local, Session } from '/@/utils/storage';
 import Push from 'push.js';
 import { signalR } from '/@/views/system/onlineUser/signalR';
-import { Avatar, CircleCloseFilled, Loading, Lock } from '@element-plus/icons-vue';
+import { Avatar, CircleCloseFilled, Loading, Lock, Switch } from '@element-plus/icons-vue';
 
 import { clearAccessTokens, getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi, SysNoticeApi } from '/@/api-services/api';
@@ -108,6 +110,7 @@ import { SysAuthApi, SysNoticeApi } from '/@/api-services/api';
 const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/topBar/userNews.vue'));
 const Search = defineAsyncComponent(() => import('/@/layout/navBars/topBar/search.vue'));
 const OnlineUser = defineAsyncComponent(() => import('/@/views/system/onlineUser/index.vue'));
+const ChangeApp = defineAsyncComponent(() => import('./changeApp.vue'));
 
 // 定义变量内容
 const { locale, t } = useI18n();
@@ -117,6 +120,7 @@ const storesThemeConfig = useThemeConfig();
 const { userInfos } = storeToRefs(stores);
 const { themeConfig } = storeToRefs(storesThemeConfig);
 const searchRef = ref();
+const changeAppRef = ref();
 const onlineUserRef = ref();
 const state = reactive({
 	isScreenfull: false,
@@ -191,7 +195,9 @@ const onHandleCommandClick = (path: string) => {
 				clearAccessTokens();
 			})
 			.catch(() => {});
-	} else {
+	} else if (path === 'changeApp') {
+    changeAppRef.value.openDialog();
+  } else {
 		router.push(path);
 	}
 };

+ 2 - 2
Web/src/utils/request.ts

@@ -122,8 +122,8 @@ service.interceptors.response.use(
 		if (accessToken === 'invalid_token') {
 			clearAccessTokens();
 		}
-		// 判断是否存在刷新 token,如果存在则存储在本地
-		else if (refreshAccessToken && accessToken && accessToken !== 'invalid_token') {
+		// 判断是否存在刷新 token,如果存在则存储在本地, 并重新加载页面
+		else if (refreshAccessToken && accessToken) {
 			Local.set(accessTokenKey, accessToken);
 			Local.set(refreshAccessTokenKey, refreshAccessToken);
 		}

+ 207 - 0
Web/src/views/system/app/component/editApp.vue

@@ -0,0 +1,207 @@
+<template>
+	<div class="sys-app-container">
+		<el-dialog v-model="state.isShowDialog" draggable :close-on-click-modal="false">
+			<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="10" :md="10" :lg="10" :xl="10" class="mb20">
+						<el-form-item label="名称" prop="name" :rules="[{ required: true, message: '名称不能为空', trigger: 'blur' }]">
+							<el-input v-model="state.ruleForm.name" maxlength="32" placeholder="应用名称" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+          <el-col :xs="24" :sm="14" :md="14" :lg="14" :xl="14" class="mb20">
+            <el-form-item label="Logo" prop="logo" :rules="[{ required: true, message: '应用Logo不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.logo" class="w80" maxlength="256" placeholder="应用Logo" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="10" :md="10" :lg="10" :xl="10" class="mb20">
+            <el-form-item label="标题" prop="title" :rules="[{ required: true, message: '标题不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.title" placeholder="应用标题" maxlength="32" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="14" :md="14" :lg="14" :xl="14" class="mb20">
+            <el-form-item label="副标题" prop="viceTitle" :rules="[{ required: true, message: '副标题不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.viceTitle" placeholder="应用副标题" maxlength="32" show-word-limit 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="viceDesc" :rules="[{ required: true, message: '副标题描述不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.viceDesc" placeholder="应用副标题描述" maxlength="64" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="24" :lg="24" :xl="24" class="mb20">
+            <el-form-item label="版权信息" prop="copyright" :rules="[{ required: true, message: '版权信息不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.copyright" placeholder="版权信息" maxlength="64" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="9" :md="9" :lg="9" :xl="9" class="mb20">
+            <el-form-item label="备案号" prop="icp" :rules="[{ required: true, message: '备案号不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.icp" placeholder="备案号" maxlength="32" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="9" :md="9" :lg="9" :xl="9" class="mb20">
+            <el-form-item label="水印" prop="watermark" :rules="[{ required: true, message: '水印不能为空', trigger: 'blur' }]">
+              <el-input v-model="state.ruleForm.watermark" placeholder="应用水印" maxlength="32" show-word-limit clearable />
+            </el-form-item>
+          </el-col>
+					<el-col :xs="24" :sm="6" :md="6" :lg="6" :xl="6" 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="请输入备注内容" show-word-limit clearable type="textarea" />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20" v-auth="'sysApp:grantMenu'">
+						<el-form-item label="菜单权限" v-loading="state.loading">
+							<el-tree
+								ref="treeRef"
+								:data="state.menuData"
+								node-key="id"
+								show-checkbox
+								:props="{ children: 'children', label: 'title', class: treeNodeClass }"
+								icon="ele-Menu"
+								highlight-current
+								default-expand-all
+								style="height: 600px;overflow-y: auto;">
+                <template #default="{ node, data }">
+                  <span>{{ node.label }}</span>
+                  <span v-if="data.path" style="margin-left: 5px!important;">
+                    <el-tag effect="plain" type="warning">{{data.path}}</el-tag>
+                  </span>
+                </template>
+              </el-tree>
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="cancel">取 消</el-button>
+					<el-button type="primary" @click="submit">确 定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+
+<script lang="ts" setup name="sysEditApp">
+import { onMounted, reactive, ref } from 'vue';
+import type { ElTree } from 'element-plus';
+
+import { getAPI } from '/@/utils/axios-utils';
+import { SysMenuApi, SysAppApi } from '/@/api-services/api';
+import { SysMenu } from '/@/api-services/models';
+import {auth} from "/@/utils/authFunction";
+
+const props = defineProps({
+	title: String,
+});
+const emits = defineEmits(['handleQuery']);
+const ruleFormRef = ref();
+const treeRef = ref<InstanceType<typeof ElTree>>();
+const state = reactive({
+	loading: false,
+	isShowDialog: false,
+	ruleForm: {} as any,
+	menuData: [] as Array<SysMenu>, // 菜单数据
+});
+
+onMounted(async () => {
+	state.loading = true;
+  if (auth('sysApp:grantMenu')) {
+    const res = await getAPI(SysMenuApi).apiSysMenuListGet(undefined, undefined, true);
+    state.menuData = res.data.result ?? [];
+  }
+	state.loading = false;
+});
+
+// 打开弹窗
+const openDialog = async (row: any) => {
+	ruleFormRef.value?.resetFields();
+	treeRef.value?.setCheckedKeys([]); // 清空选中值
+	state.ruleForm = JSON.parse(JSON.stringify(row));
+	if (row.id && auth('sysApp:grantMenu')) {
+    const res = await getAPI(SysAppApi).apiSysAppGrantMenuGet(row.id);
+    treeRef.value?.setCheckedKeys(res.data.result ?? []);
+	}
+	state.isShowDialog = true;
+};
+
+// 关闭弹窗
+const closeDialog = () => {
+	emits('handleQuery');
+	state.isShowDialog = false;
+};
+
+// 取消
+const cancel = () => {
+	state.isShowDialog = false;
+};
+
+// 提交
+const submit = () => {
+	ruleFormRef.value.validate(async (valid: boolean) => {
+		if (!valid) return;
+		const menuIdList = treeRef.value?.getCheckedKeys() as Array<number> ?? []; //.concat(treeRef.value?.getHalfCheckedKeys());
+		if (state.ruleForm.id != undefined && state.ruleForm.id > 0) {
+			await getAPI(SysAppApi).apiSysAppUpdatePost(state.ruleForm);
+		} else {
+      state.ruleForm.id = await getAPI(SysAppApi).apiSysAppAddPost(state.ruleForm).then(res => res.data.result);
+		}
+    if (state.ruleForm.id && auth('sysApp:grantMenu')) {
+      await getAPI(SysAppApi).apiSysAppGrantMenuPost({ id: state.ruleForm.id, menuIdList: menuIdList });
+    }
+		closeDialog();
+	});
+};
+
+// 叶子节点同行显示样式
+const treeNodeClass = (node: SysMenu) => {
+	let addClass = true; // 添加叶子节点同行显示样式
+	for (const index in node.children) {
+		// 如果存在子节点非叶子节点,不添加样式
+		if (node.children[index].children?.length ?? 0 > 0) {
+			addClass = false;
+			break;
+		}
+	}
+	return addClass ? 'penultimate-node' : '';
+};
+
+// 导出对象
+defineExpose({ openDialog });
+</script>
+
+<style lang="scss" scoped>
+.menu-data-tree {
+	width: 100%;
+	border: 1px solid var(--el-border-color);
+	border-radius: var(--el-input-border-radius, var(--el-border-radius-base));
+	padding: 5px;
+}
+
+:deep(.penultimate-node) {
+	.el-tree-node__children {
+		padding-left: 40px;
+		white-space: pre-wrap;
+		line-height: 100%;
+
+		.el-tree-node {
+			display: inline-block;
+		}
+
+		.el-tree-node__content {
+			padding-left: 5px !important;
+			padding-right: 5px;
+		}
+	}
+}
+</style>

+ 147 - 0
Web/src/views/system/app/index.vue

@@ -0,0 +1,147 @@
+<template>
+	<div class="sys-app-container">
+		<el-card shadow="hover" :body-style="{ paddingBottom: '0' }">
+			<el-form :model="state.queryParams" ref="queryForm" :inline="true">
+				<el-form-item label="关键字">
+					<el-input v-model="state.queryParams.keyword" placeholder="关键字" clearable />
+				</el-form-item>
+				<el-form-item>
+					<el-button-group>
+						<el-button type="primary" icon="ele-Search" @click="handleQuery" v-auth="'sysApp:page'"> 查询 </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="openAddApp" v-auth="'sysApp: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.appData" style="width: 100%" v-loading="state.loading" border>
+				<el-table-column type="index" label="序号" width="55" align="center" fixed />
+        <el-table-column prop="logo" label="图标" width="55" align="center" show-overflow-tooltip>
+          <template #default="scope">
+            <el-avatar shape="square" :src="scope.row.logo" size="small" />
+          </template>
+        </el-table-column>
+				<el-table-column prop="name" label="名称" width="180" align="center" show-overflow-tooltip />
+				<el-table-column prop="title" label="标题" width="180" align="center" show-overflow-tooltip />
+				<el-table-column prop="viceTitle" label="副标题" width="180" align="center" show-overflow-tooltip />
+				<el-table-column prop="viceDesc" label="描述" width="300" align="center" show-overflow-tooltip />
+				<el-table-column prop="watermark" label="水印" width="130" align="center" show-overflow-tooltip />
+				<el-table-column prop="copyright" label="版权信息" width="350" align="center" show-overflow-tooltip />
+				<el-table-column prop="icp" label="备案号" width="130" align="center" show-overflow-tooltip />
+				<el-table-column prop="orderNo" label="排序" width="70" align="center" 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="130" fixed="right" align="center" show-overflow-tooltip>
+					<template #default="scope">
+						<el-button icon="ele-Edit" size="small" text type="primary" @click="openEditApp(scope.row)" v-auth="'sysApp:update'"> 编辑 </el-button>
+						<el-button icon="ele-Delete" size="small" text type="danger" @click="delApp(scope.row)" v-auth="'sysApp:delete'"> 删除 </el-button>
+					</template>
+				</el-table-column>
+			</el-table>
+			<el-pagination
+				v-model:currentPage="state.tableParams.page"
+				v-model:page-size="state.tableParams.pageSize"
+				:total="state.tableParams.total"
+				:page-sizes="[10, 20, 50, 100]"
+				size="small"
+				background
+				@size-change="handleSizeChange"
+				@current-change="handleCurrentChange"
+				layout="total, sizes, prev, pager, next, jumper"
+			/>
+		</el-card>
+
+		<EditApp ref="editAppRef" :title="state.editAppTitle" @handleQuery="handleQuery" />
+	</div>
+</template>
+
+<script lang="ts" setup name="sysApp">
+import { onMounted, reactive, ref } from 'vue';
+import { ElMessageBox, ElMessage } from 'element-plus';
+import EditApp from '/@/views/system/app/component/editApp.vue';
+import ModifyRecord from '/@/components/table/modifyRecord.vue';
+
+import { getAPI } from '/@/utils/axios-utils';
+import { SysAppApi } from '/@/api-services/api';
+
+const editAppRef = ref<InstanceType<typeof EditApp>>();
+const state = reactive({
+	loading: false,
+	appData: [] as any[],
+	queryParams: {
+		keyword: undefined,
+	},
+	tableParams: {
+		page: 1,
+		pageSize: 50,
+		total: 0 as any,
+	},
+	editAppTitle: '',
+});
+
+onMounted(async () => {
+	await handleQuery();
+});
+
+// 查询操作
+const handleQuery = async () => {
+	state.loading = true;
+	let params = Object.assign(state.queryParams, state.tableParams);
+	let res = await getAPI(SysAppApi).apiSysAppPagePost(params);
+	state.appData = res.data.result?.items ?? [];
+	state.tableParams.total = res.data.result?.total;
+	state.loading = false;
+};
+
+// 重置操作
+const resetQuery = async () => {
+	state.queryParams.keyword = undefined;
+	await handleQuery();
+};
+
+// 打开新增页面
+const openAddApp = () => {
+	state.editAppTitle = '添加应用';
+	editAppRef.value?.openDialog({ id: undefined, status: 1, orderNo: 100 });
+};
+
+// 打开编辑页面
+const openEditApp = async (row: any) => {
+	state.editAppTitle = '编辑应用';
+	editAppRef.value?.openDialog(row);
+};
+
+// 删除
+const delApp = (row: any) => {
+	ElMessageBox.confirm(`确定删除应用:【${row.name}】?`, '提示', {
+		confirmButtonText: '确定',
+		cancelButtonText: '取消',
+		type: 'warning',
+	})
+		.then(async () => {
+			await getAPI(SysAppApi).apiSysAppDeletePost({ id: row.id });
+			await handleQuery();
+			ElMessage.success('删除成功');
+		})
+		.catch(() => {});
+};
+
+// 改变页面容量
+const handleSizeChange = async (val: number) => {
+	state.tableParams.pageSize = val;
+	await handleQuery();
+};
+
+// 改变页码序号
+const handleCurrentChange = async (val: number) => {
+	state.tableParams.page = val;
+	await handleQuery();
+};
+</script>

+ 11 - 2
Web/src/views/system/tenant/component/editTenant.vue

@@ -9,6 +9,13 @@
 			</template>
 			<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto">
 				<el-row :gutter="35">
+          <el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+            <el-form-item label="应用" prop="appId" :rules="[{ required: true, message: '应用不能为空', trigger: 'blur' }]">
+              <el-select v-model="state.ruleForm.appId" :disabled="(state.ruleForm?.id ?? 0) > 0" value-key="appId" placeholder="应用" class="w100" clearable>
+                <el-option v-for="item in state.appList" :key="item.id" :label="item.label" :value="item.id" />
+              </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="租户类型" :rules="[{ required: true, message: '租户类型不能为空', trigger: 'blur' }]">
 							<el-radio-group v-model="state.ruleForm.tenantType" :disabled="state.ruleForm.id != undefined">
@@ -120,7 +127,7 @@ import { reactive, ref } from 'vue';
 
 import {useUserInfo} from "/@/stores/userInfo";
 import { getAPI } from '/@/utils/axios-utils';
-import { SysTenantApi } from '/@/api-services/api';
+import {SysAppApi, SysTenantApi} from '/@/api-services/api';
 import { UpdateTenantInput } from '/@/api-services/models';
 
 const getDictDataByCode = useUserInfo().getDictDataByCode;
@@ -131,11 +138,13 @@ const emits = defineEmits(['handleQuery']);
 const ruleFormRef = ref();
 const state = reactive({
 	isShowDialog: false,
+  appList: [] as Array<any>,
 	ruleForm: {} as UpdateTenantInput,
 });
 
 // 打开弹窗
-const openDialog = (row: any) => {
+const openDialog = async (row: any) => {
+  if (state.appList.length == 0) state.appList = await getAPI(SysAppApi).apiSysAppChangeAppGet().then(res => res.data.result ?? []);
 	state.ruleForm = JSON.parse(JSON.stringify(row));
 	state.isShowDialog = true;
 	ruleFormRef.value?.resetFields();

+ 3 - 8
Web/src/views/system/tenant/component/grantMenu.vue

@@ -49,23 +49,18 @@ const state = reactive({
 	isShowDialog: false,
 	ruleForm: {
 		id: 0,
+    appId: 0,
 		menuIdList: [] as any, // 菜单集合
 	},
 	menuData: [] as any, // 菜单数据
 });
 
-onMounted(async () => {
-	state.loading = true;
-	var res = await getAPI(SysMenuApi).apiSysMenuListGet();
-	state.menuData = res.data.result;
-	state.loading = false;
-});
-
 // 打开弹窗
 const openDialog = async (row: any) => {
 	treeRef.value?.setCheckedKeys([]); // 先清空已选择节点
 	state.ruleForm = row;
-	var res = await getAPI(SysTenantApi).apiSysTenantOwnMenuListGet(row.userId);
+  state.menuData = await getAPI(SysMenuApi).apiSysMenuListGet(undefined,undefined, undefined, state.ruleForm.appId).then(res => res.data.result);
+	const res = await getAPI(SysTenantApi).apiSysTenantOwnMenuListGet(row.userId);
 	setTimeout(() => {
 		// 延迟传递数据
 		treeRef.value?.setCheckedKeys(res.data.result);

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

@@ -24,6 +24,7 @@
 			<el-table :data="state.tenantData" 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="租户名称" width="160" align="center" show-overflow-tooltip />
+        <el-table-column prop="appName" label="关联应用" width="160" align="center" show-overflow-tooltip />
 				<el-table-column prop="adminAccount" label="租管账号" align="center" width="120" show-overflow-tooltip />
 				<el-table-column prop="phone" label="电话" width="120" align="center" show-overflow-tooltip />
 				<el-table-column prop="host" label="域名" width="150" show-overflow-tooltip />