#!/usr/bin/env python # -*- coding: utf-8 -*- """ 自动生成 CRUD 代码的 Django 管理命令 使用方法: python manage.py generate_crud 例如: 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 = 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