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