上一篇我们实现动态菜单,这里我们接着实现Tba栏和面包屑,为什么需要一起实现,因为这两个功能是紧密连接在一起的,这里便放在一起写。代码量略多, 请耐心阅读。

上一篇博客的进度页面展示

页面

修改布局

可以在本地运行npm run dev查看,这个页面的布局这边略微调整下,修改MainLayout.vue

说明:代码块中’-‘开头表示删除,’+’开头表示增加,接下来的学习中,可能会出现很多这样的情况,因为直接粘贴全部代码,实在是太占篇幅了。

1
2
- <q-layout view="lHh Lpr lFf">
+ <q-layout view="hHh LpR lff">

修改过后运行代码,可以看到布局已经改了,关于布局,可以查看layout,了解更多。

布局图

增加Tab栏

layouts/comp文件夹下新建HeadTab.vue组件,编写静态代码,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<q-tabs v-model="tab" align="left" dense inline-label>
<q-tab name="main" icon="mail" label="主页" />
<q-tab name="alarms" icon="alarm" label="菜单管理" />
<q-tab name="movies" icon="movie" label="用户管理" />
</q-tabs>
</template>

<script setup lang="ts">
import { ref } from 'vue';
const tab = ref('main');
</script>

<style scoped></style>

紧接着修改MainLayout.vue文件引入HeadTab.vue组件,编辑过后完整MainLayout.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
34
35
36
37
38
39
40
41
42
<template>
<q-layout view="hHh LpR lff">
<q-header elevated>
<q-toolbar>
<q-btn
flat
dense
round
icon="menu"
aria-label="Menu"
@click="toggleLeftDrawer"
/>

<q-toolbar-title> Quasar App </q-toolbar-title>

<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
<head-tab></head-tab>
</q-header>

<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<LeftDrawer></LeftDrawer>
</q-drawer>

<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import LeftDrawer from './comp/LeftDrawer.vue';
import HeadTab from './comp/HeadTab.vue';

const leftDrawerOpen = ref(false);

function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

重启项目或者刷新页面,可以看到如图所示

headTab

Tab栏已经正常的添加,现在还没有和菜单联动起来,接下来我们编写代码让菜单和Tab栏联动起来,

修改layout-store.ts文件,笔者这里习惯了Setup写法,这里便改为使用pinia的setup写法,改动也不大,主要使用了vue的自带的功能,感觉是更容易理解的,ref就是state,getters就是computed,普通的函数就是actions,最后需要暴露出去的return即可。非常经典的组合式API

主要原因可能是项目中都是setup组件,使用optionsAPI的全局状态,思维上有点差异,也感觉处处受限制。

改动过后,新增部分工具方法,完整代码如下

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
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
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
import { LocalStorage } from 'quasar'

/* 项目全局变量配置 */
export const useLayoutStore = defineStore('layout', () => {
// 首页
const mainPage: WJ.MenuItem = {
id: '0',
pid: '0',
name: '首页',
sort: 0,
icon: 'home',
auth: '',
path: '/'
}

const menuKey = 'menu';
const menu = LocalStorage.getItem<Array<WJ.MenuItem>>(menuKey)
// 原始的菜单数据(平级)
const menuList = ref<Array<WJ.MenuItem>>(menu ? menu : []);
// Tab栏的List
const tabList = ref<Array<WJ.MenuItem>>([]);
// 面包屑
const crumbsList = ref<Array<WJ.MenuItem>>([]);
// 左边侧边栏宽度
const leftDrawerWeight = ref(250);

// 菜单排序
const menuSort = (a: WJ.MenuItem, b: WJ.MenuItem) => a.sort - b.sort

// 树形菜单数据,操作中过滤隐藏菜单
const menuTree = computed(() => {
// 将子元素依次放入父元素中
const res: Array<WJ.MenuItem> = [];
menuList.value.forEach((item) => {
const parent = menuIdMap.value.get(item.pid);
if (parent) {
const pc = (parent.children || (parent.children = []));
pc.indexOf(item) === -1 ? pc.push(item) : ''
pc.sort(menuSort)
} else {
res.push(item)
}
});
res.sort(menuSort)
return res;
})
/**
* id:menuitem
*/
const menuIdMap = computed(() => {
const map = new Map<string, WJ.MenuItem>();
menuList.value.forEach((item) => {
map.set(item.id, item);
});
return map
})

/**
* path:menuitem
*/
const menuPathMap = computed(() => {
const map = new Map<string, WJ.MenuItem>();
menuList.value.forEach((item) => {
map.set(item.path, item);
});
return map
})

/**
* true表示是http或https开头的url
* @param url 待检测url
*/
function isHttp(url: string) {
return url.indexOf('http') !== -1;
};

// 处理外链
function toBind(item: WJ.MenuItem) {
return isHttp(item.path)
? {
path: '/outside-link',
query: {
url: item.path,
},
}
: item.path
}

// 递归向上获取所有父节点
function findP(item: WJ.MenuItem) {
const result: Array<WJ.MenuItem> = []
const findFn = (i: WJ.MenuItem) => {
result.push(i)
if (i.pid !== '0') {
const it = menuIdMap.value.get(i.pid);
if (it) {
findFn(it)
}
}
}
findFn(item);
return result.reverse();
}
return {
menuList, tabList, crumbsList,
menuTree, findP, menuPathMap,
menuIdMap, mainPage, leftDrawerWeight, menuKey, toBind, isHttp
}
});

/* 简单项目权限配置后面添加登陆的时候使用 */
export const useAuthStore = defineStore('auth', () => {
// token
const token = ref('');

return {
token
}
});

这个写好了,主要功能包括方便后期的一些扩展,比如下面的面包屑的展示,还有登陆用户的token

修改quasar.config.js文件

编辑framework.plugins内容为,安装quasar的一些插件关于插件,可在文档中查看,用到的时候查看一下API即可。

1
2
3
4
5
6
7
8
9
10
11
plugins: [
'LoadingBar',
'Notify',
'LocalStorage',
'SessionStorage',
'Loading',
'Dialog',
'BottomSheet',
'AppVisibility',
'AppFullscreen',
],

增加登录页

pages目录中新建LoginPage.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
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
<template>
<q-layout>
<q-page-container>
<q-page class="flex bg-image flex-center">
<q-card
v-bind:style="$q.screen.lt.sm ? { width: '80%' } : { width: '30%' }"
>
<q-card-section>
<q-avatar size="103px" class="absolute-center shadow-10">
<img
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202107%2F19%2F20210719150601_4401e.thumb.1000_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685108172&t=232426938cbc64554c6c143fdb13fb4c"
/>
</q-avatar>
</q-card-section>
<q-card-section>
<div class="text-center q-pt-lg">
<div class="col text-h6 ellipsis">登录</div>
</div>
</q-card-section>
<q-card-section>
<q-form class="q-gutter-md">
<q-input filled v-model="username" label="用户名" lazy-rules />

<q-input
type="password"
filled
v-model="password"
label="密码"
lazy-rules
/>

<div>
<q-btn
label="登录"
type="button"
@click="login"
color="primary"
/>
</div>
</q-form>
</q-card-section>
</q-card>
</q-page>
</q-page-container>
</q-layout>
</template>

<script setup lang="ts">
import { useAuthStore, useLayoutStore } from 'src/stores/layout-store';
import { ref } from 'vue';
import myRoutes from 'src/router/my-router';
import { useRouter } from 'vue-router';
import { useQuasar } from 'quasar';
const username = ref('小王');
const password = ref('12345');
const auth = useAuthStore();
const layout = useLayoutStore();
const router = useRouter();
const $q = useQuasar();

const login = () => {
// 点击登录写入token
auth.token = '2333333333';
layout.menuList = myRoutes as Array<WJ.MenuItem>;
$q.localStorage.set(layout.menuKey, myRoutes);

router.push({ path: layout.mainPage.path }).then(() => {
$q.notify({
position: 'top',
color: 'positive',
message: '欢迎回来~ ' + username.value,
});
});
};
</script>

<style scoped>
.bg-image {
background-image: linear-gradient(135deg, #7028e4 0%, #e5b2ca 100%);
}
</style>

编辑src\router\routes.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
import { RouteRecordRaw } from 'vue-router';

const routes: RouteRecordRaw[] = [
{
path: '/login',
component: () => import('pages/LoginPage.vue'),
},
{
path: '/',
component: () => import('layouts/MainLayout.vue'),
children: [
{ path: '', component: () => import('pages/IndexPage.vue') },
{ path: '/sys-menu', component: () => import('pages/IndexPage.vue') },
{ path: '/sys-user', component: () => import('pages/IndexPage.vue') },
],
},

// Always leave this as last one,
// but you can also remove it
{
path: '/:catchAll(.*)*',
component: () => import('pages/ErrorNotFound.vue'),
},
];

export default routes;

编写路由跳转前置拦截

这里的逻辑有点多,我这边一一列举一下

  • 处理无需鉴权鉴权的路由,比如登录页面,
  • 处理Tab栏的联动。
  • 处理面包屑的联动
  • 处理三方链接的跳转逻辑

编辑src\router\index.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
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
import { route } from 'quasar/wrappers';
import {
createMemoryHistory,
createRouter,
createWebHashHistory,
createWebHistory,
} from 'vue-router';
import routes from './routes';
import { LoadingBar } from 'quasar'
import type { RouteLocationNormalized } from 'vue-router';
import { useAuthStore, useLayoutStore } from 'src/stores/layout-store';

/*
* If not building with SSR mode, you can
* directly export the Router instantiation;
*
* The function below can be async too; either use
* async/await or return a Promise which resolves
* with the Router instance.
*/

export default route(function (/* { store, ssrContext } */) {
const createHistory = process.env.SERVER
? createMemoryHistory
: (process.env.VUE_ROUTER_MODE === 'history' ? createWebHistory : createWebHashHistory);

const Router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }),
routes,

// Leave this as is and make changes in quasar.conf.js instead!
// quasar.conf.js -> build -> vueRouterMode
// quasar.conf.js -> build -> publicPath
history: createHistory(
process.env.MODE === 'ssr' ? void 0 : process.env.VUE_ROUTER_BASE
),
});

/* 数组里面的URL里面无需鉴权 */
const notCheckPath = ['/login'];

// 路由跳转前置拦截器
Router.beforeEach((to: RouteLocationNormalized, form: RouteLocationNormalized, next) => {
LoadingBar.start()
console.log('form: ', form);
console.log('to: ', to);
const auth = useAuthStore();
const layout = useLayoutStore();
if (notCheckPath.indexOf(to.fullPath) > -1) {
next()
} else {
// 不存在则跳转到登录页面
if (auth.token) {
if (to.fullPath === '/') {
// 切换面包屑
layout.crumbsList = [layout.mainPage]
} else {
const item = to.query.url ?
layout.menuPathMap.get(to.query.url as string) :
layout.menuPathMap.get(to.fullPath)
console.log('layout.menuPathMap: ', layout.menuPathMap);
console.log('item: ', item);
if (item) {
// 切换面包屑
layout.crumbsList = layout.findP(item)
// 增加Tab栏
if (layout.tabList.filter(i => i.id === item.id).length === 0) {
layout.tabList.push(item);
}
}

}
next()
} else {
next({ path: '/login' })
}
}
})

// 路由跳转之后的回调
Router.afterEach(() => {
console.log('afterEach: ');

LoadingBar.stop()
LoadingBar.stop()
})
return Router;
});

最后修改HeadTab.vue,再加一点细节,

Tab的一些操作,如,关闭,关闭左侧,右侧等等,写这种代码需要一定的编码能力,修改过后完整代码,

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
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
<template>
<!-- Tba栏 -->
<q-tabs align="left" dense inline-label>
<transition-group name="tab-list">
<q-route-tab to="/" class="q-px-xs" :key="layout.mainPage.id">
<q-icon :name="layout.mainPage.icon" size="1.2rem" />
<div class="q-mx-sm">{{ layout.mainPage.name }}</div>
</q-route-tab>
<q-route-tab
:to="layout.toBind(item)"
class="q-px-xs"
v-for="item in layout.tabList"
:key="item.id"
>
<q-icon v-if="item.icon" :name="item.icon" size="1.2rem" />
<div class="q-mx-sm">{{ item.name }}</div>
<div @click.prevent.stop="closeThis(item)">
<q-icon name="close" size="1.2rem" class="remove-icon" />
<q-tooltip> 移除一个标签 </q-tooltip>
</div>
<q-menu touch-position context-menu>
<q-list dense>
<q-item clickable v-close-popup @click="closeThis(item)">
<q-item-section>移除当前</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="closeLeft(item)">
<q-item-section>关闭左侧</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="closeRight(item)">
<q-item-section>关闭右侧</q-item-section>
</q-item>
<q-item clickable v-close-popup @click="closeAll()">
<q-item-section>移除全部</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-route-tab>
</transition-group>
</q-tabs>
</template>

<script setup lang="ts">
import { useLayoutStore } from 'src/stores/layout-store';
import { useRoute, useRouter } from 'vue-router';
const layout = useLayoutStore();
const router = useRouter();
const route = useRoute();

// 关闭当前
const closeThis = (item: WJ.MenuItem) => {
let tabl = layout.tabList.slice(0);
const it = tabl[tabl.indexOf(item) - 1];
tabl = tabl.filter((i) => i.id !== item.id);
// 如果移除的是当前路由
if (
route.query.url
? route.query.url === item.path
: route.fullPath === item.path
) {
if (it) {
router.push(layout.toBind(it)).then(() => {
layout.tabList = tabl;
});
} else {
router.push({ path: layout.mainPage.path }).then(() => {
layout.tabList = tabl;
});
}
} else {
layout.tabList = tabl;
}
};
// 关闭左侧
const closeLeft = (item: WJ.MenuItem) => {
let tabl = layout.tabList.slice(0);
const index = tabl.indexOf(item);
if (index !== 0) {
tabl = tabl.slice(tabl.indexOf(item));
if (
route.query.url
? route.query.url !== item.path
: route.fullPath !== item.path
) {
router.push(layout.toBind(item)).then(() => {
layout.tabList = tabl;
});
} else {
layout.tabList = tabl;
}
}
};
// 关闭右侧
const closeRight = (item: WJ.MenuItem) => {
let tabl = layout.tabList.slice(0);
const index = tabl.indexOf(item) + 1;
if (index !== tabl.length) {
tabl = tabl.slice(0, index);
if (
route.query.url
? route.query.url !== item.path
: route.fullPath !== item.path
) {
router.push(layout.toBind(item)).then(() => {
layout.tabList = tabl;
});
} else {
layout.tabList = tabl;
}
}
};
// 全部关闭
const closeAll = () => {
router.push({ path: layout.mainPage.path }).then(() => {
layout.tabList = [];
});
};
</script>

<style lang="scss" scoped>
.remove-icon {
opacity: 0.58;
transition: all 0.3s;
&:hover {
opacity: 1;
}
}
.tab-list {
/* 对移动中的元素应用的过渡 */
&-move,
&-enter-active,
&-leave-active {
transition: all 0.5s ease;
}

&-enter-from,
&-leave-to {
opacity: 0;
transform: translateX(30px);
}

/* 确保将离开的元素从布局流中删除
以便能够正确地计算移动的动画。 */
&-leave-active {
position: absolute;
}
}
</style>

最后总结

修改完成过后重启项目,访问主页,发现自动跳转到登录页了,点击登录,进入主界面,点点点菜单发现侧边菜单功能已经实现。

增加面包屑

新建HeadCrumbs.vue文件

在src\layouts\comp\下新建HeadCrumbs.vue文件,完整代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<!-- 面包屑 -->
<q-breadcrumbs active-color="none">
<transition-group appear enter-active-class="animated fadeInLeft">
<q-breadcrumbs-el
:icon="item.icon"
:label="item.name"
v-for="item in layout.crumbsList"
:key="item.id"
/>
</transition-group>
</q-breadcrumbs>
</template>

<script setup lang="ts">
import { useLayoutStore } from 'src/stores/layout-store';
const layout = useLayoutStore();
</script>

<style scoped></style>

面包屑组件中使用了一些动画,quasar集成animations动画库十分简单,修改quasar.config.js文件,

1
2
3
4
5
6
7
8
9
10
11
12
13
- animations: [],
+ animations: 'all',

// (可选)每次重启项目都会打开浏览器是不是有点烦,修改这里即可关闭每次重启打开浏览器

devServer: {
server: {
type: 'http',
},
port: 8080,
- open: true, // opens browser window automatically
+ open: false, // opens browser window automatically
},

引入HeadCrumbs.vue组件

修改MainLayout.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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
<template>
<q-layout view="hHh LpR lff">
<q-header elevated>
<q-toolbar>
<q-btn
dense
flat
round
:icon="leftDrawerOpen ? 'menu_open' : 'menu'"
@click="toggleLeftDrawer"
/>
<q-btn flat no-caps no-wrap class="q-ml-xs" v-if="$q.screen.gt.xs">
<q-icon name="catching_pokemon" size="2rem" />
<q-toolbar-title shrink class="text-weight-bold">
W_LF
</q-toolbar-title>
</q-btn>
<head-crumbs v-if="$q.screen.gt.xs"></head-crumbs>
<q-space />

<div>Quasar v{{ $q.version }}</div>
</q-toolbar>
<head-tab></head-tab>
</q-header>

<q-drawer v-model="leftDrawerOpen" show-if-above bordered>
<LeftDrawer></LeftDrawer>
</q-drawer>

<q-page-container>
<router-view />
</q-page-container>
</q-layout>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import LeftDrawer from './comp/LeftDrawer.vue';
import HeadTab from './comp/HeadTab.vue';
import HeadCrumbs from './comp/HeadCrumbs.vue';

const leftDrawerOpen = ref(false);

function toggleLeftDrawer() {
leftDrawerOpen.value = !leftDrawerOpen.value;
}
</script>

重启项目,不报错,继续优化,现在项目的雏形已经完成个七七八八了。最后我们让Head栏变得好看一点。首先编辑quasar.config.js文件,在extras块中添加fontawesome-v6图标,继续编辑MainLayout.vue文件,删去显示Quasar版本号的内容,替换成下面的内容,重启项目。即可看到最终结果

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
75
76
77
78
79
80
81
- <div>Quasar v{{ $q.version }}</div>

<div class="q-gutter-sm row items-center no-wrap">
<q-btn
round
dense
flat
color="white"
:icon="$q.fullscreen.isActive ? 'fullscreen_exit' : 'fullscreen'"
@click="$q.fullscreen.toggle()"
v-if="$q.screen.gt.sm"
>
</q-btn>
<q-btn
v-if="$q.screen.gt.sm"
round
dense
flat
color="white"
icon="fab fa-github"
type="a"
href="https://gitee.com/wlf213/wlf-admin"
target="_blank"
>
</q-btn>
<q-btn
v-if="$q.screen.gt.sm"
round
dense
flat
class="text-red"
type="a"
href="https://gitee.com/wlf213"
target="_blank"
>
<i class="fa fa-heart fa-2x fa-beat"></i>
</q-btn>
<q-btn round dense flat color="white" icon="notifications">
<q-badge color="red" text-color="white" floating> 5 </q-badge>
<q-menu>
<q-list style="min-width: 100px">
<q-card class="text-center no-shadow no-border">
<q-btn
label="全部查看"
style="max-width: 120px !important"
flat
dense
class="text-indigo-8"
></q-btn>
</q-card>
</q-list>
</q-menu>
</q-btn>
<q-btn round flat>
<q-avatar size="26px">
<img
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202107%2F19%2F20210719150601_4401e.thumb.1000_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685108172&t=232426938cbc64554c6c143fdb13fb4c"
/>
</q-avatar>
<q-menu transition-show="scale" transition-hide="scale">
<q-list dense>
<q-item v-ripple>
<q-item-section avatar>
<q-avatar>
<img
src="https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fc-ssl.duitang.com%2Fuploads%2Fblog%2F202107%2F19%2F20210719150601_4401e.thumb.1000_0.jpg&refer=http%3A%2F%2Fc-ssl.duitang.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=auto?sec=1685108172&t=232426938cbc64554c6c143fdb13fb4c"
/>
</q-avatar>
</q-item-section>
<q-item-section>你好, W</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section class="text-center">个人主页</q-item-section>
</q-item>
<q-item clickable v-close-popup>
<q-item-section class="text-center">退出登录</q-item-section>
</q-item>
</q-list>
</q-menu>
</q-btn>
</div>

最后结果展示

结果展示

此篇博客进度对代码分支——tab-and-crumbs