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>方向控制选项说明
| 选项 | 类型 | 默认值 | 描述 |
|---|---|---|---|
enableTop | boolean | true | 是否启用顶部遮罩 |
enableBottom | boolean | true | 是否启用底部遮罩 |
enableRight | boolean | true | 是否启用右侧遮罩 |
enableLeft | boolean | true | 是否启用左侧遮罩 |
高级用法
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 Hook | v-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-image和box-shadow属性 - 避免在 CSS 中覆盖这些属性
故障排除
问题:scrollState 不更新
解决方案:
- 检查
elementRef是否正确绑定 - 确保容器有滚动内容
- 验证容器的
overflow属性
问题:遮罩效果不显示
解决方案:
- 检查浏览器是否支持 CSS
mask属性 - 确保容器有固定高度
- 验证内容是否超出容器
问题:依赖项不生效
解决方案:
- 确保依赖项是响应式的
Ref对象 - 检查依赖项数组是否正确传递
- 使用
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>相关链接
- v-scroll-mask 指令文档 - 指令版本
- 在线演示 - 查看实际效果
- GitHub 仓库 - 源码地址
更新日志
v1.3.0
- ✨ 新增细粒度方向控制功能
- ✨ 支持独立控制各方向遮罩显示
- ✨ 新增动态方向控制示例
- 🔧 优化双向滚动组合逻辑
v1.2.0
- ✨ 新增依赖项监听功能
- ✨ 新增响应式状态管理
- 🐛 修复双向滚动状态计算问题
- 🔧 优化 TypeScript 类型定义
v1.1.0
- ✨ 新增手动更新方法
- ✨ 新增自定义颜色支持
- 🔧 改进性能和内存管理
v1.0.0
- 🎉 首次发布
- ✨ 基础滚动遮罩功能
- ✨ 多方向滚动支持
- ✨ 完整的 TypeScript 支持