后台布局开发
# 后台布局开发
# 后台主布局实现
主要使用elemnt-plus
的Container
容器:文档地址https://element-plus.gitee.io/zh-CN/component/container.html (opens new window)
我们在src
下新建layouts
目录,新建一个admin.vue
后台的一个布局页面
<template>
<el-container>
<!-- 头部 -->
<el-header>
<f-header />
</el-header>
<el-container>
<el-aside>
<f-menu />
</el-aside>
<el-main>
<f-tag-list />
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import FHeader from "./components/FHeader.vue";
import FMenu from "./components/FMenu.vue";
import FTagList from "./components/FTagList.vue";
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
我们分别在其目录下新建components
,然后分离出单独的头部,左侧菜单以及标签导航栏,都是简单的页面,分别加上对应的名称
<template>
<div>头部</div>
</template>
<script setup>
</script>
<style scoped>
</style>
2
3
4
5
6
7
8
9
10
这里就只写一个了,别的都是复制粘贴,然后就
头部
两个字不一样
我们在路由中设置后台的路由,并添加子路由
import { createRouter, createWebHashHistory } from 'vue-router'
import Admin from '~/layouts/admin.vue'
import Index from '~/pages/index.vue'
import Login from '~/pages/login.vue'
import NotFound from '~/pages/404.vue'
const routes = [
{
path: '/',
component: Admin,
// 子路由
children: [
{
path: '/',
component: Index,
meta: {
title: '后台首页',
},
},
],
},
{
path: '/login',
component: Login,
meta: {
title: '登录页',
},
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound,
},
]
const router = createRouter({
history: createWebHashHistory(),
routes,
})
export default router
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
# 公共头部开发
下拉菜单文档地址https://element-plus.gitee.io/zh-CN/component/dropdown.html (opens new window)
头像组件https://element-plus.gitee.io/zh-CN/component/avatar.html (opens new window)
<template>
<!-- 水平方向 -->
<div class="f-header">
<span class="logo">
<el-icon class="mr-1"><Promotion /></el-icon>
无解的游戏
</span>
<!-- 收缩图标 -->
<el-icon class="icon-btn"><Fold /></el-icon>
<!-- 刷新图标 -->
<el-icon class="icon-btn"><RefreshRight /></el-icon>
<div class="ml-auto flex items-center">
<!-- 全屏图标 -->
<el-icon class="icon-btn"><FullScreen /></el-icon>
<el-dropdown class="dropdown">
<span class="flex items-center text-light-50">
<!-- 头像 -->
<el-avatar
class="mr-2"
:size="25"
:src="$store.state.user.avatar"
/>
<!-- 昵称 -->
{{ $store.state.user.username }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>修改密码</el-dropdown-item>
<el-dropdown-item>退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
</template>
<script setup>
</script>
<style scoped>
.f-header {
@apply flex bg-indigo-700 text-light-50 fixed top-0 left-0 right-0 items-center;
height: 64px;
}
.logo {
width: 250px;
@apply flex justify-center items-center text-xl font-thin;
}
.icon-btn {
@apply flex justify-center items-center;
width: 42px;
height: 64px;
cursor: pointer;
}
.icon-btn:hover {
@apply bg-indigo-600;
}
.f-header .dropdown {
height: 64px;
cursor: pointer;
@apply flex justify-center items-center mx-5;
}
</style>
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
# 刷新和全屏按钮
还需要使用到vueuse
的一个核心包
npm install @vueuse/core
简单实现页面点击刷新
// 刷新
const handleRefresh = () => location.reload()
2
然后给刷新按钮加上点击事件即可
<el-tooltip effect="dark" content="刷新" placement="bottom">
<el-icon class="icon-btn" @click="handleRefresh"><RefreshRight /></el-icon>
</el-tooltip>
2
3
使用vueuse
的包来实现全屏功能
引入包
import { useFullscreen } from '@vueuse/core'
// 是否全屏 全屏切换
const { isFullscreen, toggle } = useFullscreen()
2
3
4
<el-tooltip effect="dark" content="全屏" placement="bottom">
<el-icon class="icon-btn" @click="toggle">
// 判断不是全屏状态下显示全屏按钮
<FullScreen v-if="!isFullscreen" />
// 另一个图标
<Aim v-else />
</el-icon>
</el-tooltip>
2
3
4
5
6
7
8
# 修改密码
点击修改密码,弹出一个抽屉表单来进行提交修改。
抽屉文档地址:https://element-plus.gitee.io/zh-CN/component/drawer.html (opens new window)
<template>
<!-- 水平方向 -->
<div class="f-header">
<span class="logo">
<el-icon class="mr-1"><Promotion /></el-icon>
无解的游戏
</span>
<!-- 收缩图标 -->
<el-icon class="icon-btn"><Fold /></el-icon>
<!-- 刷新图标 -->
<el-tooltip effect="dark" content="刷新" placement="bottom">
<el-icon class="icon-btn" @click="handleRefresh"
><RefreshRight
/></el-icon>
</el-tooltip>
<div class="ml-auto flex items-center">
<!-- 全屏图标 -->
<el-tooltip effect="dark" content="全屏" placement="bottom">
<el-icon class="icon-btn" @click="toggle">
<FullScreen v-if="!isFullscreen" />
<Aim v-else />
</el-icon>
</el-tooltip>
<el-dropdown class="dropdown" @command="handleCommand">
<span class="flex items-center text-light-50">
<!-- 头像 -->
<el-avatar
class="mr-2"
:size="25"
:src="$store.state.user.avatar"
/>
<!-- 昵称 -->
{{ $store.state.user.username }}
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item command="rePassword"
>修改密码</el-dropdown-item
>
<el-dropdown-item command="logout"
>退出登录</el-dropdown-item
>
</el-dropdown-menu>
</template>
</el-dropdown>
</div>
</div>
<el-drawer
v-model="showDrawer"
title="修改密码"
size="45%"
:close-on-click-modal="false"
>
<el-form
ref="formRef"
:rules="rules"
:model="form"
label-width="80px"
size="small"
>
<el-form-item prop="oldpassword" label="旧密码">
<el-input
v-model="form.oldpassword"
placeholder="请输入旧密码"
></el-input>
</el-form-item>
<el-form-item prop="password" label="新密码">
<el-input
type="password"
v-model="form.password"
placeholder="请输入密码"
show-password
></el-input>
</el-form-item>
<el-form-item prop="repassword" label="确认密码">
<el-input
type="password"
v-model="form.repassword"
placeholder="请输入确认密码"
show-password
></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit" :loading="loading"
>提交</el-button
>
</el-form-item>
</el-form>
</el-drawer>
</template>
<script setup>
import { ref, reactive } from "vue";
import { logout, updatepassword } from "~/api/manager";
import { showModal, toast } from "~/composables/util";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
import { useFullscreen } from "@vueuse/core";
// 是否全屏 全屏切换
const { isFullscreen, toggle } = useFullscreen();
const store = useStore();
const router = useRouter();
// 修改密码抽屉是否弹出
const showDrawer = ref(false);
// do not use same name with ref
const form = reactive({
oldpassword: "",
password: "",
repassword: "",
});
// 定义登录验证规则
// 必须和上面表单属性一样
const rules = {
oldpassword: [
{
required: true,
message: "旧密码不能为空",
trigger: "blur",
},
],
password: [
{
required: true,
message: "新密码不能为空",
trigger: "blur",
},
],
repassword: [
{
required: true,
message: "确认密码不能为空",
trigger: "blur",
},
],
};
// 让formRef变成响应式
const formRef = ref(null);
const loading = ref(false);
const onSubmit = () => {
formRef.value.validate((valid) => {
if (!valid) {
return false;
}
loading.value = true;
updatepassword(form)
.then((res) => {
toast("修改密码成功,请重新登录");
store.dispatch("logout");
// 跳转回登录页
router.push("/login");
})
.finally(() => {
loading.value = false;
});
});
};
// 刷新
const handleRefresh = () => location.reload();
const handleCommand = (c) => {
switch (c) {
case "logout":
handleLogout();
break;
case "rePassword":
// 修改密码
showDrawer.value = true;
break;
default:
break;
}
};
function handleLogout() {
showModal("是否要退出登录?").then((res) => {
// console.log("退出登录");
logout().finally(() => {
// 不管成功,都要到这
store.dispatch("logout");
// 跳转回登录
router.push("/login");
// 提示退出成功
toast("退出成功");
});
});
}
</script>
<style scoped>
.f-header {
@apply flex bg-indigo-700 text-light-50 fixed top-0 left-0 right-0 items-center;
height: 64px;
}
.logo {
width: 250px;
@apply flex justify-center items-center text-xl font-thin;
}
.icon-btn {
@apply flex justify-center items-center;
width: 42px;
height: 64px;
cursor: pointer;
}
.icon-btn:hover {
@apply bg-indigo-600;
}
.f-header .dropdown {
height: 64px;
cursor: pointer;
@apply flex justify-center items-center mx-5;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
:::caution 注意
我们还需要在axio.js
中的全局响应拦截器里添加判断,是否是token
失效了,失效,虽然后端是退出了,但是前端的还保留着,我们还需要前端也清理一下数据。
import store from './store'
// 添加响应拦截器
service.interceptors.response.use(
function(response) {
// 对响应数据做点什么
return response.data.data
},
function(error) {
const msg = error.response.data.msg || '请求失败'
// todo 这里感觉使用文字来判断不可行,后期可以进行优化
if (msg == '非法token,请先登录!') {
store.dispatch('logout').finally(() => location.reload())
}
// 对响应错误做点什么
toast(msg, 'error')
return Promise.reject(error)
}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
:::
# form 表单抽屉组件封装
vue3 setup
暴露出去属性文档:https://cn.vuejs.org/api/sfc-script-setup.html#defineexpose (opens new window)
vue3 defimeProps defineEmits 组件文档
:https://cn.vuejs.org/api/sfc-script-setup.html#defineprops-defineemits (opens new window)
<template>
<el-drawer
v-model="showDrawer"
:title="title"
:size="size"
:close-on-click-modal="false"
:destroy-on-close="destroyOnClose"
>
<div class="formDrawer">
<div class="body">
<slot></slot>
</div>
<div class="actions">
<el-button :loading="loading" type="primary" @click="submit">{{
confrimText
}}</el-button>
<el-button type="default" @click="close">取消</el-button>
</div>
</div>
</el-drawer>
</template>
<script setup>
import { ref } from "vue";
const showDrawer = ref(false);
// 打开抽屉
const open = () => (showDrawer.value = true);
// 关闭抽屉
const close = () => (showDrawer.value = false);
const loading = ref(false);
// 显示进度条
const showLoading = () => (loading.value = true);
// 隐藏进度条
const hideLoading = () => (loading.value = false);
const emit = defineEmits(["submit"]);
// 提交 执行通知父组件
const submit = () => emit("submit");
const props = defineProps({
title: String,
size: {
type: String,
default: "45%",
},
destroyOnClose: {
type: Boolean,
default: false,
},
confrimText: {
type: String,
default: "提交",
},
});
// 向父组件暴露以下方法
defineExpose({
open,
close,
showLoading,
hideLoading,
});
</script>
<style scoped>
.formDrawer {
width: 100%;
height: 100%;
position: relative;
@apply flex flex-col;
}
.formDrawer .body {
flex: 1;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 50px;
overflow-y: auto;
}
.formDrawer .actions {
height: 50px;
@apply mt-auto flex items-center;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
然后上面的修改密码的弹出框和表单就可以简化为
<form-drawer ref="formDrawerRef" title="修改密码" destroyOnClose @submit="onSubmit">
<el-form ref="formRef" :rules="rules" :model="form" label-width="80px" size="small">
<el-form-item prop="oldpassword" label="旧密码">
<el-input v-model="form.oldpassword" placeholder="请输入旧密码"></el-input>
</el-form-item>
<el-form-item prop="password" label="新密码">
<el-input type="password" v-model="form.password" placeholder="请输入密码" show-password></el-input>
</el-form-item>
<el-form-item prop="repassword" label="确认密码">
<el-input type="password" v-model="form.repassword" placeholder="请输入确认密码" show-password></el-input>
</el-form-item>
</el-form>
</form-drawer>
2
3
4
5
6
7
8
9
10
11
12
13
// ... 其他代码
// 引入组件
import FormDrawer from '~/components/FormDrawer.vue'
const formDrawerRef = ref(null)
// 让formRef变成响应式
const formRef = ref(null)
const onSubmit = () => {
formRef.value.validate(valid => {
if (!valid) {
return false
}
// 显示进度条
formDrawerRef.value.showLoading()
updatepassword(form)
.then(res => {
toast('修改密码成功,请重新登录')
store.dispatch('logout')
// 跳转回登录页
router.push('/login')
})
.finally(() => {
// 隐藏进度条
formDrawerRef.value.hideLoading()
})
})
}
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
# 使用组合式 api 封装简化代码
我们可以将退出登录和修改密码的方法使用组合式api
进行脱离出去
新建一个composables/useManager.js
import { ref, reactive } from 'vue'
import { logout, updatepassword } from '~/api/manager'
import { showModal, toast } from '~/composables/util'
import { useRouter } from 'vue-router'
import { useStore } from 'vuex'
export function useRepassword() {
const store = useStore()
const router = useRouter()
const formDrawerRef = ref(null)
const form = reactive({
oldpassword: '',
password: '',
repassword: '',
})
// 定义登录验证规则
// 必须和上面表单属性一样
const rules = {
oldpassword: [
{
required: true,
message: '旧密码不能为空',
trigger: 'blur',
},
],
password: [
{
required: true,
message: '新密码不能为空',
trigger: 'blur',
},
],
repassword: [
{
required: true,
message: '确认密码不能为空',
trigger: 'blur',
},
],
}
// 让formRef变成响应式
const formRef = ref(null)
const onSubmit = () => {
formRef.value.validate(valid => {
if (!valid) {
return false
}
formDrawerRef.value.showLoading()
updatepassword(form)
.then(res => {
toast('修改密码成功,请重新登录')
store.dispatch('logout')
// 跳转回登录页
router.push('/login')
})
.finally(() => {
formDrawerRef.value.hideLoading()
})
})
}
const openRePasswordForm = () => formDrawerRef.value.open()
return {
formDrawerRef,
form,
rules,
formRef,
onSubmit,
openRePasswordForm,
}
}
export function useLogout() {
// 这2个内容需要写在函数内部,否则报错
const store = useStore()
const router = useRouter()
function handleLogout() {
showModal('是否要退出登录?').then(res => {
// console.log("退出登录");
logout().finally(() => {
// 不管成功,都要到这
store.dispatch('logout')
// 跳转回登录
router.push('/login')
// 提示退出成功
toast('退出成功')
})
})
}
return {
handleLogout,
}
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
原先的FHeader.vue
简化代码后
<script setup>
import { useFullscreen } from "@vueuse/core";
import FormDrawer from "~/components/FormDrawer.vue";
import { useRepassword, useLogout } from "~/composables/useManager";
// 是否全屏 全屏切换
const { isFullscreen, toggle } = useFullscreen();
const { formDrawerRef, form, rules, formRef, onSubmit, openRePasswordForm } =
useRepassword();
const { handleLogout } = useLogout();
// 刷新
const handleRefresh = () => location.reload();
const handleCommand = (c) => {
switch (c) {
case "logout":
handleLogout();
break;
case "rePassword":
// 修改密码
// showDrawer.value = true;
// formDrawerRef.value.open();
openRePasswordForm();
break;
default:
break;
}
};
</script>
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
# 侧边菜单开发-样式布局和路由跳转
我们需要使用到element-plus
的菜单栏组件:文档地址https://element-plus.gitee.io/zh-CN/component/menu.html (opens new window)
<template>
<div class="f-menu">
<el-menu @select="handleSelect" default-active="2" class="border-0">
<template v-for="(item, index) in asideMenus" :key="index">
<el-sub-menu
v-if="item.child && item.child.length > 0"
:index="item.name"
>
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</template>
<el-menu-item
v-for="(item2, index2) in item.child"
:key="index2"
:index="item2.frontpath"
>
<el-icon>
<component :is="item2.icon"></component>
</el-icon>
<span>{{ item2.name }}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.frontpath">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script setup>
import { useRouter } from "vue-router";
const router = useRouter();
const asideMenus = [
{
name: "后台面板",
icon: "help",
child: [
{
name: "主控台",
frontpath: "/",
icon: "home-filled",
},
],
},
{
name: "商城管理",
icon: "shopping-bag",
child: [
{
name: "商品管理",
frontpath: "/goods/list",
icon: "shopping-cart-full",
},
],
},
];
const handleSelect = (e) => {
// 二级菜单点击跳转
router.push(e);
};
</script>
<style scope>
.f-menu {
width: 250px;
top: 64px;
bottom: 0;
left: 0;
overflow: auto;
@apply shadow-md fixed bg-light-50;
}
</style>
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
75
76
77
78
79
80
81
# 菜单展开和收起
我们需要涉及头部组件,菜单组件以及中间布局组件的宽度的变化,所以我们需要在vuex
里定义一个状态用于存储
const store = createStore({
state() {
return {
user: {}, // 默认空对象 用户信息
// 侧边宽度
asideWidth: '250px',
}
},
mutations: {
// 记录用户信息
SET_USERINFO(state, user) {
state.user = user
},
// 展开或缩起侧边
handleAsideWidth(state) {
state.asideWidth = state.asideWidth === '250px' ? '64px' : '250px'
},
},
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 菜单栏默认宽度设置为
250px
- 收缩设置宽度为
64px
然后我们就得对菜单组件设置动态宽度
<template>
<el-container>
<!-- 头部 -->
<el-header>
<f-header />
</el-header>
<el-container>
<el-aside :width="$store.state.asideWidth">
<f-menu />
</el-aside>
<el-main>
<f-tag-list />
<router-view></router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import FHeader from "./components/FHeader.vue";
import FMenu from "./components/FMenu.vue";
import FTagList from "./components/FTagList.vue";
</script>
<style scope>
// 动画效果 收缩展开不闪
.el-aside {
transition: all 0.2s;
}
</style>
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
还需要对el-menu
组件进行设置
<template>
<div class="f-menu" :style="{ width: $store.state.asideWidth }">
<el-menu
:collapse="isCollapse"
@select="handleSelect"
default-active="2"
class="border-0"
:collapse-transition="false"
unique-opened="true"
>
<template v-for="(item, index) in asideMenus" :key="index">
<el-sub-menu
v-if="item.child && item.child.length > 0"
:index="item.name"
>
<template #title>
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</template>
<el-menu-item
v-for="(item2, index2) in item.child"
:key="index2"
:index="item2.frontpath"
>
<el-icon>
<component :is="item2.icon"></component>
</el-icon>
<span>{{ item2.name }}</span>
</el-menu-item>
</el-sub-menu>
<el-menu-item v-else :index="item.frontpath">
<el-icon>
<component :is="item.icon"></component>
</el-icon>
<span>{{ item.name }}</span>
</el-menu-item>
</template>
</el-menu>
</div>
</template>
<script setup>
import { computed } from "vue";
import { useRouter } from "vue-router";
import { useStore } from "vuex";
const router = useRouter();
const store = useStore();
// 是否折叠
const isCollapse = computed(() => !(store.state.asideWidth == "250px"));
const asideMenus = [
{
name: "后台面板",
icon: "help",
child: [
{
name: "主控台",
frontpath: "/",
icon: "home-filled",
},
],
},
{
name: "商城管理",
icon: "shopping-bag",
child: [
{
name: "商品管理",
frontpath: "/goods/list",
icon: "shopping-cart-full",
},
],
},
];
const handleSelect = (e) => {
router.push(e);
};
</script>
<style scope>
.f-menu {
transition: all 0.2s;
top: 64px;
bottom: 0;
left: 0;
overflow-y: auto;
overflow-x: hidden;
@apply shadow-md fixed bg-light-50;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
主要内容
<div class="f-menu" :style="{ width: $store.state.asideWidth }">
<el-menu
:collapse="isCollapse"
@select="handleSelect"
default-active="2"
class="border-0"
:collapse-transition="false"
unique-opened="true"
>
2
3
4
5
6
7
8
9
- 动态绑定宽度样式
- 使用
collapse
属性来决定是否展开,下面使用isCollapse
判断宽度是否是250px
来返回布尔值 collapse-transition
取消它本身的一个动画效果,会比较慢unique-opened
让菜单栏只能有一个点击展开
# 设置菜单选中和路由关联
即菜单的一个属性:default-active
要和当前路由相等
<el-menu
:collapse="isCollapse"
@select="handleSelect"
:default-active="defaultActive"
class="border-0"
:collapse-transition="false"
unique-opened="true"
>
2
3
4
5
6
7
8
给菜单动态绑定一个属性值默认等于当前路由
我们需要使用useRoute
来获取
import { computed, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
const route = useRoute()
// 当前路由路径 默认选中
const defaultActive = ref(route.path)
2
3
4
5
6
7
# 菜单数据前后端交互
我们在前面有一个获取后台用户信息的接口,调用里面存储了菜单信息和一些权限信息,我们需要在vuex
里进行存储调用
import { createStore } from 'vuex'
import { login, getInfo } from '~/api/manager'
import { setToken, removeToken } from '~/composables/auth'
// 创建一个新的 store 实例
const store = createStore({
state() {
return {
user: {}, // 默认空对象 用户信息
// 侧边宽度
asideWidth: '250px',
// 菜单数据
menus: [],
// 权限菜单相关数据
ruleNames: [],
}
},
mutations: {
// ... 其他代码
SET_MENUS(state, menus) {
state.menus = menus
},
SET_RULENAMES(state, ruleNames) {
state.ruleNames = ruleNames
},
},
// 异步
actions: {
// 获取当前登录用户信息
getAdminUserInfo({ commit }) {
return new Promise((resolve, reject) => {
getInfo()
.then(res => {
commit('SET_USERINFO', res)
commit('SET_MENUS', res.menus)
commit('SET_RULENAMES', res.ruleNames)
resolve(res)
})
.catch(err => reject(err))
})
},
// ... 其他代码
},
})
export default store
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
然后需要将我们菜单组件里的设置的假的菜单数组变成从vuex
里获取菜单数据
import { useStore } from 'vuex'
const store = useStore()
const asideMenus = computed(() => store.state.menus)
2
3
4
同时我们将菜单栏的一个下拉的宽度的设置掉,否则比较难看
.f-menu::-webkit-scrollbar {
width: 0px;
}
2
3
# 根据菜单动态添加路由
我们需要拆分出 2 个路由数组,一个是默认路由,比如首页、登录、404 页面等,
别的则是根据菜单动态添加的。
就是需要指定一个父级路由的name
值
router/index.js
import { createRouter, createWebHashHistory } from 'vue-router'
import Admin from '~/layouts/admin.vue'
import Index from '~/pages/index.vue'
import Login from '~/pages/login.vue'
import NotFound from '~/pages/404.vue'
import GoodList from '~/pages/goods/list.vue'
import CategoryList from '~/pages/category/list.vue'
// 默认路由 所有用户共享
const routes = [
{
path: '/',
component: Admin,
name: 'admin',
},
{
path: '/login',
component: Login,
meta: {
title: '登录页',
},
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound,
},
]
// 动态路由,用于匹配菜单动态添加路由
const asyncRoutes = [
{
path: '/',
component: Index,
name: '/',
meta: {
title: '后台首页',
},
},
{
path: '/goods/list',
name: '/goods/list',
component: GoodList,
meta: {
title: '商品管理',
},
},
{
path: '/category/list',
name: '/category/list',
component: CategoryList,
meta: {
title: '分类列表',
},
},
]
// const routes = [
// {
// path: '/',
// component: Admin,
// // 子路由
// children: [
// {
// path: '/',
// component: Index,
// meta: {
// title: '后台首页',
// },
// },
// {
// path: '/goods/list',
// component: GoodList,
// meta: {
// title: '商品管理',
// },
// },
// {
// path: '/category/list',
// component: CategoryList,
// meta: {
// title: '分类列表',
// },
// },
// ],
// },
// {
// path: '/login',
// component: Login,
// meta: {
// title: '登录页',
// },
// },
// {
// path: '/:pathMatch(.*)*',
// name: 'NotFound',
// component: NotFound,
// },
// ]
export const router = createRouter({
history: createWebHashHistory(),
routes,
})
// 动态添加路由的方法
// 接收后端的一个菜单
export function addRoutes(menus) {
// 是否有新的路由
let hasNewRoutes = false
const findAndAddRoutesByMenus = arr => {
arr.forEach(e => {
// e 菜单的每个对象
// e.frontpath 菜单路径
let item = asyncRoutes.find(o => o.path == e.frontpath)
// 判断是否相同 且检查路由是否存在 传入的是一个name值 先设置和path一样
if (item && !router.hasRoute(item.path)) {
router.addRoute('admin', item)
hasNewRoutes = true
}
// 是否存在子菜单
if (e.child && e.child.length > 0) {
// 递归调用
findAndAddRoutesByMenus(e.child)
}
})
}
// 外部执行一下
findAndAddRoutesByMenus(menus)
// 查看现有路由
console.log(router.getRoutes())
return hasNewRoutes
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
permission.js
// 处理权限相关的内容
import { router, addRoutes } from './router'
import { getToken } from '~/composables/auth'
import { toast, showFullLoading, hideFullLoading } from '~/composables/util'
import store from './store'
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 显示loading
showFullLoading()
// console.log('全局前置守卫')
const token = getToken()
// 没有登录强制跳转回登录页
if (!token && to.path != '/login') {
toast('请先登录', 'error')
return next({ path: '/login' })
}
// 防止重复登录判断
if (token && to.path == '/login') {
toast('请务重复登录', 'error')
// 从哪里来就从哪里去
return next({ path: from.path ? from.path : '/' })
}
let hasNewRoutes = false
// 如果用户登录了就自动获取用户信息,并存储在vuex中
if (token) {
// 异步 resolve(res) 的res 进行解构出菜单
let { menus } = await store.dispatch('getAdminUserInfo')
// 动态添加路由
hasNewRoutes = addRoutes(menus)
}
// 设置页面标题
// console.log(to.meta.title) // 拿到标题
let title = (to.meta.title ? to.meta.title : '') + '-无解的管理后台'
document.title = title
// 如果有新的路由走指定的 否则直接 next()
hasNewRoutes ? next(to.fullPath) : next() // 放行
})
// 全局后置守卫
router.afterEach((to, from) => hideFullLoading())
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
# 标签导航组件实现
# 样式布局
使用element-plus
的动态标签:https://element-plus.gitee.io/zh-CN/component/tabs.html (opens new window)
<template>
<div class="f-tag-list" :style="{ left: $store.state.asideWidth }">
<el-tabs
v-model="editableTabsValue"
type="card"
class="flex-1"
closable
@tab-remove="removeTab"
style="min-width: 100px"
>
<el-tab-pane
v-for="item in editableTabs"
:key="item.name"
:label="item.title"
:name="item.name"
>
</el-tab-pane>
</el-tabs>
<span class="tag-btn">
<el-dropdown>
<span class="el-dropdown-link">
<el-icon class="el-icon--right">
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item disabled>Action 4</el-dropdown-item>
<el-dropdown-item divided>Action 5</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
</template>
<script setup>
import { ref } from "vue";
let tabIndex = 2;
const editableTabsValue = ref("2");
const editableTabs = ref([
{
title: "Tab 1",
name: "1",
content: "Tab 1 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
{
title: "Tab 2",
name: "2",
content: "Tab 2 content",
},
]);
const addTab = (targetName) => {
const newTabName = `${++tabIndex}`;
editableTabs.value.push({
title: "New Tab",
name: newTabName,
content: "New Tab content",
});
editableTabsValue.value = newTabName;
};
const removeTab = (targetName) => {
const tabs = editableTabs.value;
let activeName = editableTabsValue.value;
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
const nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeName = nextTab.name;
}
}
});
}
editableTabsValue.value = activeName;
editableTabs.value = tabs.filter((tab) => tab.name !== targetName);
};
</script>
<style scoped>
.f-tag-list {
top: 65px;
right: 0;
height: 44px;
@apply fixed bg-gray-100 flex items-center px-2;
z-index: 100;
}
.tag-btn {
@apply bg-white rounded ml-auto flex items-center justify-center px-2;
height: 32px;
}
:deep(.el-tabs__header) {
@apply mb-0;
}
:deep(.el-tabs__nav) {
border: 0 !important;
}
:deep(.el-tabs__item) {
border: 0 !important;
@apply bg-white mx-1 rounded;
height: 32px;
line-height: 32px;
}
:deep(.el-tabs__nav-next),
:deep(.el-tabs__nav-prev) {
line-height: 32px;
height: 32px;
}
:deep(.is-disabled) {
cursor: not-allowed;
@apply text-gray-300;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
# 同步路由和存储
<template>
<div class="f-tag-list" :style="{ left: $store.state.asideWidth }">
<el-tabs
v-model="activeTab"
type="card"
class="flex-1"
@tab-remove="removeTab"
style="min-width: 100px"
@tab-change="changeTab"
>
<!-- 如果是首页不显示关闭按钮 -->
<el-tab-pane
v-for="item in tabList"
:key="item.path"
:label="item.title"
:name="item.path"
:closable="item.path != '/'"
>
</el-tab-pane>
</el-tabs>
<span class="tag-btn">
<el-dropdown>
<span class="el-dropdown-link">
<el-icon>
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item disabled>Action 4</el-dropdown-item>
<el-dropdown-item divided>Action 5</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div style="height: 44px"></div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, onBeforeRouteUpdate, useRouter } from "vue-router";
import { useCookies } from "@vueuse/integrations/useCookies";
const route = useRoute();
const cookie = useCookies();
const router = useRouter();
const activeTab = ref(route.path);
const tabList = ref([
{
title: "后台首页",
path: "/",
},
{
title: "商城管理",
path: "/goods/list",
},
]);
const changeTab = (t) => {
activeTab.value = t;
router.push(t);
};
function addTab(tab) {
let notTab = tabList.value.findIndex((t) => t.path == tab.path) === -1;
if (notTab) {
tabList.value.push(tab);
}
cookie.set("tabList", tabList.value);
}
// 初始化标签导航列表
function initTabList() {
let tabs = cookie.get("tabList");
if (tabs) {
tabList.value = tabs;
}
}
initTabList();
onBeforeRouteUpdate((to, from) => {
// 设置tab激活
activeTab.value = to.path;
// console.log(to, from);
addTab({
title: to.meta.title,
path: to.path,
});
});
const removeTab = (targetName) => {};
</script>
<style scoped>
.f-tag-list {
top: 65px;
right: 0;
height: 44px;
@apply fixed bg-gray-100 flex items-center px-2;
z-index: 100;
}
.tag-btn {
@apply bg-white rounded ml-auto flex items-center justify-center px-2;
height: 32px;
}
:deep(.el-tabs__header) {
border: 0 !important;
@apply mb-0;
}
:deep(.el-tabs__nav) {
border: 0 !important;
}
:deep(.el-tabs__item) {
border: 0 !important;
@apply bg-white mx-1 rounded;
height: 32px;
line-height: 32px;
}
:deep(.el-tabs__nav-next),
:deep(.el-tabs__nav-prev) {
line-height: 32px;
height: 32px;
}
:deep(.is-disabled) {
cursor: not-allowed;
@apply text-gray-300;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# 关闭当前标签处理
<template>
<div class="f-tag-list" :style="{ left: $store.state.asideWidth }">
<el-tabs
v-model="activeTab"
type="card"
class="flex-1"
@tab-remove="removeTab"
style="min-width: 100px"
@tab-change="changeTab"
>
<!-- 如果是首页不显示关闭按钮 -->
<el-tab-pane
v-for="item in tabList"
:key="item.path"
:label="item.title"
:name="item.path"
:closable="item.path != '/'"
>
</el-tab-pane>
</el-tabs>
<span class="tag-btn">
<el-dropdown>
<span class="el-dropdown-link">
<el-icon>
<arrow-down />
</el-icon>
</span>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>Action 1</el-dropdown-item>
<el-dropdown-item>Action 2</el-dropdown-item>
<el-dropdown-item>Action 3</el-dropdown-item>
<el-dropdown-item disabled>Action 4</el-dropdown-item>
<el-dropdown-item divided>Action 5</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
</span>
</div>
<div style="height: 44px"></div>
</template>
<script setup>
import { ref } from "vue";
import { useRoute, onBeforeRouteUpdate, useRouter } from "vue-router";
import { useCookies } from "@vueuse/integrations/useCookies";
const route = useRoute();
const cookie = useCookies();
const router = useRouter();
const activeTab = ref(route.path);
const tabList = ref([
{
title: "后台首页",
path: "/",
},
{
title: "商城管理",
path: "/goods/list",
},
]);
const changeTab = (t) => {
activeTab.value = t;
router.push(t);
};
function addTab(tab) {
let notTab = tabList.value.findIndex((t) => t.path == tab.path) === -1;
if (notTab) {
tabList.value.push(tab);
}
cookie.set("tabList", tabList.value);
}
// 初始化标签导航列表
function initTabList() {
let tabs = cookie.get("tabList");
if (tabs) {
tabList.value = tabs;
}
}
initTabList();
onBeforeRouteUpdate((to, from) => {
// 设置tab激活
activeTab.value = to.path;
// console.log(to, from);
addTab({
title: to.meta.title,
path: to.path,
});
});
const removeTab = (t) => {
// console.log(t);
// 1. 判断关闭的是否是当前激活的
let tabs = tabList.value;
let a = activeTab.value;
if (a == t) {
tabs.forEach((tab, index) => {
if (tab.path == t) {
const nextTab = tabs[index + 1] || tabs[index - 1]; // 如果没有下一个就去拿上一个
if (nextTab) {
// 设置下一个激活的值
a = nextTab.path;
}
}
});
}
activeTab.value = a;
// 只有不等于当前关闭的留下来
tabList.value = tabList.value.filter((tab) => tab.path != t);
// 更新存储的cookie
cookie.set("tabList", tabList.value);
};
</script>
<style scoped>
.f-tag-list {
top: 65px;
right: 0;
height: 44px;
@apply fixed bg-gray-100 flex items-center px-2;
z-index: 100;
}
.tag-btn {
@apply bg-white rounded ml-auto flex items-center justify-center px-2;
height: 32px;
}
:deep(.el-tabs__header) {
border: 0 !important;
@apply mb-0;
}
:deep(.el-tabs__nav) {
border: 0 !important;
}
:deep(.el-tabs__item) {
border: 0 !important;
@apply bg-white mx-1 rounded;
height: 32px;
line-height: 32px;
}
:deep(.el-tabs__nav-next),
:deep(.el-tabs__nav-prev) {
line-height: 32px;
height: 32px;
}
:deep(.is-disabled) {
cursor: not-allowed;
@apply text-gray-300;
}
</style>
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
# 实现关闭全部标签功能
我们使用组合式api
来简化页面的script
部分
<script setup>
import {useTabList} from "~/composables/useTabList.js"; const {(activeTab, tabList, changeTab, removeTab, handleClose)} = useTabList();
</script>
2
3
import { ref } from 'vue'
import { useRoute, onBeforeRouteUpdate, useRouter } from 'vue-router'
import { useCookies } from '@vueuse/integrations/useCookies'
export function useTabList() {
const route = useRoute()
const cookie = useCookies()
const router = useRouter()
const activeTab = ref(route.path)
const tabList = ref([
{
title: '后台首页',
path: '/',
},
{
title: '商城管理',
path: '/goods/list',
},
])
const changeTab = t => {
activeTab.value = t
router.push(t)
}
function addTab(tab) {
let notTab = tabList.value.findIndex(t => t.path == tab.path) === -1
if (notTab) {
tabList.value.push(tab)
}
cookie.set('tabList', tabList.value)
}
// 初始化标签导航列表
function initTabList() {
let tabs = cookie.get('tabList')
if (tabs) {
tabList.value = tabs
}
}
initTabList()
onBeforeRouteUpdate((to, from) => {
// 设置tab激活
activeTab.value = to.path
// console.log(to, from);
addTab({
title: to.meta.title,
path: to.path,
})
})
const removeTab = t => {
// console.log(t);
// 1. 判断关闭的是否是当前激活的
let tabs = tabList.value
let a = activeTab.value
if (a == t) {
tabs.forEach((tab, index) => {
if (tab.path == t) {
const nextTab = tabs[index + 1] || tabs[index - 1] // 如果没有下一个就去拿上一个
if (nextTab) {
// 设置下一个激活的值
a = nextTab.path
}
}
})
}
activeTab.value = a
// 只有不等于当前关闭的留下来
tabList.value = tabList.value.filter(tab => tab.path != t)
// 更新存储的cookie
cookie.set('tabList', tabList.value)
}
const handleClose = c => {
console.log(c)
if (c == 'clearAll') {
// 清除所有,切换回首页
activeTab.value = '/'
// 过滤只想剩下首页
tabList.value = [
{
title: '后台首页',
paht: '/',
},
]
} else if (c == 'clearOther') {
// 过滤只剩下首页和当前激活
tabList.value = tabList.value.filter(tab => tab.path == '/' || tab.path == activeTab.value)
}
cookie.set('tabList', tabList.value)
}
return {
activeTab,
tabList,
changeTab,
removeTab,
handleClose,
}
}
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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# 优化加载菜单的速度
这里会发现,点击一个菜单,会触发 2 次调用getInfo
接口,就会导致页面加载比较慢。
我们在全局前置守卫里进行设置
// 防止重复加载 getinfo
let hasGetInfo = false
// 全局前置守卫
router.beforeEach(async (to, from, next) => {
// 显示loading
showFullLoading()
// console.log('全局前置守卫')
const token = getToken()
// 没有登录强制跳转回登录页
if (!token && to.path != '/login') {
toast('请先登录', 'error')
return next({ path: '/login' })
}
// 防止重复登录判断
if (token && to.path == '/login') {
toast('请务重复登录', 'error')
// 从哪里来就从哪里去
return next({ path: from.path ? from.path : '/' })
}
let hasNewRoutes = false
// 如果用户登录了就自动获取用户信息,并存储在vuex中
if (token && !hasGetInfo) {
// 异步 resolve(res) 的res 进行解构出菜单
let { menus } = await store.dispatch('getAdminUserInfo')
hasGetInfo = true
// 动态添加路由
hasNewRoutes = addRoutes(menus)
}
// 设置页面标题
// console.log(to.meta.title) // 拿到标题
let title = (to.meta.title ? to.meta.title : '') + '-无解的管理后台'
document.title = title
// 如果有新的路由走指定的 否则直接 next()
hasNewRoutes ? next(to.fullPath) : next() // 放行
})
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
# 使用 keep-alive 页面缓存
vue3
相关文档地址:https://cn.vuejs.org/guide/built-ins/keep-alive.html#keepalive (opens new window)
<template>
<el-container>
<!-- 头部 -->
<el-header>
<f-header />
</el-header>
<el-container>
<el-aside :width="$store.state.asideWidth">
<f-menu />
</el-aside>
<el-main>
<f-tag-list />
<router-view v-slot="{ Component }">
<!-- 缓存10个 -->
<keep-alive :max="10">
<component :is="Component"></component>
</keep-alive>
</router-view>
</el-main>
</el-container>
</el-container>
</template>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 使用 transition 全局过渡动画
<template>
<el-container>
<!-- 头部 -->
<el-header>
<f-header />
</el-header>
<el-container>
<el-aside :width="$store.state.asideWidth">
<f-menu />
</el-aside>
<el-main>
<f-tag-list />
<router-view v-slot="{ Component }">
<transition name="fade">
<!-- 缓存10个 -->
<keep-alive :max="10">
<component :is="Component"></component>
</keep-alive>
</transition>
</router-view>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import FHeader from "./components/FHeader.vue";
import FMenu from "./components/FMenu.vue";
import FTagList from "./components/FTagList.vue";
</script>
<style scoped>
.el-aside {
transition: all 0.2s;
}
/* 进入之前 */
.fade-enter-from {
opacity: 0;
}
/* 进入之后 */
.fade-enter-to {
opacity: 1;
}
/* 离开之前 */
.fade-leave-from {
opacity: 1;
}
/* 离开之后 */
.fade-leave-to {
opacity: 0;
}
.fade-enter-active,
.fade-leave-active {
transition: all 0.3s;
}
/* 进入动画延迟 */
.fade-enter-active {
transition-delay: 0.3s;
}
</style>
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
:::caution 注意
如果使用transition
,每个页面的根节点只能有一个,也就是说template
下的div
只能有一个,否则就会失效,而且console
页面还会警告。
:::