feat: Add useScrollTo hook
This commit is contained in:
parent
9d926b2760
commit
7d7fd9ed64
|
@ -1,7 +1,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
|
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
|
||||||
import { usePermissionStore } from '@/store/modules/permission'
|
import { usePermissionStore } from '@/store/modules/permission'
|
||||||
import { useTagsViewStore } from '@/store/modules/tagsView'
|
import { useTagsViewStore } from '@/store/modules/tagsView'
|
||||||
import { useI18n } from '@/hooks/web/useI18n'
|
import { useI18n } from '@/hooks/web/useI18n'
|
||||||
|
@ -9,6 +9,8 @@ import { filterAffixTags } from './helper'
|
||||||
import { ContextMenu, ContextMenuExpose } from '@/components/ContextMenu'
|
import { ContextMenu, ContextMenuExpose } from '@/components/ContextMenu'
|
||||||
import { useDesign } from '@/hooks/web/useDesign'
|
import { useDesign } from '@/hooks/web/useDesign'
|
||||||
import { useTemplateRefsList } from '@vueuse/core'
|
import { useTemplateRefsList } from '@vueuse/core'
|
||||||
|
import { ElScrollbar } from 'element-plus'
|
||||||
|
import { useScrollTo } from '@/hooks/event/useScrollTo'
|
||||||
|
|
||||||
const { getPrefixCls } = useDesign()
|
const { getPrefixCls } = useDesign()
|
||||||
|
|
||||||
|
@ -111,13 +113,97 @@ const toLastView = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 滚动到选中的tag
|
||||||
|
const moveToCurrentTag = async () => {
|
||||||
|
await nextTick()
|
||||||
|
for (const v of unref(visitedViews)) {
|
||||||
|
if (v.fullPath === unref(currentRoute).path) {
|
||||||
|
moveToTarget(v)
|
||||||
|
if (v.fullPath !== unref(currentRoute).fullPath) {
|
||||||
|
tagsViewStore.updateVisitedView(unref(currentRoute))
|
||||||
|
}
|
||||||
|
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
|
||||||
|
|
||||||
|
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
|
||||||
|
const wrap$ = unref(scrollbarRef)?.wrap$
|
||||||
|
let firstTag: Nullable<RouterLinkProps> = null
|
||||||
|
let lastTag: Nullable<RouterLinkProps> = null
|
||||||
|
|
||||||
|
const tagList = unref(tagLinksRefs)
|
||||||
|
// find first tag and last tag
|
||||||
|
if (tagList.length > 0) {
|
||||||
|
firstTag = tagList[0]
|
||||||
|
lastTag = tagList[tagList.length - 1]
|
||||||
|
}
|
||||||
|
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||||
|
// 直接滚动到0的位置
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: wrap$!,
|
||||||
|
position: 'scrollLeft',
|
||||||
|
to: 0,
|
||||||
|
duration: 500
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
|
||||||
|
// 滚动到最后的位置
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: wrap$!,
|
||||||
|
position: 'scrollLeft',
|
||||||
|
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
|
||||||
|
duration: 500
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
} else {
|
||||||
|
// find preTag and nextTag
|
||||||
|
const currentIndex: number = tagList.findIndex(
|
||||||
|
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
|
||||||
|
)
|
||||||
|
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
|
||||||
|
|
||||||
|
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
|
||||||
|
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
|
||||||
|
|
||||||
|
// the tag's offsetLeft after of nextTag
|
||||||
|
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
|
||||||
|
|
||||||
|
// the tag's offsetLeft before of prevTag
|
||||||
|
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
|
||||||
|
|
||||||
|
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: wrap$!,
|
||||||
|
position: 'scrollLeft',
|
||||||
|
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
|
||||||
|
duration: 500
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: wrap$!,
|
||||||
|
position: 'scrollLeft',
|
||||||
|
to: beforePrevTagOffsetLeft,
|
||||||
|
duration: 500
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 是否是当前tag
|
// 是否是当前tag
|
||||||
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
|
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
|
||||||
return route.path === unref(currentRoute).path
|
return route.path === unref(currentRoute).path
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 所有右键菜单组件的元素
|
||||||
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
|
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
|
||||||
|
|
||||||
|
// 右键菜单装填改变的时候
|
||||||
const visibleChange = (
|
const visibleChange = (
|
||||||
visible: boolean,
|
visible: boolean,
|
||||||
ref: ComponentRef<typeof ContextMenu & ContextMenuExpose>
|
ref: ComponentRef<typeof ContextMenu & ContextMenuExpose>
|
||||||
|
@ -133,6 +219,28 @@ const visibleChange = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// elscroll 实例
|
||||||
|
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
|
||||||
|
|
||||||
|
// 保存滚动位置
|
||||||
|
const scrollLeftNumber = ref(0)
|
||||||
|
|
||||||
|
const scroll = ({ scrollLeft }) => {
|
||||||
|
scrollLeftNumber.value = scrollLeft as number
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动到某个位置
|
||||||
|
const move = (to: number) => {
|
||||||
|
const wrap$ = unref(scrollbarRef)?.wrap$
|
||||||
|
const { start } = useScrollTo({
|
||||||
|
el: wrap$!,
|
||||||
|
position: 'scrollLeft',
|
||||||
|
to: unref(scrollLeftNumber) + to,
|
||||||
|
duration: 500
|
||||||
|
})
|
||||||
|
start()
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
initTags()
|
initTags()
|
||||||
addTags()
|
addTags()
|
||||||
|
@ -142,7 +250,7 @@ watch(
|
||||||
() => currentRoute.value,
|
() => currentRoute.value,
|
||||||
() => {
|
() => {
|
||||||
addTags()
|
addTags()
|
||||||
// moveToCurrentTag()
|
moveToCurrentTag()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
</script>
|
</script>
|
||||||
|
@ -152,12 +260,14 @@ watch(
|
||||||
<span
|
<span
|
||||||
:class="`${prefixCls}__tool`"
|
:class="`${prefixCls}__tool`"
|
||||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
||||||
|
@click="move(-200)"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:d-arrow-left" color="#333" />
|
<Icon icon="ep:d-arrow-left" color="#333" />
|
||||||
</span>
|
</span>
|
||||||
<div class="overflow-hidden flex-1">
|
<div class="overflow-hidden flex-1">
|
||||||
<ElScrollbar class="h-full">
|
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
|
||||||
<div class="flex h-full">
|
<div class="flex h-full">
|
||||||
|
<div></div>
|
||||||
<ContextMenu
|
<ContextMenu
|
||||||
:ref="itemRefs.set"
|
:ref="itemRefs.set"
|
||||||
:schema="[
|
:schema="[
|
||||||
|
@ -228,10 +338,10 @@ watch(
|
||||||
]"
|
]"
|
||||||
@visible-change="visibleChange"
|
@visible-change="visibleChange"
|
||||||
>
|
>
|
||||||
<router-link :to="{ ...item }" custom v-slot="{ navigate }">
|
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
|
||||||
<div
|
<div
|
||||||
@click="navigate"
|
@click="navigate"
|
||||||
class="h-full flex justify-center items-center whitespace-nowrap"
|
class="h-full flex justify-center items-center whitespace-nowrap pl-15px"
|
||||||
>
|
>
|
||||||
{{ t(item?.meta?.title as string) }}
|
{{ t(item?.meta?.title as string) }}
|
||||||
<Icon
|
<Icon
|
||||||
|
@ -250,6 +360,7 @@ watch(
|
||||||
<span
|
<span
|
||||||
:class="`${prefixCls}__tool`"
|
:class="`${prefixCls}__tool`"
|
||||||
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
|
||||||
|
@click="move(200)"
|
||||||
>
|
>
|
||||||
<Icon icon="ep:d-arrow-right" color="#333" />
|
<Icon icon="ep:d-arrow-right" color="#333" />
|
||||||
</span>
|
</span>
|
||||||
|
@ -358,9 +469,8 @@ watch(
|
||||||
position: relative;
|
position: relative;
|
||||||
top: 2px;
|
top: 2px;
|
||||||
height: calc(~'100% - 4px');
|
height: calc(~'100% - 4px');
|
||||||
padding: 0 15px;
|
// padding: 0 15px;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
line-height: calc(~'var( - -tags-view-height) - 4px');
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid #d9d9d9;
|
border: 1px solid #d9d9d9;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,62 @@
|
||||||
|
import { ref, unref } from 'vue'
|
||||||
|
|
||||||
|
export interface ScrollToParams {
|
||||||
|
el: HTMLElement
|
||||||
|
to: number
|
||||||
|
position: string
|
||||||
|
duration?: number
|
||||||
|
callback?: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
|
||||||
|
t /= d / 2
|
||||||
|
if (t < 1) {
|
||||||
|
return (c / 2) * t * t + b
|
||||||
|
}
|
||||||
|
t--
|
||||||
|
return (-c / 2) * (t * (t - 2) - 1) + b
|
||||||
|
}
|
||||||
|
const move = (el: HTMLElement, position: string, amount: number) => {
|
||||||
|
el[position] = amount
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useScrollTo({
|
||||||
|
el,
|
||||||
|
position = 'scrollLeft',
|
||||||
|
to,
|
||||||
|
duration = 500,
|
||||||
|
callback
|
||||||
|
}: ScrollToParams) {
|
||||||
|
const isActiveRef = ref(false)
|
||||||
|
const start = el[position]
|
||||||
|
const change = to - start
|
||||||
|
const increment = 20
|
||||||
|
let currentTime = 0
|
||||||
|
|
||||||
|
function animateScroll() {
|
||||||
|
if (!unref(isActiveRef)) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
currentTime += increment
|
||||||
|
const val = easeInOutQuad(currentTime, start, change, duration)
|
||||||
|
move(el, position, val)
|
||||||
|
if (currentTime < duration && unref(isActiveRef)) {
|
||||||
|
requestAnimationFrame(animateScroll)
|
||||||
|
} else {
|
||||||
|
if (callback) {
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function run() {
|
||||||
|
isActiveRef.value = true
|
||||||
|
animateScroll()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stop() {
|
||||||
|
isActiveRef.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return { start: run, stop }
|
||||||
|
}
|
|
@ -53,7 +53,6 @@ export const usePermissionStore = defineStore({
|
||||||
// 直接读取静态路由表
|
// 直接读取静态路由表
|
||||||
routerMap = cloneDeep(asyncRouterMap)
|
routerMap = cloneDeep(asyncRouterMap)
|
||||||
}
|
}
|
||||||
console.log(routerMap)
|
|
||||||
// 动态路由,404一定要放到最后面
|
// 动态路由,404一定要放到最后面
|
||||||
this.addRouters = routerMap.concat([
|
this.addRouters = routerMap.concat([
|
||||||
{
|
{
|
||||||
|
|
|
@ -124,6 +124,14 @@ export const useTagsViewStore = defineStore({
|
||||||
})
|
})
|
||||||
this.addCachedView()
|
this.addCachedView()
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
updateVisitedView(view: RouteLocationNormalizedLoaded) {
|
||||||
|
for (let v of this.visitedViews) {
|
||||||
|
if (v.path === view.path) {
|
||||||
|
v = Object.assign(v, view)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in New Issue