关于 dashboard-builder
dashboard-builder 是用于构建数据可视化仪表盘的技能,面向需要开发大屏数据界面的团队。它提供基于 Next.js、Recharts 等技术的界面规范、组件示例和自动化初始化脚本。
主要功能
- 01采用暗色卡片式仪表盘设计
- 02提供 KPI、数据卡片与数字动画示例
- 03包含滚动、悬停和进度动画规范
- 04支持 API、数据库或静态数据
- 05脚本创建项目并启动开发服务器
适用场景
- 01搭建面向大屏的运营仪表盘
- 02用图表展示 KPI 和时间序列数据
- 03为仪表盘接入 API 或数据库数据
- 04快速初始化 Next.js 仪表盘项目
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ qwenlm/ qwen-code-examples --skill dashboard-builder安装后在 Claude Code 中输入/dashboard-builder确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
ADDITIONAL_COMPONENTS.mdDATA_INTEGRATION.mdpackage.jsonQUICKSTART.mdsetup-and-run.shreferences/big-screen-ui.mddata-api-integration.mdrecharts-patterns.md详细介绍
仪表板构建器
使用现代 UI、丰富的可视化效果和动态特效,构建专业的数据可视化仪表板。
技术栈
- 前端:Next.js 14+(App Router)+ React 18+
- UI 组件:shadcn/ui + Tailwind CSS
- 图表:Recharts
- 动画:Framer Motion
- 后端:Express.js
- 数据源:可配置的 API、数据库或静态数据
设计要求
视觉风格(专业 PC 仪表板)
- 深色主题,搭配微妙渐变(不花哨)
- 简洁的卡片设计,搭配柔和阴影和边框
- 专业配色方案:
- 背景:slate-900(#0f172a)到 slate-950(#020617)
- 卡片:slate-800/50,搭配微妙边框
- 成功:emerald-500(#10b981)
- 危险:rose-500(#f43f5e)
- 强调色:blue-500(#3b82f6)
- 大字号排版,针对大屏幕(4K、超宽屏)优化
- 基于网格的布局,具有清晰的视觉层次
动画要求
- 入场动画:页面加载时轻微淡入并向上滑动
- 滚动触发动画:卡片滚动进入视野时播放动画
- 数字递增动画:数值平滑变化
- 进度条动画:进度条以缓动效果填充
- 悬停效果:卡片轻微上浮并显示阴影
- 实时数据脉冲:数据更新时显示微妙的指示效果
项目结构
project-root/
├── app/
│ ├── layout.tsx
│ ├── page.tsx # Landing/input page
│ └── dashboard/
│ └── page.tsx # Results dashboard
├── components/
│ ├── ui/ # shadcn components
│ ├── charts/
│ │ ├── LineChart.tsx
│ │ ├── BarChart.tsx
│ │ ├── AreaChart.tsx
│ │ └── PieChart.tsx
│ ├── dashboard/
│ │ ├── KPICard.tsx
│ │ ├── DataCard.tsx
│ │ ├── StatsPanel.tsx
│ │ └── DataTable.tsx
│ └── effects/
│ └── AnimatedNumber.tsx
├── lib/
│ ├── api.ts
│ ├── utils.ts
│ └── animations.ts
├── server/
│ ├── index.ts
│ ├── routes/
│ │ └── data.ts
│ └── services/
│ └── dataService.ts
└── styles/
└── globals.css
PC 大屏布局
4K 仪表板网格(3840x2160)
export default function DashboardPage() {
return (
<div className="min-h-screen bg-gradient-to-br from-slate-900 via-slate-900 to-slate-950 p-6">
{/* Header */}
<header className="flex items-center justify-between mb-8">
<h1 className="text-4xl font-bold text-white">Dashboard</h1>
<div className="flex items-center gap-4">
<LiveIndicator />
<span className="text-slate-400 text-lg">Last updated: {time}</span>
</div>
</header>
{/* KPI Row */}
<div className="grid grid-cols-4 gap-6 mb-8">
<KPICard title="Metric 1" value={125000} prefix="$" trend={5.2} />
<KPICard title="Metric 2" value={2340} prefix="$" trend={1.8} />
<KPICard title="Metric 3" value={18.5} suffix="%" trend={0} />
<KPICard title="Metric 4" value={12} trend={0} />
</div>
{/* Main Content */}
<div className="grid grid-cols-12 gap-6">
{/* Main Chart - 8 columns */}
<div className="col-span-8">
<Card className="h-[500px]">
<CardHeader>
<CardTitle>Main Metric Trend</CardTitle>
</CardHeader>
<CardContent>
<LineChart data={metricData} />
</CardContent>
</Card>
</div>
{/* Side Panel - 4 columns */}
<div className="col-span-4 space-y-6">
<Card>
<CardHeader><CardTitle>Top Items</CardTitle></CardHeader>
<CardContent>
<DataTable data={topItems} />
</CardContent>
</Card>
<Card>
<CardHeader><CardTitle>Comparison</CardTitle></CardHeader>
<CardContent>
<PieChart data={comparisonData} />
</CardContent>
</Card>
</div>
{/* Timeline Row - Full width */}
<div className="col-span-12">
<h2 className="text-2xl font-semibold text-white mb-4">Trend Analysis</h2>
<div className="grid grid-cols-4 gap-6">
{timelineData.map(item => (
<DataCard key={item.period} {...item} />
))}
</div>
</div>
</div>
</div>
)
}
核心组件
KPI 卡片
import { Card, CardContent } from "@/components/ui/card"
import { TrendingUp, TrendingDown, Minus } from "lucide-react"
import { AnimatedNumber } from "./AnimatedNumber"
interface KPICardProps {
title: string
value: number
prefix?: string
suffix?: string
trend?: number
}
export function KPICard({ title, value, prefix = '', suffix = '', trend = 0 }: KPICardProps) {
return (
<Card className="bg-slate-800/50 border-slate-700/50">
<CardContent className="pt-6">
<p className="text-slate-400 text-sm font-medium mb-2">{title}</p>
<div className="flex items-end justify-between">
<span className="text-3xl font-bold text-white">
{prefix}<AnimatedNumber value={value} />{suffix}
</span>
{trend !== 0 && (
<span className={cn(
"flex items-center text-sm font-medium",
trend > 0 ? "text-emerald-500" : "text-rose-500"
)}>
{trend > 0 ? <TrendingUp className="w-4 h-4 mr-1" /> : <TrendingDown className="w-4 h-4 mr-1" />}
{Math.abs(trend)}%
</span>
)}
</div>
</CardContent>
</Card>
)
}
数据卡片(通用时间线卡片)
'use client'
import { motion } from 'framer-motion'
import { useInView } from 'framer-motion'
import { useRef } from 'react'
import { Card, CardContent } from "@/components/ui/card"
interface DataCardProps {
period: string
currentValue: number
baselineValue: number
percentChange: number
description: string
}
export function DataCard({ period, currentValue, baselineValue, percentChange, description }: DataCardProps) {
const ref = useRef(null)
const isInView = useInView(ref, { once: true, margin: "-50px" })
const isPositive = percentChange >= 0
return (
<motion.div
ref={ref}
initial={{ opacity: 0, y: 30 }}
animate={isInView ? { opacity: 1, y: 0 } : {}}
transition={{ duration: 0.5, ease: "easeOut" }}
>
<Card className="bg-slate-800/50 border-slate-700/50 overflow-hidden">
<div className={cn(
"h-1",
isPositive ? "bg-emerald-500" : "bg-rose-500"
)} />
<CardContent className="pt-6">
<div className="flex items-baseline gap-2 mb-4">
<span className="text-4xl font-bold text-white">{period}</span>
</div>
<div className="space-y-3">
<div className="flex justify-between text-sm">
<span className="text-slate-400">Baseline Value</span>
<span className="text-slate-300">{baselineValue}</span>
</div>
<div className="flex justify-between text-sm">
<span className="text-slate-400">Current Value</span>
<span className="text-white font-medium">{currentValue}</span>
</div>
{/* Change percentage */}
<div className={cn(
"text-2xl font-bold text-center py-3 rounded-lg",
isPositive ? "text-emerald-500 bg-emerald-500/10" : "text-rose-500 bg-rose-500/10"
)}>
{isPositive ? '+' : ''}{percentChange.toFixed(2)}%
</div>
{/* Progress bar */}
<div className="h-1.5 bg-slate-700 rounded-full overflow-hidden">
<motion.div
initial={{ width: 0 }}
animate={isInView ? { width: `${Math.min(Math.abs(percentChange), 100)}%` } : {}}
transition={{ duration: 1, ease: "easeOut", delay: 0.3 }}
className={cn(
"h-full rounded-full",
isPositive ? "bg-emerald-500" : "bg-rose-500"
)}
/>
</div>
<p className="text-slate-400 text-sm">{description}</p>
</div>
</CardContent>
</Card>
</motion.div>
)
}
动画数字
'use client'
import { useEffect, useState } from 'react'
import { animate } from 'framer-motion'
interface AnimatedNumberProps {
value: number
decimals?: number
duration?: number
}
export function AnimatedNumber({ value, decimals = 2, duration = 1 }: AnimatedNumberProps) {
const [display, setDisplay] = useState(0)
useEffect(() => {
const controls = animate(display, value, {
duration,
ease: "easeOut",
onUpdate: (v) => setDisplay(v)
})
return () => controls.stop()
}, [value, duration])
return <>{display.toFixed(decimals)}</>
}
实时指示器
export function LiveIndicator() {
return (
<div className="flex items-center gap-2">
<span className="relative flex h-3 w-3">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75" />
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500" />
</span>
<span className="text-emerald-500 text-sm font-medium">LIVE</span>
</div>
)
}
自动化脚本
自动设置和运行
为简化开发环境的设置流程,我们提供了自动化脚本,可用于:
- 创建包含所有必需依赖项的新 Next.js 项目
- 安装 shadcn/ui 组件
- 设置示例仪表板组件
- 启动开发服务器
运行设置脚本:
./setup-and-run.sh
脚本会提示你输入项目名称,并自动完成所有设置步骤,然后在 http://localhost:3000 启动开发服务器。
手动设置
如果你更喜欢手动设置,请按以下步骤操作:
-
创建新的 Next.js 应用:
npx create-next-app@latest my-dashboard --typescript --tailwind --eslint -
安装必需的依赖项:
cd my-dashboard npm install shadcn-ui recharts framer-motion lucide-react -
初始化并添加 shadcn/ui 组件:
npx shadcn-ui@latest init npx shadcn-ui@latest add card button badge table scroll-area alert -
启动开发服务器:
npm run dev
CSS 样式
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
}
/* Big screen typography */
@media (min-width: 2560px) {
html { font-size: 18px; }
}
@media (min-width: 3840px) {
html { font-size: 20px; }
}
/* Smooth scrolling */
html {
scroll-behavior: smooth;
}
/* Card hover effect */
.card-hover {
@apply transition-all duration-300;
}
.card-hover:hover {
@apply -translate-y-1 shadow-lg shadow-slate-900/50;
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}
自动刷新 Hook
'use client'
import { useEffect, useState, useCallback } from 'react'
export function useLiveData<T>(
fetcher: () => Promise<T>,
interval = 60000
) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [isRefreshing, setIsRefreshing] = useState(false)
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const refresh = useCallback(async () => {
setIsRefreshing(true)
try {
const result = await fetcher()
setData(result)
setLastUpdated(new Date())
} finally {
setLoading(false)
setTimeout(() => setIsRefreshing(false), 300)
}
}, [fetcher])
useEffect(() => {
refresh()
const timer = setInterval(refresh, interval)
return () => clearInterval(timer)
}, [refresh, interval])
return { data, loading, isRefreshing, lastUpdated, refresh }
}
资源
- references/data-api-integration.md - API 端点、数据处理、错误处理
- references/recharts-patterns.md - 图表模式和自定义
- references/big-screen-ui.md - 大屏布局指南
- QUICKSTART.md - 快速入门模板和初始化指南
- DATA_INTEGRATION.md - 详细的数据集成步骤和示例
- ADDITIONAL_COMPONENTS.md - 面向大屏仪表板的其他组件
评论
支持 Markdown