手把手教你实现一个vue3+ts+nodeJS后台管理系统(十八)
前言
封装完了接口方法、ts类型和axios,我们接下来就要编写前端路由了。前端路由主要是编写一些静态路由(登录、layout),其它路由由登录后获取对应角色的权限然后拼接到路由上,例如下图的用户管理、角色管理等。
router路由模块
我们观察上图可以将此系统分为2个结构,一是layout部分(包含header和sidebar),二是内容部分即点击菜单后跳转到的对应路由部分。其中layout部分是属于非路由组件,我们放在components文件夹中。内容部分称为路由组件,我们新建一个views文件夹来存放。我们这里先不管后端传来的路由,先按照上图去构建对应的路由能够显示对应文件的结构再去考虑后端路由。
先构建如下目录结构
然后构建路由
router/index.ts
import { createRouter, createWebHashHistory, RouteRecordRaw } from 'vue-router';
import Layout from '@/components/layout/index.vue';
const routes: Array<RouteRecordRaw> = [
{
path: '/',
component: Layout,
redirect: '/home',
children: [
{
path: '/home',
name: 'Home',
meta: {
title: '首页'
},
component: () => import('@/views/home/index.vue')
}
]
},
{
path: '/system',
name: 'System',
meta: {
title: '系统管理'
},
redirect: '/system/user',
children: [
{
path: '/system/user',
name: 'User',
meta: {
title: '用户管理',
icon: 'user'
},
component: () => import('@/views/system/user.vue')
},
{
path: '/system/role',
name: 'Role',
meta: {
title: '角色管理',
icon: 'peoples'
},
component: () => import('@/views/system/role.vue')
},
{
path: '/system/menu',
name: 'Menu',
meta: {
title: '菜单管理'
},
component: () => import('@/views/system/menu.vue')
}
]
},
{
path: '/login',
name: 'Login',
meta: {
// 设置此属性控制路由项是否显示在侧边栏中,为true隐藏
hidden: true
},
component: () => import('../views/login.vue')
}
];
const router = createRouter({
history: createWebHashHistory(),
routes
});
export default router;
Layout页面模块实现
接下来通过第一张图所示的页面结构来构建layout页面布局部分。分为header及sidebar及内容区域(用路由视图控制进行路由视图的显示)
但首先,菜单的话会用到svg
图标,这里简要描述一下做法
svg图标的封装
-
安装
vite-plugin-svg-icons
插件和fast-glob插件npm i vite-plugin-svg-icons -D npm i fast-glob -D
-
配置
vite.config.ts
- 配置
tsconfig.json
tsconfig.json
```
{
"compilerOptions": {
"types": ["vite-plugin-svg-icons/client"]
}
}
```
4. 封装SvgIcon组件
components/SvgIcon/index.vue
```
<template>
<svg aria-hidden="true" class="svg-icon" :style="'width:' + size + ';height:' + size">
<use :xlink:href="symbolId" :fill="color" />
</svg>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps({
prefix: {
type: String,
default: 'icon'
},
iconClass: {
type: String,
required: false
},
color: {
type: String
},
size: {
type: String,
default: '1em'
}
});
const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`);
</script>
<style scoped>
.svg-icon {
vertical-align: -0.15em;
overflow: hidden;
fill: currentColor;
}
</style>
```
5. 配置main.ts导入组件
实现Layout页面
-
安装sass
npm install sass@1.54.4 -D
-
实现标题链接页面(用于路由跳转)
components/layout/Link.vue
```
<template>
<div @click="push">
<!-- 插槽内置菜单项 -->
<slot />
</div>
</template>
<script lang="ts" setup>
import { useRouter } from 'vue-router';
// 接收到点击的菜单页
const props = defineProps({
to: {
type: String,
required: true
}
})
// 导入路由
const router = useRouter();
// 路由跳转方法
const push = () => {
router.push(props.to).catch(err => {
console.log(err);
});
};
</script>
```
3. 实现菜单项页面
主要就是判断有没有孩子,有一个孩子和无孩子直接展示单个的菜单项,有孩子展示下拉菜单
components/layout/SidebarItem.vue
```
<template>
<!-- 如果菜单hidden值为假,即菜单显示 -->
<div v-if="!item.meta || !item.meta.hidden">
<!-- 如果菜单无孩子,直接以menu-item单个显示 -->
<template v-if="
hasOneShowingChild(item.children, item) &&
(!onlyOneChild.children || onlyOneChild.noShowingChildren)
">
<app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
<el-menu-item :index="resolvePath(onlyOneChild.path)">
<svg-icon v-if="onlyOneChild.meta && onlyOneChild.meta.icon" :icon-class="onlyOneChild.meta.icon"
style="margin-right: 10px;" />
<template #title>
{{ onlyOneChild.meta.title }}
</template>
</el-menu-item>
</app-link>
</template>
<!-- 如果菜单有孩子,以sub-menu的形式显示,且进行递归直到无孩子 -->
<el-sub-menu v-else :index="resolvePath(item.path)" popper-append-to-body>
<!-- popper-append-to-body -->
<template #title>
<svg-icon v-if="item.meta && item.meta.icon" :icon-class="item.meta.icon" style="margin-right: 10px;">
</svg-icon>
<span v-if="item.meta && item.meta.title">{{
item.meta.title
}}</span>
</template>
<sidebar-item v-for="child in item.children" :key="child.path" :item="child"
:base-path="resolvePath(child.path)" />
</el-sub-menu>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
// vite中默认无path模块,需下载导入
import path from 'path-browserify';
import AppLink from './Link.vue';
const props = defineProps({
item: {
type: Object,
required: true
},
basePath: {
type: String,
required: true
}
});
const onlyOneChild = ref();
function hasOneShowingChild(children = [] as any, parent: any) {
if (!children) {
children = [];
}
const showingChildren = children.filter((item: any) => {
if (item.meta && item.meta.hidden) {
return false;
} else {
onlyOneChild.value = item;
return true;
}
});
// 当只有一个孩子时
if (showingChildren.length === 1) {
return true;
}
// 当没有孩子时
if (showingChildren.length === 0) {
onlyOneChild.value = { ...parent, path: '', noShowingChildren: true };
return true;
}
return false;
}
// 将地址拼接为一个完整的绝对路径
function resolvePath(routePath: string) {
return path.resolve(props.basePath, routePath);
}
</script>
```
4. 实现总的布局 总的布局就是header页头+sidebar侧边栏+内容区域。注意!这里静态菜单routes仅为测试所用,路由菜单是后端返回的
components/layout/index.vue
```
<template>
<div class="app-container">
<!-- header菜单 -->
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" text-color="#ffffff"
:ellipsis="false">
<el-menu-item index="1" class="menu-title">
<el-icon>
<Tools />
</el-icon>
 后台管理系统
</el-menu-item>
<div class="flex-grow"></div>
<el-sub-menu index="2">
<template #title>
<div class="user_name">用户</div>
</template>
<el-menu-item index="2-1" style="color: #3c8dbc;">个人中心</el-menu-item>
<el-menu-item index="2-2" style="color: #3c8dbc;">重置密码</el-menu-item>
<el-menu-item index="2-3" style="color: #3c8dbc;">退出登录</el-menu-item>
</el-sub-menu>
</el-menu>
<!-- 侧边栏与内容区域 -->
<div class="main-content">
<!-- 侧边栏 -->
<el-menu active-text-color="#ffd04b" background-color="#545c64" class="side-menu" :default-active="$route.path"
text-color="#fff" router>
<SidebarItem v-for="route in routes" :item="route" :key="route.path" :base-path="route.path" />
</el-menu>
<!-- 内容区域 -->
<div class="main-container">
<router-view />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import SidebarItem from './SidebarItem.vue';
import { useRouter } from 'vue-router';
const activeIndex = ref('1')
// 从后端获取树状结构路由
const router = useRouter()
const routes = router.getRoutes()
onMounted(() => {
console.log(routes);
})
</script>
<style scoped lang="scss">
...
</style>
```
测试效果
可以看到显示效果符合预期,但是由于上个代码块的getRoutes方法是每个菜单项都列出来(可在控制台打印看到),所以每个路由项都循环遍历显示了。到时候给的会是顶级菜单(例如系统管理)的集合,就没有问题。
转载自:https://juejin.cn/post/7176906672215064613