Vue3+Vite+Pinia+Naive后台管理系统搭建之九:layout 动态路由布局

前言

如果对 vue3 的语法不熟悉的,可以移步Vue3.0 基础入门,快速入门。

github 开源库:Vue3-Vite-Pinia-Naive-Js

gitee   开源库:Vue3-Vite-Pinia-Naive-Js

1. 管理系统页面结构

menu面包屑用户信息页面标签页面内容 组成

2. 创建页面​

2.1 编辑 src/pages/layout.vue 布局页 

<script setup>import { ref } from "vue";import {NLayout,NLayoutSider,NLayoutHeader,NLayoutContent,} from "naive-ui";// menuimport layoutMenu from "./components/layout-menu.vue";// 面包屑import layoutCrumbs from "./components/layout-crumbs.vue";// 用户信息import layoutUser from "./components/layout-user.vue";// 页面标签import layoutTag from "./components/layout-tag.vue";// 页面内容import layoutContent from "./components/layout-content.vue";// 是否展开menulet isOpen = ref(true);// n-layout-sider 折叠状态发生改变时的回调函数function handleChangeSider(isHide) {if (isHide) {isOpen.value = !isHide;}}// n-layout-sider 完成展开后的回调function handleEnter() {isOpen.value = true;}// n-layout-sider 是否显示边框let bordered = ref(true);// n-layout-sider 是否反转背景色let inverted = ref(false);// n-layout-sider 是否在自身使用原生滚动条。如果设定为 false,Sider 将会对内容使用 naive-ui 风格的滚动条let scrollbar = ref(false);// n-layout-sider 折叠宽度let colWidth = ref(50);// n-layout-sider 展开宽度let siderWidth = ref(155);
</script><template><!-- layout 盒子 --><n-layout has-sider class="layout-box"><!-- 左侧导航 --><n-layout-sidercollapse-mode="width"show-trigger="arrow-circle":bordered="bordered":inverted="inverted":native-scrollbar="scrollbar":collapsed-width="colWidth":width="siderWidth"@update:collapsed="handleChangeSider"@after-enter="handleEnter"><layout-menu :isOpen="isOpen" :inverted="inverted"></layout-menu></n-layout-sider><!-- 右侧内容 --><n-layout><n-layout-header :bordered="bordered"><div class="layout-header__box"><layout-crumbs></layout-crumbs><layout-user></layout-user></div><!--  --><layout-tag></layout-tag><div class="layout-header__shadow"></div></n-layout-header><n-layout-content><layout-content></layout-content></n-layout-content></n-layout></n-layout>
</template><style lang="scss" scoped>.layout-box {width: 100vw;height: 100vh;box-sizing: border-box;}.layout-header__box {display: flex;align-items: center;justify-content: space-between;padding: 20px;box-sizing: border-box;height: 50px;border-bottom: 1px solid rgb(239, 239, 245);}.layout-header__shadow {width: 100%;height: 2px;background: #d9d9d9;}
</style>

2.2 新增 src/pages/components/layout-menu.vue menu 组件

<script setup>import { ref, watch, computed } from "vue";import { useRoute } from "vue-router";import { NMenu } from "naive-ui";import { usePermissionStore } from "@/store/permission.js";import { useTagStore } from "@/store/tag.js";import router from "@/router/index.js";defineProps({isOpen: Boolean,inverted: Boolean,});let route = useRoute();let permissionStore = usePermissionStore();let tagStore = useTagStore();let menuOptions = computed(() => {return permissionStore.siderMenu;});let activeMenuValue = ref("");watch(() => route.name,() => {activeMenuValue.value = route.name;permissionStore.activeMenuValue = route.name;},{ immediate: true, deep: true });// 新增 taglet obj = { title: route.meta.title, key: route.name };tagStore.addTag(obj);let handleUpdateMenu = (value, item) => {// 新增 taglet { title, key } = item;let obj = { title, key };tagStore.addTag(obj);router.push(`/${value}`);activeMenuValue.value = value;};
</script><template><!-- logo --><divclass="layout-sider__logo c-center":class="{ isHide: !isOpen }"@click="$router.push('/home')"><svg-icon name="vite"></svg-icon><!-- <img src="@/assets/images/logo.png" /> --><h1 v-show="isOpen">后台管理系统</h1></div><!-- menu组件 --><n-menu:inverted="inverted":indent="15":root-indent="15":options="menuOptions":value="activeMenuValue"@update:value="handleUpdateMenu"></n-menu>
</template><style lang="scss" scoped>.layout-sider__logo {height: 50px;font-size: 14px;font-weight: bold;cursor: pointer;img {margin-right: 5px;width: 25px;object-fit: contain;}svg {margin-right: 5px;}}.isHide {img {width: 30px;}}
</style>

2.2.1 新增 src/store/permission.js 权限状态管理

根据后端返回动态路由数据,构建 导航 menu动态路由

import { defineStore } from "pinia";
import { h } from "vue";
import { RouterLink } from "vue-router";
// 接口获取路由 自己对接口
// import { getRouters } from "@/api/menu.js";
import SvgIcon from "@/components/SvgIcon.vue";
import { routerData } from "@/mock/datas.js";const modules = import.meta.glob("../pages/*.vue");//  icon 标签
let renderIcon = (name) => {return () => h(SvgIcon, { name }, null);
};// 单个路由
let getRouterItem = (item) => {let { name, path, meta, component } = item;let obj = {path,name,meta,component: modules[`../pages/${component}`],};return obj;
};// 获取异步路由
// 所有异步路由都是layout的子路由,并且routers只有一层children,没有考虑很复杂的情况。
// 将所有异步路由都存放在rmenu数组中,返回。
let getAayncRouter = (routers) => {let rmenu = [];routers.forEach((item) => {if (item.children && item.children.length) {item.children.map((_item) => {let obj = getRouterItem(_item);obj.meta.parentTitle = item.meta.title;rmenu.push(obj);});} else {rmenu.push(getRouterItem(item));}});return rmenu;
};// 获取侧边栏导航
let getSiderMenu = (routers) => {let smenu = [];routers.forEach((item) => {let children = [];let obj = {};if (item.children && item.children.length) {// 二级 menuitem.children.map((_item) => {if (!_item.hidden) {children.push({label: () =>h(RouterLink,{ to: _item.path },{ default: () => _item.meta.title }),title: _item.meta.title,key: _item.name,icon: renderIcon(_item.meta.icon),});}});obj = {label: item.meta.title,title: item.meta.title,key: item.name,icon: renderIcon(item.meta.icon),children,};} else {// 一级 menuobj = {label: () =>h(RouterLink, { to: item.path }, { default: () => item.meta.title }),title: item.meta.title,key: item.name,icon: renderIcon(item.meta.icon),};}smenu.push(obj);});return smenu;
};export const usePermissionStore = defineStore({id: "permissionStore",state: () => {return {siderMenu: [],activeMenuValue: "",};},actions: {getRouters() {return new Promise((resolve, reject) => {this.siderMenu = getSiderMenu(routerData);resolve(getAayncRouter(routerData));// getRouters()//   .then(({ data }) => {//     this.siderMenu = getSiderMenu(data);//     resolve(data);//   })//   .catch((err) => {//     reject(err);//   });});},},
});

2.2.2 新增 src/mock/datas.js 虚拟路由数据

模拟后端返回动态路由数据结构

export const routerData = [{name: "home",path: "/home",hidden: false,component: "home.vue",meta: {title: "首页",icon: "home",},children: null,},{name: "system",path: "/system",hidden: false,component: null,meta: {title: "系统管理",icon: "system",},children: [{name: "system-menu",path: "/system-menu",hidden: false,component: "system-menu.vue",meta: {title: "系统菜单",icon: "system-menu",},children: null,},{name: "system-dict",path: "/system-dict",hidden: false,component: "system-dict.vue",meta: {title: "系统字典",icon: "system-dict",},children: null,},],},{name: "user",path: "/user",hidden: false,component: null,meta: {title: "用户管理",icon: "user",},children: [{name: "user-user",path: "/user-user",hidden: false,component: "user-user.vue",meta: {title: "用户管理",icon: "user-user",},children: null,},{name: "user-role",path: "/user-role",hidden: false,component: "user-role.vue",meta: {title: "角色管理",icon: "user-role",},children: null,},],},
];

2.2.3 新增 src/assets/svg 路由图标

自己去图标库下,改命名

2.2.4 新增 src/store/tag.js 页面标签状态管理

点击左侧导航路由,页面标签变化

import { defineStore } from "pinia";export const useTagStore = defineStore({id: "tag",state: () => {return {tags: [{ title: "首页", key: "home" }],activeTagIndex: 0,};},getters: {tagsKey(state) {let arr = [];state.tags.map((tag) => {arr.push(tag.key);});return arr;},},actions: {addTag(tag) {if (!this.tagsKey.includes(tag.key)) {this.tags.push(tag);}},removeTag(key) {let index = this.tagsKey.indexOf(key);this.tags.splice(index, 1);this.activeTagIndex = index - 1;},},
});

2.2.5 编辑 src/router/index.js 完善路由

路由监听动态加载路由

import { createRouter, createWebHistory } from "vue-router";
import NProgress from "nprogress";
import "nprogress/nprogress.css";
import baseRouters from "./baseRouter.js";
import { getToken } from "@/utils/cookie.js";
import { useUserStore } from "@/store/user.js";
import { usePermissionStore } from "@/store/permission.js";const whiteList = ["/", "/login"];const routes = [...baseRouters];
const _createRouter = () =>createRouter({history: createWebHistory(),routes,scrollBehavior() {return {el: "#app",top: 0,behavior: "smooth",};},});export function resetRouter() {const newRouter = _createRouter();router.matcher = newRouter.matcher;
}const router = _createRouter();// 路由监听
router.beforeEach((to, from, next) => {NProgress.start();let userStore = useUserStore();let permissionStore = usePermissionStore();// 判断是否登录if (!!getToken()) {// 已登录,跳转登录页,转跳首页if (to.path === "/login") {next("");NProgress.done();} else {if (userStore.roles.length === 0) {userStore.getInfo().then((res) => {// 获取动态路由permissionStore.getRouters().then((_res) => {let resetRouters = {path: "/layout",name: "layout",component: () => import("@/pages/layout.vue"),children: _res,};router.addRoute(resetRouters);// 这句代码,重要!重要!重要!// 来确保addRoute()时动态添加的路由已经被完全加载上去。没有这句,动态路由加载后无效next({ ...to, replace: true });});}).catch((err) => {window.$msg.error(err);userStore.logout().then(() => {next({ name: "login" });});});} else {next();}}NProgress.done();} else {// 判断路由是否在白名单,是直接跳转if (whiteList.indexOf(to.path) !== -1) {next();// 未登录页面跳转,直接跳转到登录页} else {next(`/login?redirect=${to.fullPath}`);}NProgress.done();}
});export default router;

2.3 新增 src/pages/components/layout-crumbs.vue 面包屑 组件

<script setup>import { watch, ref } from "vue";import { NBreadcrumb, NBreadcrumbItem } from "naive-ui";import { useRoute } from "vue-router";let route = useRoute();// 判断是二级目录let getCrumList = (nowRoute) => {let arr = [nowRoute.meta.title];!!nowRoute.meta.parentTitle && arr.unshift(nowRoute.meta.parentTitle);return arr;};let crumbList = ref([]);// 监听路由,获取crumlistwatch(() => route,(newRoute) => {crumbList.value = getCrumList(newRoute);},{ immediate: true, deep: true });
</script><template><n-breadcrumb><n-breadcrumb-itemclass="layout-crumbs-item"v-for="(item, index) in crumbList":key="index">{{ item }}</n-breadcrumb-item></n-breadcrumb>
</template><style lang="scss" scoped>.layout-crumbs-item {font-size: 16px;}
</style>

2.4 新增 src/pages/components/layout-user.vue 用户信息 组件

<script setup>import { reactive, h, computed } from "vue";import { useDialog, NDropdown, NButton } from "naive-ui";import { useUserStore } from "@/store/user.js";import { useTagStore } from "@/store/tag.js";import router from "@/router/index.js";let userStore = useUserStore();let tagStore = useTagStore();// 登录才获取用户信息userStore.getInfo();// 获取 用户信息let avatar = computed(() => {if (!!userStore.user?.avatar) {return userStore.user.avatar;} else {return "";}});let userName = computed(() => {if (!!userStore.user?.userName) {return userStore.user.userName;} else {return "";}});// 下拉选项let baseOptions = reactive([{label: "个人信息",key: "userinfo",},{label: "修改密码",key: "editpassword",},{label: "退出系统",key: "logout",},]);// 选择操作let dialog = useDialog();// 确认登出let submitLogout = () => {userStore.logout().then(() => {router.push("/home");dialog.destroyAll();window.location.reload();});};// 取消登出let cancelLogOut = () => {dialog.destroyAll();};let handleSelect = (key, item) => {if (["userinfo", "editpassword"].includes(key)) {// 新增 taglet obj = { title: item.label, key };tagStore.addTag(obj);router.push(`/${key}`);} else {dialog.warning({closable: false,showIcon: false,style: {width: "20%",},title: () => {return h("div",{style: {position: "absolute",top: 0,left: 0,right: 0,textAlign: "center",height: "40px",lineHeight: "40px",background: "#cee6f0",color: "#1d69a3",fontWeight: "bold",fontSize: "16px",},},"退出登录");},content: () => {return h("p",{style: {textAlign: "center",height: "80px",lineHeight: "108px",color: "#000",fontSize: "14px",fontWeight: "bolder",userSelect: "none",},},"是否退出当前账号?");},action: () => {return h("div",{style: {width: "100%",display: "flex",justifyContent: "space-around",},},[h(NButton,{onClick: cancelLogOut,style: {width: "40%",},},{default: () => "取消",}),h(NButton,{onClick: submitLogout,type: "info",style: {width: "40%",},},{default: () => "退出",}),]);},});}};
</script><template><n-dropdowntrigger="click":options="baseOptions"@select="handleSelect"size="small"><div class="header-right_user-box"><div class="header-right_user-avatar"><img v-if="avatar" class="header-right_avatar" :src="avatar" /><svg-icon v-else name="avatar" width="35" height="35"></svg-icon></div><div class="header-right_user-name"><span>{{ userName }}</span><svg-icon name="down" width="10"></svg-icon></div></div></n-dropdown>
</template><style lang="scss" scoped>.header-right_user-box {display: flex;align-items: center;cursor: pointer;user-select: none;}.header-right_user-avatar {display: flex;align-items: center;justify-content: center;width: 40px;height: 40px;border-radius: 10px;overflow: hidden;img {width: 100%;height: 100%;object-fit: contain;}}.header-right_user-name {span {margin: 0 5px;}}
</style>

2.5 新增 src/pages/components/layout-tag.vue 页面标签 组件

<script setup>import { computed } from "vue";import { NTag } from "naive-ui";import { useTagStore } from "@/store/tag.js";import { usePermissionStore } from "@/store/permission.js";import router from "@/router/index.js";let tagStore = useTagStore();let permissionStore = usePermissionStore();let tags = computed(() => {return tagStore.tags;});function handleClose(key) {tagStore.removeTag(key);if (permissionStore.activeMenuValue == key) {permissionStore.activeMenuValue = tags.value[tagStore.activeTagIndex].key;router.push(`/${permissionStore.activeMenuValue}`);}}function handleCheck(item) {let { key } = item;permissionStore.activeMenuValue = key;router.push(`/${key}`);}
</script><template><div class="layout-header__tag"><n-tagv-for="item in tags":key="item.key"class="tag-item":closable="item.key !== 'home'":type="item.key == permissionStore.activeMenuValue ? 'success' : ''"size="small"@close="handleClose(item.key)"@click="handleCheck(item)">{{ item.title }}</n-tag></div>
</template><style lang="scss" scoped>.layout-header__tag {padding-left: 10px;display: flex;align-items: center;height: 30px;}.tag-item {margin-right: 5px;cursor: pointer;}
</style>

2.6 新增 src/pages/components/layout-content.vue 页面内容 组件

<script setup></script><template><div class="layout-content"><router-view v-slot="{ Component, route }"><transition name="mainFade" mode="out-in"><component :is="Component" :key="route.path"></component></transition></router-view></div>
</template><style lang="scss" scoped>.layout-content {padding: 20px;margin: 20px;// height: auto;height: calc(100vh - 170px);border: 1px solid #e9e9e9;border-radius: 5px;-webkit-box-shadow: rgba(0, 0, 0, 0.047) 0 0 5px;box-shadow: 0 0 5px rgba(0, 0, 0, 0.047);}.mainFade-enter-from {transform: translate(-80px);opacity: 0;}.mainFade-leave-to {transform: translate(80px);opacity: 0;}.mainFade-leave-from,.mainFade-enter-to {transform: translate(0px);opacity: 1;}.mainFade-enter-active {transition: all 0.1s ease;}.mainFade-leave-active {transition: all 0.1s cubic-bezier(1, 0.6, 0.6, 1);}
</style>

 

3. 创建如下内容页

src/pages/404.vue

src/pages/demo.vue

src/pages/eidtpassword.vue

src/pages/userinfo.vue

src/pages/system-dict.vue

src/pages/system-menu.vue

src/pages/user-user.vue

src/pages/user-role.vue

 页面基础结构如 src/pages/demo.vue

<script setup></script><template><div class="demo">demo</div>
</template><style lang="scss" scoped></style>

4. 编辑 src/router/baseRouter.js 完善静态路由

export default [{path: "",redirect: "/home",},{path: "/login",name: "login",component: () => import("@/pages/login.vue"),},// 所有未知页面都是404{path: '/:catchAll(.*)',component: () => import("@/pages/404.vue")},{component: () => import("@/pages/layout.vue"),children: [{path: '/userinfo',name: 'userinfo',meta: {title: '个人信息'},component: () => import("@/pages/userinfo.vue")},{path: '/editpassword',name: 'editpassword',meta: {title: '修改密码'},component: () => import("@/pages/editpassword.vue")},],}
];

 综上

layout 动态路由布局构建完成。下一章:基础框架搭建完了,后续完善到哪更新哪

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/59553.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux进程篇】进程概念(2)

【Linux进程篇】进程概念&#xff08;2&#xff09; 目录 【Linux进程篇】进程概念&#xff08;2&#xff09;进程状态Linux对进程的说法linux中的信号 进程状态查看Z(zombie)——僵尸进程僵尸进程的危害 孤儿进程 进程优先级基本概念查看系统进程PRI &#xff08;优先级priori…

wireshark 安装和使用

wireshark&#xff0c;世界上最受欢迎的网络协议分析器。是一个网络流量分析器&#xff0c;或“嗅探器”&#xff0c;适用于Linux、macOS、*BSD和其他Unix和类Unix操作系统以及Windows。它使用图形用户界面库Qt以及libpcap和npcap作为数据包捕获和过滤库。 wireshark&#xff…

解决Vs Code工具开发时 保存React文件时出现乱码情况

Vs Code工具开发时 保存React文件时出现乱码情况 插件库搜索:JS-CSS-HTML Formatter 把这个插件禁用或者卸载就解决保存时出现乱码的问题了; 如果没有解决,再看下面方案! 出现乱码问题通常是因为文件的编码格式不正确。您可以尝试以下解决方法&#xff1a; 确认文件编码格式&a…

【瑞吉外卖】Git部分学习

Git简介 Git是一个分布式版本控制工具&#xff0c;通常用来对软件开发过程中的源代码文件进行管理。通过Git仓库来存储和管理这些文件&#xff0c;Git仓库分为两种&#xff1a; 本地仓库&#xff1a;开发人员自己电脑上的Git仓库 远程仓库&#xff1a;远程服务器上的Git仓库…

回归预测 | MATLAB实现POA-CNN-LSTM鹈鹕算法优化卷积长短期记忆神经网络多输入单输出回归预测

回归预测 | MATLAB实现POA-CNN-LSTM鹈鹕算法优化卷积长短期记忆神经网络多输入单输出回归预测 目录 回归预测 | MATLAB实现POA-CNN-LSTM鹈鹕算法优化卷积长短期记忆神经网络多输入单输出回归预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现POA-CNN…

创建Springboot+vue3项目

项目概述创建springboot项目加入mybatis-plus支持1.加入依赖代码2.创建数据库实例3.yml文件的配置4.编写测试代码5.测试结果 创建vue项目报错错误一错误二错误三 项目概述 后端&#xff1a;Springboot、mybatis-plus、java 前端&#xff1a;nodejs、vue脚手架、element-ui 数据…

idea打开多个项目需要开多个窗口(恢复询问弹窗)

【版权所有&#xff0c;文章允许转载&#xff0c;但须以链接方式注明源地址&#xff0c;否则追究法律责任】【创作不易&#xff0c;点个赞就是对我最大的支持】 前言 仅作为学习笔记&#xff0c;供大家参考 总结的不错的话&#xff0c;记得点赞收藏关注哦&#xff01; 使用…

安卓证书生成教程

1.下载安装JDK文件&#xff08;如已安装请跳过&#xff09; 根据电脑系统版本下载JDK版本文件 下载地址&#xff1a;[https://www.oracle.com/java/technologies/downloads/](https://www.oracle.com/java/technologies/downloads/) 如果电脑上安装过JDK文件可以跳过2.生成密钥…

Celery嵌入工程的使用

文章目录 1.config 1.1 通过app.conf进行配置1.2 通过app.conf.update进行配置1.3 通过配置文件进行配置1.4 通过配置类的方式进行配置2.任务相关 2.1 任务基类(base)2.2 任务名称(name)2.3 任务请求(request)2.4 任务重试(retry) 2.4.1 指定最大重试次数2.4.2 设置重试间隔时间…

【数据结构】单链表

&#x1f525;博客主页&#xff1a;小王又困了 &#x1f4da;系列专栏&#xff1a;数据结构 &#x1f31f;人之为学&#xff0c;不日近则日退 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 目录 一、什么是链表 1.1链表的概念及结构 1.2单链表的结构 二、链表的实现 …

Elasticsearch官方测试数据导入

一、数据准备 百度网盘链接 链接&#xff1a;https://pan.baidu.com/s/1rPZBvH-J0367yQDg9qHiwQ?pwd7n5n 提取码&#xff1a;7n5n文档格式 {"index":{"_id":"1"}} {"account_number":1,"balance":39225,"firstnam…

C++的auto究竟是何方神圣

C的auto究竟是何方神圣 前言&#x1f64c;auto&#xff08;C 11&#xff09; 的使用细则auto是什么&#xff1f; auto声明的变量是在什么时期被编译器推导出来呢&#xff1f;为什么使用auto进行定义变量时&#xff0c;必须进行初始化&#xff1f; auto 的使用场景auto与指针和引…