Files
django-vue3-admin-gd/backend/system/management/commands/generate_crud.py
2025-10-02 14:52:01 +08:00

339 lines
12 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
自动生成 CRUD 代码的 Django 管理命令
使用方法: python manage.py generate_crud <app_name> <model_name>
例如: python manage.py generate_crud system Dept
"""
import os
from string import Template
from django.core.management.base import BaseCommand, CommandError
from django.apps import apps
from django.db import models
from utils.string_utils import camel_to_snake
TPL_DIR = os.path.join(os.path.dirname(__file__), 'tpl')
def render_tpl(tpl_name, context):
tpl_path = os.path.join(TPL_DIR, tpl_name)
with open(tpl_path, 'r', encoding='utf-8') as f:
tpl = Template(f.read())
return tpl.substitute(context)
def ensure_view_dirs(app_name, model_name_snake):
base_dir = f'../web/apps/web-antd/src/views/{app_name.lower()}'
model_dir = os.path.join(base_dir, model_name_snake)
if not os.path.exists(base_dir):
os.makedirs(model_dir, exist_ok=True)
else:
if not os.path.exists(model_dir):
os.makedirs(model_dir, exist_ok=True)
def capitalize_first(s):
return s[:1].upper() + s[1:] if s else s
def camel_case(s):
return ''.join(word.capitalize() for word in s.split('_'))
def get_context(app_name, model_name, model, model_name_snake):
return {
'model_name': model_name,
'app_name': app_name,
'app_name_camel': camel_case(app_name),
'model_name_lower': model_name.lower(),
'model_name_snake': model_name_snake,
'verbose_name': model._meta.verbose_name or model_name,
}
class Command(BaseCommand):
help = '根据模型自动生成 CRUD 代码(后端视图 + 前端页面 + 模型)'
def add_arguments(self, parser):
parser.add_argument('app_name', type=str, help='应用名称')
parser.add_argument('model_name', type=str, help='模型名称')
parser.add_argument(
'--frontend',
action='store_true',
help='是否同时生成前端代码',
)
def handle(self, *args, **options):
app_name = options['app_name']
model_name = options['model_name']
model_name_snake = camel_to_snake(model_name)
generate_frontend = options.get('frontend', False)
try:
# 获取模型
model = apps.get_model(app_name, model_name)
except LookupError:
raise CommandError(f'模型 {app_name}.{model_name} 不存在')
# 生成后端代码
self.generate_backend_code(app_name, model_name, model, model_name_snake)
if generate_frontend:
# 生成前端代码
self.generate_frontend_code(app_name, model_name, model, model_name_snake)
self.stdout.write(
self.style.SUCCESS(f'成功生成 {app_name}.{model_name} 的 CRUD 代码')
)
def generate_backend_code(self, app_name, model_name, model, model_name_snake):
"""生成后端代码"""
# 生成视图集
self.generate_viewset(app_name, model_name, model, model_name_snake)
def generate_viewset(self, app_name, model_name, model, model_name_snake):
"""生成视图集"""
filter_fields = []
for field in model._meta.fields:
if isinstance(field, (models.CharField, models.IntegerField, models.BooleanField)):
filter_fields.append(f"'{field.name}'")
context = get_context(app_name, model_name, model, model_name_snake)
context['filterset_fields'] = ', '.join(filter_fields)
viewset_code = render_tpl('viewset.py.tpl', context)
viewset_path = f'{app_name}/views/{model_name_snake}.py'
os.makedirs(os.path.dirname(viewset_path), exist_ok=True)
with open(viewset_path, 'w', encoding='utf-8') as f:
f.write(viewset_code)
self.stdout.write(f'生成视图集: {viewset_path}')
def generate_frontend_code(self, app_name, model_name, model, model_name_snake):
ensure_view_dirs(app_name, model_name_snake)
self.generate_frontend_model(app_name, model_name, model, model_name_snake)
self.generate_frontend_list(app_name, model_name, model, model_name_snake)
self.generate_frontend_data(app_name, model_name, model, model_name_snake)
self.generate_frontend_form_component(app_name, model_name, model, model_name_snake)
def generate_frontend_model(self, app_name, model_name, model, model_name_snake):
"""生成前端模型定义"""
interface_fields = []
for field in model._meta.fields:
field_type = self.get_typescript_type(field)
interface_fields.append(f' {field.name}: {field_type};')
interface_content = '\n'.join(interface_fields)
context = get_context(app_name, model_name, model, model_name_snake)
context['interface_fields'] = interface_content
model_code = render_tpl('frontend_model.ts.tpl', context)
model_path = f'../web/apps/web-antd/src/models/{app_name.lower()}/{model_name_snake}.ts'
os.makedirs(os.path.dirname(model_path), exist_ok=True)
with open(model_path, 'w', encoding='utf-8') as f:
f.write(model_code)
self.stdout.write(f'生成前端模型: {model_path}')
def generate_frontend_list(self, app_name, model_name, model, model_name_snake):
context = get_context(app_name, model_name, model, model_name_snake)
list_code = render_tpl('frontend_list.vue.tpl', context)
list_path = f'../web/apps/web-antd/src/views/{app_name.lower()}/{model_name_snake}/list.vue'
with open(list_path, 'w', encoding='utf-8') as f:
f.write(list_code)
self.stdout.write(f'生成前端列表页面: {list_path}')
def generate_frontend_data(self, app_name, model_name, model, model_name_snake):
CORE_FIELDS = ['create_time', 'update_time', 'creator', 'modifier', 'is_deleted', 'remark']
business_fields = []
core_fields = []
for field in model._meta.fields:
if field.name in CORE_FIELDS:
core_fields.append(field)
else:
business_fields.append(field)
# 生成 useSchema
form_fields = []
for field in business_fields + core_fields:
if field.name in ['id', 'create_time', 'update_time', 'creator', 'modifier', 'is_deleted']:
continue # 这些一般不在表单里
field_config = self.generate_form_field(field)
if field_config:
form_fields.append(field_config)
# useGridFormSchema
grid_form_fields = []
for field in business_fields:
if field.name in ['id']:
continue
field_config = self.generate_grid_form_field(field)
if field_config:
grid_form_fields.append(field_config)
# 生成 useColumns
columns = self.get_columns_code(business_fields + core_fields)
context = get_context(app_name, model_name, model, model_name_snake)
context['form_fields'] = '\n'.join(form_fields)
context['grid_form_fields'] = '\n'.join(grid_form_fields)
context['columns'] = '\n'.join(columns)
data_path = f'../web/apps/web-antd/src/views/{app_name.lower()}/{model_name_snake}/data.ts'
data_code = render_tpl('frontend_data.ts.tpl', context)
with open(data_path, 'w', encoding='utf-8') as f:
f.write(data_code)
self.stdout.write(f'生成前端表单配置: {data_path}')
def generate_frontend_form_component(self, app_name, model_name, model, model_name_snake):
ensure_view_dirs(app_name, model_name_snake)
context = get_context(app_name, model_name, model, model_name_snake)
form_path = f'../web/apps/web-antd/src/views/{app_name.lower()}/{model_name_snake}/modules/form.vue'
form_code = render_tpl('frontend_form.vue.tpl', context)
os.makedirs(os.path.dirname(form_path), exist_ok=True)
with open(form_path, 'w', encoding='utf-8') as f:
f.write(form_code)
self.stdout.write(f'生成前端表单组件: {form_path}')
def get_typescript_type(self, field):
"""获取 TypeScript 类型"""
if isinstance(field, models.CharField):
return 'string'
elif isinstance(field, models.TextField):
return 'string'
elif isinstance(field, models.IntegerField):
return 'number'
elif isinstance(field, models.BooleanField):
return 'boolean'
elif isinstance(field, models.DateTimeField):
return 'string'
elif isinstance(field, models.DateField):
return 'string'
elif isinstance(field, models.ForeignKey):
return 'number'
else:
return 'any'
def generate_form_field(self, field):
field_name = field.name
field_label = getattr(field, 'verbose_name', field_name)
if field_name == 'status':
return f''' {{
component: 'RadioGroup',
componentProps: {{
buttonStyle: 'solid',
options: [
{{ label: $t('common.enabled'), value: 1 }},
{{ label: $t('common.disabled'), value: 0 }},
],
optionType: 'button',
}},
defaultValue: 1,
fieldName: 'status',
label: $t('system.status'),
}},'''
if isinstance(field, models.CharField):
return f''' {{
component: 'Input',
fieldName: '{field_name}',
label: '{field_label}',
rules: z.string().min(1, $t('ui.formRules.required', ['{field_label}'])).max(100, $t('ui.formRules.maxLength', ['{field_label}', 100])),
}},'''
elif isinstance(field, models.TextField):
return f''' {{
component: 'Input',
componentProps: {{ rows: 3, showCount: true }},
fieldName: '{field_name}',
label: '{field_label}',
rules: z.string().max(500, $t('ui.formRules.maxLength', ['{field_label}', 500])).optional(),
}},'''
elif isinstance(field, models.IntegerField):
return f''' {{
component: 'InputNumber',
fieldName: '{field_name}',
label: '{field_label}',
}},'''
elif isinstance(field, models.BooleanField):
return f''' {{
component: 'RadioGroup',
componentProps: {{
buttonStyle: 'solid',
options: [
{{ label: '开启', value: 1 }},
{{ label: '关闭', value: 0 }},
],
optionType: 'button',
}},
defaultValue: 1,
fieldName: '{field_name}',
label: '{field_label}',
}},'''
else:
return f''' {{
component: 'Input',
fieldName: '{field_name}',
label: '{field_label}',
}},'''
def generate_grid_form_field(self, field):
field_name = field.name
field_label = getattr(field, 'verbose_name', field_name)
if field_name == 'status':
return f''' {{
component: 'Select',
fieldName: 'status',
label: '状态',
componentProps: {{
allowClear: true,
options: [
{{ label: '启用', value: 1 }},
{{ label: '禁用', value: 0 }},
],
}},
}},'''
if isinstance(field, models.CharField):
return f''' {{
component: 'Input',
fieldName: '{field_name}',
label: '{field_label}',
}},'''
elif isinstance(field, models.IntegerField):
return f''' {{
component: 'InputNumber',
fieldName: '{field_name}',
label: '{field_label}',
}},'''
else:
return f''' {{
component: 'Input',
fieldName: '{field_name}',
label: '{field_label}',
}},'''
def get_columns_code(self, fields):
columns = []
for field in fields:
if field.name == 'status':
columns.append(
""" {
field: 'status',
title: '状态',
cellRender: { name: 'CellTag' },
},"""
)
continue
if isinstance(field, (models.DateField, models.DateTimeField)):
columns.append(
f""" {{
field: '{field.name}',
title: '{getattr(field, 'verbose_name', field.name)}',
width: 150,
formatter: ({{ cellValue }}) => format_datetime(cellValue),
}},"""
)
continue
columns.append(
f""" {{
field: '{field.name}',
title: '{getattr(field, 'verbose_name', field.name)}',
}},"""
)
return columns