按钮
Toggle Button
Toggle button component for selection and mode switching
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 默认单选示例
*/
export function ToggleButtonDefault() {
const [selected, setSelected] = useState<string | undefined>();
const options = [
{ id: "harmful", label: "有害/不安全" },
{ id: "false", label: "信息虚假" },
{ id: "inappropriate", label: "内容不当" },
];
return (
<div className="space-y-4">
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
variant="default"
/>
{selected && (
<div className="text-sm text-muted-foreground">
当前选择:{options.find((opt) => opt.id === selected)?.label}
</div>
)}
</div>
);
}
Toggle Button 组件提供了用于选择和多选场景的开关按钮样式,支持单选和多选模式,适用于反馈选项选择、模式切换(如深度思考、联网搜索)等场景。
概述
- 两种变体样式:default(默认,高度 32px)和 compact(紧凑,使用 CSS 变量)
- 单选和多选模式:支持单选按钮组和多选按钮组
- 图标支持:支持
icon选项,设置后仅显示图标不显示 label,适用于工具栏等紧凑场景 - 清晰的状态反馈:选中/未选中视觉反馈明确
- 禁用选项支持:可单独禁用某些选项
- 类型安全:完整的 TypeScript 类型定义
- 灵活组合:基于原语组件构建,可自由组合使用
快速开始
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
export function Example() {
const [selected, setSelected] = useState<string>();
return (
<ToggleButton
options={[
{ id: "harmful", label: "有害/不安全" },
{ id: "false", label: "信息虚假" },
{ id: "inappropriate", label: "内容不当" },
]}
value={selected}
onChange={setSelected}
/>
);
}特性
- 两种变体样式:default 适用于反馈组件等场景,compact 适用于 sender 组件等场景
- Tooltip 支持:通过
option.tooltip由组件内部正确包裹,DOM 结构为 Tooltip > button - 单选模式:点击选中,再次点击取消选择
- 多选模式:可同时选中多个选项
- 禁用状态:支持单独禁用某些选项,禁用的选项无法被选中
- 状态管理:内置状态管理逻辑,简化使用
- 样式定制:支持自定义按钮和容器样式
安装
代码演示
默认样式
默认样式的开关按钮,适用于反馈组件等场景。
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 默认单选示例
*/
export function ToggleButtonDefault() {
const [selected, setSelected] = useState<string | undefined>();
const options = [
{ id: "harmful", label: "有害/不安全" },
{ id: "false", label: "信息虚假" },
{ id: "inappropriate", label: "内容不当" },
];
return (
<div className="space-y-4">
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
variant="default"
/>
{selected && (
<div className="text-sm text-muted-foreground">
当前选择:{options.find((opt) => opt.id === selected)?.label}
</div>
)}
</div>
);
}
紧凑样式
紧凑样式的开关按钮,适用于 sender 组件等场景。
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 紧凑样式示例(用于 sender 组件等场景)
*/
export function ToggleButtonCompact() {
const [modes, setModes] = useState<string[]>([]);
const modeOptions = [
{ id: "web-search", label: "联网搜索" },
{ id: "deep-think", label: "深度思考" },
];
return (
<div className="space-y-4">
<ToggleButton
options={modeOptions}
values={modes}
onValuesChange={setModes}
multiple
variant="compact"
/>
{modes.length > 0 && (
<div className="text-sm text-muted-foreground">
已启用模式:
{modes
.map((id) => modeOptions.find((opt) => opt.id === id)?.label)
.join("、")}
</div>
)}
</div>
);
}
多选模式
多选模式示例。
选择你的兴趣(可多选)
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 多选模式示例
*/
export function ToggleButtonMultiple() {
const [interests, setInterests] = useState<string[]>([]);
const interestOptions = [
{ id: "tech", label: "科技" },
{ id: "sports", label: "体育" },
{ id: "music", label: "音乐" },
{ id: "travel", label: "旅行" },
{ id: "food", label: "美食" },
];
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium">选择你的兴趣(可多选)</div>
<ToggleButton
options={interestOptions}
values={interests}
onValuesChange={setInterests}
multiple
variant="default"
/>
</div>
{interests.length > 0 && (
<div className="text-sm text-muted-foreground">
已选择:
{interests
.map((id) => interestOptions.find((opt) => opt.id === id)?.label)
.join("、")}
</div>
)}
</div>
);
}
禁用选项
带禁用选项的开关按钮。
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 禁用状态示例
*/
export function ToggleButtonDisabled() {
const [selected, setSelected] = useState<string | undefined>();
const options = [
{ id: "option1", label: "可选项 1" },
{ id: "option2", label: "可选项 2" },
{ id: "option3", label: "禁用项", disabled: true },
{ id: "option4", label: "可选项 4" },
];
return (
<div className="space-y-4">
<ToggleButton options={options} value={selected} onChange={setSelected} />
{selected && (
<div className="text-sm text-muted-foreground">
当前选择:{options.find((opt) => opt.id === selected)?.label}
</div>
)}
</div>
);
}
API
ToggleButton
开关按钮主组件,支持单选和多选模式。
Props
| Prop | Type | Default | Description |
|---|---|---|---|
options | ToggleOption[] | - | 按钮选项列表(必填) |
value | string | - | 当前选中的选项 ID(单选模式,受控) |
values | string[] | - | 当前选中的选项 ID 列表(多选模式,受控) |
defaultValue | string | - | 单选模式默认值(非受控) |
defaultValues | string[] | - | 多选模式默认值(非受控) |
onChange | (value: string | undefined) => void | - | 选项变化回调(单选模式) |
onValuesChange | (values: string[]) => void | - | 选项变化回调(多选模式) |
multiple | boolean | false | 是否为多选模式 |
variant | "default" | "compact" | "default" | 按钮变体样式 |
ariaLabel | string | - | 按钮组的 aria-label(无障碍) |
className | string | - | 按钮自定义类名 |
groupClassName | string | - | 按钮容器自定义类名 |
renderOption | (option, context) => ReactNode | - | 自定义选项渲染,复杂场景使用 |
ToggleOption Type
interface ToggleOption {
id: string; // 选项唯一标识
label: string; // 选项显示文本(兼作 aria-label)
icon?: React.ReactNode; // 图标(设置后仅显示图标,不显示 label)
tooltip?: React.ReactNode; // 悬停提示(由组件内部正确包裹 Tooltip)
disabled?: boolean; // 是否禁用
className?: string; // 单个选项的类名
}Example
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
function FeedbackSelector() {
const [feedback, setFeedback] = useState<string | undefined>();
return (
<div>
{/* 单选模式 */}
<ToggleButton
options={[
{ id: "harmful", label: "有害/不安全" },
{ id: "false", label: "信息虚假" },
{ id: "inappropriate", label: "内容不当" },
]}
value={feedback}
onChange={setFeedback}
variant="default"
/>
</div>
);
}
function ModeSelector() {
const [modes, setModes] = useState<string[]>([]);
return (
<div>
{/* 多选模式 */}
<ToggleButton
options={[
{ id: "web-search", label: "联网搜索" },
{ id: "deep-think", label: "深度思考" },
]}
values={modes}
onValuesChange={setModes}
multiple
variant="compact"
/>
</div>
);
}变体样式
Default Variant
- 高度:32px (
h-8) - 内边距:使用 CSS 变量
var(--Padding-padding-com-md) - 未选中状态:中性边框和背景
- 选中状态:品牌色边框和文字
- 适用场景:反馈组件、问卷选择等
Compact Variant
- 高度:使用 CSS 变量
var(--size-com-md) - 内边距:
px-3 - 未选中状态:透明背景,中性边框
- 选中状态:品牌色浅背景和边框(单选)或透明背景(多选)
- 适用场景:Sender 组件、工具栏等
使用场景
- 反馈选择:用户反馈选项,如"有害/不安全"、"信息虚假"等
- 模式切换:启用/禁用某些功能模式,如"联网搜索"、"深度思考"
- 兴趣选择:多选用户兴趣标签
- 筛选器:内容分类筛选,可多选
- 偏好设置:用户偏好选项的开关
- 表单选择:问卷调查、表单中的选择题
最佳实践
- 选择合适的变体:反馈场景使用 default,工具栏场景使用 compact
- 选择合适的模式:互斥选择用单选,非互斥选择用多选
- 选项数量控制:单行建议不超过 5 个选项,过多时考虑换行或其他组件
- 文本简洁:选项文本应简短明了,通常 2-6 个字
- 状态反馈:在单选模式下,可以通过再次点击取消选择
- 禁用状态:合理使用禁用状态,并提供必要的提示信息
注意事项
- 单选模式使用
value/onChange,多选模式使用values/onValuesChange,不能混用 - 不传
value/values时可使用defaultValue/defaultValues实现非受控模式 - 单选模式下点击已选中的选项会取消选择(value 变为 undefined)
- 多选模式下,选中状态的背景色根据
multiple属性自动调整 - 禁用的选项无法被选中或取消选中
- 建议在使用多选模式时,至少保留一个选项可选(如果需要的话)
原语组件
Toggle Button 基于以下原语组件构建:
ToggleButtonPrimitive- 单个开关按钮原语ToggleButtonGroupPrimitive- 开关按钮组容器原语
原语组件提供了基础的样式和结构,可以在 registry/wuhan/blocks/toggle-button/toggle-button-01.tsx 中找到。
样式定制
组件使用 Tailwind CSS,可以通过以下方式定制:
// 通过 className 定制按钮样式
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
className="font-bold"
/>
// 通过 groupClassName 定制容器样式
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
groupClassName="gap-4"
/>扩展示例
Feedback Scene
完整的反馈场景示例,包含提交逻辑。
这个回答有什么问题?
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 反馈场景示例
*/
export function ToggleButtonFeedback() {
const [feedback, setFeedback] = useState<string | undefined>();
const [submitted, setSubmitted] = useState(false);
const feedbackOptions = [
{ id: "harmful", label: "有害/不安全" },
{ id: "false", label: "信息虚假" },
{ id: "inappropriate", label: "内容不当" },
{ id: "unhelpful", label: "无用信息" },
{ id: "other", label: "其他问题" },
];
const handleSubmit = () => {
if (feedback) {
setSubmitted(true);
setTimeout(() => {
setSubmitted(false);
setFeedback(undefined);
}, 2000);
}
};
return (
<div className="space-y-4 p-4 border rounded-lg">
<div className="space-y-2">
<div className="text-sm font-medium">这个回答有什么问题?</div>
<ToggleButton
options={feedbackOptions}
value={feedback}
onChange={setFeedback}
variant="default"
/>
</div>
<button
onClick={handleSubmit}
disabled={!feedback || submitted}
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{submitted ? "已提交" : "提交反馈"}
</button>
</div>
);
}
Sender Mode Switching
Sender 组件中的模式切换示例。
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
import { Search, Brain } from "lucide-react";
/**
* Sender 模式切换示例
*/
export function ToggleButtonSender() {
const [modes, setModes] = useState<string[]>([]);
const [input, setInput] = useState("");
const modeOptions = [
{ id: "web-search", label: "联网搜索" },
{ id: "deep-think", label: "深度思考" },
];
const handleSend = () => {
console.log("发送消息:", input);
console.log("已启用模式:", modes);
setInput("");
};
return (
<div className="space-y-4 p-4 border rounded-lg max-w-2xl">
<div className="space-y-2">
<textarea
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="输入你的问题..."
className="w-full min-h-[100px] p-3 border rounded-md resize-none focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<div className="flex items-center justify-between">
<ToggleButton
options={modeOptions}
values={modes}
onValuesChange={setModes}
multiple
variant="compact"
/>
<button
onClick={handleSend}
disabled={!input.trim()}
className="px-4 py-2 text-sm bg-primary text-primary-foreground rounded-md hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
发送
</button>
</div>
{modes.length > 0 && (
<div className="text-xs text-muted-foreground flex items-center gap-2">
{modes.includes("web-search") && (
<span className="flex items-center gap-1">
<Search className="w-3 h-3" />
联网搜索已启用
</span>
)}
{modes.includes("deep-think") && (
<span className="flex items-center gap-1">
<Brain className="w-3 h-3" />
深度思考已启用
</span>
)}
</div>
)}
</div>
);
}
Content Filter
内容筛选器示例,支持"全部"选项的特殊逻辑。
选择类别
显示所有类别的内容
"use client";
import { useState } from "react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 筛选器示例
*/
export function ToggleButtonFilter() {
const [categories, setCategories] = useState<string[]>(["all"]);
const categoryOptions = [
{ id: "all", label: "全部" },
{ id: "tech", label: "科技" },
{ id: "design", label: "设计" },
{ id: "business", label: "商业" },
{ id: "lifestyle", label: "生活" },
];
// 处理全部选项的特殊逻辑
const handleCategoryChange = (newValues: string[]) => {
// 如果选择了 "全部",则清空其他选项
if (newValues.includes("all") && !categories.includes("all")) {
setCategories(["all"]);
}
// 如果有其他选项被选中,则移除 "全部"
else if (newValues.length > 1 && newValues.includes("all")) {
setCategories(newValues.filter((id) => id !== "all"));
}
// 如果取消所有选项,默认选中 "全部"
else if (newValues.length === 0) {
setCategories(["all"]);
} else {
setCategories(newValues);
}
};
return (
<div className="space-y-4">
<div className="space-y-2">
<div className="text-sm font-medium">选择类别</div>
<ToggleButton
options={categoryOptions}
values={categories}
onValuesChange={handleCategoryChange}
multiple
variant="default"
/>
</div>
<div className="p-4 border rounded-lg bg-muted/50">
<div className="text-sm text-muted-foreground">
{categories.includes("all") ? (
<span>显示所有类别的内容</span>
) : (
<span>
显示以下类别的内容:
{categories
.map(
(id) => categoryOptions.find((opt) => opt.id === id)?.label,
)
.join("、")}
</span>
)}
</div>
</div>
</div>
);
}
仅图标的开关按钮
使用 option.tooltip 由组件内部正确包裹 Tooltip(Tooltip > button),适用于工具栏等紧凑场景。
"use client";
import { useState } from "react";
import { Globe } from "lucide-react";
import { ToggleButton } from "@/components/composed/toggle-button/toggle-button";
/**
* 仅图标的开关按钮示例
* 使用 option.tooltip 由组件内部正确包裹 Tooltip(Tooltip > button)
*/
export function ToggleButtonIcon() {
const [webSearchEnabled, setWebSearchEnabled] = useState<
string | undefined
>();
return (
<ToggleButton
options={[
{
id: "web-search",
label: "联网搜索",
icon: <Globe className="size-4" />,
tooltip: "联网搜索",
},
]}
value={webSearchEnabled}
onChange={setWebSearchEnabled}
variant="compact"
className="p-2"
/>
);
}
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
import { Globe } from "lucide-react";
function IconOnlyToggleButton() {
const [webSearchEnabled, setWebSearchEnabled] = useState<string | undefined>();
return (
<ToggleButton
options={[
{
id: "web-search",
label: "联网搜索",
icon: <Globe className="size-4" />,
tooltip: "联网搜索",
},
]}
value={webSearchEnabled}
onChange={setWebSearchEnabled}
variant="compact"
className="p-2"
/>
);
}带图标的开关按钮(图标+文本)
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
import { Globe, Brain } from "lucide-react";
function IconToggleButton() {
const [modes, setModes] = useState<string[]>([]);
return (
<div className="space-y-4">
<ToggleButton
options={[
{ id: "web-search", label: "联网搜索" },
{ id: "deep-think", label: "深度思考" },
]}
values={modes}
onValuesChange={setModes}
multiple
variant="compact"
/>
{/* 显示已启用的模式图标 */}
<div className="flex gap-2">
{modes.includes("web-search") && (
<span className="flex items-center gap-1 text-sm text-primary">
<Globe className="w-4 h-4" />
联网搜索已启用
</span>
)}
{modes.includes("deep-think") && (
<span className="flex items-center gap-1 text-sm text-primary">
<Brain className="w-4 h-4" />
深度思考已启用
</span>
)}
</div>
</div>
);
}动态选项加载
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
function DynamicOptions() {
const [options, setOptions] = useState<ToggleOption[]>([]);
const [selected, setSelected] = useState<string>();
const [loading, setLoading] = useState(true);
useEffect(() => {
// 模拟从 API 加载选项
setTimeout(() => {
setOptions([
{ id: "opt1", label: "选项 1" },
{ id: "opt2", label: "选项 2" },
{ id: "opt3", label: "选项 3" },
]);
setLoading(false);
}, 1000);
}, []);
if (loading) {
return <div>加载中...</div>;
}
return (
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
/>
);
}条件禁用
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
function ConditionalDisable() {
const [selected, setSelected] = useState<string>();
const isPremiumUser = false; // 假设这是从用户状态获取的
const options = [
{ id: "basic", label: "基础功能" },
{ id: "advanced", label: "高级功能", disabled: !isPremiumUser },
{ id: "pro", label: "专业功能", disabled: !isPremiumUser },
];
return (
<div className="space-y-2">
<ToggleButton
options={options}
value={selected}
onChange={setSelected}
/>
{!isPremiumUser && (
<p className="text-sm text-muted-foreground">
部分功能需要升级为高级用户
</p>
)}
</div>
);
}表单集成
import { ToggleButton } from "@/registry/wuhan/composed/toggle-button";
import { useForm } from "react-hook-form";
function FormIntegration() {
const { register, setValue, watch } = useForm({
defaultValues: {
feedback: undefined,
interests: [],
},
});
const feedback = watch("feedback");
const interests = watch("interests");
return (
<form className="space-y-4">
{/* 单选字段 */}
<div>
<label className="text-sm font-medium mb-2 block">反馈类型</label>
<ToggleButton
options={[
{ id: "helpful", label: "有帮助" },
{ id: "unhelpful", label: "没帮助" },
]}
value={feedback}
onChange={(value) => setValue("feedback", value)}
/>
</div>
{/* 多选字段 */}
<div>
<label className="text-sm font-medium mb-2 block">感兴趣的主题</label>
<ToggleButton
options={[
{ id: "tech", label: "科技" },
{ id: "sports", label: "体育" },
{ id: "music", label: "音乐" },
]}
values={interests}
onValuesChange={(values) => setValue("interests", values)}
multiple
/>
</div>
</form>
);
}