Files
2026-09-12 12:07:15 +08:00

1326 lines
55 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# +======================================================================
# | 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)