diff --git a/src/views/common/DocumentSearchWord.vue b/src/views/common/DocumentSearchWord.vue index 5c11a83..c7dfbec 100644 --- a/src/views/common/DocumentSearchWord.vue +++ b/src/views/common/DocumentSearchWord.vue @@ -65,6 +65,7 @@ export default { searchSegments: [], docRendered: false, abortController: null, + scrollAnimationFrame: null, } }, mounted() { @@ -75,6 +76,7 @@ export default { }, beforeDestroy() { this.cancelFetch() + this.cancelScrollAnimation() }, watch: { '$route.query.url'(newUrl, oldUrl) { @@ -403,7 +405,7 @@ export default { this.currentResultIndex = 0 this.updateActiveHighlight() this.$nextTick(() => { - this.navigateToResult(this.currentResultIndex) + this.navigateToResult(this.currentResultIndex, false) }) }, @@ -463,7 +465,53 @@ export default { return offset }, - navigateToResult(index, useSmoothScroll = false) { + cancelScrollAnimation() { + if (this.scrollAnimationFrame) { + cancelAnimationFrame(this.scrollAnimationFrame) + this.scrollAnimationFrame = null + } + }, + + smoothScrollTo(container, target, baseDuration = 300) { + if (!container) return + const maxScroll = container.scrollHeight - container.clientHeight + const finalTarget = Math.min(Math.max(target, 0), Math.max(maxScroll, 0)) + const start = container.scrollTop + const change = finalTarget - start + if (Math.abs(change) < 1) { + container.scrollTop = finalTarget + return + } + + const distanceFactor = Math.min(Math.abs(change) / Math.max(container.clientHeight, 1), 2.4) + const duration = Math.min(600, baseDuration + distanceFactor * 120) + + const startTime = performance.now() + const ease = (t) => 1 - Math.pow(1 - t, 3) + const velocityBoost = Math.min(Math.max(Math.abs(change) / 2400, 0), 0.25) + + this.cancelScrollAnimation() + + const step = (now) => { + const elapsed = now - startTime + const progress = Math.min(elapsed / duration, 1) + const eased = ease(progress) + const basePosition = start + change * eased + const overshoot = velocityBoost * Math.sin(eased * Math.PI) + container.scrollTop = Math.min(Math.max(basePosition + overshoot * change, 0), maxScroll) + + if (progress < 1) { + this.scrollAnimationFrame = requestAnimationFrame(step) + } else { + container.scrollTop = finalTarget + this.scrollAnimationFrame = null + } + } + + this.scrollAnimationFrame = requestAnimationFrame(step) + }, + + navigateToResult(index, useSmoothScroll = true) { if (!this.searchResults.length || index < 0 || index >= this.searchResults.length) return this.currentResultIndex = index this.updateActiveHighlight() @@ -485,28 +533,44 @@ export default { return } - try { - const wrapperRect = wrapper.getBoundingClientRect() - const currentScrollTop = wrapper.scrollTop - const targetTopRelativeToWrapper = targetRect.top - wrapperRect.top + currentScrollTop - const margin = 24 - const desiredScrollTop = Math.max(targetTopRelativeToWrapper - margin, 0) - - wrapper.scrollTop = desiredScrollTop - } catch (error) { + const performScroll = () => { try { - let absoluteTop = 0 - let node = target - while (node && node !== wrapper) { - absoluteTop += node.offsetTop || 0 - node = node.offsetParent - } + const wrapperRect = wrapper.getBoundingClientRect() + const currentScrollTop = wrapper.scrollTop + const targetTopRelativeToWrapper = targetRect.top - wrapperRect.top + currentScrollTop const margin = 24 - wrapper.scrollTop = Math.max(absoluteTop - margin, 0) - } catch (e) { - console.error('Scroll error:', e) + const desiredScrollTop = Math.max(targetTopRelativeToWrapper - margin, 0) + + if (useSmoothScroll) { + this.smoothScrollTo(wrapper, desiredScrollTop) + } else { + this.cancelScrollAnimation() + wrapper.scrollTop = desiredScrollTop + } + } catch (error) { + try { + let absoluteTop = 0 + let node = target + while (node && node !== wrapper) { + absoluteTop += node.offsetTop || 0 + node = node.offsetParent + } + const margin = 24 + const desiredScrollTop = Math.max(absoluteTop - margin, 0) + + if (useSmoothScroll) { + this.smoothScrollTo(wrapper, desiredScrollTop) + } else { + this.cancelScrollAnimation() + wrapper.scrollTop = desiredScrollTop + } + } catch (e) { + console.error('Scroll error:', e) + } } } + + performScroll() }) }) })