perf: 优化多标签滚动

This commit is contained in:
张传龙 2022-06-11 20:17:30 +08:00
parent 67d966e096
commit 0636ac4716
7 changed files with 196 additions and 89 deletions

View File

@ -23,6 +23,7 @@
},
"devDependencies": {
"@iconify-json/ant-design": "^1.1.1",
"@iconify-json/ic": "^1.1.2",
"@iconify-json/mdi": "^1.1.9",
"@iconify-json/simple-icons": "^1.1.7",
"@vitejs/plugin-vue": "^1.10.2",

8
pnpm-lock.yaml generated
View File

@ -2,6 +2,7 @@ lockfileVersion: 5.4
specifiers:
'@iconify-json/ant-design': ^1.1.1
'@iconify-json/ic': ^1.1.2
'@iconify-json/mdi': ^1.1.9
'@iconify-json/simple-icons': ^1.1.7
'@vitejs/plugin-vue': ^1.10.2
@ -48,6 +49,7 @@ dependencies:
devDependencies:
'@iconify-json/ant-design': 1.1.1
'@iconify-json/ic': 1.1.2
'@iconify-json/mdi': 1.1.9
'@iconify-json/simple-icons': 1.1.7
'@vitejs/plugin-vue': 1.10.2_vite@2.9.9
@ -158,6 +160,12 @@ packages:
'@iconify/types': 1.1.0
dev: true
/@iconify-json/ic/1.1.2:
resolution: {integrity: sha512-OXLXNMECrwg1N7HqG9z+p8eF9NleoV5tZvIH/W3ip3HdZsg1VbEWl0RpI9K5VJjT95xVn2n+gcAgLPD5HNAsXg==}
dependencies:
'@iconify/types': 1.1.0
dev: true
/@iconify-json/mdi/1.1.9:
resolution: {integrity: sha512-iZY3d7nLmEhSxLU5YBHIxVVPySqNjj6zYcf448TXGBPp2PyToITCOuLEaub0rQ9jBAPOlupQsuPX9ylBfgIJ1w==}
dependencies:

View File

@ -20,5 +20,7 @@ export { default as IconExit } from '~icons/mdi/exit-to-app'
export { default as IconFullscreen } from '~icons/ant-design/fullscreen-outlined'
export { default as IconFullscreenExit } from '~icons/ant-design/fullscreen-exit-outlined'
export { default as IconArrowLeft } from '~icons/ic/baseline-keyboard-arrow-left'
export { default as IconArrowRight } from '~icons/ic/baseline-keyboard-arrow-right'
export { default as IconLogo } from './IconLogo.vue'

View File

@ -0,0 +1,145 @@
<template>
<div ref="wrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
<template v-if="showArrow && isOverflow">
<div class="left" @click="handleMouseWheel({ wheelDelta: 50 })">
<IconArrowLeft />
</div>
<div class="right" @click="handleMouseWheel({ wheelDelta: -50 })">
<IconArrowRight />
</div>
</template>
<div
ref="content"
class="tags-content"
:class="{ overflow: isOverflow && showArrow }"
:style="{
height: height + 'px',
transform: `translateX(${translateX}px)`,
}"
>
<slot />
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { IconArrowLeft, IconArrowRight } from '@/components/AppIcons'
import { debounce } from '@/utils'
import { isNullOrUndef } from '@/utils/is'
defineProps({
height: {
type: Number,
default: 50,
},
showArrow: {
type: Boolean,
default: true,
},
})
onMounted(() => {
refreshIsOverflow()
})
const translateX = ref(0)
const content = ref(null)
const wrapper = ref(null)
const isOverflow = ref(false)
function refreshIsOverflow(isIncrease) {
isOverflow.value = content.value.offsetWidth > wrapper.value.offsetWidth
if (isNullOrUndef(isIncrease)) return
if (isOverflow.value) {
handleMouseWheel({ wheelDelta: isIncrease ? -100 : 100 })
} else if (!isIncrease && translateX.value < 0) {
handleMouseWheel({ wheelDelta: 100 })
}
}
function handleMouseWheel(e) {
const { wheelDelta } = e
const wrapperWidth = wrapper.value.offsetWidth
const contentWidth = content.value.offsetWidth
/**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值
* @wrapperWidth 容器的宽度
* @contentWidth 内容的宽度
*/
if (wheelDelta < 0 && -translateX.value > contentWidth - wrapperWidth + 10) {
return
}
if (wheelDelta > 0 && translateX.value > 10) {
return
}
translateX.value += wheelDelta
resetTranslateX(wrapperWidth, contentWidth)
}
const resetTranslateX = debounce(function (wrapperWidth, contentWidth) {
if (!isOverflow.value) {
translateX.value = 0
} else if (-translateX.value > contentWidth - wrapperWidth) {
translateX.value = wrapperWidth - contentWidth
} else if (translateX.value > 0) {
translateX.value = 0
}
}, 200)
defineExpose({
refreshIsOverflow,
})
</script>
<style lang="scss" scoped>
.tags-wrapper {
display: flex;
background-color: #fff;
position: sticky;
top: 0;
z-index: 9;
overflow: hidden;
.tags-content {
padding: 0 10px;
display: flex;
align-items: center;
flex-wrap: nowrap;
transition: transform 0.5s;
&.overflow {
padding-left: 30px;
padding-right: 30px;
}
}
.left,
.right {
background-color: #fff;
position: absolute;
top: 0;
bottom: 0;
margin: auto;
width: 20px;
height: 35px;
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
border: 1px solid #e0e0e6;
border-radius: 2px;
z-index: 2;
cursor: pointer;
}
.left {
left: 0;
}
.right {
right: 0;
}
}
</style>

View File

@ -5,5 +5,6 @@ import SideMenu from './components/SideMenu.vue'
<template>
<SideLogo />
<div h-1 bg-gray-200></div>
<SideMenu />
</template>

View File

@ -1,14 +1,5 @@
<template>
<div ref="tagsWrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
<div
ref="tagsContent"
class="tags-content"
:style="{
height: useTheme.tags.height + 'px',
transform: `translateX(${translateX}px)`,
}"
:wrap="false"
>
<ScrollX ref="scrollX" :height="useTheme.tags.height">
<n-tag
v-for="tag in tagsStore.tags"
:key="tag.path"
@ -20,8 +11,7 @@
>
{{ tag.title }}
</n-tag>
</div>
</div>
</ScrollX>
<ContextMenu
v-model:show="contextMenuOption.show"
@ -37,6 +27,7 @@ import { nextTick, reactive, ref, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { useTagsStore } from '@/store/modules/tags'
import { useThemeStore } from '@/store/modules/theme'
import ScrollX from '@/components/Common/ScrollX.vue'
const route = useRoute()
const router = useRouter()
@ -49,9 +40,6 @@ const contextMenuOption = reactive({
y: 0,
currentPath: '',
})
let translateX = ref(0)
const tagsContent = ref()
const tagsWrapper = ref()
watch(
() => route.path,
@ -63,6 +51,15 @@ watch(
{ immediate: true }
)
const scrollX = ref(null)
watch(
() => tagsStore.tags,
async (newVal, oldVal) => {
await nextTick()
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
}
)
const handleTagClick = (path) => {
tagsStore.setActiveTag(path)
router.push(path)
@ -86,55 +83,9 @@ async function handleContextMenu(e, tagItem) {
await nextTick()
showContextMenu()
}
function handleMouseWheel(e) {
let { wheelDelta } = e
const tagsWrapperWidth = tagsWrapper.value.offsetWidth
const tagsContentWidth = tagsContent.value.offsetWidth
/**
* @wheelDelta 平行滚动的值 >0 右移 <0: 左移
* @translateX 内容translateX的值
* @tagsWrapperWidth 容器的宽度
* @tagsContentWidth 内容的宽度
*/
//
if (wheelDelta > 0 && translateX.value >= 0) {
return
}
// +
if (wheelDelta < 0 && tagsWrapperWidth > tagsContentWidth + translateX.value) {
return
}
if (wheelDelta > 0 && wheelDelta + translateX.value > 0) {
wheelDelta = -translateX.value
}
if (wheelDelta < 0 && -wheelDelta > tagsWrapperWidth - (tagsContentWidth + translateX.value)) {
wheelDelta = tagsWrapperWidth - (tagsContentWidth + translateX.value)
}
translateX.value += wheelDelta
}
</script>
<style lang="scss">
.tags-wrapper {
display: flex;
background-color: #fff;
position: sticky;
top: 0;
z-index: 9;
overflow: hidden;
.tags-content {
padding: 0 10px;
display: flex;
align-items: center;
flex-wrap: nowrap;
transition: transform 0.3s;
}
.n-tag {
padding: 0 15px;
margin: 0 5px;
@ -155,5 +106,4 @@ function handleMouseWheel(e) {
color: $primaryColor;
}
}
}
</style>

View File

@ -31,7 +31,7 @@ body {
.cur-scroll {
&::-webkit-scrollbar{
width:8px;
height:8px;
height:6px;
}
&::-webkit-scrollbar-thumb{
background-color: transparent;