Files
chinese-family-tree-2/docs/BUTTON_USAGE_GUIDE.md
T
freedakgmail e44dd0bd95 0.0.8.0
2025-11-23 18:03:04 +08:00

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 之间

用途: 链接样式的按钮

// ✅ 正确使用
<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
  • 添加合适的图标(如需要)
  • 考虑权限控制
  • 添加加载状态
  • 添加错误处理
  • 危险操作添加确认
  • 添加无障碍属性
  • 测试响应式布局

📚 相关资源