---
nav:
  title: 组件
  order: 1
group:
  title: Layout
  order: 1
title: PisellScrollView
order: 1
category: atom
---

# PisellScrollView 滚动容器

一个功能强大的增强型滚动容器组件，提供自动滚动检测、自定义滚动条样式、滚动控制按钮、受控滚动模式、丰富的滚动事件系统以及视图交叉检测能力。

## 何时使用

- 需要对滚动行为进行精确控制时
- 需要自定义滚动条样式，统一视觉风格
- 需要监听滚动事件（到达顶部/底部、滚动方向等）
- 需要为用户提供滚动按钮辅助导航
- 需要实现无限滚动、懒加载图片等功能
- 需要实现吸附滚动效果（轮播、全屏页面等）

## 代码演示

### 基础用法

最简单的滚动容器，内容超出时自动显示滚动条。

```tsx
import { PisellScrollView } from '@pisell/materials';

export default () => (
  <PisellScrollView height={400} overflow="y">
    <div style={{ padding: 20 }}>
      {Array.from({ length: 50 }).map((_, index) => (
        <div
          key={index}
          style={{ padding: '10px 0', borderBottom: '1px solid #f0f0f0' }}
        >
          列表项 {index + 1}
        </div>
      ))}
    </div>
  </PisellScrollView>
);
```

### 自定义滚动条样式

提供多种内置滚动条样式。

```tsx
import { PisellScrollView } from '@pisell/materials';
import { Space } from 'antd';

export default () => (
  <Space direction="vertical" style={{ width: '100%' }} size="large">
    <div>
      <h4>细滚动条 (thin)</h4>
      <PisellScrollView height={200} scrollbarStyle="thin" overflow="y">
        <div style={{ height: 600, padding: 20 }}>长内容...</div>
      </PisellScrollView>
    </div>

    <div>
      <h4>极简滚动条 (minimal)</h4>
      <PisellScrollView height={200} scrollbarStyle="minimal" overflow="y">
        <div style={{ height: 600, padding: 20 }}>长内容...</div>
      </PisellScrollView>
    </div>

    <div>
      <h4>圆角滚动条 (rounded)</h4>
      <PisellScrollView height={200} scrollbarStyle="rounded" overflow="y">
        <div style={{ height: 600, padding: 20 }}>长内容...</div>
      </PisellScrollView>
    </div>

    <div>
      <h4>隐藏滚动条 (none)</h4>
      <PisellScrollView height={200} scrollbarStyle="none" overflow="y">
        <div style={{ height: 600, padding: 20 }}>
          长内容（滚动条已隐藏）...
        </div>
      </PisellScrollView>
    </div>
  </Space>
);
```

### 滚动控制按钮

支持显示滚动按钮，辅助用户导航。

```tsx
import { PisellScrollView } from '@pisell/materials';

export default () => (
  <PisellScrollView
    height={400}
    overflow="y"
    scrollActionsConfig={{
      show: true,
      type: 'rArrows', // 右侧箭头按钮
      scrollDistance: 200, // 每次滚动 200px
    }}
  >
    <div style={{ padding: 20 }}>
      {Array.from({ length: 30 }).map((_, index) => (
        <div
          key={index}
          style={{ padding: '15px 0', borderBottom: '1px solid #f0f0f0' }}
        >
          内容项 {index + 1}
        </div>
      ))}
    </div>
  </PisellScrollView>
);
```

### 横向滚动

实现横向滚动容器。

```tsx
import { PisellScrollView } from '@pisell/materials';

export default () => (
  <PisellScrollView
    width="100%"
    height={300}
    overflow="x"
    scrollbarStyle="thin"
    scrollActionsConfig={{
      show: true,
      type: 'lrArrows', // 左右箭头
      scrollDistance: 300,
    }}
  >
    <div style={{ display: 'flex', gap: 16, padding: 20 }}>
      {Array.from({ length: 10 }).map((_, index) => (
        <div
          key={index}
          style={{
            minWidth: 250,
            height: 250,
            background: `hsl(${index * 36}, 70%, 80%)`,
            borderRadius: 8,
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center',
            fontSize: 24,
            fontWeight: 'bold',
          }}
        >
          卡片 {index + 1}
        </div>
      ))}
    </div>
  </PisellScrollView>
);
```

### 监听滚动事件

监听滚动事件，实现更多互动效果。

```tsx
import { PisellScrollView } from '@pisell/materials';
import { message } from 'antd';
import { useState } from 'react';

export default () => {
  const [scrollInfo, setScrollInfo] = useState({ scrollTop: 0, percentY: 0 });

  return (
    <div>
      <div
        style={{
          marginBottom: 16,
          padding: 16,
          background: '#f5f5f5',
          borderRadius: 4,
        }}
      >
        <p>滚动位置: {Math.round(scrollInfo.scrollTop)}px</p>
        <p>滚动百分比: {Math.round(scrollInfo.percentY)}%</p>
      </div>

      <PisellScrollView
        height={400}
        overflow="y"
        onScroll={(data) => {
          setScrollInfo({
            scrollTop: data.scrollTop,
            percentY: data.percentY,
          });
        }}
        onReachTop={() => message.info('已到达顶部')}
        onReachBottom={() => message.success('已到达底部，加载更多...')}
        reachThreshold={50} // 距离底部 50px 时触发
      >
        <div style={{ padding: 20 }}>
          {Array.from({ length: 50 }).map((_, index) => (
            <div
              key={index}
              style={{ padding: '10px 0', borderBottom: '1px solid #f0f0f0' }}
            >
              列表项 {index + 1}
            </div>
          ))}
        </div>
      </PisellScrollView>
    </div>
  );
};
```

### 使用 Ref 控制滚动

通过 Ref 方法命令式控制滚动位置。

```tsx
import { PisellScrollView } from '@pisell/materials';
import { Button, Space } from 'antd';
import { useRef } from 'react';

export default () => {
  const scrollRef = useRef(null);

  return (
    <div>
      <Space style={{ marginBottom: 16 }}>
        <Button onClick={() => scrollRef.current?.scrollToTop('smooth')}>
          滚动到顶部
        </Button>
        <Button onClick={() => scrollRef.current?.scrollToBottom('smooth')}>
          滚动到底部
        </Button>
        <Button
          onClick={() =>
            scrollRef.current?.scrollToPercentage({
              percentY: 50,
              behavior: 'smooth',
            })
          }
        >
          滚动到 50%
        </Button>
        <Button
          onClick={() =>
            scrollRef.current?.scrollToElement({
              targetId: 'item-25',
              align: 'center',
              behavior: 'smooth',
            })
          }
        >
          滚动到第 25 项
        </Button>
      </Space>

      <PisellScrollView ref={scrollRef} height={400} overflow="y">
        <div style={{ padding: 20 }}>
          {Array.from({ length: 50 }).map((_, index) => (
            <div
              key={index}
              id={`item-${index}`}
              style={{
                padding: '10px 0',
                borderBottom: '1px solid #f0f0f0',
                background: index === 25 ? '#e6f7ff' : 'transparent',
              }}
            >
              列表项 {index}
            </div>
          ))}
        </div>
      </PisellScrollView>
    </div>
  );
};
```

### 吸附滚动

实现吸附滚动效果，常用于轮播、全屏页面等场景。

```tsx
import { PisellScrollView } from '@pisell/materials';
import { useState } from 'react';

export default () => {
  const [currentIndex, setCurrentIndex] = useState(0);

  return (
    <div>
      <div style={{ marginBottom: 16, textAlign: 'center' }}>
        当前页: {currentIndex + 1} / 4
      </div>

      <PisellScrollView
        height={500}
        overflow="y"
        scrollbarStyle="none"
        snapScroll={{
          enabled: true,
          type: 'mandatory', // 强制吸附
          onSnapEnd: (index) => setCurrentIndex(index),
        }}
      >
        {Array.from({ length: 4 }).map((_, index) => (
          <div
            key={index}
            style={{
              height: 500,
              display: 'flex',
              alignItems: 'center',
              justifyContent: 'center',
              background: `hsl(${index * 60}, 70%, 80%)`,
              fontSize: 48,
              fontWeight: 'bold',
              scrollSnapAlign: 'start', // 重要：为子元素添加吸附对齐
            }}
          >
            页面 {index + 1}
          </div>
        ))}
      </PisellScrollView>
    </div>
  );
};
```

### 视图监听 - 懒加载图片

使用 IntersectionObserver 实现图片懒加载。

```tsx
import { PisellScrollView } from '@pisell/materials';

export default () => (
  <PisellScrollView
    height={600}
    overflow="y"
    intersection={{
      enabled: true,
      threshold: 0.1, // 元素露出 10% 时触发
      targetSelector: '.lazy-image',
      onEnterViewport: (entry, element) => {
        const img = element as HTMLImageElement;
        const src = img.getAttribute('data-src');
        if (src && !img.src) {
          img.src = src;
          img.style.opacity = '1';
        }
      },
    }}
  >
    <div style={{ padding: 20 }}>
      {Array.from({ length: 10 }).map((_, index) => (
        <div key={index} style={{ marginBottom: 20 }}>
          <h3>图片 {index + 1}</h3>
          <img
            className="lazy-image"
            data-src={`https://picsum.photos/600/400?random=${index}`}
            alt={`图片 ${index + 1}`}
            style={{
              width: '100%',
              height: 400,
              objectFit: 'cover',
              background: '#f0f0f0',
              opacity: 0,
              transition: 'opacity 0.3s',
            }}
          />
        </div>
      ))}
    </div>
  </PisellScrollView>
);
```

## API

### PisellScrollViewProps

| 参数                | 说明                                                     | 类型                                                                               | 默认值                                 |
| ------------------- | -------------------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------- |
| children            | 子元素内容                                               | `ReactNode`                                                                        | -                                      |
| className           | 自定义类名                                               | `string`                                                                           | -                                      |
| style               | 自定义样式                                               | `CSSProperties`                                                                    | -                                      |
| overflow            | 滚动方向                                                 | `'auto' \| 'scroll' \| 'hidden' \| 'x' \| 'y' \| 'both'`                           | `'auto'`                               |
| scrollbarStyle      | 滚动条样式                                               | `'default' \| 'thin' \| 'minimal' \| 'rounded' \| 'overlay' \| 'custom' \| 'none'` | `'default'`                            |
| scrollbarClassName  | 自定义滚动条类名（当 scrollbarStyle 为 'custom' 时使用） | `string`                                                                           | -                                      |
| scrollActionsConfig | 滚动按钮配置                                             | `ScrollActionsConfig`                                                              | `{ show: true, type: 'rArrows' }`      |
| snapScroll          | 吸附滚动配置                                             | `SnapScrollConfig`                                                                 | `{ enabled: true, type: 'mandatory' }` |
| width               | 容器宽度                                                 | `number \| string`                                                                 | -                                      |
| height              | 容器高度                                                 | `number \| string`                                                                 | -                                      |
| maxWidth            | 最大宽度                                                 | `number \| string`                                                                 | -                                      |
| maxHeight           | 最大高度                                                 | `number \| string`                                                                 | -                                      |
| minWidth            | 最小宽度                                                 | `number \| string`                                                                 | -                                      |
| minHeight           | 最小高度                                                 | `number \| string`                                                                 | -                                      |
| onScroll            | 滚动时触发                                               | `(data: ScrollEventData) => void`                                                  | -                                      |
| onScrollStart       | 开始滚动时触发                                           | `(data: ScrollEventData) => void`                                                  | -                                      |
| onScrollEnd         | 滚动结束时触发（防抖）                                   | `(data: ScrollEventData) => void`                                                  | -                                      |
| scrollEndDelay      | 滚动结束防抖延迟时间（ms）                               | `number`                                                                           | `150`                                  |
| onReachTop          | 到达顶部时触发                                           | `(data: ScrollEventData) => void`                                                  | -                                      |
| onReachBottom       | 到达底部时触发                                           | `(data: ScrollEventData) => void`                                                  | -                                      |
| onReachLeft         | 到达左边时触发                                           | `(data: ScrollEventData) => void`                                                  | -                                      |
| onReachRight        | 到达右边时触发                                           | `(data: ScrollEventData) => void`                                                  | -                                      |
| reachThreshold      | 边界阈值（到达边界的像素阈值）                           | `number`                                                                           | `0`                                    |
| intersection        | IntersectionObserver 配置                                | `IntersectionConfig`                                                               | -                                      |

### ScrollActionsConfig

滚动按钮配置对象。

| 参数           | 说明                             | 类型                                                             | 默认值      |
| -------------- | -------------------------------- | ---------------------------------------------------------------- | ----------- |
| show           | 是否显示按钮                     | `boolean`                                                        | `true`      |
| type           | 按钮类型                         | `'backTop' \| 'rArrows' \| 'lrArrows' \| 'tbArrows' \| 'indicator' \| 'custom'` | `'rArrows'` |
| style          | 按钮自定义样式                   | `CSSProperties`                                                  | -           |
| scrollDistance | 每次滚动距离（像素）             | `number`                                                         | `300`       |
| config         | 按钮配置（根据 type 不同而不同） | `any`                                                            | -           |

### SnapScrollConfig

吸附滚动配置对象。

| 参数        | 说明             | 类型                                                      | 默认值        |
| ----------- | ---------------- | --------------------------------------------------------- | ------------- |
| enabled     | 是否启用吸附滚动 | `boolean`                                                 | `false`       |
| type        | 吸附类型         | `'mandatory' \| 'proximity'`                              | `'mandatory'` |
| align       | 吸附对齐方式     | `'start' \| 'center' \| 'end'`                            | `'start'`     |
| onSnapStart | 吸附开始回调     | `(targetIndex: number, targetPosition: number) => void`   | -             |
| onSnapEnd   | 吸附结束回调     | `(currentIndex: number, currentPosition: number) => void` | -             |

### ScrollEventData

滚动事件数据对象。

| 参数             | 说明             | 类型                                          |
| ---------------- | ---------------- | --------------------------------------------- |
| scrollTop        | 当前竖向滚动位置 | `number`                                      |
| scrollLeft       | 当前横向滚动位置 | `number`                                      |
| percentY         | 竖向滚动百分比   | `number`                                      |
| percentX         | 横向滚动百分比   | `number`                                      |
| scrollHeight     | 可滚动高度       | `number`                                      |
| scrollWidth      | 可滚动宽度       | `number`                                      |
| clientHeight     | 容器可视高度     | `number`                                      |
| clientWidth      | 容器可视宽度     | `number`                                      |
| direction        | 滚动方向         | `'up' \| 'down' \| 'left' \| 'right' \| null` |
| isScrollToTop    | 是否在顶部       | `boolean`                                     |
| isScrollToBottom | 是否在底部       | `boolean`                                     |
| isScrollToLeft   | 是否在最左侧     | `boolean`                                     |
| isScrollToRight  | 是否在最右侧     | `boolean`                                     |
| isOverflows      | 是否已产生滚动条 | `boolean`                                     |

### IntersectionConfig

视图监听配置对象。

| 参数               | 说明                 | 类型                                                           | 默认值  |
| ------------------ | -------------------- | -------------------------------------------------------------- | ------- |
| enabled            | 是否启用视图监听     | `boolean`                                                      | `false` |
| threshold          | 交叉阈值 (0-1)       | `number \| number[]`                                           | `0`     |
| rootMargin         | 根边距               | `string`                                                       | `'0px'` |
| targetSelector     | 需要监听的元素选择器 | `string`                                                       | -       |
| onEnterViewport    | 元素进入视图回调     | `(entry: IntersectionObserverEntry, element: Element) => void` | -       |
| onLeaveViewport    | 元素离开视图回调     | `(entry: IntersectionObserverEntry, element: Element) => void` | -       |
| onVisibilityChange | 可见度变化回调       | `(entry: IntersectionObserverEntry, element: Element) => void` | -       |

### Ref 方法

通过 ref 可以调用以下方法：

| 方法                | 说明                  | 参数                                                     | 返回值                |
| ------------------- | --------------------- | -------------------------------------------------------- | --------------------- |
| scrollTo            | 滚动到指定位置        | `(position: ScrollPosition) => void`                     | `void`                |
| scrollToElement     | 滚动到指定元素        | `(config: ScrollToElementConfig) => void`                | `void`                |
| scrollToPercentage  | 滚动到百分比位置      | `(percentage: ScrollPercentage) => void`                 | `void`                |
| scrollToTop         | 滚动到顶部            | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |
| scrollToBottom      | 滚动到底部            | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |
| scrollToLeft        | 滚动到最左侧          | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |
| scrollToRight       | 滚动到最右侧          | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |
| getScrollData       | 获取当前滚动信息      | `() => ScrollEventData`                                  | `ScrollEventData`     |
| getScrollContainer  | 获取滚动容器 DOM 元素 | `() => HTMLElement \| null`                              | `HTMLElement \| null` |
| scrollToSnapPoint   | 滚动到指定吸附点      | `(index: number, behavior?: 'auto' \| 'smooth') => void` | `void`                |
| getSnapPoints       | 获取所有吸附点位置    | `() => number[]`                                         | `number[]`            |
| getCurrentSnapIndex | 获取当前吸附点索引    | `() => number`                                           | `number`              |
| scrollToNextSnap    | 滚动到下一个吸附点    | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |
| scrollToPrevSnap    | 滚动到上一个吸附点    | `(behavior?: 'auto' \| 'smooth') => void`                | `void`                |

#### ScrollPosition

| 参数       | 说明                 | 类型                 |
| ---------- | -------------------- | -------------------- |
| scrollTop  | 竖向滚动位置（像素） | `number`             |
| scrollLeft | 横向滚动位置（像素） | `number`             |
| behavior   | 滚动行为             | `'auto' \| 'smooth'` |

#### ScrollToElementConfig

| 参数     | 说明        | 类型                                        | 默认值    |
| -------- | ----------- | ------------------------------------------- | --------- |
| targetId | 目标元素 ID | `string`                                    | -         |
| align    | 对齐方式    | `'start' \| 'center' \| 'end' \| 'nearest'` | `'start'` |
| behavior | 滚动行为    | `'auto' \| 'smooth'`                        | `'auto'`  |
| offset   | 偏移量      | `number`                                    | `0`       |

#### ScrollPercentage

| 参数     | 说明                   | 类型                 |
| -------- | ---------------------- | -------------------- |
| percentY | 竖向滚动百分比 (0-100) | `number`             |
| percentX | 横向滚动百分比 (0-100) | `number`             |
| behavior | 滚动行为               | `'auto' \| 'smooth'` |

## 注意事项

1. **容器尺寸**：必须设置明确的 `width` 或 `height`，否则滚动功能无法正常工作。

2. **滚动条样式兼容性**：自定义滚动条样式主要基于 Webkit 内核浏览器，Firefox 和其他浏览器的表现可能有差异。

3. **吸附滚动**：使用吸附滚动时，需要为子元素添加 `scroll-snap-align` CSS 属性来指定吸附对齐方式。

4. **性能优化**：

   - 避免在 `onScroll` 回调中执行耗时操作
   - 大量元素监听时优先使用 `IntersectionObserver`
   - 滚动结束事件已内置防抖，默认延迟 150ms

5. **边界检测**：`reachThreshold` 用于设置提前触发边界事件的阈值，常用于实现"加载更多"功能。

6. **Ref 调用时机**：通过 ref 调用方法时，建议在组件挂载后（`useEffect`）或用户交互时调用，避免在渲染阶段调用。

7. **移动端适配**：组件在移动端会自动使用原生滚动行为，但某些样式可能需要额外适配。
