mcpField
D
dashboard-builder

dashboard-builder

github.com/qwenlm设计与 UI143105 次安装23 周前更新
代码仓库

关于 dashboard-builder

dashboard-builder 是用于构建数据可视化仪表盘的技能,面向需要开发大屏数据界面的团队。它提供基于 Next.js、Recharts 等技术的界面规范、组件示例和自动化初始化脚本。

主要功能

  1. 01采用暗色卡片式仪表盘设计
  2. 02提供 KPI、数据卡片与数字动画示例
  3. 03包含滚动、悬停和进度动画规范
  4. 04支持 API、数据库或静态数据
  5. 05脚本创建项目并启动开发服务器

适用场景

  1. 01搭建面向大屏的运营仪表盘
  2. 02用图表展示 KPI 和时间序列数据
  3. 03为仪表盘接入 API 或数据库数据
  4. 04快速初始化 Next.js 仪表盘项目

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/qwenlm/qwen-code-examples --skill dashboard-builder

安装后在 Claude Code 中输入/dashboard-builder确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

ADDITIONAL_COMPONENTS.md
DATA_INTEGRATION.md
package.json
QUICKSTART.md
setup-and-run.sh
references/
big-screen-ui.md
data-api-integration.md
recharts-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>
  )
}

自动化脚本

自动设置和运行

为简化开发环境的设置流程,我们提供了自动化脚本,可用于:

  1. 创建包含所有必需依赖项的新 Next.js 项目
  2. 安装 shadcn/ui 组件
  3. 设置示例仪表板组件
  4. 启动开发服务器

运行设置脚本:

./setup-and-run.sh

脚本会提示你输入项目名称,并自动完成所有设置步骤,然后在 http://localhost:3000 启动开发服务器。

手动设置

如果你更喜欢手动设置,请按以下步骤操作:

  1. 创建新的 Next.js 应用:

    npx create-next-app@latest my-dashboard --typescript --tailwind --eslint
    
  2. 安装必需的依赖项:

    cd my-dashboard
    npm install shadcn-ui recharts framer-motion lucide-react
    
  3. 初始化并添加 shadcn/ui 组件:

    npx shadcn-ui@latest init
    npx shadcn-ui@latest add card button badge table scroll-area alert
    
  4. 启动开发服务器:

    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 }
}

资源

评论

支持 Markdown