likes
comments
collection
share

Vue 实战——后台管理系统(二)

作者站长头像
站长
· 阅读数 56

引言

基本主页框架

我们从创建主页的基本结构开始。这涉及设置主要布局,并整合诸如 Header.vueSidebar.vue 等子组件。 首先我们先写一个主页的基本框架。 Home.vue

<template>
    <div class="wrapper">
        <v-header />
        <v-sidebar />
        <main class="content-box" :class="{'content-collapse':sidebarStore.Collapse}">
            <router-view />
        </main>
        
    </div>
</template>

<script setup>
        import vHeader from '../components/header.vue';
        import vSidebar from '../components/sidebar.vue';
        // 转译成了 v-header
        import { useSidebarStore } from '../store/sidebar';

        const sidebarStore = useSidebarStore();


</script>

<style  >
    /* 没 scoped 就是全局样式 */
    .wrapper{
        height: 100vh;
        overflow: hidden;
    }
    .content-box {
    position: absolute;
    left: 250px;
    right: 0;
    top: 70px;
    bottom: 0;
    padding-bottom: 30px;
    -webkit-transition: left 0.3s ease-in-out;
    transition: left 0.3s ease-in-out;
    background: #eef0fc;
    overflow: hidden;
}

.content {
    width: auto;
    height: 100%;
    padding: 20px;
    overflow-y: scroll;
    box-sizing: border-box;
}

.content::-webkit-scrollbar {
    width: 0;
}

.content-collapse {
    left: 55px;
}

</style>

这段代码展示了一个基本的主页布局,包括了 HeaderSidebar 组件,用于去渲染头部栏和侧边导航栏,以及一个主要内容区域用于展示路由视图。

设计头部组件

header.vue 是一个包含网站头部的 Vue 组件,主要功能包括显示 logo、标题、用户头像、下拉菜单等。

首先,我们在 components 文件夹下创建一个头部组件 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="collaseChange">
                <el-icon v-if="sidebarStore.Collapse">
                    <Expand />
                </el-icon>
                <el-icon v-else>
                    <Fold />
                </el-icon>
            </div>
        </div>
        <div class="header-right">
            <el-avatar class="user-avator" :size="30" :src="imgurl" />
            <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>
                <!-- 具名插槽 -->
                <!-- 定位到 dropdown里 -->
                <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>

                        <el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>

                    </el-dropdown-menu>
                </template>

            </el-dropdown>
        </div>
    </header>
</template>

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

const router = useRouter()

const sidebarStore = useSidebarStore()

const collaseChange = () => {
    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){
            collaseChange()


        }



    })


</script>

<style scoped>
.header {
    display: flex;
    justify-content: space-between;
    align-items: center;
    box-sizing: border-box;
    width: 100%;
    height: 70px;
    /* css变量  CSS4新语法  方便后面修改 */
    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>

在头部栏主要就两个版块:

  • Collapse 按钮:用于切换侧边栏的展开/收起状态。

  • 用户信息:显示用户头像和名称,并提供下拉菜单,允许用户访问项目仓库、官方文档和退出登录。

而用户信息部分主要是用了两种插槽去实现。 默认插槽具名插槽

默认插槽

默认插槽定义

默认插槽是最基础的插槽类型,它允许父组件向子组件传递内容。子组件没有指定名称的插槽部分,接收的内容会被渲染在子组件中预留的位置。

在项目中的使用

默认插槽里插入下拉菜单的触发元素(用户名称和箭头图标)

Vue 实战——后台管理系统(二)

<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>
</el-dropdown>

其中 span 标签中的 usernameel-icon 便是插槽里的元素,也是显示在头部栏的。

具名插槽

具名插槽定义

具名插槽允许你定义多个插槽,并为每个插槽指定一个名称。在父组件中,你可以通过插槽名称来插入特定内容到子组件中的指定位置。

在项目中的使用

具名插槽用于定义下拉菜单的内容。#dropdown 是具名插槽的名称,它会插入到 el-dropdown 组件的特定位置:

<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>
        <el-dropdown-item divided command="loginout">退出登录</el-dropdown-item>
    </el-dropdown-menu>
</template>

将具体内容(项目仓库、官方文档、退出登录)插入到下拉菜单,需要点击下拉菜单才能展开看到。 Vue 实战——后台管理系统(二)

且我们在 el-dropdown 组件上,使用了 @command 事件监听器来监听菜单项的点击事件,并将其传递给 handleCommand 方法。

<el-dropdown class="user-name" trigger="click" @command="handleCommand">

handleCommand 方法中,根据传递的 command 值来执行特定的逻辑。在这个案例中,handleCommand 方法检查 command 是否等于 'loginout',如果是,则执行退出登录操作。

const handleCommand = (command) => {
    if (command === 'loginout') {
        localStorage.removeItem('ms_name'); // 移除存储的用户信息
        router.push('/login'); // 重定向到登录页面
    }
}

侧边栏状态

我们在头部栏还设计了一个 Collapse 按钮。用于切换侧边栏的展开/收起状态。 我们设计了一个切换状态的函数 collaseChange ,并将其绑定成了按钮的响应事件,当点击后会调用我们存在 sidebar.js 仓库中的改变状态的函数,去改变全局的状态。

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

const router = useRouter()
const sidebarStore = useSidebarStore()

const collaseChange = () => {
    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) {
        collaseChange()
    }
})
</script>

这里有一点值得注意,我们用了钩子函数,在挂载后就去判断客户端的屏幕宽度是否小于设定的值,小于则把侧边栏设为折叠状态。做前端页面这些细节很重要!

数据管理

我们在store目录下再新建一个 sidebar.js 去存放侧边栏的状态。 我们使用 defineStore 来创建一个 Pinia 状态管理模块。这个模块包含了侧边栏的状态和操作方法:

// sidebar 模块的共享状态

import { defineStore } from 'pinia'
// 一个文件就是一个状态模块

export const useSidebarStore = defineStore('sidebar',{
        // 有点类似计算属性
        state:() =>{
            return {
                Collapse: false
            }
        },
        actions:{
                // 状态的改变
                handleCollapse(){
                    this.Collapse =!this.Collapse
                }
        }
})

我们定义了一个代表侧边栏展开或收缩的状态,handleCollapse 方法用于切换 Collapse 状态,便于在 header.vue 组件中去调用改变侧边栏展开或收缩状态。

设计侧边栏

sidebar.vue 组件负责渲染侧边栏的 UI,包括菜单项和子菜单。它通过 Pinia 状态管理模块来控制侧边栏的折叠和展开状态。以下是该组件的详细讲解:

<template>
    <aside class="sidebar">
        <el-menu
            class="sidebar-el-menu"
            :collapse="sidebar.collapse"
            background-color="#324157"
            text-color="#bfcbd9"
            :default-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 { useSidebarStore } from '../store/sidebar'
import { menuData } from './menu'
import {useRoute} from 'vue-router'

const sidebar = useSidebarStore()

const onRoutes = () =>{
    return route.path
}

</script>

我们用 <el-menu>Element Plus 提供的菜单组件,用于构建侧边栏。

菜单组件

:collapse="sidebar.collapse" 绑定 collapse 属性,以控制菜单的折叠状态。这个属性从 Pinia 状态管理模块 sidebar 中获取。:default-active="onRoutes" 绑定当前激活的菜单项,通过 onRoutes 函数获取当前路由路径,用于高亮显示当前选中的菜单项。

菜单项渲染

递归渲染是一种常见的编程模式,特别是在处理树状数据结构时。在这个侧边栏组件中,我们通过递归方式来渲染多级菜单项。 首先我们解构 menu.js 中的存放菜单数据的数组 menuData

初始模板遍历

<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">
                <!-- 递归渲染子菜单项 -->
            </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>

在上述代码中,首先通过 v-for 指令遍历 menuData 数组,对每个菜单项 item 进行渲染。

判断子菜单

<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">
            <!-- 递归渲染子菜单项 -->
        </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 v-if="item.children"> :如果当前菜单项 item 有子菜单,则渲染 el-sub-menu 组件,并递归遍历其子菜单 item.children

  • <template v-else> :如果当前菜单项 item 没有子菜单,则直接渲染 el-menu-item 组件。

递归渲染子菜单项

<template v-for="subItem in item.children">
    <template v-if="subItem.children">
        <el-sub-menu :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>
    </template>
    <el-menu-item v-else :index="subItem.index" v-permiss="item.id">
        {{ subItem.title }}
    </el-menu-item>
</template>
  • <template v-for="subItem in item.children"> :在 el-sub-menu 内部,再次通过 v-for 指令遍历 item.children,并对每个子菜单项 subItem 进行渲染。

  • <template v-if="subItem.children"> :如果当前子菜单项 subItem 还有子菜单,则渲染嵌套的 el-sub-menu 组件,并递归遍历其子菜单 subItem.children

  • <template v-else> :如果当前子菜单项 subItem 没有子菜单,则直接渲染 el-menu-item 组件。

Vue 实战——后台管理系统(二)

选择性渲染有权限菜单

值得留意的是我们在这里用了 v-permissv-permiss 是一个自定义指令,用于控制菜单项的显示与否,具体取决于用户的权限。

我们一般在主函数中定义并且注册。

app.directive('permiss', {
    mounted(el, binding) {
        if (binding.value && !permissStore.key.includes(String(binding.value))) {
            el['hidden'] = true
        }
    }
})

这里定义了自定义指令 v-permiss,用于控制元素的显示与否。该指令在元素被挂载到 DOM 上时触发,通过检查绑定值(权限 ID)和用户权限列表,如果用户没有相应权限,则将元素隐藏。

Vue 实战——后台管理系统(二)

主函数

完整的主函数如下

import { createApp } from 'vue'
import {createPinia}  from 'pinia'
// 引入Vue组件库  70%的组件有组件库提供了
import { 
    ElButton, 
    ElForm,
    ElFormItem,
    ElInput,
    ElCheckbox,
    ElLink,
    ElIcon,
    ElAvatar,
    ElDropdown,
    ElDropdownMenu,
    ElDropdownItem,
    ElMenu,
    ElMenuItem,
    ElSubMenu
} from 'element-plus'
// 组件库依赖的样式
import 'element-plus/dist/index.css'
import App from './App.vue'
import router from './router'
// * as 引入全部组件库
import * as ElementPlusIconsVue from '@element-plus/icons-vue'
import './assets/styles/variable.css'

console.log(ElementPlusIconsVue,'///');


 const app = createApp(App)
    
 // 注册全部组件库
 for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
    // app.component()这个函数需要的两个参数是 key 和 component ,用entries 将对象拆分成这样的格式去注册
 }

 app
    .use(createPinia())
    .use(router)
    .use(ElButton)   
    .use(ElForm)
    .use(ElFormItem)
    .use(ElInput)
    .use(ElCheckbox)
    .use(ElLink)
    .use(ElIcon)
    .use(ElAvatar)
    .use(ElDropdown)
    .use(ElDropdownMenu)
    .use(ElDropdownItem)
    .use(ElMenu)
    .use(ElMenuItem)
    .use(ElSubMenu)
    
   // 自定义指令
   import { usePermissStore } from './store/permiss'
   const permissStore = usePermissStore();
   app.directive('permiss',{
      // v-if  v-show  el 承载指令的节点  binding  绑定的属性,就是传递的值
      mounted(el,binding){
            if(binding.value && !permissStore.key.includes(String(binding.value))){
               el['hidden'] = true
            }
      }
   })


    app
       .mount('#app')

此外主函数还有些特色之处。 在项目的 main.js 中,可以看到以下两种引入方法的应用:

引入全部组件

import * as ElementPlusIconsVue from '@element-plus/icons-vue'
  • import * as ... from '' 是一种将模块中所有导出内容一次性引入的方式。

  • 这种方法将 @element-plus/icons-vue 模块中所有的图标组件都导入到 ElementPlusIconsVue 对象中。这个对象的每个属性都是一个图标组件,属性名是组件的名称。

逐个解构引入

import { ElButton, ElForm, ElFormItem, ElInput, ElCheckbox, ElLink, ElIcon, ElAvatar, ElDropdown, ElDropdownMenu, ElDropdownItem, ElMenu, ElMenuItem, ElSubMenu } from 'element-plus'
  • 这种方法用于从 element-plus 模块中按需引入特定的组件。

  • 我们逐个指定了要引入的组件,如 ElButtonElForm 等,这种方法可以避免引入不必要的组件,减少最终打包后的文件体积。

Object.entries

我们还使用了 Object.entries ,Object.entries 是 ES6 新增的一个方法,用于遍历对象。它返回一个数组,数组中的每个元素都是一个 [key, value] 数组,代表对象的键值对。

for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
    app.component(key, component)
}
  • 这里的 Object.entries 被用来将 ElementPlusIconsVue 对象中的每个图标组件遍历出来。
  • for...of 循环配合 Object.entries 使得可以将每个 [key, component] 对作为参数传递给 app.component 方法,从而在 Vue 应用中全局注册所有的图标组件。

这些亮点可以丰富你的项目,且很好地体现出了对八股文的掌握。

总结

在本篇文章中,我们对后台管理系统的首页布局进行了详细讲解,并实现了以下几个关键点:

  1. 基本主页框架

    • 我们首先创建了 Home.vue 组件,定义了主页的基本结构。这个结构包括 HeaderSidebar 组件,以及一个主要内容区域来展示路由视图。
  2. 头部组件设计

    • header.vue 组件包括了网站的 logo、标题、用户头像和下拉菜单。我们使用了默认插槽和具名插槽来实现用户信息部分的灵活展示,同时设计了一个切换侧边栏状态的按钮,并通过 Pinia 管理侧边栏的折叠状态。
  3. 侧边栏组件设计

    • sidebar.vue 组件负责渲染侧边栏的菜单项和子菜单。我们通过递归渲染多级菜单项,并利用 Pinia 状态管理模块来控制侧边栏的折叠和展开状态。
  4. 数据管理

    • 我们在 store 目录下创建了 sidebar.js 文件,使用 Pinia 管理侧边栏的状态,并通过 defineStore 方法创建了一个状态模块来控制侧边栏的折叠状态。

通过本篇文章,我们不仅学习了如何设计和实现一个高效的后台管理系统首页,还深入了解了 Vue 的组件化思想和状态管理的最佳实践。希望这些内容能帮助你构建更加优雅和功能丰富的后台管理系统。如果这篇文章对你有帮助可以点个赞哦😊。

转载自:https://juejin.cn/post/7397274001066213385
评论
请登录