上一篇我们集成项目所需要的插件,在整个main.ts里的结构都抽离到了各自相应的文件中,使整体main的结构看起来更加简介明了。今天我们讲下项目页面的布局
布局分析
分析我们页面的布局,主要是比较常见的左右结构布局,这在后台管理系统中是很常见的布局。当然也有其他的布局,在我们这次的项目里没有其它类型,但你可以自己添加,这里提供一种思路
- 首先在页面提供component这个动态组件,我们设置不同类型的布局或者Json搜索项都可以采用这种方式
- 其次将需要的页面布局模式引入到当前页面,并通过对象模式引用
- 通过结合pinia的store改变layout的属性值
<template>
<component :is="components[layout]" />
</template>
<script setup lang="ts">
import { computed } from "vue";
import { useThemeStore } from "@/stores/modules/theme";
import mode1 from "xxx.vue"; // 布局模式1
import mode2 from "xxx.vue"; // 布局模式2
import mode2 from "xxx.vue"; // 布局模式3
const components = {
mode1: mode1,
mode2: mode2,
mode3: mode3,
};
const themeStore = useThemeStore();
const layout = computed(() => themeStore.layout);
</script>
下面我们说下本项目具体的布局方式
<template>
<el-container class="h-full" ref="appWrapperRef">
<AppMask v-show="showAppMask" @click="closeAppMask" />
<Asider />
<el-container direction="vertical" class="relative">
<Header />
<NavTab />
<Main />
<AppSetting />
</el-container>
</el-container>
</template>
Asider
Asider主要有两个组件,分别是Logo组件和Menu组件,Logo就放个文本和图片通过Store控制表现形式就可以了,主要讲下Menu组件
在Menu组件外部需要包裹一个el-scrollbar
组件,通过这个组件可以让menu过多时产生滑动效果,里面内部放置一个menuItem
组件,注意组件拆分,方便维护。
在menu组件中,我们主要把一些不太常用的属性功能交给全局Store去配置,对于默认激活项这里采用useRouter
获取当前路由的path,点击事件使用select
事件,可以通过回调拿到menuItem
的index,这里我们通过一个正则判断/http(s)?:/.test(key)
判断是否是外链链接,如果是通过window.open
跳转,如果不是通过router.push
跳转就行了
// menu
<template>
<el-scrollbar>
<el-menu
:default-active="activeMenu"
class="!border-0 !w-full"
:unique-opened="themeStore.menuUnique"
:collapse-transition="false"
:collapse="appStore.isCollapse"
@select="handleSelect"
>
<MenuItem v-for="item in menus" :key="item.path" :menu="item" />
</el-menu>
</el-scrollbar>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import { useRouter } from 'vue-router';
import { useAppStore, useThemeStore, useRouteStore } from '@/store';
import MenuItem from './MenuItem.vue';
const router = useRouter();
const appStore = useAppStore();
const themeStore = useThemeStore();
const routeStore = useRouteStore();
const menus = computed(() => routeStore.menus);
const activeMenu = computed(() => router.currentRoute.value.path);
const handleSelect = (key: string) => {
if (/http(s)?:/.test(key)) {
window.open(key);
} else {
router.push(key);
}
};
</script>
这里需要提醒一点,使用
el-icon
的setting图标,如果size设置大小为18,在浏览器下会出现卡段,所以这里我们直接设置size大小19避开它。这里的Icon是自己的封装组件,后面会详细介绍
// menuItem
<template>
<el-sub-menu :index="menu.path" v-if="menu.children && menu.children.length > 0">
<template #title>
<Icon :name="menu.icon" size="19" v-if="menu.icon" />
<span class="truncate">{{ $t(menu.title) }}</span>
</template>
<MenuItem v-for="menuItem in menu.children" :key="menuItem.path" :menu="menuItem" />
</el-sub-menu>
<el-menu-item :index="menu.path" v-else>
<Icon :name="menu.icon" size="19" v-if="menu.icon" />
<span class="truncate">{{ $t(menu.title) }}</span>
</el-menu-item>
</template>
<script lang="ts" setup>
interface Props {
menu: App.Menu;
}
defineProps<Props>();
</script>
NavTab
header比较简单我们就不说了,navTab组件有几种方式实现,可以使用el-scrollbar
配置滑动
自行实现,还有一种就是借助element的el-tabs
- 初始化tab需要的tag
首先获取本地缓存的tab属性值,如果没有,从router获取路由传递给filterAffixTags
方法进行过滤,拿到meta属性里affix
- 添加、切换、删除tab都需要通过store里的方法进行调用
- 在onMounted里调用
initTabs & addTab
- 通过watch检测
route.fullPath
,如果发生变化触发addTab
,内部可以对已存在的tag进行判断,这样可以保证刷新页面或者切换菜单自动增加tab
// index.ts
// 初始化tabs
const router = useRouter();
const initTabs = () => {
const routes = router.getRoutes();
const tabs = storage.get('navTab') ?? filterAffixTags(routes);
for (const tab of tabs) {
navTabStore.add(tab);
}
};
// 添加tab
const addTab = () => {
const tab = {
fullPath: route.fullPath,
title: route.meta.title,
affix: route.meta.affix || false,
icon: route.meta.icon
};
navTabStore.add(tab);
};
// 切换tab
const handleChange = (fullPath: TabPaneName) => {
router.push(fullPath as string);
resetMenuStatus();
};
// 删除tab
const handleRemove = (fullPath: TabPaneName) => {
navTabStore.closeCurrent(fullPath as string);
};
onMounted(() => {
initTabs();
addTab();
});
watch(
() => route.fullPath,
() => {
addTab();
}
);
// ./helper.ts
// 过滤固定页签
import { RouteRecordRaw } from 'vue-router';
export const filterAffixTags = (routes: RouteRecordRaw[]) => {
const tags: App.TabsView[] = [];
routes.forEach((route: RouteRecordRaw) => {
if (route.meta?.affix) {
tags.push({
title: route.meta?.title,
fullPath: route.path,
icon: route.meta?.icon,
affix: route.meta?.affix
});
}
});
return tags;
};
NavTab 右键菜单
- 使用
@contextmenu.prevent
检测右键事件 - 准备一个ul的列表,可以搭配
transition
在切换时做下动画
//右键事件
//打开ul列表显示,通过传入的affix和fullPath控制ul列表的显示隐藏和禁用状态,
调整ul的left和top属性防止溢出
const handleContextMenu = (e: any, fullPath: string, affix?: boolean) => {
showFilterMenu(fullPath, affix); //筛选显示的右键菜单
const menuMinWidth = 105;
const offsetLeft = navTabRef.value.getBoundingClientRect().left; // container margin left
const offsetWidth = navTabRef.value.offsetWidth; // container width
const maxLeft = offsetWidth - menuMinWidth; // left boundary
const left = e.clientX - offsetLeft + 15; // 15: margin right
contextmenuLeft.value = left > maxLeft ? maxLeft : left;
contextmenuTop.value = e.clientY;
contextmenuVisible.value = true;
};
Main
el-main
配置一下背景颜色,我们项目的页面基础颜色全部采用它的颜色- 配置
el-scrollbar
router-view
使用配置缺口路由- 使用
keep-alive
缓存页面
对于缓存在vue里实现比react简单,但是如果路由是嵌套状态的情况下,配置根路由的缓存对子路由是不起作用的,我们需要在子路由下配置缓存状态,或者还有一种方法,可以结合beforeEach路由钩子在这里将路由缓存到store里
<template>
<el-main class="mt-1px !p-0 bg-[var(--el-bg-color-page)]">
<el-scrollbar>
<div class="p-3 h-full">
<router-view>
<template #default="{ Component, route }">
<el-backtop title="回到顶部" target=".el-main .el-scrollbar__wrap" />
<transition :name="themeStore.animateMode" mode="out-in" appear>
<keep-alive :include="routeStore.cacheList">
<component :is="Component" :key="route.fullPath" v-if="appStore.reloadFlag" />
</keep-alive>
</transition>
</template>
</router-view>
</div>
</el-scrollbar>
</el-main>
</template>
本文项目地址:Element-Admin
这样项目的布局结构就介绍完毕了,下一篇将介绍一下本项目的一些hooks封装