
今天看到了一个博主的鼠标跟随背景丝滑移动,但是他是用纯css 写的。 组建的可复用不高,而且是放在整个 app 的style 标签外面。感觉组建多次使用会造成污染。
下面是我根据二次改进的
这是HTML片段
js
<div class="container" ref="containerRef" @mouseover="onContainerMouseOver">
<!-- 使用事件委托 -->
<div class="item" v-for="index in itemCount" :key="index" :data-index="index">
<slot name="content" :index="index"></slot>
</div>
</div>
scss
<style lang="scss" scoped>
$surface-2: #767676;
$item-height: 151px;
$border-radius: 0.4rem;
$transition-timing: cubic-bezier(0.2, 1, 0.2, 1);
$transition-duration: 0.5s;
.container {
position: relative;
}
.item {
--height: #{$item-height};
--surface-2: #{$surface-2};
cursor: pointer;
padding: 30px 16px;
border-bottom: 1px #ddd solid;
box-sizing: border-box;
// 使用 transform 替代 top,性能更好
&:last-child {
--y: 0;
--h: 0;
&::before {
content: "";
display: block;
position: absolute;
background: var(--surface-2);
opacity: 0;
width: 100%;
top: 0;
left: 0;
height: var(--h);
border-radius: $border-radius;
pointer-events: none;
transition: all $transition-duration $transition-timing;
transform: translateY(var(--y)); // 使用 transform 优化性能
will-change: transform; // 提示浏览器优化
}
}
// 减少选择器复杂度
&:hover ~ .item:last-child::before,
&:last-child:hover::before {
opacity: 0.06;
}
}
// 减少重绘范围
@media (prefers-reduced-motion: reduce) {
.item:last-child::before {
transition: opacity 0.1s ease;
}
}
</style>
js 开始
移动背景 的是绑定在最后一个列表的 item 的 before 上 首先我们坐的是插槽,先把 移动背景设置好高度-初始化设置高度,和防抖节约性能
js
// 缓存计算值
let itemHeight = 0
let itemPositions = []
// 防抖重计算
let resizeObserver
const recalculatePositions = () => {
if (!containerRef.value) return
const items = containerRef.value.querySelectorAll(".item")
itemHeight = items[0]?.offsetHeight || 0
lastItemRef.value?.style.setProperty("--h", `${itemHeight}px`)
// 预计算所有位置-到时候鼠标在每个item 移动会用到
itemPositions = Array.from(items).map(item => item.offsetTop)
}
onMounted(() => {
lastItemRef.value = containerRef.value?.querySelector(".item:last-child")
recalculatePositions()
// 监听容器尺寸变化
resizeObserver = new ResizeObserver(() => {
recalculatePositions()
})
if (containerRef.value) {
resizeObserver.observe(containerRef.value)
}
})
接下来就处理每次进入如何,把背景移动到当前地方了
js
// 使用事件委托,减少事件监听器数量
const onContainerMouseOver = e => {
const target = e.target.closest(".item")
if (!target || !lastItemRef.value) return
const index = parseInt(target.dataset.index) - 1
if (index >= 0 && index < itemPositions.length) {
lastItemRef.value.style.setProperty("--y", `${itemPositions[index]}px`)
}
}
全部代码
js
<template>
<div class="container" ref="containerRef" @mouseover="onContainerMouseOver">
<!-- 使用事件委托 -->
<div class="item" v-for="index in itemCount" :key="index" :data-index="index">
<slot name="content" :index="index"></slot>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from "vue"
const itemCount = 15
const containerRef = ref(null)
const lastItemRef = ref(null) // 单独引用最后一个item
// 缓存计算值
let itemHeight = 0
let itemPositions = []
// 使用事件委托,减少事件监听器数量
const onContainerMouseOver = e => {
const target = e.target.closest(".item")
if (!target || !lastItemRef.value) return
const index = parseInt(target.dataset.index) - 1
if (index >= 0 && index < itemPositions.length) {
lastItemRef.value.style.setProperty("--y", `${itemPositions[index]}px`)
}
}
// 防抖重计算
let resizeObserver
const recalculatePositions = () => {
if (!containerRef.value) return
const items = containerRef.value.querySelectorAll(".item")
itemHeight = items[0]?.offsetHeight || 0
lastItemRef.value?.style.setProperty("--h", `${itemHeight}px`)
// 预计算所有位置
itemPositions = Array.from(items).map(item => item.offsetTop)
}
onMounted(() => {
lastItemRef.value = containerRef.value?.querySelector(".item:last-child")
recalculatePositions()
// 监听容器尺寸变化
resizeObserver = new ResizeObserver(() => {
recalculatePositions()
})
if (containerRef.value) {
resizeObserver.observe(containerRef.value)
}
})
onUnmounted(() => {
resizeObserver?.disconnect()
})
</script>
<style lang="scss" scoped>
$surface-2: #767676;
$item-height: 151px;
$border-radius: 0.4rem;
$transition-timing: cubic-bezier(0.2, 1, 0.2, 1);
$transition-duration: 0.5s;
.container {
position: relative;
}
.item {
--height: #{$item-height};
--surface-2: #{$surface-2};
cursor: pointer;
padding: 30px 16px;
border-bottom: 1px #ddd solid;
box-sizing: border-box;
// 使用 transform 替代 top,性能更好
&:last-child {
--y: 0;
--h: 0;
&::before {
content: "";
display: block;
position: absolute;
background: var(--surface-2);
opacity: 0;
width: 100%;
top: 0;
left: 0;
height: var(--h);
border-radius: $border-radius;
pointer-events: none;
transition: all $transition-duration $transition-timing;
transform: translateY(var(--y)); // 使用 transform 优化性能
will-change: transform; // 提示浏览器优化
}
}
// 减少选择器复杂度
&:hover ~ .item:last-child::before,
&:last-child:hover::before {
opacity: 0.06;
}
}
// 减少重绘范围
@media (prefers-reduced-motion: reduce) {
.item:last-child::before {
transition: opacity 0.1s ease;
}
}
</style>
js
<SmoothMovement>
<template #content="{ index }">
<div class="item_contnent">
{{ index }}
</div>
</template>
</SmoothMovement>