附录 B:Tailwind + shadcn/ui 最佳实践组件示例

B.1 Button 组件(核心中的核心) B.1.1 设计目标 * 支持 :primary / secondary / outline / ghost / destructive / link * 支持 :sm / md / lg / icon * 支持 状态(显示 spinner) * 支持 dis...

B.0 基础工具:cn 函数(必须有)

cn 是 shadcn/ui 的经典组合:clsx + tailwind-merge。
在所有组件里用于合并类名并解决冲突。

// src/lib/cn.ts
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs))
}

后面所有组件都默认从这里引用 cn。


B.1 Button 组件(核心中的核心)

B.1.1 设计目标

  • 支持 variant:primary / secondary / outline / ghost / destructive / link
  • 支持 size:sm / md / lg / icon
  • 支持 loading 状态(显示 spinner)
  • 支持 disabled
  • 支持 asChild(配合 Radix / Next Link)
  • 使用 Tailwind + shadcn 风格

B.1.2 代码示例

// src/components/ui/button.tsx
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/cn"

const buttonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors " +
    "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 " +
    "disabled:pointer-events-none disabled:opacity-50 " +
    "ring-offset-background",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        secondary:
          "bg-secondary text-secondary-foreground hover:bg-secondary/80",
        outline:
          "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        destructive:
          "bg-destructive text-destructive-foreground hover:bg-destructive/90",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-10 px-4 py-2",
        sm: "h-9 px-3 rounded-md",
        lg: "h-11 px-8 rounded-md",
        icon: "h-9 w-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  asChild?: boolean
  loading?: boolean
}

export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, asChild = false, loading, children, ...props }, ref) => {
    const Comp = asChild ? Slot : "button"

    return (
      <Comp
        className={cn(
          buttonVariants({ variant, size, className }),
          loading && "relative text-transparent cursor-wait"
        )}
        ref={ref}
        disabled={props.disabled || loading}
        {...props}
      >
        {loading && (
          <span className="absolute inset-0 flex items-center justify-center">
            <span className="h-4 w-4 animate-spin rounded-full border-2 border-current border-r-transparent" />
          </span>
        )}
        <span className={cn(loading && "invisible")}>{children}</span>
      </Comp>
    )
  }
)

Button.displayName = "Button"

用法示例:

<Button>默认按钮</Button>
<Button variant="outline">Outline</Button>
<Button variant="destructive" size="sm">删除</Button>
<Button loading>保存中...</Button>

B.2 Input / Textarea 表单组件

B.2.1 Input 组件

// src/components/ui/input.tsx
import * as React from "react"
import { cn } from "@/lib/cn"

export interface InputProps
  extends React.InputHTMLAttributes<HTMLInputElement> {}

export const Input = React.forwardRef<HTMLInputElement, InputProps>(
  ({ className, type, ...props }, ref) => {
    return (
      <input
        type={type}
        className={cn(
          "flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm " +
            "ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium " +
            "placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 " +
            "focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)

Input.displayName = "Input"

B.2.2 Textarea 组件

// src/components/ui/textarea.tsx
import * as React from "react"
import { cn } from "@/lib/cn"

export interface TextareaProps
  extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}

export const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  ({ className, ...props }, ref) => {
    return (
      <textarea
        className={cn(
          "flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm " +
            "ring-offset-background placeholder:text-muted-foreground " +
            "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring " +
            "focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
          className
        )}
        ref={ref}
        {...props}
      />
    )
  }
)

Textarea.displayName = "Textarea"

B.3 表单字段组合:FormField / Label / Description / Error

一个常见模式:label + input + hint + error,统一间距和样式,避免每个页面都手写。

// src/components/ui/form-field.tsx
import { cn } from "@/lib/cn"

interface FormFieldProps {
  label?: string
  description?: string
  error?: string
  requiredMark?: boolean
  className?: string
  children: React.ReactNode
}

export function FormField({
  label,
  description,
  error,
  requiredMark,
  className,
  children,
}: FormFieldProps) {
  return (
    <div className={cn("space-y-1", className)}>
      {label && (
        <label className="flex items-center text-sm font-medium text-foreground">
          <span>{label}</span>
          {requiredMark && <span className="ml-1 text-destructive">*</span>}
        </label>
      )}

      {children}

      {description && !error && (
        <p className="text-xs text-muted-foreground">{description}</p>
      )}

      {error && (
        <p className="text-xs text-destructive flex items-center gap-1">
          <span className="h-1.5 w-1.5 rounded-full bg-destructive" />
          <span>{error}</span>
        </p>
      )}
    </div>
  )
}

使用示例:

<FormField label="邮箱" description="用于登录和通知">
  <Input type="email" placeholder="you@example.com" />
</FormField>

<FormField label="密码" error="密码长度至少 8 位" requiredMark>
  <Input type="password" />
</FormField>

B.4 Card(卡片组件)

用于后台 / 控制台的基础模块容器。

// src/components/ui/card.tsx
import * as React from "react"
import { cn } from "@/lib/cn"

export function Card({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn(
        "rounded-xl border bg-card text-card-foreground shadow-sm",
        className
      )}
      {...props}
    />
  )
}

export function CardHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("flex flex-col space-y-1.5 p-6 border-b", className)}
      {...props}
    />
  )
}

export function CardTitle({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <h3
      className={cn("text-lg font-semibold leading-none tracking-tight", className)}
      {...props}
    />
  )
}

export function CardDescription({
  className,
  ...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
  return (
    <p
      className={cn("text-sm text-muted-foreground", className)}
      {...props}
    />
  )
}

export function CardContent({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div className={cn("p-6", className)} {...props} />
  )
}

export function CardFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("flex items-center justify-end gap-2 p-6 border-t", className)}
      {...props}
    />
  )
}

使用示例:

<Card>
  <CardHeader>
    <CardTitle>本月收入</CardTitle>
    <CardDescription>按自然月统计,不含退款</CardDescription>
  </CardHeader>
  <CardContent>
    <div className="text-3xl font-semibold">¥ 128,900</div>
  </CardContent>
  <CardFooter>
    <Button variant="outline" size="sm">导出报表</Button>
    <Button size="sm">查看详情</Button>
  </CardFooter>
</Card>

B.5 Badge(小徽章)

统一用在标签、状态标识、计数等。

// src/components/ui/badge.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/cn"

const badgeVariants = cva(
  "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold " +
    "transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
  {
    variants: {
      variant: {
        default:
          "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
        secondary:
          "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
        outline: "text-foreground",
        success:
          "border-transparent bg-emerald-500/10 text-emerald-600 dark:text-emerald-300",
        warning:
          "border-transparent bg-amber-500/10 text-amber-600 dark:text-amber-300",
        destructive:
          "border-transparent bg-destructive/10 text-destructive",
      },
    },
    defaultVariants: {
      variant: "default",
    },
  }
)

export interface BadgeProps
  extends React.HTMLAttributes<HTMLDivElement>,
    VariantProps<typeof badgeVariants> {}

export function Badge({ className, variant, ...props }: BadgeProps) {
  return (
    <div className={cn(badgeVariants({ variant }), className)} {...props} />
  )
}

示例:

<Badge>默认</Badge>
<Badge variant="success">已完成</Badge>
<Badge variant="warning">待审核</Badge>
<Badge variant="destructive">已关闭</Badge>

B.6 表格封装:DataTable(简化版)

只给你一个简化但实用的版本,适合后台常见列表:

  • 支持 striped rows
  • 支持 hover
  • 支持空态
  • Tailwind 样式可进一步抽象
// src/components/ui/simple-table.tsx
import { cn } from "@/lib/cn"

export interface Column<T> {
  key: keyof T | string
  header: React.ReactNode
  render?: (row: T) => React.ReactNode
  className?: string
}

interface SimpleTableProps<T> {
  columns: Column<T>[]
  data: T[]
  emptyText?: string
  className?: string
}

export function SimpleTable<T>({
  columns,
  data,
  emptyText = "暂无数据",
  className,
}: SimpleTableProps<T>) {
  return (
    <div className={cn("overflow-hidden rounded-xl border bg-card", className)}>
      <table className="min-w-full divide-y divide-border">
        <thead className="bg-muted/50">
          <tr>
            {columns.map((col, i) => (
              <th
                key={String(col.key) + i}
                className={cn(
                  "px-4 py-3 text-left text-xs font-medium text-muted-foreground",
                  col.className
                )}
              >
                {col.header}
              </th>
            ))}
          </tr>
        </thead>
        <tbody className="divide-y divide-border bg-card">
          {data.length === 0 && (
            <tr>
              <td
                colSpan={columns.length}
                className="px-4 py-6 text-center text-sm text-muted-foreground"
              >
                {emptyText}
              </td>
            </tr>
          )}

          {data.map((row, rowIndex) => (
            <tr
              key={rowIndex}
              className="hover:bg-muted/40 transition-colors"
            >
              {columns.map((col, colIndex) => (
                <td
                  key={String(col.key) + colIndex}
                  className={cn(
                    "px-4 py-3 text-sm text-foreground align-middle",
                    col.className
                  )}
                >
                  {col.render
                    ? col.render(row)
                    : (row as any)[col.key as keyof T]}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  )
}

使用示例:

type User = {
  id: string
  name: string
  email: string
  status: "active" | "pending" | "suspended"
}

const columns: Column<User>[] = [
  { key: "name", header: "姓名" },
  { key: "email", header: "邮箱" },
  {
    key: "status",
    header: "状态",
    render: (row) => (
      <Badge
        variant={
          row.status === "active"
            ? "success"
            : row.status === "pending"
            ? "warning"
            : "destructive"
        }
      >
        {row.status === "active"
          ? "正常"
          : row.status === "pending"
          ? "待激活"
          : "已停用"}
      </Badge>
    ),
  },
]

<SimpleTable columns={columns} data={userList} />

B.7 Dialog / Modal(结合 Radix + Tailwind)

标准做法是用 Radix UI 的 Dialog,配合 Tailwind 封装。

// src/components/ui/dialog.tsx
import * as React from "react"
import * as RadixDialog from "@radix-ui/react-dialog"
import { X } from "lucide-react"
import { cn } from "@/lib/cn"

export const Dialog = RadixDialog.Root
export const DialogTrigger = RadixDialog.Trigger
export const DialogClose = RadixDialog.Close

export function DialogContent({
  className,
  children,
  ...props
}: React.ComponentPropsWithoutRef<typeof RadixDialog.Content>) {
  return (
    <RadixDialog.Portal>
      <RadixDialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm" />
      <RadixDialog.Content
        className={cn(
          "fixed left-1/2 top-1/2 z-50 w-full max-w-lg -translate-x-1/2 -translate-y-1/2 " +
            "rounded-xl border bg-background p-6 shadow-lg outline-none focus-visible:ring-2 focus-visible:ring-ring",
          className
        )}
        {...props}
      >
        {children}
        <RadixDialog.Close className="absolute right-3 top-3 rounded-full p-1 text-muted-foreground hover:bg-muted">
          <X className="h-4 w-4" />
        </RadixDialog.Close>
      </RadixDialog.Content>
    </RadixDialog.Portal>
  )
}

export function DialogHeader({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div className={cn("space-y-1.5 mb-4", className)} {...props} />
  )
}

export function DialogTitle({
  className,
  ...props
}: React.HTMLAttributes<HTMLHeadingElement>) {
  return (
    <h2
      className={cn("text-lg font-semibold leading-none tracking-tight", className)}
      {...props}
    />
  )
}

export function DialogDescription({
  className,
  ...props
}: React.HTMLAttributes<HTMLParagraphElement>) {
  return (
    <p className={cn("text-sm text-muted-foreground", className)} {...props} />
  )
}

export function DialogFooter({
  className,
  ...props
}: React.HTMLAttributes<HTMLDivElement>) {
  return (
    <div
      className={cn("mt-6 flex items-center justify-end gap-2", className)}
      {...props}
    />
  )
}

使用示例:

<Dialog>
  <DialogTrigger asChild>
    <Button>新建项目</Button>
  </DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>新建项目</DialogTitle>
      <DialogDescription>项目用于组织你的应用、数据和成员。</DialogDescription>
    </DialogHeader>
    <div className="space-y-3">
      <FormField label="项目名称" requiredMark>
        <Input placeholder="例如:MiniPlay Studio" />
      </FormField>
      <FormField label="描述">
        <Textarea rows={3} />
      </FormField>
    </div>
    <DialogFooter>
      <DialogClose asChild>
        <Button variant="outline">取消</Button>
      </DialogClose>
      <Button>创建</Button>
    </DialogFooter>
  </DialogContent>
</Dialog>

B.8 Navbar / 顶部导航(SaaS 通用)

简化版:左边 Logo,中间导航,右边用户菜单。

// src/components/layout/top-nav.tsx
import Link from "next/link"
import { cn } from "@/lib/cn"
import { Button } from "@/components/ui/button"

interface NavItem {
  label: string
  href: string
  active?: boolean
}

interface TopNavProps {
  items: NavItem[]
  className?: string
}

export function TopNav({ items, className }: TopNavProps) {
  return (
    <header className={cn("border-b bg-background", className)}>
      <div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
        <div className="flex items-center gap-2">
          <div className="h-7 w-7 rounded-lg bg-gradient-to-br from-sky-500 to-indigo-600" />
          <span className="text-sm font-semibold tracking-tight">
            Birdor Studio
          </span>
        </div>

        <nav className="hidden items-center gap-4 text-sm font-medium md:flex">
          {items.map((item) => (
            <Link
              key={item.href}
              href={item.href}
              className={cn(
                "text-muted-foreground hover:text-foreground",
                item.active && "text-foreground"
              )}
            >
              {item.label}
            </Link>
          ))}
        </nav>

        <div className="flex items-center gap-2">
          <Button variant="ghost" size="sm" className="hidden md:inline-flex">
            文档
          </Button>
          <Button size="sm">控制台</Button>
        </div>
      </div>
    </header>
  )
}

使用:

<TopNav
  items={[
    { label: "概览", href: "/", active: true },
    { label: "项目", href: "/projects" },
    { label: "账单", href: "/billing" },
  ]}
/>

B.8.5 Select 下拉选择组件

Select 是表单中最复杂的交互组件之一,需要处理展开/收起、选项列表、键盘导航和选中状态。

// src/components/ui/select.tsx
import * as React from "react"
import { ChevronDown, Check } from "lucide-react"
import { cn } from "@/lib/cn"

interface SelectOption {
  value: string
  label: string
  disabled?: boolean
}

interface SelectProps {
  options: SelectOption[]
  value?: string
  onChange?: (value: string) => void
  placeholder?: string
  disabled?: boolean
  className?: string
}

export function Select({
  options,
  value,
  onChange,
  placeholder = "请选择...",
  disabled,
  className,
}: SelectProps) {
  const [open, setOpen] = React.useState(false)
  const [selected, setSelected] = React.useState(value)

  const handleSelect = (val: string) => {
    setSelected(val)
    onChange?.(val)
    setOpen(false)
  }

  const selectedLabel = options.find((o) => o.value === selected)?.label

  return (
    <div className={cn("relative", className)}>
      <button
        type="button"
        onClick={() => !disabled && setOpen(!open)}
        disabled={disabled}
        className={cn(
          "flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm",
          "focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
          "disabled:cursor-not-allowed disabled:opacity-50",
          open && "ring-2 ring-ring ring-offset-2"
        )}
      >
        <span className={cn(!selectedLabel && "text-muted-foreground")}>
          {selectedLabel || placeholder}
        </span>
        <ChevronDown className={cn("h-4 w-4 shrink-0 transition-transform", open && "rotate-180")} />
      </button>

      {open && (
        <div className="absolute z-50 mt-1 w-full rounded-md border bg-background shadow-lg">
          <ul className="max-h-60 overflow-auto py-1">
            {options.map((option) => (
              <li
                key={option.value}
                onClick={() => !option.disabled && handleSelect(option.value)}
                className={cn(
                  "flex cursor-pointer items-center justify-between px-3 py-2 text-sm",
                  "hover:bg-accent hover:text-accent-foreground",
                  option.disabled && "cursor-not-allowed opacity-50",
                  selected === option.value && "bg-accent text-accent-foreground"
                )}
              >
                <span>{option.label}</span>
                {selected === option.value && <Check className="h-4 w-4" />}
              </li>
            ))}
          </ul>
        </div>
      )}
    </div>
  )
}

用法示例:

const options = [
  { value: "active", label: "正常" },
  { value: "pending", label: "待审核" },
  { value: "suspended", label: "已停用" },
]

<Select
  options={options}
  value={status}
  onChange={setStatus}
  placeholder="选择状态"
/>

B.8.6 Tooltip 与 Popover

悬浮提示和下拉菜单是后台系统的必备交互元素。

// Tooltip 基础封装
import * as React from "react"
import * as RadixTooltip from "@radix-ui/react-tooltip"
import { cn } from "@/lib/cn"

export function TooltipProvider({ children }: { children: React.ReactNode }) {
  return <RadixTooltip.Provider delayDuration={200}>{children}</RadixTooltip.Provider>
}

export function Tooltip({
  children,
  content,
  side = "top",
}: {
  children: React.ReactNode
  content: string
  side?: "top" | "bottom" | "left" | "right"
}) {
  return (
    <RadixTooltip.Root>
      <RadixTooltip.Trigger asChild>{children}</RadixTooltip.Trigger>
      <RadixTooltip.Portal>
        <RadixTooltip.Content
          side={side}
          className={cn(
            "z-50 rounded-md bg-foreground px-3 py-1.5 text-xs text-background shadow-md",
            "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out"
          )}
        >
          {content}
          <RadixTooltip.Arrow className="fill-foreground" />
        </RadixTooltip.Content>
      </RadixTooltip.Portal>
    </RadixTooltip.Root>
  )
}

B.9 PageShell:SaaS 控制台页面骨架

一个典型的布局:

  • 顶部导航
  • 左侧菜单
  • 右侧主内容
  • 响应式收缩

这里只给主要结构及 Tailwind 写法,便于你跟掌机业务或 Birdor 工具后台直接对接。

// src/components/layout/app-shell.tsx
import { ReactNode } from "react"
import { cn } from "@/lib/cn"

interface AppShellProps {
  sidebar: ReactNode
  header?: ReactNode
  children: ReactNode
}

export function AppShell({ sidebar, header, children }: AppShellProps) {
  return (
    <div className="flex h-screen bg-background">
      {/* Sidebar */}
      <aside className="hidden w-60 border-r bg-muted/40 md:flex md:flex-col">
        {sidebar}
      </aside>

      {/* Main */}
      <div className="flex min-w-0 flex-1 flex-col">
        {header && (
          <header className="border-b bg-background">
            <div className="mx-auto flex h-14 max-w-6xl items-center px-4">
              {header}
            </div>
          </header>
        )}
        <main className="flex-1 overflow-y-auto">
          <div className="mx-auto max-w-6xl px-4 py-6">{children}</div>
        </main>
      </div>
    </div>
  )
}

用法示例:

<AppShell
  sidebar={<YourSidebar />}
  header={<div className="text-sm font-medium">项目概览</div>}
>
  {/* 页面主体内容 */}
  <DashboardContent />
</AppShell>

B.10 小结:如何把这些组件变成”你自己的 shadcn/ui”

你可以按下面思路整理成「企业内部 UI 库」:

Design Token 设计体系

一套成熟的 UI 库不只是组件堆砌,更需要底层的 Design Token 支撑:

Token 类型示例说明
Colors--primary-500, --destructive品牌色、功能色、语义色
Spacing--space-1 ~ --space-164px 为基准的间距系统
Typography--font-sans, --text-sm字体族 + 字号层级
Shadows--shadow-sm, --shadow-lg层级感
Border Radius--radius-md, --radius-lg品牌调性
/* tailwind.config.ts 中的 theme 扩展 */
theme: {
  extend: {
    colors: {
      primary: {
        DEFAULT: 'hsl(var(--primary))',
        foreground: 'hsl(var(--primary-foreground))',
      },
      // 与你的品牌色对齐
    },
    borderRadius: {
      lg: 'var(--radius)',
      md: 'calc(var(--radius) - 2px)',
    },
  },
}

Monorepo 目录结构

packages/ui/
├── src/
│   ├── components/
│   │   ├── ui/           # 基础组件
│   │   │   ├── button.tsx
│   │   │   ├── input.tsx
│   │   │   ├── card.tsx
│   │   │   └── ...
│   │   └── layout/       # 布局组件
│   │       ├── app-shell.tsx
│   │       └── top-nav.tsx
│   ├── lib/
│   │   └── utils.ts      # cn、variants 等工具
│   └── styles/
│       └── globals.css   # Tailwind + CSS Variables
├── package.json
├── tailwind.config.ts
└── tsconfig.json

barrel 导出

// packages/ui/src/components/ui/index.ts
export { Button, buttonVariants } from './button'
export { Input } from './input'
export { Card, CardHeader, CardTitle, CardContent, CardFooter } from './card'
export { Badge, badgeVariants } from './badge'
export { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, DialogClose } from './dialog'
export { FormField } from './form-field'
export { SimpleTable, type Column } from './simple-table'

发布与使用

# 构建 UI 包
pnpm build

# 发布到私有 registry
pnpm publish --registry https://npm.your-company.com

# 项目中使用
import { Button, Card, Input } from '@birdor/ui'

B.11 组件库的工程化实践

测试策略

组件库的可靠性需要通过测试来保证:

测试类型工具覆盖率目标
单元测试Vitest / Jest核心逻辑 80%+
视觉回归Chromatic / Storybook关键组件
可访问性axe-coreWCAG 2.1 AA
交互测试Testing Library用户操作流程

文档站点

使用 Storybook 作为组件文档站点:

npx storybook@latest init

每个组件编写 Stories 展示不同变体:

// Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from './button'

const meta: Meta<typeof Button> = {
  component: Button,
  tags: ['autodocs'],
}

export default meta
type Story = StoryObj<typeof Button>

export const Primary: Story = { args: { variant: 'default', children: '主按钮' } }
export const Outline: Story = { args: { variant: 'outline', children: '边框按钮' } }
export const Loading: Story = { args: { loading: true, children: '加载中' } }
export const Disabled: Story = { args: { disabled: true, children: '禁用' } }

版本发布与变更日志

# 使用 Changesets 管理版本
npx changeset
# 选择受影响包和变更类型 (major/minor/patch)
# 填写变更描述

# 版本提升和 changelog 生成
npx changeset version

# 发布
npx changeset publish

总结

Tailwind CSS 与 shadcn/ui 的组合代表了现代前端组件库的新范式:原子化 CSS 提供了极致的开发体验和运行时性能,Radix UI 的无头组件保证了可访问性和行为正确性,而开发者只需要专注于视觉层和主题的定制。从几个基础组件开始(Button、Input、Dialog),逐步扩展到自己的设计体系,是中小型团队构建一致 UI 体验的最佳路径。这套方案的核心优势在于零运行时开销、完全可控的样式和良好的 TypeScript 支持。


如果你愿意,我可以继续帮你:

  • 把这些组件整理成一个完整的 ui 目录结构清单 + barrel 文件
  • 或者直接设计一套**”Birdor Admin UI Kit”**,按你现在的掌机 / Birdor 工具站 / Shunhei 品牌统一风格来定制 Tailwind 主题(颜色、圆角、阴影、排版),再配一套组件库代码骨架。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「frontend」更多文章

  1. GraphQL BFF 与微前端:多前端团队的 Schema 分片与协作模式
  2. 前端 GraphQL 客户端集成:Apollo Client、Relay 与 urql 选型
  3. 前端性能调试实战:从首屏加载到运行时瓶颈