《Vue 后台管理的优雅华章:小型实例鉴赏,2.0》

447 阅读10分钟

《Vue 后台管理的优雅华章:小型实例鉴赏,2.0》

上一篇文章写了路由和登录组件以及登录账号密码定义的一些规则,接下来我将完善登录成功后面的页面,并且将路由中的细节写出来。

1721616031490.png

这将是我登录成功后的页面,比较简陋,不过后续会再加以完善。

状态文件

image.png

permiss.js
import { defineStore } from "pinia";

export const usePermissStore = defineStore("permiss", {
  state: () => {
    const defaultList = {
      admin: [
        "0",
        "1",
        "11",
        "12",
        "13",
        "2",
        "21",
        "22",
        "23",
        "24",
        "25",
        "26",
        "27",
        "28",
        "29",
        "291",
        "292",
        "3",
        "31",
        "32",
        "33",
        "34",
        "4",
        "41",
        "42",
        "5",
        "7",
        "6",
        "61",
        "62",
        "63",
        "64",
        "65",
        "66",
      ],
      user: ["0", "1", "11", "12", "13"],
    };
    const username = localStorage.getItem("ms_name");
    return {
      key: username == "admin" ? defaultList.admin : defaultList.user,
      defaultList,
    };
  },
  actions: {
    handleSet(val) {
      this.key = val;
    },
  },
});

这段代码使用 Pinia 的 defineStore 函数创建了一个名为 permiss 的存储。

在 state 部分:

  • 定义了一个名为 defaultList 的对象,其中包含了 admin 和 user 两种角色的权限列表。

  • 通过 localStorage.getItem("ms_name") 获取用户名,并根据用户名决定将 defaultList.admin 或 defaultList.user 赋值给 key 。

在 actions 部分:

  • 定义了 handleSet 方法,用于设置 key 的值。
sidebar.js

// sidebar 模块的共享状态

import { defineStore } from 'pinia';
// 一个文件就是一个状态模块
export const useSidebarStore = defineStore('sidebar', {
    // state
    state: () => {
        return {
            collapse: false
        }
    },
    actions: {
        // 状态的改变
        handleCollapse() {
            this.collapse = !this.collapse
        }
    }
})

这段代码使用 Pinia 的 defineStore 函数创建了一个名为 sidebar 的状态存储模块。

在 state 部分:

定义了一个名为 collapse 的状态,初始值为 false。这个状态可能用于表示侧边栏的展开或折叠状态。

在 actions 部分:

定义了一个名为 handleCollapse 的方法。当调用这个方法时,它会将 collapse 状态的值取反,实现侧边栏展开/折叠状态的切换。

上面两个js文件使用的是Pinia状态管理库,使用 defineStore 方法创建store。

Pinia 中的 store 包含 state(状态)、getters(类似于计算属性)和 actions(方法)等概念。例如,定义一个具有 getter 的 store:

export const useCounterStore = defineStore('counter', {
  state: () => ({ count: 0 }), 
  getters: {
    doubleCount: (state) => state.count * 2, 
  },
  actions: {
    increment() {
      this.count++;
    },
  },
});

state存放的是状态,也可以理解我数据,getters(类似于计算属性)只计算,并不会改变值,actions(方法)主要就是用来修改值。

组件文件

image.png

用来呈存储数据

menu.js
export const menuData = [    {        id: '0',        title: '系统首页',        index: '/dashboard',        icon: 'Odometer',    },    {        id: '1',        title: '系统管理',        index: '1',        icon: 'HomeFilled',        children: [            {                id: '11',                pid: '1',                index: '/system-user',                title: '用户管理',            },            {                id: '12',                pid: '1',                index: '/system-role',                title: '角色管理',            },            {                id: '13',                pid: '1',                index: '/system-menu',                title: '菜单管理',            },        ],
    },
    {
        id: '2',
        title: '组件',
        index: '2-1',
        icon: 'Calendar',
        children: [
            {
                id: '21',
                pid: '3',
                index: '/form',
                title: '表单',
            },
            {
                id: '22',
                pid: '3',
                index: '/upload',
                title: '上传',
            },
            {
                id: '23',
                pid: '2',
                index: '/carousel',
                title: '走马灯',
            },
            {
                id: '24',
                pid: '2',
                index: '/calendar',
                title: '日历',
            },
            {
                id: '25',
                pid: '2',
                index: '/watermark',
                title: '水印',
            },
            {
                id: '26',
                pid: '2',
                index: '/tour',
                title: '分布引导',
            },
            {
                id: '27',
                pid: '2',
                index: '/steps',
                title: '步骤条',
            },
            {
                id: '28',
                pid: '2',
                index: '/statistic',
                title: '统计',
            },
            {
                id: '29',
                pid: '3',
                index: '29',
                title: '三级菜单',
                children: [
                    {
                        id: '291',
                        pid: '29',
                        index: '/editor',
                        title: '富文本编辑器',
                    },
                    {
                        id: '292',
                        pid: '29',
                        index: '/markdown',
                        title: 'markdown编辑器',
                    },
                ],
            },
        ],
    },
    {
        id: '3',
        title: '表格',
        index: '3',
        icon: 'Calendar',
        children: [
            {
                id: '31',
                pid: '3',
                index: '/table',
                title: '基础表格',
            },
            {
                id: '32',
                pid: '3',
                index: '/table-editor',
                title: '可编辑表格',
            },
            {
                id: '33',
                pid: '3',
                index: '/import',
                title: '导入Excel',
            },
            {
                id: '34',
                pid: '3',
                index: '/export',
                title: '导出Excel',
            },
        ],
    },
    {
        id: '4',
        icon: 'PieChart',
        index: '4',
        title: '图表',
        children: [
            {
                id: '41',
                pid: '4',
                index: '/schart',
                title: 'schart图表',
            },
            {
                id: '42',
                pid: '4',
                index: '/echarts',
                title: 'echarts图表',
            },
        ],
    },
    {
        id: '5',
        icon: 'Guide',
        index: '/icon',
        title: '图标',
        permiss: '5',
    },
    {
        id: '7',
        icon: 'Brush',
        index: '/theme',
        title: '主题',
    },
    {
        id: '6',
        icon: 'DocumentAdd',
        index: '6',
        title: '附加页面',
        children: [
            {
                id: '61',
                pid: '6',
                index: '/ucenter',
                title: '个人中心',
            },
            {
                id: '62',
                pid: '6',
                index: '/login',
                title: '登录',
            },
            {
                id: '63',
                pid: '6',
                index: '/register',
                title: '注册',
            },
            {
                id: '64',
                pid: '6',
                index: '/reset-pwd',
                title: '重设密码',
            },
            {
                id: '65',
                pid: '6',
                index: '/403',
                title: '403',
            },
            {
                id: '66',
                pid: '6',
                index: '/404',
                title: '404',
            },
        ],
    },
];

header.vue
<template>
  <header class="header">
    <div class="header-left">
      <img src="../assets/images/logo.svg" alt="" class="logo" />
      <div class="web-title">后台管理系统</div>
      <div class="collapse-btn" @click="collapseChange">
        <el-icon v-if="sidebarStore.collapse">
          <Expand></Expand>
        </el-icon>
        <el-icon v-else>
          <Fold></Fold>
        </el-icon>
      </div>
    </div>
    <div class="header-right">
      <el-avatar class="user-avator" :size="30" :src="imgurl"></el-avatar>
      <el-dropdown class="user-name" trigger="click" @command="handleCommand">
        <span class="el-dropdown-link">
          {{ username }}<el-icon class="el-icon--right"><arrow-down /></el-icon>
        </span>
        <!-- 具名插槽 -->
        <template #dropdown>
          <el-dropdown-menu>
            <a
              href="https://github.com/linxin/vue-manage-system"
              target="_black"
            >
              <el-dropdown-item>官方文档</el-dropdown-item>
            </a>
            <a
              href="https://github.com/linxin/vue-manage-system"
              target="_black"
            >
              <el-dropdown-item>项目仓库</el-dropdown-item>
            </a>
            <a
              href="https://github.com/linxin/vue-manage-system"
              target="_black"
            >
              <el-dropdown-item>我的世界</el-dropdown-item>
            </a>
            <el-dropdown-item divided command="loginOut"
              >退出登录</el-dropdown-item
            >
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </header>
</template>

<script setup>
import { useRouter } from "vue-router";
import { useSidebarStore } from "../store/sidebar.js";
import { ref, onMounted } from "vue";

const router = useRouter();
const sidebarStore = useSidebarStore();
const collapseChange = () => {
  sidebarStore.handleCollapse();
};
const imgurl = ref(
  "https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
);
const username = localStorage.getItem("ms_name") || "游客";

const handleCommand = (command) => {
  if ((command = "loginOut")) {
    localStorage.removeItem("ms_name");
    router.push("/login");
  }
};
onMounted(() => {
  if (document.body.clientWidth < 1500) {
    collapseChange();
  }
});
</script>

<style lang="css" scoped>
.header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  box-sizing: border-box;
  width: 100%;
  height: 70px;
  color: var(--header-text-color);
  background-color: var(--header-bg-color);
  border-bottom: 1px solid #ddd;
}

.header-left {
  display: flex;
  align-items: center;
  padding-left: 20px;
  height: 100%;
}

.logo {
  width: 35px;
}

.web-title {
  margin: 0 40px 0 10px;
  font-size: 22px;
}

.collapse-btn {
  display: flex;
  justify-content: center;
  align-items: center;
  height: 100%;
  padding: 0 10px;
  cursor: pointer;
  opacity: 0.8;
  font-size: 22px;
}

.collapse-btn:hover {
  opacity: 1;
}

.header-right {
  float: right;
  padding-right: 50px;
}

.header-user-con {
  display: flex;
  height: 70px;
  align-items: center;
}

.btn-fullscreen {
  transform: rotate(45deg);
  margin-right: 5px;
  font-size: 24px;
}

.btn-icon {
  position: relative;
  width: 30px;
  height: 30px;
  text-align: center;
  cursor: pointer;
  display: flex;
  align-items: center;
  color: var(--header-text-color);
  margin: 0 5px;
  font-size: 20px;
}

.btn-bell-badge {
  position: absolute;
  right: 4px;
  top: 0px;
  width: 8px;
  height: 8px;
  border-radius: 4px;
  background: #f56c6c;
  color: var(--header-text-color);
}

.user-avator {
  margin: 0 10px 0 20px;
}

.el-dropdown-link {
  color: var(--header-text-color);
  cursor: pointer;
  display: flex;
  align-items: center;
}

.el-dropdown-menu__item {
  text-align: center;
}
</style>

模板部分(<template>

  • 整体布局为一个页面头部,包含左右两部分。

    • 左边部分:依次展示了网站的 logo 图片、标题“后台管理系统”和一个用于控制侧边栏折叠状态的按钮。根据 sidebarStore.collapse 的值,显示不同的图标。
    • 右边部分:展示了用户的头像,以及一个点击触发的下拉菜单。下拉菜单中包含了链接和“退出登录”选项。
  • 事件处理:点击折叠按钮触发 collapseChange 函数,下拉菜单的命令触发 handleCommand 函数。

脚本部分(<script setup>

  • 引入了必要的模块和函数,包括路由 useRouter、侧边栏状态 useSidebarStore 以及 Vue 的 ref 和 onMounted 。

  • 定义了以下变量和函数:

    • router:用于页面路由的操作。

    • sidebarStore:获取侧边栏的状态和操作方法。

    • collapseChange:调用侧边栏的折叠操作方法。

    • imgurl:通过 ref 定义的图片 URL 响应式数据。

    • username:从本地存储获取用户名,如果未获取到则默认为“游客”。

    • handleCommand:处理下拉菜单的命令,如果是“loginOut”,则清除本地存储的用户名并跳转到登录页面。

    • onMounted 钩子:在组件挂载时,如果页面宽度小于 1500 像素,执行折叠侧边栏的操作。

样式部分(<style>

  • 定义了头部相关元素的样式:

    • .header:整体头部的布局、宽度、高度、颜色、背景、边框等样式。
    • .header-left:左侧部分的布局和内边距。
    • .logo:logo 图片的宽度。
    • .web-title:标题的边距和字体大小。
    • .collapse-btn:折叠按钮的样式,包括布局、鼠标悬停效果等。
    • .header-right:右侧部分的浮动和内边距。
    • 还定义了一些其他相关元素的样式,如用户头像、下拉菜单链接等。

补充:插槽

在模板21行开始使用了插槽,它可以被看作是子组件中预留的占位符,父组件可以在这些占位符中填充具体的内容,包括数据、HTML 代码、组件等。

插槽有三种:

默认插槽(或匿名插槽) :没有指定 name 属性的<slot>就是默认插槽。

具名插槽:带有 name 属性的<slot>即为具名插槽。

作用域插槽:是一种带有 props 数据的插槽。子组件通过在<slot>上绑定数据,将数据传递给父组件使用。

我使用的是具名插槽,插槽的使用方式如下:

子组件模板中定义插槽


<template>
  <div>
    <!-- 具名插槽 -->
    <slot name="content">我是插槽默认的内容,当父组件不填充任何内容时,我这句话才会出现</slot> 
  </div>
</template>

父组件中使用子组件并填充插槽内容:

<template>
  <div>
    <child>
      <template v-slot:content> 
        <div>这是父组件在子组件中填充的内容,在子组件中显示</div> 
        <img src="https://s3.ax1x.com/2021/01/16/srjlq0.jpg" alt=""/>
      </template>
    </child>
  </div>
</template>
sidebar.vue
<template>
  <aside class="sidebar">
    <el-menu
      class="sidebar-el-menu"
      :collapse="sidebar.collapse"
      background-color="#324157"
      text-color="#bfcbd9"
      :defalut-active="onRoutes"
      router
    >
      <template v-for="item in menuData">
        <template v-if="item.children">
          <el-sub-menu
            :index="item.index"
            :key="item.index"
            v-permiss="item.id"
          >
            <template #title>
              <el-icon>
                <component :is="item.icon"></component>
              </el-icon>
              <span>{{ item.title }}</span>
            </template>
            <template v-for="subItem in item.children">
              <el-sub-menu
                v-if="subItem.children"
                :index="subItem.index"
                :key="subItem.index"
                v-permiss="item.id"
              >
                <template #title>{{ subItem.title }}</template>
                <el-menu-item
                  v-for="(threeItem, i) in subItem.children"
                  :key="i"
                  :index="threeItem.index"
                >
                  {{ threeItem.title }}
                </el-menu-item>
              </el-sub-menu>
              <el-menu-item v-else :index="subItem.index" v-permiss="item.id">
                {{ subItem.title }}
              </el-menu-item>
            </template>
          </el-sub-menu>
        </template>
        <template v-else>
          <el-menu-item
            :index="item.index"
            :key="item.index"
            v-permiss="item.id"
          >
            <el-icon>
              <component :is="item.icon"></component>
            </el-icon>
            <template #title>{{ item.title }}</template>
          </el-menu-item>
        </template>
      </template>
    </el-menu>
  </aside>
</template>
<script setup>
import { useRoute } from "vue-router";
import { useSidebarStore } from "../store/sidebar";
import { menuData } from "./menu";

const sidebar = useSidebarStore();
const onRoutes = () => {
  return route.path;
};
</script>

模板部分(<template>

  • 定义了一个侧边栏 <aside class="sidebar"> ,其中使用了 el-menu 组件来构建菜单结构。

    • :collapse="sidebar.collapse" 根据 sidebar 状态决定菜单是否折叠。

    • 设置了背景颜色和文字颜色。

    • 通过 :default-active="onRoutes" 与路由关联,以确定默认选中的菜单项。

    • 使用 router 属性启用路由模式。

    • 通过 v-for 循环遍历 menuData 来动态生成菜单项和子菜单项。

    • 根据菜单项是否有子项,分别使用 el-sub-menu 或 el-menu-item 来展示。对于有子项的菜单项,还会进一步嵌套子菜单项的生成。

脚本部分(<script setup>

  • 引入了 useRoute 用于获取当前路由信息,useSidebarStore 用于获取和操作侧边栏的状态。
  • 引入了 menuData 用于提供菜单的数据。
  • sidebar = useSidebarStore() 获取侧边栏的状态。
  • onRoutes 函数用于返回当前路由的路径,以便与菜单的默认选中状态关联。

src文件

element-user.js
import {
    ElButton,
    ElForm,
    ElFormItem,
    ElInput,
    ElCheckbox,
    ElLink,
    ElIcon,
    ElAvatar,
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    ElMenu,
    ElSubMenu,
    ElMenuItem
} from 'element-plus';
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
// console.log(ElementPlusIconsVue);
const components = [ElButton, ElForm,
    ElFormItem, ElInput, ElCheckbox,
    ElLink, ElIcon, ElAvatar, ElDropdown,
    ElDropdownMenu, ElDropdownItem, ElMenu,
    ElSubMenu, ElMenuItem];
export default (app) => {
    components.forEach((component) => {
        app.use(component);
    })
    for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
        app.component(key, component)
    }
}

以下是对代码的详细解释:

首先,引入了 Element Plus 中的一系列组件,如 ElButtonElForm 等。

然后,定义了一个包含这些组件的数组 components 。

在 export default 导出的函数中,通过遍历 components 数组,使用 app.use(component) 来注册这些组件。

同时,通过遍历 ElementPlusIconsVue 对象的键值对,使用 app.component(key, component) 来注册其中的图标组件。

这样做的目的是让整个应用能够方便地使用 Element Plus 提供的组件和图标,提高开发效率和代码的复用性。

main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia' // 凤梨
// 引入Vue组件库    70%的组件有组件库提供了
//  组件库依赖的样式
import 'element-plus/dist/index.css'
import './assets/styles/variable.css'
import App from './App.vue'
import ElementUse from './element-use.js'
import router from './router/index.js'
import * as DemoData from './test'
// console.log(DemoData);
// console.log(Object.entries(DemoData));


const app = createApp(App);
app.use(createPinia());
ElementUse(app);
app.use(router);
import { usePermissStore } from './store/permiss.js'

const permissStore = usePermissStore();
// 自定义指令
app.directive('permiss', {
    // el dom , binding 绑定的属性
    mounted(el, binding) {
        if (binding.value && !permissStore.key.includes(String(binding.value))) {
            el['hidden'] = true;
        }
    }
})
app.mount('#app');

这段 Vue 代码主要完成了以下几个重要的操作:

  1. 引入必要的模块和资源:

    • 从 'vue' 导入 createApp 函数来创建 Vue 应用实例。
    • 从 'pinia' 导入 createPinia 用于状态管理。
    • 引入 Element Plus 组件库的样式和自定义的样式文件。
    • 引入根组件 App.vue 、路由配置 router 以及一个用于注册 Element Plus 组件的模块 ElementUse 。
    • 引入一些测试数据 DemoData 。
  2. 创建和配置 Vue 应用:

    • 使用 createApp(App) 创建应用实例并赋值给 app 变量。
    • 使用 app.use(createPinia()) 启用 Pinia 状态管理。
    • 调用 ElementUse(app) 注册 Element Plus 组件。
    • 使用 app.use(router) 应用路由配置。
  3. 定义自定义指令 'permiss' :

    • 在指令的 mounted 钩子函数中,根据条件判断是否隐藏元素。如果 binding.value 存在且不在 permissStore.key 数组中,就将元素隐藏。

总结

总的来说, 这个组件通过巧妙的模板和脚本结合,实现了侧边栏的灵活构建和与路由的集成,为用户提供了一个动态且功能丰富的侧边栏菜单体验。