perf: 优化多标签滚动
This commit is contained in:
parent
67d966e096
commit
0636ac4716
@ -23,6 +23,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/ant-design": "^1.1.1",
|
"@iconify-json/ant-design": "^1.1.1",
|
||||||
|
"@iconify-json/ic": "^1.1.2",
|
||||||
"@iconify-json/mdi": "^1.1.9",
|
"@iconify-json/mdi": "^1.1.9",
|
||||||
"@iconify-json/simple-icons": "^1.1.7",
|
"@iconify-json/simple-icons": "^1.1.7",
|
||||||
"@vitejs/plugin-vue": "^1.10.2",
|
"@vitejs/plugin-vue": "^1.10.2",
|
||||||
|
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@ -2,6 +2,7 @@ lockfileVersion: 5.4
|
|||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@iconify-json/ant-design': ^1.1.1
|
'@iconify-json/ant-design': ^1.1.1
|
||||||
|
'@iconify-json/ic': ^1.1.2
|
||||||
'@iconify-json/mdi': ^1.1.9
|
'@iconify-json/mdi': ^1.1.9
|
||||||
'@iconify-json/simple-icons': ^1.1.7
|
'@iconify-json/simple-icons': ^1.1.7
|
||||||
'@vitejs/plugin-vue': ^1.10.2
|
'@vitejs/plugin-vue': ^1.10.2
|
||||||
@ -48,6 +49,7 @@ dependencies:
|
|||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@iconify-json/ant-design': 1.1.1
|
'@iconify-json/ant-design': 1.1.1
|
||||||
|
'@iconify-json/ic': 1.1.2
|
||||||
'@iconify-json/mdi': 1.1.9
|
'@iconify-json/mdi': 1.1.9
|
||||||
'@iconify-json/simple-icons': 1.1.7
|
'@iconify-json/simple-icons': 1.1.7
|
||||||
'@vitejs/plugin-vue': 1.10.2_vite@2.9.9
|
'@vitejs/plugin-vue': 1.10.2_vite@2.9.9
|
||||||
@ -158,6 +160,12 @@ packages:
|
|||||||
'@iconify/types': 1.1.0
|
'@iconify/types': 1.1.0
|
||||||
dev: true
|
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:
|
/@iconify-json/mdi/1.1.9:
|
||||||
resolution: {integrity: sha512-iZY3d7nLmEhSxLU5YBHIxVVPySqNjj6zYcf448TXGBPp2PyToITCOuLEaub0rQ9jBAPOlupQsuPX9ylBfgIJ1w==}
|
resolution: {integrity: sha512-iZY3d7nLmEhSxLU5YBHIxVVPySqNjj6zYcf448TXGBPp2PyToITCOuLEaub0rQ9jBAPOlupQsuPX9ylBfgIJ1w==}
|
||||||
dependencies:
|
dependencies:
|
||||||
|
@ -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 IconFullscreen } from '~icons/ant-design/fullscreen-outlined'
|
||||||
export { default as IconFullscreenExit } from '~icons/ant-design/fullscreen-exit-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'
|
export { default as IconLogo } from './IconLogo.vue'
|
||||||
|
145
src/components/Common/ScrollX.vue
Normal file
145
src/components/Common/ScrollX.vue
Normal 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>
|
@ -5,5 +5,6 @@ import SideMenu from './components/SideMenu.vue'
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<SideLogo />
|
<SideLogo />
|
||||||
|
<div h-1 bg-gray-200></div>
|
||||||
<SideMenu />
|
<SideMenu />
|
||||||
</template>
|
</template>
|
||||||
|
@ -1,27 +1,17 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="tagsWrapper" class="tags-wrapper" @mousewheel.prevent="handleMouseWheel">
|
<ScrollX ref="scrollX" :height="useTheme.tags.height">
|
||||||
<div
|
<n-tag
|
||||||
ref="tagsContent"
|
v-for="tag in tagsStore.tags"
|
||||||
class="tags-content"
|
:key="tag.path"
|
||||||
:style="{
|
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
|
||||||
height: useTheme.tags.height + 'px',
|
:closable="tagsStore.tags.length > 1"
|
||||||
transform: `translateX(${translateX}px)`,
|
@click="handleTagClick(tag.path)"
|
||||||
}"
|
@close.stop="tagsStore.removeTag(tag.path)"
|
||||||
:wrap="false"
|
@contextmenu.prevent="handleContextMenu($event, tag)"
|
||||||
>
|
>
|
||||||
<n-tag
|
{{ tag.title }}
|
||||||
v-for="tag in tagsStore.tags"
|
</n-tag>
|
||||||
:key="tag.path"
|
</ScrollX>
|
||||||
:type="tagsStore.activeTag === tag.path ? 'primary' : 'default'"
|
|
||||||
:closable="tagsStore.tags.length > 1"
|
|
||||||
@click="handleTagClick(tag.path)"
|
|
||||||
@close.stop="tagsStore.removeTag(tag.path)"
|
|
||||||
@contextmenu.prevent="handleContextMenu($event, tag)"
|
|
||||||
>
|
|
||||||
{{ tag.title }}
|
|
||||||
</n-tag>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
v-model:show="contextMenuOption.show"
|
v-model:show="contextMenuOption.show"
|
||||||
@ -37,6 +27,7 @@ import { nextTick, reactive, ref, watch } from 'vue'
|
|||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useTagsStore } from '@/store/modules/tags'
|
import { useTagsStore } from '@/store/modules/tags'
|
||||||
import { useThemeStore } from '@/store/modules/theme'
|
import { useThemeStore } from '@/store/modules/theme'
|
||||||
|
import ScrollX from '@/components/Common/ScrollX.vue'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -49,9 +40,6 @@ const contextMenuOption = reactive({
|
|||||||
y: 0,
|
y: 0,
|
||||||
currentPath: '',
|
currentPath: '',
|
||||||
})
|
})
|
||||||
let translateX = ref(0)
|
|
||||||
const tagsContent = ref()
|
|
||||||
const tagsWrapper = ref()
|
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => route.path,
|
() => route.path,
|
||||||
@ -63,6 +51,15 @@ watch(
|
|||||||
{ immediate: true }
|
{ immediate: true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const scrollX = ref(null)
|
||||||
|
watch(
|
||||||
|
() => tagsStore.tags,
|
||||||
|
async (newVal, oldVal) => {
|
||||||
|
await nextTick()
|
||||||
|
scrollX.value?.refreshIsOverflow(newVal.length > oldVal.length)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
const handleTagClick = (path) => {
|
const handleTagClick = (path) => {
|
||||||
tagsStore.setActiveTag(path)
|
tagsStore.setActiveTag(path)
|
||||||
router.push(path)
|
router.push(path)
|
||||||
@ -86,74 +83,27 @@ async function handleContextMenu(e, tagItem) {
|
|||||||
await nextTick()
|
await nextTick()
|
||||||
showContextMenu()
|
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>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.tags-wrapper {
|
.n-tag {
|
||||||
display: flex;
|
padding: 0 15px;
|
||||||
background-color: #fff;
|
margin: 0 5px;
|
||||||
position: sticky;
|
cursor: pointer;
|
||||||
top: 0;
|
.n-tag__close {
|
||||||
z-index: 9;
|
margin-left: 5px;
|
||||||
overflow: hidden;
|
box-sizing: content-box;
|
||||||
.tags-content {
|
font-size: 12px;
|
||||||
padding: 0 10px;
|
padding: 2px;
|
||||||
display: flex;
|
border-radius: 50%;
|
||||||
align-items: center;
|
transition: all 0.7s;
|
||||||
flex-wrap: nowrap;
|
|
||||||
transition: transform 0.3s;
|
|
||||||
}
|
|
||||||
.n-tag {
|
|
||||||
padding: 0 15px;
|
|
||||||
margin: 0 5px;
|
|
||||||
cursor: pointer;
|
|
||||||
.n-tag__close {
|
|
||||||
margin-left: 5px;
|
|
||||||
box-sizing: content-box;
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 2px;
|
|
||||||
border-radius: 50%;
|
|
||||||
transition: all 0.7s;
|
|
||||||
&:hover {
|
|
||||||
color: #fff;
|
|
||||||
background-color: $primaryColor;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: $primaryColor;
|
color: #fff;
|
||||||
|
background-color: $primaryColor;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
&:hover {
|
||||||
|
color: $primaryColor;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@ -31,7 +31,7 @@ body {
|
|||||||
.cur-scroll {
|
.cur-scroll {
|
||||||
&::-webkit-scrollbar{
|
&::-webkit-scrollbar{
|
||||||
width:8px;
|
width:8px;
|
||||||
height:8px;
|
height:6px;
|
||||||
}
|
}
|
||||||
&::-webkit-scrollbar-thumb{
|
&::-webkit-scrollbar-thumb{
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
Loading…
Reference in New Issue
Block a user