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>
相关推荐
lovingsoft3 分钟前
Chrome Lighthouse 详解
前端·chrome
Qlly13 分钟前
全栈工程师怎么接需求?从一句「加个功能」聊到能开发、能验收
前端·后端
马可家的菠萝17 分钟前
你的微信“文件传输助手”,是不是已经成了第二个网盘?
前端·后端·架构
略略略啦18 分钟前
Vibe Coding 基于Spec DD和TDD的实践指南
前端
shmily麻瓜小菜鸡39 分钟前
前端实现图片与PDF文字识别敏感信息打码方案
前端·pdf·状态模式
打工仔折腾 AI1 小时前
LangChain 工具调用实战:从 Tool 定义到 Agent 执行链路拆解
java·前端·人工智能·后端·python·langchain·ai agent 实战
汉堡大王95271 小时前
单次输出拉到100万Token:拆完 Gemini 4 Argon 的续写协议,才看懂谷歌这7个月在憋什么
前端·人工智能·后端
看浪的路人1 小时前
第9讲:大模型应用监控与可观测性——OpenTelemetry + Prometheus + Grafana 全链路追踪
java·前端·spring
内存过载1 小时前
用表格模拟甘特图:项目排期的土办法
前端·低代码
葡萄城技术团队1 小时前
用 SpreadJS 数据验证实现省市区三级联动
前端