【Dv3Admin】菜单转换选项卡平铺到页面

在多业务模块项目中,菜单与路由的统一管理对前后端协作和功能扩展至关重要。缺乏规范会导致组件路径混乱、权限控制分散,增加维护难度。

基于统一规则的路由与组件路径配置方案,结合后端动态路由数据输出与前端通用组件渲染机制,实现多业务模块下的可扩展菜单管理和工作台页面构建。

菜单设置

在项目中需要根据不同业务功能,统一配置路由地址与组件路径,并生成对应的 component_name

字段名称含义说明格式规则示例
web_path路由访问路径/modules下的一级目录/路由模块目录/Workbenches/NDAYTrainingSchool/Homeroom/Workbenches
componentVue 组件路径(index.vue 所在位置),用于菜单中选择组件modules下的一级目录/路由模块目录/Workbenches/index.vuemodules/NDAYTrainingSchool/Homeroom/Workbenches/index.vue
component_name组件命名规则modules下的一级目录 + 路由模块目录 + WorkbenchesNDAYTrainingSchoolHomeroomWorkbenches

在这里插入图片描述

数据类型与路由配置

根据业务功能,数据分为三类,每类都有固定的路由后缀规则:

类型名称菜单显示名路由地址规则示例
查询列表数据信息/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_pathcomponentcomponent_name 规则,实现路由与组件映射的标准化。后端依托 API 按业务类型输出菜单数据,结合权限过滤,保证数据安全与准确。前端利用通用工作台组件解析并渲染不同业务模块页面,降低重复开发成本。

未来可结合角色动态配置与多语言支持,将菜单系统扩展为支持个性化定制与国际化展示的统一入口,并通过缓存与懒加载机制提升大型项目的访问性能与交互体验。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Mr数据杨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值