【Dv3Admin】菜单转换选项卡平铺到页面
在多业务模块项目中,菜单与路由的统一管理对前后端协作和功能扩展至关重要。缺乏规范会导致组件路径混乱、权限控制分散,增加维护难度。
基于统一规则的路由与组件路径配置方案,结合后端动态路由数据输出与前端通用组件渲染机制,实现多业务模块下的可扩展菜单管理和工作台页面构建。
菜单设置
在项目中需要根据不同业务功能,统一配置路由地址与组件路径,并生成对应的 component_name
。
字段名称 | 含义说明 | 格式规则 | 示例 |
---|---|---|---|
web_path | 路由访问路径 | /modules下的一级目录/路由模块目录/Workbenches | /NDAYTrainingSchool/Homeroom/Workbenches |
component | Vue 组件路径(index.vue 所在位置),用于菜单中选择组件 | modules下的一级目录/路由模块目录/Workbenches/index.vue | modules/NDAYTrainingSchool/Homeroom/Workbenches/index.vue |
component_name | 组件命名规则 | modules下的一级目录 + 路由模块目录 + Workbenches | NDAYTrainingSchoolHomeroomWorkbenches |
数据类型与路由配置
根据业务功能,数据分为三类,每类都有固定的路由后缀规则:
类型名称 | 菜单显示名 | 路由地址规则 | 示例 |
---|---|---|---|
查询列表 | 数据信息 | /modules下的一级目录 + 路由模块目录 + Data | /NDAYTrainingSchoolFinanceData |
功能设置 | 数据设置 | /modules下的一级目录 + 路由模块目录 + Setting | /NDAYTrainingSchoolFinanceSetting |
数据可视化 | 数据统计 | /modules下的一级目录 + 路由模块目录 + Statistics | /NDAYTrainingSchoolFinanceStatistics |
后端配置
后端配置通用API视图,根据请求路径解析出对应的菜单父节点(加上 Data、Setting、Statistics 三种后缀),查出其子菜单并按用户权限过滤,最后把三个分类的菜单数据一次性打包成 JSON 返回,找不到或无权限时返回空列表。
# coding:utf-8
'''
@IDE :PyCharm
@Project :ManageBak.py
@File :Control.py
@Author :Mr数据杨
@Date :2025/6/11
@Desc :
'''from dvadmin.system.models import Users
from rest_framework.decorators import action
from dvadmin.system.views.menu import WebRouterSerializer
from dvadmin.utils.viewset import CustomModelViewSet
from modules.Config.views_app.DropDownOptions import DummySerializer
from dvadmin.system.models import Menu, RoleMenuPermission
from dvadmin.utils.json_response import SuccessResponse, ErrorResponse# 你自己的 WebRouterSerializer 按原样导入def _menu_block_serializer(request, suffix: str):"""返回纯 list 数据;找不到菜单或无权限时返回 []"""parts = [p for p in request.path.strip("/").split("/") if p]web = parts[1] if len(parts) >= 2 else Nonerouter = parts[2] if len(parts) >= 3 else Noneif not (web and router):return [] # 保证是可序列化类型target_web_path = f"/{web}{router}{suffix}"menu = Menu.objects.filter(web_path=target_web_path, status=True).first()if not menu:return [] # 没找到菜单,返回空列表children = Menu.objects.filter(parent=menu, status=True).order_by("sort")if not request.user.is_superuser:role_ids = request.user.role.values_list("id", flat=True)permitted_ids = RoleMenuPermission.objects.filter(role_id__in=role_ids).values_list("menu_id", flat=True)children = children.filter(id__in=permitted_ids)return WebRouterSerializer(children, many=True, request=request).datadef DataSerializer(request):# /{web}{router} + Datareturn _menu_block_serializer(request, suffix="Data")def SettingSerializer(request):# /{web}{router} + Settingreturn _menu_block_serializer(request, suffix="Setting")def StatisticsSerializer(request):# /{web}{router} + Settingreturn _menu_block_serializer(request, suffix="Statistics")class WorkbenchesViewSet(CustomModelViewSet):http_method_names = ['get', 'post', 'put']queryset = Users.objects.none()serializer_class = DummySerializer@action(methods=['GET'], detail=False, permission_classes=[])def web_router(self, request):self.extra_filter_class = []data_block = DataSerializer(request)setting_block = SettingSerializer(request)statistics_block = StatisticsSerializer(request)data = {'Data': data_block or [],'Setting': setting_block or [],'Statistics': statistics_block or []}total = len(data['Data']) + len(data['Setting'])return SuccessResponse(data=data, total=total, msg="获取成功")
目录配置 基于项目目录根据实际情况调整就行了。
modules
└── NDAYTrainingSchool└── Homeroom├── migrations└── views_app├── AskLeave.py├── Attendance.py├── BehavioralNorms.py├── Communicate.py├── StatisticsClass.py
前端配置
配置通用的api.ts,用于获取后端数据信息的API接口。
import { request } from '/@/utils/service';
export function GetList(url:string) {return request({url: url,method: 'get',});
}
接收后端返回数据标准格式。
{"code": 2000,"page": 1,"limit": 1,"total": 5,"data": {"Data": [{"id": 96,"parent": 69,"icon": "fa fa-tasks","sort": 1,"path": "/NDAYTrainingSchool/Homeroom/AskLeave","name": "请假管理明细","title": "请假管理明细","is_link": false,"link_url": null,"is_catalog": false,"web_path": "/NDAYTrainingSchool/Homeroom/AskLeave","component": "NDAYTrainingSchool/Homeroom/AskLeave/index","image": "","component_name": "NDAYTrainingSchoolHomeroomAskLeave","cache": true,"visible": true,"is_iframe": false,"is_affix": false,"status": true}],"Setting": [],"Statistics": []},"msg": "获取成功"
}
Vue展示页面
组件地址对应前端项目vue文件地址,地址不对,跳转不过去,进入步骤一的页面请求接口拿到路由数据代码,这有多个这样的页面,采用的是组件形式,传递接口地址,主要是后端返回的路由数据。
<script lang="ts" setup>
import {ref} from 'vue'
import {GetList} from './api'
import {useRouter} from 'vue-router'
import {ArrowRight} from '@element-plus/icons-vue'type CardItem = {path: stringname: stringimage?: stringdesc?: string
}
const props = defineProps({apiUrl: {type: String, required: true},// 每组最多展示几条;<=0 表示全部limit: {type: Number, default: 0}
})
const router = useRouter()
const sections = ref<{ key: string; title: string; items: CardItem[] }[]>([])
const TITLE_MAP: Record<string, string> = {Data: '数据信息',Setting: '配置信息',Statistics: '统计可视化'
}
const ORDER = ['Data', 'Setting', 'Statistics']
GetList(props.apiUrl).then((res: any) => {const src = (res && res.data) || {}const built = ORDER.map((k) => {let arr: CardItem[] = Array.isArray(src[k]) ? src[k] : []if (props.limit > 0) arr = arr.slice(0, props.limit)return {key: k, title: TITLE_MAP[k] || k, items: arr}}).filter(s => s.items.length > 0)sections.value = built
})
const handleToSubMenu = (path: string) => {if (!path) returnrouter.push({path})
}
</script><template><div class="wb"><template v-if="sections.length"><section v-for="sec in sections" :key="sec.key" class="wb-section"><h3 class="wb-title"><span class="wb-dot"></span>{{ sec.title }}</h3><div class="wb-grid"><el-cardv-for="item in sec.items":key="item.path"class="wb-card"shadow="never"role="button":aria-label="item.name"tabindex="0"@click="handleToSubMenu(item.path)"@keydown.enter="handleToSubMenu(item.path)"@keydown.space.prevent="handleToSubMenu(item.path)"><div class="wb-card__inner"><!-- 图标:等比不变形 --><div class="wb-card__media"><el-image:src="item.image"fit="contain"loading="lazy"class="wb-card__img"/></div><!-- 文案 --><div class="wb-card__content"><div class="wb-card__title">{{ item.name }}</div><div class="wb-card__desc">{{ item.desc || '进入模块' }}</div></div><!-- 箭头 --><div class="wb-card__chevron"><el-icon><ArrowRight/></el-icon></div></div></el-card></div></section></template><el-empty v-else description="暂无数据"/></div>
</template><style lang="scss">// 具体样式自行修改,或者直接问GPT
</style>
选项卡引用 vue
在 Vue 组件里指定一个 apiUrl
,然后把它传给 CommonWorkbenches
组件去调用。
整个逻辑是:这个页面什么业务都不直接做,只是告诉公共的工作台组件 “我的数据接口地址是 /api/NDAYHighSchool/MoralEdu/Workbenches/web_router/
”,然后由公共组件自己去请求和渲染对应的工作台内容。
相当于这是一个“入口壳子”,每个真正的内容和渲染逻辑都在 CommonWorkbenches
里。
<script lang="ts" setup>
import CommonWorkbenches from '/@/components/commonWorkbenches/index.vue'
const apiUrl = '/api/NDAYHighSchool/MoralEdu/Workbenches/web_router/'
</script><template><CommonWorkbenches :apiUrl="apiUrl" />
</template><style lang="scss"></style>
目录配置 基于项目目录根据实际情况调整就行了。
views
└── NDAYTrainingSchool└── Homeroom├── AskLeave├── Attendance├── BehavioralNorms├── Communicate├── StatisticsClass├── StudentManage└── Workbenches
展示效果
这样布局将左侧菜单统一为选项卡,可集中相关功能入口,减少层级切换,界面更简洁,导航更高效,同时便于模块扩展与后端数据驱动渲染,保持一致的交互体验并降低维护成本。
总结
该方案通过统一的 web_path
、component
与 component_name
规则,实现路由与组件映射的标准化。后端依托 API 按业务类型输出菜单数据,结合权限过滤,保证数据安全与准确。前端利用通用工作台组件解析并渲染不同业务模块页面,降低重复开发成本。
未来可结合角色动态配置与多语言支持,将菜单系统扩展为支持个性化定制与国际化展示的统一入口,并通过缓存与懒加载机制提升大型项目的访问性能与交互体验。