基础组件
Button, Input, Label, Select
本项目使用基于 Base UI 和 shadcn/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="禁用状态" />特性:
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:
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
columns | ColumnDef<TData, TValue>[] | 必填 | 列定义 |
data | TData[] | 必填 | 数据数组 |
isLoading | boolean | false | 加载状态 |
error | Error | null | null | 错误对象 |
enablePagination | boolean | true | 启用分页 |
enableSorting | boolean | true | 启用排序 |
enableFiltering | boolean | true | 启用筛选 |
enableSearch | boolean | true | 启用搜索 |
searchPlaceholder | string | '搜索所有字段...' | 搜索框占位符 |
pageSize | number | 10 | 每页显示数量 |
pageSizeOptions | number[] | [10, 20, 30, 50, 100] | 每页数量选项 |
emptyMessage | string | '暂无数据' | 空数据提示 |
renderToolbar | (table) => ReactNode | - | 自定义工具栏 |
renderEmpty | () => ReactNode | - | 自定义空状态 |
onRowClick | (row: TData) => void | - | 行点击回调 |
className | string | - | 自定义样式类 |
列定义示例:
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对于大量数据:
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()window、localStorage)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 # 用户菜单