diff --git a/backend/dvadmin/system/views/menu_button.py b/backend/dvadmin/system/views/menu_button.py index dc5130b..a14da74 100644 --- a/backend/dvadmin/system/views/menu_button.py +++ b/backend/dvadmin/system/views/menu_button.py @@ -87,12 +87,22 @@ class MenuButtonViewSet(CustomModelViewSet): @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}, @@ -101,11 +111,22 @@ class MenuButtonViewSet(CustomModelViewSet): {'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},] - serializer = self.get_serializer(data=result_list, many=True) - serializer.is_valid(raise_exception=True) - serializer.save() - return SuccessResponse(serializer.data, msg=_("Batch creation successful")) + {'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): diff --git a/docs/auto-permission-scan.md b/docs/auto-permission-scan.md index 9986fa2..7c4c9d3 100644 --- a/docs/auto-permission-scan.md +++ b/docs/auto-permission-scan.md @@ -17,7 +17,18 @@ ## 2. 菜单管理 - 自动扫描功能 +![image-20260409194934989](https://images-warehouse.oss-cn-hangzhou.aliyuncs.com/img/202604091949170.png) + +![image-20260409194952656](https://images-warehouse.oss-cn-hangzhou.aliyuncs.com/img/202604091949701.png) + +![image-20260409195119115](https://images-warehouse.oss-cn-hangzhou.aliyuncs.com/img/202604091951173.png) + +![image-20260409195141404](https://images-warehouse.oss-cn-hangzhou.aliyuncs.com/img/202604091951457.png) + +![image-20260409195244451](https://images-warehouse.oss-cn-hangzhou.aliyuncs.com/img/202604091952531.png) + ### 流程 + ``` 菜单管理页面 → 选中菜单 → 点击「自动扫描」→ 选择 App → 开始扫描 → 预览分组表格(按 ViewSet 分组,可折叠) diff --git a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx index 8bcf20a..65635de 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/crud.tsx +++ b/web/src/views/system/menu/components/MenuButtonCom/crud.tsx @@ -3,8 +3,7 @@ import { useI18n } from 'vue-i18n'; import * as api from './api'; import {auth} from '/@/utils/authFunction' import {request} from '/@/utils/service'; -import { successNotification } from '/@/utils/message'; -import { ElMessage } from 'element-plus'; +import { ElMessage, ElMessageBox } from 'element-plus'; import { nextTick, ref } from 'vue'; import XEUtils from 'xe-utils'; //此处为crudOptions配置 @@ -92,11 +91,61 @@ export const createCrudOptions = function ({crudExpose, context}: CreateCrudOpti } const result = await api.BatchAdd({ menu: context!.selectOptions.value.id }); if (result.code == 2000) { - successNotification(result.msg); + ElMessage.success(result.msg); crudExpose.doRefresh(); + } else { + ElMessage.error(result.msg); } }, }, + batchDelete: { + show: auth('menu:DeleteButton'), + type: 'danger', + text: t('message.pages.menu.buttons.batchDelete'), + disabled: () => selectedRows.value.length === 0, + click: async () => { + if (selectedRows.value.length === 0) { + ElMessage.warning(t('message.pages.menu.messages.selectMenu')); + return; + } + await ElMessageBox.confirm( + t('message.pages.menu.messages.batchDeleteConfirm', { count: selectedRows.value.length }), + t('message.pages.menu.buttons.confirm'), + { type: 'warning', confirmButtonText: t('message.pages.menu.buttons.confirm'), cancelButtonText: t('message.pages.menu.buttons.cancel') } + ); + await api.BatchDelete(XEUtils.pluck(selectedRows.value, 'id')); + ElMessage.success(t('message.pages.menu.messages.deleteSuccess')); + selectedRows.value = []; + crudExpose.doRefresh(); + }, + }, + selectAll: { + show: true, + text: t('message.pages.menu.buttons.selectAll'), + disabled: () => !context!.selectOptions.value.id, + click: () => { + const tableRef = crudExpose.getBaseTableRef(); + const tableData = crudExpose.getTableData(); + nextTick(() => { + XEUtils.arrayEach(tableData, (row: any) => { + tableRef.toggleRowSelection(row, true); + }); + }); + }, + }, + selectNone: { + show: true, + text: t('message.pages.menu.buttons.selectNone'), + disabled: () => selectedRows.value.length === 0, + click: () => { + const tableRef = crudExpose.getBaseTableRef(); + const tableData = crudExpose.getTableData(); + XEUtils.arrayEach(tableData, (row: any) => { + tableRef.toggleRowSelection(row, false); + }); + selectedRows.value = []; + }, + }, }, }, rowHandle: { diff --git a/web/src/views/system/menu/components/MenuButtonCom/index.vue b/web/src/views/system/menu/components/MenuButtonCom/index.vue index ec55e35..6fb69d9 100644 --- a/web/src/views/system/menu/components/MenuButtonCom/index.vue +++ b/web/src/views/system/menu/components/MenuButtonCom/index.vue @@ -1,26 +1,23 @@ + +