Skip to content

Commit 6caea5b

Browse files
author
ricardo
committed
feat: 在前端添加标签式导航
- default.vue 中实现了标签页的添加、切换和移除逻辑 - 更新了路由配置,使用命名路由来更好地管理标签页 - 调整了相关页面布局,以适应标签页的显示
1 parent 287a1bc commit 6caea5b

File tree

10 files changed

+163
-40
lines changed

10 files changed

+163
-40
lines changed

frontend/src/layouts/default.vue

Lines changed: 98 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22
import zhCn from "element-plus/es/locale/lang/zh-cn"
33
import { HomeFilled, Tools, List, InfoFilled, Sunny, Moon, Calendar, Collection } from "@element-plus/icons-vue"
44
import ContextMenu from '@imengyu/vue3-context-menu'
5-
import { ElMessage, type MenuItemInstance, type CardConfigContext } from "element-plus"
5+
import { ElMessage, type MenuItemInstance, type CardConfigContext, type TabPaneName } from "element-plus"
66
import { request } from "@/api"
77
88
defineOptions({
99
name: "defaultLayout"
1010
})
1111
1212
const themeStore = useThemeStore()
13-
const route = useRoute()
14-
const router = useRouter()
13+
const route = useRoute() // 当前路由实例
14+
const router = useRouter() // 路由器实例
1515
const cardConfig: CardConfigContext = {
1616
shadow: "always"
1717
}
@@ -28,6 +28,14 @@ const messageConfig = {
2828
plain: true
2929
}
3030
31+
const activeTabName = ref("/")
32+
const activeMenu = ref("0")
33+
const openTabs = ref<TabItem[]>([])
34+
// 缓存已打开的tab
35+
const components = computed(() => {
36+
return openTabs.value.map(item => item.componentName).filter(Boolean)
37+
})
38+
3139
const menuItemList = [
3240
{
3341
title: "点歌板",
@@ -133,6 +141,11 @@ const closeTour = async () => {
133141
navSideTour.value = false
134142
}
135143
144+
const updateActiveMenu = (routeName: string) => {
145+
const menuItemIndex = menuItemList.findIndex(item => item.routerName == routeName)
146+
activeMenu.value = menuItemIndex.toString()
147+
}
148+
136149
/**
137150
* 跳转到指定页面
138151
* @param {string} name 路由的name
@@ -144,7 +157,62 @@ const goto = (name: string) => {
144157
return
145158
}
146159
147-
router.push({ path: name })
160+
changeTab(name as TabPaneName)
161+
}
162+
163+
/**
164+
* 切换标签页
165+
* @param name
166+
*/
167+
const changeTab = (name: TabPaneName) => {
168+
const tabName = name.toString()
169+
170+
const currentRouteTitle = menuItemList.find(item => item.routerName == tabName)?.title || route.path
171+
let tab = openTabs.value.find(tab => tab.name === tabName)
172+
173+
if (!tab) {
174+
tab = {
175+
name: tabName,
176+
title: currentRouteTitle,
177+
path: tabName,
178+
closable: tabName !== "/",
179+
componentName: tabName == "/" ? "home" : tabName.replaceAll('/', '')
180+
}
181+
openTabs.value.push(tab)
182+
}
183+
184+
activeTabName.value = tabName
185+
router.push({ path: tabName })
186+
updateActiveMenu(tabName)
187+
}
188+
189+
/**
190+
* 移除标签页
191+
* @param {TabPaneName} targetTabName 要移除的标签页的name
192+
*/
193+
const removeTab = (targetTabName: TabPaneName) => {
194+
const tabs = openTabs.value
195+
const tabName = targetTabName.toString()
196+
let newActiveTabName = activeTabName.value
197+
198+
if (newActiveTabName === tabName) {
199+
tabs.forEach((tab, index) => {
200+
if (tab.name === tabName) {
201+
const nextTab = tabs[index + 1] || tabs[index - 1]
202+
if (nextTab) {
203+
newActiveTabName = nextTab.name
204+
} else {
205+
// 如果没有其他标签页,则回到首页
206+
newActiveTabName = "/"
207+
}
208+
}
209+
})
210+
}
211+
212+
openTabs.value = tabs.filter(tab => tab.name !== tabName)
213+
activeTabName.value = newActiveTabName
214+
router.push({ path: newActiveTabName })
215+
updateActiveMenu(newActiveTabName)
148216
}
149217
150218
/**
@@ -225,23 +293,35 @@ const isDarktheme = computed(() => {
225293
})
226294
227295
watch(isFetching, () => {
228-
if(globalConfigData.value) initGlobalConfig()
296+
if (globalConfigData.value) initGlobalConfig()
229297
}, { once: true })
298+
299+
onMounted(() => {
300+
const currentRouteTitle = menuItemList.find(item => item.routerName == route.path)?.title || route.name
301+
openTabs.value.push({
302+
name: route.path,
303+
title: currentRouteTitle,
304+
path: route.path,
305+
closable: route.path !== "/",
306+
componentName: route.name as string
307+
})
308+
activeTabName.value = route.path
309+
})
230310
</script>
231311

232312
<template>
233313
<el-container class="layout-container-demo">
234314
<el-aside :style="asideStyle">
235-
<el-menu default-active="0" :collapse="globalConfig.collapse" class="layout-aside-menu">
315+
<el-menu :default-active="activeMenu" :collapse="globalConfig.collapse" class="layout-aside-menu">
236316
<template v-for="item, index in menuItemList" :key="index">
237-
<el-menu-item :index="`${index}`" @click="goto(item.routerName)" :ref="item.ref || undefined">
317+
<el-menu-item :index="index.toString()" @click="goto(item.routerName)" :ref="item.ref || undefined">
238318
<el-icon>
239319
<component :is="item.icon" />
240320
</el-icon>
241321
<template #title>{{ item.title }}</template>
242322
</el-menu-item>
243323
</template>
244-
<el-menu-item ref="themeRef" @click="updateTheme">
324+
<el-menu-item :index="(menuItemList.length + 1).toString()" ref="themeRef" @click="updateTheme">
245325
<el-icon v-if="!isDarktheme">
246326
<Sunny />
247327
</el-icon>
@@ -253,11 +333,16 @@ watch(isFetching, () => {
253333
</el-aside>
254334
<el-main @contextmenu="onContextMenu" :style="mainStyle" ref="mainRef">
255335
<el-config-provider :locale="zhCn" :card="cardConfig" :dialog="dialogConfig" :message="messageConfig">
256-
<router-view v-slot="{ Component }">
257-
<keep-alive :include="['home']">
258-
<component :is="Component" />
259-
</keep-alive>
260-
</router-view>
336+
<el-tabs v-model="activeTabName" type="card" closable @tab-remove="removeTab" @tab-change="changeTab">
337+
<el-tab-pane v-for="item in openTabs" :key="item.name" :label="item.title" :name="item.name"
338+
:closable="item.closable">
339+
<router-view v-slot="{ Component }">
340+
<keep-alive :include="components">
341+
<component :is="Component" />
342+
</keep-alive>
343+
</router-view>
344+
</el-tab-pane>
345+
</el-tabs>
261346
<!--
262347
el-tour组件finish和close事件的的使用示例
263348
https://github.com/element-plus/element-plus/issues/22419

frontend/src/pages/about/index.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,8 @@ onMounted(() => {
7171
</div>
7272
</el-card>
7373
</template>
74+
<route lang="json">
75+
{
76+
"name": "about"
77+
}
78+
</route>

frontend/src/pages/changelog/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,4 +65,9 @@ onMounted(() => {
6565
<div class="md-content" v-html="model.changelog"></div>
6666
</div>
6767
</el-card>
68-
</template>
68+
</template>
69+
<route lang="json">
70+
{
71+
"name": "changelog"
72+
}
73+
</route>

frontend/src/pages/danmaku/index.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ onMounted(() => {
8989
</template>
9090
<route lang="json">
9191
{
92+
"name": "danmaku",
9293
"meta": {
9394
"layout": "blank"
9495
}

frontend/src/pages/history/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,4 +180,9 @@ onMounted(() => {
180180
</el-card>
181181
</el-main>
182182
</el-container>
183-
</template>
183+
</template>
184+
<route lang="json">
185+
{
186+
"name": "history"
187+
}
188+
</route>

frontend/src/pages/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ watch(danmakuList, async () => {
145145
})
146146
147147
onMounted(() => {
148-
const height = window.innerHeight * 0.9
148+
const height = window.innerHeight * 0.9 - 40
149149
chatMainRef.value?.$el.style.setProperty("overflow", "hidden")
150150
151151
const listHeight = height * 0.8
@@ -256,3 +256,8 @@ onMounted(() => {
256256
</el-main>
257257
</el-container>
258258
</template>
259+
<route lang="json">
260+
{
261+
"name": "home"
262+
}
263+
</route>

frontend/src/pages/playlist/index.vue

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,7 @@ const nextPage = (remainDistance: number) => {
236236
}
237237
238238
onMounted(() => {
239-
const height = window.innerHeight * 0.9
239+
const height = window.innerHeight * 0.9 - 60
240240
cardRef.value?.$el.style.setProperty("overflow", "hidden")
241241
242242
const listHeight = height * 0.6
@@ -309,4 +309,9 @@ onMounted(() => {
309309
<add-playlist-dialog @submit="emitSubmit" ref="addPlaylistDialogRef" />
310310
</el-main>
311311
</el-container>
312-
</template>
312+
</template>
313+
<route lang="json">
314+
{
315+
"name": "playlist"
316+
}
317+
</route>

frontend/src/pages/settings/index.vue

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import type { TabsPaneContext } from "element-plus"
33
44
defineOptions({
5-
name: "settings"
5+
name: "settings"
66
})
77
88
const activeName = ref("1")
@@ -30,3 +30,8 @@ const tabClickHandle = (pane: TabsPaneContext, ev: Event) => {
3030
</el-main>
3131
</el-container>
3232
</template>
33+
<route lang="json">
34+
{
35+
"name": "settings"
36+
}
37+
</route>

frontend/src/route-map.d.ts

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -26,50 +26,50 @@ declare module 'vue-router/auto-routes' {
2626
* Route name map generated by vue-router
2727
*/
2828
export interface RouteNamedMap {
29-
'/': RouteRecordInfo<
30-
'/',
29+
'home': RouteRecordInfo<
30+
'home',
3131
'/',
3232
Record<never, never>,
3333
Record<never, never>,
3434
| never
3535
>,
36-
'/about/': RouteRecordInfo<
37-
'/about/',
36+
'about': RouteRecordInfo<
37+
'about',
3838
'/about',
3939
Record<never, never>,
4040
Record<never, never>,
4141
| never
4242
>,
43-
'/changelog/': RouteRecordInfo<
44-
'/changelog/',
43+
'changelog': RouteRecordInfo<
44+
'changelog',
4545
'/changelog',
4646
Record<never, never>,
4747
Record<never, never>,
4848
| never
4949
>,
50-
'/danmaku/': RouteRecordInfo<
51-
'/danmaku/',
50+
'danmaku': RouteRecordInfo<
51+
'danmaku',
5252
'/danmaku',
5353
Record<never, never>,
5454
Record<never, never>,
5555
| never
5656
>,
57-
'/history/': RouteRecordInfo<
58-
'/history/',
57+
'history': RouteRecordInfo<
58+
'history',
5959
'/history',
6060
Record<never, never>,
6161
Record<never, never>,
6262
| never
6363
>,
64-
'/playlist/': RouteRecordInfo<
65-
'/playlist/',
64+
'playlist': RouteRecordInfo<
65+
'playlist',
6666
'/playlist',
6767
Record<never, never>,
6868
Record<never, never>,
6969
| never
7070
>,
71-
'/settings/': RouteRecordInfo<
72-
'/settings/',
71+
'settings': RouteRecordInfo<
72+
'settings',
7373
'/settings',
7474
Record<never, never>,
7575
Record<never, never>,
@@ -90,43 +90,43 @@ declare module 'vue-router/auto-routes' {
9090
export interface _RouteFileInfoMap {
9191
'src/pages/index.vue': {
9292
routes:
93-
| '/'
93+
| 'home'
9494
views:
9595
| never
9696
}
9797
'src/pages/about/index.vue': {
9898
routes:
99-
| '/about/'
99+
| 'about'
100100
views:
101101
| never
102102
}
103103
'src/pages/changelog/index.vue': {
104104
routes:
105-
| '/changelog/'
105+
| 'changelog'
106106
views:
107107
| never
108108
}
109109
'src/pages/danmaku/index.vue': {
110110
routes:
111-
| '/danmaku/'
111+
| 'danmaku'
112112
views:
113113
| never
114114
}
115115
'src/pages/history/index.vue': {
116116
routes:
117-
| '/history/'
117+
| 'history'
118118
views:
119119
| never
120120
}
121121
'src/pages/playlist/index.vue': {
122122
routes:
123-
| '/playlist/'
123+
| 'playlist'
124124
views:
125125
| never
126126
}
127127
'src/pages/settings/index.vue': {
128128
routes:
129-
| '/settings/'
129+
| 'settings'
130130
views:
131131
| never
132132
}

0 commit comments

Comments
 (0)