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>
This commit is contained in:
goodman 2025-04-30 16:03:35 +08:00 committed by GitHub
parent 44ebd5f19e
commit 27d081cb23
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 94 additions and 21 deletions

64
src/hooks/useTabScroll.ts Normal file
View File

@ -0,0 +1,64 @@
import type { NScrollbar } from 'naive-ui'
import { ref, watchEffect, type Ref } from 'vue'
import { throttle } from 'radash'
export function useTabScroll(currentTabPath: Ref<string>) {
const scrollbar = ref<InstanceType<typeof NScrollbar>>()
const safeArea = ref(150)
const handleTabSwitch = (distance: number) => {
scrollbar.value?.scrollTo({
left: distance,
behavior: 'smooth'
})
}
const scrollToCurrentTab = () => {
nextTick(() => {
const currentTabElement = document.querySelector(`[data-tab-path="${currentTabPath.value}"]`) as HTMLElement
const tabBarScrollWrapper = document.querySelector('.tab-bar-scroller-wrapper .n-scrollbar-container')
const tabBarScrollContent = document.querySelector('.tab-bar-scroller-content')
if (currentTabElement && tabBarScrollContent && tabBarScrollWrapper) {
const tabLeft = currentTabElement.offsetLeft
const tabBarLeft = tabBarScrollWrapper.scrollLeft
const wrapperWidth = tabBarScrollWrapper.getBoundingClientRect().width
const tabWidth = currentTabElement.getBoundingClientRect().width
const containerPR = Number.parseFloat(window.getComputedStyle(tabBarScrollContent).paddingRight)
if (tabLeft + tabWidth + safeArea.value + containerPR > wrapperWidth + tabBarLeft) {
handleTabSwitch(tabLeft + tabWidth + containerPR - wrapperWidth + safeArea.value)
} else if (tabLeft - safeArea.value < tabBarLeft) {
handleTabSwitch(tabLeft - safeArea.value)
}
}
})
}
const handleScroll = throttle({ interval: 120 }, (step: number) => {
scrollbar.value?.scrollBy({
left: step * 400,
behavior: 'smooth'
})
})
const onWheel = (e: WheelEvent) => {
e.preventDefault()
if (Math.abs(e.deltaY) > Math.abs(e.deltaX)) {
handleScroll(e.deltaY > 0 ? 1 : -1)
}
}
watchEffect(() => {
if (currentTabPath.value) {
scrollToCurrentTab()
}
})
return {
scrollbar,
onWheel,
safeArea,
handleTabSwitch
}
}

View File

@ -1,6 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import type { RouteLocationNormalized } from 'vue-router' import type { RouteLocationNormalized } from 'vue-router'
import { useAppStore, useTabStore } from '@/store' import { useAppStore, useTabStore } from '@/store'
import { useTabScroll } from '@/hooks/useTabScroll'
import { useDraggable } from 'vue-draggable-plus' import { useDraggable } from 'vue-draggable-plus'
import IconClose from '~icons/icon-park-outline/close' import IconClose from '~icons/icon-park-outline/close'
import IconDelete from '~icons/icon-park-outline/delete-four' import IconDelete from '~icons/icon-park-outline/delete-four'
@ -17,6 +18,8 @@ const tabStore = useTabStore()
const { tabs } = storeToRefs(useTabStore()) const { tabs } = storeToRefs(useTabStore())
const appStore = useAppStore() const appStore = useAppStore()
const {scrollbar, onWheel } = useTabScroll(computed(() => tabStore.currentTabPath))
const router = useRouter() const router = useRouter()
function handleTab(route: RouteLocationNormalized) { function handleTab(route: RouteLocationNormalized) {
router.push(route.fullPath) router.push(route.fullPath)
@ -111,32 +114,38 @@ useDraggable(el, tabs, {
</script> </script>
<template> <template>
<div class="p-l-2 flex w-full relative"> <n-scrollbar ref="scrollbar" class="relative flex tab-bar-scroller-wrapper" content-class="pr-34 tab-bar-scroller-content" :x-scrollable="true" @wheel="onWheel">
<div class="flex items-end"> <div class="p-l-2 flex w-full relative">
<TabBarItem <div class="flex items-end">
v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item" <TabBarItem
@click="handleTab(item)" v-for="item in tabStore.pinTabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item"
/> @click="handleTab(item)"
/>
</div>
<div ref="el" class="flex items-end flex-1">
<TabBarItem
v-for="item in tabStore.tabs"
:key="item.fullPath"
:value="tabStore.currentTabPath"
:route="item"
closable
:data-tab-path="item.fullPath"
@close="tabStore.closeTab"
@click="handleTab(item)"
@contextmenu="handleContextMenu($event, item)"
/>
<n-dropdown
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
:on-clickoutside="onClickoutside" @select="handleSelect"
/>
</div>
</div> </div>
<div ref="el" class="flex items-end flex-1"> <n-el class="absolute right-0 top-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
<TabBarItem
v-for="item in tabStore.tabs" :key="item.fullPath" :value="tabStore.currentTabPath" :route="item" closable
@close="tabStore.closeTab"
@click="handleTab(item)"
@contextmenu="handleContextMenu($event, item)"
/>
<n-dropdown
placement="bottom-start" trigger="manual" :x="x" :y="y" :options="options" :show="showDropdown"
:on-clickoutside="onClickoutside" @select="handleSelect"
/>
</div>
<!-- <span class="m-l-auto" /> -->
<n-el class="absolute right-0 flex items-center gap-1 bg-[var(--base-color)] h-full">
<Reload /> <Reload />
<ContentFullScreen /> <ContentFullScreen />
<DropTabs /> <DropTabs />
</n-el> </n-el>
</div> </n-scrollbar>
</template> </template>
<style scoped> <style scoped>