vue 组件实现 、background-hover随鼠标丝滑移动~

今天看到了一个博主的鼠标跟随背景丝滑移动,但是他是用纯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>
相关推荐
焦糖小布丁2 小时前
加http和https访问的网站不同?
前端
人工智能的苟富贵2 小时前
用 Rust 写一个前端项目辅助工具:JSON 格式化器
前端·rust·json
季春二九3 小时前
Edge 卸载工具 | 版本号1.0 | 专为彻底卸载Microsoft Edge设计
前端·microsoft·edge·edge 卸载工具
雨过天晴而后无语3 小时前
HTML中JS监听输入框值的即时变化
前端·javascript·html
座山雕~3 小时前
html 和css基础常用的标签和样式(2)-css
前端·css·html
一勺菠萝丶3 小时前
为什么 HTTP 能访问,但 HTTPS 却打不开?——Nginx SSL 端口配置详解
前端
4Forsee3 小时前
【Android】消息机制
android·java·前端
不爱说话郭德纲3 小时前
UniappX不会运行到鸿蒙?超超超保姆级鸿蒙开发生成证书以及配置证书步骤
前端·uni-app·harmonyos
Olafur_zbj3 小时前
【IC】NoC设计入门 -- 网络接口NI Slave
前端·javascript·php