Skip to content

Commit 7d7fd9e

Browse files
feat: Add useScrollTo hook
1 parent 9d926b2 commit 7d7fd9e

File tree

4 files changed

+187
-8
lines changed

4 files changed

+187
-8
lines changed

src/components/TagsView/src/TagsView.vue

Lines changed: 117 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
<script setup lang="ts">
22
import { onMounted, watch, computed, unref, ref, nextTick } from 'vue'
33
import { useRouter } from 'vue-router'
4-
import type { RouteLocationNormalizedLoaded } from 'vue-router'
4+
import type { RouteLocationNormalizedLoaded, RouterLinkProps } from 'vue-router'
55
import { usePermissionStore } from '@/store/modules/permission'
66
import { useTagsViewStore } from '@/store/modules/tagsView'
77
import { useI18n } from '@/hooks/web/useI18n'
88
import { filterAffixTags } from './helper'
99
import { ContextMenu, ContextMenuExpose } from '@/components/ContextMenu'
1010
import { useDesign } from '@/hooks/web/useDesign'
1111
import { useTemplateRefsList } from '@vueuse/core'
12+
import { ElScrollbar } from 'element-plus'
13+
import { useScrollTo } from '@/hooks/event/useScrollTo'
1214
1315
const { getPrefixCls } = useDesign()
1416
@@ -111,13 +113,97 @@ const toLastView = () => {
111113
}
112114
}
113115
116+
// 滚动到选中的tag
117+
const moveToCurrentTag = async () => {
118+
await nextTick()
119+
for (const v of unref(visitedViews)) {
120+
if (v.fullPath === unref(currentRoute).path) {
121+
moveToTarget(v)
122+
if (v.fullPath !== unref(currentRoute).fullPath) {
123+
tagsViewStore.updateVisitedView(unref(currentRoute))
124+
}
125+
126+
break
127+
}
128+
}
129+
}
130+
131+
const tagLinksRefs = useTemplateRefsList<RouterLinkProps>()
132+
133+
const moveToTarget = (currentTag: RouteLocationNormalizedLoaded) => {
134+
const wrap$ = unref(scrollbarRef)?.wrap$
135+
let firstTag: Nullable<RouterLinkProps> = null
136+
let lastTag: Nullable<RouterLinkProps> = null
137+
138+
const tagList = unref(tagLinksRefs)
139+
// find first tag and last tag
140+
if (tagList.length > 0) {
141+
firstTag = tagList[0]
142+
lastTag = tagList[tagList.length - 1]
143+
}
144+
if ((firstTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
145+
// 直接滚动到0的位置
146+
const { start } = useScrollTo({
147+
el: wrap$!,
148+
position: 'scrollLeft',
149+
to: 0,
150+
duration: 500
151+
})
152+
start()
153+
} else if ((lastTag?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath) {
154+
// 滚动到最后的位置
155+
const { start } = useScrollTo({
156+
el: wrap$!,
157+
position: 'scrollLeft',
158+
to: wrap$!.scrollWidth - wrap$!.offsetWidth,
159+
duration: 500
160+
})
161+
start()
162+
} else {
163+
// find preTag and nextTag
164+
const currentIndex: number = tagList.findIndex(
165+
(item) => (item?.to as RouteLocationNormalizedLoaded).fullPath === currentTag.fullPath
166+
)
167+
const tgsRefs = document.getElementsByClassName(`${prefixCls}__item`)
168+
169+
const prevTag = tgsRefs[currentIndex - 1] as HTMLElement
170+
const nextTag = tgsRefs[currentIndex + 1] as HTMLElement
171+
172+
// the tag's offsetLeft after of nextTag
173+
const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + 4
174+
175+
// the tag's offsetLeft before of prevTag
176+
const beforePrevTagOffsetLeft = prevTag.offsetLeft - 4
177+
178+
if (afterNextTagOffsetLeft > unref(scrollLeftNumber) + wrap$!.offsetWidth) {
179+
const { start } = useScrollTo({
180+
el: wrap$!,
181+
position: 'scrollLeft',
182+
to: afterNextTagOffsetLeft - wrap$!.offsetWidth,
183+
duration: 500
184+
})
185+
start()
186+
} else if (beforePrevTagOffsetLeft < unref(scrollLeftNumber)) {
187+
const { start } = useScrollTo({
188+
el: wrap$!,
189+
position: 'scrollLeft',
190+
to: beforePrevTagOffsetLeft,
191+
duration: 500
192+
})
193+
start()
194+
}
195+
}
196+
}
197+
114198
// 是否是当前tag
115199
const isActive = (route: RouteLocationNormalizedLoaded): boolean => {
116200
return route.path === unref(currentRoute).path
117201
}
118202
203+
// 所有右键菜单组件的元素
119204
const itemRefs = useTemplateRefsList<ComponentRef<typeof ContextMenu & ContextMenuExpose>>()
120205
206+
// 右键菜单装填改变的时候
121207
const visibleChange = (
122208
visible: boolean,
123209
ref: ComponentRef<typeof ContextMenu & ContextMenuExpose>
@@ -133,6 +219,28 @@ const visibleChange = (
133219
}
134220
}
135221
222+
// elscroll 实例
223+
const scrollbarRef = ref<ComponentRef<typeof ElScrollbar>>()
224+
225+
// 保存滚动位置
226+
const scrollLeftNumber = ref(0)
227+
228+
const scroll = ({ scrollLeft }) => {
229+
scrollLeftNumber.value = scrollLeft as number
230+
}
231+
232+
// 移动到某个位置
233+
const move = (to: number) => {
234+
const wrap$ = unref(scrollbarRef)?.wrap$
235+
const { start } = useScrollTo({
236+
el: wrap$!,
237+
position: 'scrollLeft',
238+
to: unref(scrollLeftNumber) + to,
239+
duration: 500
240+
})
241+
start()
242+
}
243+
136244
onMounted(() => {
137245
initTags()
138246
addTags()
@@ -142,7 +250,7 @@ watch(
142250
() => currentRoute.value,
143251
() => {
144252
addTags()
145-
// moveToCurrentTag()
253+
moveToCurrentTag()
146254
}
147255
)
148256
</script>
@@ -152,12 +260,14 @@ watch(
152260
<span
153261
:class="`${prefixCls}__tool`"
154262
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
263+
@click="move(-200)"
155264
>
156265
<Icon icon="ep:d-arrow-left" color="#333" />
157266
</span>
158267
<div class="overflow-hidden flex-1">
159-
<ElScrollbar class="h-full">
268+
<ElScrollbar ref="scrollbarRef" class="h-full" @scroll="scroll">
160269
<div class="flex h-full">
270+
<div></div>
161271
<ContextMenu
162272
:ref="itemRefs.set"
163273
:schema="[
@@ -228,10 +338,10 @@ watch(
228338
]"
229339
@visible-change="visibleChange"
230340
>
231-
<router-link :to="{ ...item }" custom v-slot="{ navigate }">
341+
<router-link :ref="tagLinksRefs.set" :to="{ ...item }" custom v-slot="{ navigate }">
232342
<div
233343
@click="navigate"
234-
class="h-full flex justify-center items-center whitespace-nowrap"
344+
class="h-full flex justify-center items-center whitespace-nowrap pl-15px"
235345
>
236346
{{ t(item?.meta?.title as string) }}
237347
<Icon
@@ -250,6 +360,7 @@ watch(
250360
<span
251361
:class="`${prefixCls}__tool`"
252362
class="w-[var(--tags-view-height)] h-[var(--tags-view-height)] text-center leading-[var(--tags-view-height)] cursor-pointer"
363+
@click="move(200)"
253364
>
254365
<Icon icon="ep:d-arrow-right" color="#333" />
255366
</span>
@@ -358,9 +469,8 @@ watch(
358469
position: relative;
359470
top: 2px;
360471
height: calc(~'100% - 4px');
361-
padding: 0 15px;
472+
// padding: 0 15px;
362473
font-size: 12px;
363-
line-height: calc(~'var( - -tags-view-height) - 4px');
364474
cursor: pointer;
365475
border: 1px solid #d9d9d9;
366476

src/hooks/event/useScrollTo.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { ref, unref } from 'vue'
2+
3+
export interface ScrollToParams {
4+
el: HTMLElement
5+
to: number
6+
position: string
7+
duration?: number
8+
callback?: () => void
9+
}
10+
11+
const easeInOutQuad = (t: number, b: number, c: number, d: number) => {
12+
t /= d / 2
13+
if (t < 1) {
14+
return (c / 2) * t * t + b
15+
}
16+
t--
17+
return (-c / 2) * (t * (t - 2) - 1) + b
18+
}
19+
const move = (el: HTMLElement, position: string, amount: number) => {
20+
el[position] = amount
21+
}
22+
23+
export function useScrollTo({
24+
el,
25+
position = 'scrollLeft',
26+
to,
27+
duration = 500,
28+
callback
29+
}: ScrollToParams) {
30+
const isActiveRef = ref(false)
31+
const start = el[position]
32+
const change = to - start
33+
const increment = 20
34+
let currentTime = 0
35+
36+
function animateScroll() {
37+
if (!unref(isActiveRef)) {
38+
return
39+
}
40+
currentTime += increment
41+
const val = easeInOutQuad(currentTime, start, change, duration)
42+
move(el, position, val)
43+
if (currentTime < duration && unref(isActiveRef)) {
44+
requestAnimationFrame(animateScroll)
45+
} else {
46+
if (callback) {
47+
callback()
48+
}
49+
}
50+
}
51+
52+
function run() {
53+
isActiveRef.value = true
54+
animateScroll()
55+
}
56+
57+
function stop() {
58+
isActiveRef.value = false
59+
}
60+
61+
return { start: run, stop }
62+
}

src/store/modules/permission.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export const usePermissionStore = defineStore({
5353
// 直接读取静态路由表
5454
routerMap = cloneDeep(asyncRouterMap)
5555
}
56-
console.log(routerMap)
5756
// 动态路由,404一定要放到最后面
5857
this.addRouters = routerMap.concat([
5958
{

src/store/modules/tagsView.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,14 @@ export const useTagsViewStore = defineStore({
124124
})
125125
this.addCachedView()
126126
}
127+
},
128+
updateVisitedView(view: RouteLocationNormalizedLoaded) {
129+
for (let v of this.visitedViews) {
130+
if (v.path === view.path) {
131+
v = Object.assign(v, view)
132+
break
133+
}
134+
}
127135
}
128136
}
129137
})

0 commit comments

Comments
 (0)