一、代码布局

二、页面布局

三、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
<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>
<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
<!-- 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>
<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
<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':''
}
let leftContainerRef = ref<null | HTMLElement>(null)
const tagListRef = ref<any>({})
const setTagListRef = (e:any,index:number)=>{
if(e){
tagListRef.value[index] = e
}
}
const isShowMoreRoute = ref(false)
watch(tagsView,(value)=>{
if(value){
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)
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)
const scrollToView =async (tag:any)=>{
await nextTick(()=>{
const findIndex = tagsView.value.findIndex((item:any)=>item.name===tag.name)
tagListRef.value[findIndex]?.$el.scrollIntoView()
})
}
const countTagsWidth = (data:any)=>{
data.forEach((item:any,index:number) => {
item.meta.width = tagListRef.value[index]?.$el?.offsetWidth+5
});
return data
}
const isInVisibleArea =async (value?:any)=>{
if(isEmpty(tagsView?.value)) return
if(value){
await scrollToView(value)
}
const clientSumWidth:any = leftContainerRef.value?.clientWidth
const scrollLeftWidth = leftContainerRef.value?.scrollLeft
const newTagsView = countTagsWidth(tagsView.value) || []
let compareWidth = 0
let scrollLeftIndex= 0
let scrollRightIndex = 0
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>
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>
备注:可疯狂吐槽(咸鱼不不翻身)