mirror of
https://github.com/huge-dream/django-vue3-admin.git
synced 2026-10-13 04:39:33 +00:00
- batch_create: check component_name exists, dedupe by value, use bulk_create - Add batchDelete/selectAll/selectNone buttons to Fast-CRUD actionbar - Add visible batch toolbar in MenuButtonCom with row selection count - Fix successNotification → ElMessage.success (was treating text as i18n key) - Update docs to reflect implementation status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
325 lines
13 KiB
Python
325 lines
13 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
"""
|
|
@author: 猿小天
|
|
@contact: QQ:1638245306
|
|
@Created on: 2021/6/3 003 0:30
|
|
@Remark: 菜单按钮管理
|
|
"""
|
|
from django.db.models import F
|
|
from rest_framework.decorators import action
|
|
from rest_framework.permissions import IsAuthenticated
|
|
|
|
from dvadmin.system.models import MenuButton, RoleMenuButtonPermission, Menu
|
|
from dvadmin.utils.json_response import DetailResponse, SuccessResponse
|
|
from dvadmin.utils.serializers import CustomModelSerializer
|
|
from dvadmin.utils.viewset import CustomModelViewSet
|
|
|
|
|
|
|
|
|
|
class MenuButtonSerializer(CustomModelSerializer):
|
|
"""
|
|
菜单按钮-序列化器
|
|
"""
|
|
|
|
class Meta:
|
|
model = MenuButton
|
|
fields = ['id', 'name', 'name_en', 'name_zh_tw', 'value', 'api', 'method', 'menu']
|
|
read_only_fields = ["id"]
|
|
|
|
|
|
|
|
|
|
class MenuButtonCreateUpdateSerializer(CustomModelSerializer):
|
|
"""
|
|
初始化菜单按钮-序列化器
|
|
"""
|
|
|
|
class Meta:
|
|
model = MenuButton
|
|
fields = "__all__"
|
|
read_only_fields = ["id"]
|
|
|
|
|
|
class MenuButtonViewSet(CustomModelViewSet):
|
|
"""
|
|
菜单按钮接口
|
|
list:查询
|
|
create:新增
|
|
update:修改
|
|
retrieve:单例
|
|
destroy:删除
|
|
"""
|
|
queryset = MenuButton.objects.order_by('create_datetime')
|
|
serializer_class = MenuButtonSerializer
|
|
create_serializer_class = MenuButtonCreateUpdateSerializer
|
|
update_serializer_class = MenuButtonCreateUpdateSerializer
|
|
extra_filter_class = []
|
|
|
|
def list(self, request, *args, **kwargs):
|
|
"""
|
|
重写list方法
|
|
:param request:
|
|
:param args:
|
|
:param kwargs:
|
|
:return:
|
|
"""
|
|
queryset = self.filter_queryset(self.get_queryset()).order_by('name')
|
|
serializer = self.get_serializer(queryset, many=True, request=request)
|
|
return SuccessResponse(serializer.data,msg="获取成功")
|
|
|
|
@action(methods=['get'],detail=False,permission_classes=[IsAuthenticated])
|
|
def menu_button_all_permission(self,request):
|
|
"""
|
|
获取所有的按钮权限
|
|
:param request:
|
|
:return:
|
|
"""
|
|
is_superuser = request.user.is_superuser
|
|
if is_superuser:
|
|
queryset = MenuButton.objects.values_list('value',flat=True)
|
|
else:
|
|
role_id = request.user.role.values_list('id', flat=True)
|
|
queryset = RoleMenuButtonPermission.objects.filter(role__in=role_id).values_list('menu_button__value',flat=True).distinct()
|
|
return DetailResponse(data=queryset)
|
|
|
|
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
|
def batch_create(self, request, *args, **kwargs):
|
|
"""
|
|
批量创建菜单"增删改查查"权限
|
|
创建的数据来源于菜单,需要规范创建菜单参数
|
|
value:菜单的component_name:method
|
|
api:菜单的web_path增加'/api'前缀,并根据method增加{id}
|
|
"""
|
|
menu_obj = Menu.objects.filter(id=request.data['menu']).first()
|
|
if not menu_obj:
|
|
return DetailResponse(data={}, msg="菜单不存在", code=4000)
|
|
if not menu_obj.component_name:
|
|
return DetailResponse(data={}, msg="该菜单未设置组件名称(component_name),无法自动生成权限", code=4000)
|
|
|
|
# 获取已存在的 value,避免唯一约束冲突
|
|
existing_values = set(
|
|
MenuButton.objects.filter(menu=menu_obj.id).values_list('value', flat=True)
|
|
)
|
|
|
|
result_list = [
|
|
{'menu': menu_obj.id, 'name': '新增', 'name_en': 'Add', 'name_zh_tw': '新增', 'value': f'{menu_obj.component_name}:Create', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
|
{'menu': menu_obj.id, 'name': '删除', 'name_en': 'Delete', 'name_zh_tw': '刪除', 'value': f'{menu_obj.component_name}:Delete', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 3},
|
|
{'menu': menu_obj.id, 'name': '编辑', 'name_en': 'Edit', 'name_zh_tw': '編輯', 'value': f'{menu_obj.component_name}:Update', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 2},
|
|
{'menu': menu_obj.id, 'name': '查询', 'name_en': 'Search', 'name_zh_tw': '查詢', 'value': f'{menu_obj.component_name}:Search', 'api': f'/api/{menu_obj.component_name}/', 'method': 0},
|
|
{'menu': menu_obj.id, 'name': '详情', 'name_en': 'Detail', 'name_zh_tw': '詳情', 'value': f'{menu_obj.component_name}:Retrieve', 'api': f'/api/{menu_obj.component_name}/{{id}}/', 'method': 0},
|
|
{'menu': menu_obj.id, 'name': '复制', 'name_en': 'Copy', 'name_zh_tw': '複製', 'value': f'{menu_obj.component_name}:Copy', 'api': f'/api/{menu_obj.component_name}/', 'method': 1},
|
|
{'menu': menu_obj.id, 'name': '导入', 'name_en': 'Import', 'name_zh_tw': '導入', 'value': f'{menu_obj.component_name}:Import', 'api': f'/api/{menu_obj.component_name}/import_data/', 'method': 1},
|
|
{'menu': menu_obj.id, 'name': '导出', 'name_en': 'Export', 'name_zh_tw': '導出', 'value': f'{menu_obj.component_name}:Export', 'api': f'/api/{menu_obj.component_name}/export_data/', 'method': 1},
|
|
]
|
|
|
|
# 只保留不存在的新记录
|
|
to_create = [r for r in result_list if r['value'] not in existing_values]
|
|
skipped = len(result_list) - len(to_create)
|
|
|
|
if to_create:
|
|
serializer = self.get_serializer(data=to_create, many=True)
|
|
serializer.is_valid(raise_exception=True)
|
|
MenuButton.objects.bulk_create(
|
|
[MenuButton(**item) for item in to_create],
|
|
ignore_conflicts=True
|
|
)
|
|
|
|
return SuccessResponse(data={'created': len(to_create), 'skipped': skipped}, msg=f"批量创建成功 {len(to_create)} 项,跳过 {skipped} 项(已存在)")
|
|
|
|
@action(methods=['get'], detail=False, permission_classes=[IsAuthenticated])
|
|
def scan_get_apps(self, request):
|
|
"""
|
|
获取可扫描的 Django App 列表
|
|
扫描 dvadmin 下的所有 app(每个 app 下必须有 views.py 才纳入)
|
|
"""
|
|
import os
|
|
from pathlib import Path
|
|
|
|
dvadmin_dir = Path(__file__).resolve().parent.parent.parent
|
|
custom_apps = []
|
|
for item in dvadmin_dir.iterdir():
|
|
if not item.is_dir():
|
|
continue
|
|
# 查找 views/ 子目录(dvadmin 项目风格:views 在子目录中)
|
|
views_dir = item / 'views'
|
|
if views_dir.exists() and views_dir.is_dir():
|
|
if item.name not in ('utils',):
|
|
custom_apps.append(item.name)
|
|
|
|
return DetailResponse(data=sorted(custom_apps))
|
|
|
|
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
|
def scan_viewset(self, request):
|
|
"""
|
|
扫描指定 app 下所有 ViewSet,返回接口预览数据
|
|
请求体: {"app": "system"}
|
|
"""
|
|
import importlib
|
|
import pkgutil
|
|
from pathlib import Path
|
|
from rest_framework.viewsets import GenericViewSet
|
|
from rest_framework.decorators import action
|
|
from dvadmin.system.models import MenuButton
|
|
|
|
app_name = request.data.get('app')
|
|
if not app_name:
|
|
return DetailResponse(data=[], msg="app 参数必填")
|
|
|
|
full_app_name = f'dvadmin.{app_name}'
|
|
result = []
|
|
|
|
try:
|
|
views_pkg = importlib.import_module(f'{full_app_name}.views')
|
|
except ModuleNotFoundError:
|
|
return DetailResponse(data=[], msg=f"App '{app_name}' 下未找到 views 模块")
|
|
|
|
views_pkg_path = Path(views_pkg.__file__).parent
|
|
view_modules = []
|
|
for importer, modname, ispkg in pkgutil.iter_modules([str(views_pkg_path)]):
|
|
if modname not in ('__init__',):
|
|
view_modules.append(f'{full_app_name}.views.{modname}')
|
|
|
|
existing_values = set(MenuButton.objects.values_list('value', flat=True))
|
|
seen_viewset_ids = set() # 去重:同一 ViewSet 类可能被多个子模块引用
|
|
|
|
# 标准 CRUD action 映射
|
|
standard_actions = {
|
|
'list': ('GET', 0, '列表查询'),
|
|
'create': ('POST', 1, '新增'),
|
|
'retrieve': ('GET', 0, '详情查询'),
|
|
'update': ('PUT', 2, '更新'),
|
|
'partial_update': ('PATCH', 2, '部分更新'),
|
|
'destroy': ('DELETE', 3, '删除'),
|
|
}
|
|
|
|
import inspect
|
|
for module_name in view_modules:
|
|
try:
|
|
module = importlib.import_module(module_name)
|
|
except Exception:
|
|
continue
|
|
|
|
for attr_name in dir(module):
|
|
cls = getattr(module, attr_name)
|
|
if not isinstance(cls, type):
|
|
continue
|
|
if not issubclass(cls, GenericViewSet):
|
|
continue
|
|
if cls is GenericViewSet:
|
|
continue
|
|
# 去重
|
|
if id(cls) in seen_viewset_ids:
|
|
continue
|
|
seen_viewset_ids.add(id(cls))
|
|
|
|
viewset_name = cls.__name__
|
|
model_name = viewset_name.removesuffix('ViewSet') if viewset_name.endswith('ViewSet') else viewset_name
|
|
|
|
viewset_verbose_name = viewset_name
|
|
if cls.__doc__:
|
|
lines = cls.__doc__.strip().split('\n')
|
|
viewset_verbose_name = lines[0].strip()
|
|
|
|
found_actions = {}
|
|
|
|
# 先加入标准 CRUD action
|
|
found_actions.update(standard_actions)
|
|
|
|
# 再找自定义 @action
|
|
import inspect
|
|
for name, method in inspect.getmembers(cls, predicate=inspect.isfunction):
|
|
if hasattr(method, 'mapping'):
|
|
# 这是 @action 装饰的方法
|
|
http_method = list(method.mapping.keys())[0].upper()
|
|
method_map = {'GET': 0, 'POST': 1, 'PUT': 2, 'PATCH': 2, 'DELETE': 3}
|
|
method_int = method_map.get(http_method, 0)
|
|
action_doc = (method.__doc__ or '').strip().split('\n')[0] if method.__doc__ else name
|
|
found_actions[name] = (http_method, method_int, action_doc)
|
|
|
|
# 获取该 ViewSet 对应的 URL 前缀
|
|
prefix = viewset_name.lower().replace('viewset', '')
|
|
lookup = 'id'
|
|
if hasattr(cls, 'lookup_field'):
|
|
lookup = getattr(cls, 'lookup_field') or 'id'
|
|
|
|
buttons = []
|
|
for action_name, (http_method, method_int, name) in found_actions.items():
|
|
action_title = action_name.replace('_', ' ').title().replace(' ', '')
|
|
value = f"{app_name}:{model_name}:{action_title}"
|
|
|
|
if action_name in ('list', 'create'):
|
|
path = f"/api/{app_name}/{prefix}/"
|
|
elif action_name in ('retrieve', 'update', 'partial_update', 'destroy'):
|
|
path = f"/api/{app_name}/{prefix}/{{{lookup}}}/"
|
|
else:
|
|
path = f"/api/{app_name}/{prefix}/{action_name}/"
|
|
|
|
buttons.append({
|
|
'path': path,
|
|
'method': http_method,
|
|
'action': action_name,
|
|
'name': name,
|
|
'value': value,
|
|
'is_existing': value in existing_values,
|
|
'method_int': method_int,
|
|
})
|
|
|
|
if buttons:
|
|
result.append({
|
|
'viewset': viewset_name,
|
|
'viewset_verbose_name': viewset_verbose_name,
|
|
'buttons': buttons,
|
|
})
|
|
|
|
return DetailResponse(data=result)
|
|
|
|
@action(methods=['post'], detail=False, permission_classes=[IsAuthenticated])
|
|
def scan_batch_create(self, request):
|
|
"""
|
|
批量创建 MenuButton
|
|
请求体: {"menu_id": 12, "buttons": [...]}
|
|
"""
|
|
from dvadmin.system.models import MenuButton
|
|
|
|
menu_id = request.data.get('menu_id')
|
|
buttons = request.data.get('buttons', [])
|
|
|
|
if not menu_id:
|
|
return DetailResponse(data={}, msg="menu_id 参数必填", code=4000)
|
|
if not buttons:
|
|
return DetailResponse(data={}, msg="buttons 不能为空", code=4000)
|
|
|
|
created_count = 0
|
|
skipped_count = 0
|
|
|
|
# 获取已存在的 value
|
|
existing_values = set(MenuButton.objects.filter(menu_id=menu_id).values_list('value', flat=True))
|
|
|
|
objects_to_create = []
|
|
for btn in buttons:
|
|
value = btn.get('value')
|
|
if value in existing_values:
|
|
skipped_count += 1
|
|
continue
|
|
|
|
method_int = btn.get('method_int', 0)
|
|
if isinstance(btn.get('method'), str):
|
|
method_map = {'GET': 0, 'POST': 1, 'PUT': 2, 'PATCH': 2, 'DELETE': 3}
|
|
method_int = method_map.get(btn['method'].upper(), 0)
|
|
|
|
objects_to_create.append(MenuButton(
|
|
menu_id=menu_id,
|
|
name=btn.get('name', ''),
|
|
value=value,
|
|
api=btn.get('path', ''),
|
|
method=method_int,
|
|
))
|
|
existing_values.add(value)
|
|
|
|
if objects_to_create:
|
|
MenuButton.objects.bulk_create(objects_to_create)
|
|
created_count = len(objects_to_create)
|
|
|
|
return DetailResponse(data={'count': created_count, 'skipped': skipped_count}, msg=f"已创建 {created_count} 项,跳过 {skipped_count} 项(已存在)") |