---
title: 设计文档
order: 2
category: pro
---

# PisellScrollView 设计文档

## 1. 概述

PisellScrollView 是一个功能强大的滚动视图组件，当内部内容超出容器时自动开启滚动。它提供了灵活的滚动配置能力、丰富的滚动控制选项和完善的事件系统，适用于各种复杂的滚动场景。

> **设计原则**：该组件专注于滚动功能的增强，不依赖 Ant Design，提供灵活的自定义能力，同时内置常用样式和功能供快速使用。

### 核心特性

1. **自动滚动检测**：自动根据内容进行开启横向或竖向滚动，或者禁止滚动
2. **多种滚动条样式**：内置多种滚动条效果，支持隐藏滚动条
3. **滚动控制按钮**：支持上下左右切换按钮，内置多种样式，支持自定义
4. **受控模式**：支持通过 scrollTop/Left、元素 ID、百分比等多种方式控制滚动位置
5. **丰富的事件系统**：滚动开始/进行/结束、到达边界等多种事件
6. **视图监听**：基于 IntersectionObserver，监听元素进入/离开视图

## 2. 核心功能

### 2.1 滚动方向控制 (overflow)

| 值       | 说明               | CSS 对应值                             |
| -------- | ------------------ | -------------------------------------- |
| `auto`   | 自动检测（默认）   | `overflow: auto`                       |
| `scroll` | 始终显示滚动条     | `overflow: scroll`                     |
| `hidden` | 禁止滚动           | `overflow: hidden`                     |
| `x`      | 仅横向滚动         | `overflow-x: auto; overflow-y: hidden` |
| `y`      | 仅竖向滚动         | `overflow-y: auto; overflow-x: hidden` |
| `both`   | 横向和竖向都可滚动 | `overflow: auto`                       |

### 2.2 滚动条样式 (scrollbarStyle)

内置多种滚动条样式供用户选择：

| 值        | 说明                     | 效果描述                       |
| --------- | ------------------------ | ------------------------------ |
| `default` | 浏览器原生滚动条（默认） | 使用浏览器默认样式             |
| `thin`    | 细滚动条                 | 8px 宽度，圆角样式             |
| `minimal` | 极简滚动条               | 4px 宽度，hover 时显示         |
| `rounded` | 圆角滚动条               | 12px 宽度，完全圆角            |
| `overlay` | 悬浮滚动条（macOS 风格） | 悬浮在内容上方，不占用空间     |
| `custom`  | 自定义样式               | 通过 scrollbarClassName 自定义 |

### 2.3 隐藏滚动条 (hideScrollbar)

| 值      | 说明                       |
| ------- | -------------------------- |
| `false` | 显示滚动条（默认）         |
| `true`  | 隐藏滚动条，但保持滚动功能 |

### 2.4 滚动控制按钮 (scrollButtons)

#### 注意：此功能我需要后续进行扩展！！！

#### 按钮配置

```
interface ScrollButtonsConfig {
  /** 是否显示滚动按钮 */
  show?: boolean;
  /** 按钮位置 */
  position?: 'start' | 'end' | 'both' | 'outside' | 'inside';
  /** 内置样式 */
  style?: 'default' | 'circle' | 'square' | 'minimal' | 'floating';
  /** 点击滚动距离（像素或百分比） */
  scrollDistance?: number | string;
  /** 自定义渲染 */
  render?: (
    direction: 'up' | 'down' | 'left' | 'right',
    disabled: boolean
  ) => ReactNode;
  /** 按钮位置偏移 */
  offset?: number | string;
  /** 是否自动隐藏（滚动到边界时） */
  autoHide?: boolean;
}
```

#### 内置按钮样式

| 样式       | 说明     | 效果描述               |
| ---------- | -------- | ---------------------- |
| `default`  | 默认样式 | 简单箭头按钮，边框圆角 |
| `circle`   | 圆形按钮 | 圆形背景，箭头居中     |
| `square`   | 方形按钮 | 方形背景，轻微圆角     |
| `minimal`  | 极简按钮 | 无背景，仅箭头图标     |
| `floating` | 悬浮按钮 | 半透明背景，悬浮效果   |

#### 按钮位置

| 值        | 说明             | 适用场景                           |
| --------- | ---------------- | ---------------------------------- |
| `start`   | 内容开始位置     | 竖向滚动显示在顶部，横向显示在左侧 |
| `end`     | 内容结束位置     | 竖向滚动显示在底部，横向显示在右侧 |
| `both`    | 两端都显示       | 同时显示上下或左右按钮             |
| `outside` | 容器外部         | 按钮在滚动容器外部                 |
| `inside`  | 容器内部（默认） | 按钮悬浮在内容上方                 |

### 2.5 受控模式 - 滚动位置控制

支持多种方式控制滚动位置：

#### 方式一：数值控制

```
interface ScrollPosition {
  /** 竖向滚动位置（像素） */
  scrollTop?: number;
  /** 横向滚动位置（像素） */
  scrollLeft?: number;
  /** 滚动行为 */
  behavior?: 'auto' | 'smooth';
}
```

#### 方式二：元素 ID 控制

```
interface ScrollToElementConfig {
  /** 目标元素 ID */
  targetId: string;
  /** 对齐方式 */
  align?: 'start' | 'center' | 'end' | 'nearest';
  /** 滚动行为 */
  behavior?: 'auto' | 'smooth';
  /** 偏移量 */
  offset?: number;
}
```

#### 方式三：百分比控制

```
interface ScrollPercentage {
  /** 竖向滚动百分比 (0-100) */
  percentY?: number;
  /** 横向滚动百分比 (0-100) */
  percentX?: number;
  /** 滚动行为 */
  behavior?: 'auto' | 'smooth';
}
```

### 2.6 滚动事件系统

#### 基础滚动事件

```
interface ScrollEventData {
  /** 当前滚动位置 */
  scrollTop: number;
  scrollLeft: number;
  /** 滚动百分比 */
  percentY: number;
  percentX: number;
  /** 可滚动高度/宽度 */
  scrollHeight: number;
  scrollWidth: number;
  /** 容器尺寸 */
  clientHeight: number;
  clientWidth: number;
  /** 滚动方向 */
  direction: 'up' | 'down' | 'left' | 'right' | null;
}
```

| 事件            | 说明           | 触发时机                   |
| --------------- | -------------- | -------------------------- |
| `onScroll`      | 滚动时触发     | 滚动过程中持续触发         |
| `onScrollStart` | 开始滚动时触发 | 第一次滚动时触发           |
| `onScrollEnd`   | 滚动结束时触发 | 滚动停止后延迟触发（防抖） |

#### 边界事件

| 事件            | 说明     | 触发时机           |
| --------------- | -------- | ------------------ |
| `onReachTop`    | 到达顶部 | 滚动到顶部时触发   |
| `onReachBottom` | 到达底部 | 滚动到底部时触发   |
| `onReachLeft`   | 到达左边 | 滚动到最左侧时触发 |
| `onReachRight`  | 到达右边 | 滚动到最右侧时触发 |

### 2.7 视图监听 (IntersectionObserver)

监听子元素进入/离开视口：

```
interface IntersectionConfig {
  /** 是否启用视图监听 */
  enabled?: boolean;
  /** 交叉阈值 (0-1) */
  threshold?: number | number[];
  /** 根边距 */
  rootMargin?: string;
  /** 元素进入视图回调 */
  onEnterViewport?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  /** 元素离开视图回调 */
  onLeaveViewport?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  /** 可见度变化回调 */
  onVisibilityChange?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  /** 需要监听的元素选择器 */
  targetSelector?: string;
}
```

#### 使用示例

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

export default () => (
  <PisellScrollView
    intersection={{
      enabled: true,
      threshold: [0, 0.5, 1],
      rootMargin: '0px 0px -100px 0px',
      targetSelector: '.scroll-item',
      onEnterViewport: (entry, element) => {
        console.log('元素进入视图', element);
      },
      onLeaveViewport: (entry, element) => {
        console.log('元素离开视图', element);
      },
    }}
  >
    <div className="scroll-item">Item 1</div>
    <div className="scroll-item">Item 2</div>
    <div className="scroll-item">Item 3</div>
  </PisellScrollView>
);
```

### 2.8 尺寸和样式

| 属性        | 类型               | 说明     |
| ----------- | ------------------ | -------- |
| `width`     | `number \| string` | 容器宽度 |
| `height`    | `number \| string` | 容器高度 |
| `maxWidth`  | `number \| string` | 最大宽度 |
| `maxHeight` | `number \| string` | 最大高度 |
| `minWidth`  | `number \| string` | 最小宽度 |
| `minHeight` | `number \| string` | 最小高度 |

## 3. Props 设计

```
/**
 * 滚动位置配置
 */
interface ScrollPosition {
  scrollTop?: number;
  scrollLeft?: number;
  behavior?: 'auto' | 'smooth';
}

/**
 * 滚动到元素配置
 */
interface ScrollToElementConfig {
  targetId: string;
  align?: 'start' | 'center' | 'end' | 'nearest';
  behavior?: 'auto' | 'smooth';
  offset?: number;
}

/**
 * 滚动百分比配置
 */
interface ScrollPercentage {
  percentY?: number;
  percentX?: number;
  behavior?: 'auto' | 'smooth';
}

/**
 * 滚动按钮配置
 */
interface ScrollButtonsConfig {
  show?: boolean;
  position?: 'start' | 'end' | 'both' | 'outside' | 'inside';
  style?: 'default' | 'circle' | 'square' | 'minimal' | 'floating';
  scrollDistance?: number | string;
  render?: (
    direction: 'up' | 'down' | 'left' | 'right',
    disabled: boolean
  ) => ReactNode;
  offset?: number | string;
  autoHide?: boolean;
}

/**
 * 滚动事件数据
 */
interface ScrollEventData {
  scrollTop: number;
  scrollLeft: number;
  percentY: number;
  percentX: number;
  scrollHeight: number;
  scrollWidth: number;
  clientHeight: number;
  clientWidth: number;
  direction: 'up' | 'down' | 'left' | 'right' | null;
}

/**
 * 视图监听配置
 */
interface IntersectionConfig {
  enabled?: boolean;
  threshold?: number | number[];
  rootMargin?: string;
  onEnterViewport?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  onLeaveViewport?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  onVisibilityChange?: (
    entry: IntersectionObserverEntry,
    element: Element
  ) => void;
  targetSelector?: string;
}

/**
 * PisellScrollView 组件 Props
 */
interface PisellScrollViewProps {
  /** 子元素 */
  children?: React.ReactNode;

  /** 自定义类名 */
  className?: string;

  /** 自定义样式 */
  style?: React.CSSProperties;

  /* ==================== 滚动方向控制 ==================== */

  /** 滚动方向 */
  overflow?: 'auto' | 'scroll' | 'hidden' | 'x' | 'y' | 'both';

  /* ==================== 滚动条样式 ==================== */

  /** 滚动条样式 */
  scrollbarStyle?:
    | 'default'
    | 'thin'
    | 'minimal'
    | 'rounded'
    | 'overlay'
    | 'custom';

  /** 是否隐藏滚动条 */
  hideScrollbar?: boolean;

  /** 自定义滚动条类名（当 scrollbarStyle 为 'custom' 时使用） */
  scrollbarClassName?: string;

  /* ==================== 滚动控制按钮 ==================== */

  /** 滚动按钮配置 */
  scrollButtons?: ScrollButtonsConfig;

  /* ==================== 受控模式 ==================== */

  /** 滚动位置（像素值） */
  scrollPosition?: ScrollPosition;

  /** 滚动到指定元素 */
  scrollToElement?: ScrollToElementConfig;

  /** 滚动百分比 (0-100) */
  scrollPercentage?: ScrollPercentage;

  /* ==================== 滚动事件 ==================== */

  /** 滚动时触发 */
  onScroll?: (data: ScrollEventData) => void;

  /** 开始滚动时触发 */
  onScrollStart?: (data: ScrollEventData) => void;

  /** 滚动结束时触发（防抖延迟，默认 150ms） */
  onScrollEnd?: (data: ScrollEventData) => void;

  /** 滚动结束防抖延迟时间（ms） */
  scrollEndDelay?: number;

  /** 到达顶部时触发 */
  onReachTop?: (data: ScrollEventData) => void;

  /** 到达底部时触发 */
  onReachBottom?: (data: ScrollEventData) => void;

  /** 到达左边时触发 */
  onReachLeft?: (data: ScrollEventData) => void;

  /** 到达右边时触发 */
  onReachRight?: (data: ScrollEventData) => void;

  /** 边界阈值（到达边界的像素阈值） */
  reachThreshold?: number;

  /* ==================== 视图监听 ==================== */

  /** IntersectionObserver 配置 */
  intersection?: IntersectionConfig;

  /* ==================== 尺寸 ==================== */

  /** 容器宽度 */
  width?: number | string;

  /** 容器高度 */
  height?: number | string;

  /** 最大宽度 */
  maxWidth?: number | string;

  /** 最大高度 */
  maxHeight?: number | string;

  /** 最小宽度 */
  minWidth?: number | string;

  /** 最小高度 */
  minHeight?: number | string;

  /* ==================== 其他 ==================== */

  /** 是否启用原生滚动优化 */
  useNativeScroll?: boolean;

  /** 是否启用虚拟滚动（预留） */
  virtual?: boolean;

  /** Ref 引用 */
  ref?: React.Ref<PisellScrollViewRef>;
}

/**
 * 组件暴露的方法
 */
interface PisellScrollViewRef {
  /** 滚动到指定位置 */
  scrollTo: (position: ScrollPosition) => void;

  /** 滚动到指定元素 */
  scrollToElement: (config: ScrollToElementConfig) => void;

  /** 滚动到百分比位置 */
  scrollToPercentage: (percentage: ScrollPercentage) => void;

  /** 滚动到顶部 */
  scrollToTop: (behavior?: 'auto' | 'smooth') => void;

  /** 滚动到底部 */
  scrollToBottom: (behavior?: 'auto' | 'smooth') => void;

  /** 滚动到最左侧 */
  scrollToLeft: (behavior?: 'auto' | 'smooth') => void;

  /** 滚动到最右侧 */
  scrollToRight: (behavior?: 'auto' | 'smooth') => void;

  /** 获取当前滚动信息 */
  getScrollData: () => ScrollEventData;

  /** 获取滚动容器 DOM 元素 */
  getScrollContainer: () => HTMLElement | null;
}
```

## 4. 使用场景

### 场景一：基础滚动容器

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

export default () => (
  <PisellScrollView height={400} overflow="y">
    <div>长内容...</div>
  </PisellScrollView>
);
```

### 场景二：自定义滚动条样式

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

export default () => (
  <PisellScrollView height={500} scrollbarStyle="thin" overflow="both">
    <div style={{ width: 2000, height: 2000 }}>大尺寸内容</div>
  </PisellScrollView>
);
```

### 场景三：带滚动按钮的列表

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

const items = [
  { id: '1', content: 'Item 1' },
  { id: '2', content: 'Item 2' },
  { id: '3', content: 'Item 3' },
];

export default () => (
  <PisellScrollView
    height={600}
    overflow="y"
    scrollButtons={{
      show: true,
      position: 'both',
      style: 'circle',
      scrollDistance: '100%',
      autoHide: true,
    }}
  >
    {items.map((item) => (
      <div key={item.id}>{item.content}</div>
    ))}
  </PisellScrollView>
);
```

### 场景四：受控滚动 - 数值控制

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

export default () => {
  const [scrollTop, setScrollTop] = useState(0);

  return (
    <PisellScrollView
      height={400}
      scrollPosition={{ scrollTop, behavior: 'smooth' }}
      onScroll={(data) => setScrollTop(data.scrollTop)}
    >
      <div>内容...</div>
    </PisellScrollView>
  );
};
```

### 场景五：滚动到指定元素

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

export default () => (
  <PisellScrollView
    height={500}
    scrollToElement={{
      targetId: 'section-3',
      align: 'center',
      behavior: 'smooth',
      offset: -50,
    }}
  >
    <div id="section-1">Section 1</div>
    <div id="section-2">Section 2</div>
    <div id="section-3">Section 3</div>
    <div id="section-4">Section 4</div>
  </PisellScrollView>
);
```

### 场景六：监听滚动事件

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

const loadMore = () => {
  /* 在此实现加载更多逻辑 */
};

export default () => (
  <PisellScrollView
    height={400}
    onScrollStart={(data) => console.log('开始滚动', data)}
    onScroll={(data) => console.log('滚动中', data.percentY + '%')}
    onScrollEnd={(data) => console.log('滚动结束', data)}
    onReachBottom={() => {
      console.log('到达底部，加载更多');
      loadMore();
    }}
    reachThreshold={50}
  >
    <div>内容...</div>
  </PisellScrollView>
);
```

### 场景七：视图监听（懒加载图片）

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

export default () => (
  <PisellScrollView
    height={800}
    intersection={{
      enabled: true,
      threshold: 0.1,
      targetSelector: '.lazy-image',
      onEnterViewport: (entry, element) => {
        const img = element as HTMLImageElement;
        const src = img.getAttribute('data-src');
        if (src) {
          img.src = src;
          img.classList.add('loaded');
        }
      },
    }}
  >
    <img className="lazy-image" data-src="image1.jpg" alt="" />
    <img className="lazy-image" data-src="image2.jpg" alt="" />
    <img className="lazy-image" data-src="image3.jpg" alt="" />
  </PisellScrollView>
);
```

### 场景八：自定义滚动按钮

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

export default () => (
  <PisellScrollView
    height={600}
    scrollButtons={{
      show: true,
      position: 'outside',
      render: (direction, disabled) => (
        <button disabled={disabled} style={{ opacity: disabled ? 0.3 : 1 }}>
          {direction === 'up' && '↑ 向上'}
          {direction === 'down' && '↓ 向下'}
        </button>
      ),
    }}
  >
    <div>内容...</div>
  </PisellScrollView>
);
```

### 场景九：使用 Ref 控制滚动

```tsx
import { useRef } from 'react';
import { PisellScrollView, type PisellScrollViewRef } from '@pisell/materials';

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

  const handleClick = () => {
    scrollRef.current?.scrollToBottom('smooth');
  };

  return (
    <>
      <button onClick={handleClick}>滚动到底部</button>
      <PisellScrollView ref={scrollRef} height={400}>
        <div>内容...</div>
      </PisellScrollView>
    </>
  );
};
```

### 场景十：横向滚动画廊

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

const images = [
  { id: '1', url: 'https://example.com/1.jpg' },
  { id: '2', url: 'https://example.com/2.jpg' },
  { id: '3', url: 'https://example.com/3.jpg' },
];

export default () => (
  <PisellScrollView
    width="100%"
    height={300}
    overflow="x"
    hideScrollbar
    scrollButtons={{
      show: true,
      style: 'floating',
      position: 'both',
      scrollDistance: 300,
    }}
  >
    <div style={{ display: 'flex', gap: 16 }}>
      {images.map((img) => (
        <img key={img.id} src={img.url} style={{ width: 250, height: 250 }} alt="" />
      ))}
    </div>
  </PisellScrollView>
);
```

### 场景十一：Ref 完整使用示例（推荐）

通过 ref 可以完全控制组件的滚动行为，这是最灵活的控制方式：

```tsx
import { useRef, useEffect } from 'react';
import { PisellScrollView, PisellScrollViewRef } from '@pisell/materials';

function MyComponent() {
  const scrollRef = useRef<PisellScrollViewRef>(null);

  // 1. 滚动到指定像素位置
  const handleScrollToPosition = () => {
    scrollRef.current?.scrollTo({
      scrollTop: 500,
      scrollLeft: 0,
      behavior: 'smooth',
    });
  };

  // 2. 滚动到指定元素
  const handleScrollToElement = () => {
    scrollRef.current?.scrollToElement({
      targetId: 'target-section',
      align: 'center', // 元素对齐到容器中心
      behavior: 'smooth',
      offset: -20, // 向上偏移 20px
    });
  };

  // 3. 滚动到百分比位置
  const handleScrollToPercent = () => {
    scrollRef.current?.scrollToPercentage({
      percentY: 50, // 滚动到 50% 位置
      behavior: 'smooth',
    });
  };

  // 4. 滚动到顶部/底部
  const handleScrollToTop = () => {
    scrollRef.current?.scrollToTop('smooth');
  };

  const handleScrollToBottom = () => {
    scrollRef.current?.scrollToBottom('smooth');
  };

  // 5. 获取当前滚动信息
  const handleGetScrollData = () => {
    const data = scrollRef.current?.getScrollData();
    console.log('当前滚动位置:', data?.scrollTop);
    console.log('滚动百分比:', data?.percentY);
    console.log('滚动方向:', data?.direction);
  };

  // 6. 获取滚动容器 DOM 元素（用于高级操作）
  const handleGetContainer = () => {
    const container = scrollRef.current?.getScrollContainer();
    if (container) {
      console.log('容器高度:', container.clientHeight);
      console.log('可滚动高度:', container.scrollHeight);
    }
  };

  // 7. 组件加载后自动滚动到指定位置
  useEffect(() => {
    // 延迟滚动，确保内容已渲染
    setTimeout(() => {
      scrollRef.current?.scrollToElement({
        targetId: 'highlighted-item',
        align: 'center',
        behavior: 'smooth',
      });
    }, 100);
  }, []);

  return (
    <div>
      {/* 控制按钮 */}
      <div
        style={{ marginBottom: 16, display: 'flex', gap: 8, flexWrap: 'wrap' }}
      >
        <button onClick={handleScrollToTop}>滚动到顶部</button>
        <button onClick={handleScrollToBottom}>滚动到底部</button>
        <button onClick={handleScrollToPosition}>滚动到 500px</button>
        <button onClick={handleScrollToElement}>滚动到目标元素</button>
        <button onClick={handleScrollToPercent}>滚动到 50%</button>
        <button onClick={handleGetScrollData}>获取滚动信息</button>
        <button onClick={handleGetContainer}>获取容器信息</button>
      </div>

      {/* 滚动容器 */}
      <PisellScrollView ref={scrollRef} height={400} overflow="y">
        <div style={{ padding: 20 }}>
          <div
            id="section-1"
            style={{ height: 300, background: '#f0f0f0', marginBottom: 16 }}
          >
            Section 1
          </div>
          <div
            id="section-2"
            style={{ height: 300, background: '#e0e0e0', marginBottom: 16 }}
          >
            Section 2
          </div>
          <div
            id="target-section"
            style={{ height: 300, background: '#d0d0d0', marginBottom: 16 }}
          >
            Target Section（点击按钮会滚动到这里）
          </div>
          <div
            id="highlighted-item"
            style={{ height: 300, background: '#c0c0c0', marginBottom: 16 }}
          >
            Highlighted Item（页面加载后会自动滚动到这里）
          </div>
          <div id="section-5" style={{ height: 300, background: '#b0b0b0' }}>
            Section 5
          </div>
        </div>
      </PisellScrollView>
    </div>
  );
}

export default MyComponent;
```

### 场景十二：结合状态管理的 Ref 使用

```tsx
import { useRef, useState } from 'react';
import { PisellScrollView, type PisellScrollViewRef } from '@pisell/materials';

function ScrollableList() {
  const scrollRef = useRef<PisellScrollViewRef>(null);
  const [activeSection, setActiveSection] = useState('section-1');

  // 导航到指定章节
  const navigateToSection = (sectionId: string) => {
    setActiveSection(sectionId);
    scrollRef.current?.scrollToElement({
      targetId: sectionId,
      align: 'start',
      behavior: 'smooth',
      offset: -60, // 预留顶部导航栏高度
    });
  };

  // 监听滚动，更新当前激活的章节
  const handleScroll = (_data: { percentY: number }) => {
    // 根据滚动位置判断当前在哪个章节
    // 这里可以实现类似目录高亮的效果
  };

  return (
    <div style={{ display: 'flex' }}>
      {/* 侧边栏导航 */}
      <div style={{ width: 200, padding: 16 }}>
        <h3>目录</h3>
        <div
          onClick={() => navigateToSection('section-1')}
          style={{
            padding: 8,
            cursor: 'pointer',
            background:
              activeSection === 'section-1' ? '#1890ff' : 'transparent',
            color: activeSection === 'section-1' ? '#fff' : '#000',
          }}
        >
          第一章
        </div>
        <div
          onClick={() => navigateToSection('section-2')}
          style={{
            padding: 8,
            cursor: 'pointer',
            background:
              activeSection === 'section-2' ? '#1890ff' : 'transparent',
            color: activeSection === 'section-2' ? '#fff' : '#000',
          }}
        >
          第二章
        </div>
        <div
          onClick={() => navigateToSection('section-3')}
          style={{
            padding: 8,
            cursor: 'pointer',
            background:
              activeSection === 'section-3' ? '#1890ff' : 'transparent',
            color: activeSection === 'section-3' ? '#fff' : '#000',
          }}
        >
          第三章
        </div>
      </div>

      {/* 内容区域 */}
      <PisellScrollView
        ref={scrollRef}
        height={600}
        overflow="y"
        onScroll={handleScroll}
        scrollbarStyle="thin"
      >
        <div id="section-1" style={{ padding: 40, minHeight: 400 }}>
          <h2>第一章内容</h2>
          <p>...</p>
        </div>
        <div id="section-2" style={{ padding: 40, minHeight: 400 }}>
          <h2>第二章内容</h2>
          <p>...</p>
        </div>
        <div id="section-3" style={{ padding: 40, minHeight: 400 }}>
          <h2>第三章内容</h2>
          <p>...</p>
        </div>
      </PisellScrollView>
    </div>
  );
}

export default ScrollableList;
```

### 场景十三：编程式控制滚动动画

```tsx
import { useRef, useState } from 'react';
import { PisellScrollView, type PisellScrollViewRef } from '@pisell/materials';

function AnimatedScroll() {
  const scrollRef = useRef<PisellScrollViewRef>(null);
  const [isAnimating, setIsAnimating] = useState(false);

  // 平滑滚动浏览所有内容（自动播放）
  const autoPlayScroll = async () => {
    if (!scrollRef.current || isAnimating) return;

    setIsAnimating(true);

    // 滚动到 25%
    scrollRef.current.scrollToPercentage({ percentY: 25, behavior: 'smooth' });
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // 滚动到 50%
    scrollRef.current.scrollToPercentage({ percentY: 50, behavior: 'smooth' });
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // 滚动到 75%
    scrollRef.current.scrollToPercentage({ percentY: 75, behavior: 'smooth' });
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // 滚动到底部
    scrollRef.current.scrollToBottom('smooth');
    await new Promise((resolve) => setTimeout(resolve, 2000));

    // 滚动回顶部
    scrollRef.current.scrollToTop('smooth');

    setIsAnimating(false);
  };

  return (
    <div>
      <button onClick={autoPlayScroll} disabled={isAnimating}>
        {isAnimating ? '播放中...' : '自动播放'}
      </button>

      <PisellScrollView ref={scrollRef} height={500} overflow="y">
        <div style={{ height: 2000, padding: 20 }}>长内容...</div>
      </PisellScrollView>
    </div>
  );
}

export default AnimatedScroll;
```

### 场景十四：与其他组件联动

```tsx
import { useRef } from 'react';
import { PisellScrollView, type PisellScrollViewRef } from '@pisell/materials';

function ScrollWithProgress() {
  const scrollRef = useRef<PisellScrollViewRef>(null);

  // 进度条点击，滚动到对应位置
  const handleProgressClick = (percent: number) => {
    scrollRef.current?.scrollToPercentage({
      percentY: percent,
      behavior: 'smooth',
    });
  };

  // 滚动时更新进度条
  const handleScroll = (data: { percentY: number }) => {
    // 更新进度条显示
    const progressBar = document.getElementById('progress-bar');
    if (progressBar) {
      progressBar.style.width = `${data.percentY}%`;
    }
  };

  return (
    <div>
      {/* 自定义进度条 */}
      <div
        style={{
          height: 4,
          background: '#f0f0f0',
          marginBottom: 16,
          cursor: 'pointer',
          position: 'relative',
        }}
        onClick={(e) => {
          const rect = e.currentTarget.getBoundingClientRect();
          const percent = ((e.clientX - rect.left) / rect.width) * 100;
          handleProgressClick(percent);
        }}
      >
        <div
          id="progress-bar"
          style={{
            height: '100%',
            background: '#1890ff',
            width: '0%',
            transition: 'width 0.2s',
          }}
        />
      </div>

      <PisellScrollView
        ref={scrollRef}
        height={400}
        overflow="y"
        onScroll={handleScroll}
      >
        <div style={{ height: 2000, padding: 20 }}>内容...</div>
      </PisellScrollView>
    </div>
  );
}

export default ScrollWithProgress;
```

## 4.1 Ref 方法详细说明

### 方法列表

| 方法                 | 参数                    | 返回值              | 说明                  |
| -------------------- | ----------------------- | ------------------- | --------------------- |
| `scrollTo`           | `ScrollPosition`        | `void`              | 滚动到指定像素位置    |
| `scrollToElement`    | `ScrollToElementConfig` | `void`              | 滚动到指定元素        |
| `scrollToPercentage` | `ScrollPercentage`      | `void`              | 滚动到百分比位置      |
| `scrollToTop`        | `behavior?`             | `void`              | 滚动到顶部            |
| `scrollToBottom`     | `behavior?`             | `void`              | 滚动到底部            |
| `scrollToLeft`       | `behavior?`             | `void`              | 滚动到最左侧          |
| `scrollToRight`      | `behavior?`             | `void`              | 滚动到最右侧          |
| `getScrollData`      | -                       | `ScrollEventData`   | 获取当前滚动信息      |
| `getScrollContainer` | -                       | `HTMLElement\|null` | 获取滚动容器 DOM 元素 |

### 使用要点

1. **Ref 的初始化**：使用 `useRef<PisellScrollViewRef>(null)` 创建 ref
2. **类型安全**：使用 TypeScript 时会有完整的类型提示
3. **空值检查**：调用方法前建议使用可选链操作符 `?.` 进行空值检查
4. **异步操作**：所有滚动方法都是同步的，但滚动动画是异步的
5. **DOM 就绪**：在组件挂载后（`useEffect`）或用户交互时调用 ref 方法

### Ref vs Props 的选择

| 控制方式       | 适用场景                 | 优势               | 劣势         |
| -------------- | ------------------------ | ------------------ | ------------ |
| **Ref 方式**   | 命令式控制、用户交互触发 | 灵活、即时、可编程 | 需要手动管理 |
| **Props 方式** | 声明式控制、状态驱动     | 响应式、自动同步   | 需要状态管理 |

**推荐做法**：

- 用户点击按钮等交互场景 → 使用 **Ref**
- 状态变化自动滚动 → 使用 **Props**
- 复杂联动场景 → **Ref + Props** 结合使用

## 5. 设计决策

### 5.1 为什么不依赖 Ant Design？

- **灵活性**：不受 Ant Design 版本限制，可以独立演进
- **体积优化**：减少不必要的依赖，降低打包体积
- **样式控制**：完全自主控制样式，避免样式冲突
- **通用性**：可以在任何 React 项目中使用

### 5.2 为什么提供多种滚动条样式？

- **场景多样**：不同场景对滚动条样式需求不同
- **快速开发**：内置常用样式，开箱即用
- **自定义能力**：支持完全自定义，满足特殊需求

### 5.3 滚动按钮的设计思路

- **灵活定位**：支持内部、外部、两端等多种位置
- **智能显隐**：到达边界时自动隐藏或禁用
- **可扩展性**：支持自定义渲染，满足个性化需求
- **易用性**：内置多种样式，快速集成

### 5.4 为什么支持三种受控方式？

| 控制方式 | 适用场景           | 优势                   |
| -------- | ------------------ | ---------------------- |
| 数值控制 | 精确控制滚动位置   | 精确、可计算           |
| 元素 ID  | 锚点导航、目录跳转 | 直观、易用             |
| 百分比   | 进度条、滑块控制   | 归一化、与容器尺寸无关 |

三种方式互补，覆盖所有常见场景。

### 5.5 事件系统设计

- **丰富完整**：覆盖滚动生命周期的所有阶段
- **性能优化**：滚动结束事件使用防抖，避免频繁触发
- **边界检测**：支持自定义阈值，提前触发加载更多等操作
- **数据完整**：提供完整的滚动状态数据，方便业务使用

### 5.6 IntersectionObserver 的优势

- **性能优秀**：原生 API，比滚动事件性能更好
- **配置灵活**：支持阈值、边距等多种配置
- **应用广泛**：懒加载、无限滚动、曝光统计等场景

## 6. 注意事项

1. **容器尺寸**：必须设置明确的 `width` 或 `height`，否则滚动功能无法正常工作
2. **受控模式优先级**：当同时设置多种受控方式时，优先级为：`scrollToElement` > `scrollPercentage` > `scrollPosition`
3. **滚动按钮显示条件**：只有当内容超出容器时，滚动按钮才会显示（除非设置 `autoHide: false`）
4. **IntersectionObserver 兼容性**：需要考虑浏览器兼容性，低版本浏览器需要 polyfill
5. **性能优化**：
   - 避免在 `onScroll` 中执行耗时操作
   - 大量元素时建议使用 `IntersectionObserver` 而不是滚动事件
   - 考虑使用虚拟滚动（预留功能）
6. **滚动条样式兼容性**：自定义滚动条样式在不同浏览器中表现可能不一致
7. **移动端适配**：移动端需要特别处理触摸滚动事件
8. **Ref 使用**：需要通过 `React.forwardRef` 暴露组件方法
9. **边界阈值**：`reachThreshold` 默认为 0，可根据需要调整，常用值为 50-100px
10. **滚动方向检测**：通过对比前后滚动位置判断方向，首次滚动时 `direction` 为 `null`

## 7. 后续扩展

### 7.1 虚拟滚动（预留）

对于大数据量列表，支持虚拟滚动以提升性能：

```
interface VirtualScrollConfig {
  enabled?: boolean;
  itemHeight?: number | ((index: number) => number);
  overscan?: number;
  estimatedItemHeight?: number;
}
```

### 7.2 滚动动画

支持自定义滚动动画效果：

```
interface ScrollAnimation {
  duration?: number;
  easing?: 'linear' | 'ease' | 'ease-in' | 'ease-out' | 'ease-in-out';
  custom?: (t: number) => number;
}
```

### 7.3 吸附滚动（Snap Scroll）

吸附滚动是一种增强用户体验的滚动方式，当用户滚动结束时，自动吸附到最近的锚点位置，常见于轮播图、卡片列表、全屏页面等场景。

#### 7.3.1 配置选项

```
/**
 * 吸附滚动配置
 */
interface SnapScrollConfig {
  /** 是否启用吸附滚动 */
  enabled?: boolean;

  /** 吸附类型 */
  type?: 'mandatory' | 'proximity';

  /** 吸附对齐方式 */
  align?: 'start' | 'center' | 'end';

  /** 吸附目标 */
  snapTo?: 'children' | 'custom';

  /** 自定义吸附点（像素值数组） */
  snapPoints?: number[];

  /** 吸附动画时长（ms） */
  duration?: number;

  /** 吸附缓动函数 */
  easing?:
    | 'linear'
    | 'ease'
    | 'ease-in'
    | 'ease-out'
    | 'ease-in-out'
    | 'cubic-bezier';

  /** 自定义缓动函数（当 easing 为 'cubic-bezier' 时） */
  customEasing?: [number, number, number, number];

  /** 吸附阈值（距离多近时触发吸附，像素） */
  threshold?: number;

  /** 是否在拖拽时实时吸附 */
  snapOnDrag?: boolean;

  /** 滚动速度阈值（超过此速度不吸附，用于快速滑动） */
  velocityThreshold?: number;

  /** 吸附方向 */
  direction?: 'x' | 'y' | 'both';

  /** 吸附开始回调 */
  onSnapStart?: (targetIndex: number, targetPosition: number) => void;

  /** 吸附结束回调 */
  onSnapEnd?: (currentIndex: number, currentPosition: number) => void;

  /** 自定义子元素选择器（当 snapTo 为 'children' 时） */
  childSelector?: string;
}
```

#### 7.3.2 吸附类型说明

| 类型        | 说明                         | 适用场景           |
| ----------- | ---------------------------- | ------------------ |
| `mandatory` | 强制吸附，必须停在吸附点上   | 轮播图、全屏页面   |
| `proximity` | 接近吸附，靠近吸附点时才吸附 | 卡片列表、商品列表 |

#### 7.3.3 吸附对齐方式

| 对齐方式 | 说明                   | 示例             |
| -------- | ---------------------- | ---------------- |
| `start`  | 元素对齐到容器起始位置 | 列表项对齐到顶部 |
| `center` | 元素对齐到容器中心     | 卡片居中显示     |
| `end`    | 元素对齐到容器结束位置 | 元素对齐到底部   |

#### 7.3.4 使用场景

##### 场景一：全屏页面吸附（强制模式）

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

export default () => (
  <PisellScrollView
    height="100vh"
    overflow="y"
    hideScrollbar
    snapScroll={{
      enabled: true,
      type: 'mandatory',
      align: 'start',
      snapTo: 'children',
      childSelector: '.page-section',
      duration: 500,
      easing: 'ease-out',
      onSnapEnd: (index) => {
        console.log('当前在第', index + 1, '页');
      },
    }}
  >
    <div
      className="page-section"
      style={{ height: '100vh', background: '#f0f0f0' }}
    >
      第一页
    </div>
    <div
      className="page-section"
      style={{ height: '100vh', background: '#e0e0e0' }}
    >
      第二页
    </div>
    <div
      className="page-section"
      style={{ height: '100vh', background: '#d0d0d0' }}
    >
      第三页
    </div>
  </PisellScrollView>
);
```

##### 场景二：横向卡片滚动（接近模式）

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

export default () => (
  <PisellScrollView
    width="100%"
    height={400}
    overflow="x"
    scrollbarStyle="minimal"
    snapScroll={{
      enabled: true,
      type: 'proximity',
      align: 'center',
      snapTo: 'children',
      childSelector: '.card-item',
      duration: 300,
      threshold: 50,
    }}
  >
    <div style={{ display: 'flex', gap: 20, padding: '0 100px' }}>
      <div
        className="card-item"
        style={{ minWidth: 300, height: 350, background: '#fff' }}
      >
        卡片 1
      </div>
      <div
        className="card-item"
        style={{ minWidth: 300, height: 350, background: '#fff' }}
      >
        卡片 2
      </div>
      <div
        className="card-item"
        style={{ minWidth: 300, height: 350, background: '#fff' }}
      >
        卡片 3
      </div>
    </div>
  </PisellScrollView>
);
```

##### 场景三：自定义吸附点

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

export default () => (
  <PisellScrollView
    height={600}
    overflow="y"
    snapScroll={{
      enabled: true,
      type: 'mandatory',
      align: 'start',
      snapTo: 'custom',
      snapPoints: [0, 200, 500, 800, 1200],
      duration: 400,
      easing: 'ease-in-out',
    }}
  >
    <div style={{ height: 1500, padding: 20 }}>
      {/* 内容会吸附到 0, 200, 500, 800, 1200px 位置 */}
    </div>
  </PisellScrollView>
);
```

##### 场景四：商品画廊（带速度检测）

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

const products = [
  { id: '1', name: '商品 A', price: '¥99', image: 'https://example.com/1.jpg' },
  { id: '2', name: '商品 B', price: '¥199', image: 'https://example.com/2.jpg' },
  { id: '3', name: '商品 C', price: '¥299', image: 'https://example.com/3.jpg' },
];

export default () => (
  <PisellScrollView
    width="100%"
    height={500}
    overflow="x"
    hideScrollbar
    snapScroll={{
      enabled: true,
      type: 'proximity',
      align: 'center',
      snapTo: 'children',
      childSelector: '.product-card',
      velocityThreshold: 1000,
      snapOnDrag: false,
      onSnapStart: (index) => {
        console.log('开始吸附到商品', index);
      },
      onSnapEnd: (index) => {
        console.log('当前查看商品', index);
      },
    }}
    scrollButtons={{
      show: true,
      style: 'circle',
      position: 'both',
    }}
  >
    <div style={{ display: 'flex', gap: 30, padding: '50px' }}>
      {products.map((product) => (
        <div key={product.id} className="product-card" style={{ minWidth: 400 }}>
          <img src={product.image} alt={product.name} />
          <h3>{product.name}</h3>
          <p>{product.price}</p>
        </div>
      ))}
    </div>
  </PisellScrollView>
);
```

##### 场景五：时间轴吸附

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

function TimelineScroll() {
  const [currentYear, setCurrentYear] = useState(2020);

  return (
    <PisellScrollView
      height={400}
      overflow="y"
      snapScroll={{
        enabled: true,
        type: 'mandatory',
        align: 'center',
        snapTo: 'children',
        childSelector: '.timeline-item',
        duration: 600,
        easing: 'ease-out',
        onSnapEnd: (index) => {
          const year = 2020 + index;
          setCurrentYear(year);
        },
      }}
    >
      <div style={{ padding: '200px 20px' }}>
        <div
          className="timeline-item"
          style={{ height: 300, marginBottom: 100 }}
        >
          <h2>2020年</h2>
          <p>重要事件...</p>
        </div>
        <div
          className="timeline-item"
          style={{ height: 300, marginBottom: 100 }}
        >
          <h2>2021年</h2>
          <p>重要事件...</p>
        </div>
        <div
          className="timeline-item"
          style={{ height: 300, marginBottom: 100 }}
        >
          <h2>2022年</h2>
          <p>重要事件...</p>
        </div>
      </div>
    </PisellScrollView>
  );
}

export default TimelineScroll;
```

##### 场景六：结合 Ref 的吸附控制

```tsx
import { useRef, useState } from 'react';
import { PisellScrollView, type PisellScrollViewRef } from '@pisell/materials';

function SnapScrollWithControl() {
  const scrollRef = useRef<PisellScrollViewRef>(null);
  const [currentIndex, setCurrentIndex] = useState(0);

  const jumpToSnap = (index: number) => {
    const snapPosition = index * 400;
    scrollRef.current?.scrollTo({
      scrollTop: snapPosition,
      behavior: 'smooth',
    });
  };

  return (
    <div>
      <div style={{ display: 'flex', gap: 8, marginBottom: 16 }}>
        {[0, 1, 2, 3].map((index) => (
          <button
            key={index}
            onClick={() => jumpToSnap(index)}
            style={{
              width: 10,
              height: 10,
              borderRadius: '50%',
              background: currentIndex === index ? '#1890ff' : '#d9d9d9',
              border: 'none',
              cursor: 'pointer',
            }}
          />
        ))}
      </div>

      <PisellScrollView
        ref={scrollRef}
        height={500}
        overflow="y"
        snapScroll={{
          enabled: true,
          type: 'mandatory',
          align: 'start',
          snapTo: 'custom',
          snapPoints: [0, 400, 800, 1200],
          onSnapEnd: (index) => {
            setCurrentIndex(index);
          },
        }}
      >
        <div style={{ height: 1600 }}>内容...</div>
      </PisellScrollView>
    </div>
  );
}

export default SnapScrollWithControl;
```

#### 7.3.5 实现方式

吸附滚动可以通过两种方式实现：

##### 方式一：CSS Scroll Snap（推荐）

利用原生 CSS Scroll Snap API，性能最优：

```css
.scroll-container {
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
}

.scroll-item {
  scroll-snap-align: start;
}
```

**优点**：

- 性能优秀，浏览器原生支持
- 无需额外 JavaScript 计算
- 触摸设备体验更好

**缺点**：

- 自定义能力有限
- 浏览器兼容性需要考虑（需要 polyfill）

##### 方式二：JavaScript 实现

通过监听滚动事件，计算吸附位置：

```
const handleScrollEnd = () => {
  const { scrollTop, clientHeight } = scrollContainer;
  const snapPoints = calculateSnapPoints();
  const nearestPoint = findNearestSnapPoint(scrollTop, snapPoints);

  animateScrollTo(nearestPoint, {
    duration: 500,
    easing: 'ease-out',
  });
};
```

**优点**：

- 完全可控，自定义能力强
- 可以实现复杂的吸附逻辑
- 兼容性好

**缺点**：

- 需要更多代码
- 性能相对较低
- 需要处理各种边界情况

**推荐策略**：优先使用 CSS Scroll Snap，在需要高度自定义时降级到 JavaScript 实现。

#### 7.3.6 Props 整合

将吸附滚动配置整合到主 Props 中：

```
interface PisellScrollViewProps {
  // ... 其他 props

  /** 吸附滚动配置 */
  snapScroll?: SnapScrollConfig;
}
```

#### 7.3.7 Ref 方法扩展

为支持吸附滚动，需要在 `PisellScrollViewRef` 中添加相关方法：

```
interface PisellScrollViewRef {
  // ... 现有方法

  /** 滚动到指定吸附点（索引） */
  scrollToSnapPoint: (index: number, behavior?: 'auto' | 'smooth') => void;

  /** 获取所有吸附点位置 */
  getSnapPoints: () => number[];

  /** 获取当前吸附点索引 */
  getCurrentSnapIndex: () => number;

  /** 滚动到下一个吸附点 */
  scrollToNextSnap: (behavior?: 'auto' | 'smooth') => void;

  /** 滚动到上一个吸附点 */
  scrollToPrevSnap: (behavior?: 'auto' | 'smooth') => void;
}
```

#### 7.3.8 注意事项

1. **性能优化**：

   - 吸附点数量不宜过多（建议 < 100 个）
   - 使用 CSS Scroll Snap 优先
   - 避免在吸附动画中执行耗时操作

2. **触摸设备**：

   - 移动端需要特别处理触摸惯性滚动
   - 考虑禁用浏览器默认的滚动行为

3. **可访问性**：

   - 提供键盘导航支持（上下左右键）
   - 提供屏幕阅读器提示

4. **用户体验**：

   - 快速滚动时不应频繁吸附
   - 提供视觉反馈（当前吸附点指示器）
   - 考虑添加滚动进度提示

5. **浏览器兼容性**：

   - CSS Scroll Snap 在旧版浏览器需要 polyfill
   - Safari 可能需要特殊处理

6. **与其他功能的冲突**：
   - 吸附滚动与虚拟滚动可能冲突
   - 需要考虑与滚动按钮的配合

#### 7.3.9 完整示例：轮播式产品展示

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

function ProductCarousel() {
  const scrollRef = useRef<PisellScrollViewRef>(null);
  const [currentIndex, setCurrentIndex] = useState(0);
  const totalProducts = 5;

  const goToPrev = () => {
    if (currentIndex > 0) {
      scrollRef.current?.scrollToPrevSnap('smooth');
    }
  };

  const goToNext = () => {
    if (currentIndex < totalProducts - 1) {
      scrollRef.current?.scrollToNextSnap('smooth');
    }
  };

  const goToIndex = (index: number) => {
    scrollRef.current?.scrollToSnapPoint(index, 'smooth');
  };

  return (
    <div style={{ position: 'relative' }}>
      {/* 控制按钮 */}
      <button
        onClick={goToPrev}
        disabled={currentIndex === 0}
        style={{
          position: 'absolute',
          left: 20,
          top: '50%',
          transform: 'translateY(-50%)',
          zIndex: 10,
        }}
      >
        上一个
      </button>

      <button
        onClick={goToNext}
        disabled={currentIndex === totalProducts - 1}
        style={{
          position: 'absolute',
          right: 20,
          top: '50%',
          transform: 'translateY(-50%)',
          zIndex: 10,
        }}
      >
        下一个
      </button>

      {/* 滚动容器 */}
      <PisellScrollView
        ref={scrollRef}
        width="100%"
        height={600}
        overflow="x"
        hideScrollbar
        snapScroll={{
          enabled: true,
          type: 'mandatory',
          align: 'center',
          snapTo: 'children',
          childSelector: '.product-slide',
          duration: 400,
          easing: 'ease-out',
          direction: 'x',
          onSnapEnd: (index) => {
            setCurrentIndex(index);
          },
        }}
      >
        <div style={{ display: 'flex', height: '100%' }}>
          {Array.from({ length: totalProducts }).map((_, index) => (
            <div
              key={index}
              className="product-slide"
              style={{
                minWidth: '100%',
                height: '100%',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                background: `hsl(${index * 60}, 70%, 80%)`,
              }}
            >
              <h2>产品 {index + 1}</h2>
            </div>
          ))}
        </div>
      </PisellScrollView>

      {/* 指示器 */}
      <div
        style={{
          position: 'absolute',
          bottom: 20,
          left: '50%',
          transform: 'translateX(-50%)',
          display: 'flex',
          gap: 8,
        }}
      >
        {Array.from({ length: totalProducts }).map((_, index) => (
          <div
            key={index}
            onClick={() => goToIndex(index)}
            style={{
              width: 12,
              height: 12,
              borderRadius: '50%',
              background:
                currentIndex === index ? '#1890ff' : 'rgba(255,255,255,0.5)',
              cursor: 'pointer',
              transition: 'all 0.3s',
            }}
          />
        ))}
      </div>
    </div>
  );
}

export default ProductCarousel;
```

### 7.4 滚动锁定

支持在特定条件下锁定滚动：

```
interface ScrollLockConfig {
  lockX?: boolean;
  lockY?: boolean;
  condition?: (data: ScrollEventData) => boolean;
}
```
