L
O
A
D
I
N
G

vite 搭建vue3项目(二)

1.login页面和功能就不多哔哔了

2.主体布局

dd7860edba87d1d65d1c03b81f01f4c6.png
layout->index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<template>
<el-container>
<el-aside>
<Menu />
</el-aside>
<el-container>
<el-header>
</el-header>
<el-main>
//mian开发中
<!-- <router-view v-slot="{ Component, route }">
<transition appear name="fade-transform" mode="out-in">
<keep-alive :include="cacheRouter">
<component :is="Component" :key="route.path"></component>
</keep-alive>
</transition>
</router-view> -->
</el-main>
<el-footer>
<Footer />
</el-footer>
</el-container>
</el-container>
</template>

<script setup lang="ts">
import Footer from "./footer/index.vue";
import Menu from "./Menu/index.vue";
</script>

<style lang="scss" scoped>
@import "./index.scss";
</style>

3.vite中的批量自动化导入:import.meta.globEager

如果想在vite中批量导入某些文件,实现项目的模块化,vite提供的import.meta.globEager函数就很好用

比如用在路由模块化:

1、需求:不想把路由文件全部放在一个文件里面,找的时候要拖动很麻烦,就想着把每一个模块的路由按功能分成单个的文件

2、思路:在routers文件夹内新增一个modules文件夹:里面放不同功能的routers文件,然后在vue引入的路由入口处批量导入模块化的routers

3、实现:

87e7997f3c15921b398a37d928ac3a32.png

在router文件内批量引入modules内模块化的文件并处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";

// * 导入所有router
//const metaRouters = import.meta.globEager("./modules/*.ts");
//最新vite应该是弃用了上面的,用下面的
const metaRouters:any = import.meta.glob('./modules/*.ts', { eager: true })
// * 处理路由表
export const routerArray: RouteRecordRaw[] = [];
Object.keys(metaRouters).forEach(item => {
Object.keys(metaRouters[item]).forEach((key: any) => {
// routerArray.push(...metaRouters[item][key]);
routerArray.push(metaRouters[item][key]);
});
});

/**
* @description 路由配置简介
* @param path ==> 路由路径
* @param name ==> 路由名称
* @param redirect ==> 路由重定向
* @param component ==> 路由组件
* @param meta ==> 路由元信息
* @param meta.requireAuth ==> 是否需要权限验证
* @param meta.keepAlive ==> 是否需要缓存该路由
* @param meta.title ==> 路由标题
* @param meta.key ==> 路由key,用来匹配按钮权限
* */
const routes: RouteRecordRaw[] = [
...routerArray,
];

const router = createRouter({
history: createWebHashHistory(),
routes,
strict: false,
// 切换页面,滚动到最顶部
scrollBehavior: () => ({ left: 0, top: 0 })
});

export default router;

注意 再使用时出现import.meta.globEager(“./modules/*.ts”);报错说什么弃用了,

去源码看标注:已弃用,使用这个什么代替

@deprecated Use import.meta.glob('*', { eager: true }) instead

4.侧边栏的开发和header里的侧边栏折叠(底部栏就不多bb)

侧边栏的开发

主要是分为两部分,一部分是logo,一部分是路由菜单

logo是图片加文字,文字根据侧边栏折叠是否展示

路由菜单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<el-scrollbar>
<el-menu
:default-active="activeMenu"
:router="true"
:collapse="isCollapse"
:collapse-transition="false"
:unique-opened="true"
background-color="#191a20"
text-color="#bdbdc0"
active-text-color="#fff"
>
//菜单项
<SubItem :menuList="menuList" />
</el-menu>
</el-scrollbar>

首先需要获取菜单列表,一般是调用接口根据登录的这个用户的权限获取列表接口,暂时用得请求的json模拟后台接口数据,把菜单数据存到pinia中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
onMounted(async () => {
// 获取菜单列表
loading.value = true;
try {
const res = await getMenuList();
if (!res.data) return;
// 把路由菜单处理成一维数组(存储到 pinia 中)
const dynamicRouter = handleRouter(res.data);
authStore.setAuthRouter(dynamicRouter);
menuStore.setMenuList(res.data);
} finally {
loading.value = false;
}
});

然后需要有默认激活菜单的index和菜单是否折叠 获取pinia里存着的菜单数据

1
2
3
4
5
6
//默认激活菜单的 index,当前路由对象的路径
const activeMenu = computed((): string => route.path);
//菜单是否折叠
const isCollapse = computed((): boolean => menuStore.isCollapse);
//菜单数据
const menuList = computed((): Menu.MenuOptions[] => menuStore.menuList);
1
2
3
4
5
6
7
8
9
10
11
12
// 监听窗口大小变化,折叠侧边栏
const screenWidth = ref<number>(0);
const listeningWindow = () => {
window.onresize = () => {
return (() => {
screenWidth.value = document.body.clientWidth;
if (isCollapse.value === false && screenWidth.value < 1200) menuStore.setCollapse();
if (isCollapse.value === true && screenWidth.value > 1200) menuStore.setCollapse();
})();
};
};
listeningWindow();

菜单项就是遍历菜单数据展示路由菜单信息(子组件需要defineProps<{ menuList: Menu.MenuOptions[] }>();)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template v-for="subItem in menuList" :key="subItem.path">
<el-sub-menu v-if="subItem.children && subItem.children.length > 0" :index="subItem.path">
<template #title>
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<span>{{ subItem.title }}</span>
</template>
<SubItem :menuList="subItem.children" />
</el-sub-menu>
<el-menu-item v-else :index="subItem.path">
<el-icon>
<component :is="subItem.icon"></component>
</el-icon>
<template v-if="!subItem.isLink" #title>
<span>{{ subItem.title }}</span>
</template>
<template v-else #title>
<a class="menu-href" :href="subItem.isLink" target="_blank">{{ subItem.title }}</a>
</template>
</el-menu-item>
</template>
<script setup lang="ts">
defineProps<{ menuList: Menu.MenuOptions[] }>();
</script>

header里的侧边栏折叠开发

监控屏幕宽度或点击叠判断是否折叠

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//菜单是否折叠
const isCollapse = computed((): boolean => menuStore.isCollapse);
//菜单数据
const menuList = computed((): Menu.MenuOptions[] => menuStore.menuList);
// 监听窗口大小变化,合并 aside
const screenWidth = ref<number>(0);
const listeningWindow = () => {
window.onresize = () => {
return (() => {
screenWidth.value = document.body.clientWidth;
if (isCollapse.value === false && screenWidth.value < 1200) menuStore.setCollapse();
if (isCollapse.value === true && screenWidth.value > 1200) menuStore.setCollapse();
})();
};
};

5.vue-router 利用 $route 的 matched 属性实现面包屑效果

matched 顾名思义 就是 匹配,假如我们目前的路由是/a/aa-01,那么此时 this.$route.matched匹配到的会是一个数组,包含 ‘/‘,’/a’,’/a/aa-01’,这三个path的路由信息。然后我们可以直接利用路由信息渲染我们的面包屑导航。

布局需要使用到el-breadcrumb ,和transition-group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<el-breadcrumb :separator-icon="ArrowRight">
<transition-group name="breadcrumb" mode="out-in">
<el-breadcrumb-item :to="{ path: HOME_URL }" key="/home">首页</el-breadcrumb-item>
<el-breadcrumb-item v-for="item in matched" :key="item.path" :to="{ path: item.path }">
{{ item.meta.title }}
</el-breadcrumb-item>
</transition-group>
</el-breadcrumb>
</template>

<script setup lang="ts">
import { computed } from "vue";
import { useRoute } from "vue-router";
import { ArrowRight } from "@element-plus/icons-vue";
import { HOME_URL } from "@/config/config";
const route = useRoute();

const matched = computed(() => route.matched.filter(item =>item.meta && item.meta.title && item.meta.title !== "首页"));
</script>

TransitionGroup# 是一个内置组件,用于对 v-for 列表中的元素或组件的插入、移除和顺序改变添加动画效果。这样每次选择侧边栏的路由时,面包屑导航这边就感觉比较平滑的展示

6.后台管理系统顶部使用el-tag或el-tab实现浏览路由历史实现 (标签栏管理)

1.默认有首页,不能关闭

主要就是在tabs.ts的state的tabsMenuList写死,剩下的路由历史就是往这里面tabsMenuList添加数据,剩下的就在actions里面处理了,完成增加,移除,选择,路由历史的操作具体在下面

1
2
3
4
state: (): TabsState => ({
tabsMenuValue: HOME_URL,
tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "home-filled", close: false }]
}),

2.点击侧边栏上路由菜单,判断有无存在,没有就添加同时定位到上面(也就是设置tabsMenuValue),有就定位到上面

在actions里写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Add Tabs
async addTabs(tabItem: TabsOptions) {
// not add tabs black list
if (TABS_BLACK_LIST.includes(tabItem.path)) return;
const tabInfo: TabsOptions = {
title: tabItem.title,
path: tabItem.path,
close: tabItem.close
};
if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
this.tabsMenuList.push(tabInfo);
}
this.setTabsMenuValue(tabItem.path);
},

3.关闭当前页,自动跳到上一个tag页面

在actions里写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// Remove Tabs
async removeTabs(tabPath: string) {
let tabsMenuValue = this.tabsMenuValue;
const tabsMenuList = this.tabsMenuList;
if (tabsMenuValue === tabPath) {
tabsMenuList.forEach((item, index) => {
if (item.path !== tabPath) return;
const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
if (!nextTab) return;
tabsMenuValue = nextTab.path;
router.push(nextTab.path);
});
}
this.tabsMenuValue = tabsMenuValue;
this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath);
},

4.选中标签 跳转到标签对应的路由

1
2
3
4
5
6
// Change Tabs
async changeTabs(tabItem: TabPaneProps) {
this.tabsMenuList.forEach(item => {
if (item.title === tabItem.label) router.push(item.path);
});
},
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<div class="tabs-box">
<div class="tabs-menu">
<el-tabs v-model="tabsMenuValue" type="card" @tab-click="tabClick" @tab-remove="removeTab">
<el-tab-pane
v-for="item in tabsMenuList"
:key="item.path"
:path="item.path"
:label="item.title"
:name="item.path"
:closable="item.close"
>
<template #label>
<el-icon class="tabs-icon" v-if="item.icon">
<component :is="item.icon"></component>
</el-icon>
{{ item.title }}
</template>
</el-tab-pane>
</el-tabs>
//<MoreButton />
</div>
</div>

页面上具体使用的el-tabs实现

总结:

在store->modules->tabs.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import { defineStore } from "pinia";
import { TabPaneProps } from "element-plus";
import { TabsState } from "../interface";
import { HOME_URL, TABS_BLACK_LIST } from "@/config/config";
import piniaPersistConfig from "@/config/piniaPersist";
import router from "@/router/index";

// TabsStore
export const TabsStore = defineStore({
id: "TabsState",
state: (): TabsState => ({
tabsMenuValue: HOME_URL,
tabsMenuList: [{ title: "首页", path: HOME_URL, icon: "home-filled", close: false }]
}),
getters: {},
actions: {
// Add Tabs
async addTabs(tabItem: TabsOptions) {
// not add tabs black list
if (TABS_BLACK_LIST.includes(tabItem.path)) return;
const tabInfo: TabsOptions = {
title: tabItem.title,
path: tabItem.path,
close: tabItem.close
};
if (this.tabsMenuList.every(item => item.path !== tabItem.path)) {
this.tabsMenuList.push(tabInfo);
}
this.setTabsMenuValue(tabItem.path);
},
// Remove Tabs
async removeTabs(tabPath: string) {
let tabsMenuValue = this.tabsMenuValue;
const tabsMenuList = this.tabsMenuList;
if (tabsMenuValue === tabPath) {
tabsMenuList.forEach((item, index) => {
if (item.path !== tabPath) return;
const nextTab = tabsMenuList[index + 1] || tabsMenuList[index - 1];
if (!nextTab) return;
tabsMenuValue = nextTab.path;
router.push(nextTab.path);
});
}
this.tabsMenuValue = tabsMenuValue;
this.tabsMenuList = tabsMenuList.filter(item => item.path !== tabPath);
},
// Change Tabs
async changeTabs(tabItem: TabPaneProps) {
this.tabsMenuList.forEach(item => {
if (item.title === tabItem.label) router.push(item.path);
});
},
// Set TabsMenuValue
async setTabsMenuValue(tabsMenuValue: string) {
this.tabsMenuValue = tabsMenuValue;
},
// Set TabsMenuList
async setTabsMenuList(tabsMenuList: TabsOptions[]) {
this.tabsMenuList = tabsMenuList;
},
// Close MultipleTab
async closeMultipleTab(tabsMenuValue?: string) {
this.tabsMenuList = this.tabsMenuList.filter(item => {
return item.path === tabsMenuValue || item.path === HOME_URL;
});
},
// Go Home
async goHome() {
router.push(HOME_URL);
this.tabsMenuValue = HOME_URL;
}
},
persist: piniaPersistConfig("TabsState")
});

最终效果图:

46972a97567f3ff7c445d12b442ef7b1.png

gitHub地址:

vue3学习完成的后管模板

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

我是穷比,在线乞讨!

支付宝
微信