效果:
![](https://img-blog.csdnimg.cn/direct/ca9b7096175d4746b6deb51d838a3a0d.png)
vue2:
创建layout/index.vue
<template>
<el-container>
<el-container>
<el-aside :width="isCollapse ? '65px' : '200px'">
<Menu :isCollapse.sync="isCollapse"></Menu>
</el-aside>
<el-main class="main">
<el-header>
<Header :isCollapse.sync="isCollapse"></Header>
</el-header>
<el-main class="minMain">
<el-tabs
v-model="tabsEditableTabsValue"
type="card"
@tab-remove="removeTab"
@tab-click="onTab"
>
<el-tab-pane
v-for="item in editableTabs"
:closable="item.closable"
:key="item.name"
:label="item.title"
:name="item.name"
>
</el-tab-pane>
</el-tabs>
<router-view class="routerView"></router-view
></el-main>
</el-main>
</el-container>
</el-container>
</template>
<script>
import Header from "./components/header.vue";
import Menu from "./components/menu.vue";
import { mapState } from "vuex";
export default {
components: { Header, Menu },
data() {
return {
isCollapse: false,
};
},
methods: {
removeTab(targetName) {
let tabs = this.editableTabs;
let activeName = this.tabsEditableTabsValue;
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
let nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeName = nextTab.name;
}
}
});
}
this.$store.commit("menuStore/setEditableTabsValue", activeName);
this.tabs = tabs.filter((tab) => tab.name !== targetName);
this.$store.commit("menuStore/setEditableTabs", this.tabs);
let dd = tabs.find((i) => i.name == activeName);
this.$store.commit("menuStore/setBreadcrumbe", {
path: dd.name,
title: dd.title,
});
this.$router.push(dd.name);
},
onTab(event) {
this.$router.push(event._props.name);
this.$store.commit("menuStore/setBreadcrumbe", {
path: event._props.name,
title: event._props.label,
});
},
},
computed: {
...mapState("menuStore", ["editableTabs", "editableTabsValue"]),
tabsEditableTabsValue: {
get() {
return this.editableTabsValue;
},
set(value) {
this.$store.commit("menuStore/setEditableTabsValue", value);
},
},
},
};
</script>
<style scoped lang="scss">
.el-container {
height: 100vh;
.el-aside {
color: #333;
background-color: #fcfcfd;
}
.main {
padding: 0;
background-color: #f5f5f5;
.el-header {
color: #333;
background-color: #fff;
}
.minMain {
background-color: #fff;
margin: 20px;
padding: 0;
height: calc(100% - 100px);
box-sizing: border-box;
.routerView {
padding: 0 10px;
box-sizing: border-box;
}
}
}
}
</style>
创建layout/components/header.vue
<template>
<div class="container dfcc">
<div class="left">
<i
:class="isCollapse ? 'el-icon-s-unfold' : 'el-icon-s-fold'"
class="icon"
@click="onIsCollapse"
></i>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/schedule' }"
>schedule</el-breadcrumb-item
>
<el-breadcrumb-item>{{
$store.state.menu.breadcrumb.title
}}</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="right dfcc">
<el-dropdown>
<i class="el-icon-setting"></i>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item>查看</el-dropdown-item>
<el-dropdown-item>新增</el-dropdown-item>
<el-dropdown-item>删除</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-avatar
size="medium"
src="../../../assets/img/2.png"
style="margin: 0 10px"
></el-avatar>
<span>王小虎</span>
</div>
</div>
</template>
<script>
export default {
props: ["isCollapse"],
data() {
return {};
},
methods: {
onIsCollapse() {
this.$emit("update:isCollapse", !this.isCollapse);
},
},
};
</script>
<style scoped lang="scss">
.container {
margin: 0;
height: 100%;
.left {
flex: 1;
display: flex;
justify-content: start;
align-items: center;
font-size: 25px;
.icon {
margin-right: 20px;
}
.el-breadcrumb {
font-size: 18px;
line-height: 1;
}
}
.right {
width: 180px;
}
}
</style>
创建layout/components/menu.vue
<template>
<div class="container">
<el-menu
class="el-menu-vertical-demo"
:default-active="editableTabsValue"
router
:collapse="isCollapse"
>
<img
src="../../../assets/img/2.png"
alt=""
style="width: 100%; height: 60px"
/>
<el-menu-item
v-for="i in list"
:key="i.id"
:index="i.path"
@click="onMenuItem(i)"
>
<i :class="i.icon"></i>
<span slot="title">{{ i.title }}</span>
</el-menu-item>
</el-menu>
</div>
</template>
<script>
import { mapState } from "vuex";
export default {
props: ["isCollapse"],
data() {
return {
dialogVisible: false,
dialogVisibleTeam: false,
list: [
{
id: 1,
title: "dashboard",
icon: "el-icon-setting",
path: "/dashboard",
},
{
id: 2,
title: "authority",
icon: "el-icon-setting",
path: "/authority",
},
{
id: 3,
title: "role",
icon: "el-icon-setting",
path: "/role",
},
{
id: 4,
title: "project",
icon: "el-icon-setting",
path: "/project",
},
{
id: 5,
title: "info",
icon: "el-icon-setting",
path: "/info",
},
{
id: 6,
title: "task",
icon: "el-icon-setting",
path: "/task",
},
{
id: 7,
title: "schedule",
icon: "el-icon-setting",
path: "/schedule",
},
],
};
},
methods: {
onMenuItem(i) {
this.$store.commit("menu/setBreadcrumbe", {
path: i.path,
title: i.title,
});
this.$store.commit("menu/setEditableTabsValue", i.path);
const newTab = {
title: i.title,
name: i.path,
closable: true,
};
const mergedTabs = [...this.editableTabs, newTab];
const uniqueTabs = mergedTabs.reduce((acc, current) => {
let existingItem = acc.find((item) => item.name === current.name);
if (!existingItem) {
acc.push(current);
}
return acc;
}, []);
this.$store.commit("menu/setEditableTabs", uniqueTabs);
},
},
computed: {
...mapState("menu", ["editableTabs", "editableTabsValue"]),
},
};
</script>
<style scoped lang="scss">
.container {
height: 100%;
}
</style>
创建store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import menuStore from './modules/menuStore'
import userStore from './modules/userStore'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
},
getters: {
},
mutations: {
},
actions: {
},
modules: {
menuStore,
userStore,
},
plugins: [createPersistedState()]
})
创建store/modules/menuStore.js
export default {
namespaced: true,
state: {
breadcrumb: {},
editableTabsValue: "/schedule",
editableTabs: [
{
title: "日程",
name: "/schedule",
closable: false,
}
]
},
mutations: {
setBreadcrumbe(state, value) {
state.breadcrumb = value
},
setEditableTabsValue(state, val) {
state.editableTabsValue = val;
},
setEditableTabs(state, val) {
state.editableTabs = val
}
},
actions: {
},
}
已经在日程页面,再次点击出现bug
![](https://img-blog.csdnimg.cn/direct/17f4d78961464bc2a2a447d25d58ff67.png)
解决 router/index.vue
import Vue from "vue";
import VueRouter from "vue-router";
Vue.use(VueRouter);
// 解决vue路由重复导航错误
// 获取原型对象上的push函数
const originalPush = VueRouter.prototype.push
//修改原型对象中的push方法
VueRouter.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
const routes = [
{
path: "/login",
name: "login",
component: () => import("../views/login/index.vue"),
},
{
path: "/",
name: "layout",
redirect: "/dashboard",
component: () => import("../views/layout/index.vue"),
children: [
{
path: "/dashboard",
component: () => import("../views/dashboard/index.vue"),
},
{
path: "/authority1",
component: () => import("../views/authority/index.vue"),
},
{
path: "/role1",
component: () => import("../views/role1/index.vue"),
},
{
path: "/project1",
component: () => import("../views/project1/index.vue"),
},
{
path: "/info1",
component: () => import("../views/info1/index.vue"),
},
{
path: "/task1",
component: () => import("../views/task1/index.vue"),
},
{
path: "/schedule1",
component: () => import("../views/schedule1/index.vue"),
},
],
},
{
path: "*",
name: "404",
component: () => import("../views/404/index.vue"),
},
];
const router = new VueRouter({
routes,
});
export default router;
vue3:
创建layout/index.vue
<template>
<el-container class="boxBGC">
<el-aside :width="isToggle ? '64px' : '200px'">
<Aside v-model:isToggle="isToggle"></Aside>
</el-aside>
<el-container class="miniContainer">
<el-header>
<Header v-model:isToggle="isToggle"></Header>
</el-header>
<el-main>
<el-tabs
v-model="useMenu.editableTabsValue"
type="card"
class="demo-tabs"
@tab-click="handleTabClick"
@tab-remove="removeTab"
>
<el-tab-pane
v-for="(item, index) in useMenu.breadcrumbs"
:key="index"
:label="item.title"
:name="item.name"
:closable="item.closable"
>
</el-tab-pane>
</el-tabs>
<router-view />
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import { computed, ref } from "vue";
import { useRouter } from "vue-router";
import { useMenuStore } from "../../store/menu";
import { useUserStore } from "../../store/user";
import Aside from "./components/aside.vue";
import Header from "./components/header.vue";
const router = useRouter();
const isToggle = ref(false);
const userStore = useUserStore();
const useMenu = useMenuStore();
const removeTab = (targetName) => {
const tabs = useMenu.breadcrumbs;
let activeName = useMenu.editableTabsValue;
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeName = nextTab.name;
}
}
});
}
router.push(activeName);
useMenu.setEditableTabsValue(activeName);
useMenu.breadcrumbs = tabs.filter((tab) => tab.name !== targetName);
useMenu.setBreadcrumbs(useMenu.breadcrumbs);
let matchedTab = tabs.find((tab) => tab.name === activeName);
useMenu.setTitlePath({
path: matchedTab.name,
title: matchedTab.title,
});
};
const handleTabClick = (event) => {
useMenu.setTitlePath({
path: event.props.name,
title: event.props.label,
});
router.push(event.props.name);
};
</script>
<style scoped lang="scss">
.boxBGC {
background-color: #f5f5f5;
}
.el-container {
height: 98vh;
}
.miniContainer {
height: calc(100vh - 20px);
box-sizing: border-box;
}
.el-header {
background-color: #fff;
}
.el-main {
background-color: #fff;
margin: 20px;
margin-bottom: 0;
box-sizing: border-box;
}
.el-aside {
background-color: #fff;
}
.footer {
display: flex;
justify-content: center;
height: 100%;
align-items: center;
}
</style>
创建layout/components/header.vue
<template>
<div class="container">
<div class="disac">
<el-icon class="elIcons" @click="toggle">
<component :is="isToggle ? 'Expand' : 'Fold'" />
</el-icon>
<el-breadcrumb separator="/">
<el-breadcrumb-item :to="{ path: '/' }">首页</el-breadcrumb-item>
<el-breadcrumb-item>
{{ menuStore.titlePath?.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</div>
<div class="disac">
<el-icon @click="handleFullScreen">
<FullScreen />
</el-icon>
<el-avatar
:size="50"
src="../../../assets/1.png"
style="margin: 0 10px 0 40px"
/>
<el-dropdown>
<span class="el-dropdown-link">
张三
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>1</el-dropdown-item>
<el-dropdown-item>2</el-dropdown-item>
<el-dropdown-item>3</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
import { ref, computed, reactive, watch } from "vue";
import { useMenuStore } from "../../../store/menu";
import { useRouter } from "vue-router";
import { useUserStore } from "../../../store/user";
import { ElMessage } from "element-plus";
const menuStore = useMenuStore();
const userStore = useUserStore();
const router = useRouter();
menuStore.breadcrumbs[0]?.title == "首页"
? menuStore.breadcrumbs
: menuStore.setBreadcrumbs([
{
closable: false,
name: "/home",
title: "首页",
},
]);
const props = defineProps({
isToggle: {
type: Boolean,
},
});
const emits = defineEmits(["update:isToggle"]);
const isToggle = computed({
get() {
return props.isToggle;
},
set(val) {
emits("update:isToggle", val);
},
});
const lastItem = ref({});
watch(
() => menuStore.breadcrumbs.slice(-1)[0],
(newLastItem) => {
lastItem.value = newLastItem;
}
);
const toggle = () => {
emits("update:isToggle", !isToggle.value);
};
// 全屏
const isFullScreen = ref(false);
const handleFullScreen = () => {
const element = document.documentElement;
if (!document.fullscreenElement) {
// 如果当前不是全屏状态,进入全屏
if (element.requestFullscreen) {
element.requestFullscreen();
} else if (element.mozRequestFullScreen) {
element.mozRequestFullScreen();
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen();
} else if (element.msRequestFullscreen) {
element.msRequestFullscreen();
}
} else {
// 如果当前是全屏状态,退出全屏
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.mozCancelFullScreen) {
document.mozCancelFullScreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
} else if (document.msExitFullscreen) {
document.msExitFullscreen();
}
}
};
</script>
<style scoped lang="scss">
.disac {
display: flex;
align-items: center;
}
.container {
display: flex;
align-items: center;
justify-content: space-between;
padding-top: 5px;
.elIcons {
font-size: 25px;
margin-right: 10px;
}
}
.example-showcase .el-dropdown-link {
cursor: pointer;
color: var(--el-color-primary);
display: flex;
align-items: center;
}
.el-dropdown-link:focus {
outline: none;
}
</style>
创建layout/components/menu.vue
<template>
<el-scrollbar>
<el-menu
:collapse="props.isToggle"
:default-active="$route.path"
class="el-menu-vertical-demo"
unique-opened
router
>
<img
src="../../../assets/1.png"
alt=""
style="width: 100%; height: 60px"
/>
<el-sub-menu
:index="item.path"
v-for="item in menuStore.menuList"
:key="item.path"
>
<template #title>
<component style="width: 20px; margin-right: 10px" :is="item.icon" />
<span>{{ item.name }}</span>
</template>
<el-menu-item
:index="i.path"
style="padding-left: 50px"
v-for="(i, index) in item.children"
:key="index.path"
@click="handleMenuItemClick(i)"
>
{{ i.title }}
</el-menu-item>
</el-sub-menu>
</el-menu>
</el-scrollbar>
</template>
<script setup>
import { onMounted, ref, toRefs } from "vue";
import { useRouter } from "vue-router";
import { useMenuStore } from "../../../store/menu";
const router = useRouter();
const menuStore = useMenuStore();
const props = defineProps(["isToggle"]);
const editableTabs = ref([]);
const handleMenuItemClick = (menuItem) => {
menuStore.setTitlePath({
path: menuItem.path,
title: menuItem.title,
});
if (
menuItem.title !== "首页" &&
!menuStore.breadcrumbs.some((tab) => tab.title === menuItem.title)
) {
menuStore.breadcrumbs.push({
closable: true,
name: menuItem.path,
title: menuItem.title,
});
}
menuStore.setEditableTabsValue(menuItem.path);
menuStore.setBreadcrumbs(menuStore.breadcrumbs);
};
</script>
<style scoped lang="scss">
.el-menu {
height: 100%;
background-color: #ffffff;
}
</style>
创建store/index.js
import { createPinia } from "pinia";
import persistedstate from 'pinia-plugin-persist'
const store = createPinia()
store.use(persistedstate)
export default store
export * from './menu.js'
创建store/modules/menu.js
import { defineStore } from "pinia";
import { ref } from "vue";
export const useMenuStore = defineStore(
"menu",
() => {
// 左侧菜单栏
const menuList = ref([]);
const setMenuList = (val) => {
menuList.value = val;
};
const delMenuList = () => {
menuList.value = undefined;
};
// tabs导航
const breadcrumbs = ref([
{
closable: false,
name: "/home",
title: "首页",
},
]);
const setBreadcrumbs = (val) => {
breadcrumbs.value = val;
};
const delBreadcrumbs = () => {
breadcrumbs.value = [];
};
// 顶部导航面包屑
const titlePath = ref({});
const setTitlePath = (val) => {
titlePath.value = val;
};
const delTitlePath = () => {
titlePath.value = {};
};
const editableTabsValue = ref("/home");
const setEditableTabsValue = (val) => {
editableTabsValue.value = val;
};
const delEditableTabsValue = () => {
editableTabsValue.value = "";
};
return {
menuList,
setMenuList,
delMenuList,
breadcrumbs,
setBreadcrumbs,
delBreadcrumbs,
titlePath,
setTitlePath,
delTitlePath,
editableTabsValue,
setEditableTabsValue,
delEditableTabsValue,
};
},
{
persist: {
enabled: true,
strategies: [
{
storage: localStorage, //存储的位置,默认为sessionStorage
// path: ['info'] //需要存储的state状态,默认为所有
},
],
},
}
);