245 lines
11 KiB
Python
245 lines
11 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
|
||
import re
|
||
from string import Template
|
||
from django.core.management.base import BaseCommand, CommandError
|
||
from django.apps import apps
|
||
from django.db import models
|
||
from django.conf import settings
|
||
|
||
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 camel_to_snake(name):
|
||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||
|
||
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 = []
|
||
for field in business_fields + core_fields:
|
||
columns.append(f" {{\n field: '{field.name}',\n title: '{getattr(field, 'verbose_name', field.name)}',\n }},")
|
||
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 isinstance(field, models.CharField):
|
||
return f''' {{\n component: 'Input',\n fieldName: '{field_name}',\n label: '{field_label}',\n rules: z\n .string()\n .min(1, $t('ui.formRules.required', ['{field_label}']))\n .max(100, $t('ui.formRules.maxLength', ['{field_label}', 100])),\n }},'''
|
||
elif isinstance(field, models.TextField):
|
||
return f''' {{\n component: 'Input',\n componentProps: {{\n rows: 3,\n showCount: true,\n }},\n fieldName: '{field_name}',\n label: '{field_label}',\n rules: z\n .string()\n .max(500, $t('ui.formRules.maxLength', ['{field_label}', 500]))\n .optional(),\n }},'''
|
||
elif isinstance(field, models.IntegerField):
|
||
return f''' {{\n component: 'InputNumber',\n fieldName: '{field_name}',\n label: '{field_label}',\n }},'''
|
||
elif isinstance(field, models.BooleanField):
|
||
return f''' {{\n component: 'RadioGroup',\n componentProps: {{\n buttonStyle: 'solid',\n options: [\n {{ label: '开启', value: 1 }},\n {{ label: '关闭', value: 0 }},\n ],\n optionType: 'button',\n }},\n defaultValue: 1,\n fieldName: '{field_name}',\n label: '{field_label}',\n }},'''
|
||
else:
|
||
return f''' {{\n component: 'Input',\n fieldName: '{field_name}',\n label: '{field_label}',\n }},'''
|
||
|
||
def generate_grid_form_field(self, field):
|
||
field_name = field.name
|
||
field_label = getattr(field, 'verbose_name', field_name)
|
||
# 查询表单一般只需要 component/fieldName/label,不需要 rules
|
||
if isinstance(field, models.CharField):
|
||
return f''' {{\n component: 'Input',\n fieldName: '{field_name}',\n label: '{field_label}',\n }},'''
|
||
elif isinstance(field, models.IntegerField):
|
||
return f''' {{\n component: 'InputNumber',\n fieldName: '{field_name}',\n label: '{field_label}',\n }},'''
|
||
# 其他类型同理
|
||
else:
|
||
return f''' {{\n component: 'Input',\n fieldName: '{field_name}',\n label: '{field_label}',\n }},''' |