实现一个鼠标滚轮横向滚动需求

在前端开发中,我们经常会遇到需要实现横向滚动的场景,如图片画廊、时间轴、横向导航等。然而,浏览器默认的滚轮事件只支持垂直方向滚动,如何优雅地实现鼠标滚轮控制横向滚动成为了一个常见的需求。本文将介绍如何使用 Vue 3 的组合式 API 创建一个自定义 Hook 来解决这个问题。

问题分析

在实现横向滚动时,我们面临两个主要挑战:

  1. 如何区分需要横向滚动和垂直滚动的场景
  2. 如何兼容不同的 UI 组件库(如 Element Plus)和原生 DOM 元素

实现思路

我们的解决方案需要具备以下特性:

  • 能够智能判断何时应该进行横向滚动
  • 兼容 Element Plus 的 Scrollbar 组件和原生 DOM 元素
  • 提供平滑的滚动体验
  • 在无法横向滚动时回退到默认垂直滚动行为

代码实现

javascript 复制代码
import { computed, nextTick } from 'vue';

/**
 * 判断是否为数组
 * @param {*} arr - 需要判断的值
 * @returns {boolean} 是否为数组
 */
export const isArray = (arr) => {
  return Array.isArray(arr);
};

/**
 * 鼠标滚轮横向滚动处理 Hook
 * @param {string} [refName] - 可选的 ref 名称,用于 Element Plus Scrollbar 组件
 * @returns {Object} 包含 handleWheel 方法的对象
 */
export function useWheel(refName) {
  // 获取模板引用
  const scrollbarRef = refName ? useTemplateRef(refName) : ref(null);
  
  // 计算目标引用,处理数组情况
  const targetRef = computed(() => {
    return isArray(scrollbarRef.value) ? scrollbarRef.value[0] : scrollbarRef.value;
  });

  /**
   * 处理鼠标滚轮事件
   * @param {Event} event - 滚轮事件对象
   */
  const handleWheel = async (event) => {
    await nextTick();
    
    // 确定滚动容器
    let scrollContainer;
    if (targetRef.value) {
      // Element Plus Scrollbar 组件
      scrollContainer = targetRef.value.$el?.querySelector(".el-scrollbar__wrap");
    } else {
      // 原生 DOM 元素
      scrollContainer = event.currentTarget;
    }
    
    if (!scrollContainer) return;

    // 获取滚动容器信息
    const { scrollLeft, scrollWidth, clientWidth } = scrollContainer;
    const maxScrollLeft = scrollWidth - clientWidth;
    const delta = event.deltaY;

    // 判断是否需要横向滚动
    const canScrollRight = delta > 0 && scrollLeft < maxScrollLeft;
    const canScrollLeft = delta < 0 && scrollLeft > 0;

    // 如果可以横向滚动,则执行横向滚动并阻止默认行为
    if (maxScrollLeft > 0 && (canScrollRight || canScrollLeft)) {
      scrollContainer.scrollTo({
        left: scrollLeft + delta * 2, // 乘以2增加滚动速度
        behavior: "smooth" // 平滑滚动效果
      });
      event.preventDefault();
    }
    // 无法横向滚动时允许默认垂直滚动
  };

  return {
    handleWheel,
  };
}

使用示例

在 Element Plus Scrollbar 组件中使用

vue 复制代码
<template>
  <el-scrollbar ref="scrollbar" @wheel="handleWheel">
    <!-- 横向内容 -->
    <div class="horizontal-content">
      <div v-for="item in 20" :key="item" class="card">
        Card {{ item }}
      </div>
    </div>
  </el-scrollbar>
</template>

<script setup>
import { useWheel } from "@/hooks/useWheel";

const { handleWheel } = useWheel("scrollbar");
</script>

<style scoped>
.horizontal-content {
  display: flex;
  width: max-content;
}

.card {
  width: 200px;
  height: 150px;
  margin: 10px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}
</style>

在原生 DOM 元素中使用

vue 复制代码
<template>
  <div class="custom-scroll-container" @wheel="handleWheel">
    <!-- 横向内容 -->
    <div class="horizontal-content">
      <div v-for="item in 20" :key="item" class="card">
        Card {{ item }
      </div>
    </div>
  </div>
</template>

<script setup>
import { useWheel } from "@/hooks/useWheel";

const { handleWheel } = useWheel();
</script>

<style scoped>
.custom-scroll-container {
  width: 100%;
  overflow-x: auto;
  overflow-y: hidden;
}

.horizontal-content {
  display: flex;
  width: max-content;
}

.card {
  width: 200px;
  height: 150px;
  margin: 10px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
}
</style>

实现原理详解

1. 引用处理

Hook 通过 useTemplateRef 获取对目标元素的引用,并处理了引用可能是数组的情况(Vue 3 中模板引用有时会是数组)。

2. 容器确定

根据是否提供了 ref 名称,Hook 能够智能确定要操作的滚动容器:

  • 对于 Element Plus Scrollbar 组件,需要通过 $el.querySelector(".el-scrollbar__wrap") 找到实际的滚动容器
  • 对于原生 DOM 元素,直接使用事件的目标元素

3. 滚动逻辑

通过比较容器的 scrollWidthclientWidth,判断是否存在横向滚动空间。然后根据滚轮方向和当前滚动位置,决定是否应该进行横向滚动。

4. 平滑体验

使用 scrollTo 方法而非直接修改 scrollLeft,配合 behavior: "smooth" 选项,提供平滑的滚动体验。

相关推荐
SUPER526637 分钟前
FastApi项目启动失败 got an unexpected keyword argument ‘loop_factory‘
java·服务器·前端
sanx181 小时前
专业电竞体育数据与系统解决方案
前端·数据库·apache·数据库开发·时序数据库
你的人类朋友3 小时前
【Node】认识一下Node.js 中的 VM 模块
前端·后端·node.js
Cosolar3 小时前
FunASR 前端语音识别代码解析
前端·面试·github
@大迁世界6 小时前
Vue 设计模式 实战指南
前端·javascript·vue.js·设计模式·ecmascript
芭拉拉小魔仙6 小时前
Vue项目中如何实现表格选中数据的 Excel 导出
前端·vue.js·excel
jump_jump6 小时前
妙用 localeCompare 获取汉字拼音首字母
前端·javascript·浏览器
U.2 SSD6 小时前
Echarts单轴坐标系散点图
前端·javascript·echarts
德育处主任Pro7 小时前
前端玩转大模型,DeepSeek-R1 蒸馏 Llama 模型的 Bedrock 部署
前端·llama
Jedi Hongbin7 小时前
Three.js NodeMaterial 节点材质系统文档
前端·javascript·three.js·nodematerial