#!/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): form_fields = [] for field in model._meta.fields: if field.name in ['id', 'create_time', 'update_time', 'creator', 'modifier']: continue field_config = self.generate_form_field(field) if field_config: form_fields.append(field_config) context = get_context(app_name, model_name, model, model_name_snake) context['form_fields'] = '\n'.join(form_fields) 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''' {{ 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: true }}, {{ label: '关闭', value: false }}, ], optionType: 'button', }}, defaultValue: true, fieldName: '{field_name}', label: '{field_label}', }},''' else: return f''' {{ component: 'Input', fieldName: '{field_name}', label: '{field_label}', }},'''