1、vue后台系统-整体布局

292 阅读1分钟

一、代码布局

Image_20230131180130.png

二、页面布局

Image_20230131175703.png

三、layout代码块(外层->里层)

1、index.vue

<template>
  <el-container class="container">
    <ZcToggle :defaultValue="false" v-slot="item" :onresize="true" >
      <Sidebar :provideFn="provideFn(item)"/>
      <el-container class="right-container">
        <Navbar />
        <TagsView />
        <AppMain />
      </el-container>
    </ZcToggle>
  </el-container>
</template>

<script lang="ts" setup>
import Sidebar from './components/Sidebar/index.vue'
import Navbar from './components/Navbar/index.vue'
import TagsView from './components/TagsView/index.vue'
import AppMain from './components/AppMain.vue'
import ZcToggle from '@/components/ZcToggle.vue'
import { ref,provide} from 'vue'
import { Fn } from '@vueuse/shared'

interface ItemObj {
  isCollapsible:boolean;
  onToggle:Fn;
}

const fatherProps = ref({} as ItemObj)

provide('fatherProps',fatherProps.value)

const provideFn = (item:ItemObj)=>{
  fatherProps.value.isCollapsible = item.isCollapsible
  fatherProps.value.onToggle = item.onToggle
}

</script>

<style lang="scss" scoped>
  .container{
    height:100%;
    .el-aside{
      background: #414954;
      width: auto;
    }
    .right-container{
      display: flex;
      flex-direction: column;
    }
  }
</style>

2、ZcToggle.vue(展开收起--类似菜单是否收缩)

<script lang="ts" setup>
import { withDefaults,defineProps,ref} from 'vue'

const props = withDefaults(defineProps<{
  defaultValue: boolean ,
  onresize:boolean
	}>(),{
	  defaultValue:true,
    onresize:false
	})

let isCollapsible = ref(props.defaultValue)

const onToggle = ()=>{
  if(isCollapsible.value){
    isCollapsible.value = !isCollapsible.value
  }else{
    isCollapsible.value = true
  }
}

// 是否需要根据视窗变更,而数据变更:菜单
if(props.onresize){
  window.onresize = () => {
    const clientWidth = document.documentElement.clientWidth
    isCollapsible.value = clientWidth<1024
  }
}

</script>

<template>
  <slot :isCollapsible="isCollapsible" :onToggle="onToggle"></slot>
</template>

3、Sidebar

// index.vue
<template>
  <el-aside class="tac">
    <div :class="isCollapse?'header-logo el-menu--collapse':'header-logo'">logo</div>
      <el-menu
        :default-active="defaultActive"
        class="el-menu-vertical-demo"
        :collapse="isCollapse"
        @select="handleSelect"
      >
      <SideItem v-for="(item,index) of routers" :key="index" :route="item" />
      </el-menu>
  </el-aside>
</template>

<script lang="ts" setup>

import SideItem from './SideItem.vue'
import {useRouter} from 'vue-router'  
import { computed,inject} from 'vue'
import {useUserInfoStore} from '@/stores/userInfo.ts'

const store = useUserInfoStore()

const router= useRouter()

// 路由
const routers = store.routerList.filter((item:any)=>{
  return !item.hidden
})

const props = inject('fatherProps') as any

const isCollapse =computed(()=>{
  return props.isCollapsible
})


const defaultActive = computed(()=>{
  return router.currentRoute.value.name 
})

const handleSelect = (index:string)=>{
  router.push({name:index})
}

</script>

<style lang="scss" scoped>
.tac{
  text-align: center;
  .header-logo{
    line-height: 64px;
    color: white;
    font-weight: 600;
  }
  .el-menu {
    border-right: 0;
  }
  .el-menu-vertical-demo:not(.el-menu--collapse) {
    width: 220px;
    min-height: 400px;
  }
}
</style>
// SideItem.vue
<template>
  <div>
    <el-sub-menu v-if="route.children&&route.children.length>1" :index="route.name">
      <template #title>
        <el-icon v-if="isFirstLevel(route.path)"><location /></el-icon>
        <span class="title-label">{{route.meta&&route.meta.title||''}}</span>
      </template>
      <SideItem v-for="(item,index) of route.children" :key="index" :route="item" />
    </el-sub-menu>
    <el-menu-item v-else  :index="route.name"> 
      <el-icon v-if="isFirstLevel(route.path)"><icon-menu /></el-icon>
      <span>{{route.meta&&route.meta.title||''}}</span>
    </el-menu-item>
  </div>
</template>

<script lang="ts" setup>
import {
  Menu as IconMenu,
  Location
} from '@element-plus/icons-vue'
import {defineProps} from 'vue'

defineProps<{
  route: any
}>()

// 是否是顶层
const isFirstLevel = (path:string)=>{
  return path.includes('/')
}
</script>

<style lang="scss" scoped>

</style>

4、Navbar

// index.vue
<!-- eslint-disable vue/multi-word-component-names -->
<template>
  <el-header class="navbar-container">
    <LeftNavbar />
    <div class="right-content">
      <el-avatar :src="src" :icon="UserFilled" />
      <el-dropdown @command="onCommand">
        <el-icon class="el-icon--right">
          <CaretBottom />
        </el-icon>
        <template #dropdown>
          <el-dropdown-menu>
            <el-dropdown-item v-for="(item,index) of dropOptions" :key="index" :disabled="item.disabled" :command="item.value">{{item.label}}</el-dropdown-item>
          </el-dropdown-menu>
        </template>
      </el-dropdown>
    </div>
  </el-header>
</template>

<script lang="ts" setup>
import LeftNavbar from './LeftNavbar.vue'
import { ref,computed} from 'vue'
import { UserFilled } from '@element-plus/icons-vue'
import { CaretBottom } from '@element-plus/icons-vue'
import {useRouter} from 'vue-router'  
import {logout} from '@/api/login.ts'

const src = ref('https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fthumbs.gfycat.com%2FCostlyEvenHydra-size_restricted.gif&refer=http%3A%2F%2Fthumbs.gfycat.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1673078839&t=f69323ab93068dd098c725c56528aa3d')

interface DropObj {
  value:string
  label:string
  disabled?:boolean
}

const router = useRouter()

const dropOptions = computed(()=>{
  let options = [
    {
        value:'Dashboard',
        label:'首页'
      },
      {
        value:'Login',
        label:'登出'
      }
    ] as DropObj[]
    const currentPath = router.currentRoute.value
    if(currentPath.name==='Dashboard'){
      options[0].disabled = true
    }
  return options
})

const onCommand =async (command: string)=>{
  if(command==='Login'){
    await logout().then((res:any)=>{
      if(res.data){
        localStorage.clear()
      }
    })
  }
  router.push({name:command})
}

</script>

<style lang="scss" scoped>
  .navbar-container{
    display: flex;
    justify-content: space-between;
    height: inherit;
    align-items: center;
    .right-content{
      img{
        cursor: pointer;
        width: 40px;
        height: 40px;
        border-radius: 10px;
      }
      .el-dropdown{
        color:var(--el-color-primary-light-9);
        vertical-align:bottom;
      }
    }
  }
</style>

// LeftNavbar.vue
<template>
  <div class="container">
    <div class="left-content">
        <el-icon v-if="props.isCollapsible" @click="props.onToggle"><Fold /></el-icon>
        <el-icon v-if="!props.isCollapsible" @click="props.onToggle"><Expand /></el-icon>
    </div>
    <div class="right-content">
      <el-breadcrumb separator="/">
        <el-breadcrumb-item v-for="(item,index) of currentRouters" :key="index">{{item.meta.title||''}}</el-breadcrumb-item>
      </el-breadcrumb>
    </div>  
  </div>
</template>

<script lang="ts" setup>
import { Fold,Expand } from '@element-plus/icons-vue'
import { inject,ref,watch} from 'vue'
import {useRouter,RouteLocationMatched,RouteLocationNormalizedLoaded} from 'vue-router'  

const props = inject('fatherProps') as any

const { currentRoute } = useRouter();

const currentRouters = ref<RouteLocationMatched[]>([]);

watch(() => currentRoute.value,(route: RouteLocationNormalizedLoaded)=>{
  currentRouters.value = route.matched || []
},{
  immediate:true
})


</script>

<style lang="scss" scoped>
  .container{
    display:flex;
    align-items: center;
    .left-content{
      height: 1em;
      margin-right: 10px;
      cursor: pointer;
    }
  }
</style>

5、TagsView

// index.vue
<template>
  <div class="tag-container">
    <div class="left-container" ref="leftContainerRef">
      <el-tag
        v-for="(tag,index) in tagsView"
        :ref="e=>setTagListRef(e,index)"
        :data-set="'data'+index"
        :key="tag.name"
        class="mx-1"
        :closable="index!=0"
        size="large"
        @click="onJumpRoute(tag)"
        @close="onCloseTag(tag,index)"
        :type="currentTagType(tag)"
        effect="plain"
        disable-transitions
      >
      {{ tag.meta.title }}
    </el-tag>
  </div>
  <div class="right-container" type="flex">
    <el-dropdown :style="isShowMoreRoute?'':{opacity:0.01}" >
      <el-icon :color="'#213547'"><MoreFilled /></el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item v-for="(moreItem,moreIndex) of moreTagsView" :key="moreIndex" @click="onJumpRoute(moreItem)">
            <el-icon><Minus /></el-icon>{{ moreItem.meta.title }}
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
    <el-dropdown>
        <el-icon>
          <ArrowDownBold />
        </el-icon>
      <template #dropdown>
        <el-dropdown-menu>
          <el-dropdown-item @click="onCloseTag" >
            <el-icon><Minus /></el-icon>关闭全部标签页
          </el-dropdown-item>
        </el-dropdown-menu>
      </template>
    </el-dropdown>
  </div>
  </div>
</template>
<script lang="ts" setup>
  import {ref,watch,nextTick,computed} from 'vue'
  import { useRouter } from "vue-router";
  import {useTagsViewStore} from '@/stores/tagsView.ts'
  import { MoreFilled,ArrowDownBold,Minus } from '@element-plus/icons-vue'
  import {isEmpty,uniqBy} from 'lodash'

  const store = useTagsViewStore()

  const tagsView = ref(store.tagsView as any)

  const router= useRouter()

  const currentRoute = router.currentRoute as any;

  // 获取选中状态
  const currentTagType = (tag:any)=>{
    return tag.name===currentRoute.value.name?'success':''
  }

   // 左侧容器dom
   let leftContainerRef = ref<null | HTMLElement>(null)

  // 左侧容器tag的dom
  const tagListRef = ref<any>({})

  // 左侧容器tag塞ref
  const setTagListRef = (e:any,index:number)=>{
    if(e){
      tagListRef.value[index] = e
    }
  }
  const isShowMoreRoute = ref(false)

  watch(tagsView,(value)=>{
    if(value){
       // 容器总width
      const clientSumWidth:any = leftContainerRef.value?.clientWidth
      const scrollWidth:any = leftContainerRef.value?.scrollWidth
      isShowMoreRoute.value = scrollWidth>clientSumWidth
    }
  },{
    deep:true
  })

  const onCloseTag = (tag:any,index:Number)=>{
      store.deleteTag(index)
      // 如果删除的是当前的路由 tag跳转为last
      if(tag?.name===currentRoute.value.name){
        const updateRoute = tagsView.value[tagsView.value.length-1]
        router.push({name:updateRoute.name})
      }
      router.push({name:'Dashboard'})
      isInVisibleArea()
  }

  const onJumpRoute = (tag:any)=>{
    router.push({name:tag.name})
    isInVisibleArea(tag)
  }

   // 更多路由集合
   const moreTagsView = ref([] as any)

   // more路由,tag滚动到可视区域
   const scrollToView =async (tag:any)=>{
    await nextTick(()=>{
      const findIndex = tagsView.value.findIndex((item:any)=>item.name===tag.name)
      tagListRef.value[findIndex]?.$el.scrollIntoView()
    })
  }

  // 计算tag的width
  const countTagsWidth = (data:any)=>{
     data.forEach((item:any,index:number) => {
      item.meta.width = tagListRef.value[index]?.$el?.offsetWidth+5
    });
    return data
  } 

  // 计算溢出的tag
  const isInVisibleArea =async (value?:any)=>{
    if(isEmpty(tagsView?.value)) return
    if(value){
      await scrollToView(value)
    }
    // 容器总width
    const clientSumWidth:any = leftContainerRef.value?.clientWidth
    // 左边容器滚动的width(隐藏)
    const scrollLeftWidth = leftContainerRef.value?.scrollLeft

    // 容器中tag数据(每个tag添加width)
    const newTagsView = countTagsWidth(tagsView.value) || []

    let compareWidth = 0
    let scrollLeftIndex= 0 // left隐藏结束index
    let scrollRightIndex = 0  // right隐藏开始index

      for(let i = 0;i<newTagsView.length;i++){
        compareWidth= compareWidth+(newTagsView[i].meta.width)
        if(scrollLeftWidth&&compareWidth>scrollLeftWidth){
          scrollLeftIndex = i
            break
        }
      }
      compareWidth = 0
      for(let i = scrollLeftIndex;i<newTagsView.length;i++){
        compareWidth= compareWidth+(newTagsView[i].meta.width)
        if(compareWidth>clientSumWidth){
          scrollRightIndex = i
          break
        }
      }
      moreTagsView.value = []
      if(scrollLeftWidth){
        moreTagsView.value = scrollLeftIndex===0?[newTagsView[0]]:newTagsView.slice(0,scrollLeftIndex+1)
      }
      if(scrollRightIndex){
        moreTagsView.value = moreTagsView.value.concat(newTagsView.slice(scrollRightIndex))
      }
      moreTagsView.value = moreTagsView.value.filter((item:any)=>{
        return currentRoute.value.name!=item.name
      })
      moreTagsView.value = uniqBy(moreTagsView.value,'name')
    }

    // 监听当前路由
    watch(currentRoute, (value)=>{
      if(value){
        store.addTag(value)
        isInVisibleArea(value)
      }
    },{immediate:true,deep:true})

</script>

<style lang="scss" scoped>
  .tag-container{
    width:100%;
    border-top: 1px solid #d9d9d9;
    display: flex;
    justify-content:space-between;
    .left-container{
      padding:5px;
      display: flex;
      flex-wrap:nowrap;
      width: calc(100% - 70px );
      overflow-x:auto;
      .el-tag{
        scroll-margin: 5px;
      }
    }
    .right-container{
      display: flex;
      .el-icon{
        width:34px;
        height: inherit;
        cursor: pointer;
      }
      .el-icon:last-child{
        border-left: 1px solid #d9d9d9;
      }
    }
  }
  ::-webkit-scrollbar{
    height: 3px;
  }
</style>
<style lang="scss">
  .el-popper.is-pure{
      width:180px!important;
    }

</style>
//stores->tagsView.ts

import { defineStore } from 'pinia'
import { ref} from 'vue'

export const useTagsViewStore = defineStore('tagsView', () => {
  
  const initTag = {name:'Dashboard',meta:{title:'首页'}}
  const tagsView = ref([initTag] as any)
  
  
  const addTag = (route:any)=>{
    const routerNameList = tagsView.value.map((item:any)=>item.name)
    if(!routerNameList.includes(route.name)){
      tagsView.value.push(route)
    }
  }

  const deleteTag = (index:any)=>{
    if(index){
      return tagsView.value.splice(index,1)
    }
    tagsView.value.length = 0
    tagsView.value.push(initTag)
  }
  
  return { tagsView,addTag,deleteTag }
})

6、AppMain.vue

<template>
  <el-main >
    <div class="appMain-container">
      <router-view></router-view>
    </div>
  </el-main>
</template>

<style lang="scss" scoped>
.el-main{
  background-color: #f7f7f7;
  .appMain-container{
    height: 100%;
    background: white;
  }
}
</style>

备注:可疯狂吐槽(咸鱼不不翻身)