unnamed-ui
输入控件

Block Select

下拉选择组件,支持单选、多选、icon 定制等功能

import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectDemo() {
  return (
    <BlockSelect
      options={[
        { label: "苹果", value: "apple" },
        { label: "香蕉", value: "banana" },
        { label: "橙子", value: "orange" },
        { label: "西瓜", value: "watermelon" },
      ]}
      placeholder="选择水果"
    />
  );
}

BlockSelect 下拉选择组件提供丰富的配置选项,支持单选和多选模式,可自定义 icon 位置、圆角样式等,适用于各种表单场景。

概述

  • 单选/多选:支持单选和多选两种模式
  • Icon 定制:支持自定义 icon 及位置(前缀/后缀)
  • 多选展示:多选模式下以 tag 形式展示选中项
  • 圆角控制:支持圆角 100% 显示
  • 状态丰富:支持默认、hover、focus、disabled 等状态
  • 完全受控:支持受控和非受控两种模式
  • 类型安全:完整的 TypeScript 类型定义

快速开始

import { BlockSelect } from "@/registry/wuhan/composed/block-select/block-select";

export function Example() {
  return (
    <BlockSelect
      options={[
        { label: "选项 1", value: "option1" },
        { label: "选项 2", value: "option2" },
        { label: "选项 3", value: "option3" },
      ]}
      placeholder="请选择"
    />
  );
}

特性

  • 基于 Radix UI:构建在 @radix-ui/react-select 之上
  • 灵活的配置:通过 options 数组或 children 两种方式配置选项
  • 多选支持:集成 Checkbox,支持多项选择
  • Tag 展示:多选模式下以可关闭的 tag 形式展示
  • 无障碍支持:完整的 ARIA 属性和键盘导航
  • 透传属性:支持所有 Radix UI Select 的原生属性

安装

pnpm dlx shadcn@latest add http://localhost:3000/r/wuhan/block-select.json

代码演示

基础用法

基础的下拉选择使用。

import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectDemo() {
  return (
    <BlockSelect
      options={[
        { label: "苹果", value: "apple" },
        { label: "香蕉", value: "banana" },
        { label: "橙子", value: "orange" },
        { label: "西瓜", value: "watermelon" },
      ]}
      placeholder="选择水果"
    />
  );
}

受控模式

通过 value 和 onValueChange 控制选中状态。

当前选中: apple
"use client";

import { useState } from "react";
import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectControlled() {
  const [value, setValue] = useState("apple");

  return (
    <div className="flex flex-col gap-4">
      <BlockSelect
        value={value}
        onValueChange={(val) => setValue(val as string)}
        options={[
          { label: "苹果", value: "apple" },
          { label: "香蕉", value: "banana" },
          { label: "橙子", value: "orange" },
        ]}
        placeholder="选择水果"
      />
      <div className="text-sm text-gray-500">当前选中: {value}</div>
    </div>
  );
}

多选模式

支持多项选择,选中项以 tag 形式展示。

普通多选

已选择:

禁用状态

已选择: react, vue, angular, svelte
"use client";

import { useState } from "react";
import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectMultiple() {
  const [open, setOpen] = useState(false);
  const [values, setValues] = useState<string[]>([]);

  const [openDisabled, setOpenDisabled] = useState(false);
  const [valuesDisabled, setValuesDisabled] = useState<string[]>([
    "react",
    "vue",
    "angular",
    "svelte",
  ]);

  return (
    <div className="flex flex-col gap-8">
      {/* 普通多选 */}
      <div className="flex flex-col gap-4">
        <h3 className="text-sm font-medium text-[var(--Text-text-primary)]">
          普通多选
        </h3>
        <BlockSelect
          multiple
          open={open}
          onOpenChange={setOpen}
          value={values}
          onValueChange={(val) => setValues(val as string[])}
          options={[
            { label: "React", value: "react" },
            { label: "Vue", value: "vue" },
            { label: "Angular", value: "angular" },
            { label: "Svelte", value: "svelte" },
          ]}
          placeholder="选择框架"
        />
        <div className="text-sm text-[var(--Text-text-secondary)]">
          已选择: {values.join(", ") || "无"}
        </div>
      </div>

      {/* 禁用多选 */}
      <div className="flex flex-col gap-4">
        <h3 className="text-sm font-medium text-[var(--Text-text-primary)]">
          禁用状态
        </h3>
        <BlockSelect
          multiple
          disabled
          open={openDisabled}
          onOpenChange={setOpenDisabled}
          value={valuesDisabled}
          onValueChange={(val) => setValuesDisabled(val as string[])}
          options={[
            { label: "React", value: "react" },
            { label: "Vue", value: "vue" },
            { label: "Angular", value: "angular" },
            { label: "Svelte", value: "svelte" },
          ]}
          placeholder="选择框架"
        />
        <div className="text-sm text-[var(--Text-text-secondary)]">
          已选择: {valuesDisabled.join(", ")}
        </div>
      </div>
    </div>
  );
}

Icon 位置

Icon 可以放在前缀或后缀位置。

前缀 Icon
后缀 Icon(默认)
import { BlockSelect } from "@/components/composed/block-select/block-select";
import { Star } from "lucide-react";

export function BlockSelectWithIcon() {
  return (
    <div className="flex flex-col gap-4">
      <div>
        <div className="text-sm font-medium mb-2">前缀 Icon</div>
        <BlockSelect
          iconPosition="prefix"
          icon={<Star className="h-4 w-4" />}
          options={[
            { label: "选项 1", value: "option1" },
            { label: "选项 2", value: "option2" },
            { label: "选项 3", value: "option3" },
          ]}
          placeholder="选择选项"
        />
      </div>

      <div>
        <div className="text-sm font-medium mb-2">后缀 Icon(默认)</div>
        <BlockSelect
          iconPosition="suffix"
          options={[
            { label: "选项 1", value: "option1" },
            { label: "选项 2", value: "option2" },
            { label: "选项 3", value: "option3" },
          ]}
          placeholder="选择选项"
        />
      </div>
    </div>
  );
}

圆角样式

支持圆角 100% 显示。

import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectFullRounded() {
  return (
    <BlockSelect
      fullRounded
      options={[
        { label: "苹果", value: "apple" },
        { label: "香蕉", value: "banana" },
        { label: "橙子", value: "orange" },
      ]}
      placeholder="圆角 100%"
    />
  );
}

禁用状态

支持禁用整个组件或单个选项。

import { BlockSelect } from "@/components/composed/block-select/block-select";

export function BlockSelectDisabled() {
  return (
    <div className="flex flex-col gap-4">
      <BlockSelect
        disabled
        defaultValue="apple"
        options={[
          { label: "苹果", value: "apple" },
          { label: "香蕉", value: "banana" },
          { label: "橙子", value: "orange" },
        ]}
        placeholder="禁用状态"
      />

      <BlockSelect
        options={[
          { label: "苹果", value: "apple" },
          { label: "香蕉(禁用)", value: "banana", disabled: true },
          { label: "橙子", value: "orange" },
        ]}
        placeholder="部分选项禁用"
      />
    </div>
  );
}

高级用法

使用子组件进行更灵活的配置。

import {
  BlockSelect,
  SelectGroup,
  SelectLabel,
  SelectSeparator,
  SelectItem,
} from "@/components/composed/block-select/block-select";

export function BlockSelectAdvanced() {
  return (
    <BlockSelect placeholder="选择选项">
      <SelectGroup>
        <SelectLabel>水果</SelectLabel>
        <SelectItem value="apple">苹果</SelectItem>
        <SelectItem value="banana">香蕉</SelectItem>
        <SelectItem value="orange">橙子</SelectItem>
      </SelectGroup>

      <SelectSeparator />

      <SelectGroup>
        <SelectLabel>蔬菜</SelectLabel>
        <SelectItem value="carrot">胡萝卜</SelectItem>
        <SelectItem value="potato">土豆</SelectItem>
        <SelectItem value="tomato">西红柿</SelectItem>
      </SelectGroup>
    </BlockSelect>
  );
}

API

BlockSelect

参数说明类型默认值
value受控模式下的选中值string | string[]-
defaultValue非受控模式下的默认值string | string[]-
onValueChange值变化时的回调(value: string | string[]) => void-
options选项配置数组SelectOption[][]
placeholder占位文本string"请选择..."
triggerClassNameTrigger 的自定义类名string-
contentClassNameContent 的自定义类名string-
fullRounded是否圆角 100%booleanfalse
icon自定义 iconReact.ReactNode<ChevronDown />
iconPositionIcon 位置"prefix" | "suffix""suffix"
multiple是否多选booleanfalse
disabled是否禁用booleanfalse
name表单 name 属性string-
required是否必填boolean-
dir文本方向"ltr" | "rtl"-
open受控的打开状态boolean-
onOpenChange打开状态变化回调(open: boolean) => void-
position下拉内容定位方式"item-aligned" | "popper""popper"
side下拉内容显示位置"top" | "right" | "bottom" | "left"-
align下拉内容对齐方式"start" | "center" | "end"-
children子元素(用于高级用法)React.ReactNode-

SelectOption

属性说明类型默认值
label选项显示文本string-
value选项值string-
disabled是否禁用该选项booleanfalse

子组件

使用子组件可以实现更灵活的配置:

  • SelectGroup - 选项组
  • SelectLabel - 组标签
  • SelectItem - 选项
  • SelectSeparator - 分隔线
  • SelectTrigger - 触发器(原语)
  • SelectContent - 内容容器(原语)
  • SelectValue - 值显示(原语)

Radix UI 属性

BlockSelect 组件支持透传以下 Radix UI Select 的属性:

  • value / onValueChange - 受控值
  • defaultValue / defaultOpen - 默认值
  • open / onOpenChange - 打开状态控制
  • dir - 文本方向(RTL 支持)
  • name / disabled / required - 表单属性
  • position / side / align - 下拉位置控制

更多属性请参考 Radix UI Select 文档

使用场景

表单选择

在表单中收集用户的选择输入。

<BlockSelect
  name="category"
  required
  options={[
    { label: "技术", value: "tech" },
    { label: "设计", value: "design" },
    { label: "产品", value: "product" },
  ]}
  placeholder="选择分类"
/>

多选标签

选择多个标签或分类。

<BlockSelect
  multiple
  options={[
    { label: "React", value: "react" },
    { label: "Vue", value: "vue" },
    { label: "Angular", value: "angular" },
  ]}
  placeholder="选择技术栈"
/>

搜索框样式

使用圆角和前缀 icon 创建搜索框样式。

import { Search } from "lucide-react";

<BlockSelect
  fullRounded
  iconPosition="prefix"
  icon={<Search className="h-4 w-4" />}
  options={searchOptions}
  placeholder="搜索..."
/>

最佳实践

选项数量

  • 少于 5 个选项:可以考虑使用 Radio 单选框
  • 5-15 个选项:使用 Select 下拉选择
  • 超过 15 个选项:考虑添加搜索功能或分组

标签文本

  • 使用清晰、简洁的标签文本
  • 避免使用过长的选项文本
  • 保持选项之间的一致性

多选模式

  • 限制可选数量,避免选择过多
  • 提供"全选"/"清空"快捷操作
  • 考虑使用 tag 的最大显示数量

默认值

  • 为常用场景提供合理的默认值
  • 对于必填项,考虑设置默认选中项
  • 避免在没有明确偏好时强制默认选中

无障碍

  • 始终提供有意义的 placeholder 文本
  • 使用 SelectLabel 对选项进行分组
  • 确保键盘可以正常导航和选择

扩展示例

带搜索的选择器

"use client";

import { useState } from "react";
import { BlockSelect } from "@/registry/wuhan/composed/block-select/block-select";
import { Search } from "lucide-react";

export function SelectWithSearch() {
  const [searchTerm, setSearchTerm] = useState("");
  const allOptions = [
    { label: "苹果", value: "apple" },
    { label: "香蕉", value: "banana" },
    { label: "橙子", value: "orange" },
    // ... more options
  ];

  const filteredOptions = allOptions.filter((option) =>
    option.label.toLowerCase().includes(searchTerm.toLowerCase())
  );

  return (
    <div>
      <input
        type="text"
        value={searchTerm}
        onChange={(e) => setSearchTerm(e.target.value)}
        placeholder="搜索..."
        className="mb-2 w-full px-3 py-2 border rounded"
      />
      <BlockSelect
        options={filteredOptions}
        placeholder="选择水果"
      />
    </div>
  );
}

动态加载选项

"use client";

import { useState, useEffect } from "react";
import { BlockSelect } from "@/registry/wuhan/composed/block-select/block-select";

export function DynamicSelect() {
  const [options, setOptions] = useState([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 模拟 API 调用
    fetch("/api/options")
      .then((res) => res.json())
      .then((data) => {
        setOptions(data);
        setLoading(false);
      });
  }, []);

  if (loading) {
    return <div>加载中...</div>;
  }

  return (
    <BlockSelect
      options={options}
      placeholder="选择选项"
    />
  );
}

级联选择

"use client";

import { useState } from "react";
import { BlockSelect } from "@/registry/wuhan/composed/block-select/block-select";

export function CascadeSelect() {
  const [province, setProvince] = useState("");
  const [city, setCity] = useState("");

  const cityOptions = {
    guangdong: [
      { label: "广州", value: "guangzhou" },
      { label: "深圳", value: "shenzhen" },
    ],
    zhejiang: [
      { label: "杭州", value: "hangzhou" },
      { label: "宁波", value: "ningbo" },
    ],
  };

  return (
    <div className="flex gap-4">
      <BlockSelect
        value={province}
        onValueChange={(val) => {
          setProvince(val as string);
          setCity(""); // 重置城市
        }}
        options={[
          { label: "广东", value: "guangdong" },
          { label: "浙江", value: "zhejiang" },
        ]}
        placeholder="选择省份"
      />
      
      <BlockSelect
        value={city}
        onValueChange={(val) => setCity(val as string)}
        options={province ? cityOptions[province] : []}
        placeholder="选择城市"
        disabled={!province}
      />
    </div>
  );
}