diff --git a/backend/api/dao/setting.py b/backend/api/dao/setting.py index 25c18b26..0bbe5ac9 100644 --- a/backend/api/dao/setting.py +++ b/backend/api/dao/setting.py @@ -12,15 +12,18 @@ import typing import uuid from typing import ( - Any + Any, + List ) from django.core.exceptions import ValidationError from django.db import IntegrityError +from django.forms import model_to_dict from api.models.setting import ( Functions, TestEnvironment, BindDataSource, - Notice + Notice, + Menu ) from api.response.fatcory import ResponseStandard from config.settings import BASE_DIR @@ -533,3 +536,56 @@ def notice_save(cls, request: Any, pk: int = None) -> int: except Exception as e: logger.error(f"Error saving notice: {e}") raise Exception(f"An error occurred while saving the notice: {e}") + + @staticmethod + def get_all_menu(): + try: + queryset = Menu.objects.all() + stmt = [model_to_dict(obj) for obj in queryset] + return stmt + except (Menu.DoesNotExist, Exception) as err: + logger.debug( + f"🏓查询菜单数据失败 -> {err}" + ) + raise Exception(f"{err} ❌") + + @staticmethod + def get_parent_menu(): + + try: + queryset = Menu.objects.filter(parent_id=0) + stmt = [model_to_dict(obj) for obj in queryset] + return stmt + except (Menu.DoesNotExist, Exception) as err: + logger.debug( + f"🏓查询父级菜单数据失败 -> {err}" + ) + raise Exception(f"{err} ❌") + + @staticmethod + def menu_assembly(parent_menu: typing.List[typing.Any], all_menu: typing.List[typing.Any]) -> list[Any]: + for parent in parent_menu: + SettingDao.assemble_meta_data(parent) + for menu in all_menu: + if int(menu['parent_id']) == int(parent['id']): + parent['children'] = [] if not parent.get('children', None) else parent['children'] + SettingDao.assemble_meta_data(menu) + parent['children'].append(menu) + SettingDao.menu_assembly(parent['children'], all_menu) if parent.get('children', None) else ... + return parent_menu + + @staticmethod + def assemble_meta_data(menu: typing.Dict): + menu['meta'] = { + 'title': menu.get('title', None), + 'icon': menu.pop('icon', None), + 'roles': ['all'] + } + return menu + + @staticmethod + def all_menu_nesting() -> typing.List[typing.Any]: + all_menu = SettingDao.get_all_menu() + parent_menu = SettingDao.get_parent_menu() + result = SettingDao.menu_assembly(parent_menu, all_menu) + return result diff --git a/backend/api/migrations/0030_alter_datastructure_type_menu.py b/backend/api/migrations/0030_alter_datastructure_type_menu.py new file mode 100644 index 00000000..d430fb63 --- /dev/null +++ b/backend/api/migrations/0030_alter_datastructure_type_menu.py @@ -0,0 +1,111 @@ +# Generated by Django 5.0.4 on 2024-05-08 06:33 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0029_datastructure"), + ] + + operations = [ + migrations.AlterField( + model_name="datastructure", + name="type", + field=models.CharField( + choices=[ + (0, "None"), + (1, "Form Data"), + (2, "X Www Form Urlencoded"), + (3, "Json"), + ], + default=0, + max_length=50, + verbose_name="DataStructure Type", + ), + ), + migrations.CreateModel( + name="Menu", + fields=[ + ("id", models.AutoField(primary_key=True, serialize=False)), + ( + "name", + models.CharField( + blank=True, max_length=50, null=True, verbose_name="Menu Name" + ), + ), + ( + "path", + models.TextField(blank=True, null=True, verbose_name="Menu Path"), + ), + ( + "component", + models.TextField( + blank=True, null=True, verbose_name="Menu Component" + ), + ), + ( + "redirect", + models.TextField( + blank=True, null=True, verbose_name="Menu Redirect" + ), + ), + ( + "title", + models.CharField( + blank=True, max_length=200, null=True, verbose_name="Menu Title" + ), + ), + ( + "icon", + models.CharField( + blank=True, max_length=200, null=True, verbose_name="Menu Icon" + ), + ), + ( + "hidden", + models.BooleanField(default=False, verbose_name="Menu Hidden"), + ), + ( + "roles", + models.TextField(blank=True, null=True, verbose_name="Menu Roles"), + ), + ( + "parent_id", + models.CharField( + blank=True, + max_length=200, + null=True, + verbose_name="Menu ParentId", + ), + ), + ( + "create_time", + models.DateTimeField( + auto_now_add=True, verbose_name="Menu CreateTime" + ), + ), + ( + "update_time", + models.DateTimeField(auto_now=True, verbose_name="Menu UpdateTime"), + ), + ( + "user", + models.ForeignKey( + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="menu_creator", + to=settings.AUTH_USER_MODEL, + verbose_name="User", + ), + ), + ], + options={ + "verbose_name": "Menu", + "verbose_name_plural": "Menu", + }, + ), + ] diff --git a/backend/api/migrations/0031_alter_menu_hidden.py b/backend/api/migrations/0031_alter_menu_hidden.py new file mode 100644 index 00000000..be69dea5 --- /dev/null +++ b/backend/api/migrations/0031_alter_menu_hidden.py @@ -0,0 +1,20 @@ +# Generated by Django 5.0.4 on 2024-05-08 06:37 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("api", "0030_alter_datastructure_type_menu"), + ] + + operations = [ + migrations.AlterField( + model_name="menu", + name="hidden", + field=models.BooleanField( + blank=True, default=False, null=True, verbose_name="Menu Hidden" + ), + ), + ] diff --git a/backend/api/models/setting.py b/backend/api/models/setting.py index 8d4b10a2..77782328 100644 --- a/backend/api/models/setting.py +++ b/backend/api/models/setting.py @@ -1,12 +1,12 @@ """ DESCRIPTION:设置模型 :Created by Null. - * table-TestEnvironment: 环境配置 * table-DataSource: 数据库配置 * table-BindDataSource: 关联的数据库 * table-Notice: 消息通知 * table-DataStructure: 数据结构 + * table-Menu: 菜单管理 """ from django.contrib.auth import get_user_model from django.core.cache import cache @@ -18,15 +18,13 @@ SET_NULL, TextField, AutoField, - IntegerChoices + IntegerChoices, BooleanField ) from django.db.models.signals import ( post_save, post_delete ) from django.utils.translation import gettext_lazy as _ -from api.emus.CaseParametersEnum import CaseParametersEnum -from api.models.project import Project User = get_user_model() @@ -220,6 +218,46 @@ def __str__(self): return self.name +class Menu(Model): + + """ + 菜单管理 + + * name: 菜单名称 + * path: 页面路径 + * component: 前端组件 + * redirect: 重定向, + * title: 页面名称 + * icon: 菜单Icon + * hidden: 是否隐藏 + * roles: 菜单权限 + * parent_id: 父级菜单 + * user: 创建者 + * create_time: 创建时间 + * update_time: 更新时间 + """ + id = AutoField(primary_key=True) + name = CharField(max_length=50, null=True, blank=True, verbose_name=_('Menu Name')) + path = TextField(null=True, blank=True, verbose_name=_('Menu Path')) + component = TextField(null=True, blank=True, verbose_name=_('Menu Component')) + redirect = TextField(null=True, blank=True, verbose_name=_('Menu Redirect')) + title = CharField(max_length=200, null=True, blank=True, verbose_name=_('Menu Title')) + icon = CharField(max_length=200, null=True, blank=True, verbose_name=_('Menu Icon')) + hidden = BooleanField(default=False, null=True, blank=True, verbose_name=_('Menu Hidden')) + roles = TextField(null=True, blank=True, verbose_name=_('Menu Roles')) + parent_id = CharField(max_length=200, null=True, blank=True, verbose_name=_('Menu ParentId')) + user = ForeignKey(User, related_name="menu_creator", null=True, on_delete=SET_NULL, verbose_name=_('User')) + create_time = DateTimeField(auto_now_add=True, verbose_name=_('Menu CreateTime')) + update_time = DateTimeField(auto_now=True, verbose_name=_('Menu UpdateTime')) + + class Meta: + verbose_name = _('Menu') + verbose_name_plural = verbose_name + + def __str__(self): + return self.name + + def _generate_cache_key(sender, instance): # noqa instance = instance cache_key = f"{instance.user} - {sender._meta.model_name}" # noqa diff --git a/backend/api/routers/notice.py b/backend/api/routers/notice.py index 00263da7..53569b92 100644 --- a/backend/api/routers/notice.py +++ b/backend/api/routers/notice.py @@ -5,7 +5,7 @@ NoticeListViewSet, NoticeDestroyViewSet, NoticeDetailView, - NoticeSaveOrUpdateApiView + NoticeSaveOrUpdateApiView, TestView ) @@ -16,6 +16,7 @@ path("delete/", NoticeDestroyViewSet.as_view()), path("detail/", NoticeDetailView.as_view()), path("saveOrUpdate/", NoticeSaveOrUpdateApiView.as_view()), + path("test", TestView.as_view()) ] diff --git a/backend/api/service/setting.py b/backend/api/service/setting.py index 0dd81b71..b2c96fe7 100644 --- a/backend/api/service/setting.py +++ b/backend/api/service/setting.py @@ -261,3 +261,17 @@ class DataSourceCreateViewSet(MagicCreateApi): # noqa queryset = DataStructure.objects.all() serializer_class = DataStructureSerializers permission_classes = [IsAuthenticated] + + +class TestView(APIView): + + permission_classes = [IsAuthenticated] + + @staticmethod + def post(request, **kwargs): + + try: + response = SettingDao.all_menu_nesting() + return Response(ResponseStandard.success(data=response)) + except Exception as err: + return Response(ResponseStandard.failed(msg=str(err))) diff --git a/web/src/routers/index.ts b/web/src/routers/index.ts index 3d3f9be1..f950e692 100644 --- a/web/src/routers/index.ts +++ b/web/src/routers/index.ts @@ -10,7 +10,6 @@ import Layout from '@/layout/index.vue' interface extendRoute { hidden?: boolean } - import systemRouter from './modules/system' import othersRouter from './modules/other' import projectRouter from './modules/project' @@ -20,12 +19,12 @@ import toolsRouter from './modules/tools' // 异步组件 export const asyncRoutes = [ - ...projectRouter, - ...systemRouter, - ...httpsRouter, - ...recordRouter, + // ...projectRouter, + // ...systemRouter, + // ...httpsRouter, + // ...recordRouter, + // ...toolsRouter, ...othersRouter, - ...toolsRouter ] /** @@ -43,26 +42,7 @@ export const asyncRoutes = [ * meta.breadcrumb ==> 如果设置为false,该项将隐藏在breadcrumb中(默认值为true) */ -export const constantRoutes: Array = [ - { - path: '/404', - name: '404', - component: () => import('@/views/errorPages/404.vue'), - hidden: true, - }, - { - path: '/403', - name: '403', - component: () => import('@/views/errorPages/403.vue'), - hidden: true, - }, - { - path: '/login', - name: 'Login', - component: () => import('@/views/login/index.vue'), - hidden: true, - meta: { title: '登录' }, - }, +export const dynamicRoutes: Array = [ { path: '/', name: 'layout', @@ -80,14 +60,41 @@ export const constantRoutes: Array = [ }, ] + /** * notFoundRouter(找不到路由) */ -export const notFoundRouter = { - path: '/:pathMatch(.*)', - name: 'notFound', - redirect: '/404', -} +export const notFoundRouter: Array = [ + { + path: '/:pathMatch(.*)', + name: 'notFound', + redirect: '/404', + }, + { + path: '/403', + name: '403', + component: () => import('@/views/errorPages/403.vue'), + hidden: true, + }, +] + + +export const staticRoutes: Array = [ + { + path: '/login', + name: 'Login', + component: () => import('@/views/login/index.vue'), + hidden: true, + meta: { title: '登录' }, + }, +] + + +export const constantRoutes = [ + ...dynamicRoutes, + ...staticRoutes +] + const router = createRouter({ // history: createWebHistory(process.env.BASE_URL), // history diff --git a/web/src/routers/modules/https.ts b/web/src/routers/modules/https.ts index 023f6b77..5cf70a02 100644 --- a/web/src/routers/modules/https.ts +++ b/web/src/routers/modules/https.ts @@ -9,14 +9,14 @@ const httpsRouter = [{ name: 'https', meta: { title: '接口测试', - icon: 'ElementPlus' + icon: 'ElementPlus', }, children: [ { path: '/https/apis', component: () => import('@/views/https/api/index.vue'), name: 'apis', - meta: { title: '接口管理', icon: 'Lightning' } + meta: { title: '接口管理', icon: 'Lightning'} }, { path: '/http/detail', diff --git a/web/src/routers/modules/other.ts b/web/src/routers/modules/other.ts index 611bdc41..5f151d1e 100644 --- a/web/src/routers/modules/other.ts +++ b/web/src/routers/modules/other.ts @@ -9,21 +9,22 @@ const othersRouter = [{ name: 'other', meta: { title: '常用组件', - icon: 'management' + icon: 'management', + roles:['other'] }, children: [ { path: '/other/clipboard', - component: () => import('@/views/other/clipboard/index.vue'), + component: "other/clipboard/index.vue", name: 'clipboard', meta: { title: '剪贴板', roles:['other'] ,icon: 'MenuIcon',} }, - { - path: '/other/iconfont', - component: () => import('@/views/other/iconfont/index.vue'), - name: 'iconfont', - meta: { title: '阿里图标库', icon: 'MenuIcon' } - }, + // { + // path: '/other/iconfont', + // component: () => import('@/views/other/iconfont/index.vue'), + // name: 'iconfont', + // meta: { title: '阿里图标库', icon: 'MenuIcon' } + // }, ] }] diff --git a/web/src/routers/modules/project.ts b/web/src/routers/modules/project.ts index 9f5137bc..7a232a57 100644 --- a/web/src/routers/modules/project.ts +++ b/web/src/routers/modules/project.ts @@ -16,14 +16,14 @@ const projectRouter = [{ path: '/project/list', component: () => import('@/views/project/index.vue'), name: 'projectList', - meta: { title: '项目管理', icon: 'FolderRemove' } + meta: { title: '项目管理', icon: 'FolderRemove' } }, { path: '/project/detail', component: () => import('@/views/project/components/editProject.vue'), name: 'projectDetail', hidden: true, - meta: { title: '项目详情', icon: 'FolderRemove' } + meta: { title: '项目详情', icon: 'FolderRemove' } } ] }] diff --git a/web/src/store/modules/permission.ts b/web/src/store/modules/permission.ts index 864e6231..38819be7 100644 --- a/web/src/store/modules/permission.ts +++ b/web/src/store/modules/permission.ts @@ -1,7 +1,7 @@ import {defineStore} from 'pinia' -import { asyncRoutes, constantRoutes,routerArray,notFoundRouter } from '@/routers/index' +import { asyncRoutes, constantRoutes, notFoundRouter } from "@/routers" import {hasPermission,filterAsyncRoutes} from "@/utils/routers" -import {filterKeepAlive,filterRoutes} from "@/utils/routers"; +import {filterKeepAlive} from "@/utils/routers"; export const usePermissionStore = defineStore({ // id: 必须的,在所有 Store 中唯一 id:'permissionState', diff --git a/web/src/store/modules/user.ts b/web/src/store/modules/user.ts index 5aedf9ab..2a2b3645 100644 --- a/web/src/store/modules/user.ts +++ b/web/src/store/modules/user.ts @@ -35,7 +35,7 @@ export const useUserStore = defineStore({ getRoles() { return new Promise((resolve, reject) => { // 获取权限列表 默认就是超级管理员,因为没有进行接口请求 写死 - this.roles = ['admin'] + this.roles = ['other'] localStorage.roles = JSON.stringify(this.roles) resolve(this.roles) }) diff --git a/web/src/utils/routers.ts b/web/src/utils/routers.ts index c1cf8ec9..909eae88 100644 --- a/web/src/utils/routers.ts +++ b/web/src/utils/routers.ts @@ -1,4 +1,9 @@ -import path from 'path-browserify' +import path from "path-browserify"; + +const modules = import.meta.glob('../../views/**/**.vue'); +export const Layout = () => import('@/layout/index.vue'); + + /** * 通过递归过滤异步路由表 * @param routes asyncRoutes