Skip to content

UI 组件库

本项目使用基于 Base UIshadcn/ui 风格的组件库,所有组件位于 apps/web/src/components/ui/ 目录。

基础组件

Button, Input, Label, Select

布局组件

Card, Table, Sheet, Dialog

反馈组件

Sonner (Toast), Skeleton

数据展示

DataTable (自定义)


按钮组件,支持多种变体和尺寸。

位置: @/components/ui/button

使用示例:

import { Button } from '@/components/ui/button'
// 默认按钮
<Button>点击我</Button>
// 不同变体
<Button variant="outline">轮廓按钮</Button>
<Button variant="secondary">次要按钮</Button>
<Button variant="ghost">幽灵按钮</Button>
<Button variant="destructive">危险按钮</Button>
<Button variant="link">链接按钮</Button>
// 不同尺寸
<Button size="sm">小按钮</Button>
<Button size="default">默认</Button>
<Button size="lg">大按钮</Button>
// 禁用状态
<Button disabled>禁用按钮</Button>

变体:

  • default - 默认主按钮
  • outline - 轮廓按钮
  • secondary - 次要按钮
  • ghost - 无背景按钮
  • destructive - 危险操作按钮
  • link - 链接样式按钮

尺寸:

  • sm - 小尺寸
  • default - 默认尺寸
  • lg - 大尺寸

输入框组件。

位置: @/components/ui/input

使用示例:

import { Input } from '@/components/ui/input'
<Input type="text" placeholder="请输入..." />
<Input type="email" placeholder="邮箱" />
<Input type="password" placeholder="密码" />
<Input disabled placeholder="禁用状态" />

特性:

  • 支持所有原生 input 属性
  • 自动适配深色模式
  • 内置验证状态样式(aria-invalid

标签组件,通常与表单控件配合使用。

位置: @/components/ui/label

使用示例:

import { Label } from '@/components/ui/label'
import { Input } from '@/components/ui/input'
<Label htmlFor="email">邮箱</Label>
<Input id="email" type="email" />

下拉选择组件。

位置: @/components/ui/select

使用示例:

import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select'
;<Select value={value} onValueChange={setValue}>
<SelectTrigger>
<SelectValue placeholder="请选择..." />
</SelectTrigger>
<SelectContent>
<SelectItem value="option1">选项 1</SelectItem>
<SelectItem value="option2">选项 2</SelectItem>
<SelectItem value="option3">选项 3</SelectItem>
</SelectContent>
</Select>

注意: onValueChange 的类型是 (value: string | null) => void,需要处理 null 值。


复选框组件。

位置: @/components/ui/checkbox

使用示例:

import { Checkbox } from '@/components/ui/checkbox'
<Checkbox checked={checked} onCheckedChange={setChecked} />
<Checkbox disabled />

徽章/标签组件,用于状态标识。

位置: @/components/ui/badge

使用示例:

import { Badge } from '@/components/ui/badge'
// 不同变体
<Badge>默认</Badge>
<Badge variant="secondary">次要</Badge>
<Badge variant="destructive">危险</Badge>
<Badge variant="outline">轮廓</Badge>
<Badge variant="ghost">幽灵</Badge>
<Badge variant="link">链接</Badge>

变体:

  • default - 主色背景
  • secondary - 次要色背景
  • destructive - 危险/错误状态
  • outline - 边框样式
  • ghost - 透明背景
  • link - 链接样式

典型用法:

// 状态标签
<Badge variant={isActive ? 'default' : 'destructive'}>
{isActive ? '正常' : '已禁用'}
</Badge>
// 类型标签
<Badge variant={isPublic ? 'secondary' : 'default'}>
{isPublic ? '公开' : '机密'}
</Badge>

卡片容器组件,用于组织相关内容。

位置: @/components/ui/card

使用示例:

import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
;<Card>
<CardHeader>
<CardTitle>标题</CardTitle>
<CardDescription>描述信息</CardDescription>
</CardHeader>
<CardContent>{/* 内容 */}</CardContent>
</Card>

基础表格组件。

位置: @/components/ui/table

使用示例:

import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table'
;<Table>
<TableHeader>
<TableRow>
<TableHead>列1</TableHead>
<TableHead>列2</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>数据1</TableCell>
<TableCell>数据2</TableCell>
</TableRow>
</TableBody>
</Table>

注意: 通常配合 DataTable 组件使用,而不是直接使用。


侧边栏抽屉组件。

位置: @/components/ui/sheet

使用示例:

import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger
} from '@/components/ui/sheet'
;<Sheet open={open} onOpenChange={setOpen}>
<SheetTrigger>打开</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>标题</SheetTitle>
<SheetDescription>描述</SheetDescription>
</SheetHeader>
{/* 内容 */}
</SheetContent>
</Sheet>

对话框/模态框组件。

位置: @/components/ui/dialog

使用示例:

import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger
} from '@/components/ui/dialog'
;<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger>打开对话框</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>标题</DialogTitle>
<DialogDescription>描述</DialogDescription>
</DialogHeader>
{/* 内容 */}
<DialogFooter>
<Button>确定</Button>
<Button variant="outline">取消</Button>
</DialogFooter>
</DialogContent>
</Dialog>

下拉菜单组件,用于操作菜单。

位置: @/components/ui/dropdown-menu

使用示例:

import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react'
;<DropdownMenu>
<DropdownMenuTrigger className="inline-flex size-8 items-center justify-center rounded-md hover:bg-accent">
<MoreHorizontal className="size-4" />
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>
<Pencil className="mr-2 size-4" />
编辑
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-destructive" onClick={handleDelete}>
<Trash2 className="mr-2 size-4" />
删除
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>

注意: 本项目使用 @base-ui/react/menu 实现,不支持 asChild 属性。

组件说明:

组件说明
DropdownMenu根容器
DropdownMenuTrigger触发按钮
DropdownMenuContent下拉内容容器
DropdownMenuItem菜单项
DropdownMenuSeparator分隔线

轻量级通知组件。

位置: @/components/ui/sonner

使用示例:

import { toast } from 'sonner'
import { Toaster } from '@/components/ui/sonner'
// 在根布局中添加
;<Toaster richColors />
// 使用
toast.success('操作成功')
toast.error('操作失败')
toast.info('提示信息')
toast.warning('警告信息')

特性:

  • 支持丰富的颜色主题
  • 自动定位
  • 可配置持续时间

骨架屏加载组件。

位置: @/components/ui/skeleton

使用示例:

import { Skeleton } from '@/components/ui/skeleton'
<Skeleton className="h-4 w-full" />
<Skeleton className="h-12 w-12 rounded-full" />

功能完整的数据表格组件,基于 @tanstack/react-table 构建。

位置: @/components/data-table

特性:

  • ✅ 排序
  • ✅ 筛选
  • ✅ 分页
  • ✅ 全局搜索
  • ✅ 字段筛选下拉框
  • ✅ 加载状态
  • ✅ 错误处理
  • ✅ 空状态
  • ✅ 自定义工具栏
  • ✅ 行点击事件

使用示例:

import { DataTable } from '@/components/data-table'
import { type ColumnDef } from '@tanstack/react-table'
type User = {
id: string
name: string
email: string
role: string
}
const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: '姓名'
},
{
accessorKey: 'email',
header: '邮箱'
},
{
accessorKey: 'role',
header: '角色'
}
]
function UsersPage() {
const { data: users, isLoading, error } = useQuery(...)
return (
<DataTable
columns={columns}
data={users ?? []}
isLoading={isLoading}
error={error}
enablePagination
enableSorting
enableFiltering
enableSearch
searchPlaceholder="搜索用户..."
emptyMessage="暂无用户"
pageSize={10}
onRowClick={(user) => {
// 处理行点击
console.log('点击了用户:', user)
}}
/>
)
}

Props:

属性类型默认值说明
columnsColumnDef<TData, TValue>[]必填列定义
dataTData[]必填数据数组
isLoadingbooleanfalse加载状态
errorError | nullnull错误对象
enablePaginationbooleantrue启用分页
enableSortingbooleantrue启用排序
enableFilteringbooleantrue启用筛选
enableSearchbooleantrue启用搜索
searchPlaceholderstring'搜索所有字段...'搜索框占位符
pageSizenumber10每页显示数量
pageSizeOptionsnumber[][10, 20, 30, 50, 100]每页数量选项
emptyMessagestring'暂无数据'空数据提示
renderToolbar(table) => ReactNode-自定义工具栏
renderEmpty() => ReactNode-自定义空状态
onRowClick(row: TData) => void-行点击回调
classNamestring-自定义样式类

列定义示例:

const columns: ColumnDef<User>[] = [
{
accessorKey: 'name',
header: '姓名',
// 自定义单元格渲染
cell: ({ row }) => <strong>{row.original.name}</strong>
},
{
accessorKey: 'email',
header: '邮箱',
// 可排序
enableSorting: true
},
{
accessorKey: 'status',
header: '状态',
cell: ({ row }) => (
<span className={row.original.status === 'active' ? 'text-green-600' : 'text-gray-600'}>
{row.original.status}
</span>
)
}
]

搜索功能:

DataTable 内置了强大的搜索功能:

  • 全局搜索:在所有可搜索列中搜索
  • 字段筛选:通过下拉框选择特定字段进行搜索
  • 自动识别:自动识别有 accessorKey 的列作为可搜索列

自定义工具栏:

<DataTable
columns={columns}
data={data}
renderToolbar={(table) => (
<div className="flex items-center justify-between">
<Button onClick={handleExport}>导出</Button>
<div>{table.getFilteredRowModel().rows.length}</div>
</div>
)}
/>

使用 @/ 别名导入组件:

import { Button } from '@/components/ui/button'
import { DataTable } from '@/components/data-table'

充分利用 TypeScript 类型:

import { type ColumnDef } from '@tanstack/react-table'
const columns: ColumnDef<User>[] = [...]

组件已内置响应式支持,但建议在移动端测试:

<Card className="w-full md:w-1/2 lg:w-1/3">{/* 内容 */}</Card>

组件已内置 ARIA 属性,但建议:

  • 为表单控件添加 Label
  • 使用语义化的 HTML
  • 测试键盘导航

对于大量数据:

  • 使用 DataTable 的分页功能
  • 使用 useMemo 优化列定义
  • 使用 useCallback 优化事件处理

客户端组件使用异步数据时,可能出现服务端/客户端渲染不一致:

// ✅ 推荐:使用 isHydrated 状态
const [isHydrated, setIsHydrated] = useState(false)
useEffect(() => {
setIsHydrated(true)
}, [])
<DataTable
data={isHydrated ? data : []}
isLoading={!isHydrated || isLoading}
/>
// ✅ 推荐:使用 suppressHydrationWarning
<span suppressHydrationWarning>
{isHydrated ? count : 0} 条记录
</span>
// ✅ 推荐:条件渲染敏感内容
{isHydrated && !isSessionPending && !isAdmin && (
<Card>权限不足</Card>
)}

常见 Hydration 错误场景:

  • 基于 session 状态的条件渲染
  • 使用 Date.now()Math.random()
  • 依赖浏览器 API(windowlocalStorage

apps/web/src/components/
├── ui/ # 基础 UI 组件 (Shadcn UI)
│ ├── badge.tsx # 徽章/标签
│ ├── button.tsx # 按钮
│ ├── card.tsx # 卡片
│ ├── checkbox.tsx # 复选框
│ ├── dialog.tsx # 对话框
│ ├── dropdown-menu.tsx # 下拉菜单
│ ├── input.tsx # 输入框
│ ├── label.tsx # 标签
│ ├── select.tsx # 下拉选择
│ ├── sheet.tsx # 侧边抽屉
│ ├── skeleton.tsx # 骨架屏
│ ├── sonner.tsx # Toast 通知
│ └── table.tsx # 表格
├── data-table.tsx # 数据表格组件(推荐使用)
├── loader.tsx # 加载动画
├── loading.tsx # 加载页面
├── mode-toggle.tsx # 主题切换
├── admin/ # 管理后台组件
│ ├── admin-header.tsx # 顶部导航
│ ├── admin-sidebar.tsx # 侧边栏
│ ├── admin-layout-client.tsx
│ └── session-guard.tsx # 会话守卫
├── sign-in-form.tsx # 登录表单
├── sign-up-form.tsx # 注册表单
└── user-menu.tsx # 用户菜单