Procházet zdrojové kódy

1.添加多语言翻译表
2.用户基本信息添加语言设置
3.系统菜单实现多语言翻译设置和输出。
4.客户端添加全局多语言组件g-multi-lang-Input
使用方式:<g-multi-lang-Input entityName="SysMenu" fieldName="Title" :entityId="state.ruleForm.id" v-model="state.ruleForm.title" placeholder="菜单名称" clearable />

Cyrus Zhou před 9 měsíci
rodič
revize
e7f0853f71
34 změnil soubory, kde provedl 2760 přidání a 25 odebrání
  1. 5 0
      Admin.NET/Admin.NET.Core/Const/ClaimConst.cs
  2. 44 0
      Admin.NET/Admin.NET.Core/Entity/SysLangText.cs
  3. 5 0
      Admin.NET/Admin.NET.Core/Entity/SysUser.cs
  4. 39 0
      Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs
  5. 1 1
      Admin.NET/Admin.NET.Core/SeedData/SysLangSeedData.cs
  6. 8 1
      Admin.NET/Admin.NET.Core/SeedData/SysMenuSeedData.cs
  7. 5 0
      Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs
  8. 3 1
      Admin.NET/Admin.NET.Core/Service/Auth/SysAuthService.cs
  9. 2 0
      Admin.NET/Admin.NET.Core/Service/Lang/SysLangService.cs
  10. 74 0
      Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs
  11. 261 0
      Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs
  12. 83 0
      Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs
  13. 261 0
      Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextService.cs
  14. 81 15
      Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs
  15. 2 0
      Admin.NET/Admin.NET.Core/Service/User/UserManager.cs
  16. 3 1
      Web/src/App.vue
  17. 1 0
      Web/src/api-services/api.ts
  18. 907 0
      Web/src/api-services/apis/sys-lang-text-api.ts
  19. 57 0
      Web/src/api-services/models/admin-result-list-sys-lang-text-output.ts
  20. 57 0
      Web/src/api-services/models/admin-result-sys-lang-text.ts
  21. 63 0
      Web/src/api-services/models/sql-sugar-paged-list-sys-lang-text-output.ts
  22. 26 0
      Web/src/api-services/models/sys-lang-text-import-body.ts
  23. 92 0
      Web/src/api-services/models/sys-lang-text-output.ts
  24. 92 0
      Web/src/api-services/models/sys-lang-text.ts
  25. 54 0
      Web/src/api/system/sysLangText.ts
  26. 154 0
      Web/src/components/multiLangInput/index.vue
  27. 3 2
      Web/src/layout/navBars/topBar/user.vue
  28. 3 0
      Web/src/main.ts
  29. 17 0
      Web/src/stores/useLangStore.ts
  30. 121 0
      Web/src/views/system/langText/component/editDialog.vue
  31. 206 0
      Web/src/views/system/langText/index.vue
  32. 2 2
      Web/src/views/system/menu/component/editMenu.vue
  33. 14 1
      Web/src/views/system/user/component/editUser.vue
  34. 14 1
      Web/src/views/system/user/component/userCenter.vue

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

@@ -65,4 +65,9 @@ public class ClaimConst
     /// 登录模式PC、APP
     /// </summary>
     public const string LoginMode = "LoginMode";
+
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public const string LangCode = "LangCode";
 }

+ 44 - 0
Admin.NET/Admin.NET.Core/Entity/SysLangText.cs

@@ -0,0 +1,44 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+[SugarTable(null, "翻译表")]
+[SysTable]
+[SugarIndex("index_{table}_N", nameof(EntityName), OrderByType.Asc)]
+[SugarIndex("index_{table}_F", nameof(FieldName), OrderByType.Asc)]
+public class SysLangText : EntityBase
+{
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    [SugarColumn( ColumnDescription = "所属实体名")]
+    public string EntityName { get; set; }
+
+    /// <summary>
+    /// 语言代码(如 zh_CN)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "所属实体ID")]
+    public long EntityId { get; set; }
+
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    [SugarColumn(ColumnDescription = "字段名")]
+    public string FieldName { get; set; }
+
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    [SugarColumn(ColumnDescription = "语言代码")]
+    public string LangCode { get; set; }
+
+    /// <summary>
+    /// 多语言内容
+    /// </summary>
+    [SugarColumn(ColumnDescription = "翻译内容")]
+    public string Content { get; set; }
+}

+ 5 - 0
Admin.NET/Admin.NET.Core/Entity/SysUser.cs

@@ -313,6 +313,11 @@ public partial class SysUser : EntityBaseTenantOrg
     [MaxLength(512)]
     public string? Signature { get; set; }
 
+    /// <summary>
+    /// 语言代码(如 zh_CN)
+    /// </summary>
+    [SugarColumn(ColumnDescription = "语言代码")]
+    public string LangCode { get; set; } = "zh_CN";
     /// <summary>
     /// 验证超级管理员类型,若账号类型为超级管理员则报错
     /// </summary>

+ 39 - 0
Admin.NET/Admin.NET.Core/Extension/SqlSugarExtension.cs

@@ -324,4 +324,43 @@ public static class SqlSugarExtension
     }
 
     #endregion 视图操作
+    /// <summary>
+    /// 列表转换为树形结构
+    /// </summary>
+    /// <typeparam name="T"></typeparam>
+    /// <param name="source">列表数据</param>
+    /// <param name="childrenSelector">设置子节点列表。例如:item => item.Children</param>
+    /// <param name="parentIdSelector">设置元素的父级 Id。例如:item => item.ParentId</param>
+    /// <param name="rootParentId">根节点的父级 Id,默认为 0 </param>
+    /// <returns></returns>
+    /// <exception cref="Exception"></exception>
+    public static IEnumerable<T> ToTree<T>(
+       this IEnumerable<T> source,
+       Func<T, List<T>> childrenSelector,
+       Func<T, long> parentIdSelector,
+       int rootParentId)
+       where T : class
+    {
+        var lookup = source.ToLookup(parentIdSelector);
+        List<T> BuildTree(long parentId)
+        {
+            return lookup[parentId].Select(item =>
+            {
+                var children = BuildTree(GetId(item));
+                childrenSelector(item).Clear();
+                childrenSelector(item).AddRange(children);
+                return item;
+            }).ToList();
+        }
+
+        // 需要提供获取Id的方法,可以用反射或者自己传参数
+        long GetId(T item)
+        {
+            var prop = typeof(T).GetProperty("Id");
+            if (prop == null) throw new Exception("没有找到Id属性");
+            return (long)prop.GetValue(item);
+        }
+
+        return BuildTree(rootParentId);
+    }
 }

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

@@ -25,7 +25,7 @@ public class SysLangSeedData : ISqlSugarEntitySeedData<SysLang>
             new SysLang{ Id=1300000000002,Name="Chinese (HK) / 繁體中文", Code="zh_HK", IsoCode="zh_HK", UrlCode="zh-hk", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日 %A", TimeFormat="%I時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true},
             new SysLang{ Id=1300000000003,Name="Chinese (Traditional) / 繁體中文", Code="zh_TW", IsoCode="zh_TW", UrlCode="zh-tw", Direction=DirectionEnum.Ltr, DateFormat="%Y年%m月%d日", TimeFormat="%H時%M分%S秒",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true},
             new SysLang{ Id=1300000000004,Name="Italian / Italiano", Code="it_IT", IsoCode="it", UrlCode="it", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Monday,Grouping="[3,0]",DecimalPoint=",",ThousandsSep=".",Active=true},
-            new SysLang{ Id=1300000000005,Name="English (US)", Code="en_US", IsoCode="en", UrlCode="en", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false},
+            new SysLang{ Id=1300000000005,Name="English (US)", Code="en_US", IsoCode="en", UrlCode="en", Direction=DirectionEnum.Ltr, DateFormat="%m/%d/%Y", TimeFormat="%H:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=true},
             new SysLang{ Id=1300000000006,Name="Amharic / አምሃርኛ", Code="am_ET", IsoCode="am_ET", UrlCode="am-et", Direction=DirectionEnum.Ltr, DateFormat="%d/%m/%Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Sunday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false},
             new SysLang{ Id=1300000000007,Name="Arabic / الْعَرَبيّة", Code="ar_001", IsoCode="ar", UrlCode="ar", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false},
             new SysLang{ Id=1300000000008,Name="Arabic (Syria) / الْعَرَبيّة", Code="ar_SY", IsoCode="ar_SY", UrlCode="ar-sy", Direction=DirectionEnum.Rtl, DateFormat="%d %b, %Y", TimeFormat="%I:%M:%S",WeekStart=WeekEnum.Saturday,Grouping="[3,0]",DecimalPoint=".",ThousandsSep=",",Active=false},

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

@@ -174,12 +174,19 @@ public class SysMenuSeedData : ISqlSugarEntitySeedData<SysMenu>
             new SysMenu{ Id=1300200090501, Pid=1300200090101, Title="删除", Permission="sysTenantConfig:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             
             // 语言管理
-            new SysMenu{ Id=1300200100101, Pid=1300200000101, Title="语言管理", Path="/system/lang", Name="sysLang", Component="/system/lang/index", Icon="iconfont icon-zhongyingwen", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=190 },
+            new SysMenu{ Id=1300200100101, Pid=1300200000101, Title="语言管理", Path="/system/lang", Name="sysLang", Component="/system/lang/index", Icon="iconfont icon-diqiu", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=190 },
             new SysMenu{ Id=1300200100201, Pid=1300200100101, Title="查询", Permission="sysLang:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300200100301, Pid=1300200100101, Title="编辑", Permission="sysLang:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300200100401, Pid=1300200100101, Title="增加", Permission="sysLang:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
             new SysMenu{ Id=1300200100501, Pid=1300200100101, Title="删除", Permission="sysLang:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
 
+            // 翻译管理
+            new SysMenu{ Id=1300200200101, Pid=1300200000101, Title="翻译管理", Path="/system/langText", Name="sysLangText", Component="/system/langText/index", Icon="iconfont icon-zhongyingwen", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2025-06-28 00:00:00"), OrderNo=200 },
+            new SysMenu{ Id=1300200200201, Pid=1300200200101, Title="查询", Permission="sysLangText:page", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300200200301, Pid=1300200200101, Title="编辑", Permission="sysLangText:update", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300200200401, Pid=1300200200101, Title="增加", Permission="sysLangText:add", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+            new SysMenu{ Id=1300200200501, Pid=1300200200101, Title="删除", Permission="sysLangText:delete", Type=MenuTypeEnum.Btn, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=100 },
+
             // 系统监控
             new SysMenu{ Id=1300300070101, Pid=1300300000101, Title="系统监控", Path="/platform/server", Name="sysServer", Component="/system/server/index", Icon="ele-Monitor", Type=MenuTypeEnum.Menu, CreateTime=DateTime.Parse("2022-02-10 00:00:00"), OrderNo=150 },
 

+ 5 - 0
Admin.NET/Admin.NET.Core/Service/Auth/Dto/LoginUserOutput.cs

@@ -110,4 +110,9 @@ public class LoginUserOutput
     /// 当前切换到的租户Id
     /// </summary>
     public long? CurrentTenantId { get; set; }
+
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LangCode { get; internal set; }
 }

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

@@ -244,6 +244,7 @@ public class SysAuthService : IDynamicApiController, ITransient
             { ClaimConst.OrgId, user.OrgId },
             { ClaimConst.OrgName, user.SysOrg?.Name },
             { ClaimConst.OrgType, user.SysOrg?.Type },
+            { ClaimConst.LangCode, user.LangCode }
         }, tokenExpire);
 
         // 生成刷新Token令牌
@@ -323,7 +324,8 @@ public class SysAuthService : IDynamicApiController, ITransient
             Buttons = buttons,
             RoleIds = roleIds,
             TenantId = user.TenantId,
-            WatermarkText = watermarkText
+            WatermarkText = watermarkText,
+            LangCode = user.LangCode,
         };
 
         //将登录信息中的当前租户id,更新为当前所切换到的租户

+ 2 - 0
Admin.NET/Admin.NET.Core/Service/Lang/SysLangService.cs

@@ -95,6 +95,7 @@ public partial class SysLangService : IDynamicApiController, ITransient
     /// 获取下拉列表数据 🔖
     /// </summary>
     /// <returns></returns>
+    [AllowAnonymous]
     [DisplayName("获取下拉列表数据")]
     [ApiDescriptionSettings(Name = "DropdownData"), HttpPost]
     public async Task<dynamic> DropdownData()
@@ -103,6 +104,7 @@ public partial class SysLangService : IDynamicApiController, ITransient
             .Where(m => m.Active == true)
             .Select(u => new
             {
+                Code = u.Code,
                 Value = u.UrlCode,
                 Label = $"{u.Name}"
             }).ToListAsync();

+ 74 - 0
Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextDto.cs

@@ -0,0 +1,74 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 翻译表输出参数
+/// </summary>
+public class SysLangTextDto
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    public long Id { get; set; }
+    
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    public string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    public long EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    public string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    public string Content { 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; }
+    
+}

+ 261 - 0
Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextInput.cs

@@ -0,0 +1,261 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 翻译表基础输入参数
+/// </summary>
+public class SysLangTextBaseInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    public virtual long? Id { get; set; }
+    
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    [Required(ErrorMessage = "所属实体名不能为空")]
+    public virtual string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    [Required(ErrorMessage = "所属实体ID不能为空")]
+    public virtual long? EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    [Required(ErrorMessage = "字段名不能为空")]
+    public virtual string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    [Required(ErrorMessage = "语言代码不能为空")]
+    public virtual string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    [Required(ErrorMessage = "翻译内容不能为空")]
+    public virtual string Content { get; set; }
+    
+}
+
+/// <summary>
+/// 翻译表分页查询输入参数
+/// </summary>
+public class PageSysLangTextInput : BasePageInput
+{
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    public string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    public long? EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    public string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    public string Content { get; set; }
+    
+    /// <summary>
+    /// 选中主键列表
+    /// </summary>
+     public List<long> SelectKeyList { get; set; }
+}
+
+/// <summary>
+/// 翻译表增加输入参数
+/// </summary>
+public class AddSysLangTextInput
+{
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    [Required(ErrorMessage = "所属实体名不能为空")]
+    [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")]
+    public string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    [Required(ErrorMessage = "所属实体ID不能为空")]
+    public long? EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    [Required(ErrorMessage = "字段名不能为空")]
+    [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")]
+    public string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    [Required(ErrorMessage = "语言代码不能为空")]
+    [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
+    public string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    [Required(ErrorMessage = "翻译内容不能为空")]
+    public string Content { get; set; }
+    
+}
+/// <summary>
+/// 翻译表输入参数
+/// </summary>
+public class ListSysLangTextInput
+{
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    [Required(ErrorMessage = "所属实体名不能为空")]
+    public string EntityName { get; set; }
+
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    [Required(ErrorMessage = "所属实体ID不能为空")]
+    public long? EntityId { get; set; }
+
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    [Required(ErrorMessage = "字段名不能为空")]
+    public string FieldName { get; set; }
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LangCode { get; set; }
+
+}
+/// <summary>
+/// 翻译表删除输入参数
+/// </summary>
+public class DeleteSysLangTextInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    [Required(ErrorMessage = "主键Id不能为空")]
+    public long? Id { get; set; }
+    
+}
+
+/// <summary>
+/// 翻译表更新输入参数
+/// </summary>
+public class UpdateSysLangTextInput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>    
+    [Required(ErrorMessage = "主键Id不能为空")]
+    public long? Id { get; set; }
+    
+    /// <summary>
+    /// 所属实体名
+    /// </summary>    
+    [Required(ErrorMessage = "所属实体名不能为空")]
+    [MaxLength(255, ErrorMessage = "所属实体名字符长度不能超过255")]
+    public string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>    
+    [Required(ErrorMessage = "所属实体ID不能为空")]
+    public long? EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>    
+    [Required(ErrorMessage = "字段名不能为空")]
+    [MaxLength(255, ErrorMessage = "字段名字符长度不能超过255")]
+    public string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>    
+    [Required(ErrorMessage = "语言代码不能为空")]
+    [MaxLength(255, ErrorMessage = "语言代码字符长度不能超过255")]
+    public string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>    
+    [Required(ErrorMessage = "翻译内容不能为空")]
+    public string Content { get; set; }
+    
+}
+
+/// <summary>
+/// 翻译表主键查询输入参数
+/// </summary>
+public class QueryByIdSysLangTextInput : DeleteSysLangTextInput
+{
+}
+
+/// <summary>
+/// 翻译表数据导入实体
+/// </summary>
+[ExcelImporter(SheetIndex = 1, IsOnlyErrorRows = true)]
+public class ImportSysLangTextInput : BaseImportInput
+{
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    [ImporterHeader(Name = "*所属实体名")]
+    [ExporterHeader("*所属实体名", Format = "", Width = 25, IsBold = true)]
+    public string EntityName { get; set; }
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    [ImporterHeader(Name = "*所属实体ID")]
+    [ExporterHeader("*所属实体ID", Format = "", Width = 25, IsBold = true)]
+    public long? EntityId { get; set; }
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    [ImporterHeader(Name = "*字段名")]
+    [ExporterHeader("*字段名", Format = "", Width = 25, IsBold = true)]
+    public string FieldName { get; set; }
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    [ImporterHeader(Name = "*语言代码")]
+    [ExporterHeader("*语言代码", Format = "", Width = 25, IsBold = true)]
+    public string LangCode { get; set; }
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    [ImporterHeader(Name = "*翻译内容")]
+    [ExporterHeader("*翻译内容", Format = "", Width = 25, IsBold = true)]
+    public string Content { get; set; }
+    
+}

+ 83 - 0
Admin.NET/Admin.NET.Core/Service/LangText/Dto/SysLangTextOutput.cs

@@ -0,0 +1,83 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+namespace Admin.NET.Core;
+
+/// <summary>
+/// 翻译表输出参数
+/// </summary>
+public class SysLangTextOutput
+{
+    /// <summary>
+    /// 主键Id
+    /// </summary>
+    public long Id { get; set; }    
+    
+    /// <summary>
+    /// 所属实体名
+    /// </summary>
+    public string EntityName { get; set; }    
+    
+    /// <summary>
+    /// 所属实体ID
+    /// </summary>
+    public long EntityId { get; set; }    
+    
+    /// <summary>
+    /// 字段名
+    /// </summary>
+    public string FieldName { get; set; }    
+    
+    /// <summary>
+    /// 语言代码
+    /// </summary>
+    public string LangCode { get; set; }    
+    
+    /// <summary>
+    /// 翻译内容
+    /// </summary>
+    public string Content { 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 class ExportSysLangTextOutput : ImportSysLangTextInput
+{
+    [ImporterHeader(IsIgnore = true)]
+    [ExporterHeader(IsIgnore = true)]
+    public override string Error { get; set; }
+}

+ 261 - 0
Admin.NET/Admin.NET.Core/Service/LangText/SysLangTextService.cs

@@ -0,0 +1,261 @@
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 翻译服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 100, Description = "翻译服务")]
+public partial class SysLangTextService : IDynamicApiController, ITransient
+{
+    private readonly SqlSugarRepository<SysLangText> _sysLangTextRep;
+    private readonly ISqlSugarClient _sqlSugarClient;
+
+    public SysLangTextService(SqlSugarRepository<SysLangText> sysLangTextRep, ISqlSugarClient sqlSugarClient)
+    {
+        _sysLangTextRep = sysLangTextRep;
+        _sqlSugarClient = sqlSugarClient;
+    }
+
+    /// <summary>
+    /// 分页查询翻译表 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("分页查询翻译表")]
+    [ApiDescriptionSettings(Name = "Page"), HttpPost]
+    public async Task<SqlSugarPagedList<SysLangTextOutput>> Page(PageSysLangTextInput input)
+    {
+        input.Keyword = input.Keyword?.Trim();
+        var query = _sysLangTextRep.AsQueryable()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Keyword), u => u.EntityName.Contains(input.Keyword) || u.FieldName.Contains(input.Keyword) || u.LangCode.Contains(input.Keyword) || u.Content.Contains(input.Keyword))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.EntityName), u => u.EntityName.Contains(input.EntityName.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.FieldName), u => u.FieldName.Contains(input.FieldName.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode.Contains(input.LangCode.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.Content), u => u.Content.Contains(input.Content.Trim()))
+            .WhereIF(input.EntityId != null, u => u.EntityId == input.EntityId)
+            .Select<SysLangTextOutput>();
+        return await query.OrderBuilder(input).ToPagedListAsync(input.Page, input.PageSize);
+    }
+    [DisplayName("获取翻译表")]
+    [ApiDescriptionSettings(Name = "List"), HttpPost]
+    public async Task<List<SysLangTextOutput>> List(ListSysLangTextInput input)
+    {
+        var query = _sysLangTextRep.AsQueryable()
+            .Where(u => u.EntityName == input.EntityName.Trim() && u.FieldName == input.FieldName.Trim() && u.EntityId == input.EntityId)
+            .WhereIF(!string.IsNullOrWhiteSpace(input.LangCode), u => u.LangCode == input.LangCode.Trim())
+            .Select<SysLangTextOutput>();
+        return await query.ToListAsync();
+    }
+    /// <summary>
+    /// 获取翻译表详情 ℹ️
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取翻译表详情")]
+    [ApiDescriptionSettings(Name = "Detail"), HttpGet]
+    public async Task<SysLangText> Detail([FromQuery] QueryByIdSysLangTextInput input)
+    {
+        return await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id);
+    }
+
+    /// <summary>
+    /// 增加翻译表 ➕
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("增加翻译表")]
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    public async Task<long> Add(AddSysLangTextInput input)
+    {
+        var entity = input.Adapt<SysLangText>();
+        return await _sysLangTextRep.InsertAsync(entity) ? entity.Id : 0;
+    }
+
+    /// <summary>
+    /// 更新翻译表 ✏️
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("更新翻译表")]
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    public async Task Update(UpdateSysLangTextInput input)
+    {
+        var entity = input.Adapt<SysLangText>();
+        await _sysLangTextRep.AsUpdateable(entity)
+        .ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 删除翻译表 ❌
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("删除翻译表")]
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    public async Task Delete(DeleteSysLangTextInput input)
+    {
+        var entity = await _sysLangTextRep.GetFirstAsync(u => u.Id == input.Id) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        await _sysLangTextRep.DeleteAsync(entity);   //真删除
+    }
+
+    /// <summary>
+    /// 批量删除翻译表 ❌
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("批量删除翻译表")]
+    [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
+    public async Task BatchDelete([Required(ErrorMessage = "主键列表不能为空")] List<DeleteSysLangTextInput> input)
+    {
+        var exp = Expressionable.Create<SysLangText>();
+        foreach (var row in input) exp = exp.Or(it => it.Id == row.Id);
+        var list = await _sysLangTextRep.AsQueryable().Where(exp.ToExpression()).ToListAsync();
+
+        await _sysLangTextRep.DeleteAsync(list);   //真删除
+    }
+
+    private static readonly object _sysLangTextBatchSaveLock = new object();
+    /// <summary>
+    /// 批量保存翻译表 ✏️
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("批量保存翻译表")]
+    [ApiDescriptionSettings(Name = "BatchSave"), HttpPost]
+    public void BatchSave([Required(ErrorMessage = "列表不能为空")] List<ImportSysLangTextInput> input)
+    {
+        lock (_sysLangTextBatchSaveLock)
+        {
+            // 校验并过滤必填基本类型为null的字段
+            var rows = input.Where(x =>
+            {
+                if (!string.IsNullOrWhiteSpace(x.Error)) return false;
+                if (x.EntityId == null)
+                {
+                    x.Error = "所属实体ID不能为空";
+                    return false;
+                }
+                return true;
+            }).Adapt<List<SysLangText>>();
+
+            var storageable = _sysLangTextRep.Context.Storageable(rows)
+                .SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空")
+                .SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符")
+                .SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空")
+                .SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符")
+                .SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空")
+                .SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符")
+                .SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空")
+                .WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode })
+                .SplitInsert(it => it.NotAny())
+                .SplitUpdate(it => it.Any())
+                .ToStorage();
+
+            storageable.AsInsertable.ExecuteCommand();// 不存在插入
+            storageable.AsUpdateable.UpdateColumns(it => new
+            {
+                it.EntityName,
+                it.EntityId,
+                it.FieldName,
+                it.LangCode,
+                it.Content,
+            }).ExecuteCommand();// 存在更新
+            if (storageable.ErrorList.Any())
+            {
+                throw Oops.Oh($"处理过程中出现以下错误:{string.Join(";", storageable.ErrorList.Distinct())}");
+            }
+        }
+    }
+
+    /// <summary>
+    /// 导出翻译表记录 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("导出翻译表记录")]
+    [ApiDescriptionSettings(Name = "Export"), HttpPost, NonUnify]
+    public async Task<IActionResult> Export(PageSysLangTextInput input)
+    {
+        var list = (await Page(input)).Items?.Adapt<List<ExportSysLangTextOutput>>() ?? new();
+        if (input.SelectKeyList?.Count > 0) list = list.Where(x => input.SelectKeyList.Contains(x.Id)).ToList();
+        return ExcelHelper.ExportTemplate(list, "翻译表导出记录");
+    }
+
+    /// <summary>
+    /// 下载翻译表数据导入模板 ⬇️
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("下载翻译表数据导入模板")]
+    [ApiDescriptionSettings(Name = "Import"), HttpGet, NonUnify]
+    public IActionResult DownloadTemplate()
+    {
+        return ExcelHelper.ExportTemplate(new List<ExportSysLangTextOutput>(), "翻译表导入模板");
+    }
+
+    private static readonly object _sysLangTextImportLock = new object();
+    /// <summary>
+    /// 导入翻译表记录 💾
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("导入翻译表记录")]
+    [ApiDescriptionSettings(Name = "Import"), HttpPost, NonUnify, UnitOfWork]
+    public IActionResult ImportData([Required] IFormFile file)
+    {
+        lock (_sysLangTextImportLock)
+        {
+            var stream = ExcelHelper.ImportData<ImportSysLangTextInput, SysLangText>(file, (list, markerErrorAction) =>
+            {
+                _sqlSugarClient.Utilities.PageEach(list, 2048, pageItems =>
+                {
+
+                    // 校验并过滤必填基本类型为null的字段
+                    var rows = pageItems.Where(x =>
+                    {
+                        if (!string.IsNullOrWhiteSpace(x.Error)) return false;
+                        if (x.EntityId == null)
+                        {
+                            x.Error = "所属实体ID不能为空";
+                            return false;
+                        }
+                        return true;
+                    }).Adapt<List<SysLangText>>();
+
+                    var storageable = _sysLangTextRep.Context.Storageable(rows)
+                        .SplitError(it => string.IsNullOrWhiteSpace(it.Item.EntityName), "所属实体名不能为空")
+                        .SplitError(it => it.Item.EntityName?.Length > 255, "所属实体名长度不能超过255个字符")
+                        .SplitError(it => string.IsNullOrWhiteSpace(it.Item.FieldName), "字段名不能为空")
+                        .SplitError(it => it.Item.FieldName?.Length > 255, "字段名长度不能超过255个字符")
+                        .SplitError(it => string.IsNullOrWhiteSpace(it.Item.LangCode), "语言代码不能为空")
+                        .SplitError(it => it.Item.LangCode?.Length > 255, "语言代码长度不能超过255个字符")
+                        .SplitError(it => string.IsNullOrWhiteSpace(it.Item.Content), "翻译内容不能为空")
+                        .SplitError(it => it.Item.Content?.Length > 255, "翻译内容长度不能超过255个字符")
+                        .WhereColumns(it => new { it.EntityId, it.EntityName, it.FieldName, it.LangCode })
+                        .SplitInsert(it => it.NotAny())
+                        .SplitUpdate(it => it.Any())
+                        .ToStorage();
+
+                    storageable.AsInsertable.ExecuteCommand();// 不存在插入
+                    storageable.AsUpdateable.UpdateColumns(it => new
+                    {
+                        it.EntityName,
+                        it.EntityId,
+                        it.FieldName,
+                        it.LangCode,
+                        it.Content,
+                    }).ExecuteCommand();// 存在更新
+
+                    // 标记错误信息
+                    markerErrorAction.Invoke(storageable, pageItems, rows);
+                });
+            });
+
+            return stream;
+        }
+    }
+}

+ 81 - 15
Admin.NET/Admin.NET.Core/Service/Menu/SysMenuService.cs

@@ -19,10 +19,12 @@ public class SysMenuService : IDynamicApiController, ITransient
     private readonly SysUserMenuService _sysUserMenuService;
     private readonly SysCacheService _sysCacheService;
     private readonly UserManager _userManager;
+    private readonly SqlSugarRepository<SysLangText> _sysLangTextRep;
 
     public SysMenuService(
         SqlSugarRepository<SysTenantMenu> sysTenantMenuRep,
         SqlSugarRepository<SysMenu> sysMenuRep,
+        SqlSugarRepository<SysLangText> sysLangTextRep,
         SysRoleMenuService sysRoleMenuService,
         SysUserRoleService sysUserRoleService,
         SysUserMenuService sysUserMenuService,
@@ -36,6 +38,7 @@ public class SysMenuService : IDynamicApiController, ITransient
         _sysUserMenuService = sysUserMenuService;
         _sysTenantMenuRep = sysTenantMenuRep;
         _sysCacheService = sysCacheService;
+        _sysLangTextRep = sysLangTextRep;
     }
 
     /// <summary>
@@ -45,18 +48,41 @@ public class SysMenuService : IDynamicApiController, ITransient
     [DisplayName("获取登录菜单树")]
     public async Task<List<MenuOutput>> GetLoginMenuTree()
     {
+        var langCode = _userManager.LangCode;
         var (query, _) = GetSugarQueryableAndTenantId(_userManager.TenantId);
-        if (_userManager.SuperAdmin || _userManager.SysAdmin)
+
+        // 先把菜单表作为主表
+        var menuQuery = query.Where(u => u.Type != MenuTypeEnum.Btn && u.Status == StatusEnum.Enable);
+
+        if (!(_userManager.SuperAdmin || _userManager.SysAdmin))
         {
-            var menuList = await query.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>>();
+            var menuIdList = await GetMenuIdList();
+            menuQuery = menuQuery.Where(u => menuIdList.Contains(u.Id));
         }
 
-        var menuIdList = await GetMenuIdList();
-        var menuTree = await query.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, menuIdList.Select(d => (object)d).ToArray());
+        // 联表翻译表 LEFT JOIN (翻译表可无则保留原值)
+        var finalQuery = menuQuery
+            .OrderBy(u => new { u.OrderNo, u.Id })
+            .LeftJoin<SysLangText>((menu, lang) =>
+                lang.EntityName == "SysMenu"
+                && lang.FieldName == "Title"
+                && lang.EntityId == menu.Id
+                && lang.LangCode == langCode
+            )
+            .Select((menu, lang) => new
+            {
+                menu,
+                Title = SqlFunc.IIF(SqlFunc.IsNullOrEmpty(lang.Content), menu.Title, lang.Content),
+            }).Mapper(it =>
+            {
+                it.menu.Title = it.Title;
+            }).ToList()
+            .Select(it=>it.menu);
+
+        var menuTree = finalQuery.ToTree(
+            it => it.Children, it => it.Pid, 0
+        );
+
         return menuTree.Adapt<List<MenuOutput>>();
     }
 
@@ -67,21 +93,61 @@ public class SysMenuService : IDynamicApiController, ITransient
     [DisplayName("获取菜单列表")]
     public async Task<List<SysMenu>> GetList([FromQuery] MenuInput input)
     {
+        var langCode = _userManager.LangCode;
         var menuIdList = _userManager.SuperAdmin || _userManager.SysAdmin ? new List<long>() : await GetMenuIdList();
         var (query, _) = GetSugarQueryableAndTenantId(input.TenantId);
 
         // 有筛选条件时返回list列表(防止构造不出树)
         if (!string.IsNullOrWhiteSpace(input.Title) || input.Type is > 0)
         {
-            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 }).Distinct().ToListAsync();
+            var menuQuery = 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));
+            var finalQuery = menuQuery
+            .OrderBy(u => new { u.OrderNo, u.Id })
+            .LeftJoin<SysLangText>((menu, lang) =>
+                lang.EntityName == "SysMenu"
+                && lang.FieldName == "Title"
+                && lang.EntityId == menu.Id
+                && lang.LangCode == langCode
+            )
+            .Select((menu, lang) => new
+            {
+                menu,
+                Title = SqlFunc.IIF(SqlFunc.IsNullOrEmpty(lang.Content), menu.Title, lang.Content),
+            }).Mapper(it =>
+            {
+                it.menu.Title = it.Title;
+            }).ToList()
+            .Select(it => it.menu);
+            return finalQuery.Distinct().ToList();
         }
 
-        return _userManager.SuperAdmin || _userManager.SysAdmin ?
-            await query.OrderBy(u => new { u.OrderNo, u.Id }).Distinct().ToTreeAsync(u => u.Children, u => u.Pid, 0) :
-            await query.OrderBy(u => new { u.OrderNo, u.Id }).Distinct().ToTreeAsync(u => u.Children, u => u.Pid, 0, menuIdList.Select(d => (object)d).ToArray()); // 角色菜单授权时
+        if (!(_userManager.SuperAdmin || _userManager.SysAdmin))
+        {
+            query = query.Where(u => menuIdList.Contains(u.Id));
+        }
+        var final1Query = query
+            .OrderBy(u => new { u.OrderNo, u.Id })
+            .LeftJoin<SysLangText>((menu, lang) =>
+                lang.EntityName == "SysMenu"
+                && lang.FieldName == "Title"
+                && lang.EntityId == menu.Id
+                && lang.LangCode == langCode
+            )
+            .Select((menu, lang) => new
+            {
+                menu,
+                Title = SqlFunc.IIF(SqlFunc.IsNullOrEmpty(lang.Content), menu.Title, lang.Content),
+            }).Mapper(it =>
+            {
+                it.menu.Title = it.Title;
+            }).ToList()
+            .Select(it => it.menu);
+        var menuTree = final1Query.ToTree(
+            it => it.Children, it => it.Pid, 0
+        );
+        return menuTree.ToList(); // 角色菜单授权时
     }
 
     /// <summary>

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

@@ -58,6 +58,8 @@ public class UserManager : IScoped
     /// </summary>
     public string OpenId => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.OpenId)?.Value;
 
+    public string LangCode => _httpContextAccessor.HttpContext?.User.FindFirst(ClaimConst.LangCode)?.Value ?? "zh_CN";
+
     public UserManager(IHttpContextAccessor httpContextAccessor)
     {
         _httpContextAccessor = httpContextAccessor;

+ 3 - 1
Web/src/App.vue

@@ -23,6 +23,7 @@ import setIntroduction from '/@/utils/setIconfont';
 // import Watermark from '/@/utils/watermark';
 import { SysConfigApi } from '/@/api-services';
 import { getAPI } from '/@/utils/axios-utils';
+import { useLangStore } from '/@/stores/useLangStore';
 
 // 引入组件
 const LockScreen = defineAsyncComponent(() => import('/@/layout/lockScreen/index.vue'));
@@ -168,7 +169,8 @@ const updateFavicon = (url: string): void => {
 
 // 加载系统信息
 loadSysInfo();
-
+const langStore = useLangStore();
+langStore.loadLanguages();
 // 阻止火狐浏览器在拖动时打开新窗口
 document.body.ondrop = function (event) {
 	event.preventDefault();

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

@@ -28,6 +28,7 @@ export * from './apis/sys-enum-api';
 export * from './apis/sys-file-api';
 export * from './apis/sys-job-api';
 export * from './apis/sys-lang-api';
+export * from './apis/sys-lang-text-api';
 export * from './apis/sys-ldap-api';
 export * from './apis/sys-log-diff-api';
 export * from './apis/sys-log-ex-api';

+ 907 - 0
Web/src/api-services/apis/sys-lang-text-api.ts

@@ -0,0 +1,907 @@
+/* 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 { AddSysLangTextInput } from '../models';
+import { AdminResultInt64 } from '../models';
+import { AdminResultListSysLangTextOutput } from '../models';
+import { AdminResultSqlSugarPagedListSysLangTextOutput } from '../models';
+import { AdminResultSysLangText } from '../models';
+import { DeleteSysLangTextInput } from '../models';
+import { ImportSysLangTextInput } from '../models';
+import { ListSysLangTextInput } from '../models';
+import { PageSysLangTextInput } from '../models';
+import { UpdateSysLangTextInput } from '../models';
+/**
+ * SysLangTextApi - axios parameter creator
+ * @export
+ */
+export const SysLangTextApiAxiosParamCreator = function (configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加翻译表 ➕
+         * @param {AddSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextAddPost: async (body?: AddSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/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
+
+            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 {Array<DeleteSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextBatchDeletePost: async (body: Array<DeleteSysLangTextInput>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'body' is not null or undefined
+            if (body === null || body === undefined) {
+                throw new RequiredError('body','Required parameter body was null or undefined when calling apiSysLangTextBatchDeletePost.');
+            }
+            const localVarPath = `/api/sysLangText/batchDelete`;
+            // 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
+
+            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 {Array<ImportSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextBatchSavePost: async (body: Array<ImportSysLangTextInput>, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'body' is not null or undefined
+            if (body === null || body === undefined) {
+                throw new RequiredError('body','Required parameter body was null or undefined when calling apiSysLangTextBatchSavePost.');
+            }
+            const localVarPath = `/api/sysLangText/batchSave`;
+            // 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
+
+            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 {DeleteSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextDeletePost: async (body?: DeleteSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/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
+
+            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 主键Id
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextDetailGet: async (id: number, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            // verify required parameter 'id' is not null or undefined
+            if (id === null || id === undefined) {
+                throw new RequiredError('id','Required parameter id was null or undefined when calling apiSysLangTextDetailGet.');
+            }
+            const localVarPath = `/api/sysLangText/detail`;
+            // 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
+
+            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 {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextExportPost: async (body?: PageSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/export`;
+            // 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
+
+            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}
+         */
+        apiSysLangTextImportGet: async (options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/import`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'GET', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+
+            const query = new URLSearchParams(localVarUrlObj.search);
+            for (const key in localVarQueryParameter) {
+                query.set(key, localVarQueryParameter[key]);
+            }
+            for (const key in options.params) {
+                query.set(key, options.params[key]);
+            }
+            localVarUrlObj.search = (new URLSearchParams(query)).toString();
+            let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {};
+            localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers};
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 导入翻译表记录 💾
+         * @param {Blob} [file] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextImportPostForm: async (file?: Blob, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/import`;
+            // 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;
+            const localVarFormParams = new FormData();
+
+            // authentication Bearer required
+
+
+            if (file !== undefined) { 
+                localVarFormParams.append('file', file as any);
+            }
+
+            localVarHeaderParameter['Content-Type'] = 'multipart/form-data';
+            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};
+            localVarRequestOptions.data = localVarFormParams;
+
+            return {
+                url: localVarUrlObj.pathname + localVarUrlObj.search + localVarUrlObj.hash,
+                options: localVarRequestOptions,
+            };
+        },
+        /**
+         * 
+         * @summary 获取翻译表
+         * @param {ListSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextListPost: async (body?: ListSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/list`;
+            // use dummy base URL string because the URL constructor only accepts absolute URLs.
+            const localVarUrlObj = new URL(localVarPath, 'https://example.com');
+            let baseOptions;
+            if (configuration) {
+                baseOptions = configuration.baseOptions;
+            }
+            const localVarRequestOptions :AxiosRequestConfig = { method: 'POST', ...baseOptions, ...options};
+            const localVarHeaderParameter = {} as any;
+            const localVarQueryParameter = {} as any;
+
+            // authentication Bearer required
+
+            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 {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextPagePost: async (body?: PageSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/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
+
+            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 {UpdateSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        apiSysLangTextUpdatePost: async (body?: UpdateSysLangTextInput, options: AxiosRequestConfig = {}): Promise<RequestArgs> => {
+            const localVarPath = `/api/sysLangText/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
+
+            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,
+            };
+        },
+    }
+};
+
+/**
+ * SysLangTextApi - functional programming interface
+ * @export
+ */
+export const SysLangTextApiFp = function(configuration?: Configuration) {
+    return {
+        /**
+         * 
+         * @summary 增加翻译表 ➕
+         * @param {AddSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextAddPost(body?: AddSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultInt64>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextAddPost(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 {Array<DeleteSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextBatchDeletePost(body: Array<DeleteSysLangTextInput>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextBatchDeletePost(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 {Array<ImportSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextBatchSavePost(body: Array<ImportSysLangTextInput>, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextBatchSavePost(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 {DeleteSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextDeletePost(body?: DeleteSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextDeletePost(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 主键Id
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextDetailGet(id: number, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultSysLangText>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextDetailGet(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 {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextExportPost(body?: PageSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextExportPost(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 apiSysLangTextImportGet(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextImportGet(options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 导入翻译表记录 💾
+         * @param {Blob} [file] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextImportPostForm(file?: Blob, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextImportPostForm(file, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+        /**
+         * 
+         * @summary 获取翻译表
+         * @param {ListSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextListPost(body?: ListSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultListSysLangTextOutput>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextListPost(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 {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextPagePost(body?: PageSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<AdminResultSqlSugarPagedListSysLangTextOutput>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextPagePost(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 {UpdateSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextUpdatePost(body?: UpdateSysLangTextInput, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => Promise<AxiosResponse<void>>> {
+            const localVarAxiosArgs = await SysLangTextApiAxiosParamCreator(configuration).apiSysLangTextUpdatePost(body, options);
+            return (axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => {
+                const axiosRequestArgs :AxiosRequestConfig = {...localVarAxiosArgs.options, url: basePath + localVarAxiosArgs.url};
+                return axios.request(axiosRequestArgs);
+            };
+        },
+    }
+};
+
+/**
+ * SysLangTextApi - factory interface
+ * @export
+ */
+export const SysLangTextApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) {
+    return {
+        /**
+         * 
+         * @summary 增加翻译表 ➕
+         * @param {AddSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextAddPost(body?: AddSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultInt64>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextAddPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 批量删除翻译表 ❌
+         * @param {Array<DeleteSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextBatchDeletePost(body: Array<DeleteSysLangTextInput>, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextBatchDeletePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 批量保存翻译表 ✏️
+         * @param {Array<ImportSysLangTextInput>} body 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextBatchSavePost(body: Array<ImportSysLangTextInput>, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextBatchSavePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 删除翻译表 ❌
+         * @param {DeleteSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextDeletePost(body?: DeleteSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextDeletePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取翻译表详情 ℹ️
+         * @param {number} id 主键Id
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextDetailGet(id: number, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultSysLangText>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextDetailGet(id, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 导出翻译表记录 🔖
+         * @param {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextExportPost(body?: PageSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextExportPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 下载翻译表数据导入模板 ⬇️
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextImportGet(options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextImportGet(options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 导入翻译表记录 💾
+         * @param {Blob} [file] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextImportPostForm(file?: Blob, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextImportPostForm(file, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 获取翻译表
+         * @param {ListSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextListPost(body?: ListSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultListSysLangTextOutput>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextListPost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 分页查询翻译表 🔖
+         * @param {PageSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextPagePost(body?: PageSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<AdminResultSqlSugarPagedListSysLangTextOutput>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextPagePost(body, options).then((request) => request(axios, basePath));
+        },
+        /**
+         * 
+         * @summary 更新翻译表 ✏️
+         * @param {UpdateSysLangTextInput} [body] 
+         * @param {*} [options] Override http request option.
+         * @throws {RequiredError}
+         */
+        async apiSysLangTextUpdatePost(body?: UpdateSysLangTextInput, options?: AxiosRequestConfig): Promise<AxiosResponse<void>> {
+            return SysLangTextApiFp(configuration).apiSysLangTextUpdatePost(body, options).then((request) => request(axios, basePath));
+        },
+    };
+};
+
+/**
+ * SysLangTextApi - object-oriented interface
+ * @export
+ * @class SysLangTextApi
+ * @extends {BaseAPI}
+ */
+export class SysLangTextApi extends BaseAPI {
+    /**
+     * 
+     * @summary 增加翻译表 ➕
+     * @param {AddSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextAddPost(body?: AddSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultInt64>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextAddPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 批量删除翻译表 ❌
+     * @param {Array<DeleteSysLangTextInput>} body 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextBatchDeletePost(body: Array<DeleteSysLangTextInput>, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextBatchDeletePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 批量保存翻译表 ✏️
+     * @param {Array<ImportSysLangTextInput>} body 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextBatchSavePost(body: Array<ImportSysLangTextInput>, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextBatchSavePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 删除翻译表 ❌
+     * @param {DeleteSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextDeletePost(body?: DeleteSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextDeletePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取翻译表详情 ℹ️
+     * @param {number} id 主键Id
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextDetailGet(id: number, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultSysLangText>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextDetailGet(id, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 导出翻译表记录 🔖
+     * @param {PageSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextExportPost(body?: PageSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextExportPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 下载翻译表数据导入模板 ⬇️
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextImportGet(options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextImportGet(options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 导入翻译表记录 💾
+     * @param {Blob} [file] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextImportPostForm(file?: Blob, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextImportPostForm(file, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 获取翻译表
+     * @param {ListSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextListPost(body?: ListSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultListSysLangTextOutput>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextListPost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 分页查询翻译表 🔖
+     * @param {PageSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextPagePost(body?: PageSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<AdminResultSqlSugarPagedListSysLangTextOutput>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextPagePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+    /**
+     * 
+     * @summary 更新翻译表 ✏️
+     * @param {UpdateSysLangTextInput} [body] 
+     * @param {*} [options] Override http request option.
+     * @throws {RequiredError}
+     * @memberof SysLangTextApi
+     */
+    public async apiSysLangTextUpdatePost(body?: UpdateSysLangTextInput, options?: AxiosRequestConfig) : Promise<AxiosResponse<void>> {
+        return SysLangTextApiFp(this.configuration).apiSysLangTextUpdatePost(body, options).then((request) => request(this.axios, this.basePath));
+    }
+}

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

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

+ 63 - 0
Web/src/api-services/models/sql-sugar-paged-list-sys-lang-text-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 { SysLangTextOutput } from './sys-lang-text-output';
+/**
+ * 分页泛型集合
+ * @export
+ * @interface SqlSugarPagedListSysLangTextOutput
+ */
+export interface SqlSugarPagedListSysLangTextOutput {
+    /**
+     * 页码
+     * @type {number}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    page?: number;
+    /**
+     * 页容量
+     * @type {number}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    pageSize?: number;
+    /**
+     * 总条数
+     * @type {number}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    total?: number;
+    /**
+     * 总页数
+     * @type {number}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    totalPages?: number;
+    /**
+     * 当前页集合
+     * @type {Array<SysLangTextOutput>}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    items?: Array<SysLangTextOutput> | null;
+    /**
+     * 是否有上一页
+     * @type {boolean}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    hasPrevPage?: boolean;
+    /**
+     * 是否有下一页
+     * @type {boolean}
+     * @memberof SqlSugarPagedListSysLangTextOutput
+     */
+    hasNextPage?: boolean;
+}

+ 26 - 0
Web/src/api-services/models/sys-lang-text-import-body.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.
+ */
+/**
+ * 
+ * @export
+ * @interface SysLangTextImportBody
+ */
+export interface SysLangTextImportBody {
+    /**
+     * 
+     * @type {Blob}
+     * @memberof SysLangTextImportBody
+     */
+    file: Blob;
+}

+ 92 - 0
Web/src/api-services/models/sys-lang-text-output.ts

@@ -0,0 +1,92 @@
+/* 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 SysLangTextOutput
+ */
+export interface SysLangTextOutput {
+    /**
+     * 主键Id
+     * @type {number}
+     * @memberof SysLangTextOutput
+     */
+    id?: number;
+    /**
+     * 所属实体名
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    entityName?: string | null;
+    /**
+     * 所属实体ID
+     * @type {number}
+     * @memberof SysLangTextOutput
+     */
+    entityId?: number;
+    /**
+     * 字段名
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    fieldName?: string | null;
+    /**
+     * 语言代码
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    langCode?: string | null;
+    /**
+     * 翻译内容
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    content?: string | null;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof SysLangTextOutput
+     */
+    createTime?: Date | null;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof SysLangTextOutput
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof SysLangTextOutput
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof SysLangTextOutput
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof SysLangTextOutput
+     */
+    updateUserName?: string | null;
+}

+ 92 - 0
Web/src/api-services/models/sys-lang-text.ts

@@ -0,0 +1,92 @@
+/* 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 SysLangText
+ */
+export interface SysLangText {
+    /**
+     * 雪花Id
+     * @type {number}
+     * @memberof SysLangText
+     */
+    id?: number;
+    /**
+     * 创建时间
+     * @type {Date}
+     * @memberof SysLangText
+     */
+    createTime?: Date;
+    /**
+     * 更新时间
+     * @type {Date}
+     * @memberof SysLangText
+     */
+    updateTime?: Date | null;
+    /**
+     * 创建者Id
+     * @type {number}
+     * @memberof SysLangText
+     */
+    createUserId?: number | null;
+    /**
+     * 创建者姓名
+     * @type {string}
+     * @memberof SysLangText
+     */
+    createUserName?: string | null;
+    /**
+     * 修改者Id
+     * @type {number}
+     * @memberof SysLangText
+     */
+    updateUserId?: number | null;
+    /**
+     * 修改者姓名
+     * @type {string}
+     * @memberof SysLangText
+     */
+    updateUserName?: string | null;
+    /**
+     * 所属实体名
+     * @type {string}
+     * @memberof SysLangText
+     */
+    entityName?: string | null;
+    /**
+     * 语言代码(如 zh_CN)
+     * @type {number}
+     * @memberof SysLangText
+     */
+    entityId?: number;
+    /**
+     * 字段名
+     * @type {string}
+     * @memberof SysLangText
+     */
+    fieldName?: string | null;
+    /**
+     * 语言代码
+     * @type {string}
+     * @memberof SysLangText
+     */
+    langCode?: string | null;
+    /**
+     * 多语言内容
+     * @type {string}
+     * @memberof SysLangText
+     */
+    content?: string | null;
+}

+ 54 - 0
Web/src/api/system/sysLangText.ts

@@ -0,0 +1,54 @@
+import {useBaseApi} from '/@/api/base';
+
+// 翻译接口服务
+export const useSysLangTextApi = () => {
+	const baseApi = useBaseApi("sysLangText");
+	return {
+		// 分页查询翻译
+		page: baseApi.page,
+		// 查看翻译详细
+		detail: baseApi.detail,
+		// 新增翻译
+		add: baseApi.add,
+		// 更新翻译
+		update: baseApi.update,
+		// 删除翻译
+		delete: baseApi.delete,
+		// 批量删除翻译
+		batchDelete: baseApi.batchDelete,
+		// 导出翻译数据
+		exportData: baseApi.exportData,
+		// 导入翻译数据
+		importData: baseApi.importData,
+		// 下载翻译数据导入模板
+		downloadTemplate: baseApi.downloadTemplate,
+	}
+}
+
+// 翻译实体
+export interface SysLangText {
+	// 主键Id
+	id: number;
+	// 所属实体名
+	entityName?: string;
+	// 所属实体ID
+	entityId?: number;
+	// 字段名
+	fieldName?: string;
+	// 语言代码
+	langCode?: string;
+	// 翻译内容
+	content?: string;
+	// 创建时间
+	createTime: string;
+	// 更新时间
+	updateTime: string;
+	// 创建者Id
+	createUserId: number;
+	// 创建者姓名
+	createUserName: string;
+	// 修改者Id
+	updateUserId: number;
+	// 修改者姓名
+	updateUserName: string;
+}

+ 154 - 0
Web/src/components/multiLangInput/index.vue

@@ -0,0 +1,154 @@
+<template>
+    <div class="multi-lang-input">
+        <el-input v-model="props.modelValue" :placeholder="`请输入 ${currentLangLabel}`" clearable>
+            <template #append>
+                <el-button @click="openDialog" circle>
+                    <template #icon>
+                        <i class="iconfont icon-diqiu1"></i>
+                    </template>
+                </el-button>
+            </template>
+        </el-input>
+
+        <el-dialog v-model="dialogVisible" title="多语言设置" width="600px">
+            <el-form ref="ruleFormRef" label-width="auto">
+                <el-row :gutter="35">
+                    <el-col v-for="lang in languages" :key="lang.code" :span="24">
+                        <el-form-item :label="lang.label">
+                            <el-input v-model="multiLangValue[lang.code]" :placeholder="`请输入 ${lang.label}`"
+                                clearable />
+                        </el-form-item>
+                    </el-col>
+                </el-row>
+            </el-form>
+
+            <template #footer>
+                <el-button @click="closeDialog">关闭</el-button>
+                <el-button type="primary" @click="confirmDialog">确认修改</el-button>
+            </template>
+        </el-dialog>
+    </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, defineProps, onMounted } from 'vue';
+import { useLangStore } from '/@/stores/useLangStore';
+import { Local } from '/@/utils/storage';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysLangTextApi } from '/@/api-services/api';
+import { ElMessage } from 'element-plus';
+const ruleFormRef = ref();
+
+const fetchMultiLang = async () => {
+    const result = await getAPI(SysLangTextApi).apiSysLangTextListPost({ entityName: props.entityName, entityId: props.entityId, fieldName: props.fieldName, pageSize: 200 }).then(res => res.data.result)
+    return result ?? [];
+};
+
+
+const props = defineProps<{
+    modelValue: string;
+    entityName: string;
+    entityId: string;
+    fieldName: string;
+}>();
+
+// 全局语言
+const langStore = useLangStore();
+const languages = ref<any>([] as any);
+
+// 当前语言(可根据用户设置或浏览器设置)
+const currentLang = ref('zh_CN');
+const activeLang = ref('zh_CN');
+
+// 是否弹框
+const dialogVisible = ref(false);
+
+// 多语言对象
+const multiLangValue = ref<Record<string, string>>({});
+
+// 当前语言显示 Label
+const currentLangLabel = computed(() => {
+    return (
+        languages.value.find((l: { code: string; }) => l.code === currentLang.value)?.Label || currentLang.value
+    );
+});
+
+// 初始化语言
+onMounted(async () => {
+    if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+    const themeConfig = Local.get('themeConfig');
+    const globalI18n = themeConfig?.globalI18n;
+    if (globalI18n) {
+        const matched = langStore.languages.find(l => l.code === globalI18n);
+        const langCode = matched?.code || 'zh_CN';
+        currentLang.value = langCode;
+        activeLang.value = langCode;
+    }
+    languages.value = langStore.languages;
+
+    if (languages.value.length > 0) {
+        currentLang.value = languages.value[0].code;
+        activeLang.value = languages.value[0].code;
+    }
+});
+
+
+// 打开对话框(点击按钮)
+const openDialog = async () => {    
+    multiLangValue.value = {};
+    const res = await fetchMultiLang();
+    languages.value.forEach(element => {
+        multiLangValue.value[element.code] = props.modelValue;
+    });
+    res.forEach(element => {
+        multiLangValue.value[element.langCode] = element.content;
+    });
+    dialogVisible.value = true;
+	ruleFormRef.value?.resetFields();
+};
+
+// 关闭对话框(只是关闭)
+const closeDialog = () => {
+    dialogVisible.value = false;
+    multiLangValue.value = {};
+	ruleFormRef.value?.resetFields();
+};
+// 提交
+
+// 确认按钮(更新 + 关闭)
+const confirmDialog = async () => {
+    const langItems = Object.entries(multiLangValue.value)
+        .filter(([_, content]) => content && content.trim() !== '')
+        .map(([code, content]) => ({
+            EntityName: props.entityName,
+            EntityId: props.entityId,
+            FieldName: props.fieldName,
+            LangCode: code,
+            Content: content,
+        }));
+
+    if (langItems.length === 0) {
+        ElMessage.warning('请输入至少一条多语言内容!');
+        return;
+    }
+
+    try {
+        await getAPI(SysLangTextApi).apiSysLangTextBatchSavePost(langItems);
+        ElMessage.success('保存成功!');
+        dialogVisible.value = false;
+    } catch (err) {
+        console.error(err);
+        ElMessage.error('保存失败!');
+    }
+    dialogVisible.value = false;
+	ruleFormRef.value?.resetFields();
+};
+</script>
+
+<style scoped>
+.multi-lang-input {
+    width: 100%;
+}
+</style>

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

@@ -106,7 +106,9 @@ import { Avatar, CircleCloseFilled, Loading, Lock, Switch } from '@element-plus/
 import { clearAccessAfterReload, getAPI } from '/@/utils/axios-utils';
 import { SysAuthApi, SysNoticeApi, SysLangApi } from '/@/api-services/api';
 import { auth } from '/@/utils/authFunction';
+import { useLangStore } from '/@/stores/useLangStore';
 
+const langStore = useLangStore();
 // 引入组件
 const UserNews = defineAsyncComponent(() => import('/@/layout/navBars/topBar/userNews.vue'));
 const Search = defineAsyncComponent(() => import('/@/layout/navBars/topBar/search.vue'));
@@ -254,8 +256,7 @@ onMounted(async () => {
 			Push.clear();
 		}
 	});
-	var langRes = await getAPI(SysLangApi).apiSysLangDropdownDataPost();
-	state.languages = langRes.data.result ?? [];
+	state.languages = langStore.languages;
 	// 加载未读的站内信
 	var res = await getAPI(SysNoticeApi).apiSysNoticeUnReadListGet();
 	state.noticeList = res.data.result ?? [];

+ 3 - 0
Web/src/main.ts

@@ -22,6 +22,7 @@ import 'vform3-builds/dist/designer.style.css';
 // 关闭自动打印
 import { disAutoConnect } from 'vue-plugin-hiprint';
 import sysDict from "/src/components/sysDict/sysDict.vue";
+import multiLangInput from "/src/components/multiLangInput/index.vue";
 disAutoConnect();
 
 const app = createApp(App);
@@ -31,5 +32,7 @@ other.elSvg(app);
 
 // 注册全局字典组件
 app.component('GSysDict', sysDict);
+// 注册全局多语言组件
+app.component('GMultiLangInput', multiLangInput);
 
 app.use(pinia).use(router).use(ElementPlus).use(i18n).use(VueGridLayout).use(VForm3).use(VueSignaturePad).use(vue3TreeOrg).mount('#app');

+ 17 - 0
Web/src/stores/useLangStore.ts

@@ -0,0 +1,17 @@
+import { defineStore } from 'pinia';
+import { getAPI } from '/@/utils/axios-utils';
+import { SysLangApi } from '/@/api-services/api';
+
+export const useLangStore = defineStore('lang', {
+	state: () => ({
+		languages: [] as any[],
+	}),
+	actions: {
+		async loadLanguages() {
+			if (this.languages.length === 0) {
+				const res = await getAPI(SysLangApi).apiSysLangDropdownDataPost();
+				this.languages = res.data.result ?? [];
+			}
+		},
+	},
+});

+ 121 - 0
Web/src/views/system/langText/component/editDialog.vue

@@ -0,0 +1,121 @@
+<script lang="ts" name="sysLangText" setup>
+import { ref, reactive, onMounted } from "vue";
+import { ElMessage } from "element-plus";
+import type { FormRules } from "element-plus";
+import { formatDate } from '/@/utils/formatTime';
+import { useSysLangTextApi } from '/@/api/system/sysLangText';
+
+//父级传递来的函数,用于回调
+const emit = defineEmits(["reloadTable"]);
+const sysLangTextApi = useSysLangTextApi();
+const ruleFormRef = ref();
+
+const state = reactive({
+	title: '',
+	loading: false,
+	showDialog: false,
+	ruleForm: {} as any,
+	stores: {},
+	dropdownData: {} as any,
+});
+
+// 自行添加其他规则
+const rules = ref<FormRules>({
+  entityName: [{required: true, message: '请选择所属实体名!', trigger: 'blur',},],
+  entityId: [{required: true, message: '请选择所属实体ID!', trigger: 'blur',},],
+  fieldName: [{required: true, message: '请选择字段名!', trigger: 'blur',},],
+  langCode: [{required: true, message: '请选择语言代码!', trigger: 'blur',},],
+  content: [{required: true, message: '请选择翻译内容!', trigger: 'blur',},],
+});
+
+// 页面加载时
+onMounted(async () => {
+});
+
+// 打开弹窗
+const openDialog = async (row: any, title: string) => {
+	state.title = title;
+	row = row ?? {  };
+	state.ruleForm = row.id ? await sysLangTextApi.detail(row.id).then(res => res.data.result) : JSON.parse(JSON.stringify(row));
+	state.showDialog = true;
+};
+
+// 关闭弹窗
+const closeDialog = () => {
+	emit("reloadTable");
+	state.showDialog = false;
+};
+
+// 提交
+const submit = async () => {
+	ruleFormRef.value.validate(async (isValid: boolean, fields?: any) => {
+		if (isValid) {
+			let values = state.ruleForm;
+			await sysLangTextApi[state.ruleForm.id ? 'update' : 'add'](values);
+			closeDialog();
+		} else {
+			ElMessage({
+				message: `表单有${Object.keys(fields).length}处验证失败,请修改后再提交`,
+				type: "error",
+			});
+		}
+	});
+};
+
+//将属性或者函数暴露给父组件
+defineExpose({ openDialog });
+</script>
+<template>
+	<div class="sysLangText-container">
+		<el-dialog v-model="state.showDialog" :width="800" draggable :close-on-click-modal="false">
+			<template #header>
+				<div style="color: #fff">
+					<span>{{ state.title }}</span>
+				</div>
+			</template>
+			<el-form :model="state.ruleForm" ref="ruleFormRef" label-width="auto" :rules="rules">
+				<el-row :gutter="35">
+					<el-form-item v-show="false">
+						<el-input v-model="state.ruleForm.id" />
+					</el-form-item>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" >
+						<el-form-item label="所属实体名" prop="entityName">
+							<el-input v-model="state.ruleForm.entityName" placeholder="请输入所属实体名" maxlength="255" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" >
+						<el-form-item label="所属实体ID" prop="entityId">
+							<el-input v-model="state.ruleForm.entityId" placeholder="请输入所属实体ID" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" >
+						<el-form-item label="字段名" prop="fieldName">
+							<el-input v-model="state.ruleForm.fieldName" placeholder="请输入字段名" maxlength="255" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" >
+						<el-form-item label="语言代码" prop="langCode">
+							<el-input v-model="state.ruleForm.langCode" placeholder="请输入语言代码" maxlength="255" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" >
+						<el-form-item label="翻译内容" prop="content">
+							<el-input v-model="state.ruleForm.content" placeholder="请输入翻译内容" maxlength="255" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+				</el-row>
+			</el-form>
+			<template #footer>
+				<span class="dialog-footer">
+					<el-button @click="() => state.showDialog = false">取 消</el-button>
+					<el-button @click="submit" type="primary" v-reclick="1000">确 定</el-button>
+				</span>
+			</template>
+		</el-dialog>
+	</div>
+</template>
+<style lang="scss" scoped>
+:deep(.el-select), :deep(.el-input-number) {
+  width: 100%;
+}
+</style>

+ 206 - 0
Web/src/views/system/langText/index.vue

@@ -0,0 +1,206 @@
+<script lang="ts" setup name="sysLangText">
+import { ref, reactive, onMounted } from "vue";
+import { auth } from '/@/utils/authFunction';
+import { ElMessageBox, ElMessage } from "element-plus";
+import { downloadStreamFile } from "/@/utils/download";
+import { useSysLangTextApi } from '/@/api/system/sysLangText';
+import editDialog from '/@/views/system/langText/component/editDialog.vue'
+import printDialog from '/@/views/system/print/component/hiprint/preview.vue'
+import ModifyRecord from '/@/components/table/modifyRecord.vue';
+import ImportData from "/@/components/table/importData.vue";
+
+const sysLangTextApi = useSysLangTextApi();
+const printDialogRef = ref();
+const editDialogRef = ref();
+const importDataRef = ref();
+const state = reactive({
+  exportLoading: false,
+  tableLoading: false,
+  stores: {},
+  showAdvanceQueryUI: false,
+  dropdownData: {} as any,
+  selectData: [] as any[],
+  tableQueryParams: {} as any,
+  tableParams: {
+    page: 1,
+    pageSize: 20,
+    total: 0,
+    field: 'createTime', // 默认的排序字段
+    order: 'descending', // 排序方向
+    descStr: 'descending', // 降序排序的关键字符
+  },
+  tableData: [],
+});
+
+// 页面加载时
+onMounted(async () => {
+});
+
+// 查询操作
+const handleQuery = async (params: any = {}) => {
+  state.tableLoading = true;
+  state.tableParams = Object.assign(state.tableParams, params);
+  const result = await sysLangTextApi.page(Object.assign(state.tableQueryParams, state.tableParams)).then(res => res.data.result);
+  state.tableParams.total = result?.total;
+  state.tableData = result?.items ?? [];
+  state.tableLoading = false;
+};
+
+// 列排序
+const sortChange = async (column: any) => {
+  state.tableParams.field = column.prop;
+  state.tableParams.order = column.order;
+  await handleQuery();
+};
+
+// 删除
+const delSysLangText = (row: any) => {
+  ElMessageBox.confirm(`确定要删除吗?`, "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(async () => {
+    await sysLangTextApi.delete({ id: row.id });
+    handleQuery();
+    ElMessage.success("删除成功");
+  }).catch(() => {});
+};
+
+// 批量删除
+const batchDelSysLangText = () => {
+  ElMessageBox.confirm(`确定要删除${state.selectData.length}条记录吗?`, "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  }).then(async () => {
+    await sysLangTextApi.batchDelete(state.selectData.map(u => ({ id: u.id }) )).then(res => {
+      ElMessage.success(`成功批量删除${res.data.result}条记录`);
+      handleQuery();
+    });
+  }).catch(() => {});
+};
+
+// 导出数据
+const exportSysLangTextCommand = async (command: string) => {
+  try {
+    state.exportLoading = true;
+    if (command === 'select') {
+      const params = Object.assign({}, state.tableQueryParams, state.tableParams, { selectKeyList: state.selectData.map(u => u.id) });
+      await sysLangTextApi.exportData(params).then(res => downloadStreamFile(res));
+    } else if (command === 'current') {
+      const params = Object.assign({}, state.tableQueryParams, state.tableParams);
+      await sysLangTextApi.exportData(params).then(res => downloadStreamFile(res));
+    } else if (command === 'all') {
+      const params = Object.assign({}, state.tableQueryParams, state.tableParams, { page: 1, pageSize: 99999999 });
+      await sysLangTextApi.exportData(params).then(res => downloadStreamFile(res));
+    }
+  } finally {
+    state.exportLoading = false;
+  }
+}
+
+handleQuery();
+</script>
+<template>
+  <div class="sysLangText-container" v-loading="state.exportLoading">
+    <el-card shadow="hover" :body-style="{ paddingBottom: '0' }"> 
+      <el-form :model="state.tableQueryParams" ref="queryForm" labelWidth="90">
+        <el-row>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10">
+            <el-form-item label="关键字">
+              <el-input v-model="state.tableQueryParams.keyword" clearable placeholder="请输入模糊查询关键字"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="state.showAdvanceQueryUI">
+            <el-form-item label="所属实体名">
+              <el-input v-model="state.tableQueryParams.entityName" clearable placeholder="请输入所属实体名"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="state.showAdvanceQueryUI">
+            <el-form-item label="所属实体ID">
+              <el-input v-model="state.tableQueryParams.entityId" clearable placeholder="请输入所属实体ID"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="state.showAdvanceQueryUI">
+            <el-form-item label="字段名">
+              <el-input v-model="state.tableQueryParams.fieldName" clearable placeholder="请输入字段名"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="state.showAdvanceQueryUI">
+            <el-form-item label="语言代码">
+              <el-input v-model="state.tableQueryParams.langCode" clearable placeholder="请输入语言代码"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="state.showAdvanceQueryUI">
+            <el-form-item label="翻译内容">
+              <el-input v-model="state.tableQueryParams.content" clearable placeholder="请输入翻译内容"/>
+            </el-form-item>
+          </el-col>
+          <el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10">
+            <el-form-item >
+              <el-button-group style="display: flex; align-items: center;">
+                <el-button type="primary"  icon="ele-Search" @click="handleQuery" v-auth="'sysLangText:page'" v-reclick="1000"> 查询 </el-button>
+                <el-button icon="ele-Refresh" @click="() => state.tableQueryParams = {}"> 重置 </el-button>
+                <el-button icon="ele-ZoomIn" @click="() => state.showAdvanceQueryUI = true" v-if="!state.showAdvanceQueryUI" style="margin-left:5px;"> 高级查询 </el-button>
+                <el-button icon="ele-ZoomOut" @click="() => state.showAdvanceQueryUI = false" v-if="state.showAdvanceQueryUI" style="margin-left:5px;"> 隐藏 </el-button>
+                <el-button type="danger" style="margin-left:5px;" icon="ele-Delete" @click="batchDelSysLangText" :disabled="state.selectData.length == 0" v-auth="'sysLangText:batchDelete'"> 删除 </el-button>
+                <el-button type="primary" style="margin-left:5px;" icon="ele-Plus" @click="editDialogRef.openDialog(null, '新增翻译')" v-auth="'sysLangText:add'"> 新增 </el-button>
+                <el-dropdown :show-timeout="70" :hide-timeout="50" @command="exportSysLangTextCommand">
+                  <el-button type="primary" style="margin-left:5px;" icon="ele-FolderOpened" v-reclick="20000" v-auth="'sysLangText:export'"> 导出 </el-button>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item command="select" :disabled="state.selectData.length == 0">导出选中</el-dropdown-item>
+                      <el-dropdown-item command="current">导出本页</el-dropdown-item>
+                      <el-dropdown-item command="all">导出全部</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+                <el-button type="warning" style="margin-left:5px;" icon="ele-MostlyCloudy" @click="importDataRef.openDialog()" v-auth="'sysLangText:import'"> 导入 </el-button>
+              </el-button-group>
+            </el-form-item>
+          </el-col>
+        </el-row>
+      </el-form>
+    </el-card>
+    <el-card class="full-table" shadow="hover" style="margin-top: 5px">
+      <el-table :data="state.tableData" @selection-change="(val: any[]) => { state.selectData = val; }" style="width: 100%" v-loading="state.tableLoading" tooltip-effect="light" row-key="id" @sort-change="sortChange" border>
+        <el-table-column type="selection" width="40" align="center" v-if="auth('sysLangText:batchDelete') || auth('sysLangText:export')" />
+        <el-table-column type="index" label="序号" width="55" align="center"/>
+        <el-table-column prop='entityName' label='所属实体名' show-overflow-tooltip />
+        <el-table-column prop='entityId' label='所属实体ID' show-overflow-tooltip />
+        <el-table-column prop='fieldName' label='字段名' show-overflow-tooltip />
+        <el-table-column prop='langCode' label='语言代码' show-overflow-tooltip />
+        <el-table-column prop='content' label='翻译内容' 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="140" align="center" fixed="right" show-overflow-tooltip v-if="auth('sysLangText:update') || auth('sysLangText:delete')">
+          <template #default="scope">
+            <el-button icon="ele-Edit" size="small" text type="primary" @click="editDialogRef.openDialog(scope.row, '编辑翻译')" v-auth="'sysLangText:update'"> 编辑 </el-button>
+            <el-button icon="ele-Delete" size="small" text type="primary" @click="delSysLangText(scope.row)" v-auth="'sysLangText:delete'"> 删除 </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      <el-pagination 
+              v-model:currentPage="state.tableParams.page"
+              v-model:page-size="state.tableParams.pageSize"
+              @size-change="(val: any) => handleQuery({ pageSize: val })"
+              @current-change="(val: any) => handleQuery({ page: val })"
+              layout="total, sizes, prev, pager, next, jumper"
+              :page-sizes="[10, 20, 50, 100, 200, 500]"
+              :total="state.tableParams.total"
+              size="small"
+              background />
+      <ImportData ref="importDataRef" :import="sysLangTextApi.importData" :download="sysLangTextApi.downloadTemplate" v-auth="'sysLangText:import'" @refresh="handleQuery"/>
+      <printDialog ref="printDialogRef" :title="'打印翻译'" @reloadTable="handleQuery" />
+      <editDialog ref="editDialogRef" @reloadTable="handleQuery" />
+    </el-card>
+  </div>
+</template>
+<style scoped>
+:deep(.el-input), :deep(.el-select), :deep(.el-input-number) {
+  width: 100%;
+}
+</style>

+ 2 - 2
Web/src/views/system/menu/component/editMenu.vue

@@ -21,12 +21,12 @@
 					</el-col>
 					<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
 						<el-form-item label="菜单类型" prop="type" :rules="[{ required: true, message: '菜单类型不能为空', trigger: 'blur' }]">
-              <g-sys-dict v-model="state.ruleForm.type" code="MenuTypeEnum" render-as="radio" />
+							<g-sys-dict v-model="state.ruleForm.type" code="MenuTypeEnum" render-as="radio" />
 						</el-form-item>
 					</el-col>
 					<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						<el-form-item label="菜单名称" prop="title" :rules="[{ required: true, message: '菜单名称不能为空', trigger: 'blur' }]">
-							<el-input v-model="state.ruleForm.title" placeholder="菜单名称" clearable />
+							<g-multi-lang-Input entityName="SysMenu" fieldName="Title" :entityId="state.ruleForm.id" v-model="state.ruleForm.title" placeholder="菜单名称" clearable />
 						</el-form-item>
 					</el-col>
 					<template v-if="state.ruleForm.type === 1 || state.ruleForm.type === 2">

+ 14 - 1
Web/src/views/system/user/component/editUser.vue

@@ -46,6 +46,13 @@
 									<el-input v-model="state.ruleForm.email" placeholder="邮箱" clearable />
 								</el-form-item>
 							</el-col>
+							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+								<el-form-item label="语言" prop="langCode" :rules="[{ required: true, message: '语言不能为空', trigger: 'blur' }]">
+									<el-select clearable filterable v-model="state.ruleForm.langCode" placeholder="请选择语言">
+										<el-option v-for="(item, index) in state.languages" :key="index" :value="item.code" :label="item.label" />
+									</el-select>
+								</el-form-item>
+							</el-col>
 							<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb5">
 								<el-form-item label="排序">
 									<el-input-number v-model="state.ruleForm.orderNo" placeholder="排序" class="w100" />
@@ -226,7 +233,8 @@ import { useUserInfo } from '/@/stores/userInfo';
 import { getAPI } from '/@/utils/axios-utils';
 import { SysPosApi, SysRoleApi, SysUserApi } from '/@/api-services/api';
 import {AccountTypeEnum, RoleOutput, SysOrg, SysPos, UpdateUserInput} from '/@/api-services/models';
-
+import { useLangStore } from '/@/stores/useLangStore';
+const langStore = useLangStore();
 const props = defineProps({
 	title: String,
 	orgData: Array<SysOrg>,
@@ -243,6 +251,7 @@ const state = reactive({
 	ruleForm: {} as UpdateUserInput,
 	posData: [] as Array<SysPos>, // 职位数据
 	roleData: [] as Array<RoleOutput>, // 角色数据
+	languages: [] as any[], // 语言数据
 });
 // 级联选择器配置选项
 const cascaderProps = { checkStrictly: true, emitPath: false, value: 'id', label: 'name', expandTrigger: 'hover' };
@@ -253,6 +262,10 @@ onMounted(async () => {
 	state.posData = res.data.result ?? [];
 	var res1 = await getAPI(SysRoleApi).apiSysRoleListGet();
 	state.roleData = res1.data.result ?? [];
+	if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+	state.languages = langStore.languages;
 	state.loading = false;
 });
 

+ 14 - 1
Web/src/views/system/user/component/userCenter.vue

@@ -89,6 +89,13 @@
 											</el-radio-group>
 										</el-form-item>
 									</el-col>
+									<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+										<el-form-item label="语言" prop="langCode" :rules="[{ required: true, message: '语言不能为空', trigger: 'blur' }]">
+											<el-select clearable filterable v-model="state.ruleFormBase.langCode" placeholder="请选择语言">
+												<el-option v-for="(item, index) in state.languages" :key="index" :value="item.code" :label="item.label" />
+											</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="地址">
 											<el-input v-model="state.ruleFormBase.address" placeholder="地址" clearable type="textarea" />
@@ -173,7 +180,8 @@ import { sm2 } from 'sm-crypto-v2';
 import { clearAccessAfterReload, getAPI } from '/@/utils/axios-utils';
 import { SysFileApi, SysUserApi } from '/@/api-services/api';
 import { ChangePwdInput, SysUser, SysFile } from '/@/api-services/models';
-
+import { useLangStore } from '/@/stores/useLangStore';
+const langStore = useLangStore();
 const stores = useUserInfo();
 const { userInfos } = storeToRefs(stores);
 const uploadSignRef = ref<UploadInstance>();
@@ -198,12 +206,17 @@ const state = reactive({
 	signFileList: [] as any,
 	passwordNew2: '',
 	cropperTitle: '',
+	languages: [] as any[], // 语言数据
 });
 
 onMounted(async () => {
 	state.loading = true;
 	var res = await getAPI(SysUserApi).apiSysUserBaseInfoGet();
 	state.ruleFormBase = res.data.result ?? { account: '' };
+	if (langStore.languages.length === 0) {
+        await langStore.loadLanguages();
+    }
+	state.languages = langStore.languages;
 	state.loading = false;
 });