13 KiB
13 KiB
🎨 按钮使用规范指南
📋 目录
按钮样式规范
样式类型
type ButtonVariant =
| "default" // 默认样式
| "destructive" // 危险操作
| "outline" // 边框样式
| "secondary" // 次要操作
| "ghost" // 幽灵按钮
| "link" // 链接样式
使用场景
1. Primary (default)
用途: 页面主要操作,每个页面最多1-2个
// ✅ 正确使用
<Button variant="default">保存</Button>
<Button variant="default">创建家族树</Button>
<Button variant="default">提交</Button>
// ❌ 错误使用 - 不要在一个区域放置多个 primary 按钮
<Button variant="default">保存</Button>
<Button variant="default">提交</Button>
<Button variant="default">确认</Button>
视觉特征:
- 背景色:
bg-primary - 文字色:
text-primary-foreground - 高对比度,最显眼
2. Destructive
用途: 危险操作,如删除、清空
// ✅ 正确使用
<Button variant="destructive" onClick={handleDelete}>
<Trash className="h-4 w-4 mr-2" />
删除成员
</Button>
// ✅ 必须配合确认对话框
const handleDelete = async () => {
const confirmed = await showConfirm('确定要删除吗?')
if (confirmed) {
// 执行删除
}
}
视觉特征:
- 背景色:
bg-destructive(红色) - 文字色:
text-destructive-foreground - 警示作用
3. Outline
用途: 次要操作,常规功能
// ✅ 正确使用
<Button variant="outline">编辑</Button>
<Button variant="outline">导出</Button>
<Button variant="outline">取消</Button>
视觉特征:
- 边框:
border border-input - 背景: 透明
- 文字色:
text-foreground
4. Ghost
用途: 辅助操作,不抢眼的功能
// ✅ 正确使用
<Button variant="ghost">返回</Button>
<Button variant="ghost" size="icon">
<Search className="h-4 w-4" />
</Button>
视觉特征:
- 无边框,无背景
- hover 时显示背景
- 最不显眼
5. Secondary
用途: 次要强调操作
// ✅ 正确使用
<Button variant="secondary">添加配偶</Button>
<Button variant="secondary">筛选</Button>
视觉特征:
- 背景色:
bg-secondary - 介于 primary 和 outline 之间
6. Link
用途: 链接样式的按钮
// ✅ 正确使用
<Button variant="link">了解更多</Button>
<Button variant="link">查看详情</Button>
视觉特征:
- 无背景,无边框
- 文字带下划线
- 类似超链接
按钮尺寸规范
尺寸类型
type ButtonSize =
| "default" // 默认尺寸
| "sm" // 小尺寸
| "lg" // 大尺寸
| "icon" // 图标按钮
使用场景
1. Default
用途: 常规按钮
<Button size="default">保存</Button>
尺寸: h-10 px-4 py-2
2. Small (sm)
用途: 紧凑布局、辅助操作
<Button size="sm">刷新</Button>
<Button size="sm" variant="ghost">查看全部</Button>
尺寸: h-9 px-3
3. Large (lg)
用途: 重要操作、首屏按钮
<Button size="lg">立即开始</Button>
尺寸: h-11 px-8
4. Icon
用途: 仅图标按钮
<Button size="icon" variant="outline">
<Settings className="h-4 w-4" />
</Button>
尺寸: h-10 w-10 (正方形)
图标使用规范
图标位置
1. 左侧图标
用途: 强调操作类型
// ✅ 正确使用
<Button>
<Plus className="h-4 w-4 mr-2" />
新增成员
</Button>
<Button>
<Download className="h-4 w-4 mr-2" />
导出数据
</Button>
2. 右侧图标
用途: 表示方向或展开
// ✅ 正确使用
<Button>
查看全部
<ArrowRight className="h-4 w-4 ml-2" />
</Button>
<Button>
更多选项
<ChevronDown className="h-4 w-4 ml-2" />
</Button>
3. 仅图标
用途: 空间受限或辅助操作
// ✅ 正确使用
<Button size="icon" variant="ghost" title="搜索">
<Search className="h-4 w-4" />
</Button>
// ⚠️ 注意:必须添加 title 属性用于无障碍访问
图标尺寸
| 按钮尺寸 | 图标尺寸 | 类名 |
|---|---|---|
| sm | 3 | h-3 w-3 |
| default | 4 | h-4 w-4 |
| lg | 5 | h-5 w-5 |
图标间距
// 左侧图标
<Icon className="mr-2" />
// 右侧图标
<Icon className="ml-2" />
// 仅图标(无间距)
<Icon />
交互状态规范
状态类型
1. 默认状态
<Button>点击我</Button>
2. Hover 状态
// 自动处理,无需额外代码
// primary: hover:bg-primary/90
// outline: hover:bg-accent
// ghost: hover:bg-accent hover:text-accent-foreground
3. 禁用状态
// ✅ 正确使用
<Button disabled={isLoading || !hasPermission}>
保存
</Button>
// ✅ 配合加载状态
<Button disabled={isLoading}>
{isLoading ? (
<>
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
保存中...
</>
) : (
'保存'
)}
</Button>
视觉特征:
- 透明度:
opacity-50 - 鼠标:
cursor-not-allowed - 禁用点击事件
4. 加载状态
// ✅ 推荐方式
import { Loader2 } from "lucide-react"
<Button disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? '处理中...' : '提交'}
</Button>
5. 激活状态
// ✅ 用于切换按钮
<Button
variant={isActive ? "secondary" : "outline"}
onClick={toggle}
>
{isActive ? '已激活' : '未激活'}
</Button>
权限控制规范
权限级别
type Role = "OWNER" | "EDITOR" | "VIEWER"
实现方式
1. 条件渲染
// ✅ 推荐:完全隐藏按钮
{(role === "OWNER" || role === "EDITOR") && (
<Button onClick={handleEdit}>编辑</Button>
)}
2. 禁用状态
// ⚠️ 可选:显示但禁用
<Button
disabled={role === "VIEWER"}
title={role === "VIEWER" ? "您没有编辑权限" : ""}
>
编辑
</Button>
权限矩阵
| 操作 | VIEWER | EDITOR | OWNER |
|---|---|---|---|
| 查看 | ✅ | ✅ | ✅ |
| 新增 | ❌ | ✅ | ✅ |
| 编辑 | ❌ | ✅ | ✅ |
| 删除 | ❌ | ✅ | ✅ |
| 管理协作者 | ❌ | ❌ | ✅ |
| 删除家族树 | ❌ | ❌ | ✅ |
最佳实践
1. 按钮组合
✅ 正确的按钮组合
// 主次分明
<div className="flex gap-2">
<Button variant="outline" onClick={onCancel}>
取消
</Button>
<Button variant="default" onClick={onSave}>
保存
</Button>
</div>
❌ 错误的按钮组合
// 不要使用多个 primary 按钮
<div className="flex gap-2">
<Button variant="default">保存</Button>
<Button variant="default">提交</Button>
<Button variant="default">确认</Button>
</div>
2. 按钮位置
表单按钮
// ✅ 正确:右对齐,取消在左,确认在右
<div className="flex justify-end gap-2">
<Button variant="outline">取消</Button>
<Button variant="default">保存</Button>
</div>
对话框按钮
// ✅ 正确:底部右对齐
<DialogFooter>
<Button variant="outline" onClick={onClose}>
取消
</Button>
<Button variant="default" onClick={onConfirm}>
确认
</Button>
</DialogFooter>
3. 响应式设计
// ✅ 移动端优化
<Button className="w-full md:w-auto">
保存
</Button>
// ✅ 图标按钮在移动端隐藏文字
<Button>
<Plus className="h-4 w-4 md:mr-2" />
<span className="hidden md:inline">新增</span>
</Button>
4. 无障碍访问
// ✅ 正确:添加 aria 属性
<Button
aria-label="删除成员"
title="删除成员"
onClick={handleDelete}
>
<Trash className="h-4 w-4" />
</Button>
// ✅ 正确:键盘支持
<Button
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
handleClick()
}
}}
>
点击我
</Button>
5. 加载和错误处理
// ✅ 完整的状态处理
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async () => {
setIsLoading(true)
setError(null)
try {
await saveData()
toast.success('保存成功')
} catch (err) {
setError('保存失败')
toast.error('保存失败')
} finally {
setIsLoading(false)
}
}
return (
<>
<Button
disabled={isLoading}
onClick={handleSubmit}
>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? '保存中...' : '保存'}
</Button>
{error && <p className="text-destructive text-sm mt-2">{error}</p>}
</>
)
6. 确认对话框
// ✅ 危险操作必须确认
const handleDelete = async () => {
const confirmed = await showConfirm(
'确定要删除这个成员吗?此操作不可撤销。',
'删除确认',
'destructive'
)
if (!confirmed) return
try {
await deleteMember(id)
toast.success('删除成功')
router.push('/members')
} catch (error) {
toast.error('删除失败')
}
}
<Button variant="destructive" onClick={handleDelete}>
<Trash className="h-4 w-4 mr-2" />
删除
</Button>
🎨 样式定制
自定义按钮
// ✅ 使用 className 扩展样式
<Button
variant="outline"
className="border-2 border-primary hover:bg-primary/10"
>
自定义样式
</Button>
// ✅ 使用 CSS 变量
<Button
style={{
'--button-bg': 'hsl(var(--primary))',
'--button-fg': 'hsl(var(--primary-foreground))'
} as React.CSSProperties}
>
使用变量
</Button>
按钮组
// ✅ 使用 flex 布局
<div className="flex gap-2">
<Button variant="outline">选项1</Button>
<Button variant="outline">选项2</Button>
<Button variant="outline">选项3</Button>
</div>
// ✅ 使用 ButtonGroup(如果有)
<ButtonGroup>
<Button>选项1</Button>
<Button>选项2</Button>
<Button>选项3</Button>
</ButtonGroup>
📝 代码示例
完整示例:成员编辑页面
export default function MemberEditPage() {
const [isLoading, setIsLoading] = useState(false)
const { showConfirm } = useDialog()
const router = useRouter()
const handleSave = async () => {
setIsLoading(true)
try {
await saveMember(data)
toast.success('保存成功')
router.push(`/members/${id}`)
} catch (error) {
toast.error('保存失败')
} finally {
setIsLoading(false)
}
}
const handleDelete = async () => {
const confirmed = await showConfirm(
'确定要删除这个成员吗?',
'删除确认',
'destructive'
)
if (!confirmed) return
try {
await deleteMember(id)
toast.success('删除成功')
router.push('/members')
} catch (error) {
toast.error('删除失败')
}
}
return (
<div>
{/* 顶部操作栏 */}
<div className="flex items-center justify-between mb-6">
<Button
variant="ghost"
onClick={() => router.back()}
>
<ArrowLeft className="h-4 w-4 mr-2" />
返回
</Button>
<div className="flex gap-2">
<Button
variant="outline"
onClick={() => window.print()}
>
<Printer className="h-4 w-4 mr-2" />
打印
</Button>
<Button
variant="destructive"
onClick={handleDelete}
>
<Trash className="h-4 w-4 mr-2" />
删除
</Button>
</div>
</div>
{/* 表单内容 */}
<form onSubmit={(e) => { e.preventDefault(); handleSave(); }}>
{/* ... 表单字段 ... */}
{/* 底部按钮 */}
<div className="flex justify-end gap-2 mt-6">
<Button
type="button"
variant="outline"
onClick={() => router.back()}
disabled={isLoading}
>
取消
</Button>
<Button
type="submit"
disabled={isLoading}
>
{isLoading && (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
)}
{isLoading ? '保存中...' : '保存'}
</Button>
</div>
</form>
</div>
)
}
🔍 检查清单
新增按钮前的检查
- 确定按钮的主要用途
- 选择合适的 variant
- 选择合适的 size
- 添加合适的图标(如需要)
- 考虑权限控制
- 添加加载状态
- 添加错误处理
- 危险操作添加确认
- 添加无障碍属性
- 测试响应式布局