在多业务模块项目中,菜单与路由的统一管理对前后端协作和功能扩展至关重要。缺乏规范会导致组件路径混乱、权限控制分散,增加维护难度。
基于统一规则的路由与组件路径配置方案,结合后端动态路由数据输出与前端通用组件渲染机制,实现多业务模块下的可扩展菜单管理和工作台页面构建。
菜单设置
在项目中需要根据不同业务功能,统一配置路由地址与组件路径,并生成对应的 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 None
router = parts[2] if len(parts) >= 3 else None
if 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).data
def DataSerializer(request):
# /{web}{router} + Data
return _menu_block_serializer(request, suffix="Data")
def SettingSerializer(request):
# /{web}{router} + Setting
return _menu_block_serializer(request, suffix="Setting")
def StatisticsSerializer(request):
# /{web}{router} + Setting
return _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: string
name: string
image?: string
desc?: 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) return
router.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-card
v-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 按业务类型输出菜单数据,结合权限过滤,保证数据安全与准确。前端利用通用工作台组件解析并渲染不同业务模块页面,降低重复开发成本。
未来可结合角色动态配置与多语言支持,将菜单系统扩展为支持个性化定制与国际化展示的统一入口,并通过缓存与懒加载机制提升大型项目的访问性能与交互体验。