vue2,vue3 的 NavMenu 导航菜单,Tabs 标签页与Breadcrumb 面包屑

效果:

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

解决   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状态,默认为所有
        },
      ],
    },
  }
);

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值