Переглянути джерело

chore: 优化并修复代码部分已知bug

喵你个旺呀 1 рік тому
батько
коміт
5f64b819fa

+ 49 - 15
Admin.NET/Admin.NET.Core/Service/CodeGen/CustomViewEngine.cs

@@ -38,14 +38,14 @@ public class CustomViewEngine : ViewEngineModel
     
     public bool HasJoinTable { get; set; }
     
-    public bool HasSetStatus { get; set; }
-    
     public bool HasEnumField { get; set; }
     
     public bool HasDictField { get; set; }
     
     public bool HasConstField { get; set; }
     
+    public bool HasSetStatus => TableField.Any(IsStatus);
+    
     public List<CodeGenConfig> TableField { get; set; }
     
     public List<CodeGenConfig> ImportFieldList { get; set; }
@@ -66,7 +66,17 @@ public class CustomViewEngine : ViewEngineModel
 
     public List<string> PrimaryKeyNames => PrimaryKeyFieldList.Select(u => u.PropertyName).ToList();
     
-    public string PrimaryKeysFormat(string separator, string format) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, u.PropertyName)));
+    /// <summary>
+    /// 格式化主键查询条件
+    /// 例: PrimaryKeysFormat(" && ", "u.{0} == input.{0}")
+    /// 单主键返回 u.Id == input.Id
+    /// 组合主键返回 u.Id == input.Id && u.FkId == input.FkId
+    /// </summary>
+    /// <param name="separator">分隔符</param>
+    /// <param name="format">模板字符串</param>
+    /// <param name="lowerFirstLetter">字段首字母小写</param>
+    /// <returns></returns>
+    public string PrimaryKeysFormat(string separator, string format, bool lowerFirstLetter = false) => string.Join(separator, PrimaryKeyFieldList.Select(u => string.Format(format, lowerFirstLetter ? u.LowerPropertyName : u.PropertyName)));
     
     /// <summary>
     /// 注入的服务
@@ -75,30 +85,26 @@ public class CustomViewEngine : ViewEngineModel
     public Dictionary<string, string> InjectServiceMap {
         get
         {
-            var text = PrimaryKeysFormat(" && ", "u.{0} == input.{0}");
             var injectMap = new Dictionary<string, string>();
             if (UploadFieldList.Count > 0) injectMap.Add(nameof(SysFileService), ToLowerFirstLetter(nameof(SysFileService)));
-            if (DropdownFieldList.Count > 0) injectMap.Add(nameof(ISqlSugarClient), ToLowerFirstLetter(nameof(ISqlSugarClient).TrimStart('I')));
+            if (DropdownFieldList.Count > 0 || ImportFieldList.Count > 0) injectMap.Add(nameof(ISqlSugarClient), ToLowerFirstLetter(nameof(ISqlSugarClient).TrimStart('I')));
             if (ImportFieldList.Any(c => c.EffectType == "DictSelector")) injectMap.Add(nameof(SysDictTypeService), ToLowerFirstLetter(nameof(SysDictTypeService)));
             return injectMap;
         }
     }
-
+    
     /// <summary>
     /// 服务构造参数
     /// </summary>
-    public string InjectServiceArgs => InjectServiceMap.Count > 0 ? string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : "";
-   
-    /// <summary>
-    /// 导入唯一性校验配置
-    /// </summary>
-    public List<TableUniqueConfigItem> ImportUniqueConfigList => TableUniqueConfigList.Where(c => c.Columns.All(x1 => ImportFieldList.Any(x2 => x2.PropertyName == x1))).ToList();
+    public string InjectServiceArgs => InjectServiceMap.Count > 0 ? ", " +string.Join(", ", InjectServiceMap.Select(kv => $"{kv.Key} {kv.Value}")) : "";
     
     /// <summary>
-    /// 增改唯一性校验配置
+    /// 判断字段是否为状态字段
     /// </summary>
-    public List<TableUniqueConfigItem> AddUpdateUniqueConfigList => TableUniqueConfigList.Where(c => c.Columns.All(x1 => UploadFieldList.Any(x2 => x2.PropertyName == x1))).ToList();
-
+    /// <param name="column"></param>
+    /// <returns></returns>
+    public bool IsStatus(CodeGenConfig column) => column.PropertyName == nameof(SysUser.Status) && column.NetType == nameof(StatusEnum);
+    
     /// <summary>
     /// 获取首字母小写字符串
     /// </summary>
@@ -112,4 +118,32 @@ public class CustomViewEngine : ViewEngineModel
     /// <param name="netType"></param>
     /// <returns></returns>
     public string GetNullableNetType(string netType) => Regex.IsMatch(netType, "(.*?Enum|bool|char|int|long|double|float|decimal)[?]?") ? netType.TrimEnd('?') + "?" : netType;
+    
+    /// <summary>
+    /// 获取前端表格列定义的属性
+    /// </summary>
+    /// <param name="column"></param>
+    /// <returns></returns>
+    public string GetElTableColumnCustomProperty(CodeGenConfig column)
+    {
+        var content = $"prop='{column.LowerPropertyName}' label='{column.ColumnComment}'";
+        if (IsStatus(column)) content += $" v-auth=\"'{LowerClassName}:setStatus'\"";
+        if (column.WhetherSortable == "Y") content += " sortable='custom'";
+        return content;
+    }
+
+    /// <summary>
+    /// 设置默认值
+    /// </summary>
+    /// <param name="column"></param>
+    /// <returns></returns>
+    public string GetAddDefaultValue()
+    {
+        var content = "";
+        var status = TableField.FirstOrDefault(IsStatus);
+        var orderNo = TableField.FirstOrDefault(c => c.NetType.TrimEnd('?') == "int" && c.PropertyName == nameof(SysUser.OrderNo));
+        if (status != null) content += $"{status.LowerPropertyName}: {(int)StatusEnum.Enable},";
+        if (orderNo != null) content += $"{orderNo.LowerPropertyName}: 100,";
+        return content;
+    }
 }

+ 29 - 2
Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/CodeGenConfig.cs

@@ -177,6 +177,34 @@ public class CodeGenConfig
     /// 排序
     /// </summary>
     public int OrderNo { get; set; }
+
+    /// <summary>
+    /// 去掉尾部Id的属性名
+    /// </summary>
+    public string LowerPropertyNameTrimEndId => LowerPropertyName.TrimEnd("Id");
+
+    /// <summary>
+    /// 联表显示字段名称
+    /// </summary>
+    public string FkLinkDisplayPropertyName => EffectType switch
+    {
+        "ForeignKey" => $"{LowerPropertyNameTrimEndId}FkDisplayName",
+        "ApiTreeSelector" => $"{LowerPropertyNameTrimEndId}DisplayName",
+        _ => PropertyName
+    };
+
+    /// <summary>
+    /// 联表显示字段首字母小写名称
+    /// </summary>
+    public string LowerFkLinkDisplayPropertyName
+    {
+        get
+        {
+            var displayPropertyName = FkLinkDisplayPropertyName;
+            if (string.IsNullOrWhiteSpace(displayPropertyName)) return null;
+            return displayPropertyName[..1].ToLower() + displayPropertyName[1..];
+        }
+    }
     
     /// <summary>
     /// 获取外键显示值语句
@@ -184,6 +212,5 @@ public class CodeGenConfig
     /// <param name="tableAlias">表别名</param>
     /// <param name="separator">多字段时的连接符</param>
     /// <returns></returns>
-    public string GetDisplayColumn(string tableAlias, string separator = "-") => string.Join(separator, FkDisplayColumnList.Select(name => $"{{{tableAlias}.{name}}}"));
-
+    public string GetDisplayColumn(string tableAlias, string separator = "-") => "$\"" + string.Join(separator, FkDisplayColumnList.Select(name => $"{{{tableAlias}.{name}}}")) + "\"";
 }

+ 8 - 0
Admin.NET/Admin.NET.Core/Service/CodeGen/Dto/TableUniqueConfigItem.cs

@@ -20,4 +20,12 @@ public class TableUniqueConfigItem
     /// 描述信息
     /// </summary>
     public string Message { get; set; }
+    
+    /// <summary>
+    /// 格式化查询条件
+    /// </summary>
+    /// <param name="separator">分隔符</param>
+    /// <param name="format">模板字符串</param>
+    /// <returns></returns>
+    public string Format(string separator, string format) => string.Join(separator, Columns.Select(name => string.Format(format, name)));
 }

+ 805 - 809
Admin.NET/Admin.NET.Core/Service/CodeGen/SysCodeGenService.cs

@@ -1,810 +1,806 @@
-// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
-//
-// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
-//
-// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
-
-using System.IO.Compression;
-
-namespace Admin.NET.Core.Service;
-
-/// <summary>
-/// 系统代码生成器服务 🧩
-/// </summary>
-[ApiDescriptionSettings(Order = 270)]
-public class SysCodeGenService : IDynamicApiController, ITransient
-{
-    private readonly ISqlSugarClient _db;
-
-    private readonly SysCodeGenConfigService _codeGenConfigService;
-    private readonly IViewEngine _viewEngine;
-    private readonly CodeGenOptions _codeGenOptions;
-
-    public SysCodeGenService(ISqlSugarClient db,
-        SysCodeGenConfigService codeGenConfigService,
-        IViewEngine viewEngine,
-        IOptions<CodeGenOptions> codeGenOptions)
-    {
-        _db = db;
-        _codeGenConfigService = codeGenConfigService;
-        _viewEngine = viewEngine;
-        _codeGenOptions = codeGenOptions.Value;
-    }
-
-    /// <summary>
-    /// 获取代码生成分页列表 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [DisplayName("获取代码生成分页列表")]
-    public async Task<SqlSugarPagedList<SysCodeGen>> Page(CodeGenInput input)
-    {
-        return await _db.Queryable<SysCodeGen>()
-            .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
-            .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
-            .ToPagedListAsync(input.Page, input.PageSize);
-    }
-
-    /// <summary>
-    /// 增加代码生成 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [ApiDescriptionSettings(Name = "Add"), HttpPost]
-    [DisplayName("增加代码生成")]
-    public async Task AddCodeGen(AddCodeGenInput input)
-    {
-        var isExist = await _db.Queryable<SysCodeGen>().Where(u => u.TableName == input.TableName).AnyAsync();
-        if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
-
-        if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
-
-        var codeGen = input.Adapt<SysCodeGen>();
-        var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync();
-
-        // 增加配置表
-        _codeGenConfigService.AddList(GetColumnList(input), newCodeGen);
-    }
-
-    /// <summary>
-    /// 更新代码生成 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [ApiDescriptionSettings(Name = "Update"), HttpPost]
-    [DisplayName("更新代码生成")]
-    public async Task UpdateCodeGen(UpdateCodeGenInput input)
-    {
-        var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
-        if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
-
-        if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
-        var codeGen = input.Adapt<SysCodeGen>();
-        await _db.Updateable(codeGen).ExecuteCommandAsync();
-
-        // 更新配置表
-        await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id);
-        _codeGenConfigService.AddList(GetColumnList(input.Adapt<AddCodeGenInput>()), codeGen);
-    }
-
-    /// <summary>
-    /// 删除代码生成 🔖
-    /// </summary>
-    /// <param name="inputs"></param>
-    /// <returns></returns>
-    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
-    [DisplayName("删除代码生成")]
-    public async Task DeleteCodeGen(List<DeleteCodeGenInput> inputs)
-    {
-        if (inputs == null || inputs.Count < 1) return;
-
-        var codeGenConfigTaskList = new List<Task>();
-        inputs.ForEach(u =>
-        {
-            _db.Deleteable<SysCodeGen>().In(u.Id).ExecuteCommand();
-
-            // 删除配置表
-            codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id));
-        });
-        await Task.WhenAll(codeGenConfigTaskList);
-    }
-
-    /// <summary>
-    /// 获取代码生成详情 🔖
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    [DisplayName("获取代码生成详情")]
-    public async Task<SysCodeGen> GetDetail([FromQuery] QueryCodeGenInput input)
-    {
-        return await _db.Queryable<SysCodeGen>().SingleAsync(u => u.Id == input.Id);
-    }
-
-    /// <summary>
-    /// 获取数据库库集合 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取数据库库集合")]
-    public async Task<List<DatabaseOutput>> GetDatabaseList()
-    {
-        var dbConfigs = App.GetOptions<DbConnectionOptions>().ConnectionConfigs;
-        return await Task.FromResult(dbConfigs.Adapt<List<DatabaseOutput>>());
-    }
-
-    /// <summary>
-    /// 获取数据库表(实体)集合 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取数据库表(实体)集合")]
-    public async Task<List<TableOutput>> GetTableList(string configId = SqlSugarConst.MainConfigId)
-    {
-        var provider = _db.AsTenant().GetConnectionScope(configId);
-        var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); // 不能走缓存,否则切库不起作用
-
-        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId));
-
-        // var dbTableNames = dbTableInfos.Select(u => u.Name.ToLower()).ToList();
-        IEnumerable<EntityInfo> entityInfos = await GetEntityInfos();
-
-        var tableOutputList = new List<TableOutput>();
-        foreach (var item in entityInfos)
-        {
-            var table = dbTableInfos.FirstOrDefault(u => string.Equals(u.Name, (config!.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(item.DbTableName) : item.DbTableName), StringComparison.CurrentCultureIgnoreCase));
-            if (table == null) continue;
-            tableOutputList.Add(new TableOutput
-            {
-                ConfigId = configId,
-                EntityName = item.EntityName,
-                TableName = table.Name,
-                TableComment = item.TableDescription
-            });
-        }
-        return tableOutputList;
-    }
-
-    /// <summary>
-    /// 根据表名获取列集合 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("根据表名获取列集合")]
-    public List<ColumnOuput> GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId)
-    {
-        // 切库---多库代码生成用
-        var provider = _db.AsTenant().GetConnectionScope(configId);
-
-        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId);
-        // 获取实体类型属性
-        var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName);
-        if (entityType == null) return null;
-        var entityBasePropertyNames = _codeGenOptions.EntityBaseColumn[nameof(EntityTenant)];
-        // 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null)
-        return provider.DbMaintenance.GetColumnInfosByTableName(entityType.Name).Select(u => new ColumnOuput
-        {
-            ColumnName = config!.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
-            ColumnKey = u.IsPrimarykey.ToString(),
-            DataType = u.DataType.ToString(),
-            NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
-            ColumnComment = u.ColumnDescription
-        }).ToList();
-    }
-
-    /// <summary>
-    /// 获取数据表列(实体属性)集合
-    /// </summary>
-    /// <returns></returns>
-    private List<ColumnOuput> GetColumnList([FromQuery] AddCodeGenInput input)
-    {
-        var entityType = GetEntityInfos().GetAwaiter().GetResult().FirstOrDefault(u => u.EntityName == input.TableName);
-        if (entityType == null)
-            return null;
-        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
-        var dbTableName = config!.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(entityType.DbTableName) : entityType.DbTableName;
-
-        // 切库---多库代码生成用
-        var provider = _db.AsTenant().GetConnectionScope(!string.IsNullOrEmpty(input.ConfigId) ? input.ConfigId : SqlSugarConst.MainConfigId);
-
-        var entityBasePropertyNames = _codeGenOptions.EntityBaseColumn[nameof(EntityTenant)];
-        var columnInfos = provider.DbMaintenance.GetColumnInfosByTableName(dbTableName, false);
-        var result = columnInfos.Select(u => new ColumnOuput
-        {
-            // 转下划线后的列名需要再转回来(暂时不转)
-            //ColumnName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
-            ColumnName = u.DbColumnName,
-            ColumnLength = u.Length,
-            IsPrimarykey = u.IsPrimarykey,
-            IsNullable = u.IsNullable,
-            ColumnKey = u.IsPrimarykey.ToString(),
-            NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
-            DataType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
-            ColumnComment = string.IsNullOrWhiteSpace(u.ColumnDescription) ? u.DbColumnName : u.ColumnDescription
-        }).ToList();
-
-        // 获取实体的属性信息,赋值给PropertyName属性(CodeFirst模式应以PropertyName为实际使用名称)
-        var entityProperties = entityType.Type.GetProperties();
-
-        for (int i = result.Count - 1; i >= 0; i--)
-        {
-            var columnOutput = result[i];
-            // 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
-            var propertyInfo = entityProperties.FirstOrDefault(u => string.Equals((u.GetCustomAttribute<SugarColumn>()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ??
-                entityProperties.FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>() != null && u.Name.ToLower() == (config.DbSettings.EnableUnderLine
-                ? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower()
-                : columnOutput.ColumnName.ToLower()));
-            if (propertyInfo != null)
-            {
-                columnOutput.PropertyName = propertyInfo.Name;
-                columnOutput.ColumnComment = propertyInfo.GetCustomAttribute<SugarColumn>()!.ColumnDescription;
-                var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
-                if (propertyInfo.PropertyType.IsEnum || (propertyType?.IsEnum ?? false))
-                {
-                    columnOutput.DictTypeCode = (propertyType ?? propertyInfo.PropertyType).Name;
-                }
-                else
-                {
-                    var dict = propertyInfo.GetCustomAttribute<DictAttribute>();
-                    if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode;
-                }
-            }
-            else
-            {
-                result.RemoveAt(i); // 移除没有定义此属性的字段
-            }
-        }
-        return result;
-    }
-
-    /// <summary>
-    /// 获取库表信息
-    /// </summary>
-    /// <returns></returns>
-    private async Task<IEnumerable<EntityInfo>> GetEntityInfos()
-    {
-        var entityInfos = new List<EntityInfo>();
-
-        var type = typeof(SugarTable);
-        var types = new List<Type>();
-        if (_codeGenOptions.EntityAssemblyNames != null)
-        {
-            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
-            foreach (var assembly in assemblies)
-            {
-                var assemblyName = assembly.GetName().Name;
-                if (!_codeGenOptions.EntityAssemblyNames.Contains(assemblyName) &&
-                    !_codeGenOptions.EntityAssemblyNames.Any(name => assemblyName!.Contains(name)))
-                {
-                    continue;
-                }
-
-                Assembly asm = Assembly.Load(assemblyName!);
-                types.AddRange(asm.GetExportedTypes().ToList());
-            }
-        }
-
-        Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray();
-
-        foreach (var ct in cosType)
-        {
-            var sugarAttribute = ct.GetCustomAttributes(type, true).FirstOrDefault();
-
-            var des = ct.GetCustomAttributes(typeof(DescriptionAttribute), true);
-            var description = "";
-            if (des.Length > 0)
-            {
-                description = ((DescriptionAttribute)des[0]).Description;
-            }
-            entityInfos.Add(new EntityInfo()
-            {
-                EntityName = ct.Name,
-                DbTableName = sugarAttribute == null ? ct.Name : ((SugarTable)sugarAttribute).TableName,
-                TableDescription = sugarAttribute == null ? description : ((SugarTable)sugarAttribute).TableDescription,
-                Type = ct
-            });
-        }
-        return await Task.FromResult(entityInfos);
-
-        bool IsMyAttribute(Attribute[] o) => o.Any(a => a.GetType() == type);
-    }
-
-    /// <summary>
-    /// 获取程序保存位置 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取程序保存位置")]
-    public List<string> GetApplicationNamespaces()
-    {
-        return _codeGenOptions.BackendApplicationNamespaces;
-    }
-
-    /// <summary>
-    /// 代码生成到本地 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("代码生成到本地")]
-    public async Task<dynamic> RunLocal(SysCodeGen input)
-    {
-        if (string.IsNullOrEmpty(input.GenerateType))
-            input.GenerateType = "200";
-
-        // 先删除该表已生成的菜单列表
-        List<string> targetPathList;
-        var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
-        if (input.GenerateType.StartsWith('1'))
-        {
-            targetPathList = GetZipPathList(input);
-            if (Directory.Exists(zipPath)) Directory.Delete(zipPath, true);
-        }
-        else
-            targetPathList = GetTargetPathList(input);
-
-        var (tableFieldList, result) = await RenderTemplateAsync(input);
-        var templatePathList = GetTemplatePathList(input);
-        for (var i = 0; i < templatePathList.Count; i++)
-        {
-            var content = result.GetValueOrDefault(templatePathList[i]?.TrimEnd(".vm"));
-            if (string.IsNullOrWhiteSpace(content)) continue;
-            var dirPath = new DirectoryInfo(targetPathList[i]).Parent!.FullName;
-            if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
-            _ = File.WriteAllTextAsync(targetPathList[i], content, Encoding.UTF8);
-        }
-
-        if (input.GenerateMenu) await AddMenu(input.TableName, input.BusName, input.MenuPid ?? 0, input.MenuIcon, input.PagePath, tableFieldList);
-        
-        // 非ZIP压缩返回空
-        if (!input.GenerateType.StartsWith('1')) return null;
-        
-        // 判断是否存在同名称文件
-        string downloadPath = zipPath + ".zip";
-        if (File.Exists(downloadPath)) File.Delete(downloadPath);
-        
-        // 创建zip文件并返回下载地址
-        ZipFile.CreateFromDirectory(zipPath, downloadPath);
-        return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" };
-    }
-
-    /// <summary>
-    /// 获取代码生成预览 🔖
-    /// </summary>
-    /// <returns></returns>
-    [DisplayName("获取代码生成预览")]
-    // ReSharper disable once MemberCanBePrivate.Global
-    public async Task<Dictionary<string, string>> Preview(SysCodeGen input)
-    {
-        var (_, result) = await RenderTemplateAsync(input);
-        return result;
-    }
-
-    /// <summary>
-    /// 渲染模板
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    private async Task<(List<CodeGenConfig> tableFieldList, Dictionary<string, string> result)> RenderTemplateAsync(SysCodeGen input)
-    {
-        var tableFieldList = await _codeGenConfigService.GetList(new CodeGenConfig { CodeGenId = input.Id }); // 字段集合
-        var joinTableList = tableFieldList.Where(u => u.EffectType is "Upload" or "ForeignKey" or "ApiTreeSelector").ToList(); // 需要连表查询的字段
-
-        var data = new CustomViewEngine
-        {
-            ConfigId = input.ConfigId,
-            BusName = input.BusName,
-            PagePath = input.PagePath,
-            NameSpace = input.NameSpace,
-            ClassName = input.TableName,
-            PrintType = input.PrintType,
-            PrintName = input.PrintName,
-            AuthorName = input.AuthorName,
-            ProjectLastName = input.NameSpace!.Split('.').Last(),
-            LowerClassName = input.TableName![..1].ToLower() + input.TableName[1..],
-            TableUniqueConfigList = input.TableUniqueList ?? new(),
-            
-            TableField = tableFieldList,
-            QueryWhetherList = tableFieldList.Where(u => u.WhetherQuery == "Y").ToList(),
-            ImportFieldList = tableFieldList.Where(u => u.WhetherImport == "Y").ToList(),
-            UploadFieldList = tableFieldList.Where(u => u.EffectType == "Upload").ToList(),
-            DropdownFieldList = joinTableList.Where(u => u.EffectType != "Upload").ToList(),
-            PrimaryKeyFieldList = tableFieldList.Where(c => c.ColumnKey == "True").ToList(),
-            AddUpdateFieldList = tableFieldList.Where(u => u.WhetherAddUpdate == "Y").ToList(),
-            
-            HasJoinTable = joinTableList.Count > 0,
-            HasDictField = tableFieldList.Any(u => u.EffectType == "DictSelector"),
-            HasEnumField = tableFieldList.Any(u => u.EffectType == "EnumSelector"),
-            HasConstField = tableFieldList.Any(u => u.EffectType == "ConstSelector"),
-            HasLikeQuery = tableFieldList.Any(c => c.WhetherQuery == "Y" && c.QueryType == "like"),
-            HasSetStatus = tableFieldList.Any(c => c.NetType == nameof(StatusEnum) && c.PropertyName == nameof(SysUser.Status)),
-        };
-
-        // 获取模板文件并替换
-        var templatePathList = GetTemplatePathList();
-        var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
-
-        var result = new Dictionary<string, string>();
-        foreach (var path in templatePathList)
-        {
-            var templateFilePath = Path.Combine(templatePath, path);
-            if (!File.Exists(templateFilePath)) continue;
-            var tContent = await File.ReadAllTextAsync(templateFilePath);
-            var tResult = await _viewEngine.RunCompileFromCachedAsync(tContent, data, builderAction: builder =>
-            {
-                builder.AddAssemblyReferenceByName("System.Text.RegularExpressions");
-                builder.AddAssemblyReferenceByName("System.Collections");
-                builder.AddAssemblyReferenceByName("System.Linq");
-                
-                builder.AddUsing("System.Text.RegularExpressions");
-                builder.AddUsing("System.Collections.Generic");
-                builder.AddUsing("System.Linq");
-            });
-            result.Add(path?.TrimEnd(".vm"), tResult);
-        }
-        return (tableFieldList, result);
-    }
-
-    /// <summary>
-    /// 增加菜单
-    /// </summary>
-    /// <param name="className"></param>
-    /// <param name="busName"></param>
-    /// <param name="pid"></param>
-    /// <param name="menuIcon"></param>
-    /// <param name="pagePath"></param>
-    /// <param name="tableFieldList"></param>
-    /// <returns></returns>
-    private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
-    {
-        var pPath = string.Empty;
-        // 若 pid=0 为顶级则创建菜单目录
-        if (pid == 0)
-        {
-            // 目录
-            var menuType0 = new SysMenu
-            {
-                Pid = 0,
-                Title = busName + "管理",
-                Type = MenuTypeEnum.Dir,
-                Icon = "robot",
-                Path = "/" + className.ToLower(),
-                Component = "Layout",
-            };
-            // 若先前存在则删除本级和下级
-            var menuList0 = await _db.Queryable<SysMenu>().Where(u => u.Title == menuType0.Title && u.Type == menuType0.Type).ToListAsync();
-            if (menuList0.Count > 0)
-            {
-                var listIds = menuList0.Select(u => u.Id).ToList();
-                var childrenIds = new List<long>();
-                foreach (var item in listIds)
-                {
-                    var children = await _db.Queryable<SysMenu>().ToChildListAsync(u => u.Pid, item);
-                    childrenIds.AddRange(children.Select(u => u.Id).ToList());
-                }
-                listIds.AddRange(childrenIds);
-                await _db.Deleteable<SysMenu>().Where(u => listIds.Contains(u.Id)).ExecuteCommandAsync();
-                await _db.Deleteable<SysRoleMenu>().Where(u => listIds.Contains(u.MenuId)).ExecuteCommandAsync();
-            }
-            pid = (await _db.Insertable(menuType0).ExecuteReturnEntityAsync()).Id;
-        }
-        else
-        {
-            var pMenu = await _db.Queryable<SysMenu>().FirstAsync(u => u.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
-            pPath = pMenu.Path;
-        }
-
-        // 菜单
-        var menuType = new SysMenu
-        {
-            Pid = pid,
-            Title = busName + "管理",
-            Name = className[..1].ToLower() + className[1..],
-            Type = MenuTypeEnum.Menu,
-            Icon = menuIcon,
-            Path = pPath + "/" + className.ToLower(),
-            Component = "/" + pagePath + "/" + className[..1].ToLower() + className[1..] + "/index",
-        };
-        // 若先前存在则删除本级和下级
-        var menuListCurrent = await _db.Queryable<SysMenu>().Where(u => u.Title == menuType.Title && u.Type == menuType.Type).ToListAsync();
-        if (menuListCurrent.Count > 0)
-        {
-            var listIds = menuListCurrent.Select(u => u.Id).ToList();
-            var childListIds = new List<long>();
-            foreach (var item in listIds)
-            {
-                var childList = await _db.Queryable<SysMenu>().ToChildListAsync(u => u.Pid, item);
-                childListIds.AddRange(childList.Select(u => u.Id).ToList());
-            }
-            listIds.AddRange(childListIds);
-            await _db.Deleteable<SysMenu>().Where(u => listIds.Contains(u.Id)).ExecuteCommandAsync();
-            await _db.Deleteable<SysRoleMenu>().Where(u => listIds.Contains(u.MenuId)).ExecuteCommandAsync();
-        }
-
-        var menuPid = (await _db.Insertable(menuType).ExecuteReturnEntityAsync()).Id;
-        int menuOrder = 100;
-        // 按钮-page
-        var menuTypePage = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "查询",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":page",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-detail
-        var menuTypeDetail = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "详情",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":detail",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-add
-        var menuTypeAdd = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "增加",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":add",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-delete
-        var menuTypeDelete = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "删除",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":delete",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-update
-        var menuTypeUpdate = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "编辑",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":update",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-        
-        // 按钮-Status
-        var menuTypeStatus = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "状态",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":setStatus",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-        
-        // 按钮-batchDelete
-        var menuTypeBatchDelete = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "批量删除",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":batchDelete",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-print
-        var menuTypePrint = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "打印",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":print",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-import
-        var menuTypeImport = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "导入",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":import",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        // 按钮-export
-        var menuTypeExport = new SysMenu
-        {
-            Pid = menuPid,
-            Title = "导出",
-            Type = MenuTypeEnum.Btn,
-            Permission = className[..1].ToLower() + className[1..] + ":export",
-            OrderNo = menuOrder
-        };
-        menuOrder += 10;
-
-        var menuList = new List<SysMenu> { menuTypePage, menuTypeDetail, menuTypeAdd, menuTypeStatus, menuTypeDelete, menuTypeBatchDelete, menuTypeUpdate, menuTypePrint, menuTypeImport, menuTypeExport };
-        // 加入ForeignKey、Upload、ApiTreeSelector 等接口的权限
-        // 在生成表格时,有些字段只是查询时显示,不需要填写(WhetherAddUpdate),所以这些字段没必要生成相应接口
-        var fkTableList = tableFieldList.Where(u => u.EffectType == "ForeignKey" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")).ToList();
-        foreach (var @column in fkTableList)
-        {
-            var menuType1 = new SysMenu
-            {
-                Pid = menuPid,
-                Title = "外键" + @column.ColumnName,
-                Type = MenuTypeEnum.Btn,
-                Permission = className[..1].ToLower() + className[1..] + ":" + column.FkEntityName + column.ColumnName + "Dropdown",
-                OrderNo = menuOrder
-            };
-            menuOrder += 10;
-            menuList.Add(menuType1);
-        }
-        var treeSelectTableList = tableFieldList.Where(u => u.EffectType == "ApiTreeSelector").ToList();
-        foreach (var @column in treeSelectTableList)
-        {
-            var menuType1 = new SysMenu
-            {
-                Pid = menuPid,
-                Title = "树型" + @column.ColumnName,
-                Type = MenuTypeEnum.Btn,
-                Permission = className[..1].ToLower() + className[1..] + ":" + column.FkEntityName + "Tree",
-                OrderNo = menuOrder
-            };
-            menuOrder += 10;
-            menuList.Add(menuType1);
-        }
-        var uploadTableList = tableFieldList.Where(u => u.EffectType == "Upload").ToList();
-        foreach (var @column in uploadTableList)
-        {
-            var menuType1 = new SysMenu
-            {
-                Pid = menuPid,
-                Title = "上传" + @column.ColumnName,
-                Type = MenuTypeEnum.Btn,
-                Permission = className[..1].ToLower() + className[1..] + ":Upload" + column.ColumnName,
-                OrderNo = menuOrder
-            };
-            menuOrder += 10;
-            menuList.Add(menuType1);
-        }
-        await _db.Insertable(menuList).ExecuteCommandAsync();
-    }
-
-    /// <summary>
-    /// 获取模板文件路径集合
-    /// </summary>
-    /// <returns></returns>
-    private static List<string> GetTemplatePathList(SysCodeGen input)
-    {
-        if (input.GenerateType!.Substring(1, 1).Contains('1'))
-        {
-            return new() { "index.vue.vm", "editDialog.vue.vm", "manage.js.vm" };
-        }
-        else if (input.GenerateType.Substring(1, 1).Contains('2'))
-        {
-            return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm" };
-        }
-        else
-        {
-            return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "manage.js.vm" };
-        }
-    }
-
-    /// <summary>
-    /// 获取模板文件路径集合
-    /// </summary>
-    /// <returns></returns>
-    private static List<string> GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "manage.js.vm" };
-
-    /// <summary>
-    /// 设置生成文件路径
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    private List<string> GetTargetPathList(SysCodeGen input)
-    {
-        //var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
-        var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.FullName, input.NameSpace!, "Service", input.TableName!);
-        var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
-        var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
-        var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
-        var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
-        var frontendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
-        var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");//
-        var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
-        var apiJsPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
-
-        if (input.GenerateType!.Substring(1, 1).Contains('1'))
-        {
-            // 生成到本项目(前端)
-            return new List<string>()
-            {
-                indexPath,
-                formModalPath,
-                apiJsPath
-            };
-        }
-        else if (input.GenerateType.Substring(1, 1).Contains('2'))
-        {
-            // 生成到本项目(后端)
-            return new List<string>()
-            {
-                servicePath,
-                inputPath,
-                outputPath,
-                viewPath,
-            };
-        }
-        else
-        {
-            // 前后端同时生成到本项目
-            return new List<string>()
-            {
-                servicePath,
-                inputPath,
-                outputPath,
-                viewPath,
-                indexPath,
-                formModalPath,
-                apiJsPath
-            };
-        }
-    }
-
-    /// <summary>
-    /// 设置生成文件路径
-    /// </summary>
-    /// <param name="input"></param>
-    /// <returns></returns>
-    private List<string> GetZipPathList(SysCodeGen input)
-    {
-        var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
-
-        //var backendPath = Path.Combine(zipPath, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
-        var backendPath = Path.Combine(zipPath, input.NameSpace!, "Service", input.TableName);
-        var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
-        var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
-        var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
-        var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
-        var frontendPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
-        var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");
-        var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
-        var apiJsPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
-        if (input.GenerateType!.StartsWith("11"))
-        {
-            return new List<string>()
-            {
-                indexPath,
-                formModalPath,
-                apiJsPath
-            };
-        }
-        else if (input.GenerateType.StartsWith("12"))
-        {
-            return new List<string>()
-            {
-                servicePath,
-                inputPath,
-                outputPath,
-                viewPath
-            };
-        }
-        else
-        {
-            return new List<string>()
-            {
-                servicePath,
-                inputPath,
-                outputPath,
-                viewPath,
-                indexPath,
-                formModalPath,
-                apiJsPath
-            };
-        }
-    }
+// Admin.NET 项目的版权、商标、专利和其他相关权利均受相应法律法规的保护。使用本项目应遵守相关法律法规和许可证的要求。
+//
+// 本项目主要遵循 MIT 许可证和 Apache 许可证(版本 2.0)进行分发和使用。许可证位于源代码树根目录中的 LICENSE-MIT 和 LICENSE-APACHE 文件。
+//
+// 不得利用本项目从事危害国家安全、扰乱社会秩序、侵犯他人合法权益等法律法规禁止的活动!任何基于本项目二次开发而产生的一切法律纠纷和责任,我们不承担任何责任!
+
+using System.IO.Compression;
+
+namespace Admin.NET.Core.Service;
+
+/// <summary>
+/// 系统代码生成器服务 🧩
+/// </summary>
+[ApiDescriptionSettings(Order = 270)]
+public class SysCodeGenService : IDynamicApiController, ITransient
+{
+    private readonly ISqlSugarClient _db;
+
+    private readonly SysCodeGenConfigService _codeGenConfigService;
+    private readonly IViewEngine _viewEngine;
+    private readonly CodeGenOptions _codeGenOptions;
+
+    public SysCodeGenService(ISqlSugarClient db,
+        SysCodeGenConfigService codeGenConfigService,
+        IViewEngine viewEngine,
+        IOptions<CodeGenOptions> codeGenOptions)
+    {
+        _db = db;
+        _codeGenConfigService = codeGenConfigService;
+        _viewEngine = viewEngine;
+        _codeGenOptions = codeGenOptions.Value;
+    }
+
+    /// <summary>
+    /// 获取代码生成分页列表 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取代码生成分页列表")]
+    public async Task<SqlSugarPagedList<SysCodeGen>> Page(CodeGenInput input)
+    {
+        return await _db.Queryable<SysCodeGen>()
+            .WhereIF(!string.IsNullOrWhiteSpace(input.TableName), u => u.TableName.Contains(input.TableName.Trim()))
+            .WhereIF(!string.IsNullOrWhiteSpace(input.BusName), u => u.BusName.Contains(input.BusName.Trim()))
+            .ToPagedListAsync(input.Page, input.PageSize);
+    }
+
+    /// <summary>
+    /// 增加代码生成 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    [DisplayName("增加代码生成")]
+    public async Task AddCodeGen(AddCodeGenInput input)
+    {
+        var isExist = await _db.Queryable<SysCodeGen>().Where(u => u.TableName == input.TableName).AnyAsync();
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
+
+        if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
+
+        var codeGen = input.Adapt<SysCodeGen>();
+        var newCodeGen = await _db.Insertable(codeGen).ExecuteReturnEntityAsync();
+
+        // 增加配置表
+        _codeGenConfigService.AddList(GetColumnList(input), newCodeGen);
+    }
+
+    /// <summary>
+    /// 更新代码生成 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Update"), HttpPost]
+    [DisplayName("更新代码生成")]
+    public async Task UpdateCodeGen(UpdateCodeGenInput input)
+    {
+        var isExist = await _db.Queryable<SysCodeGen>().AnyAsync(u => u.TableName == input.TableName && u.Id != input.Id);
+        if (isExist) throw Oops.Oh(ErrorCodeEnum.D1400);
+
+        if (input.TableUniqueList?.Count > 0) input.TableUniqueConfig = JSON.Serialize(input.TableUniqueList);
+        var codeGen = input.Adapt<SysCodeGen>();
+        await _db.Updateable(codeGen).ExecuteCommandAsync();
+
+        // 更新配置表
+        await _codeGenConfigService.DeleteCodeGenConfig(codeGen.Id);
+        _codeGenConfigService.AddList(GetColumnList(input.Adapt<AddCodeGenInput>()), codeGen);
+    }
+
+    /// <summary>
+    /// 删除代码生成 🔖
+    /// </summary>
+    /// <param name="inputs"></param>
+    /// <returns></returns>
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    [DisplayName("删除代码生成")]
+    public async Task DeleteCodeGen(List<DeleteCodeGenInput> inputs)
+    {
+        if (inputs == null || inputs.Count < 1) return;
+
+        var codeGenConfigTaskList = new List<Task>();
+        inputs.ForEach(u =>
+        {
+            _db.Deleteable<SysCodeGen>().In(u.Id).ExecuteCommand();
+
+            // 删除配置表
+            codeGenConfigTaskList.Add(_codeGenConfigService.DeleteCodeGenConfig(u.Id));
+        });
+        await Task.WhenAll(codeGenConfigTaskList);
+    }
+
+    /// <summary>
+    /// 获取代码生成详情 🔖
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    [DisplayName("获取代码生成详情")]
+    public async Task<SysCodeGen> GetDetail([FromQuery] QueryCodeGenInput input)
+    {
+        return await _db.Queryable<SysCodeGen>().SingleAsync(u => u.Id == input.Id);
+    }
+
+    /// <summary>
+    /// 获取数据库库集合 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取数据库库集合")]
+    public async Task<List<DatabaseOutput>> GetDatabaseList()
+    {
+        var dbConfigs = App.GetOptions<DbConnectionOptions>().ConnectionConfigs;
+        return await Task.FromResult(dbConfigs.Adapt<List<DatabaseOutput>>());
+    }
+
+    /// <summary>
+    /// 获取数据库表(实体)集合 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取数据库表(实体)集合")]
+    public async Task<List<TableOutput>> GetTableList(string configId = SqlSugarConst.MainConfigId)
+    {
+        var provider = _db.AsTenant().GetConnectionScope(configId);
+        var dbTableInfos = provider.DbMaintenance.GetTableInfoList(false); // 不能走缓存,否则切库不起作用
+
+        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => configId.Equals(u.ConfigId));
+
+        // var dbTableNames = dbTableInfos.Select(u => u.Name.ToLower()).ToList();
+        IEnumerable<EntityInfo> entityInfos = await GetEntityInfos();
+
+        var tableOutputList = new List<TableOutput>();
+        foreach (var item in entityInfos)
+        {
+            var table = dbTableInfos.FirstOrDefault(u => string.Equals(u.Name, (config!.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(item.DbTableName) : item.DbTableName), StringComparison.CurrentCultureIgnoreCase));
+            if (table == null) continue;
+            tableOutputList.Add(new TableOutput
+            {
+                ConfigId = configId,
+                EntityName = item.EntityName,
+                TableName = table.Name,
+                TableComment = item.TableDescription
+            });
+        }
+        return tableOutputList;
+    }
+
+    /// <summary>
+    /// 根据表名获取列集合 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("根据表名获取列集合")]
+    public List<ColumnOuput> GetColumnListByTableName([Required] string tableName, string configId = SqlSugarConst.MainConfigId)
+    {
+        // 切库---多库代码生成用
+        var provider = _db.AsTenant().GetConnectionScope(configId);
+
+        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == configId);
+        // 获取实体类型属性
+        var entityType = provider.DbMaintenance.GetTableInfoList(false).FirstOrDefault(u => u.Name == tableName);
+        if (entityType == null) return null;
+        var entityBasePropertyNames = _codeGenOptions.EntityBaseColumn[nameof(EntityTenant)];
+        // 按原始类型的顺序获取所有实体类型属性(不包含导航属性,会返回null)
+        return provider.DbMaintenance.GetColumnInfosByTableName(entityType.Name).Select(u => new ColumnOuput
+        {
+            ColumnName = config!.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
+            ColumnKey = u.IsPrimarykey.ToString(),
+            DataType = u.DataType.ToString(),
+            NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
+            ColumnComment = u.ColumnDescription
+        }).ToList();
+    }
+
+    /// <summary>
+    /// 获取数据表列(实体属性)集合
+    /// </summary>
+    /// <returns></returns>
+    private List<ColumnOuput> GetColumnList([FromQuery] AddCodeGenInput input)
+    {
+        var entityType = GetEntityInfos().GetAwaiter().GetResult().FirstOrDefault(u => u.EntityName == input.TableName);
+        if (entityType == null)
+            return null;
+        var config = App.GetOptions<DbConnectionOptions>().ConnectionConfigs.FirstOrDefault(u => u.ConfigId.ToString() == input.ConfigId);
+        var dbTableName = config!.DbSettings.EnableUnderLine ? UtilMethods.ToUnderLine(entityType.DbTableName) : entityType.DbTableName;
+
+        // 切库---多库代码生成用
+        var provider = _db.AsTenant().GetConnectionScope(!string.IsNullOrEmpty(input.ConfigId) ? input.ConfigId : SqlSugarConst.MainConfigId);
+
+        var entityBasePropertyNames = _codeGenOptions.EntityBaseColumn[nameof(EntityTenant)];
+        var columnInfos = provider.DbMaintenance.GetColumnInfosByTableName(dbTableName, false);
+        var result = columnInfos.Select(u => new ColumnOuput
+        {
+            // 转下划线后的列名需要再转回来(暂时不转)
+            //ColumnName = config.DbSettings.EnableUnderLine ? CodeGenUtil.CamelColumnName(u.DbColumnName, entityBasePropertyNames) : u.DbColumnName,
+            ColumnName = u.DbColumnName,
+            ColumnLength = u.Length,
+            IsPrimarykey = u.IsPrimarykey,
+            IsNullable = u.IsNullable,
+            ColumnKey = u.IsPrimarykey.ToString(),
+            NetType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
+            DataType = CodeGenUtil.ConvertDataType(u, provider.CurrentConnectionConfig.DbType),
+            ColumnComment = string.IsNullOrWhiteSpace(u.ColumnDescription) ? u.DbColumnName : u.ColumnDescription
+        }).ToList();
+
+        // 获取实体的属性信息,赋值给PropertyName属性(CodeFirst模式应以PropertyName为实际使用名称)
+        var entityProperties = entityType.Type.GetProperties();
+
+        for (int i = result.Count - 1; i >= 0; i--)
+        {
+            var columnOutput = result[i];
+            // 先找自定义字段名的,如果找不到就再找自动生成字段名的(并且过滤掉没有SugarColumn的属性)
+            var propertyInfo = entityProperties.FirstOrDefault(u => string.Equals((u.GetCustomAttribute<SugarColumn>()?.ColumnName ?? ""), columnOutput.ColumnName, StringComparison.CurrentCultureIgnoreCase)) ??
+                entityProperties.FirstOrDefault(u => u.GetCustomAttribute<SugarColumn>() != null && u.Name.ToLower() == (config.DbSettings.EnableUnderLine
+                ? CodeGenUtil.CamelColumnName(columnOutput.ColumnName, entityBasePropertyNames).ToLower()
+                : columnOutput.ColumnName.ToLower()));
+            if (propertyInfo != null)
+            {
+                columnOutput.PropertyName = propertyInfo.Name;
+                columnOutput.ColumnComment = propertyInfo.GetCustomAttribute<SugarColumn>()!.ColumnDescription;
+                var propertyType = Nullable.GetUnderlyingType(propertyInfo.PropertyType);
+                if (propertyInfo.PropertyType.IsEnum || (propertyType?.IsEnum ?? false))
+                {
+                    columnOutput.DictTypeCode = (propertyType ?? propertyInfo.PropertyType).Name;
+                }
+                else
+                {
+                    var dict = propertyInfo.GetCustomAttribute<DictAttribute>();
+                    if (dict != null) columnOutput.DictTypeCode = dict.DictTypeCode;
+                }
+            }
+            else
+            {
+                result.RemoveAt(i); // 移除没有定义此属性的字段
+            }
+        }
+        return result;
+    }
+
+    /// <summary>
+    /// 获取库表信息
+    /// </summary>
+    /// <returns></returns>
+    private async Task<IEnumerable<EntityInfo>> GetEntityInfos()
+    {
+        var entityInfos = new List<EntityInfo>();
+
+        var type = typeof(SugarTable);
+        var types = new List<Type>();
+        if (_codeGenOptions.EntityAssemblyNames != null)
+        {
+            var assemblies = AppDomain.CurrentDomain.GetAssemblies();
+            foreach (var assembly in assemblies)
+            {
+                var assemblyName = assembly.GetName().Name;
+                if (!_codeGenOptions.EntityAssemblyNames.Contains(assemblyName) &&
+                    !_codeGenOptions.EntityAssemblyNames.Any(name => assemblyName!.Contains(name)))
+                {
+                    continue;
+                }
+
+                Assembly asm = Assembly.Load(assemblyName!);
+                types.AddRange(asm.GetExportedTypes().ToList());
+            }
+        }
+
+        Type[] cosType = types.Where(o => IsMyAttribute(Attribute.GetCustomAttributes(o, true))).ToArray();
+
+        foreach (var ct in cosType)
+        {
+            var sugarAttribute = ct.GetCustomAttributes(type, true).FirstOrDefault();
+
+            var des = ct.GetCustomAttributes(typeof(DescriptionAttribute), true);
+            var description = "";
+            if (des.Length > 0)
+            {
+                description = ((DescriptionAttribute)des[0]).Description;
+            }
+            entityInfos.Add(new EntityInfo()
+            {
+                EntityName = ct.Name,
+                DbTableName = sugarAttribute == null ? ct.Name : ((SugarTable)sugarAttribute).TableName,
+                TableDescription = sugarAttribute == null ? description : ((SugarTable)sugarAttribute).TableDescription,
+                Type = ct
+            });
+        }
+        return await Task.FromResult(entityInfos);
+
+        bool IsMyAttribute(Attribute[] o) => o.Any(a => a.GetType() == type);
+    }
+
+    /// <summary>
+    /// 获取程序保存位置 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取程序保存位置")]
+    public List<string> GetApplicationNamespaces()
+    {
+        return _codeGenOptions.BackendApplicationNamespaces;
+    }
+
+    /// <summary>
+    /// 代码生成到本地 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("代码生成到本地")]
+    public async Task<dynamic> RunLocal(SysCodeGen input)
+    {
+        if (string.IsNullOrEmpty(input.GenerateType))
+            input.GenerateType = "200";
+
+        // 先删除该表已生成的菜单列表
+        List<string> targetPathList;
+        var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
+        if (input.GenerateType.StartsWith('1'))
+        {
+            targetPathList = GetZipPathList(input);
+            if (Directory.Exists(zipPath)) Directory.Delete(zipPath, true);
+        }
+        else
+            targetPathList = GetTargetPathList(input);
+
+        var (tableFieldList, result) = await RenderTemplateAsync(input);
+        var templatePathList = GetTemplatePathList(input);
+        for (var i = 0; i < templatePathList.Count; i++)
+        {
+            var content = result.GetValueOrDefault(templatePathList[i]?.TrimEnd(".vm"));
+            if (string.IsNullOrWhiteSpace(content)) continue;
+            var dirPath = new DirectoryInfo(targetPathList[i]).Parent!.FullName;
+            if (!Directory.Exists(dirPath)) Directory.CreateDirectory(dirPath);
+            _ = File.WriteAllTextAsync(targetPathList[i], content, Encoding.UTF8);
+        }
+
+        if (input.GenerateMenu) await AddMenu(input.TableName, input.BusName, input.MenuPid ?? 0, input.MenuIcon, input.PagePath, tableFieldList);
+        
+        // 非ZIP压缩返回空
+        if (!input.GenerateType.StartsWith('1')) return null;
+        
+        // 判断是否存在同名称文件
+        string downloadPath = zipPath + ".zip";
+        if (File.Exists(downloadPath)) File.Delete(downloadPath);
+        
+        // 创建zip文件并返回下载地址
+        ZipFile.CreateFromDirectory(zipPath, downloadPath);
+        return new { url = $"{App.HttpContext.Request.Scheme}://{App.HttpContext.Request.Host.Value}/codeGen/{input.TableName}.zip" };
+    }
+
+    /// <summary>
+    /// 获取代码生成预览 🔖
+    /// </summary>
+    /// <returns></returns>
+    [DisplayName("获取代码生成预览")]
+    // ReSharper disable once MemberCanBePrivate.Global
+    public async Task<Dictionary<string, string>> Preview(SysCodeGen input)
+    {
+        var (_, result) = await RenderTemplateAsync(input);
+        return result;
+    }
+
+    /// <summary>
+    /// 渲染模板
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    private async Task<(List<CodeGenConfig> tableFieldList, Dictionary<string, string> result)> RenderTemplateAsync(SysCodeGen input)
+    {
+        var tableFieldList = await _codeGenConfigService.GetList(new CodeGenConfig { CodeGenId = input.Id }); // 字段集合
+        var joinTableList = tableFieldList.Where(u => u.EffectType is "Upload" or "ForeignKey" or "ApiTreeSelector").ToList(); // 需要连表查询的字段
+
+        var data = new CustomViewEngine
+        {
+            ConfigId = input.ConfigId,
+            BusName = input.BusName,
+            PagePath = input.PagePath,
+            NameSpace = input.NameSpace,
+            ClassName = input.TableName,
+            PrintType = input.PrintType,
+            PrintName = input.PrintName,
+            AuthorName = input.AuthorName,
+            ProjectLastName = input.NameSpace!.Split('.').Last(),
+            LowerClassName = input.TableName![..1].ToLower() + input.TableName[1..],
+            TableUniqueConfigList = input.TableUniqueList ?? new(),
+            
+            TableField = tableFieldList,
+            QueryWhetherList = tableFieldList.Where(u => u.WhetherQuery == "Y").ToList(),
+            ImportFieldList = tableFieldList.Where(u => u.WhetherImport == "Y").ToList(),
+            UploadFieldList = tableFieldList.Where(u => u.EffectType == "Upload").ToList(),
+            DropdownFieldList = joinTableList.Where(u => u.EffectType != "Upload").ToList(),
+            PrimaryKeyFieldList = tableFieldList.Where(c => c.ColumnKey == "True").ToList(),
+            AddUpdateFieldList = tableFieldList.Where(u => u.WhetherAddUpdate == "Y").ToList(),
+            
+            HasJoinTable = joinTableList.Count > 0,
+            HasDictField = tableFieldList.Any(u => u.EffectType == "DictSelector"),
+            HasEnumField = tableFieldList.Any(u => u.EffectType == "EnumSelector"),
+            HasConstField = tableFieldList.Any(u => u.EffectType == "ConstSelector"),
+            HasLikeQuery = tableFieldList.Any(c => c.WhetherQuery == "Y" && c.QueryType == "like")
+        };
+
+        // 获取模板文件并替换
+        var templatePathList = GetTemplatePathList();
+        var templatePath = Path.Combine(App.WebHostEnvironment.WebRootPath, "template");
+
+        var result = new Dictionary<string, string>();
+        foreach (var path in templatePathList)
+        {
+            var templateFilePath = Path.Combine(templatePath, path);
+            if (!File.Exists(templateFilePath)) continue;
+            var tContent = await File.ReadAllTextAsync(templateFilePath);
+            var tResult = await _viewEngine.RunCompileFromCachedAsync(tContent, data, builderAction: builder =>
+            {
+                builder.AddAssemblyReferenceByName("System.Text.RegularExpressions");
+                builder.AddAssemblyReferenceByName("System.Collections");
+                builder.AddAssemblyReferenceByName("System.Linq");
+                
+                builder.AddUsing("System.Text.RegularExpressions");
+                builder.AddUsing("System.Collections.Generic");
+                builder.AddUsing("System.Linq");
+            });
+            result.Add(path?.TrimEnd(".vm"), tResult);
+        }
+        return (tableFieldList, result);
+    }
+
+    /// <summary>
+    /// 增加菜单
+    /// </summary>
+    /// <param name="className"></param>
+    /// <param name="busName"></param>
+    /// <param name="pid"></param>
+    /// <param name="menuIcon"></param>
+    /// <param name="pagePath"></param>
+    /// <param name="tableFieldList"></param>
+    /// <returns></returns>
+    private async Task AddMenu(string className, string busName, long pid, string menuIcon, string pagePath, List<CodeGenConfig> tableFieldList)
+    {
+        var pPath = string.Empty;
+        // 若 pid=0 为顶级则创建菜单目录
+        if (pid == 0)
+        {
+            // 目录
+            var menuType0 = new SysMenu
+            {
+                Pid = 0,
+                Title = busName + "管理",
+                Type = MenuTypeEnum.Dir,
+                Icon = "robot",
+                Path = "/" + className.ToLower(),
+                Component = "Layout",
+            };
+            // 若先前存在则删除本级和下级
+            var menuList0 = await _db.Queryable<SysMenu>().Where(u => u.Title == menuType0.Title && u.Type == menuType0.Type).ToListAsync();
+            if (menuList0.Count > 0)
+            {
+                var listIds = menuList0.Select(u => u.Id).ToList();
+                var childrenIds = new List<long>();
+                foreach (var item in listIds)
+                {
+                    var children = await _db.Queryable<SysMenu>().ToChildListAsync(u => u.Pid, item);
+                    childrenIds.AddRange(children.Select(u => u.Id).ToList());
+                }
+                listIds.AddRange(childrenIds);
+                await _db.Deleteable<SysMenu>().Where(u => listIds.Contains(u.Id)).ExecuteCommandAsync();
+                await _db.Deleteable<SysRoleMenu>().Where(u => listIds.Contains(u.MenuId)).ExecuteCommandAsync();
+            }
+            pid = (await _db.Insertable(menuType0).ExecuteReturnEntityAsync()).Id;
+        }
+        else
+        {
+            var pMenu = await _db.Queryable<SysMenu>().FirstAsync(u => u.Id == pid) ?? throw Oops.Oh(ErrorCodeEnum.D1505);
+            pPath = pMenu.Path;
+        }
+
+        // 菜单
+        var menuType = new SysMenu
+        {
+            Pid = pid,
+            Title = busName + "管理",
+            Name = className[..1].ToLower() + className[1..],
+            Type = MenuTypeEnum.Menu,
+            Icon = menuIcon,
+            Path = pPath + "/" + className.ToLower(),
+            Component = "/" + pagePath + "/" + className[..1].ToLower() + className[1..] + "/index",
+        };
+        // 若先前存在则删除本级和下级
+        var menuListCurrent = await _db.Queryable<SysMenu>().Where(u => u.Title == menuType.Title && u.Type == menuType.Type).ToListAsync();
+        if (menuListCurrent.Count > 0)
+        {
+            var listIds = menuListCurrent.Select(u => u.Id).ToList();
+            var childListIds = new List<long>();
+            foreach (var item in listIds)
+            {
+                var childList = await _db.Queryable<SysMenu>().ToChildListAsync(u => u.Pid, item);
+                childListIds.AddRange(childList.Select(u => u.Id).ToList());
+            }
+            listIds.AddRange(childListIds);
+            await _db.Deleteable<SysMenu>().Where(u => listIds.Contains(u.Id)).ExecuteCommandAsync();
+            await _db.Deleteable<SysRoleMenu>().Where(u => listIds.Contains(u.MenuId)).ExecuteCommandAsync();
+        }
+
+        var menuPid = (await _db.Insertable(menuType).ExecuteReturnEntityAsync()).Id;
+        int menuOrder = 100;
+        // 按钮-page
+        var menuTypePage = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "查询",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":page",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-detail
+        var menuTypeDetail = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "详情",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":detail",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-add
+        var menuTypeAdd = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "增加",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":add",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-delete
+        var menuTypeDelete = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "删除",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":delete",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-update
+        var menuTypeUpdate = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "编辑",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":update",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+        
+        // 按钮-Status
+        var menuTypeStatus = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "状态",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":setStatus",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+        
+        // 按钮-batchDelete
+        var menuTypeBatchDelete = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "批量删除",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":batchDelete",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-print
+        var menuTypePrint = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "打印",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":print",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-import
+        var menuTypeImport = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "导入",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":import",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        // 按钮-export
+        var menuTypeExport = new SysMenu
+        {
+            Pid = menuPid,
+            Title = "导出",
+            Type = MenuTypeEnum.Btn,
+            Permission = className[..1].ToLower() + className[1..] + ":export",
+            OrderNo = menuOrder
+        };
+        menuOrder += 10;
+
+        var menuList = new List<SysMenu> { menuTypePage, menuTypeDetail, menuTypeAdd, menuTypeStatus, menuTypeDelete, menuTypeBatchDelete, menuTypeUpdate, menuTypePrint, menuTypeImport, menuTypeExport };
+        // 加入ForeignKey、Upload、ApiTreeSelector 等接口的权限
+        // 在生成表格时,有些字段只是查询时显示,不需要填写(WhetherAddUpdate),所以这些字段没必要生成相应接口
+        var fkTableList = tableFieldList.Where(u => u.EffectType == "ForeignKey" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y")).ToList();
+        foreach (var @column in fkTableList)
+        {
+            var menuType1 = new SysMenu
+            {
+                Pid = menuPid,
+                Title = "外键" + @column.ColumnName,
+                Type = MenuTypeEnum.Btn,
+                Permission = className[..1].ToLower() + className[1..] + ":" + column.FkEntityName + column.ColumnName + "Dropdown",
+                OrderNo = menuOrder
+            };
+            menuOrder += 10;
+            menuList.Add(menuType1);
+        }
+        var treeSelectTableList = tableFieldList.Where(u => u.EffectType == "ApiTreeSelector").ToList();
+        foreach (var @column in treeSelectTableList)
+        {
+            var menuType1 = new SysMenu
+            {
+                Pid = menuPid,
+                Title = "树型" + @column.ColumnName,
+                Type = MenuTypeEnum.Btn,
+                Permission = className[..1].ToLower() + className[1..] + ":" + column.FkEntityName + "Tree",
+                OrderNo = menuOrder
+            };
+            menuOrder += 10;
+            menuList.Add(menuType1);
+        }
+        var uploadTableList = tableFieldList.Where(u => u.EffectType == "Upload").ToList();
+        foreach (var @column in uploadTableList)
+        {
+            var menuType1 = new SysMenu
+            {
+                Pid = menuPid,
+                Title = "上传" + @column.ColumnName,
+                Type = MenuTypeEnum.Btn,
+                Permission = className[..1].ToLower() + className[1..] + ":Upload" + column.ColumnName,
+                OrderNo = menuOrder
+            };
+            menuOrder += 10;
+            menuList.Add(menuType1);
+        }
+        await _db.Insertable(menuList).ExecuteCommandAsync();
+    }
+
+    /// <summary>
+    /// 获取模板文件路径集合
+    /// </summary>
+    /// <returns></returns>
+    private static List<string> GetTemplatePathList(SysCodeGen input)
+    {
+        if (input.GenerateType!.Substring(1, 1).Contains('1'))
+        {
+            return new() { "index.vue.vm", "editDialog.vue.vm", "api.js.vm" };
+        }
+        if (input.GenerateType.Substring(1, 1).Contains('2'))
+        {
+            return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm" };
+        }
+        return new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.js.vm" };
+    }
+
+    /// <summary>
+    /// 获取模板文件路径集合
+    /// </summary>
+    /// <returns></returns>
+    private static List<string> GetTemplatePathList() => new() { "Service.cs.vm", "Input.cs.vm", "Output.cs.vm", "Dto.cs.vm", "index.vue.vm", "editDialog.vue.vm", "api.js.vm" };
+
+    /// <summary>
+    /// 设置生成文件路径
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    private List<string> GetTargetPathList(SysCodeGen input)
+    {
+        //var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent.FullName, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
+        var backendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.FullName, input.NameSpace!, "Service", input.TableName!);
+        var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
+        var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
+        var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
+        var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
+        var frontendPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
+        var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");//
+        var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
+        var apiJsPath = Path.Combine(new DirectoryInfo(App.WebHostEnvironment.ContentRootPath).Parent!.Parent!.FullName, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
+
+        if (input.GenerateType!.Substring(1, 1).Contains('1'))
+        {
+            // 生成到本项目(前端)
+            return new List<string>()
+            {
+                indexPath,
+                formModalPath,
+                apiJsPath
+            };
+        }
+        else if (input.GenerateType.Substring(1, 1).Contains('2'))
+        {
+            // 生成到本项目(后端)
+            return new List<string>()
+            {
+                servicePath,
+                inputPath,
+                outputPath,
+                viewPath,
+            };
+        }
+        else
+        {
+            // 前后端同时生成到本项目
+            return new List<string>()
+            {
+                servicePath,
+                inputPath,
+                outputPath,
+                viewPath,
+                indexPath,
+                formModalPath,
+                apiJsPath
+            };
+        }
+    }
+
+    /// <summary>
+    /// 设置生成文件路径
+    /// </summary>
+    /// <param name="input"></param>
+    /// <returns></returns>
+    private List<string> GetZipPathList(SysCodeGen input)
+    {
+        var zipPath = Path.Combine(App.WebHostEnvironment.WebRootPath, "CodeGen", input.TableName!);
+
+        //var backendPath = Path.Combine(zipPath, _codeGenOptions.BackendApplicationNamespace, "Service", input.TableName);
+        var backendPath = Path.Combine(zipPath, input.NameSpace!, "Service", input.TableName);
+        var servicePath = Path.Combine(backendPath, input.TableName + "Service.cs");
+        var inputPath = Path.Combine(backendPath, "Dto", input.TableName + "Input.cs");
+        var outputPath = Path.Combine(backendPath, "Dto", input.TableName + "Output.cs");
+        var viewPath = Path.Combine(backendPath, "Dto", input.TableName + "Dto.cs");
+        var frontendPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "views", input.PagePath!);
+        var indexPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "index.vue");
+        var formModalPath = Path.Combine(frontendPath, input.TableName[..1].ToLower() + input.TableName[1..], "component", "editDialog.vue");
+        var apiJsPath = Path.Combine(zipPath, _codeGenOptions.FrontRootPath, "src", "api", input.PagePath, input.TableName[..1].ToLower() + input.TableName[1..] + ".ts");
+        if (input.GenerateType!.StartsWith("11"))
+        {
+            return new List<string>()
+            {
+                indexPath,
+                formModalPath,
+                apiJsPath
+            };
+        }
+        else if (input.GenerateType.StartsWith("12"))
+        {
+            return new List<string>()
+            {
+                servicePath,
+                inputPath,
+                outputPath,
+                viewPath
+            };
+        }
+        else
+        {
+            return new List<string>()
+            {
+                servicePath,
+                inputPath,
+                outputPath,
+                viewPath,
+                indexPath,
+                formModalPath,
+                apiJsPath
+            };
+        }
+    }
 }

+ 9 - 20
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Manage.js.vm → Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Api.js.vm

@@ -6,6 +6,8 @@ export const use@(Model.ClassName)Api = () => {
 	return {
 		// 分页查询@(Model.BusName)
 		page: baseApi.page,
+		// 获取导出数据@(Model.BusName)
+		list: baseApi.list,
 		// 查看@(Model.BusName)详细
 		detail: baseApi.detail,
 		// 新增@(Model.BusName)
@@ -20,32 +22,19 @@ export const use@(Model.ClassName)Api = () => {
 		delete: baseApi.delete,
 		// 批量删除@(Model.BusName)
 		batchDelete: baseApi.batchDelete,
+		@foreach (var column in Model.UploadFieldList){
+		@:// 上传@(column.ColumnComment)
+		@:upload@(column.PropertyName): (params: any) => baseApi.uploadFile(params, baseApi.baseUrl + 'upload@(column.PropertyName)'),
+		}
 		@if (Model.ImportFieldList.Count > 0) {
 		@:// 导入@(Model.BusName)数据
 		@:importData: baseApi.importData,
 		@:// 下载@(Model.BusName)数据导入模板
 		@:downloadTemplate: baseApi.downloadTemplate,
 		}
-		@foreach (var column in Model.TableField) {
-		if (column.EffectType == "Upload") {
-		@:// 上传@(column.ColumnComment)
-		@:upload@(column.PropertyName): (params: any) => baseApi.uploadFile(params, baseApi.baseUrl + 'upload@(column.PropertyName)'),
-		} else if (column.EffectType == "ForeignKey" && (column.WhetherAddUpdate == "Y" || column.WhetherQuery == "Y")) {
-		var dropdownName = $"{column.FkEntityName}{column.PropertyName.TrimEnd("Id")}Dropdown";
-		@:// 获取@(column.ColumnComment)选择数据
-		@:get@(dropdownName): (all: Boolean = false) => baseApi.request({
-			@:url: baseApi.baseUrl + '@Model.ToLowerFirstLetter(dropdownName)',
-			@:params: { all },
-			@:method: 'get',
-		@:}),
-		}
-		}
-		@foreach (var column in Model.TableField.Where(c => c.EffectType == "ApiTreeSelector").DistinctBy(c => c.FkEntityName)) {
-		@:// 获取@(column.ColumnComment)选择数据
-		@:get@(column.FkEntityName)Tree: () => baseApi.request({
-			@:url: baseApi.baseUrl + '@Model.ToLowerFirstLetter(column.FkEntityName)Tree',
-			@:method: 'get',
-		@:}),
+		@if (Model.DropdownFieldList.Count > 0) {
+		@:// 获取下拉列表数据
+		@:getDropdownData: (fromPage: Boolean = false) => baseApi.dropdownData({ fromPage }),
 		}
 	}
 }

+ 19 - 8
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Input.cs.vm

@@ -112,19 +112,30 @@ public class QueryById@(Model.ClassName)Input : Delete@(Model.ClassName)Input
 @:/// </summary>
 @:public class DropdownData@(Model.ClassName)Input
 @:{
-    @:/// <summary>
-    @:/// 字段名称
-    @:/// </summary>
-    @:[Required(ErrorMessage = "字段名称不能为空")]
-    @:public string PropertyName { get; set; }
-    @:
     @:/// <summary>
     @:/// 是否用于分页查询
     @:/// </summary>
     @:public bool FromPage { get; set; }
 @:}
+@:
+}
+@if (Model.HasSetStatus) {
+@:/// <summary>
+@:/// 设置状态输入参数
+@:/// </summary>
+@:public class Set@(Model.ClassName)StatusInput : BaseStatusInput
+@:{
+    @foreach (var column in Model.PrimaryKeyFieldList.Where(u => u.PropertyName != "Id")) {
+    @:/// <summary>
+    @:/// @column.ColumnComment
+    @:/// </summary>
+    @:[Required(ErrorMessage = "@(column.ColumnComment)不能为空")]
+    @:public @Model.GetNullableNetType(column.NetType) @column.PropertyName { get; set; }
+    @:
+    }
+@:}
+@:
 }
-
 @if (Model.ImportFieldList.Count > 0){
 @:/// <summary>
 @:/// @(Model.BusName)数据导入实体
@@ -147,7 +158,7 @@ public class QueryById@(Model.ClassName)Input : Delete@(Model.ClassName)Input
     @:/// </summary>
     @:[ImporterHeader(Name = "@(headerName)")]
     @:[ExporterHeader("@(headerName)", Format = "@", Width = 25, IsBold = true)]
-    @:public string @(column.PropertyName)Label { get; set; }
+    @:public string @column.FkLinkDisplayPropertyName { get; set; }
     } else {
     @:/// <summary>
     @:/// @column.ColumnComment

+ 5 - 39
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Output.cs.vm

@@ -11,60 +11,26 @@ namespace @(Model.NameSpace);
 /// </summary>
 public class @(Model.ClassName)Output
 {
-@foreach (var column in Model.TableField){
+    @foreach (var column in Model.TableField){
     @:/// <summary>
     @:/// @column.ColumnComment
     @:/// </summary>
-    if(column.EffectType == "ForeignKey")
-    {
-    @:public @column.NetType @column.PropertyName { get; set; } 
+    @:public @column.NetType @(column.PropertyName) { get; set; }    
+    if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") {
     @:
     @:/// <summary>
     @:/// @(column.ColumnComment) 描述
     @:/// </summary>
-    @:public string @(column.PropertyName)FkColumn { get; set; } 
+    @:public string @(column.FkLinkDisplayPropertyName) { get; set; } 
     }else if(column.EffectType == "Upload"){
-    @:public @column.NetType @column.PropertyName { get; set; }
     @:
     @:/// <summary>
     @:/// @(column.ColumnComment) 文件信息
     @:/// </summary>
     @:public SysFile @(column.PropertyName)Attachment { get; set; }
-    }else if(column.EffectType == "ApiTreeSelector"){
-    @:public @column.NetType @column.PropertyName { get; set; } 
-    @:
-    @:/// <summary>
-    @:/// @(column.ColumnComment) 描述 
-    @:/// </summary>
-    @:public string @(column.PropertyName)Display { get; set; } 
-    }else{
-    @:public @column.NetType @(column.PropertyName) { get; set; }
     }
     @:
-}
-}
-@foreach (var column in Model.ApiTreeFieldList){
-@:
-@:/// <summary>
-@:/// @(Model.BusName)树选择器输出参数
-@:/// </summary>
-@:public class @(column.FkEntityName)TreeOutput : @(column.FkEntityName)
-@:{
-    @:/// <summary>
-    @:/// 显示文本
-    @:/// </summary>
-    @:public string Label { get; set; }
-    @:
-    @:/// <summary>
-    @:/// 选项值
-    @:/// </summary>
-    @:public @(column.FkColumnNetType) Value { get; set; }
-    @:
-    @:/// <summary>
-    @:/// 子集列表
-    @:/// </summary>
-    @:public List<@(column.FkEntityName)TreeOutput> Children { get; set; }
-@:}
+    }
 }
 @if (Model.ImportFieldList.Count > 0) {
 @:

+ 97 - 88
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/Service.cs.vm

@@ -61,25 +61,16 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
           // 联表
           if (Model.HasJoinTable) {
             foreach (var column in Model.TableField.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")){
-            var joinTableAlias = Regex.Replace(column.LowerPropertyName, "[iI]d$", "");
-            joinTableName += ", " + joinTableAlias;
-            @:.LeftJoin<@column.FkEntityName>((@joinTableName) => u.@(column.PropertyName) == @joinTableAlias.@(column.EffectType == "ForeignKey" ? column.FkLinkColumnName : column.FkLinkColumnName))
+            joinTableName += ", " + column.LowerPropertyNameTrimEndId;
+            @:.LeftJoin<@column.FkEntityName>((@joinTableName) => u.@(column.PropertyName) == @(column.LowerPropertyNameTrimEndId).@(column.FkLinkColumnName))
           }
             // 查询列表
             @:.Select((@joinTableName) => new @(Model.ClassName)Output
             @:{
             foreach (var column in Model.TableField) {
-                var joinTableAlias = Regex.Replace(column.LowerPropertyName, "[iI]d$", "");
-                if (column.EffectType == "ForeignKey") {
-                var columnList = column.FkDisplayColumnList.Select(n => $"{{{joinTableAlias}.{n}}}").ToList();
-                @:@(column.PropertyName) = u.@(column.PropertyName),
-                @:@(column.PropertyName)FkColumn = $"@(string.Join("-", columnList))",
-                } else if (column.EffectType == "ApiTreeSelector") {
-                var columnList = column.FkDisplayColumnList.Select(n => $"{{{joinTableAlias}.{n}}}").ToList();
-                @:@(column.PropertyName) = u.@(column.PropertyName),
-                @:@(column.PropertyName)Display = $"@(string.Join("-", columnList))",
-                } else {
                 @:@(column.PropertyName) = u.@(column.PropertyName),
+                if (column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector") {
+                @:@(column.FkLinkDisplayPropertyName) = @column.GetDisplayColumn(column.LowerPropertyNameTrimEndId),
                 }
             }
             @:});
@@ -92,47 +83,43 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
     }
 
     /// <summary>
-    /// 增加@(Model.BusName) ➕
+    /// 获取@(Model.BusName)列表 🔖
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [DisplayName("增加@(Model.BusName)")]
-    [ApiDescriptionSettings(Name = "Add"), HttpPost]
-    public async Task<long> Add(Add@(Model.ClassName)Input input)
+    [DisplayName("获取@(Model.BusName)列表")]
+    [ApiDescriptionSettings(Name = "List"), HttpGet]
+    public async Task<List<@(Model.ClassName)Output>> List([FromQuery] Page@(Model.ClassName)Input input)
     {
-        var entity = input.Adapt<@(Model.ClassName)>();
-        @foreach (var config in Model.AddUpdateUniqueConfigList) {
-        @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @(string.Join(" && ", @config.Columns.Select(x => $"u.{x} != null && u.{x} == input.{x}"))))) throw Oops.Oh("@(config.Message)已存在");
-        }
-        return await _@(Model.LowerClassName)Rep.InsertAsync(entity) ? entity.Id : 0;
+        return await _@(Model.LowerClassName)Rep.AsQueryable().Select<@(Model.ClassName)Output>().ToListAsync();
     }
 
     /// <summary>
-    /// 删除@(Model.BusName) ❌
+    /// 获取@(Model.BusName)详情 ℹ️
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [DisplayName("删除@(Model.BusName)")]
-    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
-    public async Task Delete(Delete@(Model.ClassName)Input input)
+    [DisplayName("获取@(Model.BusName)详情")]
+    [ApiDescriptionSettings(Name = "Detail"), HttpGet]
+    public async Task<@(Model.ClassName)> Detail([FromQuery] QueryById@(Model.ClassName)Input input)
     {
-        var entity = await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}")) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
-        await _@(Model.LowerClassName)Rep.FakeDeleteAsync(entity);   //假删除
-        //await _@(Model.LowerClassName)Rep.DeleteAsync(entity);   //真删除
+        return await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}", false));
     }
 
     /// <summary>
-    /// 批量删除@(Model.BusName) ❌
+    /// 增加@(Model.BusName) ➕
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [DisplayName("批量删除@(Model.BusName)")]
-    [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
-    public async Task<int> BatchDelete([Required(ErrorMessage = "主键列表不能为空")]List<Delete@(Model.ClassName)Input> input)
+    [DisplayName("增加@(Model.BusName)")]
+    [ApiDescriptionSettings(Name = "Add"), HttpPost]
+    public async Task<long> Add(Add@(Model.ClassName)Input input)
     {
-        var list = await _@(Model.LowerClassName)Rep.AsQueryable().In(u => new { @string.Join(", ", Model.PrimaryKeyFieldList.Select(n => $"u.{n}")) }, input).ToListAsync();
-        return await _@(Model.LowerClassName)Rep.FakeDeleteAsync(list);   //假删除
-        //return await _@(Model.LowerClassName)Rep.DeleteAsync(list);   //真删除
+        var entity = input.Adapt<@(Model.ClassName)>();
+        @foreach (var config in Model.TableUniqueConfigList) {
+        @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @(string.Join(" && ", @config.Columns.Select(x => $"u.{x} != null && u.{x} == input.{x}"))))) throw Oops.Oh("@(config.Message)已存在");
+        }
+        return await _@(Model.LowerClassName)Rep.InsertAsync(entity) ? entity.Id : 0;
     }
 
     /// <summary>
@@ -145,76 +132,57 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
     public async Task Update(Update@(Model.ClassName)Input input)
     {
         @{
-        var primaryKeyWhere = string.Join(" && ", Model.PrimaryKeyFieldList.Select(n => $"u.{n} == input.{n}"));
-        foreach (var config in Model.AddUpdateUniqueConfigList) {
-        @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => !(@primaryKeyWhere) && @(string.Join(" && ", config.Columns.Select(x => $"u.{x} != null && u.{x} == input.{x}"))))) throw Oops.Oh("@(config.Message)已存在");
+        var primaryKeyWhere = Model.PrimaryKeysFormat(" && ", "u.{0} != input.{0}", false);
+        foreach (var config in Model.TableUniqueConfigList) {
+        @:if (await _@(Model.LowerClassName)Rep.IsAnyAsync(u => @primaryKeyWhere && @config.Format(" && ", "u.{0} != null && u.{0} == input.{0}"))) throw Oops.Oh("@(config.Message)已存在");
         }
         }
         var entity = input.Adapt<@(Model.ClassName)>();
         await _@(Model.LowerClassName)Rep.AsUpdateable(entity).IgnoreColumns(ignoreAllNullColumns: true).ExecuteCommandAsync();
     }
-    @if (Model.HasSetStatus) {
-    @:
-    @:/// <summary>
-    @:/// 设置@(Model.BusName)状态 🚫
-    @:/// </summary>
-    @:/// <param name="input"></param>
-    @:/// <returns></returns>
-    @:[DisplayName("设置@(Model.BusName)状态")]
-    @:[ApiDescriptionSettings(Name = "SetStatus"), HttpPost]
-    @:public async Task Set@(Model.ClassName)Status(BaseStatusInput input)
-    @:{
-        @:await _@(Model.LowerClassName)Rep.AsUpdateable().SetColumns(u => u.Status, input.Status).Where(u => u.Id == input.Id).ExecuteCommandAsync();
-    @:} 
-    }
 
     /// <summary>
-    /// 获取@(Model.BusName)详情 ℹ️
+    /// 删除@(Model.BusName) ❌
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [DisplayName("获取@(Model.BusName)详情")]
-    [ApiDescriptionSettings(Name = "Detail"), HttpGet]
-    public async Task<@(Model.ClassName)> Detail([FromQuery] QueryById@(Model.ClassName)Input input)
+    [DisplayName("删除@(Model.BusName)")]
+    [ApiDescriptionSettings(Name = "Delete"), HttpPost]
+    public async Task Delete(Delete@(Model.ClassName)Input input)
     {
-        return await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}"));
+        var entity = await _@(Model.LowerClassName)Rep.GetFirstAsync(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}", false)) ?? throw Oops.Oh(ErrorCodeEnum.D1002);
+        await _@(Model.LowerClassName)Rep.FakeDeleteAsync(entity);   //假删除
+        //await _@(Model.LowerClassName)Rep.DeleteAsync(entity);   //真删除
     }
 
     /// <summary>
-    /// 获取@(Model.BusName)列表 🔖
+    /// 批量删除@(Model.BusName) ❌
     /// </summary>
     /// <param name="input"></param>
     /// <returns></returns>
-    [DisplayName("获取@(Model.BusName)列表")]
-    [ApiDescriptionSettings(Name = "List"), HttpGet]
-    public async Task<List<@(Model.ClassName)Output>> List([FromQuery] Page@(Model.ClassName)Input input)
+    [DisplayName("批量删除@(Model.BusName)")]
+    [ApiDescriptionSettings(Name = "BatchDelete"), HttpPost]
+    public async Task<int> BatchDelete([Required(ErrorMessage = "主键列表不能为空")]List<Delete@(Model.ClassName)Input> input)
     {
-        return await _@(Model.LowerClassName)Rep.AsQueryable().Select<@(Model.ClassName)Output>().ToListAsync();
+        var list = await _@(Model.LowerClassName)Rep.AsQueryable().In(u => new { @Model.PrimaryKeysFormat(", ", "u.{0}", false) }, input).ToListAsync();
+        return await _@(Model.LowerClassName)Rep.FakeDeleteAsync(list);   //假删除
+        //return await _@(Model.LowerClassName)Rep.DeleteAsync(list);   //真删除
     }
-    @foreach (var column in Model.TableField.Where(u => u.EffectType == "ForeignKey" && (u.WhetherAddUpdate == "Y" || u.WhetherQuery == "Y"))){
+    @if (Model.HasSetStatus) {
     @:
-    var dropdownName = $"{column.FkEntityName}{column.PropertyName.TrimEnd("Id")}Dropdown";
     @:/// <summary>
-    @:/// 获取@(column.ColumnComment)列表 🔖
+    @:/// 设置@(Model.BusName)状态 🚫
     @:/// </summary>
+    @:/// <param name="input"></param>
     @:/// <returns></returns>
-    @:[DisplayName("获取@(column.ColumnComment)列表")]
-    @:[ApiDescriptionSettings(Name = "@(dropdownName)"), HttpGet]
-    @:public async Task<dynamic> @(dropdownName)([FromQuery]bool all)
+    @:[DisplayName("设置@(Model.BusName)状态")]
+    @:[ApiDescriptionSettings(Name = "SetStatus"), HttpPost]
+    @:public async Task Set@(Model.ClassName)Status(Set@(Model.ClassName)StatusInput input)
     @:{
-        var columnList = column.FkDisplayColumnList.Select(name => $"{{u.{name}}}").ToList();
-        @:return await _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>()
-            @:.InnerJoinIF<@Model.ClassName>(!all, (u, r) => u.@(column.FkLinkColumnName) == r.@(column.PropertyName))
-            @:.Select(u => new
-            @:{
-                @:Label = $"@(string.Join("-", columnList))",
-                @:Value = u.@(column.FkLinkColumnName)
-            @:}
-        @:).ToListAsync();
+        @:await _@(Model.LowerClassName)Rep.AsUpdateable().SetColumns(u => u.Status, input.Status).Where(u => @Model.PrimaryKeysFormat(" && ", "u.{0} == input.{0}", false)).ExecuteCommandAsync();
     @:}
     }
-
-    @foreach (var column in Model.AddUpdateFieldList) {
+    @foreach (var column in Model.UploadFieldList) {
     @:
     @:/// <summary>
     @:/// 上传@(column.ColumnComment) ⬆️
@@ -228,6 +196,41 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
         @:return await _sysFileService.UploadFile(new FileUploadInput { File = file, SavePath = "upload/@(Model.ClassName)/@(column.PropertyName)" }); 
     @:}
     }
+    @if (Model.DropdownFieldList.Count > 0) {
+    @:
+    @:/// <summary>
+    @:/// 获取下拉列表数据 🔖
+    @:/// </summary>
+    @:/// <returns></returns>
+    @:[DisplayName("获取下拉列表数据")]
+    @:[ApiDescriptionSettings(Name = "DropdownData"), HttpPost]
+    @:public async Task<Dictionary<string, dynamic>> DropdownData(DropdownData@(Model.ClassName)Input input)
+    @:{
+        foreach (var column in Model.DropdownFieldList) {
+        @:var @(column.LowerPropertyName)Data = await _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>()
+            if (column.EffectType != "ApiTreeSelector") {
+            @:.InnerJoinIF<@Model.ClassName>(input.FromPage, (u, r) => u.@(column.FkLinkColumnName) == r.@(column.PropertyName))
+            }
+            @:.Select(u => new {
+                @:Value = u.@(column.FkLinkColumnName),
+                @:Label = @column.GetDisplayColumn("u")
+            if (column.EffectType != "ApiTreeSelector") {
+            @:}).ToListAsync();
+            } else {
+                @:, Id = u.Id,
+                @:ParentId = u.@(column.PidColumn),
+                @:Children = new List<dynamic>()
+            @:}).ToTreeAsync(u => u.Children, u => u.ParentId, @(column.WhetherRequired == "Y" ? "0" : "null"));
+            }
+        }
+        @:return new Dictionary<string, dynamic>
+        @:{
+            foreach (var column in Model.DropdownFieldList) {
+            @:{ "@(column.LowerPropertyName)", @(column.LowerPropertyName)Data },
+            }
+        @:};
+    @:}
+    }
     @if (Model.ImportFieldList.Count > 0) {
     @:
     @:/// <summary>
@@ -244,7 +247,7 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
         @:{
             foreach (var column in fieldsList) {
             var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList();
-            @:if (nameof(Export@(Model.ClassName)Output.@(column.PropertyName)Label) == info.Name) return _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Select(u => $"@(string.Join("-", columnList))").Distinct().ToList();
+            @:if (nameof(Export@(Model.ClassName)Output.@column.FkLinkDisplayPropertyName) == info.Name) return _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Select(u => $"@(string.Join("-", columnList))").Distinct().ToList();
             }
             @:return null;
         @:});
@@ -270,15 +273,18 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
 
             @:var stream = ExcelHelper.ImportData<Import@(Model.ClassName)Input, @(Model.ClassName)>(file, (list, markerErrorAction) =>
             @:{
-                @:_@(Model.LowerClassName)Rep.Context.Utilities.PageEach(list, 2048, pageItems =>
+                @:_sqlSugarClient.Utilities.PageEach(list, 2048, pageItems =>
                 @:{
                     foreach (var column in Model.ImportFieldList.Where(u => u.EffectType == "ForeignKey" || u.EffectType == "ApiTreeSelector")) {
                     @:// 链接 @(column.ColumnComment)
-                    @:var @(column.LowerPropertyName)LabelList = pageItems.Where(x => x.@(column.PropertyName)Label != null).Select(x => x.@(column.PropertyName)Label).Distinct().ToList();
+                    @:var @(column.LowerPropertyName)LabelList = pageItems.Where(x => x.@column.FkLinkDisplayPropertyName != null).Select(x => x.@column.FkLinkDisplayPropertyName).Distinct().ToList();
                     @:if (@(column.LowerPropertyName)LabelList.Any()) {
                         var columnList = column.FkDisplayColumnList.Select(n => $"{{u.{n}}}").ToList();
                         @:var @(column.LowerPropertyName)LinkMap = _@(Model.LowerClassName)Rep.Context.Queryable<@(column.FkEntityName)>().Where(u => @(column.LowerPropertyName)LabelList.Contains($"@(string.Join("-", columnList))")).ToList().ToDictionary(u => $"@(string.Join("-", columnList))", u => u.@(column.FkLinkColumnName));
-                        @:pageItems.ForEach(e => e.@(column.PropertyName) = @(column.LowerPropertyName)LinkMap?.GetValueOrDefault(e.@(column.PropertyName)Label, default));
+                        @:pageItems.ForEach(e => {
+                            @:e.@(column.PropertyName) = @(column.LowerPropertyName)LinkMap?.GetValueOrDefault(e.@column.FkLinkDisplayPropertyName);
+                            @:if (e.@(column.PropertyName) == null) e.Error = "@(column.ColumnComment)链接失败";
+                        @:});
                     @:}
                     }
 
@@ -286,6 +292,7 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
                     @:// 校验并过滤必填基本类型为null的字段
                     @:var rows = pageItems.Where(x => {
                         foreach (var column in Model.ImportFieldList.Where(x => x.WhetherRequired == "Y" && Regex.IsMatch(x.NetType, "(int|long|double|float|bool|Enum[?]?)"))){
+                        @:if (!string.IsNullOrWhiteSpace(x.Error)) return false;
                         @:if (x.@(column.PropertyName) == null){
                             @:x.Error = "@(column.ColumnComment)不能为空";
                             @:return false;
@@ -299,13 +306,15 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
                     @:// 映射字典值
                     @:foreach(var row in rows) {
                         foreach (var column in dictTableField){
-                        @:row.@(column.PropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(row.@(column.PropertyName) ?? "");
+                        @:if (row.@(column.PropertyName) == null) continue;
+                        @:row.@(column.PropertyName) = @(column.LowerPropertyName)DictMap.GetValueOrDefault(row.@(column.PropertyName));
+                        @:if (row.@(column.PropertyName) == null) row.Error = "@(column.ColumnComment)字典匹配失败";
                         }
                     @:}
                     }
 
                     @:
-                    @:var storageable = _@(Model.LowerClassName)Rep.Context.Storageable(rows)
+                    @:var storageable = _sqlSugarClient.Storageable(rows)
                         foreach (var column in Model.ImportFieldList){
                         if (column.WhetherRequired == "Y"){
                         if(column.NetType.TrimEnd('?') == "string"){
@@ -316,8 +325,8 @@ public class @(Model.ClassName)Service : IDynamicApiController, ITransient
                         if (column.NetType?.TrimEnd('?') == "string"){
                         @:.SplitError(it => it.Item.@(column.PropertyName)?.Length > @(column.ColumnLength), "@(column.ColumnComment)长度不能超过@(column.ColumnLength)个字符")
                         }}
-                        foreach (var config in Model.ImportUniqueConfigList) {
-                        @:.WhereColumns(it => new { @(string.Join(", ", config.Columns.Select(x => $"it.{x}"))) }).SplitError(it => it.Any(), "@(config.Message)已存在")
+                        foreach (var config in Model.TableUniqueConfigList) {
+                        @:.WhereColumns(it => new { @config.Format(", ", "it.{0}") }).SplitError(it => it.Any(), "@(config.Message)已存在")
                         }
                         @:.SplitInsert(_ => true)
                         @:.ToStorage();

+ 48 - 76
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/editDialog.vue.vm

@@ -1,12 +1,3 @@
-@{
-	var hasDictData = Model.TableField.Any(x => x.EffectType == "ConstSelector" || x.EffectType == "DictSelector" || x.EffectType == "EnumSelector");
-	bool IsStatusEnum(dynamic column) => column.NetType == "StatusEnum" && column.PropertyName == "Status";
-	string LowerFirstLetter(string text) => text.ToString()[..1].ToLower() + text[1..];
-	var definedObjects = new Dictionary<string, int>();
-	var pkField = Model.TableField.Where(c => c.ColumnKey == "True").FirstOrDefault();
-	var pkFieldName = LowerFirstLetter(pkField.PropertyName);
-	var displayColumnList = new List<string>();
-}
 <template>
 	<div class="@(Model.LowerClassName)-container">
 		<el-dialog v-model="isShowDialog" :width="800" draggable :close-on-click-modal="false">
@@ -25,22 +16,20 @@
 					</el-form-item>
 					}else{
 					if (column.WhetherAddUpdate == "Y"){
-					if(column.EffectType == "ForeignKey"){
-					var dropdownName = $"{column.FkEntityName}{Regex.Replace(column.PropertyName, "[iI]d$", "")}Dropdown";
+					if(column.EffectType == "ForeignKey") {
 					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
 							@:<el-select clearable filterable v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请选择@(column.ColumnComment)">
-								@:<el-option v-for="(item,index) in @LowerFirstLetter(dropdownName)List" :key="index" :value="item.value" :label="item.label" />
+								@:<el-option v-for="(item,index) in dropdownData.@(column.LowerPropertyName) ?? []" :key="index" :value="item.value" :label="item.label" />
 							</el-select>
 						</el-form-item>
 					</el-col>
 					}else if(column.EffectType == "ApiTreeSelector"){
-					displayColumnList = column.FkDisplayColumns.Split(",").Select(u => $"${{data.{LowerFirstLetter(u)}}}").ToList();
 					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
 							<el-cascader
-								@:options="@LowerFirstLetter(@column.FkEntityName)TreeData"
-								@:props="{ checkStrictly: true, emitPath: false, value: '@LowerFirstLetter(@column.FkLinkColumnName)' }"
+								@:options="dropdownData.@(column.LowerPropertyName) ?? []"
+								@:props="{ checkStrictly: true, emitPath: false }"
 								placeholder="请选择@(column.ColumnComment)"
 								clearable
 								filterable
@@ -54,28 +43,10 @@
 							</el-cascader>
 						</el-form-item>
 					</el-col>
-					}else if(column.EffectType == "Input"){
-					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
-							@:<el-input v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" maxlength="@(column.ColumnLength)" show-word-limit clearable />
-						</el-form-item>
-					</el-col>
-					}else if(column.EffectType == "InputNumber"){
-					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
-						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
-							@:<el-input-number v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" clearable />
-						</el-form-item>
-					</el-col>
-					}else if(column.EffectType == "InputTextArea"){
-					@:<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
-						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
-							@:<el-input v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" type="textarea" maxlength="@(column.ColumnLength)" show-word-limit clearable />
-						</el-form-item>
-					</el-col>
 					}else if(column.EffectType == "DictSelector" || column.EffectType == "EnumSelector"){
-					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" @(IsStatusEnum(column) ? $"v-if='!ruleForm.{pkFieldName}'" : "")>
+					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20" @(Model.IsStatus(column) ? $"v-if='!ruleForm.{Model.ToLowerFirstLetter(Model.PrimaryKeyNames.First())}'" : "")>
 						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
-							if (IsStatusEnum(column)) {
+							if (Model.IsStatus(column)) {
 							@:<el-switch v-model="ruleForm.@column.LowerPropertyName" :active-value="1" :inactive-value="2" size="small" />
 							} else {
 							@:<el-select clearable filterable v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请选择@(column.ColumnComment)">
@@ -92,6 +63,24 @@
 							</el-select>
 						</el-form-item>
 					</el-col>
+					}else if(column.EffectType == "Input"){
+					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
+							@:<el-input v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" maxlength="@(column.ColumnLength)" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
+					}else if(column.EffectType == "InputNumber"){
+					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
+						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
+							@:<el-input-number v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" clearable />
+						</el-form-item>
+					</el-col>
+					}else if(column.EffectType == "InputTextArea"){
+					@:<el-col :xs="24" :sm="24" :md="24" :lg="24" :xl="24" class="mb20">
+						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
+							@:<el-input v-model="ruleForm.@(column.LowerPropertyName)" placeholder="请输入@(column.ColumnComment)" type="textarea" maxlength="@(column.ColumnLength)" show-word-limit clearable />
+						</el-form-item>
+					</el-col>
 					}else if(column.EffectType == "Switch"){
 					@:<el-col :xs="24" :sm="12" :md="12" :lg="12" :xl="12" class="mb20">
 						@:<el-form-item label="@column.ColumnComment" prop="@(column.LowerPropertyName)">
@@ -145,21 +134,19 @@
 	import { ref,onMounted } from "vue";
 	import { ElMessage } from "element-plus";
 	import type { FormRules } from "element-plus";
-@if(hasDictData) {
-	@:import { useUserInfo } from "/@@/stores/userInfo";
-}
-@if(Model.TableField.Any(x=>x.EffectType == "DatePicker")){
+	@if(Model.TableField.Any(x=>x.EffectType == "DatePicker")){
   	@:import { formatDate } from '/@@/utils/formatTime';
-}
-@if(Model.TableField.Any(x=>x.EffectType == "Upload")){
+	}
+	@if(Model.TableField.Any(x=>x.EffectType == "Upload")){
     @:import { Plus } from "@@element-plus/icons-vue";
     @:import { UploadRequestOptions } from "element-plus";
-}
+	}
 	import { use@(Model.ClassName)Api } from '/@@/api/@(Model.PagePath)/@(Model.LowerClassName)';
-
-@if(hasDictData) {
+	@if(Model.HasDictField || Model.HasEnumField) {
+	@:import { useUserInfo } from "/@@/stores/userInfo";
+	@:
 	@:const getDictDataByCode = useUserInfo().getDictDataByCode;
-}
+	}
 	//父级传递来的参数
 	var props = defineProps({
 		title: {
@@ -173,22 +160,30 @@
 	const isShowDialog = ref(false);
 	const ruleForm = ref<any>({});
 	const @(Model.LowerClassName)Api = use@(Model.ClassName)Api();
+	@if (Model.DropdownFieldList.Count > 0) {
+	@:const dropdownData = ref<any>({});
+	}
 	//自行添加其他规则
 	const rules = ref<FormRules>({
-@foreach (var column in Model.TableField){
-	if(column.WhetherRequired == "Y"){
+	@foreach (var column in Model.TableField) {
+	if(column.WhetherRequired == "Y") {
 		if(column.EffectType == "Input" || @column.EffectType == "InputNumber" || @column.EffectType == "InputTextArea"){
 		@:@column.LowerPropertyName: [{required: true, message: '请输入@(column.ColumnComment)!', trigger: 'blur',},],
 		}else if(column.EffectType == "DatePicker" || @column.EffectType == "DictSelector" || @column.EffectType == "EnumSelector" || @column.EffectType == "ApiTreeSelector"){
 		@:@column.LowerPropertyName: [{required: true, message: '请选择@(column.ColumnComment)!', trigger: 'change',},],
 		}
     }
-}
+	}
 	});
 
 	// 页面加载时
-	onMounted(() => {
-
+	onMounted(async () => {
+		@if (Model.DropdownFieldList.Count > 0) {
+		@:const data = await @(Model.LowerClassName)Api.getDropdownData(false).then(res => res.data.result) ?? {};
+		@foreach (var column in Model.DropdownFieldList) {
+		@:dropdownData.value.@(column.LowerPropertyName) = data.@(column.LowerPropertyName);
+		}
+		}
 	});
 
 	@if(Model.TableField.Any(x=>x.EffectType == "ConstSelector")){
@@ -224,11 +219,7 @@
 		ruleFormRef.value.validate(async (isValid: boolean, fields?: any) => {
 			if (isValid) {
 				let values = ruleForm.value;
-				if (ruleForm.value.@(pkFieldName) == undefined || ruleForm.value.@(pkFieldName) == null || ruleForm.value.@(pkFieldName) == "" || ruleForm.value.@(pkFieldName) == 0) {
-					await @(Model.LowerClassName)Api.add(values);
-				} else {
-					await @(Model.LowerClassName)Api.update(values);
-				}
+				await @(Model.LowerClassName)Api[ruleForm.value.@(Model.ToLowerFirstLetter(Model.PrimaryKeyNames.First())) ? 'update' : 'add'](values);
 				closeDialog();
 			} else {
 				ElMessage({
@@ -239,32 +230,13 @@
 		});
 	};
 	
-	@foreach (var column in Model.TableField) {
-	if (column.EffectType == "ForeignKey" && column.WhetherAddUpdate == "Y") {
-		var dropdownName = $"{column.FkEntityName}{Regex.Replace(column.PropertyName, "[iI]d$", "")}Dropdown";
-	@:const @LowerFirstLetter(dropdownName)List = ref<any>([]);
-	@:const get@(dropdownName)List = async () => {
-		@:const list = await @(Model.LowerClassName)Api.get@(dropdownName)(true);
-		@:@LowerFirstLetter(dropdownName)List.value = list.data.result ?? [];
-	@:};
-	@:get@(dropdownName)List();
-	@:
-	} else if (column.EffectType == "ApiTreeSelector" && !definedObjects.ContainsKey("define_get@(column.FkEntityName)TreeData")) {
-		definedObjects.Add("define_get@(column.FkEntityName)TreeData", 1);
-	@:const @LowerFirstLetter(@column.FkEntityName)TreeData = ref<any>([]);
-	@:const get@(column.FkEntityName)TreeData = async () => {
-		@:const list = await @(Model.LowerClassName)Api.get@(column.FkEntityName)Tree(true);
-		@:@LowerFirstLetter(@column.FkEntityName)TreeData.value = list.data.result ?? [];
-	@:};
-	@:get@(column.FkEntityName)TreeData();
-	@:
-	} else if (column.WhetherAddUpdate == "Y" && column.EffectType == "Upload") {
+	@foreach (var column in Model.UploadFieldList) {
 	@:const upload@(column.PropertyName)Handle = async (options: UploadRequestOptions) => {
 		@:const res = await @(Model.LowerClassName)Api.upload@(column.PropertyName)(options);
 		@:ruleForm.value.@(column.LowerPropertyName) = res.data.result?.url;
 	@:};
 	@:
-	}}
+	}
 	//将属性或者函数暴露给父组件
 	defineExpose({ openDialog });
 </script>

+ 67 - 109
Admin.NET/Admin.NET.Web.Entry/wwwroot/template/index.vue.vm

@@ -1,36 +1,17 @@
-@{
-  string LowerFirstLetter(string text) => text.ToString()[..1].ToLower() + text[1..];
-
-  string getDropdownName(dynamic column) => $"{column.FkEntityName}{Regex.Replace(column.PropertyName, "[iI]d$", "")}Dropdown";
-
-  var pkField = Model.TableField.Where(c => c.ColumnKey == "True").FirstOrDefault();
-
-  string pkFieldName = pkField != null && !string.IsNullOrEmpty(pkField.PropertyName) ? LowerFirstLetter(pkField.PropertyName) : null;
-
-  var definedObjects = new Dictionary<string, int>();
-
-  bool hasImport = Model.TableField.Any(x => x.WhetherImport == "Y");
-
-  bool haveLikeCdt = Model.TableField.Any(col => col.WhetherQuery == "Y" && col.QueryType == "like");
-
-  var hasSetStatus = Model.TableField.Any(col => col.NetType == "StatusEnum" && col.PropertyName == "Status");
-
-  var isStatus = false;
-}
-<template>
+<template>
   <div class="@(Model.LowerClassName)-container">
     <el-card shadow="hover" :body-style="{ paddingBottom: '0' }"> 
       <el-form :model="queryParams" ref="queryForm" labelWidth="90">
         <el-row>
-          @if(Model.QueryWhetherList.Count > 0){
-          if(haveLikeCdt){
+          @if(Model.QueryWhetherList.Count > 0) {
+          if(Model.HasLikeQuery) {
           @:<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10">
             @:<el-form-item label="关键字">
               @:<el-input v-model="queryParams.keyword" clearable placeholder="请输入模糊查询关键字"/>
             @:</el-form-item>
           @:</el-col>
           }
-          foreach (var column in Model.QueryWhetherList){
+          foreach (var column in Model.QueryWhetherList) {
           @:<el-col :xs="24" :sm="12" :md="12" :lg="8" :xl="4" class="mb10" v-if="showAdvanceQueryUI">
             if(column.EffectType == "Input" || column.EffectType == "InputTextArea"){
             @:<el-form-item label="@column.ColumnComment">
@@ -45,35 +26,35 @@
             @:<el-form-item label="@column.ColumnComment">
               @:<el-input-number v-model="queryParams.@(column.LowerPropertyName)"  clearable placeholder="请输入@(column.ColumnComment)"/>
             @:</el-form-item>
-            }else if(column.EffectType == "ForeignKey"){
+            }else if(column.EffectType == "ForeignKey") {
             @:<el-form-item label="@column.ColumnComment">
               @:<el-select clearable filterable v-model="queryParams.@(column.LowerPropertyName)" placeholder="请选择@(column.ColumnComment)">
-                @:<el-option v-for="(item,index) in get@(getDropdownName(column))List" :key="index" :value="item.value" :label="item.label" />
+                @:<el-option v-for="(item,index) in dropdownData.@(column.LowerPropertyName) ?? []" :key="index" :value="item.value" :label="item.label" />
               @:</el-select>
             @:</el-form-item>
+            }else if(column.EffectType == "ApiTreeSelector"){
+            @:<el-form-item label="@column.ColumnComment">
+              @:<el-cascader
+                @::options="dropdownData.@(column.LowerPropertyName) ?? []"
+                @:@:props="{ checkStrictly: true, emitPath: false }"
+                @:placeholder="请选择@(column.ColumnComment)"
+                @:clearable
+                @:filterable
+                @:class="w100"
+                @:v-model="queryParams.@(column.LowerPropertyName)"
+                @:>
+                  @:<template #default="{ node, data }">
+                    @:<span>{{ data.label }}</span>
+                    @:<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
+                  @:</template>
+              @:</el-cascader>
+            @:</el-form-item>
             }else if(column.EffectType == "DictSelector" || column.EffectType == "EnumSelector"){
             @:<el-form-item label="@column.ColumnComment">
               @:<el-select clearable filterable v-model="queryParams.@(column.LowerPropertyName)" placeholder="请选择@(column.ColumnComment)">
                 @:<el-option v-for="(item,index) in getDictDataByCode('@(column.DictTypeCode)')" :key="index" :value="item.code" :label="`[${item.code}]${item.value}`" />
               @:</el-select>
             @:</el-form-item>
-            }else if(column.EffectType == "ApiTreeSelector"){
-            @:<el-form-item label="@column.ColumnComment">
-              @:<el-cascader
-                  @::options="@LowerFirstLetter(@column.FkEntityName)TreeData"
-                  @:@:props="{ checkStrictly: true, emitPath: false, value: '@LowerFirstLetter(@column.FkLinkColumnName)' }"
-                  @:placeholder="请选择@(column.ColumnComment)"
-                  @:clearable
-                  @:filterable
-                  @:class="w100"
-                  @:v-model="queryParams.@(column.LowerPropertyName)"
-                  @:>
-                @:<template #default="{ node, data }">
-                  @:<span>{{ data.label }}</span>
-                  @:<span v-if="!node.isLeaf"> ({{ data.children.length }}) </span>
-                @:</template>
-              @:</el-cascader>
-            @:</el-form-item>
             }else if(column.EffectType == "DatePicker"){
             @:<el-form-item label="@column.ColumnComment">
               if (column.QueryType == "~") {
@@ -92,14 +73,14 @@
                 <el-button type="primary"  icon="ele-Search" @@click="handleQuery" v-auth="'@(Model.LowerClassName):page'"> @(Model.QueryWhetherList.Count > 0 ? "查询" : "刷新") </el-button>
                 @if (Model.QueryWhetherList.Count > 0) {
                 @:<el-button icon="ele-Refresh" @@click="() => queryParams = {}"> 重置 </el-button>
-                @if (haveLikeCdt) {
+                @if (Model.HasLikeQuery) {
                 @:<el-button icon="ele-ZoomIn" @@click="changeAdvanceQueryUI" v-if="!showAdvanceQueryUI" style="margin-left:5px;"> 高级查询 </el-button>
                 @:<el-button icon="ele-ZoomOut" @@click="changeAdvanceQueryUI" v-if="showAdvanceQueryUI" style="margin-left:5px;"> 隐藏 </el-button>
                 }
                 }
                 <el-button type="danger" style="margin-left:5px;" icon="ele-Delete" @@click="batchDel@(Model.ClassName)" :disabled="selectData.length == 0" v-auth="'@(Model.LowerClassName):batchDelete'"> 删除 </el-button>
                 <el-button type="primary" style="margin-left:5px;" icon="ele-Plus" @@click="openAdd@(Model.ClassName)" v-auth="'@(Model.LowerClassName):add'"> 新增 </el-button>
-                @if (hasImport) {
+                @if (Model.ImportFieldList.Count > 0) {
                 @:<el-button type="warning" icon="ele-MostlyCloudy" @@click="importDataRef.openDialog()" v-auth="'@(Model.LowerClassName):import'"> 导入 </el-button>
                 }
               </el-button-group>
@@ -118,12 +99,12 @@
       </el-form>
     </el-card>
     <el-card class="full-table" shadow="hover" style="margin-top: 5px">
-      <el-table :data="tableData" @@selection-change="(val: any[]) => { selectData = val; }" style="width: 100%" v-loading="loading" tooltip-effect="light" @(pkFieldName != null ? $"row-key=\"{@pkFieldName}\"" : "") @@sort-change="sortChange" border>
+      <el-table :data="tableData" @@selection-change="(val: any[]) => { selectData = val; }" style="width: 100%" v-loading="loading" tooltip-effect="light" row-key="@Model.ToLowerFirstLetter(Model.PrimaryKeyNames.First())" @@sort-change="sortChange" border>
         <el-table-column type="selection" width="40" align="center" v-auth="'@(Model.LowerClassName):batchDelete'" />
         <el-table-column type="index" label="序号" width="55" align="center"/>
         @foreach (var column in Model.TableField.Where(u => u.WhetherTable == "Y")){
-        if(column.EffectType == "Upload"||@column.EffectType == "ForeignKey" || @column.EffectType == "ApiTreeSelector" || @column.EffectType == "Switch" || @column.EffectType == "ConstSelector"){
-        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip="">
+        if(column.EffectType == "Upload" || @column.EffectType == "ForeignKey" || @column.EffectType == "ApiTreeSelector" || @column.EffectType == "Switch" || @column.EffectType == "ConstSelector"){
+        @:<el-table-column @(Model.GetElTableColumnCustomProperty(column)) show-overflow-tooltip>
           @:<template #default="scope">
             if(column.EffectType == "Upload"){
             @:<el-image
@@ -136,10 +117,8 @@
             @::initial-index="0"
             @:fit="scale-down"
             @:preview-teleported />
-            }else if(column.EffectType == "ForeignKey"){
-            @:<span>{{scope.row.@LowerFirstLetter(@column.PropertyName)FkColumn}}</span>
-            }else if(column.EffectType == "ApiTreeSelector"){
-            @:<span>{{scope.row.@LowerFirstLetter(@column.PropertyName)Display}}</span>
+            }else if(column.EffectType == "ForeignKey" || column.EffectType == "ApiTreeSelector"){
+            @:<span>{{scope.row.@(column.LowerFkLinkDisplayPropertyName)}}</span>
             }else if(column.EffectType == "Switch"){
             @:<el-tag v-if="scope.row.@(column.LowerPropertyName)"> 是 </el-tag>
             @:<el-tag type="danger" v-else> 否 </el-tag>
@@ -149,10 +128,9 @@
           @:</template>
         @:</el-table-column>
         } else if (column.EffectType == "DictSelector" || column.EffectType == "EnumSelector") {
-        isStatus = column.NetType == "StatusEnum" && column.PropertyName == "Status";
-        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip @(isStatus ? $"v-auth=\"'{@Model.LowerClassName}:setStatus'\"": "")>
+        @:<el-table-column @(Model.GetElTableColumnCustomProperty(column)) show-overflow-tooltip>
           @:<template #default="scope">
-            if (isStatus) {
+            if (Model.IsStatus(column)) {
               @:<el-switch v-model="scope.row.@column.LowerPropertyName" :active-value="1" :inactive-value="2" size="small" @@change="change@(Model.ClassName)Status(scope.row)" />
             } else {
               @:<DictLabel :value="scope.row.@column.LowerPropertyName" code="@column.DictTypeCode" />
@@ -160,7 +138,7 @@
           @:</template>
         @:</el-table-column>
         } else {
-        @:<el-table-column prop="@column.LowerPropertyName" label="@column.ColumnComment" @(column.WhetherSortable == "Y" ? "sortable='custom'" : "") show-overflow-tooltip />
+        @:<el-table-column @(Model.GetElTableColumnCustomProperty(column)) show-overflow-tooltip />
         }
         }
         <el-table-column label="修改记录" width="100" align="center" show-overflow-tooltip>
@@ -200,7 +178,7 @@
       />
     </el-card>
   </div>
-  @if (hasImport) {
+  @if (Model.ImportFieldList.Count > 0) {
   @:<ImportData
         @:ref="importDataRef"
         @::import="@(Model.LowerClassName)Api.importData"
@@ -212,7 +190,7 @@
 </template>
 
 <script lang="ts" setup name="@(Model.LowerClassName)">
-  import { ref } from "vue";
+  import { ref, onMounted } from "vue";
   import { auth } from '/@@/utils/authFunction';
   import { getAPI } from '/@@/utils/axios-utils';
   import { ElMessageBox, ElMessage } from "element-plus";
@@ -222,35 +200,35 @@
   @if(Model.TableField.Any(x => x.EffectType == "ConstSelector")) {
   @:import { codeToName, getConstType } from '/@@/utils/constHelper';
   }
-  @if(Model.TableField.Any(x => x.EffectType == "DictSelector" || x.EffectType == "EnumSelector")) {
-  @:import { useUserInfo } from "/@@/stores/userInfo";
-  }
   @if(Model.PrintType == "custom") {
   @:// 推荐设置操作 width 为 200
   @:import { hiprint } from 'vue-plugin-hiprint';
   @:import { SysPrintApi } from '/@@/api-services/api';
   @:import { SysPrint } from '/@@/api-services/models';
   }
-  @if(Model.TableField.Any(x => x.EffectType == "DictSelector" || x.EffectType == "EnumSelector")) {
-  @:import DictLabel from "/@@/components/table/dictLabel.vue";
-  }
-  @if(hasImport) {
+  @if(Model.ImportFieldList.Count > 0) {
   @:import ImportData from "/@@/components/table/importData.vue";
   }
   import editDialog from '/@@/views/@(Model.PagePath)/@(Model.LowerClassName)/component/editDialog.vue'
   import printDialog from '/@@/views/system/print/component/hiprint/preview.vue'
   import ModifyRecord from '/@@/components/table/modifyRecord.vue';
   import { use@(Model.ClassName)Api} from '/@@/api/@(Model.PagePath)/@(Model.LowerClassName)';
-  @if(Model.TableField.Any(x => x.EffectType == "DictSelector" || x.EffectType == "EnumSelector")) {
+  @if(Model.HasDictField || Model.HasEnumField) {
+  @:import { useUserInfo } from "/@@/stores/userInfo";
+  @:import DictLabel from "/@@/components/table/dictLabel.vue";
+  @:
   @:const getDictDataByCode = useUserInfo().getDictDataByCode;
   }
-  const showAdvanceQueryUI = ref(@(haveLikeCdt ? "false" : "true"));
+  const showAdvanceQueryUI = ref(@(Model.HasLikeQuery ? "false" : "true"));
   const @(Model.LowerClassName)Api = use@(Model.ClassName)Api();
   const printDialogRef = ref();
   const editDialogRef = ref();
-  @if (hasImport){
+  @if (Model.ImportFieldList.Count > 0){
   @:const importDataRef = ref();
   }
+  @if (Model.DropdownFieldList.Count > 0) {
+  @:const dropdownData = ref<any>({});
+  }
   const loading = ref(false);
   const tableData = ref<any>([]);
   const selectData = ref<any>([]);
@@ -265,7 +243,17 @@
   });
 
   const print@(Model.ClassName)Title = ref("");
-  const edit@(Model.ClassName)Title = ref("")
+  const edit@(Model.ClassName)Title = ref("");
+
+  // 页面加载时
+  onMounted(async () => {
+    @if (Model.DropdownFieldList.Count > 0) {
+    @:const data = await @(Model.LowerClassName)Api.getDropdownData(true).then(res => res.data.result) ?? {};
+    @foreach (var column in Model.DropdownFieldList) {
+    @:dropdownData.value.@(column.LowerPropertyName) = data.@(column.LowerPropertyName);
+    }
+    }
+  });
 
   // 改变高级查询的控件显示状态
   const changeAdvanceQueryUI = () => {
@@ -291,14 +279,7 @@
   // 打开新增页面
   const openAdd@(Model.ClassName) = () => {
     edit@(Model.ClassName)Title.value = '添加@(Model.BusName)';
-    const data = {
-@if (hasSetStatus) {
-        @:status: 1,
-}
-@if (Model.TableField.Any(col => col.PropertyName == "OrderNo")) {
-        @:orderNo: 100,
-}
-    };
+    const data = { @(Model.GetAddDefaultValue()) };
     editDialogRef.value.openDialog(data);
   };
 
@@ -330,16 +311,14 @@
   // 删除
   const del@(Model.ClassName) = (row: any) => {
     ElMessageBox.confirm(`确定要删除吗?`, "提示", {
-    confirmButtonText: "确定",
-    cancelButtonText: "取消",
-    type: "warning",
-  })
-  .then(async () => {
-    await @(Model.LowerClassName)Api.delete(row);
-    handleQuery();
-    ElMessage.success("删除成功");
-  })
-  .catch(() => {});
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning",
+    }).then(async () => {
+      await @(Model.LowerClassName)Api.delete(row);
+      handleQuery();
+      ElMessage.success("删除成功");
+    }).catch(() => {});
   };
 
   // 批量删除
@@ -350,7 +329,7 @@
       type: "warning",
     }).then(async () => {
       @foreach (var column in @Model.TableField.Where(u => u.ColumnKey == "True")) {
-      @:const count = await @(Model.LowerClassName)Api.batchDelete({ @(column.LowerPropertyName)List: selectData.value.map(u => u.@(column.LowerPropertyName)) });
+      @:const count = await @(Model.LowerClassName)Api.batchDelete({ @(column.LowerPropertyName)List: selectData.value.map(u => { @(Model.PrimaryKeysFormat(", ", "u.{0}", true)) }) });
       }
       handleQuery();
       ElMessage.success(`成功批量删除${count}条记录`);
@@ -368,28 +347,7 @@
     tableParams.value.page = val;
     handleQuery();
   };
-
-@foreach (var column in Model.TableField) {
-  if (column.EffectType == "ForeignKey") {
-  var dropdownName = getDropdownName(column);
-  @:const @LowerFirstLetter(dropdownName)List = ref<any>([]);
-  @:const get@(dropdownName)List = async () => {
-    @:let list = await @(Model.LowerClassName)Api.get@(dropdownName)();
-    @:@(LowerFirstLetter(dropdownName))List.value = list.data.result ?? [];
-  @:};
-  @:get@(dropdownName)List();
-  @:
-  } else if (column.EffectType == "ApiTreeSelector" && !definedObjects.ContainsKey("define_get@(column.FkEntityName)TreeData")) {
-  definedObjects.Add("define_get@(column.FkEntityName)TreeData", 1);
-  @:const @LowerFirstLetter(@column.FkEntityName)TreeData = ref<any>([]);
-  @:const get@(column.FkEntityName)TreeData = async () => {
-    @:let list = await @(Model.LowerClassName)Api.get@(column.FkEntityName)Tree();
-    @:@LowerFirstLetter(@column.FkEntityName)TreeData.value = list.data.result ?? [];
-  @:};
-  @:get@(column.FkEntityName)TreeData();
-  @:
-  } 
-}
+  
   handleQuery();
 </script>
 <style scoped>

+ 10 - 5
Web/src/api/base/index.ts

@@ -8,21 +8,26 @@ export const useBaseApi = (module: string) => {
         request,
         uploadFile,
         baseUrl: baseUrl,
-        list: (params: any) => request({
-            url: baseUrl + "list",
-            method: 'get',
-            params,
-        }),
         page: (data: any) => request({
             url: baseUrl + "page",
             method: 'post',
             data,
         }),
+        list: (params: any) => request({
+            url: baseUrl + "list",
+            method: 'get',
+            params,
+        }),
         detail: (id: any) => request({
             url: baseUrl + "detail",
             method: 'get',
             data: { id },
         }),
+        dropdownData: (data: any) => request({
+            url: baseUrl + "dropdownData",
+            method: 'post',
+            data,
+        }),
         add: (data: any) => request({
             url: baseUrl + 'add',
             method: 'post',