fix: 优化顶部标签页的滚动条
Build and Deploy Vue3 / build (push) Successful in 1m32s
Build and Deploy Vue3 / deploy (push) Successful in 1m13s

This commit is contained in:
2026-04-21 15:16:32 +08:00
parent 13248468d3
commit 2e073c2b87
2 changed files with 76 additions and 67 deletions
+73 -65
View File
@@ -1,9 +1,9 @@
<template>
<div class="tags-view-container">
<div class="tags-view-container"
@mouseenter="hovered = true" @mouseleave="hovered = false">
<div class="tags-view-wrapper" ref="scrollWrapperRef"
@wheel.prevent="handleWheel"
@mouseenter="hovered = true" @mouseleave="hovered = false"
@scroll="updateScrollbar">
@scroll="onScroll">
<div class="tags-view-scroll">
<router-link
v-for="tag in visitedViews"
@@ -25,9 +25,10 @@
</el-icon>
</router-link>
</div>
<div class="scroll-track" :class="{ visible: hovered && hasOverflow }">
<div class="scroll-thumb" :style="thumbStyle" @mousedown="onThumbDown"></div>
</div>
</div>
<div class="scroll-track" :class="{ visible: hovered && hasOverflow }">
<div class="scroll-thumb" :style="thumbStyle" @mousedown="onThumbDown"></div>
</div>
<!-- 右键菜单 -->
@@ -71,28 +72,20 @@ const router = useRouter()
const route = useRoute()
const tagsViewStore = useTagsViewStore()
// 访问过的标签 (从 store 获取)
const visitedViews = computed(() => tagsViewStore.visitedViews)
const affixTags = computed(() => tagsViewStore.affixTags)
// 右键菜单
const visible = ref(false)
const top = ref(0)
const left = ref(0)
const selectedTag = ref({})
// 初始化标签
const initTags = () => {
// 如果当前路由不在访问过的标签中,添加它
if (route.name) {
tagsViewStore.addVisitedView(route)
}
// 添加固定标签(仪表盘)
const dashboardRoute = router.getRoutes().find(r => r.name === 'Dashboard')
if (dashboardRoute) {
// 注意:这里我们直接修改 store 的 affixTags,或者 store 应该提供一个 action
// 简单起见,我们假设 store 的 affixTags 是可以直接修改的 ref,或者我们在 store 中添加初始化逻辑
// 但为了保持一致性,我们这里只处理 visitedViews 的添加
if (!tagsViewStore.affixTags.some(tag => tag.path === dashboardRoute.path)) {
tagsViewStore.affixTags.push(dashboardRoute)
}
@@ -100,13 +93,11 @@ const initTags = () => {
}
}
// 刷新选中的标签
const refreshSelectedTag = (view) => {
const { fullPath } = view
router.replace('/redirect' + fullPath)
}
// 关闭选中的标签
const closeSelectedTag = (view) => {
tagsViewStore.delVisitedView(view).then((visitedViews) => {
if (isActive(view)) {
@@ -115,15 +106,11 @@ const closeSelectedTag = (view) => {
})
}
// 关闭其他标签
const closeOthersTags = () => {
router.push(selectedTag.value)
tagsViewStore.delOthersViews(selectedTag.value).then(() => {
// moveToCurrentTag() // 如果有滚动逻辑
})
tagsViewStore.delOthersViews(selectedTag.value)
}
// 关闭左侧标签
const closeLeftTags = () => {
tagsViewStore.delLeftViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
@@ -132,7 +119,6 @@ const closeLeftTags = () => {
})
}
// 关闭右侧标签
const closeRightTags = () => {
tagsViewStore.delRightViews(selectedTag.value).then((visitedViews) => {
if (!visitedViews.find(i => i.path === route.path)) {
@@ -141,20 +127,17 @@ const closeRightTags = () => {
})
}
// 关闭所有标签
const closeAllTags = () => {
tagsViewStore.delAllViews().then((visitedViews) => {
toLastView(visitedViews)
})
}
// 跳转到最后一个标签或首页
const toLastView = (visitedViews, view) => {
const latestView = visitedViews.slice(-1)[0]
if (latestView) {
router.push(latestView)
} else {
// 如果没有标签,则跳转到首页
if (view && view.name === 'Dashboard') {
router.push('/redirect' + '/dashboard')
} else {
@@ -163,17 +146,14 @@ const toLastView = (visitedViews, view) => {
}
}
// 判断是否是当前激活的标签
const isActive = (tag) => {
return tag.path === route.path
}
// 判断是否是固定标签
const isAffix = (tag) => {
return tag.meta && tag.meta.affix
}
// 打开右键菜单
const openMenu = (e, tag) => {
const menuMinWidth = 125
const offsetLeft = e.clientX
@@ -187,7 +167,7 @@ const openMenu = (e, tag) => {
selectedTag.value = tag
}
// 横向滚轮 + 自定义滚动条
// ---- 滚动 & 滚动条 ----
const scrollWrapperRef = ref(null)
const hovered = ref(false)
const hasOverflow = ref(false)
@@ -199,26 +179,44 @@ const handleWheel = (e) => {
}
}
const getTrackWidth = () => {
const el = scrollWrapperRef.value
if (!el) return 0
return el.clientWidth - 24
}
const updateScrollbar = () => {
const refreshState = () => {
const el = scrollWrapperRef.value
if (!el) return
hasOverflow.value = el.scrollWidth > el.clientWidth
if (!hasOverflow.value) return
const tw = getTrackWidth()
const ratio = el.clientWidth / el.scrollWidth
const thumbW = Math.max(ratio * tw, 30)
const maxScroll = el.scrollWidth - el.clientWidth
const scrollRatio = maxScroll > 0 ? el.scrollLeft / maxScroll : 0
const thumbLeft = scrollRatio * (tw - thumbW)
const { scrollLeft, scrollWidth, clientWidth } = el
const maxScroll = scrollWidth - clientWidth
hasOverflow.value = maxScroll > 1
if (!hasOverflow.value) {
thumbStyle.value = { width: '0px', left: '0px' }
return
}
const trackWidth = clientWidth
const thumbW = Math.max((clientWidth / scrollWidth) * trackWidth, 30)
const scrollRatio = maxScroll > 0 ? scrollLeft / maxScroll : 0
const thumbLeft = scrollRatio * (trackWidth - thumbW)
thumbStyle.value = { width: thumbW + 'px', left: thumbLeft + 'px' }
}
const onScroll = () => {
refreshState()
}
const scrollToActiveTag = () => {
const el = scrollWrapperRef.value
if (!el) return
const activeEl = el.querySelector('.active-tag')
if (!activeEl) return
const wrapperRect = el.getBoundingClientRect()
const tagRect = activeEl.getBoundingClientRect()
if (tagRect.left < wrapperRect.left + 28) {
el.scrollLeft -= (wrapperRect.left + 28 - tagRect.left + 12)
} else if (tagRect.right > wrapperRect.right - 28) {
el.scrollLeft += (tagRect.right - wrapperRect.right + 28 + 12)
}
}
const onThumbDown = (e) => {
e.preventDefault()
const el = scrollWrapperRef.value
@@ -226,10 +224,9 @@ const onThumbDown = (e) => {
const startX = e.clientX
const startScroll = el.scrollLeft
const maxScroll = el.scrollWidth - el.clientWidth
const tw = getTrackWidth()
const ratio = el.clientWidth / el.scrollWidth
const thumbW = Math.max(ratio * tw, 30)
const movable = tw - thumbW
const trackWidth = el.clientWidth
const thumbW = Math.max((el.clientWidth / el.scrollWidth) * trackWidth, 30)
const movable = trackWidth - thumbW
const onMove = (ev) => {
const dx = ev.clientX - startX
@@ -244,35 +241,38 @@ const onThumbDown = (e) => {
document.addEventListener('mouseup', onUp)
}
watch(visitedViews, () => nextTick(updateScrollbar), { deep: true })
watch(visitedViews, () => nextTick(() => { refreshState(); scrollToActiveTag() }), { deep: true })
// 关闭右键菜单
const closeMenu = () => {
visible.value = false
}
// 监听路由变化,添加标签
watch(route, (newRoute) => {
if (newRoute.name) {
tagsViewStore.addVisitedView(newRoute)
}
nextTick(scrollToActiveTag)
})
// 点击其他区域关闭右键菜单
const handleClickOutside = () => {
closeMenu()
}
const onResize = () => {
refreshState()
scrollToActiveTag()
}
onMounted(() => {
initTags()
document.addEventListener('click', handleClickOutside)
nextTick(updateScrollbar)
window.addEventListener('resize', updateScrollbar)
nextTick(() => { refreshState(); scrollToActiveTag() })
window.addEventListener('resize', onResize)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutside)
window.removeEventListener('resize', updateScrollbar)
window.removeEventListener('resize', onResize)
})
</script>
@@ -283,16 +283,19 @@ onBeforeUnmount(() => {
background-color: #ffffff;
border-bottom: 1px solid #e1e8ed;
z-index: 10;
display: flex;
align-items: stretch;
position: relative;
overflow: hidden;
}
/* 标签滚动区域 */
.tags-view-wrapper {
flex: 1;
min-width: 0;
height: 100%;
width: 100%;
padding: 0 12px;
overflow-x: scroll;
overflow-y: hidden;
white-space: nowrap;
position: relative;
scrollbar-width: none;
-ms-overflow-style: none;
}
@@ -302,17 +305,19 @@ onBeforeUnmount(() => {
}
.tags-view-scroll {
display: flex;
display: inline-flex;
align-items: center;
height: 100%;
padding: 0 8px;
gap: 4px;
}
/* 底部自定义滚动条(在容器上,不在滚动区域内) */
.scroll-track {
position: absolute;
left: 12px;
right: 12px;
bottom: 1px;
left: 0;
right: 0;
bottom: 0;
height: 3px;
opacity: 0;
transition: opacity 0.25s;
@@ -339,12 +344,12 @@ onBeforeUnmount(() => {
background: rgba(180,188,199,0.65);
}
/* 标签样式 */
.tag, .active-tag {
height: 32px;
display: inline-flex;
align-items: center;
padding: 0 12px;
margin-right: 0;
border-radius: 0;
font-size: 13px;
text-decoration: none;
@@ -352,6 +357,8 @@ onBeforeUnmount(() => {
transition: all 0.2s ease;
border: 1px solid transparent;
border-bottom: none;
flex-shrink: 0;
white-space: nowrap;
}
.tag {
@@ -426,6 +433,7 @@ onBeforeUnmount(() => {
background-color: rgba(231, 76, 60, 0.1);
}
/* 右键菜单 */
.contextmenu {
position: fixed;
z-index: 100;
@@ -461,4 +469,4 @@ onBeforeUnmount(() => {
.contextmenu li:hover .el-icon {
color: #2c3e50;
}
</style>
</style>