Vue3滚动条(Scrollbar)

news2024/12/23 23:56:44

效果如下图:在线预览

在这里插入图片描述
在这里插入图片描述

APIs

Scrollbar

参数说明类型默认值必传
contentStyle内容样式CSSProperties{}false
size滚动条的大小,单位 pxnumber5false
trigger显示滚动条的时机,'none' 表示一直显示‘hover’ | ‘none’‘hover’false
horizontal是否使用横向滚动booleanfalsefalse

Methods

名称说明类型
scrollTo滚动内容(options: { left?: number, top?: number, behavior?: ScrollBehavior }): void & (x: number, y: number) => void
scrollBy滚动特定距离(options: { left?: number, top?: number, behavior?: ScrollBehavior }): void & (x: number, y: number) => void

ScrollBehavior Type

名称说明
smooth平滑滚动并产生过渡效果
instant滚动会直接跳转到目标位置,没有过渡效果
auto或缺省值表示浏览器会自动选择滚动时的过渡效果

Events

名称说明类型
scroll滚动的回调(e: Event) => void

创建滚动条组件Scrollbar.vue

<script lang="ts">
/*
  一个根节点时,禁用组件根节点自动继承 attribute,必须使用这种写法!然后在要继承 attribute 的节点上绑定 v-bind="$attrs" 即可
  多个根节点时,只需在要继承 attribute 的节点上绑定 v-bind="$attrs" 即可
*/
export default {
  inheritAttrs: false
}
</script>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { CSSProperties } from 'vue'
import { useEventListener, useMutationObserver } from '../utils'
interface Props {
  contentStyle?: CSSProperties // 内容样式
  size?: number // 滚动条的大小,单位 px
  trigger?: 'hover' | 'none' // 显示滚动条的时机,'none' 表示一直显示
  horizontal?: boolean // 是否使用横向滚动
}
const props = withDefaults(defineProps<Props>(), {
  contentStyle: () => ({}),
  size: 5,
  trigger: 'hover',
  horizontal: false
})
const containerRef = ref()
const contentRef = ref()
const railVerticalRef = ref()
const railHorizontalRef = ref()
const showTrack = ref(false)
const containerScrollHeight = ref(0) // 滚动区域高度,包括溢出高度
const containerScrollWidth = ref(0) // 滚动区域宽度,包括溢出宽度
const containerClientHeight = ref(0) // 滚动区域高度,不包括溢出高度
const containerClientWidth = ref(0) // 滚动区域宽度,不包括溢出宽度
const containerHeight = ref(0) // 容器高度
const containerWidth = ref(0) // 容器宽度
const contentHeight = ref(0) // 内容高度
const contentWidth = ref(0) // 内容宽度
const railHeight = ref(0) // 滚动条高度
const railWidth = ref(0) // 滚动条宽度
const containerScrollTop = ref(0) // 垂直滚动距离
const containerScrollLeft = ref(0) // 水平滚动距离
const trackYPressed = ref(false) // 垂直滚动条是否被按下
const trackXPressed = ref(false) // 水平滚动条是否被按下
const mouseLeave = ref(false) // 鼠标在按下滚动条并拖动时是否离开滚动区域
const memoYTop = ref<number>(0) // 鼠标选中并按下垂直滚动条时已滚动的垂直距离
const memoXLeft = ref<number>(0) // 鼠标选中并按下水平滚动条时已滚动的水平距离
const memoMouseY = ref<number>(0) // 鼠标选中并按下垂直滚动条时的鼠标 Y 坐标
const memoMouseX = ref<number>(0) // 鼠标选中并按下水平滚动条时的鼠标 X 坐标
const horizontalContentStyle = { width: 'fit-content' } // 水平滚动时内容区域默认样式
const emit = defineEmits(['scroll'])
const isYScroll = computed(() => {
  // 是否存在垂直滚动
  return containerScrollHeight.value > containerClientHeight.value
})
const isXScroll = computed(() => {
  // 是否存在水平滚动
  return containerScrollWidth.value > containerClientWidth.value
})
const isScroll = computed(() => {
  // 是否存在滚动,水平或垂直
  return isYScroll.value || (props.horizontal && isXScroll.value)
})
const trackHeight = computed(() => {
  // 垂直滚动条高度
  if (isYScroll.value) {
    if (containerHeight.value && contentHeight.value && railHeight.value) {
      const value = Math.min(
        containerHeight.value,
        (railHeight.value * containerHeight.value) / contentHeight.value + 1.5 * props.size
      )
      return Number(value.toFixed(4))
    }
  }
  return 0
})
const trackTop = computed(() => {
  // 滚动条垂直偏移
  if (containerHeight.value && contentHeight.value && railHeight.value) {
    return (
      (containerScrollTop.value / (contentHeight.value - containerHeight.value)) *
      (railHeight.value - trackHeight.value)
    )
  }
  return 0
})
const trackWidth = computed(() => {
  // 横向滚动条宽度
  if (props.horizontal && isXScroll.value) {
    if (containerWidth.value && contentWidth.value && railWidth.value) {
      const value = (railWidth.value * containerWidth.value) / contentWidth.value + 1.5 * props.size
      return Number(value.toFixed(4))
    }
  }
  return 0
})
const trackLeft = computed(() => {
  // 滚动条水平偏移
  if (containerWidth.value && contentWidth.value && railWidth.value) {
    return (
      (containerScrollLeft.value / (contentWidth.value - containerWidth.value)) * (railWidth.value - trackWidth.value)
    )
  }
  return 0
})
onMounted(() => {
  updateState()
})
function updateScrollState() {
  containerScrollTop.value = containerRef.value.scrollTop
  containerScrollLeft.value = containerRef.value.scrollLeft
}
function updateScrollbarState() {
  containerScrollHeight.value = containerRef.value.scrollHeight
  containerScrollWidth.value = containerRef.value.scrollWidth
  containerClientHeight.value = containerRef.value.clientHeight
  containerClientWidth.value = containerRef.value.clientWidth

  containerHeight.value = containerRef.value.offsetHeight
  containerWidth.value = containerRef.value.offsetWidth
  contentHeight.value = contentRef.value.offsetHeight
  contentWidth.value = contentRef.value.offsetWidth
  railHeight.value = railVerticalRef.value.offsetHeight
  railWidth.value = railHorizontalRef.value.offsetWidth
}
function updateState() {
  updateScrollState()
  updateScrollbarState()
}
useEventListener(window, 'resize', updateState)
const options = { childList: true, attributes: true, subtree: true }
useMutationObserver(containerRef, updateState, options)
function onScroll(e: Event) {
  emit('scroll', e)
  updateScrollState()
}
function onMouseEnter() {
  if (props.horizontal) {
    if (trackXPressed.value) {
      mouseLeave.value = false
    } else {
      showTrack.value = true
    }
  } else {
    if (trackYPressed.value) {
      mouseLeave.value = false
    } else {
      showTrack.value = true
    }
  }
}
function onMouseLeave() {
  if (props.horizontal) {
    if (trackXPressed.value) {
      mouseLeave.value = true
    } else {
      showTrack.value = false
    }
  } else {
    if (trackYPressed.value) {
      mouseLeave.value = true
    } else {
      showTrack.value = false
    }
  }
}
function onTrackVerticalMouseDown(e: MouseEvent) {
  trackYPressed.value = true
  memoYTop.value = containerScrollTop.value
  memoMouseY.value = e.clientY
  window.onmousemove = (e: MouseEvent) => {
    const diffY = e.clientY - memoMouseY.value
    const dScrollTop =
      (diffY * (contentHeight.value - containerHeight.value)) / (containerHeight.value - trackHeight.value)
    const toScrollTopUpperBound = contentHeight.value - containerHeight.value
    let toScrollTop = memoYTop.value + dScrollTop
    toScrollTop = Math.min(toScrollTopUpperBound, toScrollTop)
    toScrollTop = Math.max(toScrollTop, 0)
    containerRef.value.scrollTop = toScrollTop
  }
  window.onmouseup = () => {
    window.onmousemove = null
    trackYPressed.value = false
    if (props.trigger === 'hover' && mouseLeave.value) {
      showTrack.value = false
      mouseLeave.value = false
    }
  }
}
function onTrackHorizontalMouseDown(e: MouseEvent) {
  trackXPressed.value = true
  memoXLeft.value = containerScrollLeft.value
  memoMouseX.value = e.clientX
  window.onmousemove = (e: MouseEvent) => {
    const diffX = e.clientX - memoMouseX.value
    const dScrollLeft =
      (diffX * (contentWidth.value - containerWidth.value)) / (containerWidth.value - trackWidth.value)
    const toScrollLeftUpperBound = contentWidth.value - containerWidth.value
    let toScrollLeft = memoXLeft.value + dScrollLeft
    toScrollLeft = Math.min(toScrollLeftUpperBound, toScrollLeft)
    toScrollLeft = Math.max(toScrollLeft, 0)
    containerRef.value.scrollLeft = toScrollLeft
  }
  window.onmouseup = () => {
    window.onmousemove = null
    trackXPressed.value = false
    if (props.trigger === 'hover' && mouseLeave.value) {
      showTrack.value = false
      mouseLeave.value = false
    }
  }
}
function scrollTo(...args: any[]) {
  containerRef.value?.scrollTo(...args)
}
function scrollBy(...args: any[]) {
  containerRef.value?.scrollBy(...args)
}

defineExpose({
  scrollTo,
  scrollBy
})
</script>
<template>
  <div
    class="m-scrollbar"
    :style="`--scrollbar-size: ${size}px;`"
    @mouseenter="isScroll && trigger === 'hover' ? onMouseEnter() : () => false"
    @mouseleave="isScroll && trigger === 'hover' ? onMouseLeave() : () => false"
  >
    <div ref="containerRef" class="m-scrollbar-container" @scroll="onScroll" v-bind="$attrs">
      <div
        ref="contentRef"
        class="m-scrollbar-content"
        :style="[horizontal ? { ...horizontalContentStyle, ...contentStyle } : contentStyle]"
      >
        <slot></slot>
      </div>
    </div>
    <div ref="railVerticalRef" class="m-scrollbar-rail rail-vertical">
      <Transition name="fade">
        <div
          v-if="trigger === 'none' || showTrack"
          class="m-scrollbar-track"
          :style="`top: ${trackTop}px; height: ${trackHeight}px;`"
          @mousedown.prevent.stop="onTrackVerticalMouseDown"
        ></div>
      </Transition>
    </div>
    <div ref="railHorizontalRef" v-show="horizontal" class="m-scrollbar-rail rail-horizontal">
      <Transition name="fade">
        <div
          v-if="trigger === 'none' || showTrack"
          class="m-scrollbar-track"
          :style="`left: ${trackLeft}px; width: ${trackWidth}px;`"
          @mousedown.prevent.stop="onTrackHorizontalMouseDown"
        ></div>
      </Transition>
    </div>
  </div>
</template>
<style lang="less" scoped>
.fade-enter-active,
.fade-leave-active {
  transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1) !important;
}
.fade-enter-from,
.fade-leave-to {
  opacity: 0;
}
.m-scrollbar {
  overflow: hidden;
  position: relative;
  z-index: auto;
  height: 100%;
  width: 100%;
  .m-scrollbar-container {
    width: 100%;
    overflow: scroll;
    height: 100%;
    min-height: inherit;
    max-height: inherit;
    scrollbar-width: none;
    &::-webkit-scrollbar,
    &::-webkit-scrollbar-track-piece,
    &::-webkit-scrollbar-thumb {
      width: 0;
      height: 0;
      display: none;
    }
    .m-scrollbar-content {
      box-sizing: border-box;
      min-width: 100%;
    }
  }
  .m-scrollbar-rail {
    position: absolute;
    pointer-events: none;
    user-select: none;
    background: transparent;
    -webkit-user-select: none;
    .m-scrollbar-track {
      z-index: 1;
      position: absolute;
      cursor: pointer;
      pointer-events: all;
      background-color: rgba(0, 0, 0, 0.25);
      transition: background-color 0.2s cubic-bezier(0.4, 0, 0.2, 1);
      &:hover {
        background-color: rgba(0, 0, 0, 0.4);
      }
    }
  }
  .rail-vertical {
    inset: 2px 4px 2px auto;
    width: var(--scrollbar-size);
    .m-scrollbar-track {
      width: var(--scrollbar-size);
      border-radius: var(--scrollbar-size);
      bottom: 0;
    }
  }
  .rail-horizontal {
    inset: auto 2px 4px 2px;
    height: var(--scrollbar-size);
    .m-scrollbar-track {
      height: var(--scrollbar-size);
      border-radius: var(--scrollbar-size);
      right: 0;
    }
  }
}
</style>

在要使用的页面引入

<script setup lang="ts">
import Scrollbar from './Scrollbar.vue'
function onScroll(e: Event) {
  console.log('scroll:', e)
}
</script>
<template>
  <div>
    <h1>{{ $route.name }} {{ $route.meta.title }}</h1>
    <h2 class="mt30 mb10">基本使用</h2>
    <Scrollbar style="max-height: 120px" @scroll="onScroll">
      我们在田野上面找猪<br />
      想象中已找到了三只<br />
      小鸟在白云上面追逐<br />
      它们在树底下跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      想象中我们是如此的疯狂<br />
      我们在城市里面找猪<br />
      想象中已找到了几百万只<br />
      小鸟在公园里面唱歌<br />
      它们独自在想象里跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      许多年之后我们又开始想象<br />
      啦啦啦啦啦啦啦啦咧
    </Scrollbar>
    <h2 class="mt30 mb10">横向滚动</h2>
    <Scrollbar horizontal>
      <div style="white-space: nowrap; padding: 12px">
        我们在田野上面找猪 想象中已找到了三只 小鸟在白云上面追逐 它们在树底下跳舞 啦啦啦啦啦啦啦啦咧 啦啦啦啦咧
        我们在想象中度过了许多年 想象中我们是如此的疯狂 我们在城市里面找猪 想象中已找到了几百万只 小鸟在公园里面唱歌
        它们独自在想象里跳舞 啦啦啦啦啦啦啦啦咧 啦啦啦啦咧 我们在想象中度过了许多年 许多年之后我们又开始想象
        啦啦啦啦啦啦啦啦咧
      </div>
    </Scrollbar>
    <h2 class="mt30 mb10">触发方式</h2>
    <Scrollbar horizontal style="max-height: 120px" trigger="none">
      我们在田野上面找猪<br />
      想象中已找到了三只<br />
      小鸟在白云上面追逐<br />
      它们在树底下跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      想象中我们是如此的疯狂<br />
      我们在城市里面找猪<br />
      想象中已找到了几百万只<br />
      小鸟在公园里面唱歌<br />
      它们独自在想象里跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      许多年之后我们又开始想象<br />
      啦啦啦啦啦啦啦啦咧
    </Scrollbar>
    <h2 class="mt30 mb10">自定义内容样式</h2>
    <Scrollbar
      style="max-height: 120px; border-radius: 12px"
      :content-style="{ backgroundColor: '#e6f4ff', padding: '16px 24px', fontSize: '16px' }"
    >
      我们在田野上面找猪<br />
      想象中已找到了三只<br />
      小鸟在白云上面追逐<br />
      它们在树底下跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      想象中我们是如此的疯狂<br />
      我们在城市里面找猪<br />
      想象中已找到了几百万只<br />
      小鸟在公园里面唱歌<br />
      它们独自在想象里跳舞<br />
      啦啦啦啦啦啦啦啦咧<br />
      啦啦啦啦咧<br />
      我们在想象中度过了许多年<br />
      许多年之后我们又开始想象<br />
      啦啦啦啦啦啦啦啦咧
    </Scrollbar>
  </div>
</template>

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1933583.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【韩顺平零基础学java】第17章补充笔记记录

1.线程终止 线程完成任务后会自动退出 或者可以使用变量来控制run方法退出的方式终止线程&#xff0c;即通知方式 【例】Threadexit 启动一个线程T。要求在main方法中终止线程T public class ThreadExit_ {public static void main(String[] args) throws InterruptedExcepti…

网络故障处理及分析工具:Wireshark和Tcpdump集成

Wireshark 是一款免费的开源数据包嗅探器和网络协议分析器&#xff0c;已成为网络故障排除、分析和安全&#xff08;双向&#xff09;中不可或缺的工具。 本文深入探讨了充分利用 Wireshark 的功能、用途和实用技巧。 无论您是开发人员、安全专家&#xff0c;还是只是对网络操…

Unity格斗游戏,两个角色之间互相锁定对方,做圆周运动

1&#xff0c;灵感来源 今天手头的工作忙完了&#xff0c;就等着服务器那边完活&#xff0c;于是开始研究同步问题。 正好想到之前想做的&#xff0c;两个小人对线PK&#xff0c;便有了这篇文章。 2&#xff0c;要实现的效果 如图所示&#xff0c;两个小人可以互相锁定&…

RuoYi-后端管理项目入门篇1

目录 前提准备 下载若依前后端 Gitee 地址 准备环境 后端数据库导入 1 克隆完成 若依后端管理后端 Gitte 地址 :若依/RuoYi-Vue 2.1 创建Data Source数据源 2.2 填写好对应的数据库User 和 Password 点击Apply 2.3 新建一个Schema 2.4 填写对应数据库名称 这边演示写的…

linux shell脚本编程(分支语句、循环语句)

一、分支语句 1、语法结构 : if 表达式 then 命令表 fi 如果表达式为真 , 则执行命令表中的命令 ; 否则退出 if 语句 , 即执行 fi 后面的语句。 if 和 fi 是条件语句的语句括号 , 必须成对使用 ;命令表中的命令可以是一条 , 也可以是若干条。 2、语法结构为 : if 表达式 t…

Flink源码学习资料

Flink系列文档脑图 由于源码分析系列文档较多&#xff0c;本人绘制了Flink文档脑图。和下面的文档目录对应。各位读者可以选择自己感兴趣的模块阅读并参与讨论。 此脑图不定期更新中…… 文章目录 以下是本人Flink 源码分析系列文档目录&#xff0c;欢迎大家查阅和参与讨论。…

leetcode日记(42)螺旋矩阵

我使用的是递归&#xff0c;每次递归遍历一圈矩阵&#xff0c;将遍历结果塞进结果vector中&#xff0c;每次遍历修改上下左右边界&#xff0c;直至遍历后其中两边界重合或交错。 class Solution { public:vector<int> spiralOrder(vector<vector<int>>&…

MySQL双主双从实现方式

双主双从&#xff08;MM-SS&#xff09; 前言 避免单一主服务器宕机&#xff0c;集群写入能力缺失 从 1 复制 主1 &#xff0c;从 2 复制 主 2 主 1 复制 主 2&#xff0c;主 2 复制主 1 也就是 主 1 和主 2 互为主从。主1主2互为主从&#xff0c; 是为了以下情景&#xff0c…

Fork_Join模式:分治和Work-Stealing的完美结合

引言 在计算机科学领域&#xff0c;解决大规模数据和复杂计算任务的需求促使了并行计算模型的发展。Fork/Join模式以其独特的分治和Work-Stealing结合的方式&#xff0c;成为解决可分解并行问题的一项卓越选择。本文将重点探讨Fork/Join模式中分治和Work-Stealing的关键优势&a…

【网络】掌握网络基础概念

文章目录 OSI七层模型TCP/IP五层&#xff08;或四层&#xff09;模型为什么要有TCP/IP协议网络传输的基本流程网络传输流程图数据包封装和分用 网络中的地址管理IP地址Mac地址比较IP地址和Mac地址 OSI七层模型 OSI即Open System Interconnection,开发系统互连。OSI七层模型是一…

电气电工增加收入需规避的16大陷阱

电工接线是电气工程中非常重要的环节&#xff0c;为什么实施项目之后&#xff0c;存在电工叫苦&#xff0c;老板说亏&#xff0c;这到底是什么原因&#xff1f;是什么影响了电工的效率&#xff0c;阻碍了电工收入的增加&#xff1f; 下面我们汇总一下平时在项目中遇到的陷阱&a…

spring核心内容基本解读、spring中IOC控制反转入门案例,debug带你剖析Spring容器机制和结构(图文讲解,简单易懂)。

目录 1.spring核心内容大概解读 2.spring的IOC(控制反转容器机制剖析) 2.1 来份快速入门案例 2.2 debug剖析spring容器的机制和结构 1.spring核心内容大概解读 1. 在线文档 : https://docs.spring.io/spring-framework/docs/current/reference/html/ 2. 离 线 文 档 …

STM32智能健康监测系统教程

目录 引言环境准备智能健康监测系统基础代码实现&#xff1a;实现智能健康监测系统 4.1 数据采集模块 4.2 数据处理与控制模块 4.3 通信与网络系统实现 4.4 用户界面与数据可视化应用场景&#xff1a;健康监测与管理问题解决方案与优化收尾与总结 1. 引言 智能健康监测系统通…

SDXL 1.0 下载和部署

SD XL 1.0 重磅更新&#xff01;免费开源可商用&#xff08;附在线使用本地部署教程&#xff09; - 优设网 - 学设计上优设 三、本地部署 SDXL 1.0 SDXL 1.0 的源文件已经在 Huggingface 上开源了&#xff0c;我们可以通过 Stable Diffusion WebUI 在本地免费使用 SDXL 1.0&am…

[C/C++入门][ifelse]19、制作一个简单计算器

简单的方法 我们将假设用户输入两个数字和一个运算符&#xff08;、-、*、/&#xff09;&#xff0c;然后根据所选的运算符执行相应的操作。 #include <iostream> using namespace std;int main() {double num1, num2;char op;cout << "输入 (,-,*,/): &quo…

python项目为什么用WSGI

小背景 Java用的时间久了&#xff0c;web项目启动的时候直接启动主程序就行&#xff0c;因为spring web项目内置了Tomcat web服务器&#xff0c;服务器的配置一般也是采用默认的配置&#xff0c;所以很少关注底层实现&#xff0c;关注点主要在应用程序功能。 初学python的时候…

DDei在线设计器-HTML渲染

Html渲染 HtmlViewer插件通过将一个外部DIV附着在图形控件上&#xff0c;从而改变原有图形的显示方式。允许使用者自己定义HTML通过HTML元素。本示例演示了通过Html来扩展渲染图形&#xff0c;从而获得更加丰富的图形展现。 通常情况下&#xff0c;我们创建的图形控件&#xff…

Wireshark抓取HTTP

HTTP请求响应 使用wireshark抓取 本地机器是192.168.33.195&#xff0c;远程机器是192.168.32.129&#xff0c;远程HTTP服务端口是9005 TCP/IP实际共分为4层&#xff0c;抓包信息中可以看到各层的数据&#xff0c;最上面的数据帧包含了所有数据。 附&#xff1a;抓取本地H…

【Harmony】SCU暑期实训鸿蒙开发学习日记Day2

目录 Git 参考文章 常用操作 ArkTS的网络编程 Http编程 发送请求 GET POST 处理响应 JSON数据解析 处理响应头 错误处理 Web组件 用生命周期钩子实现登录验证功能 思路 代码示例 解读 纯记录学习日记&#xff0c;杂乱&#xff0c;误点的师傅可以掉了&#x1…

体验智慧校园学工管理的社团类型功能

在智慧校园学工管理的框架下&#xff0c;社团类型的精细化管理成为了激发学生兴趣、促进社团多样性与规范性的关键。这一功能的核心在于构建一个清晰的社团分类体系&#xff0c;确保每一种社团都能在其所属领域内蓬勃发展&#xff0c;同时&#xff0c;也便于校园管理者进行有效…