---
nav:
  title: 组件
  order: 1
group:
  title: Form Fields
  order: 1
title: PisellSingleSelect
order: 1
category: pro
---

# PisellSingleSelect 单选下拉组件

PisellSingleSelect 是基于 Ant Design Select 封装的单选表单组件，提供只读/编辑/禁用三种状态、搜索过滤、受控与非受控模式，适用于表单与表格中的单选场景。

## 何时使用

- 表单中需要单选一项（分类、状态、城市等）
- 表格列中需要只读展示选项文案
- 需要带搜索、清空、必填校验的单选控件

## 特性

- 三种交互状态：只读、编辑、禁用
- 搜索与过滤：支持防抖搜索和自定义 `filterOption`
- 受控与非受控模式
- 样式：Token、Less、className/style
- 基于 Ant Design Select，逻辑复用 @pisell/utils

## 基础用法

### 受控模式

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

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

export default () => {
  const [value, setValue] = useState<string | null>(null);
  return (
    <PisellSingleSelect
      options={fruitOptions}
      value={value}
      onChange={(val) => setValue(val)}
      placeholder="请选择一种水果"
    />
  );
};
```

### 非受控模式

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    defaultValue="apple"
    placeholder="请选择"
  />
);
```

### 带标签和辅助文本

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    label="最爱水果"
    helperText="请选择你最喜欢的水果"
    placeholder="请选择"
    required
  />
);
```

### 只读态展示

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    value="apple"
    readOnly
    label="已选择的水果"
  />
);
```

### 禁用态

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    value="apple"
    disabled
  />
);
```

---

## 搜索与过滤

### 基础搜索

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

const cityOptions = [
  { value: 'beijing', label: '北京' },
  { value: 'shanghai', label: '上海' },
  { value: 'guangzhou', label: '广州' },
  { value: 'shenzhen', label: '深圳' },
];

export default () => {
  const [value, setValue] = useState<string | null>(null);
  return (
    <PisellSingleSelect
      options={cityOptions}
      label="选择城市"
      placeholder="搜索城市..."
      showSearch
      value={value}
      onChange={setValue}
    />
  );
};
```

### 自定义过滤逻辑

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

const cityOptions = [
  { value: 'beijing', label: '北京' },
  { value: 'shanghai', label: '上海' },
  { value: 'guangzhou', label: '广州' },
  { value: 'shenzhen', label: '深圳' },
];

export default () => {
  const [value, setValue] = useState<string | null>(null);
  return (
    <PisellSingleSelect
      options={cityOptions}
      label="选择城市"
      placeholder="输入城市名..."
      showSearch
      value={value}
      onChange={setValue}
      filterOption={(input, option) =>
        option.label.toLowerCase().includes(input.toLowerCase()) ||
        String(option.value).includes(input)
      }
    />
  );
};
```

### 搜索回调

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

const options = [
  { value: 'a', label: '选项 A' },
  { value: 'b', label: '选项 B' },
];

export default () => (
  <PisellSingleSelect
    options={options}
    showSearch
    onSearch={(keyword) => {
      console.log('搜索关键词:', keyword);
    }}
  />
);
```

---

## 错误状态与校验

### 显示错误状态

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    label="选择水果"
    value={null}
    required
    error
    errorText="此项为必填项，请选择"
  />
);
```

### 表单校验场景

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

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

export default () => {
  const [value, setValue] = useState<string | null>(null);
  const [error, setError] = useState(false);

  const handleSubmit = () => {
    if (!value) setError(true);
    else {
      setError(false);
      console.log('提交值:', value);
    }
  };

  return (
    <div>
      <PisellSingleSelect
        options={fruitOptions}
        label="选择水果（必填）"
        value={value}
        onChange={(val) => {
          setValue(val);
          setError(false);
        }}
        required
        error={error}
        errorText="请选择一个选项"
      />
      <button type="button" onClick={handleSubmit}>提交</button>
    </div>
  );
};
```

---

## 交互配置

### 清空按钮

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

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

export default () => {
  const [value, setValue] = useState<string | null>(null);
  return (
    <PisellSingleSelect
      options={fruitOptions}
      value={value}
      onChange={setValue}
      allowClear
    />
  );
};
```

### 不同尺寸

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

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

export default () => (
  <div style={{ display: 'flex', gap: 16, flexWrap: 'wrap' }}>
    <PisellSingleSelect options={fruitOptions} size="small" placeholder="小" />
    <PisellSingleSelect options={fruitOptions} size="medium" placeholder="中" />
    <PisellSingleSelect options={fruitOptions} size="large" placeholder="大" />
  </div>
);
```

---

## 事件回调

### 值变化回调

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    onChange={(value, option) => {
      console.log('选中值:', value);
      console.log('选中选项:', option);
    }}
  />
);
```

### 焦点事件

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    onFocus={() => console.log('获得焦点')}
    onBlur={() => console.log('失去焦点')}
  />
);
```

### 下拉框显隐

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    onDropdownVisibleChange={(visible) => {
      console.log('下拉框', visible ? '打开' : '关闭');
    }}
  />
);
```

---

## 表单场景

### 在 Form 中使用

```tsx
import React, { useState } from 'react';
import { Form } from 'antd';
import { PisellSingleSelect } from '@pisell/materials';

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

const cityOptions = [
  { value: 'beijing', label: '北京' },
  { value: 'shanghai', label: '上海' },
  { value: 'guangzhou', label: '广州' },
  { value: 'shenzhen', label: '深圳' },
  { value: 'chengdu', label: '成都' },
  { value: 'hangzhou', label: '杭州' },
];

export default () => {
  const [fruit, setFruit] = useState<string | null>(null);
  const [city, setCity] = useState<string | null>(null);

  return (
    <Form layout="vertical" style={{ maxWidth: 400 }}>
      <Form.Item label="最爱水果（必填）">
        <PisellSingleSelect
          options={fruitOptions}
          value={fruit}
          onChange={setFruit}
          placeholder="请选择水果"
          required
        />
      </Form.Item>
      <Form.Item label="所在城市">
        <PisellSingleSelect
          options={cityOptions}
          value={city}
          onChange={setCity}
          placeholder="请选择城市"
          showSearch
          allowClear
        />
      </Form.Item>
    </Form>
  );
};
```

---

## 表格场景

### 表格列中使用（只读态）

```tsx
import React from 'react';
import { Table } from 'antd';
import { PisellSingleSelect } from '@pisell/materials';

const statusOptions = [
  { value: 'active', label: '激活' },
  { value: 'inactive', label: '未激活' },
  { value: 'pending', label: '待审核' },
];

const columns = [
  { title: '用户名', dataIndex: 'name' },
  {
    title: '状态',
    dataIndex: 'status',
    render: (value: string) => (
      <PisellSingleSelect
        options={statusOptions}
        value={value}
        readOnly
      />
    ),
  },
];

const dataSource = [
  { id: 1, name: '张三', status: 'active' },
  { id: 2, name: '李四', status: 'pending' },
];

export default () => <Table dataSource={dataSource} columns={columns} rowKey="id" />;
```

---

## Ref 方法

### 使用 Ref 控制组件

```tsx
import React, { useRef } from 'react';
import { PisellSingleSelect, type SingleSelectRef } from '@pisell/materials';

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

export default () => {
  const selectRef = useRef<SingleSelectRef>(null);

  return (
    <div style={{ display: 'flex', gap: 8, alignItems: 'center', flexWrap: 'wrap' }}>
      <PisellSingleSelect ref={selectRef} options={fruitOptions} />
      <button type="button" onClick={() => selectRef.current?.focus()}>聚焦</button>
      <button type="button" onClick={() => selectRef.current?.clearValue()}>清空</button>
      <button type="button" onClick={() => console.log(selectRef.current?.getValue())}>获取值</button>
    </div>
  );
};
```

---

## 样式定制

### 自定义类名和样式

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

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

export default () => (
  <PisellSingleSelect
    options={fruitOptions}
    className="custom-select"
    style={{ width: 300 }}
  />
);
```

### 通过 Token 定制（全局）

在项目的 theme tokens 中修改 Select 相关 Token（如 `fontSize`、`controlHeight`、`borderRadius`、`colorBorder` 等）。

### 通过 Less 定制（组件级别）

在组件 Less 或业务 Less 中覆盖 `.pisell-single-select` 相关类名实现组件级定制。

---

## API

### Props

| 属性名              | 类型                               | 默认值        | 必填 | 说明           |
|------------------|----------------------------------|------------|-----|--------------|
| **核心数据**         |                                  |            |     |              |
| options          | `SelectOption<T>[]`              | -          | ✅  | 选项列表       |
| value            | `T \| null`                      | -          | ❌  | 当前选中值（受控）    |
| defaultValue     | `T`                              | -          | ❌  | 默认值（非受控）     |
| **交互控制**         |                                  |            |     |              |
| placeholder      | `string`                         | `'请选择'`    | ❌  | 占位符文本        |
| showSearch       | `boolean`                        | `false`    | ❌  | 是否显示搜索框      |
| allowClear       | `boolean`                        | `false`    | ❌  | 是否显示清空按钮     |
| disabled         | `boolean`                        | `false`    | ❌  | 是否禁用         |
| readOnly         | `boolean`                        | `false`    | ❌  | 是否只读         |
| **样式与布局**        |                                  |            |     |              |
| size             | `'small' \| 'medium' \| 'large'` | `'medium'` | ❌  | 尺寸           |
| className        | `string`                         | -          | ❌  | 自定义类名        |
| style            | `CSSProperties`                  | -          | ❌  | 自定义样式        |
| **表单标签与校验**      |                                  |            |     |              |
| label            | `string`                         | -          | ❌  | 标签文本         |
| helperText       | `string`                         | -          | ❌  | 辅助提示文本       |
| required         | `boolean`                        | `false`    | ❌  | 是否必填         |
| error            | `boolean`                        | `false`    | ❌  | 是否显示错误状态     |
| errorText        | `string`                         | -          | ❌  | 错误提示文本       |
| **筛选与排序**        |                                  |            |     |              |
| filterable       | `boolean`                        | `false`    | ❌  | 是否启用过滤       |
| filterOption     | `(input, option) => boolean`     | -          | ❌  | 自定义过滤函数      |
| sortable         | `boolean`                        | `false`    | ❌  | 是否启用排序       |
| sortDirection    | `'asc' \| 'desc'`                | -          | ❌  | 排序方向         |
| **无障碍访问**        |                                  |            |     |              |
| aria-label       | `string`                         | -          | ❌  | ARIA 标签      |
| aria-required    | `boolean`                        | -          | ❌  | ARIA 是否必填    |
| aria-invalid     | `boolean`                        | -          | ❌  | ARIA 是否无效    |
| aria-describedby | `string`                         | -          | ❌  | ARIA 描述关联 ID |

### SelectOption 类型

```ts
interface SelectOption<T = string | number> {
  value: T;           // 选项值（唯一标识）
  label: string;      // 选项显示文本
  disabled?: boolean; // 是否禁用该选项
  icon?: ReactNode;   // 选项图标
}
```

### 事件回调

| 事件名                     | 参数                                                            | 说明      |
|-------------------------|---------------------------------------------------------------|---------|
| onChange                | `(value: T \| null, option: SelectOption<T> \| null) => void` | 值变化时的回调 |
| onSearch                | `(value: string) => void`                                     | 搜索输入回调  |
| onFocus                 | `(event: React.FocusEvent) => void`                           | 获得焦点回调  |
| onBlur                  | `(event: React.FocusEvent) => void`                           | 失去焦点回调  |
| onDropdownVisibleChange | `(visible: boolean) => void`                                  | 下拉框显隐回调 |

### Ref 方法

| 方法名        | 参数                           | 说明    |
|------------|------------------------------|-------|
| focus      | `() => void`                 | 聚焦到组件 |
| blur       | `() => void`                 | 失焦    |
| getValue   | `() => T \| null`            | 获取当前值 |
| setValue   | `(value: T \| null) => void` | 设置值   |
| clearValue | `() => void`                 | 清空值   |

---

## 完整示例

### 商品分类选择

```tsx
import React, { useState } from 'react';
import { Form } from 'antd';
import { PisellSingleSelect } from '@pisell/materials';

const categoryOptions = [
  { value: 'electronics', label: '电子产品' },
  { value: 'clothing', label: '服装' },
  { value: 'food', label: '食品' },
  { value: 'books', label: '图书' },
];

export default () => {
  const [category, setCategory] = useState<string | null>(null);
  const [error, setError] = useState(false);

  return (
    <Form.Item label="商品分类" required>
      <PisellSingleSelect
        options={categoryOptions}
        value={category}
        onChange={(val) => {
          setCategory(val);
          setError(false);
        }}
        placeholder="请选择商品分类"
        error={error}
        errorText="请选择商品分类"
        allowClear
      />
    </Form.Item>
  );
};
```

### 城市选择（带搜索）

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

const cityOptions = [
  { value: 'beijing', label: '北京' },
  { value: 'shanghai', label: '上海' },
  { value: 'guangzhou', label: '广州' },
  { value: 'shenzhen', label: '深圳' },
  { value: 'chengdu', label: '成都' },
  { value: 'hangzhou', label: '杭州' },
];

export default () => {
  const [city, setCity] = useState<string | null>(null);
  return (
    <PisellSingleSelect
      options={cityOptions}
      label="所在城市"
      helperText="输入城市名快速查找"
      value={city}
      onChange={setCity}
      placeholder="搜索城市..."
      showSearch
      allowClear
      onSearch={(keyword) => console.log('搜索关键词:', keyword)}
    />
  );
};
```

---

## 常见问题

### 1. 如何处理空值？

只读态空值显示为 `-`，编辑态显示占位符。

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

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

export default () => (
  <PisellSingleSelect options={fruitOptions} value={null} readOnly label="只读空值" />
);
```

### 2. 如何实现搜索功能？

设置 `showSearch` 即可，组件内置防抖。

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

const options = [
  { value: 'a', label: '选项 A' },
  { value: 'b', label: '选项 B' },
];

export default () => (
  <PisellSingleSelect options={options} showSearch placeholder="搜索..." />
);
```

### 3. 如何自定义过滤逻辑？

使用 `filterOption` 属性。

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

const options = [
  { value: 'apple', label: '苹果' },
  { value: 'banana', label: '香蕉' },
];

export default () => (
  <PisellSingleSelect
    options={options}
    showSearch
    filterOption={(input, option) => option.label.includes(input)}
  />
);
```

### 4. 受控与非受控区别

- **受控**：外部通过 `value` + `onChange` 管理。
- **非受控**：使用 `defaultValue`，组件内部管理。

### 5. 如何在表格中使用？

表格列中用 `readOnly` 展示选项文案，见上文「表格列中使用（只读态）」示例。

### 6. 如何使用 Ref 方法？

通过 `ref` 调用 `focus()`、`getValue()`、`clearValue()` 等，见上文「使用 Ref 控制组件」示例。

### 7. 如何自定义样式？

通过 `className`、`style` 传入，或通过项目 Token / Less 覆盖。

---

## 最佳实践

1. **受控模式优先**：在表单场景推荐使用受控模式，便于统一管理

2. **搜索优化**：

- 选项较多时（>10个）启用 `showSearch`
- 搜索已内置防抖优化（300ms）

3. **只读态优化**：在表格、列表等展示场景使用 `readOnly`，避免渲染下拉框

4. **错误提示**：使用 `error` 和 `errorText` 提供清晰的错误反馈

5. **清空功能**：重要的单选场景建议启用 `allowClear`

6. **必填标记**：使用 `required` 属性显示必填标记（*）

---

## 注意事项

1. **类型安全**：value 的类型是 `T | null`，注意 null 值的处理
2. **选项唯一性**：确保每个选项的 `value` 是唯一的
3. **搜索防抖**：搜索已内置 300ms 防抖，无需额外处理
4. **只读态限制**：只读态不会渲染真实的 Select 组件，某些 Props 无效
5. **样式优先级**：Token > Less > inline style

---

## 相关文档

- [设计文档](./pisellSingleSelect.$tab-design.md)
- [开发文档](./pisellSingleSelect.$tab-dev.md)
- [测试文档](./pisellSingleSelect.$tab-test.md)

## 相关组件

- **PisellMultipleSelect** - 多选下拉组件
- **PisellSingleLineText** - 单行文本组件
- **PisellLongText** - 长文本组件
