diff --git a/backend/system/management/commands/gen_menu_json.py b/backend/system/management/commands/gen_menu_json.py new file mode 100644 index 0000000..fbbb4aa --- /dev/null +++ b/backend/system/management/commands/gen_menu_json.py @@ -0,0 +1,97 @@ +import json +from datetime import datetime +from django.core.management.base import BaseCommand +from system.models import Menu, MenuMeta +import re + +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 gen_menu(app_name, model_name, parent_menu_name, creator='admin'): + print(parent_menu_name, 'parent') + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + now_iso = datetime.now().isoformat() + model_lower = camel_to_snake(model_name) + model_title = model_name.capitalize() + + # 查找父菜单对象 + parent_menu = Menu.objects.filter(name=parent_menu_name).first() + parent_id = parent_menu.id if parent_menu else None + + # 创建主菜单的元数据 + meta = MenuMeta.objects.create( + title=f"{app_name}.{model_lower}.title", + icon="", + order=0, + affix_tab=False, + badge="", + badge_type="", + badge_variants="", + iframe_src="", + link="" + ) + + # 创建主菜单 + page_menu_obj = Menu.objects.create( + pid=parent_menu, + name=model_title, + status=1, + type="menu", + sort=50, + path=f"/{app_name}/{model_lower}", + component=f"/{app_name}/{model_lower}/list", + auth_code="", + meta=meta + ) + + # 按钮权限 + buttons = [ + {"name": "Query", "title": "common.query", "auth_code": f"{app_name}:{model_lower}:query"}, + {"name": "Create", "title": "common.create", "auth_code": f"{app_name}:{model_lower}:create"}, + {"name": "Edit", "title": "common.edit", "auth_code": f"{app_name}:{model_lower}:edit"}, + {"name": "Delete", "title": "common.delete", "auth_code": f"{app_name}:{model_lower}:delete"}, + ] + for idx, btn in enumerate(buttons): + btn_meta = MenuMeta.objects.create( + title=btn["title"], + icon="", + order=0, + affix_tab=False, + badge="", + badge_type="", + badge_variants="", + iframe_src="", + link="" + ) + Menu.objects.create( + pid=page_menu_obj, + name=f"{model_title}{btn['name']}", + status=1, + type="button", + sort=idx, + path="", + component="", + auth_code=btn["auth_code"], + meta=btn_meta + ) + + return page_menu_obj + +class Command(BaseCommand): + help = '自动生成菜单和按钮权限结构,并写入Menu模型' + + def add_arguments(self, parser): + parser.add_argument('--app', required=True, help='app名称') + parser.add_argument('--model', required=True, help='model名称') + parser.add_argument('--parent', required=True, help='上级菜单名称') + + def handle(self, *args, **options): + app = options['app'] + model = options['model'] + parent = options['parent'] + try: + menu = gen_menu(app, model, parent) + self.stdout.write(self.style.SUCCESS(f"菜单 {menu.name} 及其按钮权限已写入数据库 (id={menu.id})")) + except Exception as e: + self.stderr.write(self.style.ERROR(str(e))) \ No newline at end of file diff --git a/backend/system/migrations/0006_menumeta_hide_children_in_menu_menumeta_hide_in_menu_and_more.py b/backend/system/migrations/0006_menumeta_hide_children_in_menu_menumeta_hide_in_menu_and_more.py new file mode 100644 index 0000000..98a7ed8 --- /dev/null +++ b/backend/system/migrations/0006_menumeta_hide_children_in_menu_menumeta_hide_in_menu_and_more.py @@ -0,0 +1,68 @@ +# Generated by Django 5.2.1 on 2025-07-02 04:22 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("system", "0005_remove_user_login_date_menu_sort"), + ] + + operations = [ + migrations.AddField( + model_name="menumeta", + name="hide_children_in_menu", + field=models.BooleanField(db_comment="隐藏子菜单", default=False), + ), + migrations.AddField( + model_name="menumeta", + name="hide_in_menu", + field=models.BooleanField(db_comment="隐藏菜单", default=False), + ), + migrations.AlterField( + model_name="menumeta", + name="affix_tab", + field=models.BooleanField(db_comment="固定标签页", default=False), + ), + migrations.AlterField( + model_name="menumeta", + name="badge", + field=models.CharField(blank=True, db_comment="徽章文本", max_length=50), + ), + migrations.AlterField( + model_name="menumeta", + name="badge_type", + field=models.CharField(blank=True, db_comment="徽章类型", max_length=20), + ), + migrations.AlterField( + model_name="menumeta", + name="badge_variants", + field=models.CharField(blank=True, db_comment="徽章样式", max_length=20), + ), + migrations.AlterField( + model_name="menumeta", + name="icon", + field=models.CharField(blank=True, db_comment="图标", max_length=100), + ), + migrations.AlterField( + model_name="menumeta", + name="iframe_src", + field=models.URLField(blank=True, db_comment="内嵌页面URL"), + ), + migrations.AlterField( + model_name="menumeta", + name="link", + field=models.URLField(blank=True, db_comment="外部链接"), + ), + migrations.AlterField( + model_name="menumeta", + name="order", + field=models.IntegerField(db_comment="排序", default=0), + ), + migrations.AlterField( + model_name="menumeta", + name="title", + field=models.CharField(db_comment="标题", max_length=200), + ), + ] diff --git a/backend/system/migrations/0007_alter_menu_options_rename_order_menumeta_sort.py b/backend/system/migrations/0007_alter_menu_options_rename_order_menumeta_sort.py new file mode 100644 index 0000000..28c22ca --- /dev/null +++ b/backend/system/migrations/0007_alter_menu_options_rename_order_menumeta_sort.py @@ -0,0 +1,29 @@ +# Generated by Django 5.2.1 on 2025-07-02 04:26 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ( + "system", + "0006_menumeta_hide_children_in_menu_menumeta_hide_in_menu_and_more", + ), + ] + + operations = [ + migrations.AlterModelOptions( + name="menu", + options={ + "ordering": ["meta__sort", "id"], + "verbose_name": "菜单", + "verbose_name_plural": "菜单管理", + }, + ), + migrations.RenameField( + model_name="menumeta", + old_name="order", + new_name="sort", + ), + ] diff --git a/backend/system/models.py b/backend/system/models.py index 5000388..ff63ea1 100644 --- a/backend/system/models.py +++ b/backend/system/models.py @@ -16,15 +16,17 @@ class MenuType(models.TextChoices): # 菜单元数据模型(单独存储元数据,避免 JSONField) class MenuMeta(CoreModel): - title = models.CharField(max_length=200, verbose_name='标题') - icon = models.CharField(max_length=100, blank=True, verbose_name='图标') - order = models.IntegerField(default=0, verbose_name='排序') - affix_tab = models.BooleanField(default=False, verbose_name='固定标签页') - badge = models.CharField(max_length=50, blank=True, verbose_name='徽章文本') - badge_type = models.CharField(max_length=20, blank=True, verbose_name='徽章类型') - badge_variants = models.CharField(max_length=20, blank=True, verbose_name='徽章样式') - iframe_src = models.URLField(blank=True, verbose_name='内嵌页面URL') - link = models.URLField(blank=True, verbose_name='外部链接') + title = models.CharField(max_length=200, db_comment='标题') + icon = models.CharField(max_length=100, blank=True, db_comment='图标') + sort = models.IntegerField(default=0, db_comment='排序') + affix_tab = models.BooleanField(default=False, db_comment='固定标签页') + badge = models.CharField(max_length=50, blank=True, db_comment='徽章文本') + badge_type = models.CharField(max_length=20, blank=True, db_comment='徽章类型') + badge_variants = models.CharField(max_length=20, blank=True, db_comment='徽章样式') + iframe_src = models.URLField(blank=True, db_comment='内嵌页面URL') + link = models.URLField(blank=True, db_comment='外部链接') + hide_in_menu = models.BooleanField(default=False, db_comment='隐藏菜单') + hide_children_in_menu = models.BooleanField(default=False, db_comment='隐藏子菜单') def __str__(self): return self.title @@ -34,6 +36,8 @@ class MenuMeta(CoreModel): verbose_name = '菜单元数据' verbose_name_plural = '菜单元数据' + + class Dept(CoreModel): pid = models.ForeignKey( "self", @@ -113,7 +117,7 @@ class Menu(CoreModel): class Meta: verbose_name = '菜单' verbose_name_plural = '菜单管理' - ordering = ['meta__order', 'id'] + ordering = ['meta__sort', 'id'] class Role(CoreModel): name = models.CharField( diff --git a/backend/system/views/menu.py b/backend/system/views/menu.py index 8cea4aa..d49b772 100644 --- a/backend/system/views/menu.py +++ b/backend/system/views/menu.py @@ -10,11 +10,21 @@ from utils.serializers import CustomModelSerializer class MenuMetaSerializer(serializers.ModelSerializer): + """菜单元数据序列化器""" + hideChildrenInMenu = serializers.SerializerMethodField() + hideInMenu = serializers.SerializerMethodField() + class Meta: model = MenuMeta fields = '__all__' + def get_hideChildrenInMenu(self, obj): + return getattr(obj, 'hide_children_in_menu', None) + + def get_hideInMenu(self, obj): + return getattr(obj, 'hide_in_menu', None) + class MenuSerializer(CustomModelSerializer): """菜单序列化器""" @@ -31,7 +41,7 @@ class MenuSerializer(CustomModelSerializer): def get_children(self, obj): """获取子菜单""" - children = obj.children.all() + children = obj.children.all().order_by('sort') if children: return MenuSerializer(children, many=True).data return [] @@ -56,6 +66,7 @@ class MenuSerializer(CustomModelSerializer): """更新菜单及关联的元数据""" self.set_audit_user_fields(validated_data, is_create=False) meta_data = validated_data.pop('meta', {}) + print(self.fields['meta'], "self.fields['meta']") meta_serializer = self.fields['meta'] meta_serializer.update(instance.meta, meta_data) return super().update(instance, validated_data) @@ -63,7 +74,7 @@ class MenuSerializer(CustomModelSerializer): class MenuUserSerializer(MenuSerializer): def get_children(self, obj): - children = obj.children.exclude(type='button') + children = obj.children.exclude(type='button').order_by('sort') if children: return MenuUserSerializer(children, many=True).data return [] @@ -82,7 +93,7 @@ class MenuViewSet(CustomModelViewSet): filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['status', 'type', 'pid', 'name'] search_fields = ['name', 'path', 'auth_code'] - ordering_fields = ['meta__order', 'create_time'] + ordering_fields = ['meta__sort', 'create_time'] @action(detail=False, methods=['get']) def tree(self, request): diff --git a/web/apps/web-antd/src/views/system/menu/modules/form.vue b/web/apps/web-antd/src/views/system/menu/modules/form.vue index 250a6f3..6964958 100644 --- a/web/apps/web-antd/src/views/system/menu/modules/form.vue +++ b/web/apps/web-antd/src/views/system/menu/modules/form.vue @@ -395,7 +395,7 @@ const schema: VbenFormSchema[] = [ }, triggerFields: ['type'], }, - fieldName: 'meta.hideInMenu', + fieldName: 'meta.hide_in_menu', renderComponentContent() { return { default: () => $t('system.menu.hideInMenu'), @@ -410,7 +410,7 @@ const schema: VbenFormSchema[] = [ }, triggerFields: ['type'], }, - fieldName: 'meta.hideChildrenInMenu', + fieldName: 'meta.hide_children_in_menu', renderComponentContent() { return { default: () => $t('system.menu.hideChildrenInMenu'),