# +====================================================================== # | DjangoAdmin 企业数字化开发平台 [ 赋能开发者,助力企业发展 ] # +---------------------------------------------------------------------- # | Copyright (c) 2021-2026 小蚂蚁云团队 版权所有 # +---------------------------------------------------------------------- # | 开源协议: Apache-2.0 # | 重要声明: 未经书面许可,不得移除或修改本框架相关版权标识。 # +---------------------------------------------------------------------- # | 官方网站: https://www.djangoadmin.cn # +---------------------------------------------------------------------- # | 研发团队: 小蚂蚁云团队 荣誉出品 # +====================================================================== # | 版权和免责声明: # | 一、知识产权声明 # | 本团队对该软件框架产品(以下简称“本产品”)拥有完整知识产权,包括但不限于商标权、专利权、 # | 著作权、商业秘密等。前述权利均受中华人民共和国及相关国际法律法规保护。未经本团队书面授权, # | 任何个人或单位不得基于本产品本身申请任何形式的知识产权。 # | 二、使用限制与禁止条款 # | 您理解并同意,禁止将本产品用于以下任何行为: # | 1. 任何违反中华人民共和国法律法规的用途; # | 2. 任何侵害他人合法权益(包括但不限于人身权、财产权、知识产权等)的行为; # | 3. 任何危害国家安全、公共利益及社会道德风尚的行为。 # | 三、责任豁免 # | 因使用本产品而产生的任何损失或法律纠纷(包括但不限于直接损失、间接损失、附带损失、衍生损失 # | 以及因违约、侵权等引发的责任),本团队均不承担任何法律责任。若因违规使用本产品导致任何第三 # | 方提起索赔或行政机关进行调查,您应独立承担全部责任。 # | 四、违约与法律责任 # | 如发现任何违反本声明的行为,本团队有权: # | 1. 无条件配合公安机关及其他有权机关的调查取证工作; # | 2. 依法追究相关方的民事责任,并要求赔偿全部损失; # | 3. 保留进一步采取刑事控告等法律措施的权利。 # | 五、许可范围说明 # | 1、被授权方应视本产品源代码为核心机密信息,负有最高标准的保密与保管义务。禁止以任何形式(包 # | 括但不限于直接或间接的分享、复制、传播、分发、转售、出租、开源、反向工程、反编译、反汇编或通 # | 过网络服务提供给任何第三方)泄露、披露或处置源代码的全部或部分内容。任何未经授权的复制、使用、 # | 传播行为均构成对本团队知识产权的严重侵权,本团队有权依法追究其全部法律责任,并要求赔偿因此造 # | 成的一切经济损失及商誉损害。 # | 2、本产品仅限于公司或个人在法律允许范围内,用于内部且合规的软件产品研发。具体权利义务以《软件 # | 许可协议》附件为准。 # +====================================================================== """ 代码生成器配置模块 ============================================================= 该模块定义了代码生成器的配置信息,用于根据数据库表生成对应的业务代码。 支持生成后端Python代码(models、forms、views、services、urls、apps、admin) 和前端Vue代码(index.vue、edit.vue、querySchemas.ts、columns.ts、api.ts)。 """ import json import os import re import subprocess import sys from jinja2 import Environment, FileSystemLoader class CodeGenerator: """ 代码生成器类 ============================================================= 根据配置信息生成完整的业务模块代码。 支持后端代码和前端Vue代码的自动生成。 使用示例: config = {...} # 配置信息 generator = CodeGenerator(config) generator.generate() # 生成所有代码(交互模式) generator.generate(auto=True) # 生成所有代码(自动模式,跳过迁移和确认) 属性说明: config: 生成配置字典 template_dir: 模板文件目录 output_dir: 代码输出目录 env: Jinja2模板环境 """ def __init__(self, config): """ 初始化代码生成器 Args: config: 生成配置字典,包含以下关键信息: - app_name: 应用名称(如:'position') - module_comment: 模块中文注释(如:'案例') - model_class_name: 模型类名(如:'Position') - fields: 字段列表配置 - 以及其他业务相关配置 """ self.config = config # 模板目录:当前文件所在目录下的 public/templates 文件夹 self.template_dir = os.path.join(os.path.dirname(__file__), 'public/templates') # 输出目录:当前文件所在目录下的 application/{app_name} 文件夹 self.output_dir = os.path.join(os.path.dirname(__file__), 'application', config['app_name']) # 项目根目录(manage.py所在目录) self.project_root = os.path.dirname(__file__) # 初始化Jinja2模板环境 # trim_blocks: 删除模板中的空行 # lstrip_blocks: 删除模板行前的空格和制表符 self.env = Environment( loader=FileSystemLoader(self.template_dir), trim_blocks=True, lstrip_blocks=True ) def generate(self, auto=False): """ 执行代码生成主方法 ============================================================= 生成完整的业务模块代码,包括: 1. 后端Python代码(models、forms、views、services等) 2. 前端Vue代码(列表页、编辑页、API接口等) 执行流程: - 创建输出目录 - 准备模板上下文数据 - 生成后端代码文件 - 生成前端Vue代码文件 - 执行数据库迁移(仅交互模式) - 创建菜单权限节点 Args: auto: 是否自动模式(True: 自动执行,不询问;False: 交互模式,需要用户确认) """ # 步骤1:创建输出目录 # ========================================================== os.makedirs(self.output_dir, exist_ok=True) print(f"创建输出目录: {self.output_dir}") # 步骤2:准备模板上下文 # ========================================================== context = self._prepare_context() print("模板上下文准备完成") # 步骤3:生成后端代码文件 # ========================================================== backend_templates = [ 'models.py.tpl', # 数据库模型模板 'forms.py.tpl', # 表单验证模板 'views.py.tpl', # 视图类模板 'services.py.tpl', # 业务逻辑模板 'urls.py.tpl', # 路由配置模板 'apps.py.tpl', # 应用配置模板 'admin.py.tpl', # 后台管理模板 ] for template_name in backend_templates: output_path = os.path.join(self.output_dir, template_name.replace('.tpl', '')) self._render_template(template_name, output_path, context) # 步骤3.5:创建模块初始化文件和迁移目录初始化文件 # ========================================================== self._create_init_files() # 步骤3.6:更新 settings.py 的 INSTALLED_APPS self._update_installed_apps() # 步骤3.7:更新主路由文件 self._update_main_urls() # 步骤4:生成前端Vue代码文件 # ========================================================== self._generate_vue_files(context) print(f"\n代码生成完成!输出目录: {self.output_dir}") # 步骤5:询问是否执行数据库迁移(仅交互模式) # ========================================================== if not auto: self._prompt_migration() # 步骤6:创建菜单权限节点 # ========================================================== if auto: print("\n" + "=" * 60) print("自动模式:正在创建菜单权限节点...") print("=" * 60) self._create_permission_nodes() else: self._prompt_create_permission() def _prompt_migration(self): """ 提示是否执行数据库迁移 ============================================================= 在交互模式下询问用户是否要执行数据库迁移。 如果用户确认,则调用 _run_migrations 方法执行迁移。 """ print("\n" + "=" * 60) print("数据库迁移提示") print("=" * 60) print("\n 注意:执行数据库迁移前,请确保已将新生成的应用添加到 settings.py 的 INSTALLED_APPS 中") print(f" 应用路径: application.{self.config['app_name']}") print("\n" + "=" * 60) response = input("是否要执行数据库迁移?(y/n,默认n): ").strip().lower() if response == 'y' or response == 'yes': self._run_migrations() else: print("\n您可以选择手动执行以下命令:") print(f" 1. 将 'application.{self.config['app_name']}' 添加到 settings.py 的 INSTALLED_APPS") print(f" 2. cd {self.project_root}") print(" 3. python manage.py makemigrations") print(" 4. python manage.py migrate") print("\n或者在需要时重新运行迁移。") def _run_migrations(self): """ 执行数据库迁移命令 ============================================================= 依次执行以下操作: 1. python manage.py makemigrations - 生成迁移文件 2. python manage.py migrate - 执行迁移创建数据表 3. Django 4.1+ 会自动为数据表字段添加注释(通过 db_comment 参数) """ print("\n" + "=" * 60) print("开始执行数据库迁移") print("=" * 60) try: # 获取Python解释器路径 python_executable = sys.executable # 步骤1:生成建表文件(不指定应用名,让Django自动检测) print("\n 正在生成建表文件...") result1 = subprocess.run( [python_executable, 'manage.py', 'makemigrations'], cwd=self.project_root, capture_output=True, text=True, encoding='utf-8' ) print(f"返回码: {result1.returncode}") if result1.returncode == 0: print("建表文件生成成功") if result1.stdout: print("输出内容:") print(result1.stdout) else: print("建表文件生成失败") print(f"错误信息: {result1.stderr}") print("\n 迁移失败,跳过后续步骤") return # 步骤2:执行建表文件 print("\n 正在执行建表文件...") result2 = subprocess.run( [python_executable, 'manage.py', 'migrate'], cwd=self.project_root, capture_output=True, text=True, encoding='utf-8' ) print(f"返回码: {result2.returncode}") if result2.returncode == 0: print("数据表创建成功") if result2.stdout: print("输出内容:") print(result2.stdout) else: print("数据表创建失败") print(f"错误信息: {result2.stderr}") print("\n 迁移失败,跳过后续步骤") return # 步骤3:Django 4.1+ 会自动为数据表字段添加注释 # 通过模型中的 db_comment 参数和 db_table_comment 配置 print("\n" + "=" * 60) print("数据库迁移完成!") print("Django 4.1+ 会自动为数据表字段添加注释(通过 db_comment 参数)") print("=" * 60) except Exception as e: print(f"\n 执行数据库迁移时发生错误: {str(e)}") print("\n您可以手动执行以下命令:") print(f" 1. 将 'application.{self.config['app_name']}' 添加到 settings.py 的 INSTALLED_APPS") print(f" 2. cd {self.project_root}") print(" 3. python manage.py makemigrations") print(" 4. python manage.py migrate") def _prompt_create_permission(self): """ 提示是否创建菜单权限节点 ============================================================= 在交互模式下询问用户是否要创建菜单和权限节点。 """ print("\n" + "=" * 60) print("菜单权限创建提示") print("=" * 60) response = input("是否要创建菜单权限节点?(y/n,默认n): ").strip().lower() if response == 'y' or response == 'yes': self._create_permission_nodes() else: print("\n您可以稍后手动创建菜单权限节点。") def _create_permission_nodes(self): """ 创建模块菜单和权限节点 ============================================================= 根据配置信息创建菜单和权限节点,包括: - 模块主菜单 - 查询分页权限 - 查询列表权限 - 查询详情权限 - 添加权限 - 更新权限 - 设置状态权限 - 删除权限 - 批量删除权限 """ print("\n" + "=" * 60) print("开始创建菜单权限节点") print("=" * 60) try: # 获取配置信息 table_name = self.config.get('table_name', self.config['app_name']) table_annotation = self.config.get('module_comment', '') table_prefix = self.config.get('table_prefix', []) remove_prefix = self.config.get('remove_prefix', False) # 步骤1:处理表名,移除前缀 # ========================================================== original_table_name = table_name if remove_prefix and table_prefix: for prefix in table_prefix: if table_name.startswith(prefix): table_name = table_name.replace(prefix, "", 1) break print(f"表名处理: {original_table_name} -> {table_name}") # 步骤2:将下划线命名转换为帕斯卡命名(首字母大写) # ========================================================== entity_name = self._to_pascal_case(table_name) if not entity_name: print("实体类名称为空,无法创建菜单") return print(f"实体类名: {entity_name}") # 步骤3:处理表描述 # ========================================================== table_comment = table_annotation.replace("表", "").replace("管理", "").strip() if not table_comment: table_comment = entity_name print(f"模块名称: {table_comment}") # 步骤4:构建菜单路径 # ========================================================== module_path = entity_name.lower() component_path = f"/{self.config.get('menu_group', 'tool')}/{module_path}/index" menu_path = f"/{self.config.get('menu_group', 'tool')}/{module_path}" print(f"菜单路径: {menu_path}") print(f"组件路径: {component_path}") # 步骤5:准备权限节点数据 # ========================================================== permission_nodes = [ {'sort': 1, 'name': f'查询{table_comment}分页', 'path': f'/{module_path}/page', 'permission': f'{self.config["permission_prefix"]}:page'}, {'sort': 5, 'name': f'查询{table_comment}列表', 'path': f'/{module_path}/list', 'permission': f'{self.config["permission_prefix"]}:list'}, {'sort': 10, 'name': f'查询{table_comment}详情', 'path': f'/{module_path}/detail', 'permission': f'{self.config["permission_prefix"]}:detail'}, {'sort': 15, 'name': f'添加{table_comment}', 'path': f'/{module_path}/add', 'permission': f'{self.config["permission_prefix"]}:add'}, {'sort': 20, 'name': f'更新{table_comment}', 'path': f'/{module_path}/update', 'permission': f'{self.config["permission_prefix"]}:update'}, {'sort': 25, 'name': f'设置状态', 'path': f'/{module_path}/status', 'permission': f'{self.config["permission_prefix"]}:status'}, {'sort': 30, 'name': f'删除{table_comment}', 'path': f'/{module_path}/delete', 'permission': f'{self.config["permission_prefix"]}:delete'}, {'sort': 35, 'name': f'批量删除', 'path': f'/{module_path}/batchDelete', 'permission': f'{self.config["permission_prefix"]}:batchDelete'}, ] # 步骤6:输出菜单创建信息 # ========================================================== print("\n" + "-" * 60) print("即将创建以下菜单权限节点:") print("-" * 60) print(f"\n【主菜单】") print(f" 名称: {table_comment}") print(f" 路径: {menu_path}") print(f" 组件: {component_path}") print(f" 权限: {self.config['permission_prefix']}:index") print(f" 父级ID: {self.config.get('parent_menu_id', 0)}") print(f"\n【子权限节点】") for node in permission_nodes: print(f" {node['sort']:2d}. {node['name']:<20} -> {node['permission']}") # 步骤7:写入数据库(自动模式直接执行,交互模式已确认) # ========================================================== self._insert_menus_to_db(table_comment, menu_path, component_path, permission_nodes) except Exception as e: print(f"\n 创建菜单权限节点时发生错误: {str(e)}") import traceback traceback.print_exc() def _insert_menus_to_db(self, table_comment, menu_path, component_path, permission_nodes): """ 将菜单权限节点写入数据库 ============================================================= 直接操作数据库插入菜单和权限节点。 Args: table_comment: 模块名称(中文) menu_path: 菜单路径 component_path: 组件路径 permission_nodes: 权限节点列表 """ try: # 获取当前文件所在目录 current_file = os.path.abspath(__file__) current_dir = os.path.dirname(current_file) # 项目根目录(当前目录) project_root = current_dir # 添加项目根目录到Python路径 if project_root not in sys.path: sys.path.insert(0, project_root) # 设置Django环境 - 使用 application.settings os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'application.settings') # 初始化Django import django django.setup() print("Django环境初始化成功") # 导入模型 from application.menu.models import Menu from django.utils import timezone print("导入Menu模型成功") parent_menu_id = self.config.get('parent_menu_id', 0) # 步骤1:检查主菜单是否已存在 existing_menu = Menu.objects.filter( path=menu_path, type=0 ).first() if existing_menu: print(f"\n 主菜单已存在,ID: {existing_menu.id}") # 自动更新主菜单信息 existing_menu.name = table_comment existing_menu.path = menu_path existing_menu.component = component_path existing_menu.permission = f"{self.config['permission_prefix']}:index" existing_menu.sort = self.config.get('menu_sort', 5) existing_menu.update_time = timezone.now() existing_menu.save() menu_id = existing_menu.id print(f"主菜单更新成功,ID: {menu_id}") else: # 创建主菜单 menu = Menu( name=table_comment, parent_id=parent_menu_id, path=menu_path, component=component_path, permission=f"{self.config['permission_prefix']}:index", type=0, status=0, hide=0, sort=self.config.get('menu_sort', 5), target=0, icon='el-icon-Bicycle', icon2='CodeSandboxOutlined', create_time=timezone.now(), update_time=timezone.now(), create_user='system', is_delete=False ) menu.save() menu_id = menu.id print(f"主菜单创建成功,ID: {menu_id}") # 步骤2:删除旧的子权限节点 deleted_count = Menu.objects.filter(parent_id=menu_id, type=1).delete() print(f"已删除 {deleted_count[0]} 个旧的权限节点") # 步骤3:创建新的子权限节点 for node in permission_nodes: child_menu = Menu( name=node['name'], parent_id=menu_id, path=node['path'], permission=node['permission'], type=1, status=0, hide=0, sort=node['sort'], target=0, create_time=timezone.now(), update_time=timezone.now(), create_user='system', is_delete=False ) child_menu.save() print(f" 创建权限节点: {node['name']}") print(f"\n菜单权限节点创建完成!共创建 {len(permission_nodes)} 个权限节点") except ImportError as e: print(f"\n 导入Django模型失败: {str(e)}") print("\n请确保:") print("1. 在项目根目录下运行此脚本") print("2. 已激活虚拟环境") print("3. 已安装所有依赖包") print("4. 确保 application/settings.py 文件存在") print(f"5. 当前项目根目录: {project_root}") except Exception as e: print(f"\n 写入数据库失败: {str(e)}") import traceback traceback.print_exc() def _prepare_context(self): """ 准备模板上下文数据 ============================================================= 将配置信息转换为模板可用的上下文数据。 处理字段信息、选项信息、各种标识等。 Returns: dict: 包含所有模板变量的上下文字典 """ config = self.config # 字段列表处理 fields = [] # 所有字段 form_fields = [] # 表单字段 list_fields = [] # 列表显示字段 filter_fields = [] # 筛选字段 editable_fields = [] # 可编辑字段 filterable_fields = [] # 可过滤字段 searchable_fields = [] # 可搜索字段 # 遍历配置中的字段列表,处理每个字段的信息 for field in config.get('fields', []): # 原始注释 raw_comment = field.get('comment', '') # 清洗后的注释(移除选项说明) clean_comment = self._clean_field_comment(raw_comment) # 基础字段信息 field_info = { 'name': field['name'], # 字段名(下划线命名) 'camel_name': self._to_camel_case(field['name']), # 字段名(驼峰命名) 'comment': raw_comment, # 字段注释 'label': clean_comment, # 清洗后的注释 'type': field.get('type', 'CharField'), # 字段类型(用于模板判断) 'type_display': field.get('type_display', '字符串'), # 类型显示名称 'purpose': field.get('purpose', ''), # 字段用途说明 'nullable': field.get('nullable', False), # 是否可为空 'max_length': field.get('max_length'), # 最大长度 'is_unique': field.get('is_unique', False), # 是否唯一 'db_index': field.get('db_index', False), # 是否创建索引 'default': field.get('default'), # 默认值 'choices': field.get('choices'), # 选项列表 'min_value': field.get('min_value'), # 最小值 'max_value': field.get('max_value'), # 最大值 'choices_name': field['name'].upper() + '_CHOICES' if field.get('choices') else None, # 选项常量名 'is_image': field.get('is_image'), # 添加图片标识 'is_rich_text': field.get('is_rich_text'), # 添加富文本标识 } # 模型字段类型映射 # 将配置中的类型名映射到Django模型字段类 field_type_map = { 'CharField': 'CharField', 'IntegerField': 'IntegerField', 'BooleanField': 'BooleanField', 'DateTimeField': 'DateTimeField', 'DateField': 'DateField', 'TextField': 'TextField', 'FloatField': 'FloatField', 'DecimalField': 'DecimalField', } field_info['model_field_type'] = field_type_map.get(field.get('type'), 'CharField') # 表单字段类型映射 # 将配置中的类型名映射到Django表单字段类 form_field_map = { 'CharField': 'CharField', 'IntegerField': 'IntegerField', 'BooleanField': 'BooleanField', 'DateTimeField': 'DateTimeField', 'DateField': 'DateField', 'TextField': 'CharField', 'FloatField': 'FloatField', 'DecimalField': 'DecimalField', } field_info['form_field_type'] = form_field_map.get(field.get('type'), 'CharField') # 是否必填(不可为空且在表单中需要验证) field_info['required'] = not field.get('nullable', False) and field.get('required', True) # 处理选项字段,为模板准备choices列表 if field_info['choices']: choices_list = [] for value, label in field_info['choices']: choices_list.append({ 'value': value, 'label': label }) field_info['choices'] = choices_list fields.append(field_info) # 根据配置将字段添加到不同的分类列表中 if field.get('in_form', True): form_fields.append(field_info) # 表单字段 if field.get('in_list', True): list_fields.append(field_info) # 列表显示字段 if field.get('filterable', False): field_info['filter_type'] = field.get('filter_type', '模糊查询') filter_fields.append(field_info) # 筛选字段 if field.get('editable', False): editable_fields.append(field_info) # 可编辑字段 if field.get('filterable', False): filterable_fields.append(field_info) # 可过滤字段 if field.get('searchable', False): searchable_fields.append(field_info) # 可搜索字段 # 在构建 context 之前添加调试信息 print("\n" + "=" * 60) print("调试信息 - _prepare_context") print("=" * 60) print(f"总字段数: {len(fields)}") print(f"form_fields 数量: {len(form_fields)}") for f in form_fields: print( f" - {f['name']}: {f['comment']} (type: {f.get('type', 'N/A')}, required: {f.get('required', False)})") # 在构建字段列表后,添加图片字段标识 has_image_field = any(field.get('is_image', False) for field in fields) # 在构建字段列表后,添加富文本字段标识 has_rich_text_field = any(field.get('is_rich_text', False) for field in fields) # ========== 新增:生成驼峰命名的模型类名 ========== # 将帕斯卡命名转换为驼峰命名(首字母小写) # 例如:OrderItem -> orderItem model_class_name = config['model_class_name'] model_class_name_camel = model_class_name[0].lower() + model_class_name[1:] if model_class_name else '' # ========== 新增结束 ========== # ========== 新增:生成模块名称(前端目录名) ========== # 将应用名中的下划线移除,生成前端模块名称 # 例如:order_item -> orderitem # 单次词(不含下划线)保持不变 app_name = config['app_name'] module_name = app_name.replace('_', '') # 构建完整上下文 context = { # 基础信息 'app_name': app_name, # 应用名称 'module_comment': config['module_comment'], # 模块中文注释 'module_name': module_name, # 模块名称 'model_class_name': config['model_class_name'], # 模型类名(大写开头) 'model_class_name_camel': model_class_name_camel, # 模型类名(驼峰命名,首字母小写) 'route_prefix': config.get('route_prefix', config['app_name']), # URL路由前缀 # 业务描述 'business_description': config.get('business_description', ''), # 业务简要描述 'business_description_detail': config.get('business_description_detail', ''), # 业务详细描述 # 权限和标识 'permission_prefix': config.get('permission_prefix', config['app_name']), # 权限前缀 'primary_key': config.get('primary_key', 'id'), # 主键字段名 'display_field': config.get('display_field', 'name'), # 显示字段名 'display_field_camel': config.get('display_field_camel', 'name'), # 显示字段名 # 表信息 'table_name': config.get('table_name', config['app_name']), # 数据库表名 # 功能标识 'has_sort': config.get('has_sort', False), # 是否有排序字段 'has_status': config.get('has_status', True), # 是否有状态字段 'has_unique_code': config.get('has_unique_code', False), # 是否有唯一编码 'has_image_field': has_image_field, # 是否有图片字段 'has_rich_text_field': has_rich_text_field, # 是否有富文本字段 # 筛选字段注释(用于文档) 'filter_fields_comment': '、'.join([f['comment'] for f in filter_fields]), # 各分类字段列表 'fields': fields, # 所有字段 'form_fields': form_fields, # 表单字段 'list_fields': list_fields, # 列表显示字段 'filter_fields': filter_fields, # 筛选字段 'editable_fields': editable_fields, # 可编辑字段 'filterable_fields': filterable_fields, # 可过滤字段 'searchable_fields': searchable_fields, # 可搜索字段 # 选项相关 - 这些是为模板准备的全局变量 'choices_dict': {field['name']: field['choices'] for field in fields if field.get('choices')}, # 选项字典 'choices_name': '', # 这个会在具体字段中使用,不在全局使用 'choices_field': '', # 这个会在具体字段中使用,不在全局使用 } return context def _clean_field_comment(self, comment): """ 清洗字段注释,移除选项说明部分 例如: - "案例类型:1-类型1 2-类型2 3-类型3 4-类型4" -> "案例类型" - "案例状态:1-正常 2-停用" -> "案例状态" - "性别:1-男 2-女" -> "性别" Args: comment: 原始字段注释 Returns: str: 清洗后的注释 """ if not comment: return '' import re # 检查是否包含选项模式(数字-文字 或 数字:文字) option_pattern = r'\d+[\-\:]\S+' if re.search(option_pattern, comment): # 如果包含选项模式,取冒号或中文冒号前面的部分 if ':' in comment: return comment.split(':')[0].strip() elif ':' in comment: return comment.split(':')[0].strip() return comment def _prepare_vue_context(self, base_context): """ 准备Vue模板上下文数据 ============================================================= 在基础上下文上添加Vue特有的配置信息。 Args: base_context: 基础上下文数据 Returns: dict: 包含Vue模板变量的上下文字典 """ # 复制基础上下文,避免修改原数据 context = base_context.copy() print("\n" + "=" * 60) print("调试信息 - _prepare_vue_context") print("=" * 60) print(f"base_context 中的 form_fields 数量: {len(base_context.get('form_fields', []))}") # # ========== 添加URL路由前缀到前端上下文开始 ========== # # 获取URL前缀,用于前端API路径 # app_name = context.get('app_name', '') # route_prefix = self.config.get('route_prefix', app_name) # # # 将 URL 前缀添加到上下文,供前端模板使用 # context['route_prefix'] = route_prefix # # # 将 URL 前缀中的斜杠转换为下划线,用于前端目录名 # # 例如:order/item -> orderitem # vue_dir_name = route_prefix.replace('/', '') # context['vue_dir_name'] = vue_dir_name # # print(f"🔧 前端配置: app_name={app_name}, route_prefix={route_prefix}, vue_dir={vue_dir_name}") # # ========== 添加URL路由前缀到前端上下文结束 ========== # 添加API路径(默认放在tool模块下) context['api_path'] = f'tool/{context["module_name"]}' # 添加导出功能标识 context['has_export'] = self.config.get('has_export', False) # ========== 判断是否为树形结构开始 ========== # 检查 fields 中是否包含 parent_id 或 pid 字段 is_tree_structure = False parent_id_field = None for field in self.config.get('fields', []): if field['name'] in ['parent_id', 'pid']: is_tree_structure = True parent_id_field = field['name'] break # 添加树形结构标识到上下文 context['is_tree_structure'] = is_tree_structure context['parent_id_field'] = self._to_camel_case(parent_id_field) # 根据树形结构设置 has_search_form if is_tree_structure: # 树形结构:默认不显示搜索表单(可配置) context['has_search_form'] = self.config.get('has_tree_search', False) # 树形结构:默认不显示多选框 context['show_selection'] = self.config.get('show_selection', False) # 树形结构:操作列宽度 context['action_column_width'] = self.config.get('action_column_width', 200) else: # 普通分页列表:根据 filter_fields 判断 context['has_search_form'] = bool(base_context.get('filter_fields', [])) # ========== 判断是否为树形结构结束 ========== # 确保 form_fields 存在 if 'form_fields' not in context: context['form_fields'] = [] print("警告: form_fields 不存在,已初始化为空列表") else: print(f"context 中的 form_fields 数量: {len(context['form_fields'])}") for f in context['form_fields']: print(f" - {f['name']}: {f['comment']}") return context def _create_init_files(self): """ 创建模块初始化文件和迁移目录初始化文件 ============================================================= 在生成的模块目录中创建必要的 __init__.py 文件: 1. 模块根目录下的 __init__.py - 使模块成为Python包 2. migrations 目录下的 __init__.py - 使迁移目录成为Python包 这些文件是Django应用正常运行所必需的,确保模块可以被正确导入。 """ print("\n" + "=" * 60) print("创建模块初始化文件") print("=" * 60) # 步骤1:创建模块根目录的 __init__.py # ========================================================== init_file_path = os.path.join(self.output_dir, '__init__.py') # 检查文件是否已存在,避免覆盖 if not os.path.exists(init_file_path): with open(init_file_path, 'w', encoding='utf-8') as f: # 写入模块注释和基本信息 f.write(f'"""\n') f.write(f'{self.config["module_comment"]}模块\n') f.write(f'=============================================================\n') f.write(f'\n') f.write(f'该模块包含{self.config["module_comment"]}相关的模型、视图、表单、服务等代码。\n') f.write(f'"""\n') f.write(f'\n') f.write(f'# 模块初始化文件,使该目录成为一个Python包\n') f.write(f'# 可以通过以下方式导入模块中的内容:\n') f.write(f'# from application.{self.config["app_name"]} import models, views, forms\n') print(f"创建文件: {init_file_path}") else: print(f"文件已存在,跳过创建: {init_file_path}") # 步骤2:创建 migrations 目录并添加 __init__.py # ========================================================== migrations_dir = os.path.join(self.output_dir, 'migrations') os.makedirs(migrations_dir, exist_ok=True) print(f"创建迁移目录: {migrations_dir}") # 创建 migrations/__init__.py migrations_init_path = os.path.join(migrations_dir, '__init__.py') if not os.path.exists(migrations_init_path): with open(migrations_init_path, 'w', encoding='utf-8') as f: f.write(f'"""\n') f.write(f'{self.config["module_comment"]}模块数据库迁移文件\n') f.write(f'=============================================================\n') f.write(f'\n') f.write(f'该目录包含Django生成的数据库迁移文件,用于管理数据库结构变更。\n') f.write(f'"""\n') f.write(f'\n') f.write(f'# Django迁移目录初始化文件\n') f.write(f'# 该文件使migrations目录成为一个Python包,Django可以正确加载迁移文件\n') print(f"创建文件: {migrations_init_path}") else: print(f"文件已存在,跳过创建: {migrations_init_path}") # 可选:创建 migrations 目录下的其他必要文件 # ========================================================== # 创建 .gitkeep 文件(如果 migrations 目录为空,git 可能不会追踪) gitkeep_path = os.path.join(migrations_dir, '.gitkeep') if not os.path.exists(gitkeep_path): with open(gitkeep_path, 'w', encoding='utf-8') as f: f.write('# 保持 migrations 目录在版本控制中\n') print(f"创建文件: {gitkeep_path}") print("\n模块初始化文件创建完成!") def _update_main_urls(self): """ 更新主路由文件,添加新模块的路由配置 ============================================================= 在 application/urls.py 文件的 urlpatterns 列表中添加新模块的路由配置。 路由路径转换规则: - 使用配置中的 route_prefix 作为路由路径 - 例如:order_item -> /order/item/ """ print("\n" + "=" * 60) print("更新主路由文件") print("=" * 60) # 主路由文件路径 urls_file = os.path.join(self.project_root, 'application', 'urls.py') if not os.path.exists(urls_file): print(f"主路由文件不存在: {urls_file}") return # 获取配置信息 app_name = self.config['app_name'] module_comment = self.config['module_comment'] # ========== 修改:获取URL路由前缀 ========== # 优先使用配置中的 route_prefix,如果不存在则使用 app_name route_prefix = self.config.get('route_prefix', app_name) # 记录转换结果,便于调试 if '_' in app_name and route_prefix != app_name: print(f"🔧 路由转换: {app_name} -> {route_prefix}") # ========== 修改结束 ========== # 读取文件内容 with open(urls_file, 'r', encoding='utf-8') as f: content = f.read() # 检查是否已添加该模块的路由(使用转换后的路径进行检查) if f"path('{route_prefix}/'" in content: print(f"路由已存在: path('{route_prefix}/', include('application.{app_name}.urls'))") return # 构建要插入的路由配置(注意缩进,与现有路由保持一致) # 使用转换后的 route_prefix 作为URL路径 new_route = f""" # {module_comment}模块路由 # 访问地址: /{route_prefix}/ # 包含路由: application.{app_name}.urls 中定义的所有子路由 # 功能: {module_comment}模块,用于{module_comment}数据的增删改查管理 path('{route_prefix}/', include('application.{app_name}.urls')),""" # 找到 urlpatterns 列表的起始和结束位置 # 查找 "urlpatterns = [" 的位置 start_pattern = r'urlpatterns\s*=\s*\[' start_match = re.search(start_pattern, content) if not start_match: print("未找到 urlpatterns 定义,请手动添加路由") return # 找到 urlpatterns 列表的结束位置(匹配对应的 ]) start_pos = start_match.end() bracket_count = 1 end_pos = start_pos for i in range(start_pos, len(content)): if content[i] == '[': bracket_count += 1 elif content[i] == ']': bracket_count -= 1 if bracket_count == 0: end_pos = i break if end_pos <= start_pos: print("未找到 urlpatterns 列表结束位置,请手动添加路由") return # 提取 urlpatterns 列表的内容 urlpatterns_content = content[start_pos:end_pos] # 在最后一个路由后面插入新路由 # 找到最后一个 path 的位置 lines = urlpatterns_content.split('\n') # 找到最后一个非空行的位置 insert_line = -1 for i in range(len(lines) - 1, -1, -1): line = lines[i].strip() if line and not line.startswith('#'): insert_line = i + 1 break if insert_line == -1: # 如果没有找到任何路由,则在第一行插入 insert_line = 0 # 插入新路由 lines.insert(insert_line, new_route) # 重新组合内容 new_urlpatterns_content = '\n'.join(lines) new_content = content[:start_pos] + new_urlpatterns_content + content[end_pos:] with open(urls_file, 'w', encoding='utf-8') as f: f.write(new_content) print(f"已添加路由: path('{route_prefix}/', include('application.{app_name}.urls'))") def _update_installed_apps(self): """ 更新 settings.py 文件,将新模块添加到 INSTALLED_APPS ============================================================= 在 application/settings.py 文件的 INSTALLED_APPS 列表中添加新应用。 """ print("\n" + "=" * 60) print("更新 settings.py 配置") print("=" * 60) # settings.py 文件路径 settings_file = os.path.join(self.project_root, 'application', 'settings.py') if not os.path.exists(settings_file): print(f"settings.py 文件不存在: {settings_file}") return # 获取配置信息 app_name = self.config['app_name'] module_comment = self.config['module_comment'] # 读取文件内容 with open(settings_file, 'r', encoding='utf-8') as f: content = f.read() # 检查是否已添加该应用 if f"'application.{app_name}'" in content or f'"application.{app_name}"' in content: print(f"应用已存在: application.{app_name}") return # 构建要插入的应用配置(固定缩进 4 个空格) new_app = f" 'application.{app_name}', # {module_comment}管理" # 找到 INSTALLED_APPS 列表的结束位置 # 找到最后一个应用后面的位置 lines = content.split('\n') # 找到 INSTALLED_APPS 列表结束的索引 insert_line = -1 in_apps = False bracket_count = 0 for i, line in enumerate(lines): if 'INSTALLED_APPS' in line and '=' in line and '[' in line: in_apps = True bracket_count += line.count('[') - line.count(']') continue if in_apps: bracket_count += line.count('[') - line.count(']') # 找到列表结束的 ] 所在行 if ']' in line and bracket_count == 0: insert_line = i break if insert_line == -1: print("未找到 INSTALLED_APPS 列表结束位置,请手动添加") return # 在结束括号前插入新应用 lines.insert(insert_line, new_app) # 写回文件 new_content = '\n'.join(lines) with open(settings_file, 'w', encoding='utf-8') as f: f.write(new_content) print(f"已添加应用: application.{app_name}") def _generate_vue_files(self, base_context): """ 生成Vue前端文件 ============================================================= 生成前端Vue代码文件,包括: - index.vue: 列表页面 - edit.vue: 编辑对话框 - querySchemas.ts: 查询表单配置 - columns.ts: 表格列配置 - api.ts: API接口 输出目录: - Vue组件文件: ui/src/views/tool/{模块名称}/ - API接口文件: ui/src/api/tool/ Args: base_context: 基础上下文数据 """ # 准备Vue模板上下文 vue_context = self._prepare_vue_context(base_context) print("\n" + "=" * 60) print("调试信息 - _generate_vue_files") print("=" * 60) print(f"vue_context 中的 form_fields 数量: {len(vue_context.get('form_fields', []))}") # 获取模块名称(用于目录) module_name = base_context.get('module_name') # # # 原始 module_path 可能从 base_context 获取,默认为 module_name # module_path = base_context.get('module_path', module_name) # # # 将 module_path 中的下划线移除,用于前端目录名 # # 例如:order_item -> orderitem # module_path = module_path.replace('_', '') # print(f"前端目录名转换: {base_context.get('module_path', module_name)} -> {module_path}") # 获取项目根目录 project_root = os.path.dirname(os.path.abspath(__file__)) # 步骤1:生成Vue组件文件(放到 ui/src/views/{模块名称}/ 目录) # ========================================================== vue_views_dir = os.path.join(project_root, 'ui', 'src', 'views', 'tool', module_name) os.makedirs(vue_views_dir, exist_ok=True) print(f"创建Vue组件目录: {vue_views_dir}") # ========== 修改:根据是否为树形结构选择不同模板 ========== is_tree = vue_context.get('is_tree_structure', False) if is_tree: # 树形结构:使用 ui2 目录下的模板 vue_component_templates = [ ('ui2/index.vue.tpl', 'index.vue'), # 树形列表页面模板 ('ui2/edit.vue.tpl', 'edit.vue'), # 编辑对话框模板 ('ui2/detail.vue.tpl', 'detail.vue'), # 详情对话框模板 ('ui2/columns.ts.tpl', 'columns.ts'), # 表格列配置模板 ] print(f"检测到树形结构(parent_id字段),使用树形模板") else: # 普通分页列表:使用 ui 目录下的模板 vue_component_templates = [ ('ui/index.vue.tpl', 'index.vue'), # 列表页面模板 ('ui/edit.vue.tpl', 'edit.vue'), # 编辑对话框模板 ('ui/detail.vue.tpl', 'detail.vue'), # 详情对话框模板 ('ui/querySchemas.ts.tpl', 'querySchemas.ts'), # 查询表单配置模板 ('ui/columns.ts.tpl', 'columns.ts'), # 表格列配置模板 ] print(f"普通分页列表,使用标准模板") # ========== 修改结束 ========== # 渲染每个Vue组件文件 for template_name, output_name in vue_component_templates: output_path = os.path.join(vue_views_dir, output_name) self._render_template(template_name, output_path, vue_context) print(f"Vue组件文件生成完成!输出目录: {vue_views_dir}") # 步骤2:生成API接口文件(放到 ui/src/api/tool/ 目录) # ========================================================== vue_api_dir = os.path.join(project_root, 'ui', 'src', 'api', 'tool') os.makedirs(vue_api_dir, exist_ok=True) print(f"创建API目录: {vue_api_dir}") # API接口模板 api_output_path = os.path.join(vue_api_dir, f'{module_name}.ts') self._render_template('ui/api.ts.tpl', api_output_path, vue_context) print(f"API接口文件生成完成!输出文件: {api_output_path}") print(f"\nVue前端文件生成完成!") print(f" 组件目录: {vue_views_dir}") print(f" API文件: {api_output_path}") def _render_template(self, template_name, output_path, context=None): """ 渲染模板并保存文件 ============================================================= Args: template_name: 模板文件名(可以是子目录下的文件,如 'vue/index.vue.tpl') output_path: 输出文件路径 context: 模板上下文,如果为None则使用基础上下文 """ # 检查模板是否存在 try: template = self.env.get_template(template_name) except Exception as e: print(f"模板不存在,跳过: {template_name}") return False try: if context is None: context = self._prepare_context() output = template.render(**context) os.makedirs(os.path.dirname(output_path), exist_ok=True) with open(output_path, 'w', encoding='utf-8') as f: f.write(output) print(f"生成文件: {output_path}") except Exception as e: print(f"渲染模板 {template_name} 失败: {str(e)}") raise def _to_camel_case(self, snake_str): """ 将下划线命名转换为驼峰命名 例如:'user_name' -> 'userName' Args: snake_str: 下划线命名字符串 Returns: str: 驼峰命名字符串 """ if not snake_str: # 处理 None 和空字符串 return snake_str # 确保是字符串类型 snake_str = str(snake_str) components = snake_str.split('_') return components[0] + ''.join(x.title() for x in components[1:]) def _to_pascal_case(self, snake_str): """ 将下划线命名转换为帕斯卡命名 例如:'user_name' -> 'UserName' Args: snake_str: 下划线命名字符串 Returns: str: 帕斯卡命名字符串 """ if not snake_str: # 处理 None 和空字符串 return snake_str # 确保是字符串类型 snake_str = str(snake_str) components = snake_str.split('_') return ''.join(x.title() for x in components) # ============================================================================ # 使用示例 # ============================================================================ if __name__ == '__main__': """ 命令行执行示例: python generator_config.py 将生成模块的完整代码放入指定目录 Django 4.1+ 会自动为数据表字段添加注释(通过 db_comment 参数) """ print("=" * 60) print("代码生成器启动") print("=" * 60) # 获取配置文件路径(与当前文件同目录下的 config.json) config_file = os.path.join(os.path.dirname(__file__), 'config.json') # 检查配置文件是否存在 if not os.path.exists(config_file): print(f"配置文件不存在: {config_file}") print("请确保 config.json 文件存在并配置正确") sys.exit(1) # 读取配置文件 try: with open(config_file, 'r', encoding='utf-8') as f: MODULE_CONFIG = json.load(f) print(f"成功加载配置文件: {config_file}") print(f" 应用名称: {MODULE_CONFIG.get('app_name', 'N/A')}") print(f" 模块名称: {MODULE_CONFIG.get('module_comment', 'N/A')}") except json.JSONDecodeError as e: print(f"配置文件解析失败: {str(e)}") sys.exit(1) except Exception as e: print(f"读取配置文件失败: {str(e)}") sys.exit(1) # 创建代码生成器实例(使用完整配置) generator = CodeGenerator(MODULE_CONFIG) # 执行代码生成(交互模式) generator.generate() # 原有交互模式 # 自动模式示例(用于可视化界面) # generator.generate(auto=True) # 自动模式,跳过迁移确认,直接创建菜单 print("\n" + "=" * 60) print("代码生成完成!") print("=" * 60)