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

﻿# PisellSingleSelect 单选下拉组件设计文档

> **组件概述**: PisellSingleSelect（单选下拉组件）是Pisell-2.0组件库中的核心表单组件，基于Ant
> Design的Select组件进行二次封装，提供符合Pisell设计规范的单选功能。

## 设计稿链接

**Figma 设计稿
**: https://www.figma.com/design/pM8Ho6d7kCMv9vIBvFHUlj/Pisell-2.0-Component-Library?node-id=3192-584&m=dev

## 第一部分：组件概述

### 1. 组件定位与价值

PisellSingleSelect在Pisell-2.0组件库中定位为**Pro Components**下的**General**分类，是表单单选输入场景的基础组件。其核心价值在于：

* **技术统一**：基于Ant Design Select组件封装，确保与设计系统的一致性。
* **样式定制**：通过Pisell Token系统（`packages/materials/src/theme/tokens.ts`）定制样式，符合Pisell设计规范。
* **易用性**：提供简洁的API，降低使用成本。
* **复用性**：核心逻辑抽取到`@pisell/utils`，可在多个组件间复用。

### 2. 核心特性总结

1. **基础单选功能**：提供标准的下拉单选功能。
2. **完整的状态管理**：全面支持`disabled`（禁用态）、`readOnly`（只读态）、`error`（错误态）及`focused`（焦点态）。
3. **搜索与过滤**：内置搜索功能（`showSearch`），支持自定义过滤逻辑，并集成防抖优化。
4. **字段筛选与排序**：遵循统一的组件规范，可与筛选、排序控件无缝集成。
5. **样式定制**：支持通过Less自定义样式，优先使用Token配置。

### 3. 设计原则

* **基于Ant Design**：充分利用Ant Design Select组件的能力，避免重复造轮子。
* **单一职责**：组件只负责单选功能，保持简洁。
* **类型安全**：深度利用TypeScript，确保编译时就能发现类型错误。
* **性能优先**：对搜索等高频操作进行防抖优化。
* **可访问性**：遵循WAI-ARIA标准，提供完整的键盘导航支持和屏幕阅读器兼容性。

## 第二部分：详细API文档

### 1. 完整的Props接口定义

```
import { ReactNode } from 'react';
import { SelectProps as AntSelectProps } from 'antd';

interface PisellSingleSelectProps<T = string | number> extends Omit<AntSelectProps, 'mode' | 'value' | 'onChange'> {
  // 核心数据
  value?: T | null;
  defaultValue?: T;
  onChange?: (value: T | null, option: SelectOption<T> | null) => void;
  options: SelectOption<T>[];

  // 交互控制
  placeholder?: string;
  showSearch?: boolean;
  allowClear?: boolean;
  disabled?: boolean;
  readOnly?: boolean;

  // 样式与布局
  size?: 'small' | 'medium' | 'large';
  className?: string;
  style?: React.CSSProperties;

  // 表单标签与校验
  label?: string;
  helperText?: string;
  required?: boolean;
  error?: boolean;
  errorText?: string;

  // 筛选与排序
  filterable?: boolean;
  filterOption?: (input: string, option: SelectOption<T>) => boolean;
  sortable?: boolean;
  sortDirection?: 'asc' | 'desc';

  // 事件回调
  onSearch?: (value: string) => void;
  onFocus?: (event: React.FocusEvent) => void;
  onBlur?: (event: React.FocusEvent) => void;
  onDropdownVisibleChange?: (visible: boolean) => void;
}
```

### 2. 类型定义详解

```
// 选项基础类型
type SelectOption<T = string | number> = {
  value: T;
  label: string;
  disabled?: boolean;
  icon?: ReactNode;
};

// 组件实例引用方法
interface SelectRef {
  focus: () => void;
  blur: () => void;
  getValue: () => T | null;
  setValue: (value: T | null) => void;
  clearValue: () => void;
}
```

### 3. 使用示例

#### 基础单选

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

const fruitOptions = [
  { value: 'apple', label: '苹果' },
  { value: 'banana', label: '香蕉' },
  { value: 'orange', label: '橙子' },
];

function SingleSelectDemo() {
  const [fruit, setFruit] = useState<string>();
  
  return (
    <PisellSingleSelect
      value={fruit}
      onChange={setFruit}
      options={fruitOptions}
      placeholder="请选择一种水果"
      label="最爱水果"
      required
      size="medium"
    />
  );
}
```

#### 带搜索功能

```
<PisellSingleSelect
  options={largeCityOptions} // 假设有几百个城市选项
  showSearch
  onSearch={(keyword) => console.log('搜索关键词:', keyword)}
  filterOption={(input, option) =>
    option.label.toLowerCase().includes(input.toLowerCase()) ||
    option.value.toString().includes(input)
  }
  placeholder="搜索城市..."
/>
```

#### 禁用与错误状态

```
// 禁用状态
<PisellSingleSelect 
  options={options} 
  disabled 
  value="fixedValue" 
/>

// 错误状态
<PisellSingleSelect
  options={options}
  error
  errorText="此项为必填项，请选择"
  required
/>
```

## 第三部分：实现架构

### 1. 技术栈

- **基础组件**：Ant Design Select
- **样式方案**：Less + Ant Design Token 系统
- **状态管理**：自定义 Hook（从 `@pisell/utils` 导入）
- **类型系统**：TypeScript

### 2. 目录结构说明

```
src/components/pisellSingleSelect/
├── index.tsx                         # 主入口：export { PisellSingleSelect }
├── PisellSingleSelect.tsx           # 主组件实现
├── PisellSingleSelect.less          # 组件样式文件
├── PisellSingleSelect.test.tsx      # 组件单元测试
├── PisellSingleSelect.stories.tsx   # Storybook 故事
├── types.ts                         # TypeScript 类型定义
├── components/                      # 子组件目录
│   ├── DisabledView.tsx            # 禁用态视图
│   ├── ReadOnlyView.tsx            # 只读态视图
│   └── EditView.tsx                # 编辑态视图
└── docs/                           # 组件文档目录
    ├── pisellSingleSelect.md           # 组件使用文档
    ├── pisellSingleSelect.$tab-design.md  # 设计文档（本文档）
    ├── pisellSingleSelect.$tab-dev.md     # 开发文档
    └── pisellSingleSelect.$tab-test.md    # 测试用例文档
```

### 3. 复用逻辑（@pisell/utils）

```
packages/utils/src/select/
├── index.ts                        # 导出所有 select 相关工具
├── types.ts                        # 共享类型定义
├── hooks/                          # 自定义Hooks
│   ├── index.ts
│   ├── useSelectState.ts          # 核心状态管理
│   └── useDebouncedSearch.ts      # 防抖搜索
└── utils/                          # 工具函数
    ├── index.ts
    ├── filterOptions.ts            # 选项过滤
    ├── sortOptions.ts              # 选项排序
    └── helpers.ts                  # 辅助函数
```

### 4. 核心组件实现要点

#### PisellSingleSelect 主组件

* **职责**：封装Ant Design Select组件，管理状态，处理样式定制。
* **关键实现**：
  - 使用`@pisell/utils`中的`useSelectState` Hook管理状态
  - 通过Less文件覆盖Ant Design默认样式
  - 优先使用Token配置修改样式

```
const PisellSingleSelect = forwardRef<SelectRef, PisellSingleSelectProps>((props, ref) => {
  const {
    value,
    defaultValue,
    onChange,
    options,
    label,
    helperText,
    error,
    errorText,
    disabled,
    readOnly,
    ...restProps
  } = props;

  // 使用 @pisell/utils 中的状态管理
  const state = useSelectState({
    value,
    defaultValue,
    onChange,
    mode: 'single',
  });

  // 根据不同状态渲染不同视图
  if (disabled) {
    return <DisabledView {...props} />;
  }
  
  if (readOnly) {
    return <ReadOnlyView {...props} />;
  }

  return <EditView {...props} state={state} />;
});
```

## 第四部分：样式系统

### 1. Token 配置（优先方式）

组件样式优先通过修改 `packages/materials/src/theme/tokens.ts` 中的 Select Token 来实现。

当前 Select Token 配置：

```
Select: {
  // 基础样式（与 Input 保持一致）
  fontSizeIcon: 16,
  fontSize: 16,
  lineHeight: 1.5,

  // 边框和圆角（与 Input 一致）
  borderRadius: 8,
  colorBorder: '#D0D5DD',

  // 高度和内边距（与 Input 完全一致）
  controlHeight: 48,
  controlPaddingHorizontal: 16, // 水平内边距

  // 颜色
  colorText: '#101828',
  colorTextPlaceholder: '#667085',
  colorBgContainer: '#FFFFFF',

  // 禁用态
  colorBgContainerDisabled: '#F9FAFB',
  colorTextDisabled: '#98A2B3',
}
```

### 2. Less 样式文件（补充定制）

当Token无法满足需求时，使用Less文件进行补充定制。

**基础配置** (`PisellSingleSelect.less`):

```
.pisell-single-select {
  width: 100%;
  
  // 包装器
  &-wrapper {
    display: flex;
    flex-direction: column;
    gap: 6px;
  }

  // 标签
  &-label {
    font-size: 14px;
    font-weight: 500;
    color: #344054;
    line-height: 20px;
    
    &-required::after {
      content: ' *';
      color: #f5222d;
    }
  }

  // 辅助文本
  &-helper-text {
    font-size: 14px;
    color: #667085;
    line-height: 20px;
  }

  // 错误文本
  &-error-text {
    font-size: 14px;
    color: #f5222d;
    line-height: 20px;
  }

  // 错误状态
  &-error {
    :global(.ant-select-selector) {
      border-color: #f5222d !important;
      
      &:hover,
      &:focus {
        border-color: #f5222d !important;
      }
    }
  }

  // 只读状态
  &-readonly {
    :global(.ant-select-selector) {
      background-color: #F9FAFB;
      cursor: default;
      
      &:hover {
        border-color: #D0D5DD;
      }
    }
  }
}
```

### 3. 尺寸变体

尺寸通过Token和Less结合实现：

```
// Small 尺寸
.pisell-single-select-small {
  :global(.ant-select-selector) {
    height: 32px;
    padding: 0 12px;
  }
}

// Medium 尺寸（默认）
.pisell-single-select-medium {
  // 使用 Token 默认值
}

// Large 尺寸
.pisell-single-select-large {
  :global(.ant-select-selector) {
    height: 56px;
    padding: 0 20px;
  }
}
```

## 第五部分：高级特性实现

### 1. 搜索过滤与防抖

**防抖Hook实现** (在 `@pisell/utils/src/select/hooks/useDebouncedSearch.ts`):

```
import { useState, useEffect } from 'react';

export function useDebouncedSearch<T>(
  initialValue: T,
  delay: number,
  callback?: (value: T) => void
): [T, (value: T) => void] {
  const [value, setValue] = useState<T>(initialValue);
  const [debouncedValue, setDebouncedValue] = useState<T>(initialValue);

  useEffect(() => {
    const timer = setTimeout(() => {
      setDebouncedValue(value);
      callback?.(value);
    }, delay);

    return () => clearTimeout(timer);
  }, [value, delay, callback]);

  return [debouncedValue, setValue];
}
```

### 2. 键盘导航

Ant Design Select 已经内置了完整的键盘导航支持，包括：

- **ArrowDown/ArrowUp**: 在选项间导航
- **Enter**: 选择当前高亮项
- **Escape**: 关闭下拉框
- **Tab**: 失焦并关闭下拉框

组件无需额外实现，只需确保正确设置ARIA属性。

### 3. 可访问性（ARIA属性）

基于Ant Design Select的可访问性基础，确保：

```
<PisellSingleSelect
  aria-label={label}
  aria-required={required}
  aria-invalid={error}
  aria-describedby={helperText ? `${id}-helper` : undefined}
/>
```

## 第六部分：性能优化

### 1. 记忆化渲染策略

**使用`useMemo`缓存计算值**:

```
const filteredOptions = useMemo(() => {
  return options.filter(opt =>
    !searchText || opt.label.toLowerCase().includes(searchText.toLowerCase())
  );
}, [options, searchText]);
```

**使用`useCallback`缓存事件处理函数**:

```
const handleChange = useCallback((value: T | null) => {
  onChange?.(value, options.find(opt => opt.value === value) || null);
}, [onChange, options]);
```

### 2. 防抖应用

| 场景   | 技术            | 实现位置                      | 说明                   |
|------|---------------|---------------------------|----------------------|
| 搜索输入 | 防抖 (Debounce) | `useDebouncedSearch` Hook | 用户停止输入300ms后才触发搜索逻辑。 |

## 第七部分：测试策略

### 1. 单元测试用例

使用Jest + React Testing Library。

```
import { render, screen, fireEvent } from '@testing-library/react';
import { PisellSingleSelect } from './PisellSingleSelect';

const mockOptions = [
  { value: '1', label: '选项一' },
  { value: '2', label: '选项二' },
];

describe('PisellSingleSelect Component', () => {
  test('渲染基础单选组件', () => {
    render(<PisellSingleSelect options={mockOptions} />);
    expect(screen.getByRole('combobox')).toBeInTheDocument();
  });

  test('选择选项触发onChange', () => {
    const handleChange = jest.fn();
    render(
      <PisellSingleSelect
        options={mockOptions}
        onChange={handleChange}
      />
    );

    fireEvent.click(screen.getByRole('combobox'));
    fireEvent.click(screen.getByText('选项一'));

    expect(handleChange).toHaveBeenCalledWith('1', mockOptions[0]);
  });

  test('禁用状态下不可交互', () => {
    render(<PisellSingleSelect options={mockOptions} disabled />);
    const trigger = screen.getByRole('combobox');
    expect(trigger).toHaveClass('ant-select-disabled');
  });

  test('搜索功能过滤选项', async () => {
    render(<PisellSingleSelect options={mockOptions} showSearch />);
    fireEvent.click(screen.getByRole('combobox'));

    const searchInput = screen.getByRole('combobox');
    fireEvent.change(searchInput, { target: { value: '选项二' } });

    // 验证过滤结果
  });
});
```

### 2. 可访问性测试

使用`jest-axe`进行自动化可访问性测试。

```
import { axe } from 'jest-axe';

test('组件没有可访问性违规', async () => {
  const { container } = render(<PisellSingleSelect options={mockOptions} />);
  const results = await axe(container);
  expect(results).toHaveNoViolations();
});
```

## 第八部分：与其他组件的关系

### 1. 复用逻辑

核心逻辑抽取到 `@pisell/utils/src/select`，可供以下组件复用：

- **PisellMultipleSelect**: 多选下拉组件
- **PisellCascader**: 级联选择组件（未来）
- **PisellTreeSelect**: 树形选择组件（未来）

### 2. 设计系统对齐

- 样式与 `PisellInput`、`PisellDatePicker` 等表单组件保持一致
- 使用统一的 Token 系统
- 遵循相同的尺寸规范（small/medium/large）

## 总结

PisellSingleSelect 是一个基于Ant Design Select封装的单选下拉组件，通过Token系统和Less样式实现Pisell设计规范，核心逻辑抽取到
`@pisell/utils`以提高复用性。组件设计遵循单一职责原则，提供完整的类型安全和可访问性支持。
