关于 dashboard-patterns
面向 React 仪表板开发的 UI 模式技能,涵盖响应式布局、数据组件与实时数据处理,并强调避免常见性能和加载问题。
主要功能
- 01响应式 CSS Grid 布局
- 02统计卡片与组件注册表
- 03TanStack Query 实时数据模式
- 04TanStack Table 数据表
- 05骨架屏加载状态
适用场景
- 01构建管理面板布局
- 02展示实时指标
- 03实现可排序和筛选的数据表
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yonatangross/ orchestkit --skill dashboard-patterns安装后在 Claude Code 中输入/dashboard-patterns确认已加载。
附带文件references/widget-composition.mdscripts/widget-card.tsx
详细介绍
仪表盘模式
用于使用 React 构建管理面板、分析仪表盘和数据驱动界面的 UI 模式。
布局模式
响应式仪表盘网格
function DashboardLayout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-muted/40">
<aside className="fixed inset-y-0 left-0 z-10 w-64 border-r bg-background">
<Sidebar />
</aside>
<main className="pl-64">
<header className="sticky top-0 z-10 border-b bg-background px-6 py-4">
<DashboardHeader />
</header>
<div className="p-6">
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-4">{children}</div>
</div>
</main>
</div>
);
}
function DashboardGrid() {
return (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-4">
<StatCard title="Revenue" value="$45,231" change="+12%" />
<StatCard title="Users" value="2,350" change="+5.2%" />
<StatCard title="Orders" value="1,245" change="+18%" />
<StatCard title="Conversion" value="3.2%" change="-0.4%" />
<div className="col-span-1 sm:col-span-2"><RevenueChart /></div>
<div className="col-span-1 sm:col-span-2"><TrafficChart /></div>
<div className="col-span-full"><RecentOrdersTable /></div>
</div>
);
}
组件挂件
统计卡片挂件
import { TrendingUp, TrendingDown } from 'lucide-react';
interface StatCardProps {
title: string;
value: string | number;
change?: string;
changeType?: 'positive' | 'negative' | 'neutral';
icon?: React.ReactNode;
}
function StatCard({ title, value, change, changeType = 'neutral', icon }: StatCardProps) {
return (
<div className="rounded-xl border bg-card p-6">
<div className="flex items-center justify-between">
<p className="text-sm font-medium text-muted-foreground">{title}</p>
{icon && <div className="text-muted-foreground">{icon}</div>}
</div>
<div className="mt-2 flex items-baseline gap-2">
<p className="text-3xl font-bold">{value}</p>
{change && (
<span className={cn(
'flex items-center text-sm font-medium',
changeType === 'positive' && 'text-green-600',
changeType === 'negative' && 'text-red-600',
)}>
{changeType === 'positive' && <TrendingUp className="h-4 w-4" />}
{changeType === 'negative' && <TrendingDown className="h-4 w-4" />}
{change}
</span>
)}
</div>
</div>
);
}
挂件注册表模式
type WidgetType = 'stat' | 'chart' | 'table' | 'list';
interface WidgetConfig {
id: string;
type: WidgetType;
title: string;
span?: number;
props: Record<string, unknown>;
}
const widgetRegistry: Record<WidgetType, React.ComponentType<any>> = {
stat: StatCard,
chart: ChartCard,
table: DataTable,
list: ListWidget,
};
function DashboardWidget({ config }: { config: WidgetConfig }) {
const Component = widgetRegistry[config.type];
if (!Component) return null;
return (
<div style={{ gridColumn: config.span ? `span ${config.span}` : undefined }}>
<Component title={config.title} {...config.props} />
</div>
);
}
实时数据模式
TanStack Query + SSE
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
function useRealtimeMetrics() {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['metrics'],
queryFn: fetchMetrics,
});
useEffect(() => {
const eventSource = new EventSource('/api/metrics/stream');
eventSource.onmessage = (event) => {
const update = JSON.parse(event.data);
queryClient.setQueryData(['metrics'], (old: Metrics | undefined) => ({
...old,
...update,
}));
};
eventSource.onerror = () => {
eventSource.close();
queryClient.invalidateQueries({ queryKey: ['metrics'] });
};
return () => eventSource.close();
}, [queryClient]);
return { data, isLoading };
}
数据表(TanStack Table)
import {
useReactTable,
getCoreRowModel,
getSortedRowModel,
getFilteredRowModel,
getPaginationRowModel,
flexRender,
type ColumnDef,
type SortingState,
} from '@tanstack/react-table';
const columns: ColumnDef<Order>[] = [
{ accessorKey: 'id', header: 'Order ID' },
{ accessorKey: 'customer', header: 'Customer' },
{ accessorKey: 'amount', header: 'Amount', cell: ({ getValue }) => `$${getValue<number>().toLocaleString()}` },
{ accessorKey: 'status', header: 'Status', cell: ({ getValue }) => <StatusBadge status={getValue()} /> },
];
function OrdersTable({ data }: { data: Order[] }) {
const [sorting, setSorting] = useState<SortingState>([]);
const [globalFilter, setGlobalFilter] = useState('');
const table = useReactTable({
data,
columns,
state: { sorting, globalFilter },
onSortingChange: setSorting,
onGlobalFilterChange: setGlobalFilter,
getCoreRowModel: getCoreRowModel(),
getSortedRowModel: getSortedRowModel(),
getFilteredRowModel: getFilteredRowModel(),
getPaginationRowModel: getPaginationRowModel(),
});
return (
<div>
<input
value={globalFilter}
onChange={(e) => setGlobalFilter(e.target.value)}
placeholder="Search orders..."
className="mb-4 rounded border px-3 py-2"
/>
<table className="w-full">
<thead>
{table.getHeaderGroups().map((headerGroup) => (
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<th key={header.id} onClick={header.column.getToggleSortingHandler()} className="cursor-pointer px-4 py-2 text-left">
{flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() === 'asc' && ' ↑'}
{header.column.getIsSorted() === 'desc' && ' ↓'}
</th>
))}
</tr>
))}
</thead>
<tbody>
{table.getRowModel().rows.map((row) => (
<tr key={row.id} className="border-t">
{row.getVisibleCells().map((cell) => (
<td key={cell.id} className="px-4 py-2">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</td>
))}
</tr>
))}
</tbody>
</table>
<div className="mt-4 flex items-center gap-2">
<button onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>Previous</button>
<span>Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}</span>
<button onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>Next</button>
</div>
</div>
);
}
骨架屏加载
function DashboardSkeleton() {
return (
<div className="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-4">
{[...Array(4)].map((_, i) => (
<div key={i} className="rounded-xl border bg-card p-6">
<div className="h-4 w-24 animate-pulse rounded bg-muted" />
<div className="mt-2 h-8 w-32 animate-pulse rounded bg-muted" />
</div>
))}
<div className="col-span-full rounded-xl border bg-card p-6">
<div className="mt-4 space-y-2">
{[...Array(5)].map((_, i) => (
<div key={i} className="h-12 animate-pulse rounded bg-muted" />
))}
</div>
</div>
</div>
);
}
反模式(禁止)
// NEVER: Fetch data in every widget independently (duplicated queries)
// NEVER: Re-render entire dashboard on single metric change
// NEVER: Hardcoded dashboard layout (not responsive)
// NEVER: Polling without intervals (infinite loop)
// NEVER: Missing loading states (flash of empty state)
// NEVER: Real-time updates without debounce (100 re-renders/sec)
关键决策
| 决策 | 建议 |
|---|---|
| 布局 | 2D 仪表盘布局使用 CSS Grid |
| 实时数据 | 服务端到客户端使用 SSE,双向通信使用 WebSocket |
| 数据表 | 使用 TanStack Table 实现各项功能 |
| 状态 | 使用带有细粒度键的 TanStack Query |
| 加载 | 内容区域使用 骨架屏 |
相关技能
recharts-patterns- 用于仪表盘的图表组件tanstack-query-advanced- 数据获取模式streaming-api-patterns- SSE 和 WebSocket 实现
评论
支持 Markdown