【Dv3Admin】Vue3通用自定义工作台卡片

在后台管理系统不断叠加业务模块时,树状菜单会自然生长成"又深又长"的形态,入口查找需要频繁展开与定位,使用效率下降,同时导航体验波动明显。菜单路由写死在前端还会带来新增与调整模块必须改代码重发的连锁成本,直接拖慢迭代节奏。

本文记录一次已落地的改造方案,通过后端返回路由数据,前端使用通用卡片组件动态渲染模块入口,目标聚焦在入口更直观与维护成本收敛,内容仅覆盖可用实现方式与关键约束。

文章目录

需求解析

本次改造发生在后台管理系统中,前端技术栈为 Vue + Element Plus,系统模块数量较多且存在多角色、多业务入口需求。原有实现依赖前端静态菜单配置,随着功能增加出现入口查找成本上升与配置维护复杂化的问题,需要将模块入口的控制权交给后端,并以数据驱动渲染入口卡片完成跳转。

现象 对用户的影响 对开发维护的影响
菜单层级变深、数量变多 进入目标模块需要多次展开与查找,效率下降 页面入口分散,导航调整成本变高
入口因角色差异而分化 不同用户需要不同入口,查找路径不统一 静态配置越来越复杂,维护压力增加
模块变更频繁 导航体验波动,使用习惯被打断 每次新增或调整模块都要改前端并发布

后台功能增多
传统树状菜单冗长且层级多
查找效率下降 / 维护成本上升
后端统一维护路由配置
接口返回当前用户可见路由数据
前端通用组件动态渲染卡片
点击卡片跳转对应模块
入口更直观 / 扩展更灵活 / 统一维护

目录路由地址配置

路由地址的前缀对前端项目文件目录名,后缀对应服务器数据工作台返回的具体配置,(具体的卡片路由规则可根据与后端协商)

需要注意的是,组件地址必须与前端项目的 vue 文件地址一致,否则会导致点击卡片无法跳转,这一步不能省略。

功能实现

定位对象为后端维护的模块入口信息,目的在于让每个模块的入口路径与组件地址保持一致,并由接口按当前可见范围返回数据,前端不再维护静态菜单结构;关键点在于组件地址与前端文件路径一致,否则会导致跳转失效。

定位对象为通用卡片导航组件 components / commonWorkbenches / index.vue,目的在于在进入页面时请求接口获取路由数据,按分类组织展示并点击卡片完成跳转,组件只负责渲染与导航,不写死任何菜单结构。

组件职责 目的
请求接口获取模块数据 让页面入口由后端控制,前端不再维护静态配置
按分类组织数据结构 把不同类别的入口分区展示,页面更直观
点击卡片跳转对应路由 保证用户可以直接进入目标模块
javascript 复制代码
<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>
    <fs-page>
        <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">
                        <div v-for="subItem in sec.items" :key="subItem.label">
                            <div class="sub-title">{{ subItem.label }}</div>
                            <div class="wb-subgrid">
                                <el-card v-for="item in subItem.children" :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>
                        </div>
                    </div>
                </section>
            </template>

            <el-empty v-else description="暂无数据" />
        </div>
    </fs-page>
</template>

定位对象为业务页面对通用组件的引用方式,目的在于通过传入接口地址决定展示内容,页面本身不再描述模块结构,否则会导致再次回到前端写死菜单的维护路径。

javascript 复制代码
<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>

总结

该方案将模块入口从树状菜单的"结构维护"切换为接口驱动的"数据渲染",后端负责入口路径与可见范围,前端专注展示与跳转,菜单臃肿与频繁发布问题得到缓解。现有实现仍存在演进空间,模块元信息可进一步结构化用于图标、排序与业务标签的统一描述,接口层可引入缓存与版本控制降低重复计算成本。

入口卡片化更适合工作台与聚合页形态,在模块规模持续增大时更容易保持可读性与扩展性,并将新增模块的接入成本压缩到接口层面。

相关推荐
云和数据.ChenGuang3 小时前
数据分析中的dataframe详解
python·数据挖掘·数据分析·django·pygame
Betelgeuse761 天前
DjangoBlog学习案例:掌握Django MVT架构与多应用协作实践
学习·架构·django
IT 行者1 天前
Claude Code Viewer: 打造 Web 端 Claude Code 会话管理利器
前端·人工智能·python·django
龙腾AI白云1 天前
数字孪生在航空领域的应用方法及案例
学习·django·virtualenv·pygame
q_35488851531 天前
计算机毕业设计源码:锦江酒店大数据分析与个性化推荐系统 Django框架 Vue 可视化 Hadoop 爬虫 协同过滤推荐算法 民宿 客栈(建议收藏)✅
python·机器学习·信息可视化·数据分析·django·课程设计·旅游
tryCbest1 天前
Django 基础入门教程(第四篇):Form组件、Auth认证、Cookie/Session与中间件
python·django
PD我是你的真爱粉1 天前
Django MVT vs FastAPI DDD架构
架构·django·fastapi
龙腾AI白云2 天前
数据可视化实战:用AI工具制作专业数据分析图表
深度学习·数据分析·django
程序媛徐师姐2 天前
Python基于Django的网络漏洞扫描工具的开发与优化【附源码、文档说明】
python·django·漏洞扫描工具·漏洞扫描·网络漏洞扫描工具·python网络漏洞扫描工具·pytho网络漏洞扫描