在后台管理系统不断叠加业务模块时,树状菜单会自然生长成"又深又长"的形态,入口查找需要频繁展开与定位,使用效率下降,同时导航体验波动明显。菜单路由写死在前端还会带来新增与调整模块必须改代码重发的连锁成本,直接拖慢迭代节奏。
本文记录一次已落地的改造方案,通过后端返回路由数据,前端使用通用卡片组件动态渲染模块入口,目标聚焦在入口更直观与维护成本收敛,内容仅覆盖可用实现方式与关键约束。
文章目录
需求解析
本次改造发生在后台管理系统中,前端技术栈为 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>
总结
该方案将模块入口从树状菜单的"结构维护"切换为接口驱动的"数据渲染",后端负责入口路径与可见范围,前端专注展示与跳转,菜单臃肿与频繁发布问题得到缓解。现有实现仍存在演进空间,模块元信息可进一步结构化用于图标、排序与业务标签的统一描述,接口层可引入缓存与版本控制降低重复计算成本。
入口卡片化更适合工作台与聚合页形态,在模块规模持续增大时更容易保持可读性与扩展性,并将新增模块的接入成本压缩到接口层面。