diff --git a/backend/system/management/commands/README.md b/backend/system/management/commands/README.md new file mode 100644 index 0000000..ddaec2b --- /dev/null +++ b/backend/system/management/commands/README.md @@ -0,0 +1,82 @@ +# CRUD 代码生成器 + +这是一个 Django 管理命令,用于根据模型自动生成完整的 CRUD 代码,包括后端 API 和前端页面。 + +## 使用方法 + +### 基本用法 + +```bash +# 只生成后端代码 +python manage.py generate_crud + +# 同时生成前端和后端代码 +python manage.py generate_crud --frontend +``` + +### 示例 + +```bash +# 为 system 应用的 Dept 模型生成 CRUD 代码 +python manage.py generate_crud system Dept --frontend +``` + +## 生成的文件 + +### 后端文件 + +1. **序列化器**: `backend/{app_name}/serializers.py` + - 继承 `CustomModelSerializer` + - 自动处理 creator 和 modifier 字段 + +2. **视图集**: `backend/{app_name}/views/{model_name.lower()}.py` + - 继承 `CustomModelViewSet` + - 包含基本的 CRUD 操作 + - 自动配置过滤、搜索、排序字段 + +### 前端文件(使用 --frontend 参数时) + +1. **模型定义**: `web/apps/web-antd/src/models/{app_name.lower()}.ts` + - TypeScript 接口定义 + - 继承 BaseModel + +2. **API 接口**: `web/apps/web-antd/src/api/{app_name.lower()}/{model_name.lower()}.ts` + - 完整的 CRUD API 函数 + - TypeScript 类型定义 + +3. **列表页面**: `web/apps/web-antd/src/views/{app_name.lower()}/{model_name.lower()}/list.vue` + - 使用 VxeTable 的列表页面 + - 包含增删改查操作 + +4. **表单配置**: `web/apps/web-antd/src/views/{app_name.lower()}/{model_name.lower()}/data.ts` + - 表单字段配置 + - 表格列配置 + +5. **表单组件**: `web/apps/web-antd/src/views/{app_name.lower()}/{model_name.lower()}/modules/form.vue` + - 可复用的表单组件 + - 支持创建和编辑 + +## 特性 + +- **自动字段映射**: 根据 Django 模型字段自动生成对应的前端表单字段 +- **类型安全**: 自动生成 TypeScript 类型定义 +- **审计字段**: 自动处理 creator、modifier 等审计字段 +- **软删除**: 自动过滤已删除的记录 +- **分页支持**: 自动配置分页功能 +- **搜索过滤**: 自动配置搜索和过滤字段 + +## 注意事项 + +1. 确保模型继承自 `CoreModel` 以获得审计字段支持 +2. 前端代码需要根据实际需求进行调整 +3. 路由配置需要手动添加到 Django 的 URL 配置中 +4. 前端路由需要手动配置 + +## 自定义 + +你可以根据需要修改生成器代码来: + +- 调整生成的代码模板 +- 添加更多字段类型支持 +- 自定义表单验证规则 +- 添加更多前端组件支持 \ No newline at end of file diff --git a/backend/system/management/commands/generate_crud.py b/backend/system/management/commands/generate_crud.py new file mode 100644 index 0000000..e82bd6d --- /dev/null +++ b/backend/system/management/commands/generate_crud.py @@ -0,0 +1,250 @@ +#!/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}', + }},''' \ No newline at end of file diff --git a/backend/system/management/commands/tpl/frontend_api.ts.tpl b/backend/system/management/commands/tpl/frontend_api.ts.tpl new file mode 100644 index 0000000..3b94d24 --- /dev/null +++ b/backend/system/management/commands/tpl/frontend_api.ts.tpl @@ -0,0 +1,76 @@ +import { request } from '#/utils/request'; + +export namespace ${model_name}Api { + export interface ${model_name} { + id: number; + name: string; + create_time: string; + update_time: string; + // 根据实际字段添加 + } + + export interface ${model_name}ListParams { + page?: number; + pageSize?: number; + name?: string; + // 根据实际字段添加 + } + + export interface ${model_name}ListResult { + total: number; + items: ${model_name}[]; + } +} + +/** + * 获取${verbose_name}列表 + */ +export function get${model_name}List(params: ${model_name}Api.${model_name}ListParams) { + return request<${model_name}Api.${model_name}ListResult>({ + url: '/$app_name/${model_name_lower}/', + method: 'GET', + params, + }); +} + +/** + * 获取${verbose_name}详情 + */ +export function get${model_name}Detail(id: number) { + return request<${model_name}Api.${model_name}>({ + url: `/$app_name/${model_name_lower}/${id}/`, + method: 'GET', + }); +} + +/** + * 创建${verbose_name} + */ +export function create${model_name}(data: Partial<${model_name}Api.${model_name}>) { + return request<${model_name}Api.${model_name}>({ + url: '/$app_name/${model_name_lower}/', + method: 'POST', + data, + }); +} + +/** + * 更新${verbose_name} + */ +export function update${model_name}(id: number, data: Partial<${model_name}Api.${model_name}>) { + return request<${model_name}Api.${model_name}>({ + url: `/$app_name/${model_name_lower}/${id}/`, + method: 'PUT', + data, + }); +} + +/** + * 删除${verbose_name} + */ +export function delete${model_name}(id: number) { + return request({ + url: `/$app_name/${model_name_lower}/${id}/`, + method: 'DELETE', + }); +} \ No newline at end of file diff --git a/backend/system/management/commands/tpl/frontend_data.ts.tpl b/backend/system/management/commands/tpl/frontend_data.ts.tpl new file mode 100644 index 0000000..75049c8 --- /dev/null +++ b/backend/system/management/commands/tpl/frontend_data.ts.tpl @@ -0,0 +1,78 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn } from '#/adapter/vxe-table'; +import type { ${app_name_camel}${model_name}Api } from '#/models/$app_name/${model_name_snake}'; + +import { z } from '#/adapter/form'; +import { $$t } from '#/locales'; +import { format_datetime } from '#/utils/date'; + +/** + * 获取编辑表单的字段配置 + */ +export function useSchema(): VbenFormSchema[] { + return [ +$form_fields + ]; +} + +export function useColumns( + onActionClick?: OnActionClickFn<${app_name_camel}${model_name}Api.${app_name_camel}${model_name}>, +): VxeTableGridOptions<${app_name_camel}${model_name}Api.${app_name_camel}${model_name}>['columns'] { + return [ + { + align: 'left', + field: 'id', + }, + { + cellRender: { + name: 'CellTag', + options: [ + { label: $$t('common.enabled'), value: true }, + { label: $$t('common.disabled'), value: false }, + ], + }, + field: 'status', + title: '状态', + width: 100, + }, + { + field: 'remark', + title: '备注', + }, + { + field: 'create_time', + title: '创建时间', + width: 180, + formatter: ({ cellValue }) => format_datetime(cellValue), + }, + { + align: 'right', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: $$t('${app_name}.{model_name_snake}.name'), + onClick: onActionClick, + }, + name: 'CellOperation', + options: [ + 'edit', // 默认的编辑按钮 + { + code: 'view', // 新增查看详情按钮(可自定义code) + text: '数据', // 按钮文本(国际化) + }, + { + code: 'delete', // 默认的删除按钮 + }, + ], + }, + field: 'operation', + fixed: 'right', + headerAlign: 'center', + showOverflow: false, + title: '操作', + width: 200, + }, + ]; +} diff --git a/backend/system/management/commands/tpl/frontend_form.vue.tpl b/backend/system/management/commands/tpl/frontend_form.vue.tpl new file mode 100644 index 0000000..81d2d63 --- /dev/null +++ b/backend/system/management/commands/tpl/frontend_form.vue.tpl @@ -0,0 +1,79 @@ + + + + diff --git a/backend/system/management/commands/tpl/frontend_list.vue.tpl b/backend/system/management/commands/tpl/frontend_list.vue.tpl new file mode 100644 index 0000000..1ed07f4 --- /dev/null +++ b/backend/system/management/commands/tpl/frontend_list.vue.tpl @@ -0,0 +1,132 @@ + + + diff --git a/backend/system/management/commands/tpl/frontend_model.ts.tpl b/backend/system/management/commands/tpl/frontend_model.ts.tpl new file mode 100644 index 0000000..d60bf28 --- /dev/null +++ b/backend/system/management/commands/tpl/frontend_model.ts.tpl @@ -0,0 +1,13 @@ +import { BaseModel } from '#/models/base'; + +export namespace ${app_name_camel}${model_name}Api { + export interface ${app_name_camel}${model_name} { +$interface_fields + } +} + +export class ${app_name_camel}${model_name}Model extends BaseModel<${app_name_camel}${model_name}Api.${app_name_camel}${model_name}> { + constructor() { + super('/$app_name/${model_name_lower}/'); + } +} diff --git a/backend/system/management/commands/tpl/viewset.py.tpl b/backend/system/management/commands/tpl/viewset.py.tpl new file mode 100644 index 0000000..6c6b71a --- /dev/null +++ b/backend/system/management/commands/tpl/viewset.py.tpl @@ -0,0 +1,27 @@ +from $app_name.models import $model_name +from utils.serializers import CustomModelSerializer +from utils.custom_model_viewSet import CustomModelViewSet + +class ${model_name}Serializer(CustomModelSerializer): + """ + $verbose_name 序列化器 + """ + class Meta: + model = $model_name + fields = '__all__' + read_only_fields = ['id', 'create_time', 'update_time'] + + +class ${model_name}ViewSet(CustomModelViewSet): + """ + $verbose_name 视图集 + """ + queryset = $model_name.objects.filter(is_deleted=False).order_by('-id') + serializer_class = ${model_name}Serializer + filterset_fields = [$filterset_fields] + search_fields = ['name'] # 根据实际字段调整 + ordering_fields = ['create_time', 'id'] + ordering = ['-create_time'] + +# 移入urls中 +# router.register(r'${model_name_snake}', views.${model_name}ViewSet) diff --git a/backend/system/migrations/0002_post_alter_dept_creator_alter_dept_is_deleted_and_more.py b/backend/system/migrations/0002_post_alter_dept_creator_alter_dept_is_deleted_and_more.py new file mode 100644 index 0000000..33cfb34 --- /dev/null +++ b/backend/system/migrations/0002_post_alter_dept_creator_alter_dept_is_deleted_and_more.py @@ -0,0 +1,497 @@ +# Generated by Django 5.2.1 on 2025-07-01 03:55 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("system", "0001_initial"), + ] + + operations = [ + migrations.CreateModel( + name="Post", + fields=[ + ( + "id", + models.BigAutoField( + auto_created=True, + primary_key=True, + serialize=False, + verbose_name="ID", + ), + ), + ( + "remark", + models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + ( + "creator", + models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + ( + "modifier", + models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + ( + "update_time", + models.DateTimeField( + auto_now=True, + db_comment="修改时间", + help_text="修改时间", + null=True, + ), + ), + ( + "create_time", + models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + ( + "is_deleted", + models.BooleanField(db_comment="是否软删除", default=False), + ), + ("code", models.CharField(db_comment="岗位编码", max_length=64)), + ("name", models.CharField(db_comment="岗位名称", max_length=50)), + ("sort", models.IntegerField(db_comment="显示顺序", default=0)), + ("status", models.BooleanField(db_comment="状态", default=False)), + ], + options={ + "verbose_name": "岗位信息表", + "verbose_name_plural": "岗位信息表", + "db_table": "system_post", + }, + ), + migrations.AlterField( + model_name="dept", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dept", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="dept", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dept", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="dictdata", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="dictdata", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dictdata", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="dictdata", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dictdata", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="dictdata", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="dicttype", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="dicttype", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dicttype", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="dicttype", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="dicttype", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="dicttype", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="menu", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="menu", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="menu", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="menu", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="menu", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="menu", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="menumeta", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="menumeta", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="menumeta", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="menumeta", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="menumeta", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="menumeta", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="role", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="role", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="role", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="role", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="role", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="rolepermission", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="rolepermission", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="rolepermission", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="rolepermission", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="rolepermission", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AlterField( + model_name="user", + name="create_time", + field=models.DateTimeField( + auto_now_add=True, + db_comment="创建时间", + help_text="创建时间", + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="creator", + field=models.CharField( + blank=True, + db_comment="创建人", + help_text="创建人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="is_deleted", + field=models.BooleanField(db_comment="是否软删除", default=False), + ), + migrations.AlterField( + model_name="user", + name="modifier", + field=models.CharField( + blank=True, + db_comment="修改人", + help_text="修改人", + max_length=64, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="remark", + field=models.CharField( + blank=True, + db_comment="备注", + help_text="备注", + max_length=256, + null=True, + ), + ), + migrations.AlterField( + model_name="user", + name="update_time", + field=models.DateTimeField( + auto_now=True, db_comment="修改时间", help_text="修改时间", null=True + ), + ), + migrations.AddField( + model_name="user", + name="post", + field=models.ManyToManyField( + blank=True, + db_constraint=False, + related_name="users", + to="system.post", + verbose_name="岗位", + ), + ), + ] diff --git a/backend/system/models.py b/backend/system/models.py index e98a07b..c75d4bb 100644 --- a/backend/system/models.py +++ b/backend/system/models.py @@ -224,6 +224,21 @@ class DictData(CoreModel): return self.label +class Post(CoreModel): + code = models.CharField(max_length=64, db_comment='岗位编码') + name = models.CharField(max_length=50, db_comment='岗位名称') + sort = models.IntegerField(default=0, db_comment='显示顺序') + status = models.BooleanField(default=False, db_comment='状态') + + class Meta: + db_table = 'system_post' + verbose_name = '岗位信息表' + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + class User(AbstractUser, CoreModel): mobile = models.CharField(max_length=11, null=True, validators=[validate_mobile], db_comment="手机号") nickname = models.CharField(max_length=50, blank=True, null=True, db_comment="昵称") @@ -238,6 +253,10 @@ class User(AbstractUser, CoreModel): 'Dept', blank=True, verbose_name='部门', db_constraint=False, related_name='users' ) + post = models.ManyToManyField( + 'Post', blank=True, verbose_name='岗位', db_constraint=False, + related_name='users' + ) status = models.BooleanField(default=False, verbose_name='<帐号状态>(1正常 0停用)', db_comment="帐号状态") login_date = models.DateTimeField("<最后登录时间>", blank=True, null=True, db_comment="最后登录时间") login_ip = models.GenericIPAddressField(blank=True, null=True, db_comment="最后登录IP") diff --git a/backend/system/urls.py b/backend/system/urls.py index 7ae2f3c..5cc3427 100644 --- a/backend/system/urls.py +++ b/backend/system/urls.py @@ -10,6 +10,7 @@ router.register(r'menu', views.MenuViewSet) router.register(r'role', views.RoleViewSet) router.register(r'dict_data', views.DictDataViewSet) router.register(r'dict_type', views.DictTypeViewSet) +router.register(r'post', views.PostViewSet) urlpatterns = [ path('', include(router.urls)), diff --git a/backend/system/views/__init__.py b/backend/system/views/__init__.py index f079feb..28b807b 100644 --- a/backend/system/views/__init__.py +++ b/backend/system/views/__init__.py @@ -5,6 +5,7 @@ __all__ = [ 'RoleViewSet', 'DictDataViewSet', 'DictTypeViewSet', + 'PostViewSet', ] from system.views.dict_data import DictDataViewSet @@ -13,4 +14,5 @@ from system.views.menu import MenuViewSet, MenuMetaViewSet from system.views.role import RoleViewSet from system.views.dept import DeptViewSet +from system.views.post import PostViewSet from system.views.user import * \ No newline at end of file diff --git a/backend/system/views/post.py b/backend/system/views/post.py new file mode 100644 index 0000000..b356e50 --- /dev/null +++ b/backend/system/views/post.py @@ -0,0 +1,24 @@ +from system.models import Post +from utils.serializers import CustomModelSerializer +from utils.custom_model_viewSet import CustomModelViewSet + +class PostSerializer(CustomModelSerializer): + """ + 岗位信息表 序列化器 + """ + class Meta: + model = Post + fields = '__all__' + read_only_fields = ['id', 'create_time', 'update_time'] + + +class PostViewSet(CustomModelViewSet): + """ + 岗位信息表 视图集 + """ + queryset = Post.objects.filter(is_deleted=False).order_by('-id') + serializer_class = PostSerializer + filterset_fields = ['id', 'remark', 'creator', 'modifier', 'is_deleted', 'code', 'name', 'sort', 'status'] + search_fields = ['name'] # 根据实际字段调整 + ordering_fields = ['create_time', 'id'] + ordering = ['-create_time'] diff --git a/backend/utils/models.py b/backend/utils/models.py index b7c01aa..053caa7 100644 --- a/backend/utils/models.py +++ b/backend/utils/models.py @@ -6,13 +6,13 @@ from django.db import models class CoreModel(models.Model): - remark = models.CharField(max_length=256, verbose_name="备注", null=True, blank=True, help_text="备注") - creator = models.CharField(max_length=64, null=True, blank=True, help_text="创建人", verbose_name="创建人") - modifier = models.CharField(max_length=64, null=True, blank=True, help_text="修改人", verbose_name="修改人") - update_time = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", verbose_name="修改时间") + remark = models.CharField(max_length=256, db_comment="备注", null=True, blank=True, help_text="备注") + creator = models.CharField(max_length=64, null=True, blank=True, help_text="创建人", db_comment="创建人") + modifier = models.CharField(max_length=64, null=True, blank=True, help_text="修改人", db_comment="修改人") + update_time = models.DateTimeField(auto_now=True, null=True, blank=True, help_text="修改时间", db_comment="修改时间") create_time = models.DateTimeField(auto_now_add=True, null=True, blank=True, help_text="创建时间", - verbose_name="创建时间") - is_deleted = models.BooleanField(default=False, verbose_name='是否软删除') + db_comment="创建时间") + is_deleted = models.BooleanField(default=False, db_comment='是否软删除') class Meta: abstract = True diff --git a/web/apps/web-antd/src/locales/langs/en-US/system.json b/web/apps/web-antd/src/locales/langs/en-US/system.json index 8329c37..46ba602 100644 --- a/web/apps/web-antd/src/locales/langs/en-US/system.json +++ b/web/apps/web-antd/src/locales/langs/en-US/system.json @@ -90,6 +90,10 @@ "title": "Dictionary Data", "type": "Dictionary Value" }, + "post": { + "name": "Post", + "title": "Post Management" + }, "status": "Status", "remark": "Remarks", "createTime": "Created At", diff --git a/web/apps/web-antd/src/locales/langs/zh-CN/system.json b/web/apps/web-antd/src/locales/langs/zh-CN/system.json index 2dd4ba8..8af02c9 100644 --- a/web/apps/web-antd/src/locales/langs/zh-CN/system.json +++ b/web/apps/web-antd/src/locales/langs/zh-CN/system.json @@ -91,6 +91,10 @@ "title": "字典数据", "type": "字典键值" }, + "post": { + "name": "岗位", + "title": "岗位管理" + }, "status": "状态", "remark": "备注", "createTime": "创建时间", diff --git a/web/apps/web-antd/src/models/index.ts b/web/apps/web-antd/src/models/index.ts new file mode 100644 index 0000000..8a185aa --- /dev/null +++ b/web/apps/web-antd/src/models/index.ts @@ -0,0 +1 @@ +export * from './base'; diff --git a/web/apps/web-antd/src/models/system.ts b/web/apps/web-antd/src/models/system.ts deleted file mode 100644 index 056c34c..0000000 --- a/web/apps/web-antd/src/models/system.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { BaseModel } from '#/models/base'; - -export namespace SystemDictTypeApi { - export interface SystemDictType { - [key: string]: any; - id: string; - name: string; - type: string; - } -} - -export class SystemDictTypeModel extends BaseModel { - constructor() { - super('/system/dict_type/'); - } -} diff --git a/web/apps/web-antd/src/models/system/post.ts b/web/apps/web-antd/src/models/system/post.ts new file mode 100644 index 0000000..b22fc08 --- /dev/null +++ b/web/apps/web-antd/src/models/system/post.ts @@ -0,0 +1,23 @@ +import { BaseModel } from '#/models/base'; + +export namespace SystemPostApi { + export interface SystemPost { + id: number; + remark: string; + creator: string; + modifier: string; + update_time: string; + create_time: string; + is_deleted: boolean; + code: string; + name: string; + sort: number; + status: number; + } +} + +export class SystemPostModel extends BaseModel { + constructor() { + super('/system/post/'); + } +} diff --git a/web/apps/web-antd/src/router/routes/modules/system.ts b/web/apps/web-antd/src/router/routes/modules/system.ts index 0f9bcc4..8266256 100644 --- a/web/apps/web-antd/src/router/routes/modules/system.ts +++ b/web/apps/web-antd/src/router/routes/modules/system.ts @@ -39,6 +39,15 @@ const routes: RouteRecordRaw[] = [ }, component: () => import('#/views/system/dept/list.vue'), }, + { + path: '/system/post', + name: 'SystemPost', + meta: { + icon: 'charm:organisation', + title: $t('system.post.title'), + }, + component: () => import('#/views/system/post/list.vue'), + }, { path: '/system/dict_type', name: 'SystemDictType', diff --git a/web/apps/web-antd/src/views/system/dict_type/data.ts b/web/apps/web-antd/src/views/system/dict_type/data.ts index 2788cd6..0ccc9fd 100644 --- a/web/apps/web-antd/src/views/system/dict_type/data.ts +++ b/web/apps/web-antd/src/views/system/dict_type/data.ts @@ -6,7 +6,7 @@ import type { SystemDictTypeApi } from '#/api/system/dict_type'; import { z } from '#/adapter/form'; import { $t } from '#/locales'; -import {format_datetime} from "#/utils/date"; +import { format_datetime } from '#/utils/date'; /** * 获取编辑表单的字段配置。如果没有使用多语言,可以直接export一个数组常量 diff --git a/web/apps/web-antd/src/views/system/post/data.ts b/web/apps/web-antd/src/views/system/post/data.ts new file mode 100644 index 0000000..f974011 --- /dev/null +++ b/web/apps/web-antd/src/views/system/post/data.ts @@ -0,0 +1,124 @@ +import type { VxeTableGridOptions } from '@vben/plugins/vxe-table'; + +import type { VbenFormSchema } from '#/adapter/form'; +import type { OnActionClickFn } from '#/adapter/vxe-table'; +import type { SystemPostApi } from '#/models/system/post'; + +import { z } from '#/adapter/form'; +import { $t } from '#/locales'; +import { format_datetime } from '#/utils/date'; + +/** + * 获取编辑表单的字段配置 + */ +export function useSchema(): VbenFormSchema[] { + return [ + { + component: 'Input', + fieldName: 'name', + label: '名称', + rules: z + .string() + .min(1, $t('ui.formRules.required', ['名称'])) + .max(100, $t('ui.formRules.maxLength', ['名称', 100])), + }, + { + component: 'Input', + fieldName: 'code', + label: '编码', + rules: z + .string() + .min(1, $t('ui.formRules.required', ['编码'])) + .max(100, $t('ui.formRules.maxLength', ['编码', 100])), + }, + { + component: 'InputNumber', + fieldName: 'sort', + label: '排序', + rules: z.number(), + }, + { + component: 'RadioGroup', + componentProps: { + buttonStyle: 'solid', + options: [ + { label: '开启', value: true }, + { label: '关闭', value: false }, + ], + optionType: 'button', + }, + defaultValue: true, + fieldName: 'status', + label: '是否启用', + }, + { + component: 'Input', + fieldName: 'remark', + label: '备注', + }, + ]; +} + +export function useColumns( + onActionClick?: OnActionClickFn, +): VxeTableGridOptions['columns'] { + return [ + { + field: 'id', + title: 'id', + }, + { + field: 'name', + title: '岗位名称', + }, + { + field: 'code', + title: '岗位编码', + }, + { + cellRender: { + name: 'CellTag', + options: [ + { label: $t('common.enabled'), value: true }, + { label: $t('common.disabled'), value: false }, + ], + }, + field: 'status', + title: '状态', + width: 100, + }, + { + field: 'remark', + title: '备注', + }, + { + field: 'create_time', + title: '创建时间', + width: 180, + formatter: ({ cellValue }) => format_datetime(cellValue), + }, + { + align: 'right', + cellRender: { + attrs: { + nameField: 'name', + nameTitle: $t('system.{model_name_snake}.name'), + onClick: onActionClick, + }, + name: 'CellOperation', + options: [ + 'edit', // 默认的编辑按钮 + { + code: 'delete', // 默认的删除按钮 + }, + ], + }, + field: 'operation', + fixed: 'right', + headerAlign: 'center', + showOverflow: false, + title: '操作', + width: 200, + }, + ]; +} diff --git a/web/apps/web-antd/src/views/system/post/list.vue b/web/apps/web-antd/src/views/system/post/list.vue new file mode 100644 index 0000000..68699af --- /dev/null +++ b/web/apps/web-antd/src/views/system/post/list.vue @@ -0,0 +1,132 @@ + + + diff --git a/web/apps/web-antd/src/views/system/post/modules/form.vue b/web/apps/web-antd/src/views/system/post/modules/form.vue new file mode 100644 index 0000000..7f347fd --- /dev/null +++ b/web/apps/web-antd/src/views/system/post/modules/form.vue @@ -0,0 +1,79 @@ + + + +