Skip to content

Commit 27d081c

Browse files
eq1024Vigo.zhou
andauthored
feat: add scrollable tab bar (#49)
* feat: add scrollbar support and enhance tab navigation finish:1/2 * update * feat: 添加当前tab滚动功能并优化滚动条样式 * feat: 重构tab滚动逻辑,新增useTabScroll钩子以优化当前tab滚动体验 * feat: 优化TabBar组件,移除冗余代码并整合useTabScroll钩子 * fix: silly bug * refactor: Remove the debugging log --------- Co-authored-by: Vigo.zhou <eq1024@foxmail.com>
1 parent 44ebd5f commit 27d081c

File tree

2 files changed

+94
-21
lines changed

2 files changed

+94
-21
lines changed

src/hooks/useTabScroll.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import type { NScrollbar } from 'naive-ui'
2+
import { ref, watchEffect, type Ref } from 'vue'
3+
import { throttle } from 'radash'
4+
5+
export function useTabScroll(currentTabPath: Ref<string>) {
6+
const scrollbar = ref<InstanceType<typeof NScrollbar>>()
7+
const safeArea = ref(150)
8+
9+
const handleTabSwitch = (distance: number) => {
10+
scrollbar.value?.scrollTo({
11+
left: distance,
12+
behavior: 'smooth'
13+
})
14+
}
15+
16+
const scrollToCurrentTab = () => {
17+
nextTick(() => {
18+
const currentTabElement = document.querySelector(`[data-tab-path="${currentTabPath.value}"]`) as HTMLElement
19+
const tabBarScrollWrapper = document.querySelector('.tab-bar-scroller-wrapper .n-scrollbar-container')
20+
const tabBarScrollContent = document.querySelector('.tab-bar-scroller-content')
21+
22+
if (currentTabElement && tabBarScrollContent && tabBarScrollWrapper) {
23+
const tabLeft = currentTabElement.offsetLeft
24+
const tabBarLeft = tabBarScrollWrapper.scrollLeft
25+
const wrapperWidth = tabBarScrollWrapper.getBoundingClientRect().width
26+
const tabWidth = currentTabElement.getBoundingClientRect().width
27+
const containerPR = Number.parseFloat(window.getComputedStyle(tabBarScrollContent).paddingRight)
28+
29+
if (tabLeft + tabWidth + safeArea.value + containerPR > wrapperWidth + tabBarLeft) {
30+
handleTabSwitch(tabLeft + tabWidth + containerPR - wrapperWidth + safeArea.value)
31+
} else if (tabLeft - safeArea.value < tabBarLeft) {
32+
handleTabSwitch(tabLeft - safeArea.value)
33+
}
34+
}
35+
})
36+
}
37+
38+
const handleScroll = throttle({ interval: 120 }, (step: number) => {
39+
scrollbar.value?.scrollBy({
40+
left: step * 400,
41+
behavior: 'smooth'
42+
})
43+
})
44+
45+
const onWheel = (e: WheelEvent) => {
46+
e.preventDefault()
47+
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
48+
handleScroll(e.deltaY > 0 ? 1 : -1)
49+
}
50+
}
51+
52+
watchEffect(() => {
53+
if (currentTabPath.value) {
54+
scrollToCurrentTab()
55+
}
56+
})
57+
58+
return {
59+
scrollbar,
60+
onWheel,
61+
safeArea,
62+
handleTabSwitch
63+
}
64+
}

src/layouts/components/tab/TabBar.vue

Lines changed: 30 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import type { RouteLocationNormalized } from 'vue-router'
33
import { useAppStore, useTabStore } from '@/store'
4+
import { useTabScroll } from '@/hooks/useTabScroll'
45
import { useDraggable } from 'vue-draggable-plus'
56
import IconClose from '~icons/icon-park-outline/close'
67
import IconDelete from '~icons/icon-park-outline/delete-four'
@@ -17,6 +18,8 @@ const tabStore = useTabStore()
1718
const { tabs } = storeToRefs(useTabStore())
1819
const appStore = useAppStore()
1920
21+
const {scrollbar, onWheel } = useTabScroll(computed(() => tabStore.currentTabPath))
22+
2023
const router = useRouter()
2124
function handleTab(route: RouteLocationNormalized) {
2225
router.push(route.fullPath)
@@ -111,32 +114,38 @@ useDraggable(el, tabs, {
111114
</script>
112115

113116
<template>
114-
<div class="p-l-2 flex w-full relative">
115-
<div class="flex items-end">
116-
<TabBarItem
117-
v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item"
118-
@click="handleTab(item)"
119-
/>
120-
</div>
121-
<div ref="el" class="flex items-end flex-1">
122-
<TabBarItem
123-
v-for="item in tabStore.tabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item" closable
124-
@close="tabStore.closeTab"
125-
@click="handleTab(item)"
126-
@contextmenu="handleContextMenu($event, item)"
127-
/>
128-
<n-dropdown
129-
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
130-
:on-clickoutside="onClickoutside" @select="handleSelect"
131-
/>
117+
<n-scrollbar ref="scrollbar" class="relative flex tab-bar-scroller-wrapper" content-class="pr-34 tab-bar-scroller-content" :x-scrollable="true" @wheel="onWheel">
118+
<div class="p-l-2 flex w-full relative">
119+
<div class="flex items-end">
120+
<TabBarItem
121+
v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item"
122+
@click="handleTab(item)"
123+
/>
124+
</div>
125+
<div ref="el" class="flex items-end flex-1">
126+
<TabBarItem
127+
v-for="item in tabStore.tabs"
128+
:key="item.fullPath"
129+
:value="tabStore.currentTabPath"
130+
:route="item"
131+
closable
132+
:data-tab-path="item.fullPath"
133+
@close="tabStore.closeTab"
134+
@click="handleTab(item)"
135+
@contextmenu="handleContextMenu($event, item)"
136+
/>
137+
<n-dropdown
138+
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
139+
:on-clickoutside="onClickoutside" @select="handleSelect"
140+
/>
141+
</div>
132142
</div>
133-
<!-- <span class="m-l-auto" /> -->
134-
<n-el class="absolute right-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
143+
<n-el class="absolute right-0 top-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
135144
<Reload />
136145
<ContentFullScreen />
137146
<DropTabs />
138147
</n-el>
139-
</div>
148+
</n-scrollbar>
140149
</template>
141150

142151
<style scoped>

0 commit comments

Comments
 (0)