1326 lines
55 KiB
Python
1326 lines
55 KiB
Python
# +======================================================================
|
||
# | 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)
|