添加自动创建菜单及权限脚本
This commit is contained in:
97
backend/system/management/commands/gen_menu_json.py
Normal file
97
backend/system/management/commands/gen_menu_json.py
Normal file
@@ -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)))
|
||||||
@@ -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),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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",
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -16,15 +16,17 @@ class MenuType(models.TextChoices):
|
|||||||
|
|
||||||
# 菜单元数据模型(单独存储元数据,避免 JSONField)
|
# 菜单元数据模型(单独存储元数据,避免 JSONField)
|
||||||
class MenuMeta(CoreModel):
|
class MenuMeta(CoreModel):
|
||||||
title = models.CharField(max_length=200, verbose_name='标题')
|
title = models.CharField(max_length=200, db_comment='标题')
|
||||||
icon = models.CharField(max_length=100, blank=True, verbose_name='图标')
|
icon = models.CharField(max_length=100, blank=True, db_comment='图标')
|
||||||
order = models.IntegerField(default=0, verbose_name='排序')
|
sort = models.IntegerField(default=0, db_comment='排序')
|
||||||
affix_tab = models.BooleanField(default=False, verbose_name='固定标签页')
|
affix_tab = models.BooleanField(default=False, db_comment='固定标签页')
|
||||||
badge = models.CharField(max_length=50, blank=True, verbose_name='徽章文本')
|
badge = models.CharField(max_length=50, blank=True, db_comment='徽章文本')
|
||||||
badge_type = models.CharField(max_length=20, blank=True, verbose_name='徽章类型')
|
badge_type = models.CharField(max_length=20, blank=True, db_comment='徽章类型')
|
||||||
badge_variants = models.CharField(max_length=20, blank=True, verbose_name='徽章样式')
|
badge_variants = models.CharField(max_length=20, blank=True, db_comment='徽章样式')
|
||||||
iframe_src = models.URLField(blank=True, verbose_name='内嵌页面URL')
|
iframe_src = models.URLField(blank=True, db_comment='内嵌页面URL')
|
||||||
link = models.URLField(blank=True, verbose_name='外部链接')
|
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):
|
def __str__(self):
|
||||||
return self.title
|
return self.title
|
||||||
@@ -34,6 +36,8 @@ class MenuMeta(CoreModel):
|
|||||||
verbose_name = '菜单元数据'
|
verbose_name = '菜单元数据'
|
||||||
verbose_name_plural = '菜单元数据'
|
verbose_name_plural = '菜单元数据'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class Dept(CoreModel):
|
class Dept(CoreModel):
|
||||||
pid = models.ForeignKey(
|
pid = models.ForeignKey(
|
||||||
"self",
|
"self",
|
||||||
@@ -113,7 +117,7 @@ class Menu(CoreModel):
|
|||||||
class Meta:
|
class Meta:
|
||||||
verbose_name = '菜单'
|
verbose_name = '菜单'
|
||||||
verbose_name_plural = '菜单管理'
|
verbose_name_plural = '菜单管理'
|
||||||
ordering = ['meta__order', 'id']
|
ordering = ['meta__sort', 'id']
|
||||||
|
|
||||||
class Role(CoreModel):
|
class Role(CoreModel):
|
||||||
name = models.CharField(
|
name = models.CharField(
|
||||||
|
|||||||
@@ -10,11 +10,21 @@ from utils.serializers import CustomModelSerializer
|
|||||||
|
|
||||||
|
|
||||||
class MenuMetaSerializer(serializers.ModelSerializer):
|
class MenuMetaSerializer(serializers.ModelSerializer):
|
||||||
|
|
||||||
"""菜单元数据序列化器"""
|
"""菜单元数据序列化器"""
|
||||||
|
hideChildrenInMenu = serializers.SerializerMethodField()
|
||||||
|
hideInMenu = serializers.SerializerMethodField()
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
model = MenuMeta
|
model = MenuMeta
|
||||||
fields = '__all__'
|
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):
|
class MenuSerializer(CustomModelSerializer):
|
||||||
"""菜单序列化器"""
|
"""菜单序列化器"""
|
||||||
@@ -31,7 +41,7 @@ class MenuSerializer(CustomModelSerializer):
|
|||||||
|
|
||||||
def get_children(self, obj):
|
def get_children(self, obj):
|
||||||
"""获取子菜单"""
|
"""获取子菜单"""
|
||||||
children = obj.children.all()
|
children = obj.children.all().order_by('sort')
|
||||||
if children:
|
if children:
|
||||||
return MenuSerializer(children, many=True).data
|
return MenuSerializer(children, many=True).data
|
||||||
return []
|
return []
|
||||||
@@ -56,6 +66,7 @@ class MenuSerializer(CustomModelSerializer):
|
|||||||
"""更新菜单及关联的元数据"""
|
"""更新菜单及关联的元数据"""
|
||||||
self.set_audit_user_fields(validated_data, is_create=False)
|
self.set_audit_user_fields(validated_data, is_create=False)
|
||||||
meta_data = validated_data.pop('meta', {})
|
meta_data = validated_data.pop('meta', {})
|
||||||
|
print(self.fields['meta'], "self.fields['meta']")
|
||||||
meta_serializer = self.fields['meta']
|
meta_serializer = self.fields['meta']
|
||||||
meta_serializer.update(instance.meta, meta_data)
|
meta_serializer.update(instance.meta, meta_data)
|
||||||
return super().update(instance, validated_data)
|
return super().update(instance, validated_data)
|
||||||
@@ -63,7 +74,7 @@ class MenuSerializer(CustomModelSerializer):
|
|||||||
|
|
||||||
class MenuUserSerializer(MenuSerializer):
|
class MenuUserSerializer(MenuSerializer):
|
||||||
def get_children(self, obj):
|
def get_children(self, obj):
|
||||||
children = obj.children.exclude(type='button')
|
children = obj.children.exclude(type='button').order_by('sort')
|
||||||
if children:
|
if children:
|
||||||
return MenuUserSerializer(children, many=True).data
|
return MenuUserSerializer(children, many=True).data
|
||||||
return []
|
return []
|
||||||
@@ -82,7 +93,7 @@ class MenuViewSet(CustomModelViewSet):
|
|||||||
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter]
|
||||||
filterset_fields = ['status', 'type', 'pid', 'name']
|
filterset_fields = ['status', 'type', 'pid', 'name']
|
||||||
search_fields = ['name', 'path', 'auth_code']
|
search_fields = ['name', 'path', 'auth_code']
|
||||||
ordering_fields = ['meta__order', 'create_time']
|
ordering_fields = ['meta__sort', 'create_time']
|
||||||
|
|
||||||
@action(detail=False, methods=['get'])
|
@action(detail=False, methods=['get'])
|
||||||
def tree(self, request):
|
def tree(self, request):
|
||||||
|
|||||||
@@ -395,7 +395,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
fieldName: 'meta.hideInMenu',
|
fieldName: 'meta.hide_in_menu',
|
||||||
renderComponentContent() {
|
renderComponentContent() {
|
||||||
return {
|
return {
|
||||||
default: () => $t('system.menu.hideInMenu'),
|
default: () => $t('system.menu.hideInMenu'),
|
||||||
@@ -410,7 +410,7 @@ const schema: VbenFormSchema[] = [
|
|||||||
},
|
},
|
||||||
triggerFields: ['type'],
|
triggerFields: ['type'],
|
||||||
},
|
},
|
||||||
fieldName: 'meta.hideChildrenInMenu',
|
fieldName: 'meta.hide_children_in_menu',
|
||||||
renderComponentContent() {
|
renderComponentContent() {
|
||||||
return {
|
return {
|
||||||
default: () => $t('system.menu.hideChildrenInMenu'),
|
default: () => $t('system.menu.hideChildrenInMenu'),
|
||||||
|
|||||||
Reference in New Issue
Block a user