A05-Layout布局

个人主页Silence Lamb
本章内容:【Layout布局


Silence-Vue v1.0.0

基于VUE前端快速开发框架


  • vue-element-admin 中大部分页面都是基于这个 layout 的
  • 除了个别页面如:login , 404, 401 等页面没有使用该layout
  • 如果你想在一个项目中有多种不同的layout也是很方便的
  • 只要在一级路由那里选择不同的layout组件就行
// No layout
{
  path: '/401',
  component: () => import('errorPage/401')
}

// Has layout
{
  path: '/documentation',

  // 你可以选择不同的layout组件
  component: Layout,

  // 这里开始对应的路由都会显示在app-main中 如上图所示
  children: [{
    path: 'index',
    component: () => import('documentation/index'),
    name: 'documentation'
  }]
}

一、内容区域

1.1【通用常量】

  • 通用常量:theme/variables.scss
/*整体页面背景*/
$bodyBg: #ffffff;
/*侧边框宽度*/
$sideBarWidth: 210px;
/*侧边框背景*/
$sideBarBg: rgba(240, 240, 244, 0.98);

/*菜单背景*/
$menuBg: #f0f0f4;
/*菜单鼠标悬停背景*/
$menuHoverBg: rgba(95, 105, 112, 0.44);
/*菜单激活背景*/
$menuActiveBg: rgba(105, 114, 119, 0.47);

/*子菜单背景*/
$subMenuBg: rgba(105, 114, 119, 0.99);
/*子菜单鼠标悬停背景*/
$subMenuHoverBg: rgba(105, 114, 119, 0.99);
/*子菜单激活背景*/
$subMenuActiveBg: rgba(105, 114, 119, 0.99);
/*菜单字体颜色*/
$menuTextColor: #ffffff;
/*菜单字体选中状态的颜色*/
$menuTextActive: #ffffff;
/*菜单字体鼠标悬停的颜色*/
$menuTextHover: #e3ae11;

:export {
  /*整体页面背景*/
  bodyBg: $bodyBg;
  /*侧边框宽度*/
  sideBarWidth: $sideBarWidth;
  /*侧边框背景*/
  sideBarBg: $sideBarBg;
  /*侧边栏激活背景*/
  menuActiveBg: $menuActiveBg;
  /*侧边栏鼠标悬停背景*/
  menuHoverBg: $menuHoverBg;
  /*侧边栏字体颜色*/
  menuTextColor: $menuTextColor;
  /*侧边栏字体选中状态的颜色*/
  menuTextActive: $menuTextActive;
  /*菜单背景*/
  menuBg: $menuBg;
  /*子菜单背景*/
  subMenuBg: $subMenuBg;
  /*子菜单鼠标悬停背景*/
  subMenuHoverBg: $subMenuHoverBg;
  /*子菜单激活背景*/
  subMenuActiveBg:$subMenuActiveBg;
  /*菜单字体鼠标悬停的颜色*/
  menuTextHover: $menuTextHover;
}

1.2【AppMain】

  • AppMain:AppMain.vue
    • 该组件提供了一种灵活且可重复使用的方式来管理 Vue.js 应用程序的主要内容
    • 同时使用 Vue Router 进行导航
computed 属性 key 用于为 router-view 组件提供唯一标识符。这确保当 URL 发生更改时,router-view 组件被重新渲染,而不是重用先前的组件实例。

transition 组件用于在内容更改时应用过渡效果。在本例中,使用 breadcrumb 过渡和 out-in 模式,这意味着出站内容先动画出,然后入站内容动画进入。
<template>
  <section class="app-main">
    <router-view v-slot="{ Component }">
      <transition name="fade-transform" mode="out-in">
        <component :is="Component" />
      transition>
    router-view>
  section>
template>

<script>
export default {
  name: 'AppMain',
}
script>
<style lang="scss" scoped>
@import "src/styles/theme/variables/index.scss";
.app-main {
  /*50 = navbar  */
  min-height: calc(100vh - #{$sideBarWidth});
  width: 100%;
  position: relative;
  overflow: hidden;
}

style>

<style lang="scss">
// fix css style bug in open el-dialog
.el-popup-parent--hidden {
  .fixed-header {
    padding-right: 15px;
  }
}
style>

A05-Layout布局_第1张图片

二、侧边栏菜单

2.1【Vuex状态管理】

  • ☃️状态信息管理:app.js
import Cookies from 'js-cookie'
//可能是在一个 Vuex store 中作为状态的初始值进行定义
const state = {
    sidebar: {
        //包含侧边栏信息的对象,包括侧边栏是否已打开或关闭,以及是否有动画效果
        opened: Cookies.get('sidebarStatus') ? !!+Cookies.get('sidebarStatus') : true,
        //它表示侧边栏是否需要动画效果。如果设置为 true,那么在打开或关闭侧边栏时将没有过渡效果,直接切换状态
        //这可以提高页面渲染性能,特别是在较慢的设备上。如果设置为 false,那么在打开或关闭侧边栏时将启用过渡动画    
        withoutAnimation: false
    },
    //标识设备类型,这里设为“desktop”(电脑)
    device: 'desktop'
}
//用于存储更改应用程序状态的函数,这些函数通常在 actions 中被调用
const mutations = {
    //可以用来切换侧边栏打开或关闭状态,并且在切换状态时可以选择是否加入动画效果,同时会将状态保存到 cookie 中
    TOGGLE_SIDEBAR: state => {
        state.sidebar.opened = !state.sidebar.opened
        state.sidebar.withoutAnimation = false
        if (state.sidebar.opened) {
            Cookies.set('sidebarStatus', 1)
        } else {
            Cookies.set('sidebarStatus', 0)
        }
    },
    //用于手动关闭侧边栏,并且可以选择是否要加入动画效果
    CLOSE_SIDEBAR: (state, withoutAnimation) => {
        Cookies.set('sidebarStatus', 0)
        state.sidebar.opened = false
        state.sidebar.withoutAnimation = withoutAnimation
    },
    //用于切换设备类型
    //一个状态对象,包含了应用程序中所有的状态信息
    //一个表示设备类型的字符串参数。该参数用于切换设备类型
    TOGGLE_DEVICE: (state, device) => {
        state.device = device
    }
}
//用于存储异步操作的函数,通常用于执行一些异步操作
const actions = {
    toggleSideBar({ commit }) {
        commit('TOGGLE_SIDEBAR')
    },
    closeSideBar({ commit }, { withoutAnimation }) {
        commit('CLOSE_SIDEBAR', withoutAnimation)
    },
    toggleDevice({ commit }, device) {
        commit('TOGGLE_DEVICE', device)
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}
  • ⏰定义了一个名为 sidebar 的 getter:getter.js
//  Vuex store 中的 getters 对象,用于派生一些新的状态信息
const getters = {
  sidebar: state => state.app.sidebar,
}
export default getters

2.2【实现路由跳转】

  • 标签接收一个 is 属性,用于指定动态组件的类型:appLink.vue
<template>
    
    <component :is="type()" v-bind="linkProps(to)">
      
      <slot />
    component>
  template>
  <script>
  import { isExternal } from '@/utils/tool/validate'
  export default {
    name: 'App-Link',
    props: {
      to: {
        type: String,
        required: true
      }
    },
    computed: {
      //是否是外部链接:如果是返回true,反正返回false
      isExternal() {
        return isExternal(this.to)
      },
      //用于指定动态组件  的类型 
      type() {
        if (this.isExternal) {
          //如果这个组件是一个外部链接, 则返回字符串a
          return 'a'
        }
        //表示使用 Vue.js 内置的路由链接组件 
        //这个属性通常用于指定需要渲染的组件类型
        return 'router-link'
      }
    },
    methods: {
      linkProps(to) {
         //即当前链接是一个外部链接,则将要传递给组件的是一个包含 href、target 和 rel 三个属性的对象
        if (this.isExternal) {
          return {
            href: to,//href 属性的值就是传入的 to 参数,表示链接的地址
            target: '_blank',//target 属性设为 _blank,表示链接将在新标签页中打开
            rel: 'noopener'//是为了防止在打开链接的新标签页中,被链接页面的 JavaScript 访问到原先页面的 window.opener 属性,进而导致安全隐患
          }
        }
        //如果 isExternal 为假,即当前链接是一个内部路由链接,则将要传递给组件的是一个包含 to 属性的对象
        return {
          to: to//表示内部路由链接的目标地址
        }
      }
    }
  }
  script>

2.3【渲染菜单图标】

  • 渲染菜单图标和标题:item.vue
<script>
export default {
    name: 'MenuItem',
    functional: true,
    props: {
        icon: {
            type: String,
            required: false
        },
        title: {
            type: String,
            required: false
        }
    },
    render() {
        console.log(this.icon)
        console.log(this.title)
        var icon = this.icon
        var title = this.title
        const vnodes = []

        if (icon) {
            if (icon.includes('el-icon')) {
                vnodes.push(<i class={[icon, 'sub-el-icon']} />)
            } else {
                vnodes.push(<svg-icon icon-class={icon} />)
            }
        }

        if (title) {
            vnodes.push(<span slot='title'>{(title)}</span>)
        }
        return vnodes
    }
}
script>

<style scoped>

style>

2.4【菜单栏组件化】

  • 创建渲染菜单栏的组件:sidebarItem.vue

在这里插入图片描述

  • 添加一个 fallback ‘resolve.fallback: { “path”: require.resolve(“path-browserify”) }’
  • 安装 ‘path-browserify’ 模块 如果您不想包含任何 polyfill,可以像以下代码一样使用一个空模块: resolve.fallback: { “path”: false }
template>
  
  <div v-if="!item.hidden">
    
    <template
        v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
      
      <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path)">
        
        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'sub-menu-title-noDropdown': !isNest }">
          
          <span><item :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>span>
          <template #title>
            <item :title="onlyOneChild.meta.title"/>
          template>
        el-menu-item>
      app-link>
    template>
    
    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
      
      
      <template v-if="item.meta" #title>
        <item :icon="item.meta && item.meta.icon"/>
        <span> <item :title="item.meta.title"/>span>
      template>
      <sidebar-item
          v-for="child in item.children"
          :key="child.path"
          :base-path="resolvePath(child.path)"
          :is-nest="true"
          :item="child"
          class="nest-menu"
      />
    el-sub-menu>
  div>
template>
  • 引入其他组件组件:sidebarItem.vue
import path from 'path';
import { isExternal } from '@/utils/tool/validate';
import AppLink from './appLink'
import ItemVue from './item'
export default {
    name: 'Sidebar-Item',
    components: { AppLink, ItemVue },
    //表示一个路由对象,这个属性是必须的
    props: {
        item: {
            type: Object,
            required: true
        },
        //用于表示当前节点是否在嵌套子级中,默认为 false,即不在嵌套子级中,可以通过传入 true 来修改该属性
        isNest: {
            type: Boolean,
            default: false
        },
        //表示当前路由的基本路径,如果组件需要根据路由生成一些链接,则需要使用该属性
        basePath: {
            type: String,
            default: ''
        }
    },
    data() {
        this.onlyOneChild = null
        return {}
    },

    methods: {
        // 检查 item.children 中是否至少有一个子节点的 hidden 属性为 false,如果是,则返回 true,否则返回 false
        hasOneShowingChild(children = [], parent) {
            const showingChildren = children.filter(item => {
                if (item.hidden) {
                    return false
                } else {
                    // 临时设置(当只有一个子路由显示时将会被使用)
                    this.onlyOneChild = item
                    return true
                }
            })
            // 当只有一个子路由时,默认情况下会显示子路由
            if (showingChildren.length === 1) {
                return true
            }

            // 如果没有子路由可显示,则显示父级路由。
            if (showingChildren.length === 0) {
                this.onlyOneChild = { ...parent, path: '', noShowingChildren: true }
                return true
            }

            return false
        },
        //路径解析函数
        resolvePath(routePath) {
            //如果是外部链接,则直接返回该地址
            if (isExternal(routePath)) {
                return routePath
            }
            //会进一步检查当前实例对象的 basePath 属性是否为一个外部 url 地址
            if (isExternal(this.basePath)) {
                return this.basePath
            }
            //不是一个外部 url 地址,且 basePath 也不是,则将它们拼接起来,得到一个新的路径
            return path.resolve(this.basePath, routePath)
        }
    }
};
</script>

2.5【渲染出菜单栏】

  • 侧边栏菜单:Sidebar/index.vue
<template>
    <div :class="{ 'has-logo': showLogo }">
        
        <logo v-if="showLogo" :collapse="isCollapse" />
        
        <el-scrollbar wrap-class="scrollbar-wrapper">
            <el-menu :default-active="activeMenu" :collapse="isCollapse" :unique-opened="false" :collapse-transition="false"
                mode="vertical">
                
                <sidebar-item v-for="route in routes" :key="route.path" :item="route" :base-path="route.path" />
            el-menu>
        el-scrollbar>
    div>
template>
  • 编写计算属性获取路由信息:Sidebar/index.vue
import sidebarItem from './sidebarItem.vue'
import variables from '@/styles/theme/variables.scss'
import { mapGetters } from 'vuex'
export default {
    name: 'Sidebar-Index',
    //import 引入的组件需要注入到对象中才能使用
    components: { sidebarItem },
    //计算属性
    computed: {
        ...mapGetters([
            'sidebar'
        ]),
        // 获取路由信息(path,children,component)
        routes() {
            return this.$router.options.routes
        },
        // 该属性将指定菜单项的选中状态
        activeMenu() {
            const route = this.$route
            const { meta, path } = route
            //选中状态将与当前页面所使用的路由路径保持一致(如:地址栏是:/icon,则返回/icon)
            if (meta.activeMenu) {
                return meta.activeMenu
            }
            return path
        },
        //如果侧边栏为折叠状态,则返回 true;否则返回 false。
        isCollapse() {
            //在vuex中保存的属性值
            return !this.sidebar.opened
        },
        //该计算属性返回了一个名为 variables 的对象,该对象属于全局配置
        //包括了一些变量和属性(如 menuBg 表示菜单的背景色,menuText 表示菜单项的文本颜色)
        variables() {
            return variables
        },
    }
}
</script>
  • 创建全局配置文件:theme/variables .js
/*整体页面背景*/
$bodyBg: #88ada6;
/*侧边框宽度*/
$sideBarWidth: 210px;
/*侧边框背景*/
$sideBarBg: #425066;
/*侧边栏激活背景*/
$menuActiveBg: #425066;
/*侧边栏鼠标悬停背景*/
$menuHoverBg: #c2ccd0;
/*侧边栏字体颜色*/
$menuText: #f0fcff;
/*侧边栏字体选中状态的颜色*/
$menuActiveText: #c0ebd7;

// the :export directive is the magic sauce for webpack
// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
:export {
  /*整体页面背景*/
  bodyBg: $bodyBg;
  /*侧边框宽度*/
  sideBarWidth: $sideBarWidth;
  /*侧边框背景*/
  sideBarBg: $sideBarBg;
  /*侧边栏激活背景*/
  menuActiveBg: $menuActiveBg;
  /*侧边栏鼠标悬停背景*/
  menuHoverBg: $menuHoverBg;
  /*侧边栏字体颜色*/
  menuText: $menuText;
  /*侧边栏字体选中状态的颜色*/
  menuActiveText: $menuActiveText;
}

三、顶部的Logo

3.1【顶部的Logo】

  • 侧边栏顶部的Logo:logo.vue
<template>
    
    
    
    <div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
        <transition name="sidebarLogoFade">
            
            
            
            <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
                
                <img v-if="logo" :src="logo" class="sidebar-logo">
                
                <h1 v-else class="sidebar-title">{{ title }} h1>
            router-link>
            
            
            <router-link v-else key="expand" class="sidebar-logo-link" to="/">
                
                <img v-if="logo" :src="logo" class="sidebar-logo">
                
                <h1 class="sidebar-title">{{ title }} h1>
            router-link>
        transition>
    div>
template>
  • 侧边栏顶部的Logo:logo.vue
<script>
export default {
    name: 'Sidebar-Logo',
    props: {
        collapse: {
            type: Boolean,
            required: true
        }
    },
    computed: {
        //获取标题
        title() {
            return this.$store.state.settings.title
        },
        //获取logo
        logo(){
            console.log(this.$store.state.settings.logo)
            return this.$store.state.settings.logo
        }
    }
}
script>
  
<style lang="scss" scoped>
.sidebarLogoFade-enter-active {
    transition: opacity 1.5s;
}

.sidebarLogoFade-enter,
.sidebarLogoFade-leave-to {
    opacity: 0;
}

.sidebar-logo-container {
    position: relative;
    width: 100%;
    height: 50px;
    line-height: 50px;
    background: #2b2f3a;
    text-align: center;
    overflow: hidden;

    & .sidebar-logo-link {
        height: 100%;
        width: 100%;

        & .sidebar-logo {
            width: 32px;
            height: 32px;
            vertical-align: middle;
            margin-right: 12px;
        }

        & .sidebar-title {
            display: inline-block;
            margin: 0;
            color: #fff;
            font-weight: 600;
            line-height: 50px;
            font-size: 14px;
            font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
            vertical-align: middle;
        }
    }

    &.collapse {
        .sidebar-logo {
            margin-right: 0px;
        }
    }
}
style>

3.2【Vuex状态管理】

  • ‍♀️Vuex状态管理:store/setting.js
import defaultSettings from '@/setting/settings'

const { title, fixedHeader, sidebarLogo } = defaultSettings

const state = {
    //网站标题
    title: title,
    //网站logo
    logo: logo,
    //头部固定
    fixedHeader: fixedHeader,
    //是否显示Logo
    sidebarLogo: sidebarLogo
}

const mutations = {
    CHANGE_SETTING: (state, { key, value }) => {
        if (state.hasOwnProperty(key)) {
            state[key] = value
        }
    }
}

const actions = {
    changeSetting({ commit }, data) {
        commit('CHANGE_SETTING', data)
    }
}

export default {
    namespaced: true,
    state,
    mutations,
    actions
}
  • ‍♀️属性设置:settings/setting.js
module.exports = {
    /**
     * 网站标题
     */
    title: 'Silence-Vue',
    /**
     * Logo
     */
    logo: '',
    /**
     * @type {boolean} true | false
     * @description Whether fix the header
     */
    fixedHeader: true,

    /**
     * @type {boolean} true | false
     * @description Whether show the logo in sidebar
     */
    sidebarLogo: true
}

四、头部组件

4.1【汉堡组件】

  • ☃️ 汉堡组件:components/Hamburger/index.vue
<template>
  <div style="padding: 0 15px;" @click="toggleClick">
    <svg
        :class="{'is-active':isActive}"
        class="hamburger"
        height="64"
        viewBox="0 0 1024 1024"
        width="64"
        xmlns="http://www.w3.org/2000/svg"
    >
      <path
          d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z"/>
    svg>
  div>
template>

<script>
export default {
  name: 'HamBurger',
  props: {
    isActive: {
      type: Boolean,
      default: false
    }
  },
  methods: {
    toggleClick() {
      this.$emit('toggleClick')
    }
  }
}
script>

<style scoped>
.hamburger {
  display: inline-block;
  vertical-align: middle;
  width: 20px;
  height: 20px;
}

.hamburger.is-active {
  transform: rotate(180deg);
}
style>

4.2【头部组件】

  • ☃️ 头部组件:Navbar/index.vue
<template>
  <div class="navbar-container">
    <ham-burger :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar"/>
    <bread-crumb class="breadcrumb-container"/>
    
    <div class="right-container">
      <el-tooltip content="登录" placement="top">
        <a href="/login">
          <svg-icon icon-class="user"/>
        a>
      el-tooltip>
    div>
  div>
template>
  • ☃️ 头部组件:Navbar/index.vue
import {mapGetters} from 'vuex'
import Hamburger from '@/components/Hamburger'
export default {
  name: 'Navbar-Index',
  components: { Hamburger},
  computed: {
    ...mapGetters([
      'sidebar',
    ])
  },
  methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    }
  }
};
</script>
<style lang="scss" scoped>
@import "src/styles/theme/index";
.navbar-container {
  margin:0 10px 0 10px;
  height: $navbarHeight;
  overflow: hidden;
  position: relative;
  border-radius: 10px;
  background: #fff;
  box-shadow: 0 1px 4px rgba(0, 21, 41, .08);

  .hamburger {
    line-height: 46px;
    height: 100%;
    float: left;
    cursor: pointer;
    transition: background .28s;
    -webkit-tap-highlight-color: transparent;

    &:hover {
      background: rgba(102, 51, 153, 0.22);
    }
  }

  .breadcrumb{
    float: left;
  }

  .right-menu {
    float: right;
    height: 100%;
    line-height: 50px;
  }

  .svg-icon {
    right: 50px;
    fill: #425066;
  }
}
style>

在这里插入图片描述

五、整合页面

  • 通用常量:layout/index.scss
<template>
  <div class="app-wrapper">
    
    <sidebar class="sidebar-container"/>
    
    <div class="main-container">
      
      <div >
        <navbar/>
      div>
      
      <app-main/>
    div>
    
    <scrollToTop/>
  div>
template>
<script>
import Sidebar from './components/Sidebar';
import AppMain from './components/AppMain';
import Navbar from './components/Navbar';
import {mapState} from 'vuex'

export default {
  name: 'Layout-Vue',
  components: {Sidebar, AppMain, Navbar},
}
script>

<style lang="scss" scoped>
@import "src/styles/index";
.app-wrapper {
  @include clearfix;
  position: relative;
  height: 100%;
  width: 100%;
}

A05-Layout布局_第2张图片

六、页面样式

  • Layout布局:src/styles/layout/index.scss
/*主要内容区域全局样式*/
@import 'main';
/*侧边栏全局样式*/
@import 'sidebar';

6.1【整体布局】

  • 整体布局:layout/main.scss
/*主要区域容器全局样式*/
.main-container {
    transition: margin-left 0.28s;
    /*右移侧边栏宽度*/
    margin-left: $sideBarWidth;
    border-radius: 10px;
    min-height: 100%;
    position: relative;
    overflow: hidden;
}
/*内容区域全局样式*/
.app-container {
    padding-left: 20px;
}
/*主要区域容器全局样式*/
.main-container {
    transition: margin-left 0.28s;
    /*右移侧边栏宽度*/
    margin-left: $sideBarWidth;
    border-radius: 10px;
    min-height: 100%;
    position: relative;
}
/*内容区域全局样式*/
.app-container {
    padding-left: 20px;
}
/*侧边栏模块全局样式*/
.sidebar-container {
    transition: width 0.28s;
    border-radius: 10px;
    width: $sideBarWidth;
    position: fixed;
    font-size: 0;
    top: 0;
    bottom: 0;
    left: 0;
    overflow: hidden;
}
/*组件全局样式*/
.components-container {
    margin: 30px 50px;
    position: relative;
}
/*分页全局样式*/
.pagination-container {
    margin-top: 30px;
}

/*过滤器全局样式*/
.filter-container {
    padding-bottom: 10px;
    .filter-item {
        display: inline-block;
        vertical-align: middle;
        margin-bottom: 10px;
    }
}

/*组件全局样式*/
.components-container {
    margin: 30px 50px;
    position: relative;
}
/*分页全局样式*/
.pagination-container {
    margin-top: 30px;
}
/*过滤器全局样式*/
.filter-container {
    padding-bottom: 10px;
    .filter-item {
        display: inline-block;
        vertical-align: middle;
        margin-bottom: 10px;
    }
}

A05-Layout布局_第3张图片

6.2【侧边区域】

  • 通用常量:src/styles/layout/sidebar/index.scss
#app {

  .sidebar-container {
    .is-horizontal {
      display: none;
    }
    
    .el-scrollbar {
      height: 100%;
    }

    &.has-logo {
      .el-scrollbar {
        height: calc(100% - 50px);
      }
    }

    .el-menu {
      border: none;
      height: 100%;
      width: 100% !important;
      background: $sideBarBg !important;

    }
    
    /*无子菜单的菜单样式*/
    .el-menu-item {
      border-radius: 10px;

      &:hover {
        background-color: $menuHoverBg !important;
      }

      &.is-active {
        color: $menuTextActive !important;
        background-color: $menuActiveBg !important;
      }
    }

    /*有子菜单的鼠标悬停背景*/
    .el-sub-menu__title {
      border-radius: 10px;

      &:hover {
        color: $menuTextHover;
        background-color: $subMenuHoverBg !important;
      }

      &.is-active {
        color: $subMenuActiveBg !important;
        background-color: $subMenuActiveBg !important;
      }
    }

    .svg-icon {
      margin-right: 10px;
    }
  }

  /*有子菜单的菜单的箭头*/
  .el-icon svg {
    position: relative;
    animation: arrowhead 0.28s;
    animation-fill-mode: forwards;
  }

  /*菜单关闭时*/
  .hideSidebar {
    .sidebar-container {
      width: 54px !important;
    }

    .main-container {
      margin-left: 54px;
    }

    /*子菜单标题-无下拉列表*/
    .submenu-title-noDropdown {
      padding: 0 !important;
      position: relative;

      .el-tooltip {
        padding: 0px !important;

        .svg-icon {
          margin-left: 20px;
        }

        .sub-el-icon {
          margin-left: 19px;
        }
      }
    }

    .el-sub-menu {
      overflow: hidden;

      .el-sub-menu__title {
        padding: 0 !important;

        .svg-icon {
          margin-left: 20px;
        }

        .sub-el-icon {
          margin-left: 19px;
        }

        .el-sub-menu__icon-arrow {
          display: none;
        }
      }
    }

    /*菜单折叠时隐藏title和箭头*/
    .el-menu--collapse {
      .el-icon svg {
        display: none;
      }

      .el-sub-menu {
        & > .el-sub-menu__title {
          .title {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
        }
      }
    }
  }

  /*没有动画*/
  .withoutAnimation {
    .main-container,
    .sidebar-container {
      transition: none;
    }
  }
}

/*菜单折叠时*/
.el-menu--vertical {
  .vertical {
    background-color: rebeccapurple;
  }

  .el-menu {
    /*子菜单弹框菜单颜色*/
    background-color: $sideBarBg;

    .svg-icon {
      margin-right: 0px;
    }

    .sub-el-icon {
      margin-right: 0px;
    }
  }

  .el-menu-item {
    border-radius: 10px;

    &:hover {
      color: $menuTextHover;
      background-color: $menuHoverBg !important;
    }

    &.is-active {
      color: $menuTextActive;
    }
  }

  /*有子菜单的鼠标悬停背景*/
  .el-sub-menu__title {
    border-radius: 10px;

    &:hover {
      background-color: $subMenuHoverBg !important;
    }

    &.is-active {
      color: $subMenuActiveBg !important;
      background-color: $subMenuActiveBg !important;
    }
  }

  // 当子菜单太长时出现滚动条
  .el-menu--popup {
    max-height: 100vh;
    overflow-y: auto;
    @include scrollBar;
  }
}

@keyframes arrowhead {
  from {
    right: 50px;
    opacity: 0;
  }
  to {
    right: 0;
    opacity: 1;
  }
}

七、关闭侧边栏

7.1【关闭侧边栏】

  • 手动关闭侧边栏:src/layout/components/Navbar/index.vue
<ham-burger :is-active="sidebar.opened" class="hamburger" @toggleClick="toggleSideBar"/>
methods: {
    toggleSideBar() {
      this.$store.dispatch('app/toggleSideBar')
    }
  }
  • 手动关闭侧边栏添加属性:src/layout/index.vue
<div :class="classObj" class="app-wrapper">
computed: {
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
        mobile: this.device === 'mobile'
      }
    }
  },

7.2【侧边栏样式】

  • 侧边栏样式: src/styles/layout/sidebar/index.scss
 /*菜单关闭时*/
  .hideSidebar {
    .sidebar-container {
      width: $sideBarVerticalWidth !important;
    }

    .main-container {
      margin-left: $sideBarVerticalWidth;
    }

    /*子菜单标题-无下拉列表*/
    .submenu-title-noDropdown {
      padding: 0 !important;
      position: relative;

      .el-tooltip {
        padding: 0 !important;

        .svg-icon {
          margin-left: 20px;
        }

        .sub-el-icon {
          margin-left: 19px;
        }
      }
    }

    .el-sub-menu {
      overflow: hidden;

      .el-sub-menu__title {
        padding: 0 !important;

        .svg-icon {
          margin-left: 20px;
        }

        .sub-el-icon {
          margin-left: 19px;
        }

        .el-sub-menu__icon-arrow {
          display: none;
        }
      }
    }

    /*菜单折叠时隐藏title和箭头*/
    .el-menu--collapse {
      .el-icon svg {
        display: none;
      }


      .el-sub-menu {
        & > .el-sub-menu__title {
          .subMenuTitle {
            height: 0;
            width: 0;
            overflow: hidden;
            visibility: hidden;
            display: inline-block;
          }
        }
      }
    }
  }

八、自动关闭侧边栏

8.2【监听页面变化】

  • 监听页面变化: src/layout/components/Mixin/index.js
import store from '@/store'

const { body } = document //创建了一个变量来引用document.body元素
const WIDTH = 992 // WIDTH常量被设置为992,这是Bootstrap中移动和桌面响应的阈值

export default {
    /*watch属性用于监视$route对象的变化*/
    watch: {
        $route(route) {
            /*它检查当前device(在应用程序的其他地方基于屏幕宽度设置)是否为移动设备,并且侧边栏当前是否已打开*/
            if (this.device === 'mobile' && this.sidebar.opened) {
                /*如果是,则触发一个动作('app/closeSideBar')到VueX商店,传递一个选项对象来指定关闭侧边栏时的动画*/
                store.dispatch('app/closeSideBar', { withoutAnimation: false })
            }
        }
    },
    /*它通过监听window对象上的resize事件 将this.$_resizeHandler方法绑定到resize事件上*/
    beforeMount() {
        /*当window对象触发resize事件时,监听器会调用this.$_resizeHandler方法,该方法会根据窗口大小更新device属性*/
        /*this.$_resizeHandler是一个私有方法,它是在组件内部定义的,并负责更新组件中的device属性,以响应屏幕宽度的变化*/
        window.addEventListener('resize', this.$_resizeHandler)
    },
    /*它通过移除之前在window对象上绑定的resize事件监听器,来清除已添加的事件监听程序
    因为如果不清除事件监听器,它们将继续在组件销毁后继续运行,可能导致内存泄漏和其他问题*/
    beforeDestroy() {
        window.removeEventListener('resize', this.$_resizeHandler)
    },
    mounted() {
        /*mounted()方法首先调用$_isMobile()方法来检查应用程序是否在移动设备上运行*/
        const isMobile = this.$_isMobile()
        if (isMobile) {
            /*如果是,它会调用VueX store中的 app/toggleDevice方法来将device属性设置为'mobile'*/
            store.dispatch('app/toggleDevice', 'mobile')
            /*它会通过调用app/closeSideBar方法来确保侧边栏在移动设备上始终关闭,该方法将一个选项对象传递给store来指定在没有动画的情况下关闭侧边栏*/
            store.dispatch('app/closeSideBar', { withoutAnimation: true })
        }
    },
    methods: {
        /*这个方法的作用是检查设备是否为移动设备*/
        $_isMobile() {
            /*该方法首先使用getBoundingClientRect()方法来获取body元素的宽度和高度,然后将其存储在名为rect的变量中*/
            const rect = body.getBoundingClientRect()
            /*它将检查rect对象的宽度是否小于预定义的WIDTH值,并将true或false返回给调用该方法的位置*/
            return rect.width - 1 < WIDTH //如果$_isMobile()方法返回true,则该组件将认为当前设备为移动设备,并根据需要采取响应行动。
        },
        /*它是在窗口大小发生变化时调用的方法,用于更新屏幕上不同元素的尺寸和布局,以支持屏幕的响应式支持*/
        $_resizeHandler() {
            /*首先它使用document.hidden属性,检查文档是否处于隐藏状态(例如:用户最小化了浏览器窗口)*/
            if (!document.hidden) {
                /*通过调用this.$_isMobile()来检查当前设备是否为移动设备*/
                const isMobile = this.$_isMobile()
                /*如果是,则通过调用VueX store的app/toggleDevice方法来将device属性设置为'mobile',否则将其设置为'desktop'。*/
                store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
                /*如果设备是移动设备,则通过调用VueX Store的app/closeSideBar方法来确保侧边栏始终处于关闭状态*/
                if (isMobile) {
                    store.dispatch('app/closeSideBar', { withoutAnimation: true })
                }
            }
        }
    }
}

8.2【引入JS文件】

  • 引入JS文件:src/layout/index.vue
<template>
  <div :class="classObj" class="app-wrapper">
    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
    	.....
    </div>
  </div>
</template>
import AutoResizer from './components/Mixin/index'
<script>
import AutoResizer from './components/Mixin/index'
export default {
  name: 'Layout-Vue',
  components: {Sidebar, AppMain, Navbar},
  mixins: [AutoResizer],
  computed: {
    classObj() {
      return {
        hideSidebar: !this.sidebar.opened,
        openSidebar: this.sidebar.opened,
        withoutAnimation: this.sidebar.withoutAnimation,
        mobile: this.device === 'mobile'
      }
    }
  },
  methods: {
    handleClickOutside() {
      this.$store.dispatch('app/closeSideBar', {withoutAnimation: false})
    }
  }
}
</script>

8.3【侧边栏样式】

  • 侧边栏样式: src/styles/layout/sidebar/index.scss
  // 移动响应
  .mobile {
    .main-container {
      margin-left: 0;
    }

    .sidebar-container {
      transition: transform .28s;
      width: $sideBarWidth !important;
    }

    &.hideSidebar {
      .sidebar-container {
        pointer-events: none;
        transition-duration: 0.3s;
        transform: translate3d(-$sideBarWidth, 0, 0);
      }
    }
  }

你可能感兴趣的:(#,②-VUE脚手架,前端,javascript,html)