Skip to content

v-scroll-mask 滚动遮罩指令

v-scroll-mask 是一个 Vue 3 自定义指令,用于为滚动容器添加渐隐遮罩效果和阴影,提升用户体验。

特性

  • 🎨 智能遮罩:根据滚动位置自动显示/隐藏渐隐效果
  • 🎯 多方向支持:支持垂直、水平和双向滚动
  • 🎨 自定义颜色:支持自定义遮罩颜色和背景色
  • 过渡动画:可配置的平滑过渡效果
  • 📱 响应式:自动适应容器尺寸变化
  • 🔧 高度可配置:丰富的配置选项

安装

bash
npm install @pt/common-ui

基础用法

注册指令

typescript
// main.ts
import { createApp } from "vue";
import { scrollMask } from "@pt/common-ui";

const app = createApp(App);
app.directive("scroll-mask", scrollMask);

简单示例

vue
<template>
  <!-- 基础用法 -->
  <div class="scroll-container" v-scroll-mask>
    <div class="content">
      <p v-for="i in 20" :key="i">内容行 {{ i }}</p>
    </div>
  </div>
</template>

<style>
.scroll-container {
  height: 300px;
  overflow: auto;
  border: 1px solid #ccc;
  border-radius: 8px;
}
</style>

配置选项

ScrollMaskOptions 接口

typescript
interface ScrollMaskOptions {
  fadePercent?: number; // 渐隐区域百分比,默认 15
  shadowSize?: number; // 阴影大小,默认 8px
  shadowColor?: string; // 阴影颜色,默认 rgba(0,0,0,0.3)
  hideScrollbar?: boolean; // 是否隐藏滚动条,默认 true
  direction?: "auto" | "vertical" | "horizontal" | "both"; // 滚动方向,默认 auto
  maskColor?: string; // 遮罩颜色,默认 black
  backgroundColor?: string; // 背景颜色,默认 transparent
  enableTransition?: boolean; // 是否启用过渡动画,默认 false
  transitionDuration?: string; // 过渡动画持续时间,默认 0.3s
  transitionEasing?: string; // 过渡动画缓动函数,默认 ease
  // 🎯 细粒度方向控制选项
  enableTop?: boolean; // 是否启用顶部遮罩,默认 true
  enableBottom?: boolean; // 是否启用底部遮罩,默认 true
  enableLeft?: boolean; // 是否启用左侧遮罩,默认 true
  enableRight?: boolean; // 是否启用右侧遮罩,默认 true
}

使用示例

1. 自定义颜色

vue
<template>
  <!-- 蓝色主题 -->
  <div
    class="scroll-container"
    v-scroll-mask="{
      maskColor: '#1e40af',
      backgroundColor: 'rgba(59, 130, 246, 0.1)',
    }"
  >
    <div class="content">
      <p v-for="i in 15" :key="i">蓝色主题内容 {{ i }}</p>
    </div>
  </div>

  <!-- 红色主题 -->
  <div
    class="scroll-container"
    v-scroll-mask="{
      maskColor: '#dc2626',
      backgroundColor: 'rgba(239, 68, 68, 0.1)',
    }"
  >
    <div class="content">
      <p v-for="i in 15" :key="i">红色主题内容 {{ i }}</p>
    </div>
  </div>
</template>

2. 过渡动画配置

vue
<template>
  <!-- 启用过渡动画 -->
  <div
    class="scroll-container"
    v-scroll-mask="{
      enableTransition: true,
      transitionDuration: '0.2s',
      transitionEasing: 'ease-out',
    }"
  >
    <div class="content">
      <p v-for="i in 12" :key="i">带过渡动画的内容 {{ i }}</p>
    </div>
  </div>
</template>

3. 水平滚动

vue
<template>
  <div
    class="horizontal-scroll"
    v-scroll-mask="{
      direction: 'horizontal',
      maskColor: '#7c3aed',
      enableTransition: true,
    }"
  >
    <div class="horizontal-content">
      <div
        v-for="i in 10"
        :key="i"
        class="card"
        :style="{ backgroundColor: `hsl(${i * 36}, 70%, 80%)` }"
      >
        卡片 {{ i }}
      </div>
    </div>
  </div>
</template>

<style>
.horizontal-scroll {
  height: 150px;
  overflow-x: auto;
  overflow-y: hidden;
}

.horizontal-content {
  display: flex;
  gap: 16px;
  padding: 16px;
  width: max-content;
}

.card {
  min-width: 150px;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  color: white;
  font-weight: 500;
}
</style>

4. 双向滚动

vue
<template>
  <div
    class="both-scroll"
    v-scroll-mask="{
      direction: 'both',
      maskColor: '#0891b2',
      enableTransition: true,
      fadePercent: 20,
    }"
  >
    <div class="grid-content">
      <div v-for="row in 15" :key="row" class="grid-row">
        <span v-for="col in 20" :key="col" class="grid-cell">
          {{ row }}-{{ col }}
        </span>
      </div>
    </div>
  </div>
</template>

<style>
.both-scroll {
  height: 200px;
  overflow: auto;
}

.grid-content {
  width: max-content;
  padding: 16px;
}

.grid-row {
  display: flex;
  margin-bottom: 8px;
}

.grid-cell {
  min-width: 80px;
  padding: 8px;
  margin-right: 8px;
  background: #e0e7ff;
  border-radius: 4px;
  text-align: center;
  font-size: 0.875rem;
}
</style>

5. 自定义参数

vue
<template>
  <!-- 大渐隐区域 -->
  <div
    class="scroll-container"
    v-scroll-mask="{
      fadePercent: 30,
      shadowSize: 15,
      shadowColor: 'rgba(139, 69, 19, 0.5)',
    }"
  >
    <div class="content">
      <p v-for="i in 12" :key="i">大渐隐区域 (30%) - 内容 {{ i }}</p>
    </div>
  </div>

  <!-- 显示滚动条 -->
  <div
    class="scroll-container"
    v-scroll-mask="{
      hideScrollbar: false,
      maskColor: '#f59e0b',
    }"
  >
    <div class="content">
      <p v-for="i in 12" :key="i">显示滚动条 - 内容 {{ i }}</p>
    </div>
  </div>
</template>

6. 动态配置

vue
<template>
  <div class="controls">
    <label>
      遮罩颜色:
      <input type="color" v-model="config.maskColor" />
    </label>
    <label>
      渐隐百分比:
      <input
        type="range"
        min="5"
        max="50"
        v-model.number="config.fadePercent"
      />
      {{ config.fadePercent }}%
    </label>
    <label>
      <input type="checkbox" v-model="config.enableTransition" />
      启用过渡动画
    </label>
  </div>

  <div class="scroll-container" v-scroll-mask="config">
    <div class="content">
      <p v-for="i in 15" :key="i">动态配置内容 {{ i }}</p>
    </div>
  </div>
</template>

<script setup>
import { reactive } from "vue";

const config = reactive({
  maskColor: "#6366f1",
  fadePercent: 15,
  enableTransition: true,
  transitionDuration: "0.3s",
  transitionEasing: "ease",
});
</script>

滚动状态

指令会自动为元素添加相应的 CSS 类名,用于标识当前滚动状态:

状态类名描述
scroll-mask-top滚动到顶部
scroll-mask-bottom滚动到底部
scroll-mask-left滚动到左侧
scroll-mask-right滚动到右侧
scroll-mask-middle-v垂直方向中间位置
scroll-mask-middle-h水平方向中间位置
scroll-mask-middle双向滚动中间位置

工作原理

1. 遮罩实现

指令使用 CSS mask-image 属性创建渐隐效果:

css
/* 顶部遮罩 */
mask-image: linear-gradient(to bottom, black 85%, transparent 100%);

/* 底部遮罩 */
mask-image: linear-gradient(to top, black 85%, transparent 100%);

/* 中间遮罩 */
mask-image: linear-gradient(
  to bottom,
  transparent 0%,
  black 15%,
  black 85%,
  transparent 100%
);

2. 阴影效果

使用 box-shadowinset 属性创建内阴影:

css
/* 底部阴影 */
box-shadow: inset 0 -8px 8px -8px rgba(0, 0, 0, 0.3);

/* 顶部阴影 */
box-shadow: inset 0 8px 8px -8px rgba(0, 0, 0, 0.3);

3. 滚动条隐藏

通过多种方式确保跨浏览器兼容:

css
/* Firefox */
scrollbar-width: none;

/* IE/Edge */
-ms-overflow-style: none;

/* Webkit */
.hide-scrollbar::-webkit-scrollbar {
  display: none;
}

最佳实践

1. 容器设置

确保滚动容器有明确的高度和 overflow 属性:

css
.scroll-container {
  height: 300px;
  overflow: auto; /* 或 overflow-y: auto */
}

2. 性能优化

  • 指令使用 passive: true 监听滚动事件
  • 使用 ResizeObserver 监听尺寸变化
  • 避免在大量元素上同时使用

3. 颜色搭配

选择与内容背景相匹配的遮罩颜色:

javascript
// 浅色背景
{ maskColor: '#ffffff', backgroundColor: 'transparent' }

// 深色背景
{ maskColor: '#1f2937', backgroundColor: 'transparent' }

// 彩色背景
{ maskColor: '#3b82f6', backgroundColor: 'rgba(59, 130, 246, 0.1)' }

4. 响应式设计

在移动设备上考虑调整参数:

javascript
const isMobile = window.innerWidth < 768;

const config = {
  fadePercent: isMobile ? 20 : 15,
  shadowSize: isMobile ? 6 : 8,
  enableTransition: !isMobile, // 移动设备上禁用动画以提升性能
};

注意事项

1. 浏览器兼容性

  • CSS mask 属性在较老的浏览器中可能不支持
  • IE 浏览器需要使用前缀 -webkit-mask

2. 性能考虑

  • 避免在滚动过程中频繁更新配置
  • 大量元素使用时注意内存占用

3. 样式冲突

  • 指令会修改元素的 mask-imagebox-shadow 属性
  • 避免在 CSS 中覆盖这些属性

故障排除

问题:遮罩效果不显示

解决方案:

  1. 检查容器是否设置了正确的 overflow 属性
  2. 确保容器有固定高度
  3. 验证内容是否超出容器高度

问题:滚动条仍然显示

解决方案:

  1. 确保 hideScrollbar: true(默认值)
  2. 检查 CSS 是否有 !important 覆盖
  3. 验证浏览器是否支持相关属性

问题:过渡动画不流畅

解决方案:

  1. 调整 transitionDurationtransitionEasing
  2. 在移动设备上禁用过渡动画
  3. 检查是否有其他 CSS 动画冲突

API 参考

指令使用

vue
<div v-scroll-mask="options">
  <!-- 内容 -->
</div>

完整配置示例

javascript
const options = {
  fadePercent: 15, // 渐隐区域百分比 (5-50)
  shadowSize: 8, // 阴影大小 (px)
  shadowColor: "rgba(0,0,0,0.3)", // 阴影颜色
  hideScrollbar: true, // 隐藏滚动条
  direction: "auto", // 滚动方向: auto | vertical | horizontal | both
  maskColor: "black", // 遮罩颜色
  backgroundColor: "transparent", // 背景颜色
  enableTransition: false, // 启用过渡动画
  transitionDuration: "0.3s", // 过渡持续时间
  transitionEasing: "ease", // 过渡缓动函数
  // 🎯 细粒度方向控制选项
  enableTop: true, // 是否启用顶部遮罩
  enableBottom: true, // 是否启用底部遮罩
  enableLeft: true, // 是否启用左侧遮罩
  enableRight: true, // 是否启用右侧遮罩
};

📚 效果预览

v-scroll-mask 指令
请打开代码查看

<template>
  <div class="scroll-mask-test-page">
    <h1 class="page-title">ScrollMask 指令测试页面</h1>
    <p class="page-description">测试 v-scroll-mask 指令的各种配置选项和使用场景</p>

    <!-- 基础用法 -->
    <div class="test-section">
      <h2 class="section-title">1. 基础用法(默认配置)</h2>
      <div class="test-container">
        <div class="scroll-box basic-test" v-scroll-mask>
          <div class="content">
            <p v-for="i in 20" :key="i">
              这是第 {{ i }} 行内容,用于测试垂直滚动遮罩效果。滚动时会在顶部和底部显示渐隐效果。
            </p>
          </div>
        </div>
      </div>
    </div>

    <!-- 自定义颜色 -->
    <div class="test-section">
      <h2 class="section-title">2. 自定义颜色配置</h2>
      <div class="test-grid">
        <div class="test-item">
          <h3>蓝色遮罩</h3>
          <div class="scroll-box" v-scroll-mask="{ maskColor: '#1e40af', backgroundColor: 'rgba(59, 130, 246, 0.1)' }">
            <div class="content">
              <p v-for="i in 15" :key="i">蓝色主题遮罩效果 - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
        <div class="test-item">
          <h3>红色遮罩</h3>
          <div class="scroll-box" v-scroll-mask="{ maskColor: '#dc2626', backgroundColor: 'rgba(239, 68, 68, 0.1)' }">
            <div class="content">
              <p v-for="i in 15" :key="i">红色主题遮罩效果 - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 过渡动画配置 -->
    <div class="test-section">
      <h2 class="section-title">3. 过渡动画配置</h2>
      <div class="test-grid">
        <div class="test-item">
          <h3>无过渡(默认)</h3>
          <div class="scroll-box" v-scroll-mask="{ enableTransition: false }">
            <div class="content">
              <p v-for="i in 12" :key="i">无过渡动画 - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
        <div class="test-item">
          <h3>快速过渡</h3>
          <div class="scroll-box" v-scroll-mask="{ enableTransition: true, transitionDuration: '0.2s', transitionEasing: 'ease-out' }">
            <div class="content">
              <p v-for="i in 12" :key="i">快速过渡动画 - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 水平滚动 -->
    <div class="test-section">
      <h2 class="section-title">4. 水平滚动测试</h2>
      <div class="test-container">
        <div class="scroll-box horizontal-scroll" v-scroll-mask="{ direction: 'horizontal', maskColor: '#7c3aed', enableTransition: true }">
          <div class="horizontal-content">
            <div v-for="i in 10" :key="i" class="horizontal-item" :style="{ backgroundColor: `hsl(${i * 36}, 70%, 80%)` }">
              卡片 {{ i }}
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 双向滚动 -->
    <div class="test-section">
      <h2 class="section-title">5. 双向滚动测试</h2>
      <div class="test-container">
        <div class="scroll-box both-scroll" v-scroll-mask="{ direction: 'both', maskColor: '#0891b2', enableTransition: true, fadePercent: 20 }">
          <div class="both-content">
            <div v-for="row in 15" :key="row" class="both-row">
              <span v-for="col in 20" :key="col" class="both-cell">{{ row }}-{{ col }}</span>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 自定义参数测试 -->
    <div class="test-section">
      <h2 class="section-title">6. 自定义参数测试</h2>
      <div class="test-grid">
        <div class="test-item">
          <h3>大渐隐区域</h3>
          <div class="scroll-box" v-scroll-mask="{ fadePercent: 30, shadowSize: 15, shadowColor: 'rgba(139, 69, 19, 0.5)' }">
            <div class="content">
              <p v-for="i in 12" :key="i">大渐隐区域 (30%) - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
        <div class="test-item">
          <h3>显示滚动条</h3>
          <div class="scroll-box" v-scroll-mask="{ hideScrollbar: false, maskColor: '#f59e0b' }">
            <div class="content">
              <p v-for="i in 12" :key="i">显示滚动条 - 第 {{ i }} 行</p>
            </div>
          </div>
        </div>
      </div>
    </div>

    <!-- 动态配置测试 -->
    <div class="test-section">
      <h2 class="section-title">7. 动态配置测试</h2>
      <div class="controls">
        <label>
          遮罩颜色:
          <input type="color" v-model="dynamicConfig.maskColor">
        </label>
        <label>
          渐隐百分比:
          <input type="range" min="5" max="50" v-model.number="dynamicConfig.fadePercent">
          {{ dynamicConfig.fadePercent }}%
        </label>
        <label>
          <input type="checkbox" v-model="dynamicConfig.enableTransition">
          启用过渡动画
        </label>
      </div>
      <div class="test-container">
        <div class="scroll-box" v-scroll-mask="dynamicConfig">
          <div class="content">
            <p v-for="i in 15" :key="i">动态配置测试 - 第 {{ i }} 行内容</p>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
// 动态配置
const dynamicConfig = reactive({
  maskColor: '#6366f1',
  fadePercent: 15,
  enableTransition: true,
  transitionDuration: '0.3s',
  transitionEasing: 'ease'
})
</script>

<style scoped>
.scroll-mask-test-page {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
}

.page-title {
  font-size: 2rem;
  font-weight: bold;
  margin-bottom: 0.5rem;
  color: #1f2937;
}

.page-description {
  color: #6b7280;
  margin-bottom: 2rem;
}

.test-section {
  margin-bottom: 3rem;
}

.section-title {
  font-size: 1.5rem;
  font-weight: 600;
  margin-bottom: 1rem;
  color: #374151;
}

.test-container {
  display: flex;
  justify-content: center;
}

.test-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 1.5rem;
}

.test-item h3 {
  font-size: 1.1rem;
  font-weight: 500;
  margin-bottom: 0.5rem;
  color: #4b5563;
}

.scroll-box {
  width: 100%;
  height: 200px;
  border: 2px solid #e5e7eb;
  border-radius: 8px;
  background: #f9fafb;
  position: relative;
  overflow: auto;
}

.content {
  padding: 1rem;
}

.content p {
  margin-bottom: 0.5rem;
  line-height: 1.5;
  color: #374151;
}

/* 水平滚动样式 */
.horizontal-scroll {
  overflow-x: auto;
  overflow-y: hidden;
}

.horizontal-content {
  display: flex;
  gap: 1rem;
  padding: 1rem;
  width: max-content;
}

.horizontal-item {
  min-width: 150px;
  height: 100px;
  display: flex;
  align-items: center;
  justify-content: center;
  border-radius: 8px;
  color: white;
  font-weight: 500;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

/* 双向滚动样式 */
.both-scroll {
  overflow: auto;
}

.both-content {
  width: max-content;
  padding: 1rem;
}

.both-row {
  display: flex;
  margin-bottom: 0.5rem;
}

.both-cell {
  display: inline-block;
  min-width: 80px;
  padding: 0.5rem;
  margin-right: 0.5rem;
  background: #e0e7ff;
  border-radius: 4px;
  text-align: center;
  font-size: 0.875rem;
}

/* 控制面板样式 */
.controls {
  display: flex;
  gap: 1.5rem;
  margin-bottom: 1rem;
  padding: 1rem;
  background: #f3f4f6;
  border-radius: 8px;
  flex-wrap: wrap;
}

.controls label {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  font-size: 0.875rem;
  font-weight: 500;
  color: #374151;
}

.controls input[type="color"] {
  width: 40px;
  height: 30px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}

.controls input[type="range"] {
  width: 100px;
}

.controls input[type="checkbox"] {
  width: 16px;
  height: 16px;
}

/* 主题变体 */
.blue-theme {
  background: linear-gradient(135deg, #dbeafe, #bfdbfe);
}

.red-theme {
  background: linear-gradient(135deg, #fee2e2, #fecaca);
}

.green-theme {
  background: linear-gradient(135deg, #d1fae5, #a7f3d0);
}

/* 响应式设计 */
@media (max-width: 768px) {
  .test-grid {
    grid-template-columns: 1fr;
  }

  .controls {
    flex-direction: column;
    gap: 1rem;
  }

  .scroll-box {
    height: 150px;
  }
}
</style>

相关链接

更新日志

v1.3.0

  • ✨ 新增细粒度方向控制功能
  • ✨ 支持独立控制各方向遮罩显示
  • ✨ 新增方向控制相关 CSS 类名
  • 🔧 优化双向滚动组合逻辑

v1.2.0

  • ✨ 新增自定义颜色支持
  • ✨ 新增过渡动画配置
  • 🐛 修复双向滚动遮罩计算问题

v1.1.0

  • ✨ 新增双向滚动支持
  • ✨ 新增响应式尺寸监听
  • 🔧 优化性能

v1.0.0

  • 🎉 首次发布
  • ✨ 基础滚动遮罩功能
  • ✨ 垂直和水平滚动支持

Released under the MIT License.