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

# PisellSingleSelect 单选下拉组件

## 组件介绍

PisellSingleSelect 是 Pisell 2.0 组件库中处理单选数据的基础表单组件。它基于 Ant Design Select 组件封装，提供了符合 Pisell
设计规范的单选功能、搜索过滤、状态管理等核心能力，适用于各种单选场景。

**核心特性**：

- ✅ 三种交互状态：只读、编辑、禁用
- ✅ 搜索与过滤：支持防抖搜索和自定义过滤逻辑
- ✅ 完整的状态管理：受控和非受控模式
- ✅ 样式定制：基于 Token 系统和 Less
- ✅ 基于 Ant Design Select 开发
- ✅ 复用逻辑抽取到 @pisell/utils

---

## 基础用法

### 基础示例（受控模式）

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

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="请选择一种水果"
        />
    );
}
```

### 非受控模式

```
<PisellSingleSelect
    options={fruitOptions}
    defaultValue="apple"
    placeholder="请选择"
/>
```

### 带标签和辅助文本

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

### 只读态展示

```
<PisellSingleSelect
    options={fruitOptions}
    value="apple"
    readOnly
    label="已选择的水果"
/>
// 显示: 苹果
```

### 禁用态

```
<PisellSingleSelect
    options={fruitOptions}
    value="apple"
    disabled
/>
```

---

## 搜索与过滤

### 基础搜索

```
<PisellSingleSelect
    options={cityOptions}
    label="选择城市"
    placeholder="搜索城市..."
    showSearch
    value={value}
    onChange={setValue}
/>
```

### 自定义过滤逻辑

```
<PisellSingleSelect
    options={cityOptions}
    label="选择城市"
    placeholder="输入城市名..."
    showSearch
    value={value}
    onChange={setValue}
    filterOption={(input, option) => {
        // 自定义过滤：支持模糊匹配
        return option.label.toLowerCase().includes(input.toLowerCase()) ||
            option.value.toString().includes(input);
    }}
/>
```

### 搜索回调

```
<PisellSingleSelect
    options={options}
    showSearch
    onSearch={(keyword) => {
        console.log('搜索关键词:', keyword);
        // 可以在这里触发远程搜索
    }}
/>
```

---

## 错误状态与校验

### 显示错误状态

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

### 表单校验场景

```
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 onClick={handleSubmit}>提交</button>
        </div>
    );
}
```

---

## 交互配置

### 清空按钮

```
<PisellSingleSelect
    options={fruitOptions}
    value={value}
    onChange={setValue}
    allowClear
/>
```

### 不同尺寸

```
// 小尺寸
<PisellSingleSelect options={fruitOptions} size="small"/>

// 中等尺寸（默认）
<PisellSingleSelect options={fruitOptions} size="medium"/>

// 大尺寸
<PisellSingleSelect options={fruitOptions} size="large"/>
```

---

## 事件回调

### 值变化回调

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

### 焦点事件

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

### 下拉框显隐

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

---

## 表单场景

### 在 Form 中使用

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

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 {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 () => {
    return <Table dataSource={dataSource} columns={columns}/>;
}
```

---

## Ref 方法

### 使用 Ref 控制组件

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

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


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

    const handleFocus = () => {
        selectRef.current?.focus();
    };

    const handleClear = () => {
        selectRef.current?.clearValue();
    };

    const handleGetValue = () => {
        const currentValue = selectRef.current?.getValue();
        console.log('当前值:', currentValue);
    };

    return (
        <div>
            <PisellSingleSelect
                ref={selectRef}
                options={fruitOptions}
            />
            <button onClick={handleFocus}>聚焦</button>
            <button onClick={handleClear}>清空</button>
            <button onClick={handleGetValue}>获取值</button>
        </div>
    );
}
```

---

## 样式定制

### 自定义类名和样式

```
<PisellSingleSelect
    options={fruitOptions}
    className="custom-select"
    style={{width: 300}}
/>
```

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

在 `packages/materials/src/theme/tokens.ts` 中修改 Select Token：

```
Select: {
  fontSize: 16,
  controlHeight: 48,
  borderRadius: 8,
  colorBorder: '#D0D5DD',
  // ... 其他配置
}
```

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

在 `PisellSingleSelect.less` 中添加自定义样式。

---

## 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 类型

```
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 {Form} from 'antd';
import {PisellSingleSelect} from '@pisell/materials';
import {useState} from 'react';

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);

    const handleSubmit = () => {
        if (!category) {
            setError(true);
            return;
        }
        console.log('选中分类:', category);
    };

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

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

```
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. 如何处理空值？

只读态会显示 `-`，编辑态会显示占位符：

```
// 只读态显示空值
<PisellSingleSelect
    options={fruitOptions}
    value={null}
    readOnly
/>
// 显示: -
```

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

设置 `showSearch={true}`：

```
<PisellSingleSelect
    options={options}
    showSearch
    placeholder="搜索..."
/>
```

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

使用 `filterOption` 属性：

```
<PisellSingleSelect
    options={options}
    showSearch
    filterOption={(input, option) => {
        // 自定义过滤逻辑
        return option.label.includes(input);
    }}
/>
```

### 4. 受控模式和非受控模式有什么区别？

**受控模式**：值由外部管理

```
const [value, setValue] = useState<string | null>(null);
<PisellSingleSelect value={value} onChange={setValue}/>
```

**非受控模式**：值由组件内部管理

```
<PisellSingleSelect defaultValue="apple"/>
```

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

只读态适合在表格中展示：

```
{
  title: '状态',
  dataIndex: 'status',
  render: (value) => (
    <PisellSingleSelect
      options={statusOptions}
      value={value}
      readOnly
    />
  ),
}
```

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

```
const ref = useRef<SingleSelectRef>(null);

// 聚焦
ref.current?.focus();

// 获取值
const value = ref.current?.getValue();

// 清空
ref.current?.clearValue();
```

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

```
// 方式1：通过 className
<PisellSingleSelect className="custom-select"/>

// 方式2：通过 style
<PisellSingleSelect style={{width: 300}}/>

// 方式3：通过 Token（全局）
// 在 tokens.ts 中修改 Select Token
```

---

## 最佳实践

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

---

## 相关组件

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