fix: 优化顶部标签页的滚动条
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user