系统管理两层tab页前端优化设计(vue3)


前言

通过路由和vuex可以实现各模块跳转及菜单统一管理。每模块页面根据命名规范,实现分别管理。本文将假设三类模块:车辆、人员、物品来举例说明。
在这里插入图片描述


一、建立路由

在这里插入图片描述

index.js

import { createRouter, createWebHistory } from 'vue-router'
import car from './modules/car';
import person from './modules/person';
import things from './modules/things';

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('../views/HomeView.vue'),
    meta: {
      title: '首页',
    }
  },
  {
    path: '/system',
    name: 'system',
    redirect: '/car/royce',
    component: () => import('@/components/layout.vue'),
    meta: {
      title: '系统管理'
    },
    children: [
      {
        path: '/car',
        name: 'car',
        redirect: '/car/royce',
        component: () => import('@/components/leftNav.vue'),
        meta: {
          title: '车辆管理'
        },
        children: [...car]
      },
      {
        path: '/person',
        name: 'person',
        redirect: '/person/teacher',
        component: () => import('@/components/leftNav.vue'),
        meta: {
          title: '人员管理'
        },
        children: [...person]
      },
      {
        path: '/things',
        name: 'things',
        redirect: '/things/pen',
        component: () => import('@/components/leftNav.vue'),
        meta: {
          title: '物品管理'
        },
        children: [...things]
      },
      {
        path: '/map',
        name: 'map',
        component: () => import('../views/map.vue'),
        meta: {
          title: '地图管理'
        }
      },
    ]
  }
]

const router = createRouter({
  history: createWebHistory(process.env.BASE_URL),
  routes
})

router.beforeEach((to, from, next) => {
  if (to.meta.title) {
    document.title = to.meta.title
  }
  next()
});

router.afterEach((to, from) => {
  if (to.meta.title) {
    document.title = to.meta.title
  }
})

export default router

car.js

export default [
  {
    path: '/car/royce',
    name: 'car.royce',
    component: () => import('@/views/Car/royce/index.vue'),
    meta: {
      title: 'royce'
    }
  },
  {
    path: '/car/bentley',
    name: 'car.bentley',
    component: () => import('@/views/Car/bentley/index.vue'),
    meta: {
      title: 'bentley'
    }
  },
  {
    path: '/car/ferrari',
    name: 'car.ferrari',
    component: () => import('@/views/Car/ferrari/index.vue'),
    meta: {
      title: 'ferrari'
    }
  },
  {
    path: '/car/lamborghini',
    name: 'car.lamborghini',
    component: () => import('@/views/Car/lamborghini/index.vue'),
    meta: {
      title: 'lamborghini'
    }
  },
  {
    path: '/car/bugatti',
    name: 'car.bugatti',
    component: () => import('@/views/Car/bugatti/index.vue'),
    meta: {
      title: 'bugatti'
    }
  },
  {
    path: '/car/maybach',
    name: 'car.maybach',
    component: () => import('@/views/Car/maybach/index.vue'),
    meta: {
      title: 'maybach'
    }
  },
  {
    path: '/car/maserati',
    name: 'car.maserati',
    component: () => import('@/views/Car/maserati/index.vue'),
    meta: {
      title: 'maserati'
    }
  },
]

person.js

export default [
  {
    path: '/person/teacher',
    name: 'person.teacher',
    component: () => import('@/views/Person/teacher/index.vue'),
    meta: {
      title: 'teacher'
    }
  },
  {
    path: '/person/doctor',
    name: 'person.doctor',
    component: () => import('@/views/Person/doctor/index.vue'),
    meta: {
      title: 'doctor'
    }
  },
  {
    path: '/person/engineer',
    name: 'person.engineer',
    component: () => import('@/views/Person/engineer/index.vue'),
    meta: {
      title: 'engineer'
    }
  },
  {
    path: '/person/designer',
    name: 'person.designer',
    component: () => import('@/views/Person/designer/index.vue'),
    meta: {
      title: 'designer'
    }
  },
  {
    path: '/person/writer',
    name: 'person.writer',
    component: () => import('@/views/Person/writer/index.vue'),
    meta: {
      title: 'writer'
    }
  },
]

things.js

export default [
  {
    path: '/things/pen',
    name: 'things.pen',
    component: () => import('@/views/Things/pen/index.vue'),
    meta: {
      title: 'pen'
    }
  },
  {
    path: '/things/book',
    name: 'things.book',
    component: () => import('@/views/Things/book/index.vue'),
    meta: {
      title: 'book'
    }
  },
  {
    path: '/things/table',
    name: 'things.table',
    component: () => import('@/views/Things/table/index.vue'),
    meta: {
      title: 'table'
    }
  },
  {
    path: '/things/umbrella',
    name: 'things.umbrella',
    component: () => import('@/views/Things/umbrella/index.vue'),
    meta: {
      title: 'umbrella'
    }
  },
  {
    path: '/things/glasses',
    name: 'things.glasses',
    component: () => import('@/views/Things/glasses/index.vue'),
    meta: {
      title: 'glasses'
    }
  },
  {
    path: '/things/headphones',
    name: 'things.headphones',
    component: () => import('@/views/Things/headphones/index.vue'),
    meta: {
      title: 'headphones'
    }
  }
]

二、tab菜单页面

在这里插入图片描述

1.一级菜单页面 layout.vue

<template>
  <div class="main">
    <div class="header">
      <template v-for="item in menuList" :key="item.path">
        <div class="menuItem" :class="{ active: isActive(item) }" @click="router.push({ path: item.path })">
          {{ item.name }}
        </div>
      </template>
    </div>
    <router-view :key="route.fullPath" class="content"></router-view>
  </div>
</template>

<script setup>
import { ref, reactive, onMounted } from 'vue'
import { useRouter, useRoute } from "vue-router";
import { useStore } from "vuex";

const store = useStore();
const route = useRoute();
const router = useRouter();

// 菜单
let menuList = reactive([
  { name: "车辆管理", path: "/car" },
  { name: "人员管理", path: "/person" },
  { name: "物品管理", path: "/things" },
  { name: "地图管理", path: "/map" },
]);

const isActive = (item) => route.path.includes(item.path);


</script>

<style lang="scss" scoped>
.main {
  width: 100vw;
  height: 100vh;
  color: #fff;
  .header {
    width: 100%;
    height: 3.75rem;
    line-height: 3.75rem;
    padding: 0 0.625rem;
    display: flex;
    justify-content: space-evenly;
    background: linear-gradient(270deg, #3695fd 0%, #295acc 99%);

    .menuItem {
      height: 100%;
      cursor: pointer;
      padding: 0 1rem;
      color: #fff;
      font-size: 1rem;
      &:hover {
        background: rgba(0, 0, 0, 0.1);
        border-bottom: 0.125rem solid #00d4ff;
      }
      &.active {
        background: rgba(0, 0, 0, 0.1);
        border-bottom: 0.125rem solid #00d4ff;
      }
    }
  }
  .content {
    height: calc(100% - 3.75rem);
    width: 100%;
    display: flex;
  }
}
</style>

2.二级菜单页面(左侧竖行) leftNav.vue

<template>
  <div class="system_main">
    <div class="menu_list">
      <template v-for="item in menuList" :key="item.id">
        <div class="menu_item" :class="{ active: isActive(item) }" @click="handleLink(item)">
          <span>
            <p>{{ item.name }}</p>
          </span>
        </div>
      </template>
    </div>
    <div class="menu_main">
      <HistoryMenu />
      <div style="height:calc(100% - 2.5rem);overflow-y: auto;">
        <router-view></router-view>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch, computed } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import HistoryMenu from './HistoryMenu.vue'
import { useStore } from 'vuex'
import car from '@/router/modules/car';
import person from '@/router/modules/person';
import things from '@/router/modules/things';

const router = useRouter();
const route = useRoute();
const store = useStore()
const tabHistory = computed(() => store.state.tabHistory)

const menuList = ref([])

const moduleMappings = {
  '/car/': car,
  '/person/': person,
  '/things/': things,
};

const getModulesMenu = () => {
  const currentPath = route.path;
  for (const path in moduleMappings) {
    if (currentPath.includes(path)) {
      const dataSource = moduleMappings[path];
      menuList.value = dataSource.reduce((acc, item, index) => {
        const arr = item.name.split('.');
        if (arr.length === 2) {
          acc.push({
            id: index,
            name: item.meta.title,
            path: item.path,
            pathName: arr[1],
          });
        }
        return acc;
      }, []);
      if (menuList.value.length > 0) {
        handleTabHistory(route.path, menuList.value[0])
      }
      break;
    }
  }
};

const handleLink = row => {
  router.push({ path: row.path });
  handleTabHistory(row.path, row);
};

const handleTabHistory = (curPath, addTab) => {
  const existsIndex = tabHistory.value.findIndex(tab => curPath.includes(tab.path));
  if (existsIndex === -1) {
    store.commit('addTabHistory', addTab)
  }
}

const isActive = item => route.path.includes(item.path)

onMounted(() => {
  getModulesMenu();
});
</script>

<style lang='scss' scoped>
.system_main {
  overflow: hidden;
  .menu_list {
    width: 5.625rem;
    height: 100%;
    overflow-y: auto;
    background: #122c52;
    box-shadow: 0px 3px 8px 0px rgba(0, 0, 0, 0.16);
    float: left;
    .menu_item {
      height: 6.25rem;
      text-align: center;
      cursor: pointer;
      font-size: 0.875rem;
      &:hover {
        background: #1a427c;
      }
      span {
        display: inline-block;
        position: relative;
        top: 3.125rem;
        transform: translateY(-50%);
        img {
          width: 3.125rem;
          height: 3.125rem;
        }
        p {
          color: #fff;
          margin-top: -0.625rem;
        }
      }
      &.active {
        background: #1a427c;
      }
    }
  }
  .menu_main {
    width: calc(100% - 5.625rem);
    height: 100%;
    color: #000;
  }
}
</style>

3.历史页面记录(内容上方可关闭的tab) HistoryMenu.vue

<template>
  <div class="historyMenu flex justify-start items-center">
    <template v-for="(item, index) in tabHistory" :key="index">
      <div :class="['tabMenu', { 'activeTag': route.path.includes(item.path) }]" @click="router.push({ path: item.path })">
        <div class="name">{{item.name}}</div>
        <el-icon v-if="tabHistory.length !== 1" class="close" @click.stop="handleClose(item,index)">
          <Close />
        </el-icon>
      </div>
    </template>
    <el-tooltip effect="dark" content="返回" placement="bottom">
      <div @click="router.go(-1)" style="cursor: pointer;position: absolute;right: .3125rem;height: 2.1875rem">
        <img src="@/assets/back.png" alt="" style="height: 100%;">
      </div>
    </el-tooltip>
  </div>
</template>

<script setup>
import { useRoute, useRouter } from 'vue-router'
import { ref, computed } from 'vue'
import { useStore } from 'vuex'

const store = useStore()
const route = useRoute()
const router = useRouter()

const tabHistory = computed(() => store.state.tabHistory)

const handleClose = (item, index) => {
  store.commit('removeTabHistory', index)
  // 删除当前选中菜单
  if (route.path == item.path) {
    if (index === tabHistory.value.length) { // 从后面删
      router.push({ path: tabHistory.value[tabHistory.value.length - 1].path })
    } else if (index === 0) {     // 从前面删
      router.push({ path: tabHistory.value[0].path })
    } else {    // 从中间删
      router.push({ path: tabHistory.value[index ].path })
    }
  }
}

</script>


<style lang="scss" scoped>
.historyMenu {
  height: 2.1875rem;
  padding: 0;
  background: rgba(245, 245, 245, 1);
  border-bottom: 0.125rem solid #3a83fd;
  border-radius: 0.3125rem 0.3125rem 0 0;
  position: relative;
  margin-bottom: 0.625rem;
  .tabMenu {
    height: 100%;
    position: relative;
    display: inline-block;
    cursor: pointer;
    font-size: 0.875rem;
    font-weight: 400;
    color: #213d64;
    background: none;
    &:before {
      content: "";
      position: absolute;
      bottom: 0;
      left: 0;
      border-left: 0.625rem solid rgba(245, 245, 245, 1);
      border-right: 0.625rem solid rgba(245, 245, 245, 1);
      border-bottom: 2rem solid #fff;
      border-top: none;
      width: 100%;
      height: 0;
    }
    .name {
      position: relative;
      padding: 0rem 1.375rem;
      text-align: center;
      z-index: 1;
      line-height: 2.1875rem;
    }

    .close {
      position: absolute;
      top: 0.2rem;
      right: 0.625rem;
      width: 0.625rem;
      height: 0.625rem;
      line-height: 0.625rem;
      text-align: center;
      border-radius: 50%;
      font-size: 0.625rem;
      color: #409eff;
      cursor: pointer;
      z-index: 2;
      &:hover {
        background: #409eff;
        color: #fff;
      }
    }

    &.activeTag {
      color: #fff;
      &:before {
        border-bottom-color: rgba(58, 131, 253, 1);
      }
      .close {
        background: transparent;
        color: #fff;
      }
    }
  }
}
.rotate-scale-enter-active,
.rotate-scale-leave-active {
  transition: all 0.3s ease;
}
.rotate-scale-enter,
.rotate-scale-leave-to {
  transform: scaleX(0);
  opacity: 0;
}
</style>

三、页面内容

在这里插入图片描述

1.App.vue

<template>
  <router-view />
</template>

2.HomeView.vue

<template>
  我是首页
  <div @click="router.push({name:'system'})" class="btn"> 去系统管理</div>
</template>

<script setup>
import router from '@/router';
</script>

<style lang='scss' scoped>
.btn {
  color: skyblue;
  text-decoration: solid;
  cursor: pointer;
}
</style>

3.map.vue

该页面为测试页面,主要用于测试只使用一级菜单,不使用二级菜单的情况。

4.其他页面

其他页面都是统一的代码。

<template>
  {{route.meta.title}}
</template>

<script setup>
import { useRoute } from 'vue-router';
const route = useRoute()
</script>


四.vuex

在这里插入图片描述

index.js

import { createStore } from 'vuex'
import getters from "./getters";
import mutations from "./mutations";
import actions from "./actions";
import createPersistedState from 'vuex-persistedstate'

export default createStore({
  state: {
    tabHistory: [],  // 历史tab
  },
  getters,
  mutations,
  actions,
  modules: {},
  plugins: [
    createPersistedState({
      storage: window.localstorage,
      paths: ['tabHistory']
    })
  ]
})

mutations.js

export default {

  addTabHistory(state, tab) {
    state.tabHistory.push(tab)
    if (state.tabHistory.length > 12) {
      state.tabHistory.splice(0, 1)
    }
  },
  removeTabHistory(state, tabIndex) {
    state.tabHistory.splice(tabIndex, 1)
  },

}

五.在此基础上建立一个左侧有二级菜单的版本(结合elementplus的el-menu,可建立多级)

elementplus----el-menu

leftNav.vue

      <el-menu router :collapse-transition="true" :default-active="route.path" class="el-menu-vertical-demo" unique-opened="true">
        <template v-for="(item, index) in menuList">
          <el-menu-item v-if="!item.children" :index="item.router" :key="item.id" @click="e=>getInfo(e,item)">
            <img :src="require(`@/assets/img/layout/${item.icon}${isActive(item)? 'active':''}.png`)" alt="">
            <span>{{ item.name }}</span>
          </el-menu-item>
          <el-sub-menu v-else :index="item.router" :key="item.id" class="sub-menu">
            <template #title>
              <div class="menu_item_left">
                <img :src="require(`@/assets/img/layout/${item.icon}${isActive(item)? 'active':''}.png`)" alt="">
                <span>{{ item.name }}</span>
              </div>
            </template>
            <el-menu-item v-for="j in item.children" :index="j.router" :key="j.id" class="sub-menu-item" @click="e=>getInfo(e,item)">
              <img :src="require(`@/assets/img/layout/${j.icon}.png`)" alt="">
              <span>{{ j.name }}</span>
            </el-menu-item>
          </el-sub-menu>
        </template>
      </el-menu>
const getInfo = (e, item) => {
  let obj = {}
  if (item.children) {
    obj = item.children.filter(i => i.router == e.index).map(j => ({ name: j.name, path: j.router }))[0]
  } else {
    obj = { name: item.name, path: e.index }
  }

  handleTabHistory(e.index, obj);
}

const handleTabHistory = (curPath, addTab) => {
  const existsIndex = tabHistory.value.findIndex(tab => curPath.includes(tab.path));
  if (existsIndex === -1) {
    store.commit('addTabHistory', addTab)
  }
}
<style lang='scss' scoped>
::v-deep {
  .el-menu-vertical-demo {
    margin-top: 4px;
  }
  .el-menu {
    border-right: 0;
  }
  .el-sub-menu .el-sub-menu__icon-arrow {
    position: relative;
    top: 3px;
    right: 0;
    background: url("@/assets/img/layout/arrow.png") center center no-repeat;
    &:before {
      content: "";
      visibility: hidden;
    }
    svg {
      display: none;
    }
  }
  .sub-menu .el-sub-menu__title {
    display: flex;
    align-items: center;
    justify-content: space-between;
    .menu_item_left {
      display: flex;
      align-items: center;
    }
  }

  .el-menu.el-menu-vertical-demo .el-menu-item,
  .el-sub-menu__title {
    width: calc(100% - 8px);
    margin: 0 4px;
    padding: 0 10px 0 16px !important;
    height: 40px;
    line-height: 40px;
    border-radius: 2px;
    cursor: pointer;

    font-size: 14px;
    color: #656c83;
    &.is-active {
      background: #cee3ff;
      color: #2487ff;
      font-weight: 500;
    }
    img {
      width: 14px;
      height: 14px;
      margin-right: 8px;
    }
  }
  .sub-menu-item.el-menu-item {
    font-weight: 350;
    font-size: 14px;
    color: #3b4e73;
    margin: 8px 8px 8px 20px;
    width: calc(100% - 28px);
    height: 24px;
    line-height: 24px;
    cursor: pointer;

    border-radius: 2px 2px 2px 2px;
    &.active,
    &:hover {
      background: #f0f6ff;
      color: #2487ff;
    }
    img {
      margin: 0 4px 0 16px;
      width: 12px;
      height: 12px;
    }
  }
}
</style>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值