Skip to content

useScrollMask 滚动遮罩 Hook

useScrollMask 是一个 Vue 3 组合式函数,用于为滚动容器添加渐隐遮罩效果和阴影,提供响应式的滚动状态管理。

特性

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

安装

bash
npm install @pt/common-ui

基础用法

导入

typescript
import { useScrollMask } from "@pt/common-ui";

简单示例

vue
<template>
  <div class="scroll-container" ref="scrollRef">
    <div class="content">
      <p v-for="i in 20" :key="i">内容行 {{ i }}</p>
    </div>
  </div>

  <!-- 显示当前滚动状态 -->
  <div class="status">当前状态: {{ scrollState }}</div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);

// 基础用法
const { scrollState, updateMaskState } = useScrollMask(scrollRef);
</script>

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

API 参考

函数签名

typescript
function useScrollMask(
  elementRef: Ref<HTMLElement | null>,
  options?: UseScrollMaskOptions
): {
  scrollState: Ref<ScrollState>;
  updateMaskState: () => void;
};

参数

elementRef

  • 类型: Ref<HTMLElement | null>
  • 描述: 滚动容器的模板引用

options

  • 类型: UseScrollMaskOptions
  • 描述: 配置选项
  • 默认值: {}

UseScrollMaskOptions 接口

typescript
interface UseScrollMaskOptions {
  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
  deps?: Ref<any>[]; // 依赖项数组,变化时重新计算
  // 🎯 细粒度方向控制选项
  enableTop?: boolean; // 是否启用顶部遮罩,默认 true
  enableBottom?: boolean; // 是否启用底部遮罩,默认 true
  enableLeft?: boolean; // 是否启用左侧遮罩,默认 true
  enableRight?: boolean; // 是否启用右侧遮罩,默认 true
}

返回值

scrollState

  • 类型: Ref<ScrollState>
  • 描述: 响应式的滚动状态

updateMaskState

  • 类型: () => void
  • 描述: 手动更新遮罩状态的函数

ScrollState 类型

typescript
type ScrollState =
  | "top" // 滚动到顶部
  | "bottom" // 滚动到底部
  | "left" // 滚动到左侧
  | "right" // 滚动到右侧
  | "middle" // 双向滚动中间位置
  | "middle-h" // 水平方向中间位置
  | "middle-v" // 垂直方向中间位置
  | "none"; // 无滚动或无遮罩

使用示例

1. 自定义颜色

vue
<template>
  <div class="container">
    <!-- 蓝色主题 -->
    <div class="scroll-box" ref="blueScrollRef">
      <div class="content">
        <p v-for="i in 15" :key="i">蓝色主题内容 {{ i }}</p>
      </div>
    </div>

    <!-- 红色主题 -->
    <div class="scroll-box" ref="redScrollRef">
      <div class="content">
        <p v-for="i in 15" :key="i">红色主题内容 {{ i }}</p>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const blueScrollRef = ref(null);
const redScrollRef = ref(null);

// 蓝色主题
const { scrollState: blueState } = useScrollMask(blueScrollRef, {
  maskColor: "#3b82f6",
  backgroundColor: "#dbeafe",
});

// 红色主题
const { scrollState: redState } = useScrollMask(redScrollRef, {
  maskColor: "#ef4444",
  backgroundColor: "#fecaca",
});
</script>

2. 过渡动画配置

vue
<template>
  <div class="scroll-container" ref="scrollRef">
    <div class="content">
      <p v-for="i in 20" :key="i">带过渡动画的内容 {{ i }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);

const { scrollState } = useScrollMask(scrollRef, {
  enableTransition: true,
  transitionDuration: "0.2s",
  transitionEasing: "ease-out",
});
</script>

3. 水平滚动

vue
<template>
  <div class="horizontal-scroll" ref="horizontalRef">
    <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>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const horizontalRef = ref(null);

const { scrollState } = useScrollMask(horizontalRef, {
  direction: "horizontal",
  maskColor: "#7c3aed",
  enableTransition: true,
  fadePercent: 20,
});
</script>

<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" ref="bothRef">
    <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>

  <div class="status">双向滚动状态: {{ bothState }}</div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const bothRef = ref(null);

const { scrollState: bothState } = useScrollMask(bothRef, {
  direction: "both",
  maskColor: "#0891b2",
  enableTransition: true,
  fadePercent: 20,
});
</script>

<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="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>
    <button @click="manualUpdate">手动更新</button>
  </div>

  <div class="scroll-container" ref="scrollRef">
    <div class="content">
      <p v-for="i in 15" :key="i">响应式配置内容 {{ i }}</p>
    </div>
  </div>

  <div class="status">当前状态: {{ scrollState }}</div>
</template>

<script setup>
import { ref, reactive } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);

// 响应式配置
const config = reactive({
  maskColor: "#6366f1",
  fadePercent: 15,
  enableTransition: true,
  transitionDuration: "0.3s",
  transitionEasing: "ease",
});

const { scrollState, updateMaskState } = useScrollMask(scrollRef, config);

const manualUpdate = () => {
  updateMaskState();
};
</script>

6. 依赖项监听

vue
<template>
  <div class="controls">
    <button @click="toggleTheme">切换主题</button>
    <button @click="changeSize">改变尺寸</button>
  </div>

  <div
    class="scroll-container"
    ref="scrollRef"
    :class="{ 'dark-theme': isDark }"
    :style="{ height: containerHeight + 'px' }"
  >
    <div class="content">
      <p v-for="i in 20" :key="i">依赖监听内容 {{ i }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, computed } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);
const isDark = ref(false);
const containerHeight = ref(300);

// 计算属性作为依赖
const themeConfig = computed(() => ({
  maskColor: isDark.value ? "#ffffff" : "#000000",
  backgroundColor: isDark.value ? "rgba(255,255,255,0.1)" : "transparent",
  shadowColor: isDark.value ? "rgba(255,255,255,0.3)" : "rgba(0,0,0,0.3)",
}));

// 监听依赖项变化
const { scrollState } = useScrollMask(scrollRef, {
  ...themeConfig.value,
  deps: [isDark, containerHeight], // 依赖项数组
});

const toggleTheme = () => {
  isDark.value = !isDark.value;
};

const changeSize = () => {
  containerHeight.value = containerHeight.value === 300 ? 400 : 300;
};
</script>

<style>
.dark-theme {
  background: #1f2937;
  color: white;
}
</style>

7. 细粒度方向控制

🎯 新功能:v1.3.0 版本新增了细粒度方向控制,允许独立控制每个方向的遮罩显示。

vue
<template>
  <!-- 聊天消息列表:垂直滚动中间时不显示顶部遮罩 -->
  <div class="chat-list" ref="chatRef">
    <div v-for="msg in messages" :key="msg.id" class="message">
      <div class="avatar">{{ msg.user[0] }}</div>
      <div class="content">{{ msg.content }}</div>
      <div class="time">{{ msg.time }}</div>
    </div>
  </div>

  <!-- 图片轮播:水平滚动只显示右侧遮罩 -->
  <div class="carousel" ref="carouselRef">
    <div class="carousel-track">
      <img
        v-for="img in images"
        :key="img.id"
        :src="img.url"
        class="carousel-item"
      />
    </div>
  </div>

  <!-- 数据网格:双向滚动自定义控制 -->
  <div class="data-grid" ref="gridRef">
    <table>
      <thead>
        <tr>
          <th v-for="col in columns" :key="col">{{ col }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in gridData" :key="row.id">
          <td v-for="col in columns" :key="col">{{ row[col] }}</td>
        </tr>
      </tbody>
    </table>
  </div>

  <!-- 状态显示 -->
  <div class="status-panel">
    <div>聊天状态: {{ chatState }}</div>
    <div>轮播状态: {{ carouselState }}</div>
    <div>网格状态: {{ gridState }}</div>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const chatRef = ref(null);
const carouselRef = ref(null);
const gridRef = ref(null);

// 聊天列表:不显示顶部遮罩
const { scrollState: chatState } = useScrollMask(chatRef, {
  direction: "vertical",
  enableTop: false, // 禁用顶部遮罩
  enableBottom: true, // 启用底部遮罩
  maskColor: "#f8fafc",
  enableTransition: true,
});

// 图片轮播:只显示右侧遮罩
const { scrollState: carouselState } = useScrollMask(carouselRef, {
  direction: "horizontal",
  enableLeft: false, // 禁用左侧遮罩
  enableRight: true, // 启用右侧遮罩
  maskColor: "#1f2937",
  fadePercent: 25,
});

// 数据网格:自定义双向控制
const { scrollState: gridState } = useScrollMask(gridRef, {
  direction: "both",
  enableTop: true, // 启用顶部遮罩
  enableBottom: false, // 禁用底部遮罩
  enableLeft: true, // 启用左侧遮罩
  enableRight: false, // 禁用右侧遮罩
  fadePercent: 20,
  enableTransition: true,
});

const messages = ref([
  { id: 1, user: "Alice", content: "你好!", time: "10:00" },
  { id: 2, user: "Bob", content: "这是一条消息", time: "10:01" },
  // ... 更多消息
]);

const images = ref([
  { id: 1, url: "/image1.jpg" },
  { id: 2, url: "/image2.jpg" },
  // ... 更多图片
]);

const columns = ["ID", "姓名", "部门", "职位", "状态"];
const gridData = ref([
  { id: 1, 姓名: "张三", 部门: "技术部", 职位: "工程师", 状态: "在职" },
  // ... 更多数据
]);
</script>

<style>
.chat-list {
  height: 300px;
  overflow-y: auto;
  background: #f8fafc;
  border-radius: 8px;
  padding: 16px;
}

.message {
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 8px;
  margin-bottom: 8px;
  background: white;
  border-radius: 8px;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.avatar {
  width: 32px;
  height: 32px;
  border-radius: 50%;
  background: #3b82f6;
  color: white;
  display: flex;
  align-items: center;
  justify-content: center;
  font-weight: 600;
  font-size: 0.875rem;
}

.carousel {
  height: 200px;
  overflow-x: auto;
  overflow-y: hidden;
  background: #1f2937;
  border-radius: 8px;
}

.carousel-track {
  display: flex;
  gap: 16px;
  padding: 16px;
  height: 100%;
}

.carousel-item {
  height: 150px;
  width: 200px;
  object-fit: cover;
  border-radius: 8px;
  flex-shrink: 0;
}

.data-grid {
  height: 250px;
  overflow: auto;
  border: 1px solid #e5e7eb;
  border-radius: 8px;
}

.data-grid table {
  width: 100%;
  min-width: 600px;
  border-collapse: collapse;
}

.data-grid th,
.data-grid td {
  padding: 12px;
  text-align: left;
  border-bottom: 1px solid #e5e7eb;
  white-space: nowrap;
}

.data-grid th {
  background: #f9fafb;
  font-weight: 600;
  position: sticky;
  top: 0;
  z-index: 1;
}

.status-panel {
  margin-top: 16px;
  padding: 12px;
  background: #f3f4f6;
  border-radius: 8px;
  font-family: monospace;
  font-size: 0.875rem;
}

.status-panel div {
  margin-bottom: 4px;
}
</style>

8. 依赖项监听

使用场景

  • 聊天界面:垂直滚动时不显示顶部遮罩,保持消息连贯性
  • 图片轮播:水平滚动时只显示特定方向的遮罩引导用户
  • 数据表格:双向滚动时根据内容特点控制各方向遮罩
  • 导航菜单:水平滚动时控制左右遮罩显示
  • 时间轴组件:垂直滚动时选择性显示遮罩效果

动态方向控制

vue
<template>
  <div class="controls">
    <label
      ><input type="checkbox" v-model="controls.enableTop" /> 顶部遮罩</label
    >
    <label
      ><input type="checkbox" v-model="controls.enableBottom" /> 底部遮罩</label
    >
    <label
      ><input type="checkbox" v-model="controls.enableLeft" /> 左侧遮罩</label
    >
    <label
      ><input type="checkbox" v-model="controls.enableRight" /> 右侧遮罩</label
    >
  </div>

  <div class="scroll-container" ref="scrollRef">
    <div class="content">
      <p v-for="i in 20" :key="i">动态控制内容 {{ i }}</p>
    </div>
  </div>

  <div class="status">当前状态: {{ scrollState }}</div>
</template>

<script setup>
import { ref, reactive } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);

const controls = reactive({
  enableTop: true,
  enableBottom: true,
  enableLeft: true,
  enableRight: true,
});

const { scrollState } = useScrollMask(scrollRef, {
  direction: "both",
  enableTransition: true,
  deps: [controls], // 监听控制状态变化
  ...controls,
});
</script>

方向控制选项说明

选项类型默认值描述
enableTopbooleantrue是否启用顶部遮罩
enableBottombooleantrue是否启用底部遮罩
enableRightbooleantrue是否启用右侧遮罩
enableLeftbooleantrue是否启用左侧遮罩

高级用法

1. 自定义滚动状态处理

vue
<template>
  <div class="scroll-container" ref="scrollRef">
    <div class="content">
      <p v-for="i in 20" :key="i">内容 {{ i }}</p>
    </div>
  </div>

  <!-- 状态指示器 -->
  <div class="status-indicators">
    <div :class="['indicator', { active: scrollState === 'top' }]">顶部</div>
    <div :class="['indicator', { active: scrollState === 'middle-v' }]">
      中间
    </div>
    <div :class="['indicator', { active: scrollState === 'bottom' }]">底部</div>
  </div>
</template>

<script setup>
import { ref, watch } from "vue";
import { useScrollMask } from "@pt/common-ui";

const scrollRef = ref(null);

const { scrollState } = useScrollMask(scrollRef, {
  enableTransition: true,
});

// 监听状态变化
watch(scrollState, (newState, oldState) => {
  console.log(`滚动状态从 ${oldState} 变为 ${newState}`);

  // 根据状态执行不同逻辑

  switch (newState) {
    case "top":
      // 到达顶部时的逻辑
      break;
    case "bottom":
      // 到达底部时的逻辑
      break;
    case "middle-v":
      // 垂直中间位置的逻辑
      break;
  }
});
</script>

<style>
.status-indicators {
  display: flex;
  gap: 8px;
  margin-top: 16px;
}

.indicator {
  padding: 4px 8px;
  border-radius: 4px;
  background: #f3f4f6;
  color: #6b7280;
  font-size: 0.875rem;
}

.indicator.active {
  background: #3b82f6;
  color: white;
}
</style>

2. 多个滚动容器管理

vue
<template>
  <div class="multi-container">
    <div
      v-for="(container, index) in containers"
      :key="index"
      class="scroll-container"
      :ref="(el) => setContainerRef(el, index)"
    >
      <div class="content">
        <p v-for="i in 15" :key="i">容器 {{ index + 1 }} - 内容 {{ i }}</p>
      </div>
    </div>
  </div>

  <div class="status-panel">
    <div v-for="(state, index) in scrollStates" :key="index">
      容器 {{ index + 1 }}: {{ state }}
    </div>
  </div>
</template>

<script setup>
import { ref, reactive, computed } from "vue";
import { useScrollMask } from "@pt/common-ui";

const containers = ref([{}, {}, {}]); // 3个容器
const containerRefs = ref([]);
const scrollStates = reactive([]);

const setContainerRef = (el, index) => {
  if (el) {
    containerRefs.value[index] = el;
  }
};

// 为每个容器设置滚动遮罩
containers.value.forEach((_, index) => {
  const containerRef = computed(() => containerRefs.value[index] || null);

  const { scrollState } = useScrollMask(containerRef, {
    maskColor: `hsl(${index * 120}, 70%, 50%)`,
    enableTransition: true,
  });

  scrollStates[index] = scrollState;
});
</script>

与指令的对比

特性useScrollMask Hookv-scroll-mask 指令
使用方式组合式函数模板指令
状态管理✅ 响应式状态❌ 无状态返回
动态配置✅ 响应式配置✅ 支持
依赖监听✅ 支持❌ 不支持
手动更新✅ updateMaskState❌ 自动更新
TypeScript✅ 完整类型支持✅ 完整类型支持
性能相同相同

选择建议

  • 使用 Hook:需要状态管理、动态配置、依赖监听
  • 使用指令:简单场景、静态配置、模板驱动

最佳实践

1. 容器设置

确保滚动容器有正确的样式:

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

2. 性能优化

javascript
// 避免频繁创建配置对象
const config = {
  enableTransition: true,
  fadePercent: 15,
};

const { scrollState } = useScrollMask(scrollRef, config);

// 使用 computed 优化依赖监听
const dynamicConfig = computed(() => ({
  maskColor: theme.value.primary,
  enableTransition: !isMobile.value,
}));

3. 响应式设计

javascript
import { useMediaQuery } from "@vueuse/core";

const isMobile = useMediaQuery("(max-width: 768px)");

const { scrollState } = useScrollMask(scrollRef, {
  fadePercent: isMobile.value ? 20 : 15,
  shadowSize: isMobile.value ? 6 : 8,
  enableTransition: !isMobile.value, // 移动端禁用动画
});

4. 错误处理

javascript
const scrollRef = ref(null);

const { scrollState, updateMaskState } = useScrollMask(scrollRef, {
  enableTransition: true,
});

// 确保元素存在后再操作
import { onMounted } from "vue";

onMounted(() => {
  if (scrollRef.value) {
    // 元素已挂载,可以安全操作
    updateMaskState();
  }
});

注意事项

1. 元素引用

  • 确保 elementRef 在组件挂载后有值
  • 避免在 setup 阶段直接访问 elementRef.value

2. 配置更新

  • 使用响应式对象或 computed 进行动态配置
  • 避免在渲染过程中创建新的配置对象

3. 内存管理

  • Hook 会自动清理事件监听器和观察器
  • 无需手动清理,但要确保组件正常卸载

4. 样式冲突

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

故障排除

问题:scrollState 不更新

解决方案:

  1. 检查 elementRef 是否正确绑定
  2. 确保容器有滚动内容
  3. 验证容器的 overflow 属性

问题:遮罩效果不显示

解决方案:

  1. 检查浏览器是否支持 CSS mask 属性
  2. 确保容器有固定高度
  3. 验证内容是否超出容器

问题:依赖项不生效

解决方案:

  1. 确保依赖项是响应式的 Ref 对象
  2. 检查依赖项数组是否正确传递
  3. 使用 computed 包装复杂的依赖逻辑

TypeScript 支持

Hook 提供完整的 TypeScript 支持:

typescript
import { Ref } from "vue";
import {
  useScrollMask,
  UseScrollMaskOptions,
  ScrollState,
} from "@pt/common-ui";

// 类型安全的配置
const options: UseScrollMaskOptions = {
  fadePercent: 15,
  direction: "vertical",
  enableTransition: true,
};

// 类型推断
const scrollRef: Ref<HTMLElement | null> = ref(null);

const { scrollState, updateMaskState } = useScrollMask(scrollRef, options)

// 状态类型检查 if (scrollState.value === 'top') { // TypeScript 知道这是有效的状态值 }


## 实际应用场景

### 1. 聊天消息列表

```vue
<template>
  <div class="chat-container" ref="chatRef">
    <div class="messages">
      <div v-for="msg in messages" :key="msg.id" class="message">
        {{ msg.content }}
      </div>
    </div>
  </div>

  <!-- 滚动提示 -->
  <div v-if="scrollState !== 'bottom'" class="scroll-hint">
    有新消息 ↓
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'
import { useScrollMask } from '@pt/common-ui'

const chatRef = ref(null)
const messages = ref([])

const { scrollState } = useScrollMask(chatRef, {
  direction: 'vertical',
  enableTop: false,        // 不显示顶部遮罩,保持消息连贯性
  enableBottom: true,      // 显示底部遮罩,提示有更多消息
  enableTransition: true
})

// 新消息时自动滚动到底部
watch(messages, () => {
  if (chatRef.value) {
    chatRef.value.scrollTop = chatRef.value.scrollHeight
  }
}, { flush: 'post' })
</script>

2. 数据表格

vue
<template>
  <div class="table-container" ref="tableRef">
    <table class="data-table">
      <thead>
        <tr>
          <th v-for="col in columns" :key="col">{{ col }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="row in data" :key="row.id">
          <td v-for="col in columns" :key="col">{{ row[col] }}</td>
        </tr>
      </tbody>
    </table>
  </div>

  <!-- 滚动状态指示 -->
  <div class="table-status">
    <span v-if="scrollState === 'top'">📄 表格顶部</span>
    <span v-else-if="scrollState === 'bottom'">📋 表格底部</span>
    <span v-else-if="scrollState === 'middle-v'">📊 表格中间</span>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const tableRef = ref(null);

const { scrollState } = useScrollMask(tableRef, {
  direction: "both",
  fadePercent: 10,
  shadowColor: "rgba(0,0,0,0.1)",
});
</script>

3. 图片画廊

vue
<template>
  <div class="gallery" ref="galleryRef">
    <div class="gallery-track">
      <img
        v-for="img in images"
        :key="img.id"
        :src="img.url"
        :alt="img.alt"
        class="gallery-image"
      />
    </div>
  </div>

  <!-- 导航指示器 -->
  <div class="gallery-nav">
    <button :disabled="scrollState === 'left'" @click="scrollLeft">←</button>
    <span>{{ scrollState }}</span>
    <button :disabled="scrollState === 'right'" @click="scrollRight">→</button>
  </div>
</template>

<script setup>
import { ref } from "vue";
import { useScrollMask } from "@pt/common-ui";

const galleryRef = ref(null);

const { scrollState } = useScrollMask(galleryRef, {
  direction: "horizontal",
  enableLeft: true, // 显示左侧遮罩,提示可向左滚动
  enableRight: true, // 显示右侧遮罩,提示可向右滚动
  maskColor: "#000000",
  enableTransition: true,
  fadePercent: 25,
});

const scrollLeft = () => {
  galleryRef.value?.scrollBy({ left: -200, behavior: "smooth" });
};

const scrollRight = () => {
  galleryRef.value?.scrollBy({ left: 200, behavior: "smooth" });
};
</script>

📚 效果预览

useScrollMask 组合式函数
请打开代码查看

<template>
  <div class="composable-test-page">
    <h1 class="page-title">useScrollMask 组合式函数测试页面</h1>
    <p class="page-description">测试 useScrollMask hook 的各种配置选项和使用场景</p>

    <!-- 基础用法 -->
    <div class="test-section">
      <h2 class="section-title">1. 基础用法(默认配置)</h2>
      <div class="test-container">
        <div class="scroll-box" ref="basicScrollRef">
          <div class="content">
            <p v-for="i in 20" :key="i">
              这是第 {{ i }} 行内容,用于测试垂直滚动遮罩效果。当前状态:{{ basicScrollState }}
            </p>
          </div>
        </div>
      </div>
      <div class="state-info">
        <span class="state-label">滚动状态:</span>
        <span class="state-value" :class="`state-${basicScrollState}`">{{ basicScrollState }}</span>
      </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 blue-theme" ref="blueScrollRef">
            <div class="content">
              <p v-for="i in 15" :key="i">蓝色主题遮罩效果 - 第 {{ i }} 行</p>
            </div>
          </div>
          <div class="state-info">
            状态: <span class="state-value">{{ blueScrollState }}</span>
          </div>
        </div>

        <div class="test-item">
          <h3>红色遮罩</h3>
          <div class="scroll-box red-theme" ref="redScrollRef">
            <div class="content">
              <p v-for="i in 15" :key="i">红色主题遮罩效果 - 第 {{ i }} 行</p>
            </div>
          </div>
          <div class="state-info">
            状态: <span class="state-value">{{ redScrollState }}</span>
          </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" ref="noTransitionRef">
            <div class="content">
              <p v-for="i in 12" :key="i">无过渡动画 - 第 {{ i }} 行</p>
            </div>
          </div>
          <div class="state-info">
            状态: <span class="state-value">{{ noTransitionState }}</span>
          </div>
        </div>

        <div class="test-item">
          <h3>快速过渡</h3>
          <div class="scroll-box" ref="fastTransitionRef">
            <div class="content">
              <p v-for="i in 12" :key="i">快速过渡动画 - 第 {{ i }} 行</p>
            </div>
          </div>
          <div class="state-info">
            状态: <span class="state-value">{{ fastTransitionState }}</span>
          </div>
        </div>
      </div>
    </div>

    <!-- 水平滚动 -->
    <div class="test-section">
      <h2 class="section-title">4. 水平滚动测试</h2>
      <div class="test-container">
        <div class="scroll-box horizontal-scroll" ref="horizontalScrollRef">
          <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 class="state-info">
        <span class="state-label">水平滚动状态:</span>
        <span class="state-value" :class="`state-${horizontalScrollState}`">{{ horizontalScrollState }}</span>
      </div>
    </div>

    <!-- 双向滚动 -->
    <div class="test-section">
      <h2 class="section-title">5. 双向滚动测试</h2>
      <div class="test-container">
        <div class="scroll-box both-scroll" ref="bothScrollRef">
          <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 class="state-info">
        <span class="state-label">双向滚动状态:</span>
        <span class="state-value" :class="`state-${bothScrollState}`">{{ bothScrollState }}</span>
      </div>
    </div>

    <!-- 动态配置测试 -->
    <div class="test-section">
      <h2 class="section-title">6. 动态配置测试</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>
        <label>
          <input type="checkbox" v-model="dynamicConfig.hideScrollbar">
          隐藏滚动条
        </label>
        <button @click="manualUpdate" class="update-btn">手动更新</button>
      </div>
      <div class="test-container">
        <div class="scroll-box" ref="dynamicScrollRef">
          <div class="content">
            <p v-for="i in 15" :key="i">动态配置测试 - 第 {{ i }} 行内容</p>
          </div>
        </div>
      </div>
      <div class="state-info">
        <span class="state-label">动态配置状态:</span>
        <span class="state-value" :class="`state-${dynamicScrollState}`">{{ dynamicScrollState }}</span>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useScrollMask } from '@pt/common-ui'

// 模板引用
const basicScrollRef = ref<HTMLElement | null>(null)
const blueScrollRef = ref<HTMLElement | null>(null)
const redScrollRef = ref<HTMLElement | null>(null)
const noTransitionRef = ref<HTMLElement | null>(null)
const fastTransitionRef = ref<HTMLElement | null>(null)
const horizontalScrollRef = ref<HTMLElement | null>(null)
const bothScrollRef = ref<HTMLElement | null>(null)
const dynamicScrollRef = ref<HTMLElement | null>(null)

// 1. 基础用法
const { scrollState: basicScrollState } = useScrollMask(basicScrollRef)

// 2. 自定义颜色配置
const { scrollState: blueScrollState } = useScrollMask(blueScrollRef, {
  maskColor: '#3b82f6',
  backgroundColor: '#dbeafe'
})

const { scrollState: redScrollState } = useScrollMask(redScrollRef, {
  maskColor: '#ef4444',
  backgroundColor: '#fecaca'
})

// 3. 过渡动画配置
const { scrollState: noTransitionState } = useScrollMask(noTransitionRef, {
  enableTransition: false
})

const { scrollState: fastTransitionState } = useScrollMask(fastTransitionRef, {
  enableTransition: true,
  transitionDuration: '0.15s',
  transitionEasing: 'ease-out'
})

// 4. 水平滚动
const { scrollState: horizontalScrollState } = useScrollMask(horizontalScrollRef, {
  direction: 'horizontal',
  fadePercent: 20,
  shadowSize: 6
})

// 5. 双向滚动
const { scrollState: bothScrollState } = useScrollMask(bothScrollRef, {
  direction: 'both',
  fadePercent: 12,
  enableTransition: true,
  transitionDuration: '0.2s'
})

// 6. 动态配置
const dynamicConfig = reactive({
  maskColor: '#000000',
  fadePercent: 15,
  enableTransition: false,
  hideScrollbar: true
})

const { scrollState: dynamicScrollState, updateMaskState } = useScrollMask(dynamicScrollRef, dynamicConfig)

const manualUpdate = () => {
  updateMaskState()
}

onMounted(() => {
  // 确保所有滚动容器都有正确的样式
  const scrollBoxes = document.querySelectorAll('.scroll-box')
  scrollBoxes.forEach(box => {
    const element = box as HTMLElement
    if (!element.style.overflow) {
      element.style.overflow = 'auto'
    }
  })
})
</script>

<style scoped>
.composable-test-page {
  padding: 20px;
  max-width: 1200px;
  margin: 0 auto;
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

.page-title {
  color: #1f2937;
  margin-bottom: 8px;
  font-size: 2rem;
  font-weight: 700;
}

.page-description {
  color: #6b7280;
  margin-bottom: 32px;
  font-size: 1.1rem;
}

.test-section {
  margin-bottom: 40px;
  padding: 24px;
  border: 1px solid #e5e7eb;
  border-radius: 12px;
  background: #ffffff;
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}

.section-title {
  color: #374151;
  margin-bottom: 16px;
  font-size: 1.25rem;
  font-weight: 600;
  border-bottom: 2px solid #f3f4f6;
  padding-bottom: 8px;
}

.test-container {
  margin-bottom: 16px;
}

.test-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
  gap: 20px;
  margin-bottom: 16px;
}

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

.scroll-box {
  height: 200px;
  border: 2px solid #d1d5db;
  border-radius: 8px;
  padding: 16px;
  overflow: auto;
  background: #f9fafb;
  position: relative;
}

.scroll-box.blue-theme {
  border-color: #3b82f6;
  background: #eff6ff;
}

.scroll-box.red-theme {
  border-color: #ef4444;
  background: #fef2f2;
}

.scroll-box.horizontal-scroll {
  height: 120px;
  overflow-x: auto;
  overflow-y: hidden;
}

.scroll-box.both-scroll {
  height: 180px;
  overflow: auto;
}

.content p {
  margin: 8px 0;
  padding: 8px 12px;
  background: rgba(255, 255, 255, 0.8);
  border-radius: 4px;
  line-height: 1.5;
}

.horizontal-content {
  display: flex;
  gap: 16px;
  padding: 8px 0;
}

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

.both-content {
  min-width: 800px;
}

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

.both-cell {
  min-width: 80px;
  padding: 8px;
  margin-right: 4px;
  background: rgba(255, 255, 255, 0.9);
  border: 1px solid #e5e7eb;
  border-radius: 4px;
  text-align: center;
  font-size: 0.875rem;
}

.state-info {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: #f3f4f6;
  border-radius: 6px;
  font-size: 0.875rem;
}

.state-label {
  font-weight: 500;
  color: #374151;
}

.state-value {
  padding: 2px 8px;
  border-radius: 4px;
  font-weight: 600;
  font-family: 'Courier New', monospace;
}

.state-top { background: #dbeafe; color: #1e40af; }
.state-bottom { background: #dcfce7; color: #166534; }
.state-left { background: #fef3c7; color: #92400e; }
.state-right { background: #fce7f3; color: #be185d; }
.state-middle { background: #f3e8ff; color: #7c3aed; }
.state-middle-v { background: #ecfdf5; color: #059669; }
.state-middle-h { background: #fef2f2; color: #dc2626; }
.state-none { background: #f3f4f6; color: #6b7280; }

.controls {
  display: flex;
  flex-wrap: wrap;
  gap: 16px;
  margin-bottom: 20px;
  padding: 16px;
  background: #f8fafc;
  border-radius: 8px;
  border: 1px solid #e2e8f0;
}

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

.controls input[type="color"] {
  width: 40px;
  height: 32px;
  border: 1px solid #d1d5db;
  border-radius: 4px;
  cursor: pointer;
}

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

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

.update-btn {
  padding: 8px 16px;
  background: #3b82f6;
  color: white;
  border: none;
  border-radius: 6px;
  font-size: 0.875rem;
  font-weight: 500;
  cursor: pointer;
  transition: background-color 0.2s;
}

.update-btn:hover {
  background: #2563eb;
}

.update-btn:active {
  background: #1d4ed8;
}
</style>

相关链接

更新日志

v1.3.0

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

v1.2.0

  • ✨ 新增依赖项监听功能
  • ✨ 新增响应式状态管理
  • 🐛 修复双向滚动状态计算问题
  • 🔧 优化 TypeScript 类型定义

v1.1.0

  • ✨ 新增手动更新方法
  • ✨ 新增自定义颜色支持
  • 🔧 改进性能和内存管理

v1.0.0

  • 🎉 首次发布
  • ✨ 基础滚动遮罩功能
  • ✨ 多方向滚动支持
  • ✨ 完整的 TypeScript 支持

Released under the MIT License.