浏览代码

feat: 🎸 重构layout

chenkl 4 年之前
父节点
当前提交
7ede02141e

+ 39 - 0
src/assets/icons/svg/layout-classic.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+  <defs>
+    <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+    <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+    <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+      <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+        <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+          <mask id="mask-3" fill="white">
+            <use xlink:href="#path-2"></use>
+          </mask>
+          <g id="Rectangle-18">
+            <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+            <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+          </g>
+          <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+          <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 39 - 0
src/assets/icons/svg/layout-top.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1"
+  xmlns="http://www.w3.org/2000/svg"
+  xmlns:xlink="http://www.w3.org/1999/xlink">
+
+  <defs>
+    <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+      <feMerge>
+        <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+        <feMergeNode in="SourceGraphic"></feMergeNode>
+      </feMerge>
+    </filter>
+    <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+    <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+      <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+      <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+      <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+    </filter>
+  </defs>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <g id="setting-copy-2" transform="translate(-1254.000000, -337.000000)">
+      <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+        <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 338.000000)">
+          <mask id="mask-3" fill="white">
+            <use xlink:href="#path-2"></use>
+          </mask>
+          <g id="Rectangle-18">
+            <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+            <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+          </g>
+          <rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+        </g>
+      </g>
+    </g>
+  </g>
+</svg>

+ 39 - 0
src/assets/icons/svg/layout-topLeft.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+            <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-18" fill="#fff" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                    <rect id="Rectangle-11" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

+ 1 - 3
src/components/Echart/index.vue

@@ -55,9 +55,7 @@ export default defineComponent({
 
     onMounted(() => {
       // 设置异步,不然图例一开始的宽度不正确。
-      setTimeout(() => {
-        initChart()
-      }, 10)
+      initChart()
       __resizeHandler = debounce(() => {
         if (chart) {
           chart.resize()

+ 1 - 2
src/components/Hamburger/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div>
+  <div @click="toggleCollapsed(!collapsed)">
     <svg
       :class="{'is-active': !collapsed}"
       class="hamburger"
@@ -7,7 +7,6 @@
       xmlns="http://www.w3.org/2000/svg"
       width="64"
       height="64"
-      @click="toggleCollapsed(!collapsed)"
     >
       <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>

+ 3 - 15
src/components/Logo/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <router-link :class="['app-logo', 'app-logo-' + theme]" to="/">
+  <router-link class="app-logo" to="/">
     <img :src="require('@/assets/img/logo.png')">
     <div v-if="show" class="sidebar-title">{{ title }}</div>
   </router-link>
@@ -15,10 +15,6 @@ export default defineComponent({
     collapsed: {
       type: Boolean as PropType<boolean>,
       required: true
-    },
-    theme: {
-      type: String as PropType<'light' | 'dark'>,
-      default: 'dark'
     }
   },
   setup(props) {
@@ -47,13 +43,14 @@ export default defineComponent({
 .app-logo {
   display: flex;
   align-items: center;
-  padding-left: 18px;
   cursor: pointer;
   height: @topSilderHeight;
   width: 100%;
+  background-color: @menuBg;
   img {
     width: 37px;
     height: 37px;
+    margin-left: 14px;
   }
   .sidebar-title {
     font-size: 14px;
@@ -61,17 +58,8 @@ export default defineComponent({
     transition: .5s;
     margin-left: 12px;
   }
-}
-.app-logo-dark {
-  background-color: @menuBg;
   .sidebar-title {
     color: #fff;
   }
 }
-.app-logo-light {
-  background-color: #fff;
-  .sidebar-title {
-    color: #000;
-  }
-}
 </style>

+ 4 - 7
src/components/Screenfull/index.vue

@@ -1,12 +1,9 @@
 <template>
-  <a-tooltip placement="bottom">
-    <template #title>
-      <span>{{ isFullscreen ? '退出全屏' : '全屏' }}</span>
-    </template>
-    <div style="height: 100%;">
-      <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+  <el-tooltip placement="bottom" :content="isFullscreen ? '退出全屏' : '全屏'">
+    <div style="height: 100%;" class="screenfull-svg" @click="click">
+      <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" />
     </div>
-  </a-tooltip>
+  </el-tooltip>
 </template>
 
 <script lang="ts">

+ 2 - 13
src/components/ScrollPane/index.vue

@@ -1,25 +1,20 @@
 <template>
-  <scrollbar
+  <el-scrollbar
     ref="scrollContainer"
-    :show-x="false"
     class="scroll-container"
     @wheel="handleScroll"
   >
     <slot />
-  </scrollbar>
+  </el-scrollbar>
 </template>
 
 <script lang="ts">
 import { defineComponent, ref, unref, nextTick } from 'vue'
-import Scrollbar from '_c/Scrollbar/index.vue'
 import { useScrollTo } from '@/hooks/useScrollTo'
 const tagAndTagSpacing = 4 // tagAndTagSpacing
 
 export default defineComponent({
   name: 'ScrollPane',
-  components: {
-    Scrollbar
-  },
   setup() {
     const scrollContainer = ref<HTMLElement | null>(null)
 
@@ -112,11 +107,5 @@ export default defineComponent({
   position: relative;
   overflow: hidden;
   width: 100%;
-    .el-scrollbar__bar {
-      bottom: 0px;
-    }
-    // .el-scrollbar__wrap {
-    //   height: 49px;
-    // }
 }
 </style>

+ 165 - 0
src/components/Setting/index.vue

@@ -0,0 +1,165 @@
+<template>
+  <div class="setting__content">
+    <div class="setting__title">导航栏布局</div>
+    <div class="icon__wrap">
+      <span :class="{'icon--active': layout==='Classic'}" @click="setLayout('Classic')">
+        <el-tooltip effect="dark" content="经典布局" placement="bottom">
+          <svg-icon icon-class="layout-classic" class="setting-svg-icon" />
+        </el-tooltip>
+      </span>
+      <span :class="{'icon--active': layout==='LeftTop'}" @click="setLayout('LeftTop')">
+        <el-tooltip effect="dark" content="左侧顶部布局" placement="bottom">
+          <svg-icon icon-class="layout-topLeft" class="setting-svg-icon" />
+        </el-tooltip>
+      </span>
+      <span :class="{'icon--active': layout==='Top'}" @click="setLayout('Top')">
+        <el-tooltip effect="dark" content="顶部布局" placement="bottom">
+          <svg-icon icon-class="layout-top" class="setting-svg-icon" />
+        </el-tooltip>
+      </span>
+    </div>
+
+    <div class="setting__title">侧边菜单主题</div>
+
+    <div class="setting__title">顶部菜单主题</div>
+
+    <div class="setting__title">界面功能</div>
+    <div class="setting__item">
+      <span>固定顶部操作栏</span>
+      <el-switch v-model="fixedNavbar" @change="setFixedNavbar" />
+    </div>
+
+    <div class="setting__title">界面显示</div>
+    <div class="setting__item">
+      <span>顶部操作栏</span>
+      <el-switch v-model="navbar" @change="setNavbar" />
+    </div>
+    <div class="setting__item">
+      <span>侧边栏缩收</span>
+      <el-switch v-model="hamburger" @change="setHamburger" />
+    </div>
+    <div class="setting__item">
+      <span>面包屑</span>
+      <el-switch v-model="breadcrumb" @change="setBreadcrumb" />
+    </div>
+    <div class="setting__item">
+      <span>全屏按钮</span>
+      <el-switch v-model="screenfull" @change="setScreenfull" />
+    </div>
+    <div class="setting__item">
+      <span>用户头像</span>
+      <el-switch v-model="userInfo" @change="setUserInfo" />
+    </div>
+    <div class="setting__item">
+      <span>标签页</span>
+      <el-switch v-model="tagsView" @change="setTagsView" />
+    </div>
+    <div class="setting__item">
+      <span>LOGO</span>
+      <el-switch v-model="logo" @change="setLogo" />
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, computed, ref } from 'vue'
+import { appStore } from '_p/index/store/modules/app'
+export default defineComponent({
+  name: 'Setting',
+  setup() {
+    const layout = computed(() => appStore.layout)
+    function setLayout(mode: 'Classic' | 'LeftTop' | 'Top' | 'Test') {
+      if (mode === layout.value) return
+      appStore.SetLayout(mode)
+    }
+
+    const fixedNavbar = ref<boolean>(appStore.fixedNavbar)
+    function setFixedNavbar(fixedNavbar: boolean) {
+      appStore.SetFixedNavbar(fixedNavbar)
+    }
+
+    const navbar = ref<boolean>(appStore.showNavbar)
+    function setNavbar(navbar: boolean) {
+      appStore.SetShowNavbar(navbar)
+    }
+
+    const hamburger = ref<boolean>(appStore.showHamburger)
+    function setHamburger(hamburger: boolean) {
+      appStore.SetHamburger(hamburger)
+    }
+
+    const breadcrumb = ref<boolean>(appStore.showBreadcrumb)
+    function setBreadcrumb(breadcrumb: boolean) {
+      appStore.SetBreadcrumb(breadcrumb)
+    }
+
+    const screenfull = ref<boolean>(appStore.showScreenfull)
+    function setScreenfull(screenfull: boolean) {
+      appStore.SetScreenfull(screenfull)
+    }
+
+    const userInfo = ref<boolean>(appStore.showUserInfo)
+    function setUserInfo(userInfo: boolean) {
+      appStore.SetUserInfo(userInfo)
+    }
+
+    const tagsView = ref<boolean>(appStore.showTags)
+    function setTagsView(tagsView: boolean) {
+      appStore.SetShowTags(tagsView)
+    }
+
+    const logo = ref<boolean>(appStore.showLogo)
+    function setLogo(logo: boolean) {
+      appStore.SetShowLogo(logo)
+    }
+
+    return {
+      layout, setLayout,
+      fixedNavbar, setFixedNavbar,
+      navbar, setNavbar,
+      hamburger, setHamburger,
+      breadcrumb, setBreadcrumb,
+      screenfull, setScreenfull,
+      userInfo, setUserInfo,
+      tagsView, setTagsView,
+      logo, setLogo
+    }
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.setting__content {
+  background: @appBg;
+  padding: 0 20px;
+  .setting__title {
+    text-align: center;
+    padding-top: 20px;
+  }
+  .icon__wrap {
+    text-align: center;
+    margin-top: 15px;
+    &>span {
+      display: inline-block;
+      padding: 5px 10px;
+      border: 2px solid @appBg;
+      .setting-svg-icon {
+        font-size: 60px;
+        cursor: pointer;
+      }
+      .setting-svg-icon:nth-of-type(2) {
+        margin: 0 10px;
+      }
+    }
+    .icon--active {
+      border: 2px solid #018ffb;
+      border-radius: 4px;
+    }
+  }
+  .setting__item {
+    display: flex;
+    justify-content: space-between;
+    margin: 16px 0;
+  }
+}
+</style>

+ 5 - 7
src/components/Sider/Item.vue

@@ -1,11 +1,9 @@
 <template>
-  <div>
-    <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
-    <svg-icon v-else :icon-class="icon" class="anticon" />
-    <slot name="title">
-      <span class="anticon-item">{{ title }}</span>
-    </slot>
-  </div>
+  <i v-if="icon.includes('el-icon')" :class="[icon, 'sub-el-icon', 'anticon']" />
+  <svg-icon v-else :icon-class="icon" class="anticon" />
+  <slot name="title">
+    <span class="anticon-item">{{ title }}</span>
+  </slot>
 </template>
 
 <script lang="ts">

+ 0 - 27
src/components/Sider/Link.vue

@@ -1,27 +0,0 @@
-<template>
-  <a v-if="isExternal(to)" :href="to" target="_blank" rel="noopener">
-    <slot />
-  </a>
-  <router-link v-else :to="to">
-    <slot />
-  </router-link>
-</template>
-
-<script lang="ts">
-import { defineComponent, PropType } from 'vue'
-import { isExternal } from '@/utils/validate'
-
-export default defineComponent({
-  props: {
-    to: {
-      type: String as PropType<string>,
-      required: true
-    }
-  },
-  setup() {
-    return {
-      isExternal
-    }
-  }
-})
-</script>

+ 3 - 4
src/components/Sider/SiderItem.vue

@@ -2,9 +2,9 @@
   <template v-if="!item.meta?.hidden">
     <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.meta?.alwaysShow">
       <el-menu-item :index="resolvePath(onlyOneChild.path)">
-        <item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" :title="onlyOneChild.meta.title" />
+        <item v-if="onlyOneChild.meta" :icon="onlyOneChild.meta.icon || (item.meta && item.meta.icon)" />
         <template #title>
-          {{ onlyOneChild.meta.title }}
+          <span class="anticon-item">{{ onlyOneChild.meta.title }}</span>
         </template>
       </el-menu-item>
     </template>
@@ -30,10 +30,9 @@ import type { RouteRecordRaw } from 'vue-router'
 import path from 'path'
 import { isExternal } from '@/utils/validate'
 import Item from './Item.vue'
-import AppLink from './Link.vue'
 export default defineComponent({
   name: 'SiderItem',
-  components: { Item, AppLink },
+  components: { Item },
   props: {
     // route object
     item: {

+ 33 - 35
src/components/Sider/index.vue

@@ -1,27 +1,25 @@
 <template>
-  <!-- <div :class="{'has-logo': showLogo}" class="sidebar-container"> -->
-  <!-- <el-scrollbar class="menu-wrap"> -->
-  <el-menu
-    :default-active="activeMenu"
-    :collapse="collapsed"
-    :unique-opened="false"
-    :background-color="variables.menuBg"
-    :text-color="variables.menuText"
-    :active-text-color="variables.menuActiveText"
-    mode="vertical"
-    class="sidebar-container"
-    :class="{'sidebar__wrap--collapsed': collapsed}"
-    @select="selectMenu"
-  >
-    <sider-item
-      v-for="route in routers"
-      :key="route.path"
-      :item="route"
-      :base-path="route.path"
-    />
-  </el-menu>
-  <!-- </el-scrollbar> -->
-  <!-- </div> -->
+  <div :class="{'has-logo': showLogo}" class="sidebar-container">
+    <el-scrollbar>
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="collapsed"
+        :unique-opened="false"
+        :background-color="variables.menuBg"
+        :text-color="variables.menuText"
+        :active-text-color="variables.menuActiveText"
+        mode="vertical"
+        @select="selectMenu"
+      >
+        <sider-item
+          v-for="route in routers"
+          :key="route.path"
+          :item="route"
+          :base-path="route.path"
+        />
+      </el-menu>
+    </el-scrollbar>
+  </div>
 </template>
 
 <script lang="ts">
@@ -83,16 +81,16 @@ export default defineComponent({
 .has-logo {
   height: calc(~"100% - @{topSilderHeight}");
 }
-// .menu-wrap {
-//   height: 100%;
-//   overflow: hidden;
-//   @{deep}(.el-scrollbar__wrap) {
-//     overflow-x: hidden;
-//     overflow-y: auto;
-//   }
-//   @{deep}(.el-menu) {
-//     border-right: none;
-//     width: 100%;
-//   }
-// }
+@{deep}(.el-scrollbar) {
+  width: 100%;
+  height: 100%;
+  .el-scrollbar__wrap {
+    overflow: scroll;
+    overflow-x: hidden;
+    .el-menu {
+      width: 100% !important;
+      border: none;
+    }
+  }
+}
 </style>

+ 113 - 0
src/components/TagsView/ScrollPane.vue

@@ -0,0 +1,113 @@
+<template>
+  <el-scrollbar
+    ref="scrollContainer"
+    class="scroll-container"
+    @wheel="handleScroll"
+  >
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, unref, nextTick } from 'vue'
+import { useScrollTo } from '@/hooks/useScrollTo'
+const tagAndTagSpacing = 4 // tagAndTagSpacing
+
+export default defineComponent({
+  name: 'ScrollPane',
+  setup() {
+    const scrollContainer = ref<HTMLElement | null>(null)
+
+    function handleScroll(e: any): void {
+      const eventDelta: number = e.wheelDelta || -e.deltaY * 40
+      const $scrollWrapper: any = (unref(scrollContainer) as any).wrap
+      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+    }
+
+    function moveToTarget(currentTag: any) {
+      const $container: any = (unref(scrollContainer) as any).$el
+      const $containerWidth: number = $container.offsetWidth
+      const $scrollWrapper: any = (unref(scrollContainer) as any).wrap
+      const tagList = (unref(scrollContainer) as any).$parent.$parent.tagRefs
+
+      let firstTag: any = null
+      let lastTag: any = null
+
+      // find first tag and last tag
+      if (tagList.length > 0) {
+        firstTag = tagList[0]
+        lastTag = tagList[tagList.length - 1]
+      }
+
+      if (firstTag === currentTag) {
+        $scrollWrapper.scrollLeft = 0
+      } else if (lastTag === currentTag) {
+        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+      } else {
+        // find preTag and nextTag
+        const currentIndex: number = tagList.findIndex((item: any) => item === currentTag)
+        const prevTag = tagList[currentIndex - 1]
+        const nextTag = tagList[currentIndex + 1]
+        // the tag's offsetLeft after of nextTag
+        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
+
+        // the tag's offsetLeft before of prevTag
+        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
+
+        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+          nextTick(() => {
+            const { start } = useScrollTo({
+              el: $scrollWrapper,
+              position: 'scrollLeft',
+              to: afterNextTagOffsetLeft - $containerWidth,
+              duration: 500
+            })
+            start()
+          })
+        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+          nextTick(() => {
+            const { start } = useScrollTo({
+              el: $scrollWrapper,
+              position: 'scrollLeft',
+              to: beforePrevTagOffsetLeft,
+              duration: 500
+            })
+            start()
+          })
+        }
+      }
+    }
+
+    function moveTo(to: number) {
+      const $scrollWrapper: any = (unref(scrollContainer) as any).wrap
+      nextTick(() => {
+        const { start } = useScrollTo({
+          el: $scrollWrapper,
+          position: 'scrollLeft',
+          to: $scrollWrapper.scrollLeft + to,
+          duration: 500
+        })
+        start()
+      })
+    }
+
+    return {
+      handleScroll,
+      scrollContainer,
+      moveToTarget,
+      moveTo
+    }
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  width: 100%;
+  @{deep}(.el-scrollbar__wrap) {
+    overflow: hidden;
+  }
+}
+</style>

+ 363 - 0
src/components/TagsView/index.vue

@@ -0,0 +1,363 @@
+<template>
+  <div ref="wrapper" class="tags-view-container">
+    <el-button class="move-btn prev-btn" icon="el-icon-arrow-left" @click="move(-200)" />
+    <scroll-pane ref="scrollPane" class="tags-view-wrapper">
+      <div class="link-wrapper">
+        <router-link
+          v-for="tag in visitedViews"
+          :ref="setTagRef"
+          :key="tag.path"
+          :class="isActive(tag) ? 'active' : ''"
+          :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+          tag="span"
+          class="tags-view-item"
+          @click.middle="closeSelectedTag(tag)"
+          @contextmenu.prevent="openMenu(tag, $event)"
+        >
+          {{ tag.title }}
+          <i v-if="!tag.meta.affix" class="el-icon-close icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+        </router-link>
+      </div>
+    </scroll-pane>
+    <el-button class="move-btn next-btn" icon="el-icon-arrow-right" @click="move(200)" />
+    <ul v-show="visible" :style="{left: left + 'px', top: top + 'px'}" class="contextmenu">
+      <li @click="refreshSelectedTag(selectedTag)">
+        刷新
+      </li>
+      <li v-if="!(selectedTag.meta&&selectedTag.meta.affix)" @click="closeSelectedTag(selectedTag)">
+        关闭
+      </li>
+      <li @click="closeOthersTags">
+        关闭其他
+      </li>
+      <li @click="closeAllTags(selectedTag)">
+        关闭全部
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script lang="ts">
+import ScrollPane from './ScrollPane.vue'
+import path from 'path'
+import { defineComponent, ref, unref, computed, watch, onMounted, nextTick } from 'vue'
+import { tagsViewStore } from '_p/index/store/modules/tagsView'
+import { permissionStore } from '_p/index/store/modules/permission'
+import { useRouter } from 'vue-router'
+import type { RouteLocationNormalizedLoaded, RouteRecordRaw } from 'vue-router'
+
+export default defineComponent({
+  name: 'TagsView',
+  components: {
+    ScrollPane
+  },
+  setup() {
+    const { currentRoute, push, replace } = useRouter()
+    const wrapper = ref<HTMLElement | null>(null)
+    const scrollPane = ref<HTMLElement | null>(null)
+    const visible = ref<boolean>(false)
+    const top = ref<number>(0)
+    const left = ref<number>(0)
+    const selectedTag = ref<any>({})
+    const affixTags = ref<any[]>([])
+    const visitedViews = computed(() => tagsViewStore.visitedViews)
+    const routers = computed(() => permissionStore.routers)
+
+    const tagRefs = ref<any[]>([])
+
+    function setTagRef(el: any): void {
+      tagRefs.value.push(el)
+    }
+
+    function isActive(route: RouteLocationNormalizedLoaded): boolean {
+      return route.path === currentRoute.value.path
+    }
+
+    function filterAffixTags(routes: RouteRecordRaw[], basePath = '/'): any[] {
+      let tags: any[] = []
+      routes.forEach((route: RouteRecordRaw) => {
+        if (route.meta && route.meta.affix) {
+          const tagPath = path.resolve(basePath, route.path)
+          tags.push({
+            fullPath: tagPath,
+            path: tagPath,
+            name: route.name,
+            meta: { ...route.meta }
+          })
+        }
+        if (route.children) {
+          const tempTags: any[] = filterAffixTags(route.children, route.path)
+          if (tempTags.length >= 1) {
+            tags = [...tags, ...tempTags]
+          }
+        }
+      })
+
+      return tags
+    }
+
+    function initTags(): void {
+      affixTags.value = filterAffixTags(routers.value)
+      const affixTagArr: any[] = affixTags.value
+      for (const tag of affixTagArr) {
+        // Must have tag name
+        if (tag.name) {
+          tagsViewStore.addVisitedView(tag)
+        }
+      }
+    }
+
+    function addTags(): void | boolean {
+      const { name } = currentRoute.value
+      if (name) {
+        tagsViewStore.addView(currentRoute.value)
+      }
+      return false
+    }
+
+    function moveToCurrentTag() {
+      // TODO 要手动清除tagRefs,不然会一直重复,后续看看有没有什么解决办法
+      tagRefs.value = []
+      const tags = unref(tagRefs)
+      nextTick(() => {
+        for (const tag of tags) {
+          if (tag && tag.to.path === currentRoute.value.path) {
+            (unref(scrollPane) as any).moveToTarget(tag)
+            // when query is different then update
+            if (tag.to.fullPath !== currentRoute.value.fullPath) {
+              tagsViewStore.updateVisitedView(currentRoute.value)
+            }
+
+            break
+          }
+        }
+      })
+    }
+
+    async function refreshSelectedTag(view: RouteLocationNormalizedLoaded) {
+      await tagsViewStore.delCachedView()
+      const { fullPath } = view
+      nextTick(() => {
+        replace({
+          path: '/redirect' + fullPath
+        })
+      })
+    }
+
+    async function closeSelectedTag(view: RouteLocationNormalizedLoaded) {
+      const views: any = await tagsViewStore.delView(view)
+      if (isActive(view)) {
+        toLastView(views.visitedViews)
+      }
+    }
+
+    function closeOthersTags() {
+      push(selectedTag.value)
+      tagsViewStore.delOthersViews(selectedTag.value).then(() => {
+        moveToCurrentTag()
+      })
+    }
+
+    async function closeAllTags(view: RouteLocationNormalizedLoaded) {
+      const views: any = await tagsViewStore.delAllViews()
+      // console.log(affixTags.value.some(tag => tag.path === view.path))
+      // if (affixTags.value.some(tag => tag.path === view.path)) {
+      //   return
+      // }
+      toLastView(views.visitedViews)
+    }
+
+    function toLastView(visitedViews: RouteLocationNormalizedLoaded[]) {
+      const latestView = visitedViews.slice(-1)[0]
+      if (latestView) {
+        push(latestView)
+      } else {
+        // You can set another route
+        push('/')
+      }
+    }
+
+    function openMenu(tag: RouteLocationNormalizedLoaded, e: any) {
+      const menuMinWidth = 105
+      const offsetLeft: number = (unref(wrapper) as any).getBoundingClientRect().left // container margin left
+      const offsetWidth: number = (unref(wrapper) as any).offsetWidth // container width
+      const maxLeft: number = offsetWidth - menuMinWidth// left boundary
+      const itemLeft: number = e.clientX - offsetLeft + 4
+
+      if (itemLeft > maxLeft) {
+        left.value = maxLeft
+      } else {
+        left.value = itemLeft
+      }
+      top.value = e.offsetY
+
+      visible.value = true
+      selectedTag.value = tag
+    }
+
+    function closeMenu() {
+      visible.value = false
+    }
+
+    function move(to: number) {
+      (unref(scrollPane) as any).moveTo(to)
+    }
+
+    onMounted(() => {
+      initTags()
+      addTags()
+    })
+
+    watch(
+      () => currentRoute.value,
+      () => {
+        addTags()
+        moveToCurrentTag()
+      }
+    )
+
+    watch(
+      () => visible.value,
+      (visible: boolean) => {
+        if (visible) {
+          document.body.addEventListener('click', closeMenu)
+        } else {
+          document.body.removeEventListener('click', closeMenu)
+        }
+      }
+    )
+
+    return {
+      wrapper, scrollPane,
+      visible, top, left,
+      selectedTag, affixTags,
+      visitedViews, routers,
+      tagRefs, setTagRef,
+      isActive,
+      filterAffixTags,
+      initTags,
+      addTags,
+      moveToCurrentTag,
+      refreshSelectedTag,
+      closeSelectedTag,
+      closeOthersTags,
+      closeAllTags,
+      toLastView,
+      openMenu,
+      closeMenu,
+      move
+    }
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.tags-view-container {
+  height: @tagsViewHeight;
+  width: 100%;
+  background: #fff;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+  position: relative;
+  display: flex;
+  z-index: 1;
+  &::after {
+    content: "";
+    width: 100%;
+    height: 1px;
+    border-top: #d8dce5;
+    position: absolute;
+    left: 0;
+    bottom: 0;
+  }
+  .move-btn {
+    display: inline-block;
+    width: @tagsViewHeight;
+    height: @tagsViewHeight;
+    line-height: @tagsViewHeight;
+    text-align: center;
+    padding: 0;
+    border-radius: 0;
+  }
+  .tags-view-wrapper {
+    width: calc(~"100% - 78px");
+    .link-wrapper {
+      height: @tagsViewHeight;
+      line-height: @tagsViewHeight;
+    }
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 30px;
+      line-height: 30px;
+      border: 1px solid #d8dce5;
+      color: #495060;
+      background: #fff;
+      padding: 0 8px;
+      font-size: 12px;
+      margin-left: 5px;
+      &:last-of-type {
+        margin-right: 23px;
+      }
+      &.active {
+        background-color: #304156;
+        color: #fff;
+        border-color: #304156;
+        &::before {
+          content: '';
+          background: #fff;
+          display: inline-block;
+          width: 8px;
+          height: 8px;
+          border-radius: 50%;
+          position: relative;
+          margin-right: 5px;
+        }
+      }
+      .icon-close {
+        width: 16px;
+        height: 16px;
+        border-radius: 50%;
+        text-align: center;
+        line-height: 16px;
+        transition: all .3s cubic-bezier(.645, .045, .355, 1);
+        transform-origin: 100% 50%;
+        margin-left: 5px;
+        &:before {
+          transform: scale(.6);
+          display: inline-block;
+        }
+        &:hover {
+          background-color: #b4bccc;
+          color: #fff;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 200;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}
+@{deep}(.scrollbar__view) {
+  height: @tagsViewHeight;
+  line-height: @tagsViewHeight;
+}
+</style>

+ 71 - 0
src/components/UserInfo/index.vue

@@ -0,0 +1,71 @@
+<template>
+  <el-dropdown class="avatar-container" trigger="hover">
+    <div>
+      <div class="avatar-wrapper">
+        <img :src="require('@/assets/img/avatar.gif')" class="user-avatar">
+        <span class="name-item">管理员</span>
+      </div>
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item key="1">
+          <span style="display: block;" @click="toHome">首页</span>
+        </el-dropdown-item>
+        <el-dropdown-item key="2">
+          <span style="display: block;" @click="loginOut">退出登录</span>
+        </el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>
+
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { resetRouter } from '_p/index/router'
+import wsCache from '@/cache'
+import { useRouter } from 'vue-router'
+import { tagsViewStore } from '_p/index/store/modules/tagsView'
+export default defineComponent({
+  name: 'UserInfo',
+  setup() {
+    const { replace, push } = useRouter()
+    async function loginOut(): Promise<void> {
+      wsCache.clear()
+      await resetRouter() // 重置静态路由表
+      await tagsViewStore.delAllViews() // 删除所有的tags标签页
+      replace('/login')
+    }
+    function toHome() {
+      push('/')
+    }
+    return {
+      loginOut,
+      toHome
+    }
+  }
+})
+</script>
+
+<style lang="less" scoped>
+.avatar-container {
+  margin-right: 30px;
+  padding: 0 10px;
+  .avatar-wrapper {
+    display: flex;
+    align-items: center;
+    height: 100%;
+    cursor: pointer;
+    .user-avatar {
+      width: 30px;
+      height: 30px;
+      border-radius: 10px;
+    }
+    .name-item {
+      font-size: 14px;
+      font-weight: 600;
+      display: inline-block;
+      margin-left: 5px;
+    }
+  }
+}
+</style>

+ 1 - 0
src/pages/index/App.vue

@@ -22,5 +22,6 @@ html,body {
 }
 #app {
   .size;
+  background: @appBg;
 }
 </style>

+ 9 - 9
src/pages/index/layout/components/UserInfo.vue

@@ -1,24 +1,24 @@
 <template>
-  <a-dropdown class="avatar-container" :trigger="['hover']">
+  <el-dropdown class="avatar-container" :trigger="['hover']">
     <div>
       <div class="avatar-wrapper">
         <img :src="require('@/assets/img/avatar.gif')" class="user-avatar">
         <span class="name-item">管理员</span>
       </div>
     </div>
-    <template #overlay>
-      <a-menu>
-        <a-menu-item key="1">
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item key="1">
           <router-link to="/">
             首页
           </router-link>
-        </a-menu-item>
-        <a-menu-item key="2">
+        </el-dropdown-item>
+        <el-dropdown-item key="2">
           <span style="display: block;" @click="loginOut">退出登录</span>
-        </a-menu-item>
-      </a-menu>
+        </el-dropdown-item>
+      </el-dropdown-menu>
     </template>
-  </a-dropdown>
+  </el-dropdown>
 </template>
 
 <script lang="ts">

+ 205 - 44
src/pages/index/layout/modules/Test.vue

@@ -1,5 +1,5 @@
 <template>
-  <el-container class="app__wrap">
+  <div :class="classObj" class="app__wrap">
     <div class="sidebar__wrap" :class="{'sidebar__wrap--collapsed': collapsed}">
       <logo
         v-if="showLogo"
@@ -7,91 +7,252 @@
       />
       <sider />
     </div>
-    <el-main><hamburger :collapsed="collapsed" class="hamburger-container" @toggleClick="setCollapsed" /></el-main>
-  </el-container>
-  <!-- <div class="app__wrap">
-    <div class="sidebar__wrap" :class="{'sidebar__wrap--collapsed': collapsed}">
-      <logo
-        v-if="showLogo"
-        :collapsed="collapsed"
-      />
-      <sider />
+    <div
+      class="main__wrap"
+      :class="{
+        'main__wrap--collapsed': collapsed
+      }"
+    >
+      <el-scrollbar
+        class="main__wrap--content"
+        :class="{
+          'main__wrap--navFixed': fixedNavbar
+        }"
+      >
+        <div
+          v-if="showNavbar"
+          class="navbar__wrap"
+          :class="{
+            'navbar__wrap--fixed': fixedNavbar,
+            'navbar__wrap--collapsed': fixedNavbar && collapsed
+          }"
+        >
+          <hamburger
+            v-if="showHamburger"
+            :collapsed="collapsed"
+            class="hover-container"
+            @toggleClick="setCollapsed"
+          />
+          <breadcrumb v-if="showBreadcrumb" />
+          <div v-if="showScreenfull || showUserInfo" class="navbar__wrap--right">
+            <screenfull v-if="showScreenfull" class="hover-container screenfull-container" />
+            <user-info v-if="showUserInfo" class="hover-container user-container" />
+          </div>
+        </div>
+        <div v-if="showTags" class="tags__wrap">
+          <tags-view />
+        </div>
+        <app-main />
+      </el-scrollbar>
     </div>
-    <div class="main__wrap">
-      <div class="navbar__wrap">
-      </div>
-      <div class="tags__wrap" />
-      <div class="main__wrap" />
+
+    <!-- setting -->
+    <div class="setting__wrap" @click="toggleClick">
+      <i class="el-icon-setting" />
     </div>
-  </div> -->
+    <el-drawer
+      v-model="drawer"
+      direction="rtl"
+      size="20%"
+    >
+      <template #title>
+        <div class="setting__title">项目配置</div>
+      </template>
+      <setting />
+    </el-drawer>
+    <!-- setting -->
+  </div>
 </template>
 
 <script lang="ts">
-import { defineComponent, computed } from 'vue'
+import { defineComponent, computed, ref } from 'vue'
 import { appStore } from '_p/index/store/modules/app'
 
 import AppMain from '../components/AppMain.vue'
-import TagsView from '../components/TagsView.vue'
+import TagsView from '_c/TagsView/index.vue'
 import Logo from '_c/Logo/index.vue'
-import Scrollbar from '_c/Scrollbar/index.vue'
 import Sider from '_c/Sider/index.vue'
 import Hamburger from '_c/Hamburger/index.vue'
+import Breadcrumb from '_c/Breadcrumb/index.vue'
+import Screenfull from '_c/Screenfull/index.vue'
+import UserInfo from '_c/UserInfo/index.vue'
+
+import Setting from '_c/Setting/index.vue'
 export default defineComponent({
   name: 'Layout',
   components: {
     Sider,
     Hamburger,
-    // Navbar,
+    Breadcrumb,
+    Screenfull,
+    UserInfo,
     AppMain,
     TagsView,
     Logo,
-    Scrollbar
+    Setting
   },
   setup() {
+    const drawer = ref<boolean>(false)
     const collapsed = computed(() => appStore.collapsed)
     const showLogo = computed(() => appStore.showLogo)
     const showTags = computed(() => appStore.showTags)
+    const showBreadcrumb = computed(() => appStore.showBreadcrumb)
+    const showHamburger = computed(() => appStore.showHamburger)
+    const showScreenfull = computed(() => appStore.showScreenfull)
+    const showUserInfo = computed(() => appStore.showUserInfo)
+    const showNavbar = computed(() => appStore.showNavbar)
+    const fixedNavbar = computed(() => appStore.fixedNavbar)
+
+    const classObj = computed(() => {
+      const obj = {}
+      obj[`app__wrap--${appStore.layout}`] = true
+      return obj
+    })
 
     function setCollapsed(collapsed: boolean): void {
       appStore.SetCollapsed(collapsed)
     }
 
+    function toggleClick(): void {
+      drawer.value = !drawer.value
+    }
+
     return {
+      drawer,
+      classObj,
       collapsed,
       showLogo,
       showTags,
-      setCollapsed
+      showBreadcrumb,
+      showHamburger,
+      showScreenfull,
+      showUserInfo,
+      showNavbar,
+      fixedNavbar,
+      setCollapsed,
+      toggleClick
     }
   }
 })
 </script>
 
 <style lang="less" scoped>
-.app__wrap {
+.app__wrap--Classic {
   position: relative;
   height: 100%;
   width: 100%;
-  // .sidebar__wrap {
-  //   position: absolute;
-  //   width: @menuWidth;
-  //   top: 0;
-  //   left: 0;
-  //   height: 100%;
-  //   transition: all 0.2s;
-  // }
-  // .sidebar__wrap--collapsed {
-  //   width: @menuMinWidth;
+  .sidebar__wrap {
+    position: fixed;
+    width: @menuWidth;
+    top: 0;
+    left: 0;
+    height: 100%;
+    transition: width 0.2s;
+  }
+  .sidebar__wrap--collapsed {
+    width: @menuMinWidth;
+    @{deep}(.anticon-item) {
+      display: none;
+    }
+  }
+  .main__wrap {
+    position: absolute;
+    width: calc(~"100% - @{menuWidth}");
+    height: 100%;
+    top: 0;
+    left: @menuWidth;
+    transition: all 0.2s;
+    z-index: 1;
+    .navbar__wrap {
+      display: flex;
+      align-items: center;
+      height: @navbarHeight;
+      padding: 0 20px 0 15px;
+      position: relative;
+      background: @contentBg;
+      transition: all 0.2s;
+      &:after {
+        content: "";
+        width: 100%;
+        height: 1px;
+        border-top: 1px solid #d8dce5;
+        position: absolute;
+        bottom: 0;
+        left: 0;
+      }
+      @{deep}(.hover-container) {
+        transition: background 0.2s;
+        height: 100%;
+        line-height: @navbarHeight + 5px;
+        padding: 0 5px;
+        &:hover {
+          background: #f6f6f6;
+        }
+      }
+      .navbar__wrap--right {
+        display: flex;
+        align-items: center;
+        height: @navbarHeight;
+        position: absolute;
+        top: 0;
+        right: 20px;
+        @{deep}(.screenfull-container),
+        @{deep}(.user-container) {
+          line-height: @navbarHeight !important;
+        }
+      }
+    }
+    // 固定顶部操作栏
+    .navbar__wrap--fixed {
+      position: fixed;
+      width: calc(~"100% - @{menuWidth} - 35px");
+      top: 0;
+      left: @menuWidth;
+      z-index: 200;
+    }
+    .navbar__wrap--collapsed {
+      width: calc(~"100% - @{menuMinWidth} - 35px");
+      left: @menuMinWidth;
+    }
+    // 固定顶部操作栏
+
+    // content样式
+    .main__wrap--content {
+      height: 100%;
+      @{deep}(.el-scrollbar__wrap) {
+        overflow-x: hidden;
+      }
+    }
+    .main__wrap--navFixed {
+      padding-top: @navbarHeight;
+      height: calc(~"100% - @{navbarHeight}");
+    }
+    // content样式
+  }
+  .main__wrap--collapsed {
+    width: calc(~"100% - @{menuMinWidth}");
+    left: @menuMinWidth;
+  }
+}
 
-  // }
-  // .main__wrap {
-  //   position: absolute;
-  //   width: calc(~"100% - @{menuWidth}");
-  //   height: 100%;
-  //   top: 0;
-  //   left: @menuWidth;
-  //   .navbar__wrap {
-  //     height: @navbarHeight;
-  //   }
-  // }
+// 项目配置
+.setting__wrap {
+  position: fixed;
+  top: 45%;
+  right: 0;
+  z-index: 10;
+  display: flex;
+  padding: 10px;
+  color: #fff;
+  cursor: pointer;
+  background: #018ffb;
+  border-radius: 6px 0 0 6px;
+  justify-content: center;
+  align-items: center;
+}
+.setting__title {
+  font-weight: bold;
+  color: black;
 }
+// 项目配置
 </style>

+ 2 - 1
src/pages/index/router/index.ts

@@ -59,7 +59,8 @@ export const constantRouterMap: AppRouteRecordRaw[] = [
     name: 'Login',
     meta: {
       hidden: true,
-      title: '登录'
+      title: '登录',
+      noTagsView: true
     }
   },
   {

+ 53 - 1
src/pages/index/store/modules/app.ts

@@ -9,63 +9,115 @@ export interface AppState {
   fixedTags: boolean
   fixedNavbar: boolean
   layout: string
+  showBreadcrumb: boolean
+  showHamburger: boolean
+  showScreenfull: boolean
+  showUserInfo: boolean
 }
 
 @Module({ dynamic: true, namespaced: true, store, name: 'app' })
 class App extends VuexModule implements AppState {
   public collapsed = false // 菜单栏是否栏缩收
-  public showLogo = false // 是否显示logo
+  public showLogo = true // 是否显示logo
   public showTags = true // 是否显示标签栏
   public showNavbar = true // 是否显示navbar
   public fixedTags = true // 是否固定标签栏
   public fixedNavbar = true // 是否固定navbar
   public layout = 'Classic' // layout布局
+  public showBreadcrumb = true // 是否显示面包屑
+  public showHamburger = true // 是否显示侧边栏缩收按钮
+  public showScreenfull = true // 是否全屏按钮
+  public showUserInfo = true // 是否显示用户头像
 
   @Mutation
   private SET_COLLAPSED(collapsed: boolean): void {
     this.collapsed = collapsed
   }
+  @Mutation
   private SET_SHOWLOGO(showLogo: boolean): void {
     this.showLogo = showLogo
   }
+  @Mutation
   private SET_SHOWTAGS(showTags: boolean): void {
     this.showTags = showTags
   }
+  @Mutation
   private SET_NAVBAR(showNavbar: boolean): void {
     this.showNavbar = showNavbar
   }
+  @Mutation
   private SET_FIXEDTAGS(fixedTags: boolean): void {
     this.fixedTags = fixedTags
   }
+  @Mutation
   private SET_FIXEDNAVBAR(fixedNavbar: boolean): void {
     this.fixedNavbar = fixedNavbar
   }
+  @Mutation
   private SET_LAYOUT(layout: 'Classic' | 'LeftTop' | 'Top' | 'Test'): void {
     this.layout = layout
   }
+  @Mutation
+  private SET_BREADCRUMB(showBreadcrumb: boolean): void {
+    this.showBreadcrumb = showBreadcrumb
+  }
+  @Mutation
+  private SET_HAMBURGER(showHamburger: boolean): void {
+    this.showHamburger = showHamburger
+  }
+  @Mutation
+  private SET_SCREENFULL(showScreenfull: boolean): void {
+    this.showScreenfull = showScreenfull
+  }
+  @Mutation
+  private SET_USERINFO(showUserInfo: boolean): void {
+    this.showUserInfo = showUserInfo
+  }
 
   @Action
   public SetCollapsed(collapsed: boolean): void {
     this.SET_COLLAPSED(collapsed)
   }
+  @Action
   public SetShowLogo(showLogo: boolean): void {
     this.SET_SHOWLOGO(showLogo)
   }
+  @Action
   public SetShowTags(showTags: boolean): void {
     this.SET_SHOWTAGS(showTags)
   }
+  @Action
   public SetShowNavbar(showNavbar: boolean): void {
     this.SET_NAVBAR(showNavbar)
   }
+  @Action
   public SetFixedTags(fixedTags: boolean): void {
     this.SET_FIXEDTAGS(fixedTags)
   }
+  @Action
   public SetFixedNavbar(fixedNavbar: boolean): void {
     this.SET_FIXEDNAVBAR(fixedNavbar)
   }
+  @Action
   public SetLayout(layout: 'Classic' | 'LeftTop' | 'Top' | 'Test'): void {
     this.SET_LAYOUT(layout)
   }
+  @Action
+  public SetBreadcrumb(showBreadcrumb: boolean): void {
+    this.SET_BREADCRUMB(showBreadcrumb)
+  }
+  @Action
+  public SetHamburger(showHamburger: boolean): void {
+    this.SET_HAMBURGER(showHamburger)
+  }
+  @Action
+  public SetScreenfull(showScreenfull: boolean): void {
+    this.SET_SCREENFULL(showScreenfull)
+  }
+  @Action
+  public SetUserInfo(showUserInfo: boolean): void {
+    this.SET_USERINFO(showUserInfo)
+  }
 }
 
 export const appStore = getModule<App>(App)

+ 1 - 0
src/pages/index/store/modules/tagsView.ts

@@ -19,6 +19,7 @@ class TagsView extends VuexModule implements TagsViewState {
   @Mutation
   private ADD_VISITED_VIEW(view: RouteLocationNormalizedLoaded): void {
     if (this.visitedViews.some((v: RouteLocationNormalizedLoaded) => v.path === view.path)) return
+    if (view.meta?.noTagsView) return
     this.visitedViews.push(
       Object.assign({}, view, {
         title: view.meta.title || 'no-name'

+ 64 - 1
src/pages/index/views/dashboard/index.vue

@@ -16,6 +16,70 @@
     <div class="chart__wrap">
       <echart :options="lineEchatOptions" :height="'300px'" />
     </div>
+    <panel-group />
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart__wrap">
+          <echart :options="pieEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart__wrap">
+          <echart :options="barEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+    <div class="chart__wrap">
+      <echart :options="lineEchatOptions" :height="'300px'" />
+    </div>
+    <panel-group />
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart__wrap">
+          <echart :options="pieEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart__wrap">
+          <echart :options="barEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+    <div class="chart__wrap">
+      <echart :options="lineEchatOptions" :height="'300px'" />
+    </div>
+    <panel-group />
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart__wrap">
+          <echart :options="pieEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart__wrap">
+          <echart :options="barEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+    <div class="chart__wrap">
+      <echart :options="lineEchatOptions" :height="'300px'" />
+    </div>
+    <panel-group />
+    <el-row :gutter="20">
+      <el-col :span="10">
+        <div class="chart__wrap">
+          <echart :options="pieEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+      <el-col :span="14">
+        <div class="chart__wrap">
+          <echart :options="barEchatOptions" :height="'300px'" />
+        </div>
+      </el-col>
+    </el-row>
+    <div class="chart__wrap">
+      <echart :options="lineEchatOptions" :height="'300px'" />
+    </div>
   </div>
 </template>
 
@@ -46,7 +110,6 @@ export default defineComponent({
 
 <style lang="less" scoped>
 .chart__wrap {
-  width: 100%;
   margin-bottom: 20px;
   border-radius: 5px;
   background-color: @contentBg;

+ 10 - 3
src/pages/index/views/level/Menu111.vue

@@ -1,13 +1,20 @@
 <template>
   <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
     <div style="min-width: 200px;">多层级缓存-页面1-1-1:</div>
-    <el-input />
+    <el-input v-model="value" />
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { defineComponent, ref } from 'vue'
 export default defineComponent({
-  name: 'Menu111Demo'
+  name: 'Menu111Demo',
+  setup() {
+    const value = ref<string>('')
+
+    return {
+      value
+    }
+  }
 })
 </script>

+ 10 - 3
src/pages/index/views/level/Menu12.vue

@@ -1,13 +1,20 @@
 <template>
   <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
     <div style="min-width: 200px;">多层级缓存-页面1-2:</div>
-    <el-input />
+    <el-input v-model="value" />
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { defineComponent, ref } from 'vue'
 export default defineComponent({
-  name: 'Menu12Demo'
+  name: 'Menu12Demo',
+  setup() {
+    const value = ref<string>('')
+
+    return {
+      value
+    }
+  }
 })
 </script>

+ 10 - 3
src/pages/index/views/level/Menu2.vue

@@ -1,13 +1,20 @@
 <template>
   <div style="padding: 20px; background: #fff;display: flex;align-items: center;">
     <div style="min-width: 200px;">多层级缓存-页面2:</div>
-    <el-input />
+    <el-input v-model="value" />
   </div>
 </template>
 
 <script lang="ts">
-import { defineComponent } from 'vue'
+import { defineComponent, ref } from 'vue'
 export default defineComponent({
-  name: 'Menu2Demo'
+  name: 'Menu2Demo',
+  setup() {
+    const value = ref<string>('')
+
+    return {
+      value
+    }
+  }
 })
 </script>

+ 2 - 2
src/styles/reset.css

@@ -130,8 +130,8 @@ table {
 
 /* custom */
 a {
-  /* color: #7e8c8d; */
-  /* text-decoration: none; */
+  /* color: #606266; */
+  text-decoration: none;
   -webkit-backface-visibility: hidden;
 }
 

+ 1 - 0
src/styles/variables.less

@@ -31,6 +31,7 @@
 
 // content
 @contentBg: #fff;
+@appBg: #f5f7f9;
 
 // html body
 @minWidth: 992px;